[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\ninsert_final_newline = true\nend_of_line = lf\nindent_style = space\nindent_size = 2\nmax_line_length = 120"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "*                   @kommander @msmps @Hona @simonklee\n/packages/react/    @msmps @Adictya @fezproof @kommander @Hona @simonklee\n/packages/solid/    @Adictya @fezproof @msmps @kommander @Hona @simonklee\n"
  },
  {
    "path": ".github/workflows/build-core.yml",
    "content": "name: Build Core\n\non:\n  push:\n  pull_request:\n    branches: [main]\n\nenv:\n  ZIG_VERSION: 0.15.2\n  # Workaround for bug in Zig 0.15.2 (fixed in next version)\n  # https://github.com/ziglang/zig/issues/25805\n  ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-global-cache\n\njobs:\n  build:\n    name: Build Native Libraries\n    runs-on: macos-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Setup Zig\n        uses: goto-bus-stop/setup-zig@v2\n        with:\n          version: ${{ env.ZIG_VERSION }}\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Build native libraries (cross-compile all platforms)\n        run: |\n          cd packages/core\n          bun run build:native --all\n\n      - name: Upload native artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: native-libraries\n          path: packages/core/node_modules/@opentui/\n          retention-days: 1\n\n  test:\n    name: Test (${{ matrix.os }})\n    needs: build\n    strategy:\n      matrix:\n        include:\n          - os: macos-latest\n            platform: darwin-arm64\n          - os: ubuntu-latest\n            platform: linux-x64\n          - os: windows-latest\n            platform: win32-x64\n    runs-on: ${{ matrix.os }}\n    defaults:\n      run:\n        shell: bash\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Setup Zig\n        uses: goto-bus-stop/setup-zig@v2\n        with:\n          version: ${{ env.ZIG_VERSION }}\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Download native artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: native-libraries\n          path: packages/core/node_modules/@opentui/\n\n      - name: Verify native library\n        run: |\n          echo \"Checking for ${{ matrix.platform }} native library...\"\n          ls -la packages/core/node_modules/@opentui/\n          ls -la packages/core/node_modules/@opentui/core-${{ matrix.platform }}/\n\n      - name: Run native tests\n        run: |\n          cd packages/core/src/zig\n          zig build test --summary all\n\n      - name: Build TypeScript library\n        run: |\n          cd packages/core\n          bun run build:lib\n\n      - name: Run TypeScript tests\n        run: |\n          cd packages/core\n          bun run test:js\n\n  # Gate job for branch protection\n  build-complete:\n    name: Core - Build and Test\n    needs: [test]\n    runs-on: ubuntu-latest\n    if: always()\n    steps:\n      - name: Check test results\n        run: |\n          if [ \"${{ needs.test.result }}\" != \"success\" ]; then\n            echo \"Tests failed\"\n            exit 1\n          fi\n          echo \"All tests passed\"\n"
  },
  {
    "path": ".github/workflows/build-examples.yml",
    "content": "name: Build Examples\n\non:\n  workflow_call:\n    inputs:\n      version:\n        description: \"Version being released\"\n        required: true\n        type: string\n      isDryRun:\n        description: \"Whether this is a dry run release\"\n        required: false\n        type: boolean\n        default: false\n\njobs:\n  build-examples:\n    name: Build Example Executables\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Install dependencies FIRST (before copying built packages)\n        run: |\n          echo \"Installing base dependencies...\"\n          bun install\n\n          echo \"\"\n          echo \"✅ Base dependencies installed\"\n          echo \"Note: bun-webgpu will be installed by the build script (src/examples/build.ts) for all platforms\"\n\n      - name: Download npm packages artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: npm-packages-${{ inputs.version }}\n\n      - name: Extract and install npm packages\n        run: |\n          set -e\n          echo \"Extracting npm packages...\"\n          test -f npm-packages.zip || (echo \"❌ npm-packages.zip not found!\" && exit 1)\n          unzip -q npm-packages.zip\n\n          # Create node_modules structure\n          mkdir -p node_modules/@opentui\n          mkdir -p packages/core/node_modules/@opentui\n\n          # Copy core dist (required)\n          echo \"Copying core dist...\"\n          test -d \"npm-packages/core-dist\" || (echo \"❌ core-dist not found in artifact!\" && exit 1)\n          cp -r npm-packages/core-dist packages/core/dist\n          test -f packages/core/dist/package.json || (echo \"❌ core dist/package.json missing after copy!\" && exit 1)\n          echo \"✅ Core dist copied\"\n\n          # Copy native packages to core's node_modules (required - these OVERRIDE the npm versions)\n          echo \"Copying built native packages (overriding any npm versions)...\"\n          test -d \"npm-packages/core-native-packages\" || (echo \"❌ core-native-packages not found in artifact!\" && exit 1)\n          cp -r npm-packages/core-native-packages/* packages/core/node_modules/@opentui/\n          test -n \"$(ls -A packages/core/node_modules/@opentui/)\" || (echo \"❌ No native packages copied!\" && exit 1)\n          echo \"✅ Native packages copied\"\n\n          # Copy other package dists (optional)\n          if [ -d \"npm-packages/react-dist\" ]; then\n            cp -r npm-packages/react-dist packages/react/dist\n            echo \"✅ React dist copied\"\n          fi\n\n          if [ -d \"npm-packages/solid-dist\" ]; then\n            cp -r npm-packages/solid-dist packages/solid/dist\n            echo \"✅ Solid dist copied\"\n          fi\n\n          echo \"\"\n          echo \"Package extraction complete:\"\n          ls -lah packages/core/dist/\n          ls -lah packages/core/node_modules/@opentui/\n\n      - name: Verify bun-webgpu is available for build\n        run: |\n          echo \"Checking bun-webgpu installation...\"\n\n          # Check root node_modules first\n          if [ -d node_modules/bun-webgpu ]; then\n            echo \"✅ bun-webgpu found in root node_modules\"\n          # Check packages/core/node_modules\n          elif [ -d packages/core/node_modules/bun-webgpu ]; then\n            echo \"✅ bun-webgpu found in packages/core/node_modules\"\n          else\n            echo \"❌ bun-webgpu not found in root or packages/core/node_modules!\"\n            echo \"Note: The build script will install it, but it should already be present\"\n            exit 1\n          fi\n\n          # Check for platform-specific packages in .bun cache (where bun install --os=\"*\" --cpu=\"*\" puts them)\n          echo \"\"\n          echo \"Checking for platform-specific bun-webgpu packages in cache...\"\n          if [ -d node_modules/.bun/bun-webgpu-darwin-arm64@0.1.4 ]; then\n            echo \"✅ bun-webgpu-darwin-arm64 found in cache\"\n          else\n            echo \"⚠️  bun-webgpu-darwin-arm64 not found (will be installed during build)\"\n          fi\n\n          if [ -d node_modules/.bun/bun-webgpu-darwin-x64@0.1.4 ]; then\n            echo \"✅ bun-webgpu-darwin-x64 found in cache\"\n          else\n            echo \"⚠️  bun-webgpu-darwin-x64 not found (will be installed during build)\"\n          fi\n\n          if [ -d node_modules/.bun/bun-webgpu-linux-x64@0.1.4 ]; then\n            echo \"✅ bun-webgpu-linux-x64 found in cache\"\n          else\n            echo \"⚠️  bun-webgpu-linux-x64 not found (will be installed during build)\"\n          fi\n\n          if [ -d node_modules/.bun/bun-webgpu-win32-x64@0.1.4 ]; then\n            echo \"✅ bun-webgpu-win32-x64 found in cache\"\n          else\n            echo \"⚠️  bun-webgpu-win32-x64 not found (will be installed during build)\"\n          fi\n\n          echo \"\"\n          echo \"✅ bun-webgpu ready for bundling\"\n\n      - name: Build examples\n        run: |\n          set -e\n          cd packages/core\n          bun src/examples/build.ts\n\n      - name: Verify example builds\n        run: |\n          set -e\n          echo \"Verifying example executables...\"\n          test -f \"packages/core/src/examples/dist/darwin-x64/opentui-examples\" || (echo \"❌ darwin-x64 example missing!\" && exit 1)\n          test -f \"packages/core/src/examples/dist/darwin-arm64/opentui-examples\" || (echo \"❌ darwin-arm64 example missing!\" && exit 1)\n          test -f \"packages/core/src/examples/dist/linux-x64/opentui-examples\" || (echo \"❌ linux-x64 example missing!\" && exit 1)\n          test -f \"packages/core/src/examples/dist/windows-x64/opentui-examples.exe\" || (echo \"❌ windows-x64 example missing!\" && exit 1)\n\n          echo \"✅ All example executables verified\"\n          echo \"\"\n          ls -lah packages/core/src/examples/dist/*/\n\n      # Create separate zips for each platform's examples\n      - name: Package examples - darwin-x64\n        run: |\n          set -e\n          mkdir -p artifacts/examples-darwin-x64\n          cp packages/core/src/examples/dist/darwin-x64/opentui-examples artifacts/examples-darwin-x64/\n          cd artifacts\n          zip -r examples-darwin-x64.zip examples-darwin-x64/\n          test -f examples-darwin-x64.zip || (echo \"❌ Failed to create darwin-x64 zip\" && exit 1)\n          test -s examples-darwin-x64.zip || (echo \"❌ darwin-x64 zip is empty\" && exit 1)\n          echo \"✅ darwin-x64 packaged successfully ($(du -h examples-darwin-x64.zip | cut -f1))\"\n\n      - name: Package examples - darwin-arm64\n        run: |\n          set -e\n          mkdir -p artifacts/examples-darwin-arm64\n          cp packages/core/src/examples/dist/darwin-arm64/opentui-examples artifacts/examples-darwin-arm64/\n          cd artifacts\n          zip -r examples-darwin-arm64.zip examples-darwin-arm64/\n          test -f examples-darwin-arm64.zip || (echo \"❌ Failed to create darwin-arm64 zip\" && exit 1)\n          test -s examples-darwin-arm64.zip || (echo \"❌ darwin-arm64 zip is empty\" && exit 1)\n          echo \"✅ darwin-arm64 packaged successfully ($(du -h examples-darwin-arm64.zip | cut -f1))\"\n\n      - name: Package examples - linux-x64\n        run: |\n          set -e\n          mkdir -p artifacts/examples-linux-x64\n          cp packages/core/src/examples/dist/linux-x64/opentui-examples artifacts/examples-linux-x64/\n          cd artifacts\n          zip -r examples-linux-x64.zip examples-linux-x64/\n          test -f examples-linux-x64.zip || (echo \"❌ Failed to create linux-x64 zip\" && exit 1)\n          test -s examples-linux-x64.zip || (echo \"❌ linux-x64 zip is empty\" && exit 1)\n          echo \"✅ linux-x64 packaged successfully ($(du -h examples-linux-x64.zip | cut -f1))\"\n\n      - name: Package examples - windows-x64\n        run: |\n          set -e\n          mkdir -p artifacts/examples-windows-x64\n          cp packages/core/src/examples/dist/windows-x64/opentui-examples.exe artifacts/examples-windows-x64/\n          cd artifacts\n          zip -r examples-windows-x64.zip examples-windows-x64/\n          test -f examples-windows-x64.zip || (echo \"❌ Failed to create windows-x64 zip\" && exit 1)\n          test -s examples-windows-x64.zip || (echo \"❌ windows-x64 zip is empty\" && exit 1)\n          echo \"✅ windows-x64 packaged successfully ($(du -h examples-windows-x64.zip | cut -f1))\"\n\n      - name: Verify all artifacts before upload\n        run: |\n          set -e\n          echo \"Verifying all artifacts exist...\"\n          test -f artifacts/examples-darwin-x64.zip || (echo \"❌ examples-darwin-x64.zip missing!\" && exit 1)\n          test -f artifacts/examples-darwin-arm64.zip || (echo \"❌ examples-darwin-arm64.zip missing!\" && exit 1)\n          test -f artifacts/examples-linux-x64.zip || (echo \"❌ examples-linux-x64.zip missing!\" && exit 1)\n          test -f artifacts/examples-windows-x64.zip || (echo \"❌ examples-windows-x64.zip missing!\" && exit 1)\n\n          echo \"\"\n          echo \"✅ All artifacts verified. Ready to upload:\"\n          ls -lah artifacts/*.zip\n\n      - name: Upload example executables\n        uses: actions/upload-artifact@v4\n        with:\n          name: example-executables-${{ inputs.version }}\n          path: |\n            artifacts/examples-darwin-x64.zip\n            artifacts/examples-darwin-arm64.zip\n            artifacts/examples-linux-x64.zip\n            artifacts/examples-windows-x64.zip\n          if-no-files-found: error\n          retention-days: 30\n"
  },
  {
    "path": ".github/workflows/build-native.yml",
    "content": "name: Build Native\n\non:\n  workflow_call:\n    inputs:\n      version:\n        description: \"Version being released\"\n        required: true\n        type: string\n      isDryRun:\n        description: \"Whether this is a dry run release\"\n        required: false\n        type: boolean\n        default: false\n\nenv:\n  ZIG_VERSION: 0.15.2\n\njobs:\n  build-native:\n    name: Build Native Libraries\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Setup Zig\n        uses: goto-bus-stop/setup-zig@v2\n        with:\n          version: ${{ env.ZIG_VERSION }}\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Build packages (cross-compile for all platforms)\n        run: |\n          cd packages/core\n          bun run build:native --all\n          bun run build:lib\n          cd ../react\n          bun run build\n          cd ../solid\n          bun run build\n\n      - name: Verify build outputs\n        run: |\n          set -e\n          echo \"Checking for native binary packages...\"\n          ls -lah packages/core/node_modules/@opentui/\n          echo \"Checking for dist packages...\"\n          ls -lah packages/*/dist/\n\n          # Verify critical files exist (all 6 platforms)\n          echo \"\"\n          echo \"Verifying native binaries exist...\"\n          test -f packages/core/node_modules/@opentui/core-darwin-x64/libopentui.dylib || (echo \"❌ darwin-x64 binary missing!\" && exit 1)\n          test -f packages/core/node_modules/@opentui/core-darwin-arm64/libopentui.dylib || (echo \"❌ darwin-arm64 binary missing!\" && exit 1)\n          test -f packages/core/node_modules/@opentui/core-linux-x64/libopentui.so || (echo \"❌ linux-x64 binary missing!\" && exit 1)\n          test -f packages/core/node_modules/@opentui/core-linux-arm64/libopentui.so || (echo \"❌ linux-arm64 binary missing!\" && exit 1)\n          test -f packages/core/node_modules/@opentui/core-win32-x64/opentui.dll || (echo \"❌ windows-x64 binary missing!\" && exit 1)\n          test -f packages/core/node_modules/@opentui/core-win32-arm64/opentui.dll || (echo \"❌ windows-arm64 binary missing!\" && exit 1)\n\n          echo \"Verifying dist packages exist...\"\n          test -d packages/core/dist || (echo \"❌ core dist missing!\" && exit 1)\n          test -f packages/core/dist/package.json || (echo \"❌ core dist/package.json missing!\" && exit 1)\n\n          echo \"✅ All required build outputs verified (6 platforms)\"\n\n      # Create separate zips for each platform's native binaries\n      - name: Package native binaries - darwin-x64\n        run: |\n          set -e\n          mkdir -p artifacts/native-darwin-x64\n          cp packages/core/node_modules/@opentui/core-darwin-x64/libopentui.dylib artifacts/native-darwin-x64/\n          cd artifacts\n          zip -r native-darwin-x64.zip native-darwin-x64/\n          test -f native-darwin-x64.zip || (echo \"❌ Failed to create darwin-x64 zip\" && exit 1)\n          test -s native-darwin-x64.zip || (echo \"❌ darwin-x64 zip is empty\" && exit 1)\n          echo \"✅ darwin-x64 packaged successfully ($(du -h native-darwin-x64.zip | cut -f1))\"\n\n      - name: Package native binaries - darwin-arm64\n        run: |\n          set -e\n          mkdir -p artifacts/native-darwin-arm64\n          cp packages/core/node_modules/@opentui/core-darwin-arm64/libopentui.dylib artifacts/native-darwin-arm64/\n          cd artifacts\n          zip -r native-darwin-arm64.zip native-darwin-arm64/\n          test -f native-darwin-arm64.zip || (echo \"❌ Failed to create darwin-arm64 zip\" && exit 1)\n          test -s native-darwin-arm64.zip || (echo \"❌ darwin-arm64 zip is empty\" && exit 1)\n          echo \"✅ darwin-arm64 packaged successfully ($(du -h native-darwin-arm64.zip | cut -f1))\"\n\n      - name: Package native binaries - linux-x64\n        run: |\n          set -e\n          mkdir -p artifacts/native-linux-x64\n          cp packages/core/node_modules/@opentui/core-linux-x64/libopentui.so artifacts/native-linux-x64/\n          cd artifacts\n          zip -r native-linux-x64.zip native-linux-x64/\n          test -f native-linux-x64.zip || (echo \"❌ Failed to create linux-x64 zip\" && exit 1)\n          test -s native-linux-x64.zip || (echo \"❌ linux-x64 zip is empty\" && exit 1)\n          echo \"✅ linux-x64 packaged successfully ($(du -h native-linux-x64.zip | cut -f1))\"\n\n      - name: Package native binaries - windows-x64\n        run: |\n          set -e\n          mkdir -p artifacts/native-windows-x64\n          cp packages/core/node_modules/@opentui/core-win32-x64/opentui.dll artifacts/native-windows-x64/\n          cd artifacts\n          zip -r native-windows-x64.zip native-windows-x64/\n          test -f native-windows-x64.zip || (echo \"❌ Failed to create windows-x64 zip\" && exit 1)\n          test -s native-windows-x64.zip || (echo \"❌ windows-x64 zip is empty\" && exit 1)\n          echo \"✅ windows-x64 packaged successfully ($(du -h native-windows-x64.zip | cut -f1))\"\n\n      - name: Package native binaries - linux-arm64\n        run: |\n          set -e\n          mkdir -p artifacts/native-linux-arm64\n          cp packages/core/node_modules/@opentui/core-linux-arm64/libopentui.so artifacts/native-linux-arm64/\n          cd artifacts\n          zip -r native-linux-arm64.zip native-linux-arm64/\n          test -f native-linux-arm64.zip || (echo \"❌ Failed to create linux-arm64 zip\" && exit 1)\n          test -s native-linux-arm64.zip || (echo \"❌ linux-arm64 zip is empty\" && exit 1)\n          echo \"✅ linux-arm64 packaged successfully ($(du -h native-linux-arm64.zip | cut -f1))\"\n\n      - name: Package native binaries - windows-arm64\n        run: |\n          set -e\n          mkdir -p artifacts/native-windows-arm64\n          cp packages/core/node_modules/@opentui/core-win32-arm64/opentui.dll artifacts/native-windows-arm64/\n          cd artifacts\n          zip -r native-windows-arm64.zip native-windows-arm64/\n          test -f native-windows-arm64.zip || (echo \"❌ Failed to create windows-arm64 zip\" && exit 1)\n          test -s native-windows-arm64.zip || (echo \"❌ windows-arm64 zip is empty\" && exit 1)\n          echo \"✅ windows-arm64 packaged successfully ($(du -h native-windows-arm64.zip | cut -f1))\"\n\n      # Package the built npm packages (for use by build-examples and npm-publish)\n      - name: Package npm packages\n        run: |\n          set -e\n          mkdir -p artifacts/npm-packages\n\n          # Copy core package dist and native packages\n          echo \"Packaging core dist...\"\n          cp -r packages/core/dist artifacts/npm-packages/core-dist\n          test -f artifacts/npm-packages/core-dist/package.json || (echo \"❌ core dist/package.json missing after copy!\" && exit 1)\n\n          echo \"Packaging core native packages...\"\n          cp -r packages/core/node_modules/@opentui artifacts/npm-packages/core-native-packages\n\n          # Copy other package dists (optional - don't fail if missing)\n          echo \"Packaging other packages...\"\n          if [ -d packages/react/dist ]; then\n            cp -r packages/react/dist artifacts/npm-packages/react-dist\n            echo \"✅ React dist packaged\"\n          else\n            echo \"⚠️  React dist not found (skipping)\"\n          fi\n\n          if [ -d packages/solid/dist ]; then\n            cp -r packages/solid/dist artifacts/npm-packages/solid-dist\n            echo \"✅ Solid dist packaged\"\n          else\n            echo \"⚠️  Solid dist not found (skipping)\"\n          fi\n\n          cd artifacts\n          zip -r npm-packages.zip npm-packages/\n          test -f npm-packages.zip || (echo \"❌ Failed to create npm-packages zip\" && exit 1)\n          test -s npm-packages.zip || (echo \"❌ npm-packages zip is empty\" && exit 1)\n          echo \"✅ npm packages packaged successfully ($(du -h npm-packages.zip | cut -f1))\"\n\n      - name: Verify all artifacts before upload\n        run: |\n          set -e\n          echo \"Verifying all artifacts exist...\"\n          test -f artifacts/native-darwin-x64.zip || (echo \"❌ native-darwin-x64.zip missing!\" && exit 1)\n          test -f artifacts/native-darwin-arm64.zip || (echo \"❌ native-darwin-arm64.zip missing!\" && exit 1)\n          test -f artifacts/native-linux-x64.zip || (echo \"❌ native-linux-x64.zip missing!\" && exit 1)\n          test -f artifacts/native-linux-arm64.zip || (echo \"❌ native-linux-arm64.zip missing!\" && exit 1)\n          test -f artifacts/native-windows-x64.zip || (echo \"❌ native-windows-x64.zip missing!\" && exit 1)\n          test -f artifacts/native-windows-arm64.zip || (echo \"❌ native-windows-arm64.zip missing!\" && exit 1)\n          test -f artifacts/npm-packages.zip || (echo \"❌ npm-packages.zip missing!\" && exit 1)\n\n          echo \"\"\n          echo \"✅ All 6 platform artifacts verified. Ready to upload:\"\n          ls -lah artifacts/*.zip\n\n      - name: Upload native binaries artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: native-binaries-${{ inputs.version }}\n          path: |\n            artifacts/native-darwin-x64.zip\n            artifacts/native-darwin-arm64.zip\n            artifacts/native-linux-x64.zip\n            artifacts/native-linux-arm64.zip\n            artifacts/native-windows-x64.zip\n            artifacts/native-windows-arm64.zip\n          if-no-files-found: error\n          retention-days: 30\n\n      - name: Upload npm packages artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: npm-packages-${{ inputs.version }}\n          path: artifacts/npm-packages.zip\n          if-no-files-found: error\n          retention-days: 30\n"
  },
  {
    "path": ".github/workflows/build-react.yml",
    "content": "name: Build React\n\non:\n  push:\n  pull_request:\n    branches: [main]\n\nenv:\n  # Workaround for bug in Zig 0.15.2 (fixed in next version)\n  # https://github.com/ziglang/zig/issues/25805\n  ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-global-cache\n\njobs:\n  test:\n    name: Test (${{ matrix.os }})\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    defaults:\n      run:\n        shell: bash\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Setup Zig\n        uses: goto-bus-stop/setup-zig@v2\n        with:\n          version: 0.15.2\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Build core\n        run: |\n          cd packages/core\n          bun run build\n\n      - name: Build\n        run: |\n          cd packages/react\n          bun run build --ci\n\n      - name: Run tests\n        run: |\n          cd packages/react\n          bun run test\n\n  # Gate job for branch protection\n  build-complete:\n    name: React - Build and Test\n    needs: [test]\n    runs-on: ubuntu-latest\n    if: always()\n    steps:\n      - name: Check test results\n        run: |\n          if [ \"${{ needs.test.result }}\" != \"success\" ]; then\n            echo \"Tests failed\"\n            exit 1\n          fi\n          echo \"All tests passed\"\n"
  },
  {
    "path": ".github/workflows/build-solid.yml",
    "content": "name: Build Solid\n\non:\n  push:\n  pull_request:\n    branches: [main]\n\nenv:\n  # Workaround for bug in Zig 0.15.2 (fixed in next version)\n  # https://github.com/ziglang/zig/issues/25805\n  ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-global-cache\n\njobs:\n  test:\n    name: Test (${{ matrix.os }})\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    defaults:\n      run:\n        shell: bash\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Setup Zig\n        uses: goto-bus-stop/setup-zig@v2\n        with:\n          version: 0.15.2\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Build core\n        run: |\n          cd packages/core\n          bun run build\n\n      - name: Build\n        run: |\n          cd packages/solid\n          bun run build --ci\n\n      - name: Run tests\n        run: |\n          cd packages/solid\n          bun run test\n\n  # Gate job for branch protection\n  build-complete:\n    name: Solid - Build and Test\n    needs: [test]\n    runs-on: ubuntu-latest\n    if: always()\n    steps:\n      - name: Check test results\n        run: |\n          if [ \"${{ needs.test.result }}\" != \"success\" ]; then\n            echo \"Tests failed\"\n            exit 1\n          fi\n          echo \"All tests passed\"\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy to GitHub Pages\n\non:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout your repository using git\n        uses: actions/checkout@v6\n      - name: Install, build, and upload your site\n        uses: withastro/action@v5\n        with:\n          path: ./packages/web\n          package-manager: bun@latest\n\n  deploy:\n    needs: build\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/npm-latest-release.yml",
    "content": "name: NPM Publish\n\non:\n  workflow_call:\n    inputs:\n      version:\n        description: \"Version being published\"\n        required: true\n        type: string\n      isDryRun:\n        description: \"Whether this is a dry run (npm publish --dry-run)\"\n        required: false\n        type: boolean\n        default: false\n    secrets:\n      NPM_TOKEN:\n        required: true\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  publish:\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Download npm packages artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: npm-packages-${{ inputs.version }}\n\n      - name: Extract npm packages\n        run: |\n          echo \"Extracting npm packages...\"\n          unzip -q npm-packages.zip\n\n          # Copy core dist\n          if [ -d \"npm-packages/core-dist\" ]; then\n            cp -r npm-packages/core-dist packages/core/dist\n            echo \"Copied core dist\"\n          fi\n\n          # Copy native packages to core's node_modules\n          if [ -d \"npm-packages/core-native-packages\" ]; then\n            mkdir -p packages/core/node_modules/@opentui\n            cp -r npm-packages/core-native-packages/* packages/core/node_modules/@opentui/\n            echo \"Copied native packages\"\n          fi\n\n          # Copy other package dists\n          if [ -d \"npm-packages/react-dist\" ]; then\n            cp -r npm-packages/react-dist packages/react/dist\n            echo \"Copied react dist\"\n          fi\n\n          if [ -d \"npm-packages/solid-dist\" ]; then\n            cp -r npm-packages/solid-dist packages/solid/dist\n            echo \"Copied solid dist\"\n          fi\n\n          echo \"Package extraction complete\"\n\n      - name: Publish packages (dry-run)\n        if: ${{ inputs.isDryRun }}\n        run: |\n          echo \"==========================================\"\n          echo \"DRY RUN MODE - Not publishing to npm\"\n          echo \"==========================================\"\n          echo \"\"\n          echo \"Skipping pre-publish validation (npm version check would fail for dry-runs)\"\n          echo \"\"\n\n          # Show what would be published (just check if packages can be packed)\n          echo \"Packages that would be published:\"\n          echo \"\"\n\n          echo \"📦 Checking @opentui/core...\"\n          cd packages/core/dist && npm pack --dry-run\n\n          echo \"\"\n          echo \"📦 Checking @opentui/react...\"\n          cd ../../react/dist && npm pack --dry-run\n\n          echo \"\"\n          echo \"📦 Checking @opentui/solid...\"\n          cd ../../solid/dist && npm pack --dry-run\n\n          echo \"\"\n          echo \"==========================================\"\n          echo \"DRY RUN COMPLETE - All packages validated\"\n          echo \"==========================================\"\n        env:\n          NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n          CI: true\n\n      - name: Publish packages (production)\n        if: ${{ !inputs.isDryRun }}\n        run: |\n          echo \"==========================================\"\n          echo \"PRODUCTION MODE - Publishing to npm\"\n          echo \"==========================================\"\n\n          # Run pre-publish validation\n          bun run pre-publish\n\n          # Publish packages\n          bun run publish\n\n          echo \"\"\n          echo \"==========================================\"\n          echo \"PUBLISH COMPLETE\"\n          echo \"==========================================\"\n        env:\n          NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n          CI: true\n"
  },
  {
    "path": ".github/workflows/npm-release.yml",
    "content": "name: NPM Snapshot Release\n\non:\n  push:\n    tags:\n      - \"*snapshot*\"\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  release:\n    runs-on: macos-latest\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Setup Zig\n        uses: goto-bus-stop/setup-zig@v2\n        with:\n          version: 0.15.2\n\n      - name: Extract version from tag\n        id: extract_version\n        run: |\n          TAG=${GITHUB_REF#refs/tags/v}\n          echo \"version=$TAG\" >> $GITHUB_OUTPUT\n          echo \"Extracted version: $TAG\"\n\n      - name: Generate snapshot version\n        id: version_scheme\n        run: |\n          TAG_VERSION=\"${{ steps.extract_version.outputs.version }}\"\n          COMMIT_SHA=$(git rev-parse --short=8 HEAD)\n\n          # Pre-release snapshot\n          VERSION=\"0.0.0-$(date +%Y%m%d)-${COMMIT_SHA}\"\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"Using snapshot version: $VERSION\"\n\n      - name: Install dependencies\n        run: bun i\n\n      - name: Prepare release versions\n        run: bun run prepare-release \"${{ steps.version_scheme.outputs.version }}\"\n\n      - name: Build packages (cross-compile for all platforms)\n        run: |\n          cd packages/core\n          bun run build:native --all\n          bun run build:lib\n          cd ../solid && bun run build\n          cd ../react && bun run build\n\n      - name: Publish packages\n        run: bun run publish\n        env:\n          NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n          CI: true\n"
  },
  {
    "path": ".github/workflows/opencode.yml",
    "content": "name: opencode\n\non:\n  issue_comment:\n    types: [created]\n\njobs:\n  opencode:\n    if: |\n      contains(github.event.comment.body, ' /oc') ||\n      startsWith(github.event.comment.body, '/oc') ||\n      contains(github.event.comment.body, ' /opencode') ||\n      startsWith(github.event.comment.body, '/opencode')\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Run opencode\n        uses: sst/opencode/github@latest\n        env:\n          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n        with:\n          model: anthropic/claude-sonnet-4-20250514\n"
  },
  {
    "path": ".github/workflows/pkg-pr-new.yml",
    "content": "name: Publish Preview Packages\n\non:\n  issue_comment:\n    types: [created]\n\npermissions: {}\n\njobs:\n  publish:\n    name: Publish Preview\n    if: |\n      github.event.issue.pull_request &&\n      github.event.issue.state == 'open' &&\n      (contains(github.event.comment.body, ' /pkg-preview') ||\n        startsWith(github.event.comment.body, '/pkg-preview')) &&\n      contains(fromJson('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout PR branch\n        uses: actions/checkout@v4\n        with:\n          ref: refs/pull/${{ github.event.issue.number }}/head\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Setup Zig\n        uses: goto-bus-stop/setup-zig@v2\n        with:\n          version: 0.15.2\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Build core (all platforms via cross-compilation)\n        run: bun scripts/build.ts --native --all && bun scripts/build.ts --lib\n        working-directory: packages/core\n\n      - name: Get native package directories\n        id: native-packages\n        run: |\n          NATIVE_DIRS=$(find ./packages/core/node_modules/@opentui -maxdepth 1 -type d -name 'core-*' -printf '%p ' | sed 's/ $//')\n          echo \"dirs=$NATIVE_DIRS\" >> $GITHUB_OUTPUT\n          echo \"Found native packages: $NATIVE_DIRS\"\n\n      - name: Build react\n        run: bun run build --ci\n        working-directory: packages/react\n\n      - name: Build solid\n        run: bun run build --ci\n        working-directory: packages/solid\n\n      - name: Publish preview packages\n        run: |\n          npx pkg-pr-new publish --no-template --commentWithSha --bun \\\n            './packages/core/dist' \\\n            ${{ steps.native-packages.outputs.dirs }} \\\n            './packages/react/dist' \\\n            './packages/solid/dist'\n"
  },
  {
    "path": ".github/workflows/prettier.yml",
    "content": "name: Format\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  prettier-core:\n    name: Prettier Check\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n\n      - name: Check Prettier formatting\n        run: |\n          PRETTIER_VERSION=$(bun -e 'console.log(require(\"./package.json\").devDependencies.prettier)')\n          bunx prettier@$PRETTIER_VERSION --check --log-level warn .\n\n      - name: Show formatting differences\n        if: failure()\n        run: |\n          echo \"=== Showing differences for each file ===\"\n          PRETTIER_VERSION=$(bun -e 'console.log(require(\"./package.json\").devDependencies.prettier)')\n          bunx prettier@$PRETTIER_VERSION --write .\n          git diff --color=always\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  # Extract version and check if it's a dry run\n  prepare:\n    name: Prepare Release\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ steps.extract.outputs.version }}\n      fullTag: ${{ steps.extract.outputs.fullTag }}\n      isDryRun: ${{ steps.extract.outputs.isDryRun }}\n      releaseTitle: ${{ steps.extract.outputs.releaseTitle }}\n\n    steps:\n      - name: Extract version and dry-run flag\n        id: extract\n        run: |\n          TAG=${GITHUB_REF#refs/tags/v}\n          echo \"Full tag: $TAG\"\n\n          # Check if this is a dry run (contains -dry.)\n          if [[ \"$TAG\" =~ -dry\\. ]]; then\n            IS_DRY_RUN=true\n            # Extract base version (remove -dry.X suffix for validation)\n            VERSION=$(echo \"$TAG\" | sed -E 's/-dry\\.[0-9]+$//')\n            RELEASE_TITLE=\"Release v$TAG (DRY RUN)\"\n            echo \"This is a DRY RUN release\"\n          else\n            IS_DRY_RUN=false\n            VERSION=\"$TAG\"\n            RELEASE_TITLE=\"Release v$TAG\"\n            echo \"This is a PRODUCTION release\"\n          fi\n\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"fullTag=$TAG\" >> $GITHUB_OUTPUT\n          echo \"isDryRun=$IS_DRY_RUN\" >> $GITHUB_OUTPUT\n          echo \"releaseTitle=$RELEASE_TITLE\" >> $GITHUB_OUTPUT\n\n          echo \"Version (for validation): $VERSION\"\n          echo \"Full Tag (for artifacts): $TAG\"\n          echo \"Is Dry Run: $IS_DRY_RUN\"\n          echo \"Release Title: $RELEASE_TITLE\"\n\n  # Version validation check\n  validate-version:\n    name: Validate Version\n    needs: prepare\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Check version matches package.json files\n        run: |\n          TAG_VERSION=\"${{ needs.prepare.outputs.version }}\"\n          echo \"Validating version: $TAG_VERSION\"\n\n          # Check package.json versions (exclude web, not published to npm)\n          FAILED=false\n          for pkg in packages/*/; do\n            if [ -f \"$pkg/package.json\" ]; then\n              if [ \"$(basename \"$pkg\")\" = \"web\" ]; then\n                echo \"INFO: Skipping $pkg (not published to npm)\"\n                continue\n              fi\n              PKG_VERSION=$(node -p \"require('./$pkg/package.json').version\")\n              if [ \"$PKG_VERSION\" != \"$TAG_VERSION\" ]; then\n                echo \"❌ Package $pkg version ($PKG_VERSION) does not match tag version ($TAG_VERSION)\"\n                FAILED=true\n              else\n                echo \"✅ Package $pkg version matches: $PKG_VERSION\"\n              fi\n            fi\n          done\n\n          if [ \"$FAILED\" = true ]; then\n            echo \"\"\n            echo \"Version validation FAILED!\"\n            echo \"Please update package.json versions to match the tag version: $TAG_VERSION\"\n            exit 1\n          fi\n\n          echo \"\"\n          echo \"✅ All package versions match tag version: $TAG_VERSION\"\n\n  # Build native libraries and packages\n  build-native:\n    name: Build Native\n    needs: [prepare, validate-version]\n    uses: ./.github/workflows/build-native.yml\n    with:\n      version: ${{ needs.prepare.outputs.fullTag }}\n      isDryRun: ${{ needs.prepare.outputs.isDryRun == 'true' }}\n\n  # Build example executables\n  build-examples:\n    name: Build Examples\n    needs: [prepare, validate-version, build-native]\n    uses: ./.github/workflows/build-examples.yml\n    with:\n      version: ${{ needs.prepare.outputs.fullTag }}\n      isDryRun: ${{ needs.prepare.outputs.isDryRun == 'true' }}\n\n  # Publish to npm\n  npm-publish:\n    name: NPM Publish\n    needs: [prepare, validate-version, build-native]\n    uses: ./.github/workflows/npm-latest-release.yml\n    secrets:\n      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n    with:\n      version: ${{ needs.prepare.outputs.fullTag }}\n      isDryRun: ${{ needs.prepare.outputs.isDryRun == 'true' }}\n\n  # Create GitHub release with assets\n  github-release:\n    name: Create GitHub Release\n    needs: [prepare, build-native, build-examples, npm-publish]\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Download all artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts\n\n      - name: Organize release assets\n        run: |\n          set -e\n          WORK_DIR=$(pwd)\n          mkdir -p \"$WORK_DIR/release-assets\"\n\n          FULL_TAG=\"${{ needs.prepare.outputs.fullTag }}\"\n          echo \"Organizing release assets for tag: $FULL_TAG\"\n          echo \"Working directory: $WORK_DIR\"\n\n          # Verify artifact directories exist\n          test -d \"artifacts/native-binaries-$FULL_TAG\" || (echo \"❌ native-binaries artifact directory not found!\" && exit 1)\n          test -d \"artifacts/example-executables-$FULL_TAG\" || (echo \"❌ example-executables artifact directory not found!\" && exit 1)\n\n          # Copy native binaries (unzip and rezip with versioned names)\n          echo \"Processing native binaries...\"\n          cd \"artifacts/native-binaries-$FULL_TAG\"\n\n          for zip_file in native-*.zip; do\n            if [ -f \"$zip_file\" ]; then\n              platform=$(echo \"$zip_file\" | sed 's/native-//' | sed 's/.zip//')\n              echo \"  Processing $platform...\"\n              unzip -q \"$zip_file\"\n              \n              # Repackage with versioned name\n              dir_name=$(echo \"$zip_file\" | sed 's/.zip//')\n              if [ -d \"$dir_name\" ]; then\n                cd \"$dir_name\"\n                zip -r \"$WORK_DIR/release-assets/opentui-native-v${FULL_TAG}-${platform}.zip\" .\n                cd ..\n                \n                # Verify the output\n                test -f \"$WORK_DIR/release-assets/opentui-native-v${FULL_TAG}-${platform}.zip\" || (echo \"❌ Failed to create native $platform release asset!\" && exit 1)\n                echo \"  ✅ opentui-native-v${FULL_TAG}-${platform}.zip created\"\n              else\n                echo \"❌ Directory $dir_name not found after unzip!\" && exit 1\n              fi\n            fi\n          done\n\n          cd \"$WORK_DIR\"\n\n          # Copy example executables (unzip and rezip with versioned names)\n          echo \"Processing example executables...\"\n          cd \"artifacts/example-executables-$FULL_TAG\"\n\n          for zip_file in examples-*.zip; do\n            if [ -f \"$zip_file\" ]; then\n              platform=$(echo \"$zip_file\" | sed 's/examples-//' | sed 's/.zip//')\n              echo \"  Processing $platform...\"\n              unzip -q \"$zip_file\"\n              \n              # Repackage with versioned name\n              dir_name=$(echo \"$zip_file\" | sed 's/.zip//')\n              if [ -d \"$dir_name\" ]; then\n                cd \"$dir_name\"\n                zip -r \"$WORK_DIR/release-assets/opentui-examples-v${FULL_TAG}-${platform}.zip\" .\n                cd ..\n                \n                # Verify the output\n                test -f \"$WORK_DIR/release-assets/opentui-examples-v${FULL_TAG}-${platform}.zip\" || (echo \"❌ Failed to create examples $platform release asset!\" && exit 1)\n                echo \"  ✅ opentui-examples-v${FULL_TAG}-${platform}.zip created\"\n              else\n                echo \"❌ Directory $dir_name not found after unzip!\" && exit 1\n              fi\n            fi\n          done\n\n          cd \"$WORK_DIR\"\n\n          # Verify all expected release assets exist\n          echo \"\"\n          echo \"Verifying all release assets...\"\n          EXPECTED_ASSETS=10  # 6 native (darwin-x64, darwin-arm64, linux-x64, linux-arm64, windows-x64, windows-arm64) + 4 examples (no linux-arm64, no windows-arm64)\n          ACTUAL_ASSETS=$(ls -1 release-assets/*.zip 2>/dev/null | wc -l | tr -d ' ')\n\n          if [ \"$ACTUAL_ASSETS\" -ne \"$EXPECTED_ASSETS\" ]; then\n            echo \"❌ Expected $EXPECTED_ASSETS release assets, found $ACTUAL_ASSETS\"\n            ls -lah release-assets/ || echo \"No release assets found\"\n            exit 1\n          fi\n\n          echo \"✅ All $ACTUAL_ASSETS release assets prepared:\"\n          echo \"   - 6 native binaries (all platforms)\"\n          echo \"   - 4 example executables (darwin-x64, darwin-arm64, linux-x64, windows-x64)\"\n          ls -lah release-assets/\n\n      - name: Create GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          name: ${{ needs.prepare.outputs.releaseTitle }}\n          files: release-assets/*.zip\n          generate_release_notes: true\n          draft: false\n          prerelease: ${{ needs.prepare.outputs.isDryRun == 'true' }}\n          token: ${{ secrets.GITHUB_TOKEN }}\n          fail_on_unmatched_files: true\n"
  },
  {
    "path": ".github/workflows/review.yml",
    "content": "name: Guidelines Check\n\non:\n  issue_comment:\n    types: [created]\n\njobs:\n  check-guidelines:\n    if: |\n      github.event.issue.pull_request &&\n      startsWith(github.event.comment.body, '/review') &&\n      contains(fromJson('[\"OWNER\",\"MEMBER\"]'), github.event.comment.author_association)\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: write\n    steps:\n      - name: Get PR number\n        id: pr-number\n        run: |\n          if [ \"${{ github.event_name }}\" = \"pull_request_target\" ]; then\n            echo \"number=${{ github.event.pull_request.number }}\" >> $GITHUB_OUTPUT\n          else\n            echo \"number=${{ github.event.issue.number }}\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Install opencode\n        run: curl -fsSL https://opencode.ai/install | bash\n\n      - name: Get PR details\n        id: pr-details\n        run: |\n          gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json\n          echo \"title=$(jq -r .title pr_data.json)\" >> $GITHUB_OUTPUT\n          echo \"sha=$(jq -r .head.sha pr_data.json)\" >> $GITHUB_OUTPUT\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Check PR guidelines compliance\n        env:\n          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          OPENCODE_PERMISSION: '{ \"bash\": { \"gh*\": \"allow\", \"gh pr review*\": \"deny\", \"*\": \"deny\" } }'\n        run: |\n          PR_BODY=$(jq -r .body pr_data.json)\n          opencode run -m anthropic/claude-sonnet-4-5 \"A new pull request has been created: '${{ steps.pr-details.outputs.title }}'\n\n          <pr-number>\n          ${{ steps.pr-number.outputs.number }}\n          </pr-number>\n\n          <pr-description>\n          $PR_BODY\n          </pr-description>\n\n          Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do\n\n          When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.\n          When critiquing code style don't be a zealot, we don't like \"let\" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)\n\n          Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.\n\n          Command MUST be like this.\n          \\`\\`\\`\n          gh api \\\n            --method POST \\\n            -H \\\"Accept: application/vnd.github+json\\\" \\\n            -H \\\"X-GitHub-Api-Version: 2022-11-28\\\" \\\n            /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \\\n            -f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \\\"line=[line]\\\" -f 'side=RIGHT'\n          \\`\\`\\`\n\n          Only create comments for actual violations. If the code follows all guidelines, comment 'lgtm' AND NOTHING ELSE!!!!.\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# dependencies (bun install)\nnode_modules\n\nSession.vim\n\n# output\nout\ndist\npacked\n*.tgz\n*.log\n*.txt\n\n# code coverage\ncoverage\n*.lcov\n\n# logs\nlogs\n_.log\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# caches\n.eslintcache\n.cache\n*.tsbuildinfo\n\n# IntelliJ based IDEs\n.idea\n\n# Finder (MacOS) folder config\n.DS_Store\n\nscreenshot-*.png\n"
  },
  {
    "path": ".prettierignore",
    "content": "**/.zig-cache/**\npackages/core/src/lib/tree-sitter/default-parsers.ts\n"
  },
  {
    "path": ".zig-version",
    "content": "0.15.2\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Agent Guidelines for opentui\n\nDefault to using Bun instead of Node.js.\n\n- Use `bun <file>` instead of `node <file>` or `ts-node <file>`\n- Use `bun test` instead of `jest` or `vitest`\n- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`\n- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`\n- Bun automatically loads .env, so don't use dotenv.\n\nNOTE: When only changing typescript, you do NOT need to run the build script.\nThe build is only needed when changing native code.\n\n## APIs\n\nDon't use bun-specific APIs. Generated code should work in Bun, Node.js and Deno runtimes.\n\n## Testing\n\nUse `bun test` to run tests from the packages directories for a specific package.\n\n```ts#index.test.ts\nimport { test, expect } from \"bun:test\";\n\ntest(\"hello world\", () => {\n  expect(1).toBe(1);\n});\n```\n\nFor more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.\n\n## Build/Test Commands\n\nTo build the project (before running typescript tests), run\n`bun run build`\nFROM THE REPO ROOT to make sure all packages are built correctly.\n\nTo run native tests for `packages/core`, run\n`bun run test:native`\nFROM THE `packages/core` DIRECTORY.\n\nTo filter native tests, use:\n`bun run test:native -Dtest-filter=\"test name\"`\nFROM THE `packages/core` DIRECTORY.\n\n## Typescript Code Style\n\n- **Runtime**: Bun with TypeScript\n- **Formatting**: Prettier (semi: false, printWidth: 120)\n- **Imports**: Use explicit imports, group by: built-ins, external deps, internal modules\n- **Types**: Strict TypeScript, use interfaces for options/configs, explicit return types for public APIs\n- **Naming**: camelCase for variables/functions, PascalCase for classes/interfaces, UPPER_CASE for constants\n- **Error Handling**: Use proper Error objects, avoid silent failures\n- **Async**: Prefer async/await over Promises, handle errors explicitly\n- **Comments**: Minimal comments, NO JSDoc\n- **File Structure**: Index files for clean exports, group related functionality\n- **Testing**: Bun test framework, descriptive test names, use beforeEach/afterEach for setup\n\n## Debugging\n\n- NOTE this is a terminal UI lib and when running examples or apps built with it,\n  you cannot currently see log output like console.log. Ask the user to run the example/app and provide the output.\n- Reproduce the issue in a test case. Do NOT start fixing without a reproducible test case.\n  Use debug logs to see what is actually happening. DO NOT GUESS.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Opentui\n\nBug fixes and feature suggestions are always welcome. For bug fixes, open a PR\nfor reviews. Feature suggestions are subject to discussion via issues.\n\n## Code style\n\nReference existing [AGENTS.md](https://github.com/anomalyco/opentui/blob/main/AGENTS.md) or project conventions if applicable.\n\n## Code of conduct\n\n- Treat everyone with respect and empathy. We do not tolerate harassment, discrimination, or personal attacks.\n- Be kind, constructive, and assume good intent.\n- Keep feedback specific and actionable; critique code, not people.\n- No unsolicited DMs for support unless invited.\n- Follow project guidelines and maintainers’ decisions.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 opentui\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# OpenTUI\n\n<div align=\"center\">\n    <a href=\"https://www.npmjs.com/package/@opentui/core\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/@opentui/core?style=flat-square\" /></a>\n    <a href=\"https://github.com/anomalyco/opentui/actions/workflows/build-core.yml\"><img alt=\"Build status\" src=\"https://img.shields.io/github/actions/workflow/status/anomalyco/opentui/build-core.yml?style=flat-square&branch=main\" /></a>\n    <a href=\"https://github.com/msmps/awesome-opentui\"><img alt=\"awesome opentui list\" src=\"https://awesome.re/badge-flat.svg\" /></a>\n</div>\n\nOpenTUI is a native terminal UI core written in Zig with TypeScript bindings. The native core exposes a C ABI and can be used from any language. OpenTUI powers [OpenCode](https://opencode.ai) in production today and will also power [terminal.shop](https://terminal.shop). It is an extensible core with a focus on correctness, stability, and high performance. It provides a component-based architecture with flexible layout capabilities, allowing you to create complex terminal applications.\n\nDocs: https://opentui.com/docs/getting-started\n\nQuick start with [bun](https://bun.sh) and [create-tui](https://github.com/msmps/create-tui):\n\n```bash\nbun create tui\n```\n\nThis monorepo contains the following packages:\n\n- [`@opentui/core`](packages/core) - TypeScript bindings for OpenTUI's native Zig core, with an imperative API and all primitives.\n- [`@opentui/solid`](packages/solid) - The SolidJS reconciler for OpenTUI.\n- [`@opentui/react`](packages/react) - The React reconciler for OpenTUI.\n\n## Install\n\nNOTE: You must have [Zig](https://ziglang.org/learn/getting-started/) installed on your system to build the packages.\n\n### TypeScript/JavaScript\n\n```bash\nbun install @opentui/core\n```\n\n## AI Agent Skill\n\nTeach your AI coding assistant OpenTUI's APIs and patterns.\n\n**For [OpenCode](https://opencode.ai) (includes `/opentui` command):**\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/msmps/opentui-skill/main/install.sh | bash\n```\n\n**For other AI coding assistants:**\n\n```bash\nnpx skills add msmps/opentui-skill\n```\n\n## Try Examples\n\nYou can quickly try out OpenTUI examples without cloning the repository:\n\n**For macOS, Linux, WSL, Git Bash:**\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/anomalyco/opentui/main/packages/core/src/examples/install.sh | sh\n```\n\n**For Windows (PowerShell/CMD):**\n\nDownload the latest release directly from [GitHub Releases](https://github.com/anomalyco/opentui/releases/latest)\n\n## Running Examples (from the repo root)\n\n### TypeScript Examples\n\n```bash\nbun install\ncd packages/core\nbun run src/examples/index.ts\n```\n\n## Development\n\nSee the [Development Guide](packages/core/docs/development.md) for building, testing, debugging, and local development linking.\n\n### Documentation\n\n- [Website docs](https://opentui.com/docs/getting-started) - Guides and API references\n- [Development Guide](packages/core/docs/development.md) - Building, testing, and local dev linking\n- [Getting Started](packages/core/docs/getting-started.md) - API and usage guide\n- [Environment Variables](packages/core/docs/env-vars.md) - Configuration options\n\n## Showcase\n\nConsider showcasing your work on the [awesome-opentui](https://github.com/msmps/awesome-opentui) list. A curated list of awesome resources and terminal user interfaces built with OpenTUI.\n"
  },
  {
    "path": "opentui.pc.in",
    "content": "prefix=@PREFIX@\nlibdir=${prefix}/lib\nincludedir=${prefix}/include\n\nName: OpenTUI\nDescription: High-performance terminal user interface library\nVersion: @VERSION@\nLibs: -L${libdir} -lopentui -Wl,-rpath,${libdir}\nCflags: -I${includedir}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@opentui\",\n  \"private\": true,\n  \"workspaces\": [\n    \"packages/*\",\n    \"packages/solid/examples\"\n  ],\n  \"scripts\": {\n    \"build\": \"cd packages/core && bun run build && cd ../solid && bun run build && cd ../react && bun run build\",\n    \"pre-publish\": \"bun scripts/pre-publish.ts\",\n    \"publish\": \"bun run pre-publish && bun run publish:core && bun run publish:react && bun run publish:solid\",\n    \"publish:core\": \"cd packages/core && bun run publish\",\n    \"publish:react\": \"cd packages/react && bun run publish\",\n    \"publish:solid\": \"cd packages/solid && bun run publish\",\n    \"prepare-release\": \"bun scripts/prepare-release.ts\",\n    \"prettier:write\": \"prettier --write .\",\n    \"test\": \"bun run --filter '@opentui/core' --filter '@opentui/solid' --filter '@opentui/react' --if-present test\"\n  },\n  \"devDependencies\": {\n    \"prettier\": \"3.6.2\"\n  },\n  \"prettier\": {\n    \"semi\": false,\n    \"printWidth\": 120\n  }\n}\n"
  },
  {
    "path": "packages/core/.gitignore",
    "content": "# Zig build cache\n.zig-cache\nzig-out\nsrc/zig/lib\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "# OpenTUI Core\n\nOpenTUI is a native terminal UI core written in Zig with TypeScript bindings. The native core exposes a C ABI and can be used from any language. OpenTUI powers OpenCode in production today and will also power terminal.shop. It is an extensible core with a focus on correctness, stability, and high performance. It provides a component-based architecture with flexible layout capabilities, allowing you to create complex terminal applications.\n\n## Documentation\n\n- [Getting Started](docs/getting-started.md) - API and usage guide\n- [Development Guide](docs/development.md) - Building, testing, and contributing\n- [Tree-Sitter](docs/tree-sitter.md) - Syntax highlighting integration\n- [Renderables vs Constructs](docs/renderables-vs-constructs.md) - Understanding the component model\n- [Environment Variables](docs/env-vars.md) - Configuration options\n\n## Install\n\n```bash\nbun install @opentui/core\n```\n\n## Build\n\n```bash\nbun run build\n```\n\nThis creates platform-specific libraries that are automatically loaded by the TypeScript layer.\n\n## Examples\n\n```bash\nbun install\nbun run src/examples/index.ts\n```\n\n## Benchmarks\n\nRun native performance benchmarks:\n\n```bash\nbun run bench:native\n```\n\nSee [src/zig/bench.zig](src/zig/bench.zig) for available options like `--filter` and `--mem`.\n\nNativeSpanFeed TypeScript benchmarks:\n\n- [src/benchmark/native-span-feed-benchmark.md](src/benchmark/native-span-feed-benchmark.md)\n\n## CLI Renderer\n\n### Renderables\n\nRenderables are hierarchical objects that can be positioned, nested, styled and rendered to the terminal:\n\n```typescript\nimport { createCliRenderer, TextRenderable } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst obj = new TextRenderable(renderer, { id: \"my-obj\", content: \"Hello, world!\" })\n\nrenderer.root.add(obj)\n```\n"
  },
  {
    "path": "packages/core/dev/keypress-debug-renderer.ts",
    "content": "#!/usr/bin/env bun\nimport { BoxRenderable, type CliRenderer, createCliRenderer, CodeRenderable, addDefaultParsers } from \"../src/index.js\"\nimport { ScrollBoxRenderable } from \"../src/renderables/ScrollBox.js\"\nimport { SyntaxStyle } from \"../src/syntax-style.js\"\nimport { parseColor } from \"../src/lib/RGBA.js\"\n\nconst parsers = [\n  {\n    filetype: \"json\",\n    wasm: \"https://github.com/tree-sitter/tree-sitter-json/releases/download/v0.24.8/tree-sitter-json.wasm\",\n    queries: {\n      highlights: [\n        \"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/json/highlights.scm\",\n      ],\n    },\n  },\n]\naddDefaultParsers(parsers)\nlet scrollBox: ScrollBoxRenderable | null = null\nlet renderer: CliRenderer | null = null\nlet syntaxStyle: SyntaxStyle | null = null\nlet eventCount = 0\n\nfunction addEvent(eventType: string, event: object) {\n  if (!renderer || !scrollBox || !syntaxStyle) return\n\n  eventCount++\n\n  const eventData = {\n    type: eventType,\n    timestamp: new Date().toISOString(),\n    ...event,\n  }\n\n  const eventBox = new BoxRenderable(renderer, {\n    id: `event-${eventCount}`,\n    width: \"auto\",\n    marginBottom: 1,\n    padding: 1,\n    backgroundColor: \"#1f2937\",\n  })\n\n  const codeDisplay = new CodeRenderable(renderer, {\n    id: `event-code-${eventCount}`,\n    content: JSON.stringify(eventData, null, 2),\n    filetype: \"json\",\n    conceal: false,\n    syntaxStyle,\n    bg: \"#1f2937\",\n  })\n\n  eventBox.add(codeDisplay)\n  scrollBox.add(eventBox)\n\n  const children = scrollBox.getChildren()\n  if (children.length > 50) {\n    const oldest = children[0]\n    if (oldest) {\n      scrollBox.remove(oldest.id)\n      oldest.destroyRecursively()\n    }\n  }\n}\n\nasync function main() {\n  const usePrepend = process.argv.includes(\"--prepend\")\n  const prependInputHandlers = usePrepend\n    ? [\n        (sequence: string) => {\n          addEvent(\"raw-input-before\", { sequence })\n          return false\n        },\n      ]\n    : []\n  renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n    useKittyKeyboard: { events: true },\n    prependInputHandlers,\n  })\n\n  renderer.setBackgroundColor(\"#0D1117\")\n\n  const mainContainer = new BoxRenderable(renderer, {\n    id: \"main-container\",\n    flexGrow: 1,\n    flexDirection: \"column\",\n  })\n\n  renderer.root.add(mainContainer)\n\n  scrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"event-scroll-box\",\n    stickyScroll: true,\n    stickyStart: \"bottom\",\n    border: true,\n    borderColor: \"#6BCF7F\",\n    title: `Keypress Debug${usePrepend ? \" (prepend mode)\" : \"\"} (Ctrl+C to exit)`,\n    titleAlignment: \"center\",\n    contentOptions: {\n      paddingLeft: 1,\n      paddingRight: 1,\n      paddingTop: 1,\n    },\n  })\n\n  mainContainer.add(scrollBox)\n\n  syntaxStyle = SyntaxStyle.fromStyles({\n    string: { fg: parseColor(\"#A5D6FF\") },\n    number: { fg: parseColor(\"#79C0FF\") },\n    boolean: { fg: parseColor(\"#79C0FF\") },\n    keyword: { fg: parseColor(\"#FF7B72\") },\n    default: { fg: parseColor(\"#E6EDF3\") },\n  })\n\n  addEvent(\"capabilities\", renderer.capabilities)\n\n  renderer.addInputHandler((sequence) => {\n    addEvent(\"raw-input-after\", { sequence })\n    return true\n  })\n\n  renderer.keyInput.on(\"keypress\", (event) => {\n    addEvent(\"keypress\", event)\n\n    if (event.name === \"c\" && event.shift) {\n      if (renderer) {\n        addEvent(\"capabilities\", renderer.capabilities)\n      }\n    }\n  })\n\n  renderer.keyInput.on(\"keyrelease\", (event) => {\n    addEvent(\"keyrelease\", event)\n  })\n\n  renderer.keyInput.on(\"paste\", (event) => {\n    addEvent(\"paste\", event)\n  })\n\n  renderer.requestRender()\n}\n\nmain().catch((err) => {\n  console.error(\"Error:\", err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/core/dev/keypress-debug.ts",
    "content": "#!/usr/bin/env bun\nimport { parseKeypress } from \"../src/lib/parse.keypress.ts\"\n\nconsole.log(\"Keypress Debug Tool\")\nconsole.log(\"Press keys to see their parsed output. Press Ctrl+C to exit.\\n\")\n\n// Set stdin to raw mode to capture individual keypresses\nprocess.stdin.setRawMode(true)\nprocess.stdin.resume()\n\n// Listen for keypress data\nprocess.stdin.on(\"data\", (data: Buffer) => {\n  // Check for Ctrl+C to exit\n  if (data.toString() === \"\\x03\") {\n    console.log(\"\\nExiting keypress debug tool...\")\n    process.stdin.setRawMode(false)\n    process.exit(0)\n  }\n\n  const parsed = parseKeypress(data)\n\n  console.log(\"Input data:\", JSON.stringify(data.toString()))\n  console.log(\"Raw:\", JSON.stringify(parsed?.raw))\n  console.log(\"Parsed:\", {\n    name: parsed?.name,\n    ctrl: parsed?.ctrl,\n    meta: parsed?.meta,\n    shift: parsed?.shift,\n    option: parsed?.option,\n    number: parsed?.number,\n    sequence: parsed?.sequence,\n    code: parsed?.code,\n    buffer: data,\n  })\n  console.log(\"---\")\n})\n\n// Handle Ctrl+C gracefully\nprocess.on(\"SIGINT\", () => {\n  console.log(\"\\nExiting keypress debug tool...\")\n  process.stdin.setRawMode(false)\n  process.exit(0)\n})\n"
  },
  {
    "path": "packages/core/dev/print-env-vars.ts",
    "content": "#!/usr/bin/env bun\n\n/**\n * Development script to print all registered environment variables\n *\n * Usage:\n *   bun dev/print-env-vars.ts          # Colored output (default)\n *   bun dev/print-env-vars.ts --markdown  # Markdown output\n *   bun dev/print-env-vars.ts --update   # Update docs/env-vars.md\n */\n\nimport { generateEnvColored, generateEnvMarkdown } from \"../src/index.js\"\nimport { join } from \"path\"\n\nconst args = process.argv.slice(2)\nconst useMarkdown = args.includes(\"--markdown\")\nconst updateDocs = args.includes(\"--update\")\n\nconst generateMarkdownContent = () => {\n  return `# Environment Variables\\n\\n${generateEnvMarkdown()}---\\n\\n_generated via packages/core/dev/print-env-vars.ts_\\n`\n}\n\nif (updateDocs) {\n  const docsPath = join(import.meta.dir, \"../docs/env-vars.md\")\n  const content = generateMarkdownContent()\n  await Bun.write(docsPath, content)\n  console.log(`✓ Updated ${docsPath}`)\n} else if (useMarkdown) {\n  console.log(`${generateEnvMarkdown()}\\n---\\n_generated via packages/core/dev/print-env-vars.ts_`)\n} else {\n  console.log(generateEnvColored())\n}\n"
  },
  {
    "path": "packages/core/dev/test-tmux-graphics-334.sh",
    "content": "#!/bin/bash\n# Test script for issue #334: Kitty Graphics Protocol query leaks into tmux pane title\n#\n# This script verifies that the kitty graphics query doesn't corrupt tmux pane titles.\n# Run this inside tmux to test.\n#\n# Usage: ./test-tmux-graphics-334.sh\n#\n# Expected results:\n#   - Test 1 (Direct query): FAIL - demonstrates the bug\n#   - Test 2 (DCS passthrough): PASS - demonstrates the fix\n#   - Test 3 (No query): PASS - control test\n\nset -e\n\nSESSION_NAME=\"opentui-test-334-$$\"\nEXPECTED_TITLE=\"test-title\"\n\ncleanup() {\n    tmux kill-session -t \"$SESSION_NAME\" 2>/dev/null || true\n}\ntrap cleanup EXIT\n\nrun_test() {\n    local test_name=\"$1\"\n    local query_cmd=\"$2\"\n    \n    cleanup\n    tmux new-session -d -s \"$SESSION_NAME\" -x 80 -y 24\n    tmux select-pane -t \"$SESSION_NAME\" -T \"$EXPECTED_TITLE\"\n    \n    if [ -n \"$query_cmd\" ]; then\n        tmux send-keys -t \"$SESSION_NAME\" \"$query_cmd\" Enter\n        sleep 0.5\n    fi\n    \n    local after_title\n    after_title=$(tmux display-message -t \"$SESSION_NAME\" -p '#{pane_title}')\n    \n    if [[ \"$after_title\" == *\"Gi=31337\"* ]] || [[ \"$after_title\" == *\"i=31337\"* ]]; then\n        echo \"FAIL: $test_name - pane title corrupted: '$after_title'\"\n        return 1\n    elif [[ \"$after_title\" != \"$EXPECTED_TITLE\" ]]; then\n        echo \"WARN: $test_name - pane title changed: '$after_title'\"\n        return 1\n    else\n        echo \"PASS: $test_name\"\n        return 0\n    fi\n}\n\necho \"=== Issue #334 Test: Kitty Graphics Query in tmux ===\"\necho \"tmux version: $(tmux -V)\"\necho \"\"\n\necho \"Test 1: Direct query (demonstrates bug)\"\nrun_test \"Direct kitty graphics query\" \\\n    \"printf '\\\\x1b_Gi=31337,s=1,v=1,a=q,t=d,f=24;AAAA\\\\x1b\\\\\\\\\\\\x1b[c'\" || true\n\necho \"Test 2: DCS passthrough wrapped query (demonstrates fix)\"\nrun_test \"DCS passthrough wrapped\" \\\n    \"printf '\\\\x1bPtmux;\\\\x1b\\\\x1b_Gi=31337,s=1,v=1,a=q,t=d,f=24;AAAA\\\\x1b\\\\x1b\\\\\\\\\\\\x1b\\\\x1b[c\\\\x1b\\\\\\\\'\"\n\necho \"Test 3: No query (control)\"\nrun_test \"No query\" \"\"\n\necho \"\"\necho \"If Test 1 fails and Test 2 passes, the fix is working correctly.\"\n"
  },
  {
    "path": "packages/core/dev/thai-debug-test.ts",
    "content": "#!/usr/bin/env bun\nimport { TextRenderable, createCliRenderer } from \"../src/index.js\"\nimport { ScrollBoxRenderable } from \"../src/renderables/ScrollBox.js\"\nasync function main() {\n  const renderer = await createCliRenderer({ exitOnCtrlC: true })\n  console.log(\"capabilities:\", renderer.capabilities)\n  const scrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"scroll-box\",\n    border: true,\n    height: 20,\n    title: \"Thai Render Debug (Up/Down to scroll, Ctrl+C to exit)\",\n    titleAlignment: \"center\",\n  })\n  const thaiLorem = `ภาษาไทยเป็นภาษาที่มีความสวยงามและซับซ้อนมาก มีพยัญชนะ 44 ตัว สระ 32 รูป และวรรณยุกต์ 4 รูป\nมอญเป็นชนชาติที่มีอิทธิพลต่อภาษาไทยมาก คำว่า \"มอ\" เป็นพยัญชนะที่ใช้บ่อยในภาษาไทย\nตัวอย่างคำที่มี มอ: มา มี มือ มอง มัว มอบ มวย มอม มอด มอก\nการเขียนภาษาไทยต้องใส่ใจเรื่องวรรณยุกต์ เช่น ม่า ม้า หม่า หม้า\nสระลอยและสระเปลี่ยนรูปทำให้ภาษาไทยมีความพิเศษ เช่น เมือง แมว ไม้ ใหม่\nคำผสมที่น่าสนใจ: มอมแมม มอดมอด มอมเมา มืดมอม มอมดำ\nภาษาไทยมีระบบการเขียนที่ไม่เว้นวรรคระหว่างคำ ทำให้การอ่านต้องอาศัยความเข้าใจ\nตัวอักษรไทยมีสามระดับ: บน กลาง ล่าง เช่น ก็ ปี่ ฎุ ฏู ญ ฐ ฑ\nวรรณยุกต์ไทยมี 5 เสียง: สามัญ เอก โท ตรี จัตวา\nตัวอย่างเสียงวรรณยุกต์: มา ม่า ม้า หมา หม่า หม้า\nการสะกดคำในภาษาไทยมีกฎเกณฑ์ที่ซับซ้อน มีทั้งตัวสะกดมาตราแม่ ก กา และตัวสะกดไม่ตรงมาตรา\nคำยืมจากภาษาบาลีสันสกฤต: กรรม ธรรม สงฆ์ ศาสนา พระ มหา\nคำยืมจากภาษาเขมร: ถนน เสวย ขนม ตำรวจ ทหาร\nคำยืมจากภาษาจีน: ก๋วยเตี๋ยว เต้าหู้ บะหมี่ เกี๊ยว ซาลาเปา\nภาษาไทยถิ่นมีหลายภาษา: ภาษาเหนือ ภาษาอีสาน ภาษาใต้ ภาษากลาง\nมอญกับไทยมีความสัมพันธ์ทางวัฒนธรรมมายาวนาน\nตัวเลขไทย: ๐ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙\nเครื่องหมายวรรคตอน: ฯ ๆ ฯลฯ « » ๏\nอักษรไทยมีต้นกำเนิดจากอักษรเขมร ซึ่งมาจากอักษรปัลลวะของอินเดียใต้\nพ่อขุนรามคำแหงมหาราชทรงประดิษฐ์อักษรไทยเมื่อ พ.ศ. 1826\nภาษาไทยเป็นภาษาที่มีวรรณยุกต์ การออกเสียงผิดวรรณยุกต์ทำให้ความหมายเปลี่ยน\nตัวอย่าง: ขาว (สีขาว) ข่าว (news) ข้าว (rice) เข้า (enter)\nมอเตอร์ไซค์ มอนิเตอร์ มอลต์ มอร์ฟีน เป็นคำทับศัพท์ที่มี มอ\nสำนวนไทยที่มี มอ: มอมเมา หมอมหมาม มืดมอม มอดไหม้\nคำกริยาที่มี มอ: มอง มอบ มอม มอด มอก\nคำนามที่มี มอ: มอญ มอด หมอ มอเตอร์\nการผันวรรณยุกต์ของ มอ: มอ ม่อ ม้อ หมอ หม่อ หม้อ\nภาษาไทยมีคำควบกล้ำ: กร กล กว ขร ขล ขว คร คล คว\nตัวอย่างคำควบกล้ำ: กราบ กลาง กวาง ครู คลอง ความ\nอักษรสูง กลาง ต่ำ มีผลต่อการผันวรรณยุกต์\nอักษรสูง: ข ฃ ฉ ฐ ถ ผ ฝ ศ ษ ส ห\nอักษรกลาง: ก จ ฎ ฏ ด ต บ ป อ\nอักษรต่ำ: ค ฅ ฆ ง ช ซ ฌ ญ ฑ ฒ ณ ท ธ น พ ฟ ภ ม ย ร ล ว ฬ ฮ`\n  const text = new TextRenderable(renderer, {\n    id: \"thai-text\",\n    content: thaiLorem,\n  })\n  scrollBox.add(text)\n  renderer.root.add(scrollBox)\n  renderer.keyInput.on(\"keypress\", (event) => {\n    if (event.name === \"down\") scrollBox.scrollDown()\n    if (event.name === \"up\") scrollBox.scrollUp()\n  })\n  renderer.requestRender()\n\n  renderer.keyInput.on(\"keypress\", (event) => {\n    if (event.name === \"`\") {\n      renderer.console.toggle()\n    }\n  })\n}\nmain().catch((err) => {\n  console.error(\"Error:\", err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/core/docs/development.md",
    "content": "# Development Guide\n\n## Prerequisites\n\n- [Bun](https://bun.sh) - JavaScript runtime and package manager\n- [Zig](https://ziglang.org/learn/getting-started/) - Required for building native modules\n\n## Setup\n\n```bash\ngit clone https://github.com/anomalyco/opentui.git\ncd opentui\nbun install\n```\n\n## Building\n\n```bash\nbun run build\n```\n\n**Note:** Only needed when changing native Zig code. TypeScript changes don't require rebuilding.\n\n## Running Examples\n\n```bash\ncd packages/core\nbun run src/examples/index.ts\n```\n\n## Testing\n\n```bash\n# TypeScript tests\ncd packages/core\nbun test\n\n# Native tests\nbun run test:native\n\n# Filter native tests\nbun run test:native -Dtest-filter=\"test name\"\n\n# Benchmarks\nbun run bench:native\n```\n\n## Local Development Linking\n\nLink your local OpenTUI to another project:\n\n```bash\n./scripts/link-opentui-dev.sh /path/to/your/project\n```\n\n**Options:**\n\n- `--react` - Also link `@opentui/react` and React dependencies\n- `--solid` - Also link `@opentui/solid` and SolidJS dependencies\n- `--dist` - Link built `dist` directories instead of source\n- `--copy` - Copy instead of symlink (requires `--dist`)\n- `--subdeps` - Find and link packages that depend on opentui (e.g., `opentui-spinner`)\n\n**Examples:**\n\n```bash\n# Link core only\n./scripts/link-opentui-dev.sh /path/to/your/project\n\n# Link core and solid with subdependency discovery\n./scripts/link-opentui-dev.sh /path/to/your/project --solid --subdeps\n\n# Link built artifacts\n./scripts/link-opentui-dev.sh /path/to/your/project --react --dist\n\n# Copy for Docker/Windows\n./scripts/link-opentui-dev.sh /path/to/your/project --dist --copy\n```\n\nThe script automatically links:\n\n- Main packages: `@opentui/core`, `@opentui/solid`, `@opentui/react`\n- Peer dependencies: `yoga-layout`, `solid-js`, `react`, `react-dom`, `react-reconciler`\n- Subdependencies (with `--subdeps`): Packages like `opentui-spinner` that depend on opentui\n\n**Requirements:** Target project must have `node_modules` (run `bun install` first).\n\n## Debugging\n\nOpenTUI captures `console.log` output. Toggle the built-in console with backtick or use [Environment Variables](./env-vars.md) for debugging.\n\n## Terminal Compatibility\n\n### OSC 66 Artifacts on Older Terminals\n\n**Problem:** If you see weird artifacts containing \"66\" in your terminal when running OpenTUI applications, your terminal emulator doesn't support OSC 66 escape sequences (used for explicit character width detection).\n\n**Affected Terminals:**\n\n- GNOME Terminal\n- Konsole (older versions)\n- xterm (older versions)\n- Many VT100/VT220 emulators\n\n**Solution:** Disable OSC 66 queries by setting an environment variable:\n\n```bash\nexport OPENTUI_FORCE_EXPLICIT_WIDTH=false\n```\n\nOr run your application with:\n\n```bash\nOPENTUI_FORCE_EXPLICIT_WIDTH=false your-app\n```\n\n**For Application Developers:**\n\nSet it in your code before creating the renderer:\n\n```typescript\nprocess.env.OPENTUI_FORCE_EXPLICIT_WIDTH = \"false\"\n\nconst renderer = new CliRenderer()\n// ... rest of your app\n```\n\nOr add to your `.env` file:\n\n```bash\nOPENTUI_FORCE_EXPLICIT_WIDTH=false\n```\n\n**What This Does:**\n\n- Prevents OSC 66 detection queries from being sent\n- Disables the explicit width feature\n- Falls back to standard width calculation\n- No visual artifacts on unsupported terminals\n\n**Modern Terminals:** If your terminal supports OSC 66 (Kitty, Ghostty, WezTerm, Alacritty, iTerm2), you don't need this setting - they work correctly by default.\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@opentui/core\",\n  \"description\": \"OpenTUI is a TypeScript library on a native Zig core for building terminal user interfaces (TUIs)\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/anomalyco/opentui\",\n    \"directory\": \"packages/core\"\n  },\n  \"types\": \"src/index.ts\",\n  \"module\": \"src/index.ts\",\n  \"main\": \"src/index.ts\",\n  \"version\": \"0.1.90\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"bun run build:native && bun run build:lib\",\n    \"build:dev\": \"bun run build:native:dev && bun run build:lib\",\n    \"build:lib\": \"bun scripts/build.ts --lib\",\n    \"build:native\": \"bun scripts/build.ts --native\",\n    \"build:native:dev\": \"bun scripts/build.ts --native --dev\",\n    \"build:examples\": \"bun src/examples/build.ts\",\n    \"test:native\": \"cd src/zig && zig build test --summary all\",\n    \"bench:native\": \"cd src/zig && zig build bench -Dbench-optimize=ReleaseFast --\",\n    \"bench:text-table\": \"bun src/benchmark/text-table-benchmark.ts\",\n    \"bench:ts\": \"bun src/benchmark/native-span-feed-benchmark.ts --suite=quick --json=src/benchmark/latest-quick-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=default --json=src/benchmark/latest-default-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=large --json=src/benchmark/latest-large-bench-run.json && bun src/benchmark/native-span-feed-benchmark.ts --suite=all --json=src/benchmark/latest-all-bench-run.json && bun src/benchmark/native-span-feed-async-benchmark.ts --json=src/benchmark/latest-async-bench-run.json\",\n    \"publish\": \"bun scripts/publish.ts\",\n    \"test:js\": \"bun test\",\n    \"test\": \"bun run test:native && bun run test:js\"\n  },\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@types/bun\": \"latest\",\n    \"@types/node\": \"^24.0.0\",\n    \"@types/three\": \"0.177.0\",\n    \"commander\": \"^13.1.0\",\n    \"typescript\": \"^5\",\n    \"web-tree-sitter\": \"0.25.10\"\n  },\n  \"dependencies\": {\n    \"bun-ffi-structs\": \"0.1.2\",\n    \"diff\": \"8.0.2\",\n    \"jimp\": \"1.6.0\",\n    \"marked\": \"17.0.1\",\n    \"yoga-layout\": \"3.2.1\"\n  },\n  \"peerDependencies\": {\n    \"web-tree-sitter\": \"0.25.10\"\n  },\n  \"optionalDependencies\": {\n    \"@dimforge/rapier2d-simd-compat\": \"^0.17.3\",\n    \"bun-webgpu\": \"0.1.5\",\n    \"planck\": \"^1.4.2\",\n    \"three\": \"0.177.0\",\n    \"@opentui/core-darwin-x64\": \"0.1.90\",\n    \"@opentui/core-darwin-arm64\": \"0.1.90\",\n    \"@opentui/core-linux-x64\": \"0.1.90\",\n    \"@opentui/core-linux-arm64\": \"0.1.90\",\n    \"@opentui/core-win32-x64\": \"0.1.90\",\n    \"@opentui/core-win32-arm64\": \"0.1.90\"\n  },\n  \"exports\": {\n    \".\": {\n      \"types\": \"./src/index.ts\",\n      \"import\": \"./src/index.ts\"\n    },\n    \"./3d\": {\n      \"types\": \"./src/3d.ts\",\n      \"import\": \"./src/3d.ts\"\n    },\n    \"./testing\": {\n      \"types\": \"./src/testing.ts\",\n      \"import\": \"./src/testing.ts\"\n    },\n    \"./runtime-plugin\": {\n      \"types\": \"./src/runtime-plugin.ts\",\n      \"import\": \"./src/runtime-plugin.ts\"\n    },\n    \"./runtime-plugin-support\": {\n      \"types\": \"./src/runtime-plugin-support.ts\",\n      \"import\": \"./src/runtime-plugin-support.ts\"\n    },\n    \"./parser.worker\": {\n      \"types\": \"./src/lib/tree-sitter/parser.worker.ts\",\n      \"import\": \"./src/lib/tree-sitter/parser.worker.ts\"\n    }\n  },\n  \"engines\": {\n    \"bun\": \">=1.3.0\"\n  }\n}\n"
  },
  {
    "path": "packages/core/scripts/build.ts",
    "content": "import { spawnSync, type SpawnSyncReturns } from \"node:child_process\"\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from \"fs\"\nimport { dirname, join, resolve } from \"path\"\nimport { fileURLToPath } from \"url\"\nimport process from \"process\"\nimport path from \"path\"\n\ninterface Variant {\n  platform: string\n  arch: string\n}\n\ninterface PackageJson {\n  name: string\n  version: string\n  license?: string\n  repository?: any\n  description?: string\n  homepage?: string\n  author?: string\n  bugs?: any\n  keywords?: string[]\n  module?: string\n  main?: string\n  types?: string\n  type?: string\n  dependencies?: Record<string, string>\n  devDependencies?: Record<string, string>\n  optionalDependencies?: Record<string, string>\n  peerDependencies?: Record<string, string>\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = resolve(__dirname, \"..\")\nconst licensePath = path.resolve(__dirname, \"../../../LICENSE\")\nconst packageJson: PackageJson = JSON.parse(readFileSync(join(rootDir, \"package.json\"), \"utf8\"))\n\nconst args = process.argv.slice(2)\nconst buildLib = args.find((arg) => arg === \"--lib\")\nconst buildNative = args.find((arg) => arg === \"--native\")\nconst isDev = args.includes(\"--dev\")\nconst buildAll = args.includes(\"--all\") // Build for all platforms\nconst gpaSafeStats = args.includes(\"--gpa-safe-stats\")\n\nconst variants: Variant[] = [\n  { platform: \"darwin\", arch: \"x64\" },\n  { platform: \"darwin\", arch: \"arm64\" },\n  { platform: \"linux\", arch: \"x64\" },\n  { platform: \"linux\", arch: \"arm64\" },\n  { platform: \"win32\", arch: \"x64\" },\n  { platform: \"win32\", arch: \"arm64\" },\n]\n\nif (!buildLib && !buildNative) {\n  console.error(\"Error: Please specify --lib, --native, or both\")\n  process.exit(1)\n}\n\nconst getZigTarget = (platform: string, arch: string): string => {\n  const platformMap: Record<string, string> = { darwin: \"macos\", win32: \"windows\", linux: \"linux\" }\n  const archMap: Record<string, string> = { x64: \"x86_64\", arm64: \"aarch64\" }\n  return `${archMap[arch] ?? arch}-${platformMap[platform] ?? platform}`\n}\n\nconst replaceLinks = (text: string): string => {\n  return packageJson.homepage\n    ? text.replace(\n        /(\\[.*?\\]\\()(\\.\\/.*?\\))/g,\n        (_, p1: string, p2: string) => `${p1}${packageJson.homepage}/blob/HEAD/${p2.replace(\"./\", \"\")}`,\n      )\n    : text\n}\n\nconst requiredFields: (keyof PackageJson)[] = [\"name\", \"version\", \"license\", \"repository\", \"description\"]\nconst missingRequired = requiredFields.filter((field) => !packageJson[field])\nif (missingRequired.length > 0) {\n  console.error(`Error: Missing required fields in package.json: ${missingRequired.join(\", \")}`)\n  process.exit(1)\n}\n\nif (buildNative) {\n  console.log(`Building native ${isDev ? \"dev\" : \"prod\"} binaries${buildAll ? \" for all platforms\" : \"\"}...`)\n\n  const zigArgs = [\"build\", `-Doptimize=${isDev ? \"Debug\" : \"ReleaseFast\"}`]\n  if (buildAll) {\n    zigArgs.push(\"-Dall\")\n  }\n  if (gpaSafeStats) {\n    zigArgs.push(\"-Dgpa-safe-stats=true\")\n  }\n\n  const zigBuild: SpawnSyncReturns<Buffer> = spawnSync(\"zig\", zigArgs, {\n    cwd: join(rootDir, \"src\", \"zig\"),\n    stdio: \"inherit\",\n  })\n\n  if (zigBuild.error) {\n    console.error(\"Error: Zig is not installed or not in PATH\")\n    process.exit(1)\n  }\n\n  if (zigBuild.status !== 0) {\n    console.error(\"Error: Zig build failed\")\n    process.exit(1)\n  }\n\n  for (const { platform, arch } of variants) {\n    const nativeName = `${packageJson.name}-${platform}-${arch}`\n    const nativeDir = join(rootDir, \"node_modules\", nativeName)\n    const libDir = join(rootDir, \"src\", \"zig\", \"lib\", getZigTarget(platform, arch))\n\n    rmSync(nativeDir, { recursive: true, force: true })\n    mkdirSync(nativeDir, { recursive: true })\n\n    let copiedFiles = 0\n    let libraryFileName: string | null = null\n    for (const name of [\"libopentui\", \"opentui\"]) {\n      for (const ext of [\".so\", \".dll\", \".dylib\"]) {\n        const src = join(libDir, `${name}${ext}`)\n        if (existsSync(src)) {\n          const fileName = `${name}${ext}`\n          copyFileSync(src, join(nativeDir, fileName))\n          copiedFiles++\n          if (!libraryFileName) {\n            libraryFileName = fileName\n          }\n        }\n      }\n    }\n\n    if (copiedFiles === 0) {\n      // Skip platforms that weren't built\n      console.log(`Skipping ${platform}-${arch}: no libraries found`)\n      rmSync(nativeDir, { recursive: true, force: true })\n      continue\n    }\n\n    const indexTsContent = `const module = await import(\"./${libraryFileName}\", { with: { type: \"file\" } })\nconst path = module.default\nexport default path;\n`\n    writeFileSync(join(nativeDir, \"index.ts\"), indexTsContent)\n\n    writeFileSync(\n      join(nativeDir, \"package.json\"),\n      JSON.stringify(\n        {\n          name: nativeName,\n          version: packageJson.version,\n          description: `Prebuilt ${platform}-${arch} binaries for ${packageJson.name}`,\n          main: \"index.ts\",\n          types: \"index.ts\",\n          license: packageJson.license,\n          author: packageJson.author,\n          homepage: packageJson.homepage,\n          repository: packageJson.repository,\n          bugs: packageJson.bugs,\n          keywords: [...(packageJson.keywords ?? []), \"prebuild\", \"prebuilt\"],\n          os: [platform],\n          cpu: [arch],\n        },\n        null,\n        2,\n      ),\n    )\n\n    writeFileSync(\n      join(nativeDir, \"README.md\"),\n      replaceLinks(`## ${nativeName}\\n\\n> Prebuilt ${platform}-${arch} binaries for \\`${packageJson.name}\\`.`),\n    )\n\n    if (existsSync(licensePath)) copyFileSync(licensePath, join(nativeDir, \"LICENSE\"))\n    console.log(\"Built:\", nativeName)\n  }\n}\n\nif (buildLib) {\n  console.log(\"Building library...\")\n\n  const distDir = join(rootDir, \"dist\")\n  rmSync(distDir, { recursive: true, force: true })\n  mkdirSync(distDir, { recursive: true })\n\n  const externalDeps: string[] = [\n    ...Object.keys(packageJson.optionalDependencies || {}),\n    ...Object.keys(packageJson.peerDependencies || {}),\n  ]\n\n  // Build main entry point\n  if (!packageJson.module) {\n    console.error(\"Error: 'module' field not found in package.json\")\n    process.exit(1)\n  }\n\n  const entryPoints: string[] = [\n    packageJson.module,\n    \"src/3d.ts\",\n    \"src/testing.ts\",\n    \"src/runtime-plugin.ts\",\n    \"src/runtime-plugin-support.ts\",\n  ]\n\n  // Build main entry points with code splitting\n  // External patterns to prevent bundling tree-sitter assets and default-parsers\n  // to allow standalone executables to work\n  const externalPatterns = [\n    ...externalDeps,\n    \"*.wasm\",\n    \"*.scm\",\n    \"./lib/tree-sitter/assets/*\",\n    \"./lib/tree-sitter/default-parsers\",\n    \"./lib/tree-sitter/default-parsers.ts\",\n  ]\n\n  spawnSync(\n    \"bun\",\n    [\n      \"build\",\n      \"--target=bun\",\n      \"--splitting\",\n      \"--outdir=dist\",\n      \"--sourcemap\",\n      ...externalPatterns.flatMap((dep) => [\"--external\", dep]),\n      ...entryPoints,\n    ],\n    {\n      cwd: rootDir,\n      stdio: \"inherit\",\n    },\n  )\n\n  // Build parser worker as standalone bundle (no splitting) so it can be loaded as a Worker\n  // Make web-tree-sitter external so it loads from node_modules with its WASM file\n  spawnSync(\n    \"bun\",\n    [\n      \"build\",\n      \"--target=bun\",\n      \"--outdir=dist\",\n      \"--sourcemap\",\n      ...externalDeps.flatMap((dep) => [\"--external\", dep]),\n      \"--external\",\n      \"web-tree-sitter\",\n      \"src/lib/tree-sitter/parser.worker.ts\",\n    ],\n    {\n      cwd: rootDir,\n      stdio: \"inherit\",\n    },\n  )\n\n  // Post-process to fix Bun's duplicate export issue\n  // See: https://github.com/oven-sh/bun/issues/5344\n  // and: https://github.com/oven-sh/bun/issues/10631\n  console.log(\"Post-processing bundled files to fix duplicate exports...\")\n  const bundledFiles = [\n    \"dist/index.js\",\n    \"dist/3d.js\",\n    \"dist/testing.js\",\n    \"dist/runtime-plugin.js\",\n    \"dist/runtime-plugin-support.js\",\n    \"dist/lib/tree-sitter/parser.worker.js\",\n  ]\n  for (const filePath of bundledFiles) {\n    const fullPath = join(rootDir, filePath)\n    if (existsSync(fullPath)) {\n      let content = readFileSync(fullPath, \"utf8\")\n      const helperExportPattern = /^export\\s*\\{([^}]*(?:__toESM|__commonJS|__export|__require)[^}]*)\\};\\s*$/gm\n\n      let modified = false\n      content = content.replace(helperExportPattern, (match, exports) => {\n        const exportsList = exports.split(\",\").map((e: string) => e.trim())\n        const helpers = [\"__toESM\", \"__commonJS\", \"__export\", \"__require\"]\n        const nonHelpers = exportsList.filter((e: string) => !helpers.includes(e))\n\n        if (nonHelpers.length > 0) {\n          modified = true\n          const helperExports = exportsList.filter((e: string) => helpers.includes(e))\n          return `export { ${helperExports.join(\", \")} };`\n        }\n        return match\n      })\n\n      if (modified) {\n        writeFileSync(fullPath, content)\n        console.log(`  Fixed duplicate exports in ${filePath}`)\n      }\n    }\n  }\n\n  console.log(\"Generating TypeScript declarations...\")\n\n  const tsconfigBuildPath = join(rootDir, \"tsconfig.build.json\")\n\n  const tscResult: SpawnSyncReturns<Buffer> = spawnSync(\"bunx\", [\"tsc\", \"-p\", tsconfigBuildPath], {\n    cwd: rootDir,\n    stdio: \"inherit\",\n  })\n\n  if (tscResult.status !== 0) {\n    console.error(\"Error: TypeScript declaration generation failed\")\n    process.exit(1)\n  } else {\n    console.log(\"TypeScript declarations generated\")\n  }\n\n  const treeSitterSrcDir = join(rootDir, \"src\", \"lib\", \"tree-sitter\")\n\n  const copyAssets = (src: string, dest: string) => {\n    mkdirSync(dest, { recursive: true })\n    const entries = readdirSync(src, { withFileTypes: true })\n    for (const entry of entries) {\n      const srcPath = join(src, entry.name)\n      const destPath = join(dest, entry.name)\n      if (entry.isDirectory()) {\n        copyAssets(srcPath, destPath)\n      } else if (entry.isFile() && (entry.name.endsWith(\".wasm\") || entry.name.endsWith(\".scm\"))) {\n        copyFileSync(srcPath, destPath)\n      }\n    }\n  }\n\n  copyAssets(join(treeSitterSrcDir, \"assets\"), join(distDir, \"assets\"))\n  console.log(\"  Copied tree-sitter assets (*.wasm, *.scm) to dist/assets/\")\n\n  // Configure exports for multiple entry points\n  const exports = {\n    \".\": {\n      import: \"./index.js\",\n      require: \"./index.js\",\n      types: \"./index.d.ts\",\n    },\n    \"./3d\": {\n      import: \"./3d.js\",\n      require: \"./3d.js\",\n      types: \"./3d.d.ts\",\n    },\n    \"./testing\": {\n      import: \"./testing.js\",\n      require: \"./testing.js\",\n      types: \"./testing.d.ts\",\n    },\n    \"./runtime-plugin\": {\n      import: \"./runtime-plugin.js\",\n      require: \"./runtime-plugin.js\",\n      types: \"./runtime-plugin.d.ts\",\n    },\n    \"./runtime-plugin-support\": {\n      import: \"./runtime-plugin-support.js\",\n      require: \"./runtime-plugin-support.js\",\n      types: \"./runtime-plugin-support.d.ts\",\n    },\n    \"./parser.worker\": {\n      import: \"./lib/tree-sitter/parser.worker.js\",\n      require: \"./lib/tree-sitter/parser.worker.js\",\n      types: \"./lib/tree-sitter/parser.worker.d.ts\",\n    },\n  }\n\n  const optionalDeps: Record<string, string> = Object.fromEntries(\n    variants.map(({ platform, arch }) => [`${packageJson.name}-${platform}-${arch}`, packageJson.version]),\n  )\n\n  writeFileSync(\n    join(distDir, \"package.json\"),\n    JSON.stringify(\n      {\n        name: packageJson.name,\n        module: \"index.js\",\n        main: \"index.js\",\n        types: \"index.d.ts\",\n        type: packageJson.type,\n        version: packageJson.version,\n        description: packageJson.description,\n        keywords: packageJson.keywords,\n        license: packageJson.license,\n        author: packageJson.author,\n        homepage: packageJson.homepage,\n        repository: packageJson.repository,\n        bugs: packageJson.bugs,\n        exports,\n        dependencies: packageJson.dependencies,\n        devDependencies: packageJson.devDependencies,\n        peerDependencies: packageJson.peerDependencies,\n        optionalDependencies: {\n          ...packageJson.optionalDependencies,\n          ...optionalDeps,\n        },\n      },\n      null,\n      2,\n    ),\n  )\n\n  writeFileSync(join(distDir, \"README.md\"), replaceLinks(readFileSync(join(rootDir, \"README.md\"), \"utf8\")))\n  if (existsSync(licensePath)) copyFileSync(licensePath, join(distDir, \"LICENSE\"))\n\n  console.log(\"Library built at:\", distDir)\n}\n"
  },
  {
    "path": "packages/core/scripts/publish.ts",
    "content": "import { spawnSync, type SpawnSyncReturns } from \"node:child_process\"\nimport { existsSync, readFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport process from \"node:process\"\nimport { fileURLToPath } from \"node:url\"\n\ninterface PackageJson {\n  name: string\n  version: string\n  optionalDependencies?: Record<string, string>\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = resolve(__dirname, \"..\")\n\nconst packageJson: PackageJson = JSON.parse(readFileSync(join(rootDir, \"package.json\"), \"utf8\"))\n\nconsole.log(`Publishing @opentui/core@${packageJson.version}...`)\nconsole.log(\"Make sure you've run the pre-publish validation script first!\")\n\nconst libDir = join(rootDir, \"dist\")\nconst packageJsons: Record<string, PackageJson> = {\n  [libDir]: JSON.parse(readFileSync(join(libDir, \"package.json\"), \"utf8\")),\n}\n\n// Load all native package.json files\nfor (const pkgName of Object.keys(packageJsons[libDir].optionalDependencies!).filter((x) =>\n  x.startsWith(packageJson.name),\n)) {\n  const nativeDir = join(rootDir, \"node_modules\", pkgName)\n  packageJsons[nativeDir] = JSON.parse(readFileSync(join(nativeDir, \"package.json\"), \"utf8\"))\n}\n\n// Publish all packages (main + native packages)\nObject.entries(packageJsons).forEach(([dir, { name, version }]) => {\n  console.log(`\\nPublishing ${name}@${version}...`)\n\n  const isSnapshot = version.includes(\"-snapshot\") || /^0\\.0\\.0-\\d{8}-[a-f0-9]{8}$/.test(version)\n  const publishArgs = [\"publish\", \"--access=public\"]\n\n  if (isSnapshot) {\n    publishArgs.push(\"--tag\", \"snapshot\")\n    console.log(`  Publishing as snapshot (--tag snapshot)`)\n  }\n\n  const publish: SpawnSyncReturns<Buffer> = spawnSync(\"npm\", publishArgs, {\n    cwd: dir,\n    stdio: \"inherit\",\n  })\n\n  if (publish.status !== 0) {\n    console.error(`Failed to publish '${name}@${version}'.`)\n    process.exit(1)\n  }\n\n  console.log(`Successfully published '${name}@${version}'`)\n})\n\nconsole.log(`\\nAll @opentui/core packages published successfully!`)\n"
  },
  {
    "path": "packages/core/src/3d/SpriteResourceManager.ts",
    "content": "import * as THREE from \"three\"\nimport { TextureUtils } from \"./TextureUtils.js\"\nimport type { Scene } from \"three\"\n\nexport interface ResourceConfig {\n  imagePath: string\n  sheetNumFrames: number\n}\n\nexport interface SheetProperties {\n  imagePath: string\n  sheetTilesetWidth: number\n  sheetTilesetHeight: number\n  sheetNumFrames: number\n}\n\nexport interface InstanceManagerOptions {\n  maxInstances: number\n  renderOrder?: number\n  depthWrite?: boolean\n  name?: string\n  frustumCulled?: boolean\n  matrix?: THREE.Matrix4\n}\n\nexport interface MeshPoolOptions {\n  geometry: () => THREE.BufferGeometry\n  material: THREE.Material\n  maxInstances: number\n  name?: string\n}\n\nconst HIDDEN_MATRIX = new THREE.Matrix4().scale(new THREE.Vector3(0, 0, 0))\n\nexport class MeshPool {\n  private pools: Map<string, THREE.InstancedMesh[]> = new Map()\n\n  public acquireMesh(poolId: string, options: MeshPoolOptions): THREE.InstancedMesh {\n    const poolArray = this.pools.get(poolId) ?? []\n    this.pools.set(poolId, poolArray)\n\n    if (poolArray.length > 0) {\n      const mesh = poolArray.pop()!\n      mesh.material = options.material\n      mesh.count = options.maxInstances\n      return mesh\n    }\n\n    const mesh = new THREE.InstancedMesh(options.geometry(), options.material, options.maxInstances)\n\n    if (options.name) {\n      mesh.name = options.name\n    }\n\n    return mesh\n  }\n\n  public releaseMesh(poolId: string, mesh: THREE.InstancedMesh): void {\n    const poolArray = this.pools.get(poolId) ?? []\n    poolArray.push(mesh)\n    this.pools.set(poolId, poolArray)\n  }\n\n  public fill(poolId: string, options: MeshPoolOptions, count: number): void {\n    const poolArray = this.pools.get(poolId) ?? []\n    this.pools.set(poolId, poolArray)\n\n    for (let i = 0; i < count; i++) {\n      const mesh = new THREE.InstancedMesh(options.geometry(), options.material, options.maxInstances)\n\n      if (options.name) {\n        mesh.name = `${options.name}_${i}`\n      }\n\n      poolArray.push(mesh)\n    }\n  }\n\n  public clearPool(poolId: string): void {\n    const poolArray = this.pools.get(poolId)\n    if (poolArray) {\n      poolArray.forEach((mesh) => {\n        mesh.geometry.dispose()\n        if (Array.isArray(mesh.material)) {\n          mesh.material.forEach((mat) => mat.dispose())\n        } else {\n          mesh.material.dispose()\n        }\n      })\n      poolArray.length = 0\n    }\n  }\n\n  public clearAllPools(): void {\n    for (const poolId of this.pools.keys()) {\n      this.clearPool(poolId)\n    }\n    this.pools.clear()\n  }\n}\n\nexport class InstanceManager {\n  private scene: Scene\n  private instancedMesh: THREE.InstancedMesh\n  private material: THREE.Material\n  private maxInstances: number\n  private _freeIndices: number[] = []\n  private instanceCount: number = 0\n  private _matrix: THREE.Matrix4\n\n  constructor(scene: Scene, geometry: THREE.BufferGeometry, material: THREE.Material, options: InstanceManagerOptions) {\n    this.scene = scene\n    this.material = material\n    this.maxInstances = options.maxInstances\n    this._matrix = options.matrix ?? HIDDEN_MATRIX\n\n    this.instancedMesh = new THREE.InstancedMesh(geometry, material, this.maxInstances)\n    this.instancedMesh.renderOrder = options.renderOrder ?? 0\n    this.instancedMesh.frustumCulled = options.frustumCulled ?? false\n    this.instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)\n\n    if (options.name) {\n      this.instancedMesh.name = options.name\n    }\n\n    for (let i = 0; i < this.maxInstances; i++) {\n      this._freeIndices.push(i)\n      this.instancedMesh.setMatrixAt(i, this._matrix)\n    }\n    this.instancedMesh.instanceMatrix.needsUpdate = true\n\n    this.scene.add(this.instancedMesh)\n  }\n\n  acquireInstanceSlot(): number {\n    if (this._freeIndices.length === 0) {\n      throw new Error(`[InstanceManager] Max instances (${this.maxInstances}) reached. Cannot acquire slot.`)\n    }\n    const instanceIndex = this._freeIndices.pop()!\n    this.instanceCount++\n    return instanceIndex\n  }\n\n  releaseInstanceSlot(instanceIndex: number): void {\n    if (instanceIndex >= 0 && instanceIndex < this.maxInstances) {\n      this.instancedMesh.setMatrixAt(instanceIndex, this._matrix)\n      this.instancedMesh.instanceMatrix.needsUpdate = true\n\n      if (!this._freeIndices.includes(instanceIndex)) {\n        this._freeIndices.push(instanceIndex)\n        this._freeIndices.sort((a, b) => a - b)\n        this.instanceCount--\n      }\n    } else {\n      console.warn(`[InstanceManager] Attempted to release invalid instanceIndex ${instanceIndex}`)\n    }\n  }\n\n  getInstanceCount(): number {\n    return this.instanceCount\n  }\n\n  getMaxInstances(): number {\n    return this.maxInstances\n  }\n\n  get hasFreeIndices(): boolean {\n    return this._freeIndices.length > 0\n  }\n\n  get mesh(): THREE.InstancedMesh {\n    return this.instancedMesh\n  }\n\n  dispose(): void {\n    this.scene.remove(this.instancedMesh)\n    this.instancedMesh.geometry.dispose()\n    if (Array.isArray(this.material)) {\n      this.material.forEach((mat) => mat.dispose())\n    } else {\n      this.material.dispose()\n    }\n  }\n}\n\nexport class SpriteResource {\n  private _texture: THREE.DataTexture\n  private _sheetProperties: SheetProperties\n  private scene: Scene\n  private _meshPool: MeshPool\n\n  constructor(texture: THREE.DataTexture, sheetProperties: SheetProperties, scene: Scene) {\n    this._texture = texture\n    this._sheetProperties = sheetProperties\n    this.scene = scene\n    this._meshPool = new MeshPool()\n  }\n\n  public get texture(): THREE.DataTexture {\n    return this._texture\n  }\n\n  public get sheetProperties(): SheetProperties {\n    return this._sheetProperties\n  }\n\n  public get meshPool(): MeshPool {\n    return this._meshPool\n  }\n\n  public createInstanceManager(\n    geometry: THREE.BufferGeometry,\n    material: THREE.Material,\n    options: InstanceManagerOptions,\n  ): InstanceManager {\n    const managerOptions = {\n      ...options,\n      name: options.name ?? `InstancedSprites_${this._sheetProperties.imagePath.replace(/[^a-zA-Z0-9_]/g, \"_\")}`,\n    }\n\n    return new InstanceManager(this.scene, geometry, material, managerOptions)\n  }\n\n  public get uvTileSize(): THREE.Vector2 {\n    const uvTileWidth = 1.0 / this._sheetProperties.sheetNumFrames\n    const uvTileHeight = 1.0\n    return new THREE.Vector2(uvTileWidth, uvTileHeight)\n  }\n\n  public dispose(): void {\n    this._meshPool.clearAllPools()\n  }\n}\n\nexport class SpriteResourceManager {\n  private resources: Map<string, SpriteResource> = new Map()\n  private textureCache: Map<string, THREE.DataTexture> = new Map()\n  private scene: Scene\n\n  constructor(scene: Scene) {\n    this.scene = scene\n  }\n\n  private getResourceKey(sheetProps: SheetProperties): string {\n    return sheetProps.imagePath\n  }\n\n  public async getOrCreateResource(texture: THREE.DataTexture, sheetProps: SheetProperties): Promise<SpriteResource> {\n    const resourceKey = this.getResourceKey(sheetProps)\n    let resource = this.resources.get(resourceKey)\n\n    if (!resource) {\n      resource = new SpriteResource(texture, sheetProps, this.scene)\n      this.resources.set(resourceKey, resource)\n    }\n\n    return resource\n  }\n\n  public async createResource(config: ResourceConfig): Promise<SpriteResource> {\n    let texture = this.textureCache.get(config.imagePath)\n    if (!texture) {\n      const loadedTexture = await TextureUtils.fromFile(config.imagePath)\n      if (!loadedTexture) {\n        throw new Error(`[SpriteResourceManager] Failed to load texture for ${config.imagePath}`)\n      }\n      loadedTexture.needsUpdate = true\n      texture = loadedTexture\n      this.textureCache.set(config.imagePath, texture)\n    }\n\n    const sheetProps: SheetProperties = {\n      imagePath: config.imagePath,\n      sheetTilesetWidth: texture.image.width,\n      sheetTilesetHeight: texture.image.height,\n      sheetNumFrames: config.sheetNumFrames,\n    }\n\n    return await this.getOrCreateResource(texture, sheetProps)\n  }\n\n  public clearCache(): void {\n    this.resources.clear()\n    this.textureCache.clear()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/SpriteUtils.ts",
    "content": "import { TextureUtils } from \"./TextureUtils.js\"\nimport { Sprite, SpriteMaterial, DataTexture, type SpriteMaterialParameters } from \"three\"\n\nexport class SheetSprite extends Sprite {\n  private _frameIndex: number = 0\n  private _numFrames: number = 0\n\n  constructor(material: SpriteMaterial, numFrames: number) {\n    super(material)\n    this._numFrames = numFrames\n    this.setIndex(0)\n  }\n\n  setIndex = (index: number) => {\n    this._frameIndex = index\n    this.material.map?.repeat.set(1 / this._numFrames, 1)\n    this.material.map?.offset.set(this._frameIndex / this._numFrames, 0)\n  }\n}\n\nexport class SpriteUtils {\n  static async fromFile(\n    path: string,\n    {\n      materialParameters = {\n        alphaTest: 0.1,\n        depthWrite: true,\n      },\n    }: {\n      materialParameters?: Omit<SpriteMaterialParameters, \"map\">\n    } = {},\n  ): Promise<Sprite> {\n    const texture = await TextureUtils.fromFile(path)\n    if (!texture) {\n      throw new Error(`Failed to load sprite texture from ${path}`)\n    }\n\n    const spriteMaterial = new SpriteMaterial({ map: texture, ...materialParameters })\n    const sprite = new Sprite(spriteMaterial)\n\n    const textureAspectRatio = texture.image.width / texture.image.height\n\n    sprite.updateMatrix = function () {\n      this.matrix.compose(this.position, this.quaternion, this.scale.clone().setX(this.scale.x * textureAspectRatio))\n    }\n\n    return sprite\n  }\n\n  static async sheetFromFile(path: string, numFrames: number): Promise<SheetSprite> {\n    const spriteTexture = await TextureUtils.fromFile(path)\n\n    if (!spriteTexture) {\n      console.error(\"Failed to load sprite texture, exiting.\")\n      process.exit(1)\n    }\n\n    const spriteMaterial = new SpriteMaterial({ map: spriteTexture })\n    const sprite = new SheetSprite(spriteMaterial, numFrames)\n\n    const singleFrameWidth = spriteTexture.image.width / numFrames\n    const singleFrameHeight = spriteTexture.image.height\n    const frameAspectRatio = singleFrameWidth / singleFrameHeight\n\n    sprite.updateMatrix = function () {\n      this.matrix.compose(this.position, this.quaternion, this.scale.clone().setX(this.scale.x * frameAspectRatio))\n    }\n\n    return sprite\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/TextureUtils.ts",
    "content": "import { readFile } from \"node:fs/promises\"\nimport { Color, Texture, DataTexture, NearestFilter, ClampToEdgeWrapping, RGBAFormat, UnsignedByteType } from \"three\"\nimport { Jimp } from \"jimp\"\n\ninterface SimpleImageData {\n  data: Uint8ClampedArray\n  width: number\n  height: number\n}\n\n// Utility class for loading and generating THREE.Texture instances\nexport class TextureUtils {\n  /**\n   * Loads a texture from a file path using sharp.\n   * Returns a THREE.Texture with ImageData attached to its .image property.\n   */\n  static async loadTextureFromFile(path: string): Promise<DataTexture | null> {\n    try {\n      const buffer = await readFile(path)\n      const image = await Jimp.read(buffer)\n\n      image.flip({ horizontal: false, vertical: true })\n\n      const texture = new DataTexture(\n        image.bitmap.data,\n        image.bitmap.width,\n        image.bitmap.height,\n        RGBAFormat,\n        UnsignedByteType,\n      )\n      texture.needsUpdate = true\n      texture.format = RGBAFormat\n      texture.magFilter = NearestFilter\n      texture.minFilter = NearestFilter\n      texture.wrapS = ClampToEdgeWrapping\n      texture.wrapT = ClampToEdgeWrapping\n      texture.flipY = false // Usually true for webGL, but our sampler flips V\n\n      return texture\n    } catch (error) {\n      console.error(`Failed to load texture from ${path}:`, error)\n      return null\n    }\n  }\n\n  /**\n   * Alias for loadTextureFromFile for convenience.\n   */\n  static async fromFile(path: string): Promise<DataTexture | null> {\n    return this.loadTextureFromFile(path)\n  }\n\n  /**\n   * Creates a THREE.Texture with a checkerboard pattern.\n   */\n  static createCheckerboard(\n    size: number = 256,\n    color1: Color = new Color(1.0, 1.0, 1.0),\n    color2: Color = new Color(0.0, 0.0, 0.0),\n    checkSize: number = 32,\n  ): Texture {\n    const data = new Uint8ClampedArray(size * size * 4)\n\n    for (let y = 0; y < size; y++) {\n      for (let x = 0; x < size; x++) {\n        const isEvenX = Math.floor(x / checkSize) % 2 === 0\n        const isEvenY = Math.floor(y / checkSize) % 2 === 0\n        const color = (isEvenX && isEvenY) || (!isEvenX && !isEvenY) ? color1 : color2\n\n        const index = (y * size + x) * 4\n        data[index] = Math.floor(color.r * 255)\n        data[index + 1] = Math.floor(color.g * 255)\n        data[index + 2] = Math.floor(color.b * 255)\n        data[index + 3] = 255 // Alpha\n      }\n    }\n\n    const imageData: SimpleImageData = { data, width: size, height: size }\n    const texture = new DataTexture(data, size, size, RGBAFormat, UnsignedByteType)\n    texture.needsUpdate = true\n    texture.format = RGBAFormat\n    texture.magFilter = NearestFilter\n    texture.minFilter = NearestFilter\n    texture.wrapS = ClampToEdgeWrapping\n    texture.wrapT = ClampToEdgeWrapping\n    texture.flipY = false\n\n    return texture\n  }\n\n  /**\n   * Creates a THREE.Texture with a gradient pattern.\n   */\n  static createGradient(\n    size: number = 256,\n    startColor: Color = new Color(1.0, 0.0, 0.0),\n    endColor: Color = new Color(0.0, 0.0, 1.0),\n    direction: \"horizontal\" | \"vertical\" | \"radial\" = \"vertical\",\n  ): Texture {\n    const data = new Uint8ClampedArray(size * size * 4)\n\n    for (let y = 0; y < size; y++) {\n      for (let x = 0; x < size; x++) {\n        let t = 0\n\n        if (direction === \"horizontal\") {\n          t = x / (size - 1)\n        } else if (direction === \"vertical\") {\n          t = y / (size - 1)\n        } else if (direction === \"radial\") {\n          const dx = x - size / 2\n          const dy = y - size / 2\n          t = Math.min(1, Math.sqrt(dx * dx + dy * dy) / (size / 2))\n        }\n\n        const r = startColor.r * (1 - t) + endColor.r * t\n        const g = startColor.g * (1 - t) + endColor.g * t\n        const b = startColor.b * (1 - t) + endColor.b * t\n\n        const index = (y * size + x) * 4\n        data[index] = Math.floor(r * 255)\n        data[index + 1] = Math.floor(g * 255)\n        data[index + 2] = Math.floor(b * 255)\n        data[index + 3] = 255 // Alpha\n      }\n    }\n\n    const imageData: SimpleImageData = { data, width: size, height: size }\n    const texture = new DataTexture(data, size, size, RGBAFormat, UnsignedByteType)\n    texture.needsUpdate = true\n    texture.format = RGBAFormat\n    texture.magFilter = NearestFilter\n    texture.minFilter = NearestFilter\n    texture.wrapS = ClampToEdgeWrapping\n    texture.wrapT = ClampToEdgeWrapping\n    texture.flipY = false\n\n    return texture\n  }\n\n  /**\n   * Creates a THREE.Texture with a procedural noise pattern.\n   */\n  static createNoise(\n    size: number = 256,\n    scale: number = 1,\n    octaves: number = 1,\n    color1: Color = new Color(1.0, 1.0, 1.0),\n    color2: Color = new Color(0.0, 0.0, 0.0),\n  ): Texture {\n    const data = new Uint8ClampedArray(size * size * 4)\n\n    for (let y = 0; y < size; y++) {\n      for (let x = 0; x < size; x++) {\n        let noise = 0\n        let amplitude = 1\n        let frequency = 1\n\n        for (let o = 0; o < octaves; o++) {\n          const nx = (x * frequency * scale) / size\n          const ny = (y * frequency * scale) / size\n          const sampleX = Math.sin(nx * 12.9898) * 43758.5453\n          const sampleY = Math.cos(ny * 78.233) * 43758.5453\n          const sample = Math.sin(sampleX + sampleY) * 0.5 + 0.5\n          noise += sample * amplitude\n          amplitude *= 0.5\n          frequency *= 2\n        }\n\n        noise = Math.min(1, Math.max(0, noise)) // Normalize\n\n        const r = color1.r * noise + color2.r * (1 - noise)\n        const g = color1.g * noise + color2.g * (1 - noise)\n        const b = color1.b * noise + color2.b * (1 - noise)\n\n        const index = (y * size + x) * 4\n        data[index] = Math.floor(r * 255)\n        data[index + 1] = Math.floor(g * 255)\n        data[index + 2] = Math.floor(b * 255)\n        data[index + 3] = 255 // Alpha\n      }\n    }\n\n    const imageData: SimpleImageData = { data, width: size, height: size }\n    const texture = new DataTexture(data, size, size, RGBAFormat, UnsignedByteType)\n    texture.needsUpdate = true\n    texture.format = RGBAFormat\n    texture.magFilter = NearestFilter\n    texture.minFilter = NearestFilter\n    texture.wrapS = ClampToEdgeWrapping\n    texture.wrapT = ClampToEdgeWrapping\n    texture.flipY = false\n\n    return texture\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/ThreeRenderable.ts",
    "content": "import { OrthographicCamera, PerspectiveCamera, Scene } from \"three\"\n\nimport { OptimizedBuffer } from \"../buffer.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport type { CliRenderer } from \"../renderer.js\"\nimport type { RenderContext } from \"../types.js\"\nimport { ThreeCliRenderer, type ThreeCliRendererOptions } from \"./WGPURenderer.js\"\n\nexport interface ThreeRenderableOptions extends RenderableOptions<ThreeRenderable> {\n  scene?: Scene | null\n  camera?: PerspectiveCamera | OrthographicCamera\n  renderer?: Omit<ThreeCliRendererOptions, \"width\" | \"height\" | \"autoResize\">\n  autoAspect?: boolean\n}\n\nexport class ThreeRenderable extends Renderable {\n  private engine: ThreeCliRenderer\n  private scene: Scene | null\n  private autoAspect: boolean\n  private initPromise: Promise<boolean> | null = null\n  private initFailed: boolean = false\n  private drawInFlight: boolean = false\n  private frameCallback: ((deltaTime: number) => Promise<void>) | null = null\n  private frameCallbackRegistered: boolean = false\n  private cliRenderer: CliRenderer\n  private clearColor: RGBA\n\n  constructor(ctx: RenderContext, options: ThreeRenderableOptions) {\n    const { scene = null, camera, renderer, autoAspect = true, ...renderableOptions } = options\n    super(ctx, { ...renderableOptions, buffered: true, live: options.live ?? true })\n\n    const cliRenderer = ctx as CliRenderer\n    if (typeof cliRenderer.setFrameCallback !== \"function\" || typeof cliRenderer.removeFrameCallback !== \"function\") {\n      throw new Error(\"ThreeRenderable requires a CliRenderer context\")\n    }\n\n    this.cliRenderer = cliRenderer\n    this.scene = scene\n    this.autoAspect = autoAspect\n    this.clearColor = renderer?.backgroundColor ?? RGBA.fromValues(0, 0, 0, 1)\n\n    const { width, height } = this.getRenderSize()\n    this.engine = new ThreeCliRenderer(cliRenderer, {\n      width,\n      height,\n      autoResize: false,\n      ...renderer,\n    })\n\n    if (camera) {\n      this.engine.setActiveCamera(camera)\n    }\n    this.updateCameraAspect(width, height)\n\n    this.registerFrameCallback()\n  }\n\n  public get aspectRatio(): number {\n    return this.getAspectRatio(this.width, this.height)\n  }\n\n  public get renderer(): ThreeCliRenderer {\n    return this.engine\n  }\n\n  public getScene(): Scene | null {\n    return this.scene\n  }\n\n  public setScene(scene: Scene | null): void {\n    this.scene = scene\n    this.requestRender()\n  }\n\n  public getActiveCamera(): PerspectiveCamera | OrthographicCamera {\n    return this.engine.getActiveCamera()\n  }\n\n  public setActiveCamera(camera: PerspectiveCamera | OrthographicCamera): void {\n    this.engine.setActiveCamera(camera)\n    this.updateCameraAspect(this.width, this.height)\n    this.requestRender()\n  }\n\n  public setAutoAspect(autoAspect: boolean): void {\n    if (this.autoAspect === autoAspect) return\n    this.autoAspect = autoAspect\n    if (autoAspect) {\n      this.updateCameraAspect(this.width, this.height)\n    }\n  }\n\n  protected onResize(width: number, height: number): void {\n    if (width > 0 && height > 0) {\n      this.engine.setSize(width, height, true)\n      this.updateCameraAspect(width, height)\n    }\n    super.onResize(width, height)\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    if (!this.visible || this.isDestroyed) return\n    if (this.frameCallbackRegistered) return\n    if (this.buffered && !this.frameBuffer) return\n    void this.renderToBuffer(buffer, deltaTime / 1000)\n  }\n\n  protected destroySelf(): void {\n    if (this.frameCallback && this.frameCallbackRegistered) {\n      this.cliRenderer.removeFrameCallback(this.frameCallback)\n      this.frameCallbackRegistered = false\n      this.frameCallback = null\n    }\n\n    this.engine.destroy()\n    super.destroySelf()\n  }\n\n  private registerFrameCallback(): void {\n    if (this.frameCallbackRegistered) return\n\n    this.frameCallback = async (deltaTime: number) => {\n      if (this.isDestroyed || !this.visible || !this.parent) return\n      if (!this.scene || !this.frameBuffer) return\n      await this.renderToBuffer(this.frameBuffer, deltaTime / 1000)\n    }\n\n    this.cliRenderer.setFrameCallback(this.frameCallback)\n    this.frameCallbackRegistered = true\n  }\n\n  private async renderToBuffer(buffer: OptimizedBuffer, deltaTime: number): Promise<void> {\n    if (!this.scene || this.isDestroyed || this.drawInFlight) return\n\n    this.drawInFlight = true\n    try {\n      const initialized = await this.ensureInitialized()\n      if (!initialized || !this.scene) return\n\n      if (buffer === this.frameBuffer) {\n        buffer.clear(this.clearColor)\n      }\n\n      await this.engine.drawScene(this.scene, buffer, deltaTime)\n    } finally {\n      this.drawInFlight = false\n    }\n  }\n\n  private async ensureInitialized(): Promise<boolean> {\n    if (this.initFailed) return false\n    if (!this.initPromise) {\n      this.initPromise = this.engine\n        .init()\n        .then(() => true)\n        .catch((error) => {\n          this.initFailed = true\n          console.error(\"ThreeRenderable init failed:\", error)\n          return false\n        })\n    }\n    return this.initPromise\n  }\n\n  private updateCameraAspect(width: number, height: number): void {\n    if (!this.autoAspect || width <= 0 || height <= 0) return\n\n    const camera = this.engine.getActiveCamera()\n    if (camera instanceof PerspectiveCamera) {\n      camera.aspect = this.getAspectRatio(width, height)\n      camera.updateProjectionMatrix()\n    }\n  }\n\n  private getAspectRatio(width: number, height: number): number {\n    if (width <= 0 || height <= 0) return 1\n\n    const resolution = this.cliRenderer.resolution\n    if (resolution && this.cliRenderer.terminalWidth > 0 && this.cliRenderer.terminalHeight > 0) {\n      const cellWidth = resolution.width / this.cliRenderer.terminalWidth\n      const cellHeight = resolution.height / this.cliRenderer.terminalHeight\n      if (cellHeight > 0) {\n        return (width * cellWidth) / (height * cellHeight)\n      }\n    }\n\n    return width / (height * 2)\n  }\n\n  private getRenderSize(): { width: number; height: number } {\n    return {\n      width: Math.max(1, this.width),\n      height: Math.max(1, this.height),\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/WGPURenderer.ts",
    "content": "import { PerspectiveCamera, OrthographicCamera, Color, NoToneMapping, LinearSRGBColorSpace, Scene } from \"three\"\nimport { WebGPURenderer } from \"three/webgpu\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { createWebGPUDevice, setupGlobals } from \"bun-webgpu\"\nimport { CLICanvas, SuperSampleAlgorithm } from \"./canvas.js\"\nimport { CliRenderEvents, type CliRenderer } from \"../renderer.js\"\n\nexport enum SuperSampleType {\n  NONE = \"none\",\n  GPU = \"gpu\",\n  CPU = \"cpu\",\n}\n\nexport interface ThreeCliRendererOptions {\n  width: number\n  height: number\n  focalLength?: number\n  backgroundColor?: RGBA\n  superSample?: SuperSampleType\n  alpha?: boolean\n  autoResize?: boolean\n  libPath?: string\n}\n\nexport class ThreeCliRenderer {\n  private outputWidth: number\n  private outputHeight: number\n  private renderWidth: number\n  private renderHeight: number\n  private superSample: SuperSampleType\n  private backgroundColor: RGBA = RGBA.fromValues(0, 0, 0, 1)\n  private alpha: boolean = false\n  private threeRenderer?: WebGPURenderer\n  private canvas?: CLICanvas\n  private device: GPUDevice | null = null\n\n  private activeCamera: PerspectiveCamera | OrthographicCamera\n  private _aspectRatio: number | null = null\n  private doRenderStats: boolean = false\n\n  private resizeHandler: (width: number, height: number) => void\n  private debugToggleHandler: (enabled: boolean) => void\n  private destroyHandler: () => void\n\n  // Stats tracking\n  private renderTimeMs: number = 0\n  private readbackTimeMs: number = 0\n  private totalDrawTimeMs: number = 0\n\n  private renderMethod: (\n    root: Scene,\n    camera: PerspectiveCamera | OrthographicCamera,\n    buffer: OptimizedBuffer,\n    deltaTime: number,\n  ) => Promise<void> = () => Promise.resolve()\n\n  public get aspectRatio(): number {\n    if (this._aspectRatio) return this._aspectRatio\n    if (this.cliRenderer.resolution) {\n      const pixelAspectRatio = this.cliRenderer.resolution.width / this.cliRenderer.resolution.height\n      return pixelAspectRatio\n    }\n    const terminalWidth = process.stdout.columns\n    const terminalHeight = process.stdout.rows\n    return terminalWidth / (terminalHeight * 2)\n  }\n\n  constructor(\n    private readonly cliRenderer: CliRenderer,\n    options: ThreeCliRendererOptions,\n  ) {\n    this.outputWidth = options.width\n    this.outputHeight = options.height\n    this.superSample = options.superSample ?? SuperSampleType.GPU\n\n    this.renderWidth = this.outputWidth * (this.superSample !== SuperSampleType.NONE ? 2 : 1)\n    this.renderHeight = this.outputHeight * (this.superSample !== SuperSampleType.NONE ? 2 : 1)\n\n    this.backgroundColor = options.backgroundColor ?? RGBA.fromValues(0, 0, 0, 1)\n    this.alpha = options.alpha ?? false\n\n    if (process.env.CELL_ASPECT_RATIO) {\n      this._aspectRatio = parseFloat(process.env.CELL_ASPECT_RATIO)\n    }\n\n    // Create a default active camera\n    const fov = options.focalLength ? 2 * Math.atan(this.outputHeight / (2 * options.focalLength)) * (180 / Math.PI) : 1 // Default FOV if focal length not provided\n    this.activeCamera = new PerspectiveCamera(\n      fov,\n      this.aspectRatio,\n      0.1, // near plane\n      1000, // far plane\n    )\n    this.activeCamera.position.set(0, 0, 3)\n    this.activeCamera.up.set(0, 1, 0)\n    this.activeCamera.lookAt(0, 0, 0)\n    this.activeCamera.updateMatrixWorld()\n\n    this.resizeHandler = (width: number, height: number) => {\n      this.setSize(width, height, true)\n    }\n\n    this.debugToggleHandler = (enabled: boolean) => {\n      this.doRenderStats = enabled\n    }\n\n    this.destroyHandler = () => {\n      this.destroy()\n    }\n\n    if (options.autoResize !== false) {\n      this.cliRenderer.on(\"resize\", this.resizeHandler)\n    }\n\n    this.cliRenderer.on(CliRenderEvents.DEBUG_OVERLAY_TOGGLE, this.debugToggleHandler)\n    this.cliRenderer.on(CliRenderEvents.DESTROY, this.destroyHandler)\n\n    setupGlobals({ libPath: options.libPath })\n  }\n\n  public toggleDebugStats(): void {\n    this.doRenderStats = !this.doRenderStats\n  }\n\n  async init(): Promise<void> {\n    this.device = await createWebGPUDevice()\n    this.canvas = new CLICanvas(this.device, this.renderWidth, this.renderHeight, this.superSample)\n\n    try {\n      this.threeRenderer = new WebGPURenderer({\n        canvas: this.canvas as unknown as HTMLCanvasElement,\n        device: this.device,\n        alpha: this.alpha,\n      })\n\n      this.setBackgroundColor(this.backgroundColor)\n\n      this.threeRenderer.toneMapping = NoToneMapping\n      this.threeRenderer.outputColorSpace = LinearSRGBColorSpace\n\n      this.threeRenderer.setSize(this.renderWidth, this.renderHeight, false)\n    } catch (error) {\n      console.error(\"Error creating THREE.WebGPURenderer:\", error)\n      throw error\n    }\n\n    await this.threeRenderer.init().then(() => {\n      this.renderMethod = this.doDrawScene.bind(this)\n    })\n  }\n\n  public getSuperSampleAlgorithm(): SuperSampleAlgorithm {\n    return this.canvas!.getSuperSampleAlgorithm()\n  }\n\n  public setSuperSampleAlgorithm(superSampleAlgorithm: SuperSampleAlgorithm): void {\n    this.canvas!.setSuperSampleAlgorithm(superSampleAlgorithm)\n  }\n\n  public saveToFile(filePath: string): Promise<void> {\n    return this.canvas!.saveToFile(filePath)\n  }\n\n  setActiveCamera(camera: PerspectiveCamera | OrthographicCamera): void {\n    this.activeCamera = camera\n  }\n\n  getActiveCamera(): PerspectiveCamera | OrthographicCamera {\n    return this.activeCamera\n  }\n\n  public setBackgroundColor(color: RGBA): void {\n    this.backgroundColor = color\n    const clearColor = new Color(this.backgroundColor.r, this.backgroundColor.g, this.backgroundColor.b)\n    const clearAlpha = this.alpha ? this.backgroundColor.a : 1.0\n    this.threeRenderer!.setClearColor(clearColor, clearAlpha)\n  }\n\n  setSize(width: number, height: number, forceUpdate: boolean = false): void {\n    // Check against OUTPUT dimensions\n    if (!forceUpdate && this.outputWidth === width && this.outputHeight === height) return\n\n    this.outputWidth = width\n    this.outputHeight = height\n\n    this.renderWidth = this.outputWidth * (this.superSample !== SuperSampleType.NONE ? 2 : 1)\n    this.renderHeight = this.outputHeight * (this.superSample !== SuperSampleType.NONE ? 2 : 1)\n\n    this.canvas?.setSize(this.renderWidth, this.renderHeight)\n\n    this.threeRenderer?.setSize(this.renderWidth, this.renderHeight, false)\n    this.threeRenderer?.setViewport(0, 0, this.renderWidth, this.renderHeight)\n\n    if (this.activeCamera instanceof PerspectiveCamera) {\n      this.activeCamera.aspect = this.aspectRatio\n    }\n    this.activeCamera.updateProjectionMatrix()\n  }\n\n  public async drawScene(root: Scene, buffer: OptimizedBuffer, deltaTime: number): Promise<void> {\n    await this.renderMethod(root, this.activeCamera, buffer, deltaTime)\n\n    if (this.doRenderStats) {\n      this.renderStats(buffer)\n    }\n  }\n\n  private rendering: boolean = false\n  private destroyed: boolean = false\n  async doDrawScene(\n    root: Scene,\n    camera: PerspectiveCamera | OrthographicCamera,\n    buffer: OptimizedBuffer,\n    deltaTime: number,\n  ): Promise<void> {\n    if (this.rendering) {\n      console.warn(\"ThreeCliRenderer.drawScene was called concurrently, which is not supported.\")\n      return\n    }\n    if (this.destroyed) {\n      return\n    }\n    try {\n      this.rendering = true\n\n      const totalStart = performance.now()\n      const renderStart = performance.now()\n      await this.threeRenderer!.render(root, camera)\n      this.renderTimeMs = performance.now() - renderStart\n\n      const readbackStart = performance.now()\n      await this.canvas!.readPixelsIntoBuffer(buffer)\n      this.readbackTimeMs = performance.now() - readbackStart\n\n      this.totalDrawTimeMs = performance.now() - totalStart\n    } finally {\n      this.rendering = false\n    }\n  }\n\n  public toggleSuperSampling(): void {\n    if (this.superSample === SuperSampleType.NONE) {\n      this.superSample = SuperSampleType.CPU\n    } else if (this.superSample === SuperSampleType.CPU) {\n      this.superSample = SuperSampleType.GPU\n    } else {\n      this.superSample = SuperSampleType.NONE\n    }\n    this.canvas!.setSuperSample(this.superSample)\n    this.setSize(this.outputWidth, this.outputHeight, true)\n  }\n\n  public renderStats(buffer: OptimizedBuffer): void {\n    const stats = [\n      `WebGPU Renderer Stats:`,\n      ` Render: ${this.renderTimeMs.toFixed(2)}ms`,\n      ` Readback: ${this.readbackTimeMs.toFixed(2)}ms`,\n      `  ├ MapAsync: ${this.canvas!.mapAsyncTimeMs.toFixed(2)}ms`,\n      `  └ SS Draw: ${this.canvas!.superSampleDrawTimeMs.toFixed(2)}ms`,\n      ` Total Draw: ${this.totalDrawTimeMs.toFixed(2)}ms`,\n      ` SuperSample: ${this.superSample}`,\n      ` SuperSample Algorithm: ${this.getSuperSampleAlgorithm()}`,\n    ]\n    const startY = 4\n    const startX = 2\n    const fg = RGBA.fromValues(0.9, 0.9, 0.9, 1.0)\n    const bg = RGBA.fromValues(0.1, 0.1, 0.1, 1.0)\n\n    stats.forEach((line, index) => {\n      buffer.drawText(line, startX + 1, startY + index, fg, bg)\n    })\n  }\n\n  public destroy(): void {\n    this.destroyed = true\n\n    this.cliRenderer.off(\"resize\", this.resizeHandler)\n    this.cliRenderer.off(CliRenderEvents.DEBUG_OVERLAY_TOGGLE, this.debugToggleHandler)\n\n    if (this.canvas) {\n      this.canvas.destroy()\n    }\n\n    if (this.threeRenderer) {\n      this.threeRenderer.dispose()\n      this.threeRenderer = undefined\n    }\n\n    this.canvas = undefined\n    this.device = null\n    this.renderMethod = () => Promise.resolve()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/animation/ExplodingSpriteEffect.ts",
    "content": "import * as THREE from \"three\"\nimport {\n  uniform,\n  attribute,\n  texture as tslTexture,\n  uv,\n  float,\n  vec2,\n  vec3,\n  vec4,\n  step,\n  max,\n  sin,\n  cos,\n  positionLocal,\n  mat3,\n} from \"three/tsl\"\nimport { MeshBasicNodeMaterial, NodeMaterial } from \"three/webgpu\"\nimport type { TiledSprite, SpriteDefinition, SpriteAnimator } from \"./SpriteAnimator.js\"\nimport type { SpriteResource } from \"../SpriteResourceManager.js\"\n\nexport interface ExplosionEffectParameters {\n  numRows: number\n  numCols: number\n  durationMs: number\n  strength: number\n  strengthVariation: number\n  gravity: number\n  gravityScale: number\n  fadeOut: boolean\n  angularVelocityMin: THREE.Vector3\n  angularVelocityMax: THREE.Vector3\n  initialVelocityYBoost: number\n  zVariationStrength: number\n  materialFactory: () => NodeMaterial\n}\n\nexport const DEFAULT_EXPLOSION_PARAMETERS: ExplosionEffectParameters = {\n  numRows: 5,\n  numCols: 5,\n  durationMs: 2000,\n  strength: 5,\n  strengthVariation: 0.5,\n  gravity: 9.8,\n  gravityScale: 0.15,\n  fadeOut: true,\n  angularVelocityMin: new THREE.Vector3(-Math.PI, -Math.PI, -Math.PI),\n  angularVelocityMax: new THREE.Vector3(Math.PI, Math.PI, Math.PI),\n  initialVelocityYBoost: 1.0,\n  zVariationStrength: 0.3,\n  materialFactory: () =>\n    new MeshBasicNodeMaterial({\n      transparent: true,\n      alphaTest: 0.01,\n      side: THREE.DoubleSide,\n      depthWrite: true,\n    }),\n}\n\nexport interface ExplosionCreationData {\n  resource: SpriteResource\n  frameUvOffset: THREE.Vector2\n  frameUvSize: THREE.Vector2\n  spriteWorldTransform: THREE.Matrix4\n}\n\nexport interface SpriteRecreationData {\n  definition: SpriteDefinition\n  currentTransform: {\n    position: THREE.Vector3\n    quaternion: THREE.Quaternion\n    scale: THREE.Vector3\n  }\n}\n\nexport interface ExplosionHandle {\n  readonly effect: ExplodingSpriteEffect\n  readonly recreationData: SpriteRecreationData\n  hasBeenRestored: boolean\n  restoreSprite: (spriteAnimator: SpriteAnimator) => Promise<TiledSprite | null>\n}\n\nexport class ExplodingSpriteEffect {\n  private static baseMaterialCache: Map<string, NodeMaterial> = new Map()\n  private scene: THREE.Scene\n  private resource: SpriteResource\n  private frameUvOffset: THREE.Vector2\n  private frameUvSize: THREE.Vector2\n  private spriteWorldTransform: THREE.Matrix4\n  private params: ExplosionEffectParameters\n\n  private instancedMesh!: THREE.InstancedMesh\n  private material!: NodeMaterial\n  private numParticles: number\n\n  private uniformRefs!: { time: any; duration: any; gravity: any }\n\n  public isActive: boolean = true\n  private timeElapsedMs: number = 0\n\n  constructor(\n    scene: THREE.Scene,\n    resource: SpriteResource,\n    frameUvOffset: THREE.Vector2,\n    frameUvSize: THREE.Vector2,\n    spriteWorldTransform: THREE.Matrix4,\n    userParams?: Partial<ExplosionEffectParameters>,\n  ) {\n    this.scene = scene\n    this.resource = resource\n    this.frameUvOffset = frameUvOffset\n    this.frameUvSize = frameUvSize\n    this.spriteWorldTransform = spriteWorldTransform\n    this.params = { ...DEFAULT_EXPLOSION_PARAMETERS, ...userParams }\n\n    this.numParticles = this.params.numRows * this.params.numCols\n    const materialFactory = userParams?.materialFactory ?? DEFAULT_EXPLOSION_PARAMETERS.materialFactory\n\n    this._createGPUParticles(materialFactory)\n  }\n\n  private _createGPUParticles(materialFactory: () => NodeMaterial): void {\n    if (this.numParticles === 0) return\n\n    const particleUnitWidth = 1.0 / this.params.numCols\n    const particleUnitHeight = 1.0 / this.params.numRows\n\n    const poolKey = `${this.params.numRows}x${this.params.numCols}`\n\n    this._createGPUMaterial(materialFactory)\n\n    this.instancedMesh = this.resource.meshPool.acquireMesh(poolKey, {\n      geometry: () => {\n        const geometry = new THREE.PlaneGeometry(particleUnitWidth, particleUnitHeight)\n        geometry.setAttribute(\n          \"a_particleData\",\n          new THREE.InstancedBufferAttribute(new Float32Array(this.numParticles * 4), 4),\n        )\n        geometry.setAttribute(\n          \"a_velocity\",\n          new THREE.InstancedBufferAttribute(new Float32Array(this.numParticles * 4), 4),\n        )\n        geometry.setAttribute(\n          \"a_angularVel\",\n          new THREE.InstancedBufferAttribute(new Float32Array(this.numParticles * 4), 4),\n        )\n        geometry.setAttribute(\n          \"a_uvOffset\",\n          new THREE.InstancedBufferAttribute(new Float32Array(this.numParticles * 4), 4),\n        )\n        return geometry\n      },\n      material: this.material,\n      maxInstances: this.numParticles,\n      name: `ExplodingSprite_${poolKey}`,\n    })\n\n    const particleData: Float32Array = this.instancedMesh.geometry.getAttribute(\"a_particleData\").array as Float32Array\n    const velocityData: Float32Array = this.instancedMesh.geometry.getAttribute(\"a_velocity\").array as Float32Array\n    const angularVelData: Float32Array = this.instancedMesh.geometry.getAttribute(\"a_angularVel\").array as Float32Array\n    const uvOffsetData: Float32Array = this.instancedMesh.geometry.getAttribute(\"a_uvOffset\").array as Float32Array\n\n    const spriteWorldCenter = new THREE.Vector3().setFromMatrixPosition(this.spriteWorldTransform)\n\n    let particleIndex = 0\n    for (let r = 0; r < this.params.numRows; r++) {\n      for (let c = 0; c < this.params.numCols; c++) {\n        const localParticlePosX = (c + 0.5) * particleUnitWidth - 0.5\n        const localParticlePosY = (r + 0.5) * particleUnitHeight - 0.5\n\n        const initialLocalPosition = new THREE.Vector3(localParticlePosX, localParticlePosY, 0)\n        const worldPosition = initialLocalPosition.clone().applyMatrix4(this.spriteWorldTransform)\n\n        let velocityDir = worldPosition.clone().sub(spriteWorldCenter)\n        if (velocityDir.lengthSq() < 0.0001) {\n          velocityDir.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5)\n        }\n        velocityDir.normalize()\n\n        const strengthVariationRange = this.params.strengthVariation\n        const minStrengthFactor = 1.0 - strengthVariationRange * 0.5\n        const maxStrengthFactor = 1.0 + strengthVariationRange * 0.5\n        const strengthFactor = minStrengthFactor + Math.random() * (maxStrengthFactor - minStrengthFactor)\n        const strength = this.params.strength * strengthFactor * 0.1\n        const velocity = velocityDir.multiplyScalar(strength)\n\n        if (\n          Math.abs(this.spriteWorldTransform.elements[10]) < 0.1 &&\n          Math.abs(this.spriteWorldTransform.elements[11]) < 0.1\n        ) {\n          velocity.z += (Math.random() - 0.5) * strength * this.params.zVariationStrength\n        }\n\n        velocity.y += this.params.strength * this.params.initialVelocityYBoost * Math.random()\n\n        const angularVelocity = new THREE.Vector3(\n          THREE.MathUtils.randFloat(this.params.angularVelocityMin.x, this.params.angularVelocityMax.x),\n          THREE.MathUtils.randFloat(this.params.angularVelocityMin.y, this.params.angularVelocityMax.y),\n          THREE.MathUtils.randFloat(this.params.angularVelocityMin.z, this.params.angularVelocityMax.z),\n        )\n\n        const lifeVariation = 0.8 + Math.random() * 0.4\n        const randomSeed = Math.random()\n\n        const u0 = this.frameUvOffset.x + (c / this.params.numCols) * this.frameUvSize.x\n        const v0 = this.frameUvOffset.y + (r / this.params.numRows) * this.frameUvSize.y\n        const uSize = this.frameUvSize.x / this.params.numCols\n        const vSize = this.frameUvSize.y / this.params.numRows\n\n        const baseIndex = particleIndex * 4\n\n        particleData[baseIndex] = localParticlePosX\n        particleData[baseIndex + 1] = localParticlePosY\n        particleData[baseIndex + 2] = randomSeed\n        particleData[baseIndex + 3] = lifeVariation\n\n        velocityData[baseIndex] = velocity.x\n        velocityData[baseIndex + 1] = velocity.y\n        velocityData[baseIndex + 2] = velocity.z\n        velocityData[baseIndex + 3] = 0.0\n\n        angularVelData[baseIndex] = angularVelocity.x\n        angularVelData[baseIndex + 1] = angularVelocity.y\n        angularVelData[baseIndex + 2] = angularVelocity.z\n        angularVelData[baseIndex + 3] = 0.0\n\n        uvOffsetData[baseIndex] = u0\n        uvOffsetData[baseIndex + 1] = v0\n        uvOffsetData[baseIndex + 2] = uSize\n        uvOffsetData[baseIndex + 3] = vSize\n\n        particleIndex++\n      }\n    }\n\n    this.instancedMesh.onBeforeRender = () => {\n      this.uniformRefs.time.value = this.timeElapsedMs / 1000\n    }\n\n    this.timeElapsedMs = 0\n\n    this.instancedMesh.geometry.getAttribute(\"a_particleData\").needsUpdate = true\n    this.instancedMesh.geometry.getAttribute(\"a_velocity\").needsUpdate = true\n    this.instancedMesh.geometry.getAttribute(\"a_angularVel\").needsUpdate = true\n    this.instancedMesh.geometry.getAttribute(\"a_uvOffset\").needsUpdate = true\n\n    this.instancedMesh.frustumCulled = false\n\n    for (let i = 0; i < this.numParticles; i++) {\n      this.instancedMesh.setMatrixAt(i, this.spriteWorldTransform)\n    }\n    this.instancedMesh.instanceMatrix.needsUpdate = true\n\n    this.scene.add(this.instancedMesh)\n  }\n\n  private _createGPUMaterial(materialFactory: () => NodeMaterial): void {\n    const key = `${this.resource.texture.uuid}_${this.params.numRows}x${this.params.numCols}_${this.params.fadeOut ? 1 : 0}`\n\n    let template = ExplodingSpriteEffect.baseMaterialCache.get(key)\n    if (!template) {\n      template = ExplodingSpriteEffect._buildTemplateMaterial(this.resource.texture, this.params, materialFactory)\n      ExplodingSpriteEffect.baseMaterialCache.set(key, template)\n    }\n\n    this.material = template\n    this.uniformRefs = template.userData.uniformRefs as { time: any; duration: any; gravity: any }\n  }\n\n  public static _buildTemplateMaterial(\n    texture: THREE.DataTexture,\n    params: ExplosionEffectParameters,\n    materialFactory: () => NodeMaterial,\n  ): NodeMaterial {\n    const timeUniformNode = uniform(0)\n    ;(timeUniformNode as any).name = \"timeUniform\"\n    const durationUniformNode = uniform(params.durationMs / 1000)\n    ;(durationUniformNode as any).name = \"durationUniform\"\n    const gravityUniformNode = uniform(params.gravity * params.gravityScale)\n    ;(gravityUniformNode as any).name = \"gravityUniform\"\n\n    const a_particleData = attribute(\"a_particleData\", \"vec4\")\n    const a_velocity = attribute(\"a_velocity\", \"vec4\")\n    const a_angularVel = attribute(\"a_angularVel\", \"vec4\")\n    const a_uvOffset = attribute(\"a_uvOffset\", \"vec4\")\n\n    const localPos = vec2(a_particleData.x, a_particleData.y)\n    const lifeVariation = a_particleData.w\n\n    const initialVelocity = vec3(a_velocity.x, a_velocity.y, a_velocity.z)\n    const angularVelocity = vec3(a_angularVel.x, a_angularVel.y, a_angularVel.z)\n\n    const uvOffset = vec2(a_uvOffset.x, a_uvOffset.y)\n    const uvSize = vec2(a_uvOffset.z, a_uvOffset.w)\n\n    const particleLifetime = durationUniformNode.mul(lifeVariation)\n    const normalizedTime = timeUniformNode.div(particleLifetime)\n    const isAlive = step(normalizedTime, float(1.0))\n\n    const deltaTime = timeUniformNode\n    const gravity = vec3(float(0), gravityUniformNode.negate(), float(0))\n\n    const velocityContribution = initialVelocity.mul(deltaTime)\n    const gravityContribution = gravity.mul(deltaTime).mul(deltaTime).mul(float(0.5))\n    const positionOffset = velocityContribution.add(gravityContribution)\n\n    const rotationAmount = angularVelocity.mul(deltaTime)\n    const cosX = cos(rotationAmount.x)\n    const sinX = sin(rotationAmount.x)\n    const cosY = cos(rotationAmount.y)\n    const sinY = sin(rotationAmount.y)\n    const cosZ = cos(rotationAmount.z)\n    const sinZ = sin(rotationAmount.z)\n\n    const rotationMatrix = mat3(\n      cosY.mul(cosZ),\n      cosY.mul(sinZ).negate(),\n      sinY,\n      sinX.mul(sinY).mul(cosZ).add(cosX.mul(sinZ)),\n      sinX.mul(sinY).mul(sinZ).negate().add(cosX.mul(cosZ)),\n      sinX.mul(cosY).negate(),\n      cosX.mul(sinY).mul(cosZ).negate().add(sinX.mul(sinZ)),\n      cosX.mul(sinY).mul(sinZ).add(sinX.mul(cosZ)),\n      cosX.mul(cosY),\n    )\n\n    const rotatedVertexPosition = rotationMatrix.mul(positionLocal)\n    const finalOffset = vec3(localPos.x, localPos.y, float(0)).add(positionOffset)\n\n    let opacity = float(1.0)\n    if (params.fadeOut) {\n      const fadeStart = float(0.7)\n      const fadeProgress = max(float(0), normalizedTime.sub(fadeStart).div(float(1.0).sub(fadeStart)))\n      opacity = float(1.0).sub(fadeProgress)\n    }\n    opacity = opacity.mul(isAlive)\n\n    const baseUV = uv()\n    const finalUV = baseUV.mul(uvSize).add(uvOffset)\n\n    const mapNode = tslTexture(texture)\n    const sampledColor = mapNode.sample(finalUV)\n\n    const material = materialFactory()\n\n    const finalColor = vec4(sampledColor.rgb, sampledColor.a.mul(opacity))\n    material.colorNode = finalColor\n    material.positionNode = rotatedVertexPosition.add(finalOffset)\n\n    material.userData.uniformRefs = {\n      time: timeUniformNode,\n      duration: durationUniformNode,\n      gravity: gravityUniformNode,\n    }\n\n    return material\n  }\n\n  update(deltaTimeMs: number): void {\n    if (!this.isActive) return\n\n    this.timeElapsedMs += deltaTimeMs\n\n    if (this.timeElapsedMs >= this.params.durationMs) {\n      this.dispose()\n    }\n  }\n\n  dispose(): void {\n    if (!this.isActive) return\n    this.isActive = false\n\n    if (this.instancedMesh) {\n      this.scene.remove(this.instancedMesh)\n\n      const poolKey = `${this.params.numRows}x${this.params.numCols}`\n\n      this.resource.meshPool.releaseMesh(poolKey, this.instancedMesh)\n    }\n  }\n}\n\nexport class ExplosionManager {\n  private scene: THREE.Scene\n  private activeExplosions: ExplodingSpriteEffect[] = []\n\n  constructor(scene: THREE.Scene) {\n    this.scene = scene\n  }\n\n  public fillPool(resource: SpriteResource, count: number, params: Partial<ExplosionEffectParameters> = {}): void {\n    const effectParams = { ...DEFAULT_EXPLOSION_PARAMETERS, ...params }\n    const poolKey = `${effectParams.numRows}x${effectParams.numCols}`\n    const particleUnitWidth = 1.0 / effectParams.numCols\n    const particleUnitHeight = 1.0 / effectParams.numRows\n    const numParticles = effectParams.numRows * effectParams.numCols\n    const materialFactory = params.materialFactory ?? DEFAULT_EXPLOSION_PARAMETERS.materialFactory\n\n    const material = ExplodingSpriteEffect._buildTemplateMaterial(resource.texture, effectParams, materialFactory)\n\n    resource.meshPool.fill(\n      poolKey,\n      {\n        geometry: () => {\n          const geometry = new THREE.PlaneGeometry(particleUnitWidth, particleUnitHeight)\n          const particleData = new Float32Array(numParticles * 4)\n          const velocityData = new Float32Array(numParticles * 4)\n          const angularVelData = new Float32Array(numParticles * 4)\n          const uvOffsetData = new Float32Array(numParticles * 4)\n          const particleDataAttribute = new THREE.InstancedBufferAttribute(particleData, 4)\n          const velocityAttribute = new THREE.InstancedBufferAttribute(velocityData, 4)\n          const angularVelAttribute = new THREE.InstancedBufferAttribute(angularVelData, 4)\n          const uvOffsetAttribute = new THREE.InstancedBufferAttribute(uvOffsetData, 4)\n\n          geometry.setAttribute(\"a_particleData\", particleDataAttribute)\n          geometry.setAttribute(\"a_velocity\", velocityAttribute)\n          geometry.setAttribute(\"a_angularVel\", angularVelAttribute)\n          geometry.setAttribute(\"a_uvOffset\", uvOffsetAttribute)\n\n          particleDataAttribute.needsUpdate = true\n          velocityAttribute.needsUpdate = true\n          angularVelAttribute.needsUpdate = true\n          uvOffsetAttribute.needsUpdate = true\n\n          return geometry\n        },\n        material,\n        maxInstances: numParticles,\n        name: `ExplodingSprite_${poolKey}`,\n      },\n      count,\n    )\n  }\n\n  private _createEffectCreationData(sprite: TiledSprite): ExplosionCreationData {\n    const animState = sprite.currentAnimation.state\n    const resource = sprite.currentAnimation.getResource()\n    const currentAbsoluteFrame = animState.animFrameOffset + sprite.currentAnimation.currentLocalFrame\n    const frameUOffset = currentAbsoluteFrame * resource.uvTileSize.x\n    return {\n      resource: resource,\n      frameUvOffset: new THREE.Vector2(frameUOffset, 0),\n      frameUvSize: resource.uvTileSize.clone(),\n      spriteWorldTransform: sprite.getWorldTransform(),\n    }\n  }\n\n  public createExplosionForSprite(\n    spriteToExplode: TiledSprite,\n    userParams?: Partial<ExplosionEffectParameters>,\n  ): ExplosionHandle | null {\n    const effectCreationData = this._createEffectCreationData(spriteToExplode)\n    const definition = spriteToExplode.definition\n    const transform = spriteToExplode.currentTransform\n\n    let spriteRecreationData: SpriteRecreationData = {\n      definition: definition,\n      currentTransform: transform,\n    }\n\n    spriteToExplode.destroy()\n\n    const effect = new ExplodingSpriteEffect(\n      this.scene,\n      effectCreationData.resource,\n      effectCreationData.frameUvOffset,\n      effectCreationData.frameUvSize,\n      effectCreationData.spriteWorldTransform,\n      userParams,\n    )\n    this.activeExplosions.push(effect)\n\n    const handle: ExplosionHandle = {\n      effect: effect,\n      recreationData: spriteRecreationData,\n      hasBeenRestored: false,\n      restoreSprite: async (spriteAnimator: SpriteAnimator): Promise<TiledSprite | null> => {\n        if (handle.hasBeenRestored) {\n          return null\n        }\n\n        handle.effect.dispose()\n\n        const newSprite = await spriteAnimator.createSprite(handle.recreationData.definition)\n        const currentSpriteTransform = handle.recreationData.currentTransform\n        newSprite.setTransform(\n          currentSpriteTransform.position,\n          currentSpriteTransform.quaternion,\n          currentSpriteTransform.scale,\n        )\n        handle.hasBeenRestored = true\n\n        return newSprite\n      },\n    }\n    return handle\n  }\n\n  public update(deltaTimeMs: number): void {\n    for (let i = this.activeExplosions.length - 1; i >= 0; i--) {\n      const explosion = this.activeExplosions[i]\n      explosion.update(deltaTimeMs)\n      if (!explosion.isActive) {\n        this.activeExplosions.splice(i, 1)\n      }\n    }\n  }\n\n  public disposeAll(): void {\n    this.activeExplosions.forEach((exp) => exp.dispose())\n    this.activeExplosions = []\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/animation/PhysicsExplodingSpriteEffect.ts",
    "content": "import * as THREE from \"three\"\nimport { texture as tslTexture, uv, vec2, attribute } from \"three/tsl\"\nimport { MeshBasicNodeMaterial, NodeMaterial } from \"three/webgpu\"\nimport type { TiledSprite, SpriteDefinition, SpriteAnimator } from \"./SpriteAnimator.js\"\nimport type {\n  PhysicsRigidBody,\n  PhysicsWorld,\n  PhysicsRigidBodyDesc,\n  PhysicsColliderDesc,\n  PhysicsVector2,\n} from \"../physics/physics-interface.js\"\nimport type { SpriteResource } from \"../SpriteResourceManager.js\"\n\nexport interface PhysicsExplosionEffectParameters {\n  numRows: number\n  numCols: number\n  durationMs: number\n  explosionForce: number\n  forceVariation: number\n  torqueStrength: number\n  gravityScale: number\n  fadeOut: boolean\n  linearDamping: number\n  angularDamping: number\n  restitution: number\n  friction: number\n  density: number\n  materialFactory: () => NodeMaterial\n}\n\nexport const DEFAULT_PHYSICS_EXPLOSION_PARAMETERS: PhysicsExplosionEffectParameters = {\n  numRows: 5,\n  numCols: 5,\n  durationMs: 3000,\n  explosionForce: 25.0,\n  forceVariation: 0.4,\n  torqueStrength: 15.0,\n  gravityScale: 1.0,\n  fadeOut: true,\n  linearDamping: 0.8,\n  angularDamping: 0.5,\n  restitution: 0.3,\n  friction: 0.7,\n  density: 1.0,\n  materialFactory: () =>\n    new MeshBasicNodeMaterial({\n      transparent: true,\n      alphaTest: 0.01,\n      // side: THREE.DoubleSide,\n      depthWrite: false,\n    }),\n}\n\nexport interface PhysicsExplosionCreationData {\n  resource: SpriteResource\n  frameUvOffset: THREE.Vector2\n  frameUvSize: THREE.Vector2\n  spriteWorldTransform: THREE.Matrix4\n}\n\nexport interface PhysicsSpriteRecreationData {\n  definition: SpriteDefinition\n  currentTransform: {\n    position: THREE.Vector3\n    quaternion: THREE.Quaternion\n    scale: THREE.Vector3\n  }\n}\n\nexport interface PhysicsExplosionHandle {\n  readonly effect: PhysicsExplodingSpriteEffect\n  readonly recreationData: PhysicsSpriteRecreationData\n  hasBeenRestored: boolean\n  restoreSprite: (spriteAnimator: SpriteAnimator) => Promise<TiledSprite | null>\n}\n\ninterface ExplosionParticle {\n  rigidBody: PhysicsRigidBody\n  instanceIndex: number\n  uvOffset: THREE.Vector2\n  uvSize: THREE.Vector2\n  initialOpacity: number\n  lifeVariation: number\n  id: string\n}\n\nexport class PhysicsExplodingSpriteEffect {\n  private static materialCache: Map<string, NodeMaterial> = new Map()\n\n  private scene: THREE.Scene\n  private physicsWorld: PhysicsWorld\n  private resource: SpriteResource\n  private frameUvOffset: THREE.Vector2\n  private frameUvSize: THREE.Vector2\n  private spriteWorldTransform: THREE.Matrix4\n  private params: PhysicsExplosionEffectParameters\n\n  private particles: ExplosionParticle[] = []\n  private numParticles: number\n\n  private instancedMesh!: THREE.InstancedMesh\n  private material!: NodeMaterial\n  private uvOffsetAttribute!: THREE.InstancedBufferAttribute\n\n  public isActive: boolean = true\n  private timeElapsedMs: number = 0\n  private particleIdCounter: number = 0\n\n  constructor(\n    scene: THREE.Scene,\n    physicsWorld: PhysicsWorld,\n    resource: SpriteResource,\n    frameUvOffset: THREE.Vector2,\n    frameUvSize: THREE.Vector2,\n    spriteWorldTransform: THREE.Matrix4,\n    userParams?: Partial<PhysicsExplosionEffectParameters>,\n  ) {\n    this.scene = scene\n    this.physicsWorld = physicsWorld\n    this.resource = resource\n    this.frameUvOffset = frameUvOffset\n    this.frameUvSize = frameUvSize\n    this.spriteWorldTransform = spriteWorldTransform\n    this.params = { ...DEFAULT_PHYSICS_EXPLOSION_PARAMETERS, ...userParams }\n\n    this.numParticles = this.params.numRows * this.params.numCols\n    const materialFactory = userParams?.materialFactory ?? DEFAULT_PHYSICS_EXPLOSION_PARAMETERS.materialFactory\n\n    this._createPhysicsParticles(materialFactory)\n  }\n\n  private _createPhysicsParticles(materialFactory: () => NodeMaterial): void {\n    if (this.numParticles === 0) return\n\n    const particleUnitWidth = 1.0 / this.params.numCols\n    const particleUnitHeight = 1.0 / this.params.numRows\n\n    const spriteWorldCenter = new THREE.Vector3().setFromMatrixPosition(this.spriteWorldTransform)\n    const spriteScale = new THREE.Vector3().setFromMatrixScale(this.spriteWorldTransform)\n    const avgScale = (spriteScale.x + spriteScale.y) * 0.5\n    const uvOffsetData = new Float32Array(this.numParticles * 4)\n\n    let particleIndex = 0\n    for (let r = 0; r < this.params.numRows; r++) {\n      for (let c = 0; c < this.params.numCols; c++) {\n        const localParticlePosX = (c + 0.5) * particleUnitWidth - 0.5\n        const localParticlePosY = (r + 0.5) * particleUnitHeight - 0.5\n\n        const initialLocalPosition = new THREE.Vector3(localParticlePosX, localParticlePosY, 0)\n        const worldPosition = initialLocalPosition.clone().applyMatrix4(this.spriteWorldTransform)\n\n        const rigidBodyDesc: PhysicsRigidBodyDesc = {\n          translation: { x: worldPosition.x, y: worldPosition.y },\n          linearDamping: this.params.linearDamping,\n          angularDamping: this.params.angularDamping,\n        }\n\n        const rigidBody = this.physicsWorld.createRigidBody(rigidBodyDesc)\n\n        const particlePhysicsWidth = particleUnitWidth * avgScale * 0.8\n        const particlePhysicsHeight = particleUnitHeight * avgScale * 0.8\n        const colliderDesc: PhysicsColliderDesc = {\n          width: particlePhysicsWidth,\n          height: particlePhysicsHeight,\n          restitution: this.params.restitution,\n          friction: this.params.friction,\n          density: this.params.density,\n        }\n\n        this.physicsWorld.createCollider(colliderDesc, rigidBody)\n\n        let explosionDir = worldPosition.clone().sub(spriteWorldCenter)\n        if (explosionDir.lengthSq() < 0.0001) {\n          explosionDir.set(Math.random() - 0.5, Math.random() - 0.5, 0)\n        }\n        explosionDir.normalize()\n\n        const forceVariationRange = this.params.forceVariation\n        const minForceFactor = 1.0 - forceVariationRange * 0.5\n        const maxForceFactor = 1.0 + forceVariationRange * 0.5\n        const forceFactor = minForceFactor + Math.random() * (maxForceFactor - minForceFactor)\n        const explosionForce = this.params.explosionForce * forceFactor\n\n        const forceVector: PhysicsVector2 = {\n          x: explosionDir.x * explosionForce,\n          y: explosionDir.y * explosionForce + explosionForce * 0.3, // Add upward bias\n        }\n\n        rigidBody.applyImpulse(forceVector)\n\n        const torque = (Math.random() - 0.5) * this.params.torqueStrength\n        rigidBody.applyTorqueImpulse(torque)\n\n        const u0 = this.frameUvOffset.x + (c / this.params.numCols) * this.frameUvSize.x\n        const v0 = this.frameUvOffset.y + (r / this.params.numRows) * this.frameUvSize.y\n        const uSize = this.frameUvSize.x / this.params.numCols\n        const vSize = this.frameUvSize.y / this.params.numRows\n\n        const baseIndex = particleIndex * 4\n        uvOffsetData[baseIndex] = u0\n        uvOffsetData[baseIndex + 1] = v0\n        uvOffsetData[baseIndex + 2] = uSize\n        uvOffsetData[baseIndex + 3] = vSize\n\n        const particleId = `explosion_particle_${this.particleIdCounter++}`\n        const lifeVariation = 0.8 + Math.random() * 0.4\n\n        const particle: ExplosionParticle = {\n          rigidBody,\n          instanceIndex: particleIndex,\n          uvOffset: new THREE.Vector2(u0, v0),\n          uvSize: new THREE.Vector2(uSize, vSize),\n          initialOpacity: 1.0,\n          lifeVariation,\n          id: particleId,\n        }\n\n        this.particles.push(particle)\n        particleIndex++\n      }\n    }\n\n    this.uvOffsetAttribute = new THREE.InstancedBufferAttribute(uvOffsetData, 4)\n    this.material = PhysicsExplodingSpriteEffect.getSharedMaterial(this.resource.texture, materialFactory)\n\n    const poolKey = `${this.params.numRows}x${this.params.numCols}`\n\n    this.instancedMesh = this.resource.meshPool.acquireMesh(poolKey, {\n      geometry: () => new THREE.PlaneGeometry(particleUnitWidth, particleUnitHeight),\n      material: this.material,\n      maxInstances: this.numParticles,\n      name: `PhysicsExplodingSprite_${poolKey}`,\n    })\n\n    this.instancedMesh.geometry.setAttribute(\"a_uvOffset\", this.uvOffsetAttribute)\n\n    this.instancedMesh.frustumCulled = false\n\n    for (let i = 0; i < this.numParticles; i++) {\n      this.instancedMesh.setMatrixAt(i, this.spriteWorldTransform)\n    }\n    this.instancedMesh.instanceMatrix.needsUpdate = true\n\n    this.scene.add(this.instancedMesh)\n  }\n\n  public static getSharedMaterial(texture: THREE.DataTexture, materialFactory: () => NodeMaterial): NodeMaterial {\n    const key = texture.uuid\n    const cached = PhysicsExplodingSpriteEffect.materialCache.get(key)\n    if (cached) return cached\n\n    const a_uvOffset = attribute(\"a_uvOffset\", \"vec4\")\n    const uvOffset = vec2(a_uvOffset.x, a_uvOffset.y)\n    const uvSize = vec2(a_uvOffset.z, a_uvOffset.w)\n\n    const baseUV = uv()\n    const finalUV = baseUV.mul(uvSize).add(uvOffset)\n\n    const mapNode = tslTexture(texture)\n    const sampledColor = mapNode.sample(finalUV)\n\n    const material = materialFactory()\n    material.colorNode = sampledColor\n\n    PhysicsExplodingSpriteEffect.materialCache.set(key, material)\n    return material\n  }\n\n  update(deltaTimeMs: number): void {\n    if (!this.isActive) return\n\n    this.timeElapsedMs += deltaTimeMs\n\n    const tempMatrix = new THREE.Matrix4()\n    const tempScale = new THREE.Vector3()\n    this.spriteWorldTransform.decompose(new THREE.Vector3(), new THREE.Quaternion(), tempScale)\n\n    const axis = new THREE.Vector3(0, 0, 1)\n    for (const particle of this.particles) {\n      const position = particle.rigidBody.getTranslation()\n      const rotation = particle.rigidBody.getRotation()\n      const quaternion = new THREE.Quaternion().setFromAxisAngle(axis, rotation)\n\n      tempMatrix.compose(new THREE.Vector3(position.x, position.y, 0), quaternion, tempScale)\n\n      this.instancedMesh.setMatrixAt(particle.instanceIndex, tempMatrix)\n    }\n\n    this.instancedMesh.instanceMatrix.needsUpdate = true\n\n    if (this.timeElapsedMs >= this.params.durationMs) {\n      this.dispose()\n    }\n  }\n\n  dispose(): void {\n    if (!this.isActive) return\n    this.isActive = false\n\n    if (this.instancedMesh) {\n      this.scene.remove(this.instancedMesh)\n\n      const poolKey = `${this.params.numRows}x${this.params.numCols}`\n      this.resource.meshPool.releaseMesh(poolKey, this.instancedMesh)\n    }\n\n    for (const particle of this.particles) {\n      this.physicsWorld.removeRigidBody(particle.rigidBody)\n    }\n    this.particles = []\n  }\n}\n\nexport class PhysicsExplosionManager {\n  private scene: THREE.Scene\n  private physicsWorld: PhysicsWorld\n  private activeExplosions: PhysicsExplodingSpriteEffect[] = []\n\n  constructor(scene: THREE.Scene, physicsWorld: PhysicsWorld) {\n    this.scene = scene\n    this.physicsWorld = physicsWorld\n  }\n\n  public fillPool(\n    resource: SpriteResource,\n    count: number,\n    params: Partial<PhysicsExplosionEffectParameters> = {},\n  ): void {\n    const effectParams = { ...DEFAULT_PHYSICS_EXPLOSION_PARAMETERS, ...params }\n    const poolKey = `${effectParams.numRows}x${effectParams.numCols}`\n    const particleUnitWidth = 1.0 / effectParams.numCols\n    const particleUnitHeight = 1.0 / effectParams.numRows\n    const numParticles = effectParams.numRows * effectParams.numCols\n\n    const materialFactory = params.materialFactory ?? DEFAULT_PHYSICS_EXPLOSION_PARAMETERS.materialFactory\n    const material = PhysicsExplodingSpriteEffect.getSharedMaterial(resource.texture, materialFactory)\n    const geometry = new THREE.PlaneGeometry(particleUnitWidth, particleUnitHeight)\n\n    resource.meshPool.fill(\n      poolKey,\n      {\n        geometry: () => geometry,\n        material,\n        maxInstances: numParticles,\n        name: `PhysicsExplodingSprite_${poolKey}`,\n      },\n      count,\n    )\n  }\n\n  private _createEffectCreationData(sprite: TiledSprite): PhysicsExplosionCreationData {\n    const animState = sprite.currentAnimation.state\n    const resource = sprite.currentAnimation.getResource()\n    const currentAbsoluteFrame = animState.animFrameOffset + sprite.currentAnimation.currentLocalFrame\n    const frameUOffset = currentAbsoluteFrame * resource.uvTileSize.x\n    return {\n      resource: resource,\n      frameUvOffset: new THREE.Vector2(frameUOffset, 0),\n      frameUvSize: resource.uvTileSize.clone(),\n      spriteWorldTransform: sprite.getWorldTransform(),\n    }\n  }\n\n  public async createExplosionForSprite(\n    spriteToExplode: TiledSprite,\n    userParams?: Partial<PhysicsExplosionEffectParameters>,\n  ): Promise<PhysicsExplosionHandle | null> {\n    const effectCreationData = this._createEffectCreationData(spriteToExplode)\n    const definition = spriteToExplode.definition\n    const transform = spriteToExplode.currentTransform\n    const spriteRecreationData: PhysicsSpriteRecreationData = {\n      definition: definition,\n      currentTransform: transform,\n    }\n\n    spriteToExplode.destroy()\n\n    const effect = new PhysicsExplodingSpriteEffect(\n      this.scene,\n      this.physicsWorld,\n      effectCreationData.resource,\n      effectCreationData.frameUvOffset,\n      effectCreationData.frameUvSize,\n      effectCreationData.spriteWorldTransform,\n      userParams,\n    )\n    this.activeExplosions.push(effect)\n\n    const handle: PhysicsExplosionHandle = {\n      effect: effect,\n      recreationData: spriteRecreationData,\n      hasBeenRestored: false,\n      restoreSprite: async (spriteAnimator: SpriteAnimator): Promise<TiledSprite | null> => {\n        if (handle.hasBeenRestored) {\n          return null\n        }\n\n        handle.effect.dispose()\n\n        const newSprite = await spriteAnimator.createSprite(handle.recreationData.definition)\n        const currentSpriteTransform = handle.recreationData.currentTransform\n        newSprite.setTransform(\n          currentSpriteTransform.position,\n          currentSpriteTransform.quaternion,\n          currentSpriteTransform.scale,\n        )\n        handle.hasBeenRestored = true\n\n        return newSprite\n      },\n    }\n    return handle\n  }\n\n  public update(deltaTimeMs: number): void {\n    for (let i = this.activeExplosions.length - 1; i >= 0; i--) {\n      const explosion = this.activeExplosions[i]\n      explosion.update(deltaTimeMs)\n      if (!explosion.isActive) {\n        this.activeExplosions.splice(i, 1)\n      }\n    }\n  }\n\n  public disposeAll(): void {\n    this.activeExplosions.forEach((exp) => exp.dispose())\n    this.activeExplosions = []\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/animation/SpriteAnimator.ts",
    "content": "import * as THREE from \"three\"\nimport { uniform, texture as tslTexture, uv, float, vec2, bufferAttribute, mix } from \"three/tsl\"\nimport { MeshBasicNodeMaterial, NodeMaterial } from \"three/webgpu\"\nimport type { Scene } from \"three\"\nimport { type SpriteResource, InstanceManager } from \"../SpriteResourceManager.js\"\n\nexport interface AnimationStateConfig {\n  imagePath: string\n  sheetNumFrames: number\n\n  animNumFrames: number\n  animFrameOffset: number\n\n  frameDuration?: number\n  loop?: boolean\n  initialFrame?: number\n  flipX?: boolean\n  flipY?: boolean\n}\n\nexport type ResolvedAnimationState = Required<AnimationStateConfig> & {\n  sheetTilesetWidth: number\n  sheetTilesetHeight: number\n  texture: THREE.DataTexture\n}\n\nexport interface AnimationDefinition {\n  resource: SpriteResource\n  animNumFrames?: number\n  animFrameOffset?: number\n  frameDuration?: number\n  loop?: boolean\n  initialFrame?: number\n  flipX?: boolean\n  flipY?: boolean\n}\n\nexport interface SpriteDefinition {\n  id?: string\n  renderOrder?: number\n  depthWrite?: boolean\n  maxInstances?: number\n  scale?: number\n  initialAnimation: string\n  animations: Record<string, AnimationDefinition>\n}\n\nconst HIDDEN_MATRIX = new THREE.Matrix4().scale(new THREE.Vector3(0, 0, 0))\n\nconst DEFAULT_FRAME_DURATION = 100\nconst DEFAULT_INITIAL_FRAME = 0\nconst DEFAULT_SCALE = 1.0\nconst DEFAULT_FLIP_X = false\nconst DEFAULT_FLIP_Y = false\n\nclass Animation {\n  public readonly name: string\n  public state: ResolvedAnimationState\n  private resource: SpriteResource\n  public instanceIndex: number\n  private instanceManager: InstanceManager\n  private frameAttribute: THREE.InstancedBufferAttribute\n  private flipAttribute: THREE.InstancedBufferAttribute\n\n  public currentLocalFrame: number\n  public timeAccumulator: number\n  public isPlaying: boolean\n  private _isActive: boolean = false\n\n  constructor(\n    name: string,\n    state: ResolvedAnimationState,\n    resource: SpriteResource,\n    instanceIndex: number,\n    instanceManager: InstanceManager,\n    frameAttribute: THREE.InstancedBufferAttribute,\n    flipAttribute: THREE.InstancedBufferAttribute,\n  ) {\n    this.name = name\n    this.state = state\n    this.resource = resource\n    this.instanceIndex = instanceIndex\n    this.instanceManager = instanceManager\n    this.frameAttribute = frameAttribute\n    this.flipAttribute = flipAttribute\n\n    this.currentLocalFrame = state.initialFrame\n    this.timeAccumulator = 0\n    this.isPlaying = true\n\n    this.instanceManager.mesh.setMatrixAt(this.instanceIndex, HIDDEN_MATRIX)\n    const absoluteFrame = this.state.animFrameOffset + this.currentLocalFrame\n    this.frameAttribute.setX(this.instanceIndex, absoluteFrame)\n    this.flipAttribute.setXY(this.instanceIndex, this.state.flipX ? 1.0 : 0.0, this.state.flipY ? 1.0 : 0.0)\n  }\n\n  activate(worldTransform: THREE.Matrix4): void {\n    this._isActive = true\n    this.isPlaying = true // Start playing when activated\n    this.currentLocalFrame = this.state.initialFrame // Reset to initial frame\n    this.timeAccumulator = 0\n    this.updateVisuals(worldTransform) // Apply transform and set frame\n    // Ensure frame attribute is updated upon activation\n    const absoluteFrame = this.state.animFrameOffset + this.currentLocalFrame\n    this.frameAttribute.setX(this.instanceIndex, absoluteFrame)\n    this.frameAttribute.needsUpdate = true // Mark manager for update\n    this.flipAttribute.setXY(this.instanceIndex, this.state.flipX ? 1.0 : 0.0, this.state.flipY ? 1.0 : 0.0)\n    this.flipAttribute.needsUpdate = true\n  }\n\n  deactivate(): void {\n    this._isActive = false\n    this.isPlaying = false\n    this.instanceManager.mesh.setMatrixAt(this.instanceIndex, HIDDEN_MATRIX)\n  }\n\n  updateVisuals(worldTransform: THREE.Matrix4): void {\n    if (!this._isActive) return\n    this.instanceManager.mesh.setMatrixAt(this.instanceIndex, worldTransform)\n  }\n\n  updateTime(deltaTimeMs: number): boolean {\n    // Returns true if frame attribute was updated\n    if (!this.isPlaying || !this._isActive) return false\n\n    this.timeAccumulator += deltaTimeMs\n    let needsFrameAttributeUpdate = false\n\n    if (this.timeAccumulator >= this.state.frameDuration) {\n      const framesToAdvance = Math.floor(this.timeAccumulator / this.state.frameDuration)\n      this.timeAccumulator %= this.state.frameDuration\n\n      const oldLocalFrame = this.currentLocalFrame\n      let nextLocalFrame = this.currentLocalFrame + framesToAdvance\n\n      if (nextLocalFrame >= this.state.animNumFrames) {\n        if (this.state.loop) {\n          this.currentLocalFrame = nextLocalFrame % this.state.animNumFrames\n        } else {\n          this.currentLocalFrame = this.state.animNumFrames - 1\n          this.isPlaying = false\n        }\n      } else {\n        this.currentLocalFrame = nextLocalFrame\n      }\n\n      if (this.currentLocalFrame !== oldLocalFrame || !this.isPlaying) {\n        const absoluteFrame = this.state.animFrameOffset + this.currentLocalFrame\n        this.frameAttribute.setX(this.instanceIndex, absoluteFrame)\n        this.frameAttribute.needsUpdate = true\n        needsFrameAttributeUpdate = true\n      }\n    }\n    return needsFrameAttributeUpdate\n  }\n\n  play(): void {\n    if (!this._isActive) return\n    this.isPlaying = true\n  }\n\n  stop(): void {\n    this.isPlaying = false\n  }\n\n  goToFrame(localFrame: number): void {\n    if (!this._isActive) return\n    const targetLocalFrame = Math.max(0, Math.min(localFrame, this.state.animNumFrames - 1))\n    if (this.currentLocalFrame !== targetLocalFrame) {\n      this.currentLocalFrame = targetLocalFrame\n      this.timeAccumulator = 0\n      const absoluteFrame = this.state.animFrameOffset + this.currentLocalFrame\n      this.frameAttribute.setX(this.instanceIndex, absoluteFrame)\n      this.frameAttribute.needsUpdate = true // Mark manager for update\n    }\n  }\n\n  setFrameDuration(newFrameDuration: number): void {\n    if (newFrameDuration > 0) {\n      this.state = { ...this.state, frameDuration: newFrameDuration }\n    }\n  }\n\n  getResource(): SpriteResource {\n    return this.resource\n  }\n\n  releaseInstanceSlot(): void {\n    this.instanceManager.releaseInstanceSlot(this.instanceIndex)\n  }\n}\n\nexport class TiledSprite {\n  public readonly id: string\n  private animator: SpriteAnimator\n  private _animations: Map<string, Animation>\n  private _currentAnimation: Animation\n  private _transformObject: THREE.Object3D\n\n  private _reusableMatrix: THREE.Matrix4\n  private _reusableAnimGeomScale: THREE.Vector3\n  private _isVisibleState: boolean = true\n  private originalDefinition: SpriteDefinition\n\n  constructor(\n    id: string,\n    userSpriteDefinition: SpriteDefinition,\n    animator: SpriteAnimator,\n    animationInstanceParams: Array<{\n      name: string\n      state: ResolvedAnimationState\n      resource: SpriteResource\n      index: number\n      instanceManager: InstanceManager\n      frameAttribute: THREE.InstancedBufferAttribute\n      flipAttribute: THREE.InstancedBufferAttribute\n    }>,\n  ) {\n    this.id = id\n    this.originalDefinition = userSpriteDefinition\n    this.animator = animator\n    this._transformObject = new THREE.Object3D()\n    this._reusableMatrix = new THREE.Matrix4()\n    this._reusableAnimGeomScale = new THREE.Vector3()\n\n    const initialScale = userSpriteDefinition.scale ?? DEFAULT_SCALE\n    this._transformObject.scale.set(initialScale, initialScale, initialScale)\n\n    this._animations = new Map()\n    for (const params of animationInstanceParams) {\n      const anim = new Animation(\n        params.name,\n        params.state,\n        params.resource,\n        params.index,\n        params.instanceManager,\n        params.frameAttribute,\n        params.flipAttribute,\n      )\n      this._animations.set(params.name, anim)\n    }\n\n    const initialAnim = this._animations.get(userSpriteDefinition.initialAnimation)\n    if (!initialAnim) {\n      throw new Error(\n        `[TiledSprite] Initial animation \"${userSpriteDefinition.initialAnimation}\" not found for sprite \"${this.id}\".`,\n      )\n    }\n    this._currentAnimation = initialAnim\n    const initialWorldMatrix = this._calculateAnimationWorldMatrix(this._currentAnimation.state)\n    this._currentAnimation.activate(initialWorldMatrix)\n    this._isVisibleState = true\n  }\n\n  private _calculateAnimationWorldMatrix(animState: ResolvedAnimationState): THREE.Matrix4 {\n    const matrix = this._reusableMatrix\n    const animGeomScale = this._reusableAnimGeomScale\n    const worldHeight = this._transformObject.scale.y\n    const frameAspectRatio = animState.sheetTilesetWidth / animState.sheetNumFrames / animState.sheetTilesetHeight\n    const worldWidth = worldHeight * frameAspectRatio\n    animGeomScale.set(worldWidth, worldHeight, this._transformObject.scale.z)\n    matrix.compose(this._transformObject.position, this._transformObject.quaternion, animGeomScale)\n    return matrix\n  }\n\n  public get currentAnimation(): Animation {\n    return this._currentAnimation\n  }\n\n  private updateCurrentAnimationVisuals(): void {\n    if (this._isVisibleState) {\n      const currentAnim = this.currentAnimation\n      if (currentAnim) {\n        const finalMatrix = this._calculateAnimationWorldMatrix(currentAnim.state)\n        currentAnim.updateVisuals(finalMatrix)\n      }\n    }\n  }\n\n  setPosition(position: THREE.Vector3): void {\n    this._transformObject.position.copy(position)\n    this.updateCurrentAnimationVisuals()\n  }\n\n  setRotation(rotation: THREE.Quaternion): void {\n    this._transformObject.quaternion.copy(rotation)\n    this.updateCurrentAnimationVisuals()\n  }\n\n  setScale(scale: THREE.Vector3): void {\n    this._transformObject.scale.copy(scale)\n    this.updateCurrentAnimationVisuals()\n  }\n\n  getScale(): THREE.Vector3 {\n    return this._transformObject.scale.clone()\n  }\n\n  setTransform(position: THREE.Vector3, rotation: THREE.Quaternion, newScale: THREE.Vector3): void {\n    this._transformObject.position.copy(position)\n    this._transformObject.quaternion.copy(rotation)\n    this._transformObject.scale.copy(newScale)\n    this.updateCurrentAnimationVisuals()\n  }\n\n  play(): void {\n    this.currentAnimation.play()\n  }\n  stop(): void {\n    this.currentAnimation.stop()\n  }\n  goToFrame(frame: number): void {\n    this.currentAnimation.goToFrame(frame)\n  }\n  setFrameDuration(newFrameDuration: number): void {\n    this.currentAnimation.setFrameDuration(newFrameDuration)\n  }\n\n  isPlaying(): boolean {\n    return this.currentAnimation.isPlaying\n  }\n\n  async setAnimation(animationName: string): Promise<void> {\n    const newAnim = this._animations.get(animationName)\n    if (!newAnim) {\n      throw new Error(`[TiledSprite] Animation \"${animationName}\" not found for sprite \"${this.id}\".`)\n    }\n\n    const switchingToSameAnimation = this._currentAnimation.name === animationName\n    const oldAnim = this._currentAnimation\n\n    if (!switchingToSameAnimation || !this._isVisibleState) {\n      oldAnim?.deactivate()\n    }\n\n    this._currentAnimation = newAnim\n\n    if (this._isVisibleState) {\n      const finalMatrix = this._calculateAnimationWorldMatrix(newAnim.state)\n      newAnim.activate(finalMatrix)\n    } else {\n      newAnim.deactivate()\n    }\n  }\n\n  update(deltaTime: number): void {\n    if (this.visible) {\n      this.currentAnimation.updateTime(deltaTime)\n    }\n  }\n\n  destroy(): void {\n    this._animations.forEach((anim) => {\n      anim.deactivate()\n      anim.releaseInstanceSlot()\n    })\n    this._animations.clear()\n    this._isVisibleState = false\n  }\n\n  getCurrentAnimationName(): string {\n    return this._currentAnimation.name\n  }\n\n  getWorldTransform(): THREE.Matrix4 {\n    return this._calculateAnimationWorldMatrix(this._currentAnimation.state)\n  }\n\n  getWorldPlaneSize(): THREE.Vector2 {\n    const animState = this._currentAnimation.state\n    const worldHeight = this._transformObject.scale.y\n    const frameActualWidthPx = animState.sheetTilesetWidth / animState.sheetNumFrames\n    const frameAspectRatio = frameActualWidthPx / animState.sheetTilesetHeight\n    const worldWidth = worldHeight * frameAspectRatio\n    return new THREE.Vector2(worldWidth, worldHeight)\n  }\n\n  get visible(): boolean {\n    return this._isVisibleState\n  }\n\n  set visible(value: boolean) {\n    if (this._isVisibleState === value) {\n      return\n    }\n    this._isVisibleState = value\n    if (value) {\n      const finalMatrix = this._calculateAnimationWorldMatrix(this._currentAnimation.state)\n      this._currentAnimation.activate(finalMatrix)\n    } else {\n      this._currentAnimation.deactivate()\n    }\n  }\n\n  public get definition(): SpriteDefinition {\n    return this.originalDefinition\n  }\n\n  public get currentTransform(): {\n    position: THREE.Vector3\n    quaternion: THREE.Quaternion\n    scale: THREE.Vector3\n  } {\n    return {\n      position: this._transformObject.position.clone(),\n      quaternion: this._transformObject.quaternion.clone(),\n      scale: this._transformObject.scale.clone(),\n    }\n  }\n}\n\ninterface SpriteAnimatorInstanceManager {\n  instanceManager: InstanceManager\n  frameAttribute: THREE.InstancedBufferAttribute\n  flipAttribute: THREE.InstancedBufferAttribute\n  uvTileSize: THREE.Vector2\n}\n\nexport class SpriteAnimator {\n  private instances: Map<string, TiledSprite> = new Map()\n  private _idCounter = 0\n  private instanceManagers: Map<string, SpriteAnimatorInstanceManager> = new Map()\n\n  constructor(private scene: Scene) {}\n\n  private createSpriteAnimationMaterial(\n    resource: SpriteResource,\n    frameAttribute: THREE.InstancedBufferAttribute,\n    flipAttribute: THREE.InstancedBufferAttribute,\n    materialFactory: () => NodeMaterial,\n  ): NodeMaterial {\n    const texture = resource.texture\n    const sheetProps = resource.sheetProperties\n\n    const uvTileWidth = 1.0 / sheetProps.sheetNumFrames\n    const uvTileHeight = 1.0\n    const uvTileSize = new THREE.Vector2(uvTileWidth, uvTileHeight)\n\n    const tileSizeUniform = uniform(uvTileSize)\n    const epsilon = float(0.000001)\n\n    const baseUV = uv()\n    const oneFloat = float(1.0)\n\n    const a_frameIndex = bufferAttribute(frameAttribute)\n    const a_flip = bufferAttribute(flipAttribute)\n\n    const calculatedTileCoordX = a_frameIndex.mul(tileSizeUniform.x)\n    const calculatedTileCoord = vec2(calculatedTileCoordX, float(0))\n\n    const flippedX = mix(baseUV.x, oneFloat.sub(baseUV.x), a_flip.x)\n    const flippedY = mix(baseUV.y, oneFloat.sub(baseUV.y), a_flip.y)\n    const finalLocalUV = vec2(flippedX, flippedY)\n\n    const mapNode = tslTexture(texture)\n    const finalUV = finalLocalUV.mul(tileSizeUniform).min(tileSizeUniform.sub(epsilon)).add(calculatedTileCoord)\n    const sampledColor = mapNode.sample(finalUV)\n\n    const material = materialFactory()\n    material.colorNode = sampledColor\n\n    return material\n  }\n\n  private getOrCreateInstanceManager(\n    resource: SpriteResource,\n    maxInstances: number,\n    renderOrder: number,\n    depthWrite: boolean,\n    materialFactory: () => NodeMaterial,\n  ): SpriteAnimatorInstanceManager {\n    const key = `${resource.sheetProperties.imagePath}_${maxInstances}_${renderOrder}_${depthWrite}`\n    let manager = this.instanceManagers.get(key)\n\n    if (!manager) {\n      const geometry = new THREE.PlaneGeometry(1, 1)\n      const frameArray = new Float32Array(maxInstances)\n      const frameAttribute = new THREE.InstancedBufferAttribute(frameArray, 1)\n      frameAttribute.setUsage(THREE.DynamicDrawUsage)\n\n      const flipArray = new Float32Array(maxInstances * 2)\n      const flipAttribute = new THREE.InstancedBufferAttribute(flipArray, 2)\n      flipAttribute.setUsage(THREE.DynamicDrawUsage)\n\n      const material = this.createSpriteAnimationMaterial(resource, frameAttribute, flipAttribute, materialFactory)\n\n      geometry.setAttribute(\"a_frameIndexInstanced\", frameAttribute)\n      geometry.setAttribute(\"a_flipInstanced\", flipAttribute)\n\n      for (let i = 0; i < maxInstances; i++) {\n        flipAttribute.setXY(i, 0.0, 0.0)\n      }\n      flipAttribute.needsUpdate = true\n\n      const instanceManager = resource.createInstanceManager(geometry, material, {\n        maxInstances,\n        renderOrder,\n        depthWrite,\n        name: `SpriteAnimator_${key}`,\n      })\n\n      const uvTileWidth = 1.0 / resource.sheetProperties.sheetNumFrames\n      const uvTileSize = new THREE.Vector2(uvTileWidth, 1.0)\n\n      manager = {\n        instanceManager,\n        frameAttribute,\n        flipAttribute,\n        uvTileSize,\n      }\n\n      this.instanceManagers.set(key, manager)\n    }\n\n    return manager\n  }\n\n  async createSprite(\n    userSpriteDefinition: SpriteDefinition,\n    materialFactory?: () => NodeMaterial,\n  ): Promise<TiledSprite> {\n    const id = userSpriteDefinition.id ?? `sprite_${this._idCounter++}`\n    const animationInstanceParams: Array<{\n      name: string\n      state: ResolvedAnimationState\n      resource: SpriteResource\n      index: number\n      instanceManager: InstanceManager\n      frameAttribute: THREE.InstancedBufferAttribute\n      flipAttribute: THREE.InstancedBufferAttribute\n    }> = []\n\n    const resolvedMaterialFactory =\n      materialFactory ??\n      (() =>\n        new MeshBasicNodeMaterial({\n          transparent: true,\n          alphaTest: 0.1,\n          depthWrite: true,\n        }))\n\n    // Track instance managers as we encounter resources\n    const resourceManagers = new Map<SpriteResource, SpriteAnimatorInstanceManager>()\n\n    for (const animName in userSpriteDefinition.animations) {\n      const animDef = userSpriteDefinition.animations[animName]\n      const resource = animDef.resource\n\n      // Get or create instance manager for this resource\n      let managerInfo = resourceManagers.get(resource)\n      if (!managerInfo) {\n        const maxInstances = userSpriteDefinition.maxInstances ?? 1024\n        const renderOrder = userSpriteDefinition.renderOrder ?? 0\n        const depthWrite = userSpriteDefinition.depthWrite ?? true\n        managerInfo = this.getOrCreateInstanceManager(\n          resource,\n          maxInstances,\n          renderOrder,\n          depthWrite,\n          resolvedMaterialFactory,\n        )\n        resourceManagers.set(resource, managerInfo)\n      }\n\n      const instanceIndex = managerInfo.instanceManager.acquireInstanceSlot()\n\n      const resolvedState: ResolvedAnimationState = {\n        imagePath: resource.sheetProperties.imagePath,\n        sheetTilesetWidth: resource.sheetProperties.sheetTilesetWidth,\n        sheetTilesetHeight: resource.sheetProperties.sheetTilesetHeight,\n        sheetNumFrames: resource.sheetProperties.sheetNumFrames,\n        animNumFrames: animDef.animNumFrames ?? resource.sheetProperties.sheetNumFrames,\n        animFrameOffset: animDef.animFrameOffset ?? 0,\n        frameDuration: animDef.frameDuration ?? DEFAULT_FRAME_DURATION,\n        loop: animDef.loop ?? true,\n        initialFrame: animDef.initialFrame ?? DEFAULT_INITIAL_FRAME,\n        flipX: animDef.flipX ?? DEFAULT_FLIP_X,\n        flipY: animDef.flipY ?? DEFAULT_FLIP_Y,\n        texture: resource.texture,\n      }\n\n      animationInstanceParams.push({\n        name: animName,\n        state: resolvedState,\n        resource,\n        index: instanceIndex,\n        instanceManager: managerInfo.instanceManager,\n        frameAttribute: managerInfo.frameAttribute,\n        flipAttribute: managerInfo.flipAttribute,\n      })\n    }\n\n    if (\n      !userSpriteDefinition.initialAnimation ||\n      !userSpriteDefinition.animations[userSpriteDefinition.initialAnimation]\n    ) {\n      let found = false\n      for (const p of animationInstanceParams) if (p.name === userSpriteDefinition.initialAnimation) found = true\n      if (!found) {\n        for (const params of animationInstanceParams) {\n          params.instanceManager.releaseInstanceSlot(params.index)\n        }\n        throw new Error(\n          `[SpriteAnimator] initialAnimation \"${userSpriteDefinition.initialAnimation}\" not found or invalid for sprite \"${id}\".`,\n        )\n      }\n    }\n    const tiledSprite = new TiledSprite(id, userSpriteDefinition, this, animationInstanceParams)\n    this.instances.set(id, tiledSprite)\n    return tiledSprite\n  }\n\n  update(deltaTime: number): void {\n    for (const sprite of this.instances.values()) {\n      sprite.update(deltaTime)\n    }\n  }\n\n  removeSprite(id: string): void {\n    const sprite = this.instances.get(id)\n    if (sprite) {\n      sprite.destroy()\n      this.instances.delete(id)\n    }\n  }\n\n  removeAllSprites(): void {\n    const ids = Array.from(this.instances.keys())\n    for (const id of ids) {\n      this.removeSprite(id)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/animation/SpriteParticleGenerator.ts",
    "content": "import * as THREE from \"three\"\nimport {\n  uniform,\n  texture as tslTexture,\n  uv,\n  float,\n  vec2,\n  vec3,\n  vec4,\n  bufferAttribute,\n  step,\n  max,\n  sin,\n  cos,\n  positionLocal,\n  mat3,\n  mix,\n  floor,\n  mod,\n} from \"three/tsl\"\nimport { MeshBasicNodeMaterial, NodeMaterial } from \"three/webgpu\"\nimport type { SpriteResource, InstanceManager } from \"../SpriteResourceManager.js\"\n\nexport interface ParticleEffectParameters {\n  resource: SpriteResource\n  animNumFrames?: number\n  animFrameOffset?: number\n  frameDuration?: number\n  loop?: boolean\n  scale?: number\n  renderOrder?: number\n  depthWrite?: boolean\n  maxParticles: number\n  lifetimeMsMin: number\n  lifetimeMsMax: number\n  origins: THREE.Vector3[]\n  spawnRadius: number | THREE.Vector3\n  initialVelocityMin: THREE.Vector3\n  initialVelocityMax: THREE.Vector3\n  angularVelocityMin: THREE.Vector3\n  angularVelocityMax: THREE.Vector3\n  gravity?: THREE.Vector3\n  randomGravityFactorMinMax?: THREE.Vector2\n  scaleOverLifeMinMax?: THREE.Vector2\n  fadeOut?: boolean\n  materialFactory?: () => NodeMaterial\n}\n\ninterface AutoSpawnConfig {\n  resolvedParams: ParticleEffectParameters\n  originalOverrides?: Partial<ParticleEffectParameters>\n  ratePerSecond: number\n  accumulator: number\n}\n\ninterface ParticleSlot {\n  isActive: boolean\n  spawnTime: number\n  lifespan: number\n}\n\nexport class SpriteParticleGenerator {\n  private scene: THREE.Scene\n  private baseConfig: ParticleEffectParameters\n  private autoSpawnConfig: AutoSpawnConfig | null = null\n  private _currentOriginIndex: number = 0\n\n  private instanceManager: InstanceManager | null = null\n  private material: NodeMaterial | null = null\n  private texture: THREE.DataTexture | null = null\n\n  private particleDataAttribute: THREE.InstancedBufferAttribute | null = null // [originX, originY, originZ, spawnTime]\n  private velocityAttribute: THREE.InstancedBufferAttribute | null = null // [velX, velY, velZ, gravityFactor]\n  private angularVelAttribute: THREE.InstancedBufferAttribute | null = null // [angVelX, angVelY, angVelZ, lifespan]\n  private scaleDataAttribute: THREE.InstancedBufferAttribute | null = null // [initialScale, scaleMin, scaleMax, randomSeed]\n\n  private timeUniform: ReturnType<typeof uniform<number>>\n  private gravityUniform: ReturnType<typeof uniform<THREE.Vector3>>\n  private animationUniform: ReturnType<typeof uniform<THREE.Vector4>> // [frameDuration, animNumFrames, loop, animFrameOffset]\n  private sheetNumFramesUniform: ReturnType<typeof uniform<number>>\n\n  private particleSlots: ParticleSlot[] = []\n  private currentTime: number = 0\n  private maxParticles: number\n  private isInitialized: boolean = false\n\n  constructor(scene: THREE.Scene, initialBaseConfig: ParticleEffectParameters) {\n    this.scene = scene\n    this.baseConfig = { ...initialBaseConfig }\n    this.maxParticles = this.baseConfig.maxParticles\n\n    if (!this.baseConfig.resource) {\n      throw new Error(\"[SpriteParticleGenerator] resource is mandatory in initialBaseConfig.\")\n    }\n\n    this.timeUniform = uniform(0)\n    this.gravityUniform = uniform(this.baseConfig.gravity || new THREE.Vector3(0, -9.8, 0))\n    this.animationUniform = uniform(new THREE.Vector4())\n    this.sheetNumFramesUniform = uniform(1)\n  }\n\n  private async _ensureInitialized(): Promise<void> {\n    if (this.isInitialized) return\n    await this._initializeGPUParticleSystem()\n    this.isInitialized = true\n  }\n\n  private async _initializeGPUParticleSystem(): Promise<void> {\n    const resource = this.baseConfig.resource\n\n    this.texture = resource.texture\n\n    // Set up animation uniforms from particle config\n    const frameDuration = (this.baseConfig.frameDuration ?? 100) / 1000 // Convert to seconds\n    const animNumFrames = this.baseConfig.animNumFrames ?? resource.sheetProperties.sheetNumFrames\n    const loop = (this.baseConfig.loop ?? true) ? 1.0 : 0.0 // Default to true\n    const animFrameOffset = this.baseConfig.animFrameOffset ?? 0\n\n    this.animationUniform.value.set(frameDuration, animNumFrames, loop, animFrameOffset)\n    this.sheetNumFramesUniform.value = resource.sheetProperties.sheetNumFrames\n\n    const particleData = new Float32Array(this.maxParticles * 4)\n    const velocityData = new Float32Array(this.maxParticles * 4)\n    const angularVelData = new Float32Array(this.maxParticles * 4)\n    const scaleData = new Float32Array(this.maxParticles * 4)\n\n    this.particleDataAttribute = new THREE.InstancedBufferAttribute(particleData, 4)\n    this.velocityAttribute = new THREE.InstancedBufferAttribute(velocityData, 4)\n    this.angularVelAttribute = new THREE.InstancedBufferAttribute(angularVelData, 4)\n    this.scaleDataAttribute = new THREE.InstancedBufferAttribute(scaleData, 4)\n\n    this.particleDataAttribute.setUsage(THREE.DynamicDrawUsage)\n    this.velocityAttribute.setUsage(THREE.DynamicDrawUsage)\n    this.angularVelAttribute.setUsage(THREE.DynamicDrawUsage)\n    this.scaleDataAttribute.setUsage(THREE.DynamicDrawUsage)\n\n    for (let i = 0; i < this.maxParticles; i++) {\n      this.particleSlots.push({ isActive: false, spawnTime: 0, lifespan: 0 })\n\n      particleData[i * 4 + 3] = -1 // Mark as inactive with negative spawn time\n    }\n\n    const frameAspectRatio =\n      this.texture.image.width / resource.sheetProperties.sheetNumFrames / this.texture.image.height\n    const scale = this.baseConfig.scale ?? 1.0\n    const geometry = new THREE.PlaneGeometry(scale * frameAspectRatio, scale)\n\n    geometry.setAttribute(\"a_particleData\", this.particleDataAttribute)\n    geometry.setAttribute(\"a_velocity\", this.velocityAttribute)\n    geometry.setAttribute(\"a_angularVel\", this.angularVelAttribute)\n    geometry.setAttribute(\"a_scaleData\", this.scaleDataAttribute)\n\n    const materialFactory =\n      this.baseConfig.materialFactory ??\n      (() =>\n        new MeshBasicNodeMaterial({\n          transparent: true,\n          alphaTest: 0.01,\n          side: THREE.DoubleSide,\n          depthWrite: this.baseConfig.depthWrite ?? false,\n        }))\n    const material = this._createGPUMaterial(materialFactory)\n    this.instanceManager = resource.createInstanceManager(geometry, material, {\n      maxInstances: this.maxParticles,\n      renderOrder: this.baseConfig.renderOrder ?? 0,\n      depthWrite: this.baseConfig.depthWrite ?? true,\n      name: `SpriteParticleGenerator_${resource.sheetProperties.imagePath.replace(/[^a-zA-Z0-9_]/g, \"_\")}`,\n      matrix: new THREE.Matrix4(),\n    })\n  }\n\n  private _createGPUMaterial(materialFactory: () => NodeMaterial): NodeMaterial {\n    const a_particleData = bufferAttribute(this.particleDataAttribute!)\n    const a_velocity = bufferAttribute(this.velocityAttribute!)\n    const a_angularVel = bufferAttribute(this.angularVelAttribute!)\n    const a_scaleData = bufferAttribute(this.scaleDataAttribute!)\n\n    const origin = vec3(a_particleData.x, a_particleData.y, a_particleData.z)\n    const spawnTime = a_particleData.w\n\n    const initialVelocity = vec3(a_velocity.x, a_velocity.y, a_velocity.z)\n    const gravityFactor = a_velocity.w\n\n    const angularVelocity = vec3(a_angularVel.x, a_angularVel.y, a_angularVel.z)\n    const lifespan = a_angularVel.w\n\n    const initialScale = a_scaleData.x\n    const scaleMin = a_scaleData.y\n    const scaleMax = a_scaleData.z\n    const randomSeed = a_scaleData.w\n\n    const age = this.timeUniform.sub(spawnTime)\n    const normalizedAge = age.div(lifespan)\n    const isAlive = step(float(0), spawnTime).mul(step(normalizedAge, float(1.0)))\n\n    const gravity = this.gravityUniform.mul(gravityFactor)\n    const velocityContribution = initialVelocity.mul(age)\n    const gravityContribution = gravity.mul(age).mul(age).mul(float(0.5))\n    const currentPosition = origin.add(velocityContribution).add(gravityContribution)\n\n    const rotationAmount = angularVelocity.mul(age)\n    const cosX = cos(rotationAmount.x)\n    const sinX = sin(rotationAmount.x)\n    const cosY = cos(rotationAmount.y)\n    const sinY = sin(rotationAmount.y)\n    const cosZ = cos(rotationAmount.z)\n    const sinZ = sin(rotationAmount.z)\n\n    const rotationMatrix = mat3(\n      cosY.mul(cosZ),\n      cosY.mul(sinZ).negate(),\n      sinY,\n      sinX.mul(sinY).mul(cosZ).add(cosX.mul(sinZ)),\n      sinX.mul(sinY).mul(sinZ).negate().add(cosX.mul(cosZ)),\n      sinX.mul(cosY).negate(),\n      cosX.mul(sinY).mul(cosZ).negate().add(sinX.mul(sinZ)),\n      cosX.mul(sinY).mul(sinZ).add(sinX.mul(cosZ)),\n      cosX.mul(cosY),\n    )\n\n    const rotatedVertexPosition = rotationMatrix.mul(positionLocal)\n\n    let currentScale = initialScale\n    if (this.baseConfig.scaleOverLifeMinMax) {\n      const scaleMultiplier = mix(scaleMin, scaleMax, normalizedAge)\n      currentScale = initialScale.mul(scaleMultiplier)\n    }\n\n    const scaledPosition = rotatedVertexPosition.mul(currentScale)\n    const finalPosition = scaledPosition.add(currentPosition)\n\n    let opacity = float(1.0)\n    if (this.baseConfig.fadeOut) {\n      const fadeStart = float(0.7)\n      const fadeProgress = max(float(0), normalizedAge.sub(fadeStart).div(float(1.0).sub(fadeStart)))\n      opacity = float(1.0).sub(fadeProgress)\n    }\n    opacity = opacity.mul(isAlive)\n\n    // Dynamic frame calculation based on particle age\n    const frameDuration = this.animationUniform.x\n    const animNumFrames = this.animationUniform.y\n    const loopFlag = this.animationUniform.z\n    const animFrameOffset = this.animationUniform.w\n\n    const frameFloat = age.div(frameDuration)\n    const rawFrameIndex = floor(frameFloat)\n\n    // Handle looping vs clamping\n    const maxFrame = animNumFrames.sub(float(1))\n    const clampedFrame = max(float(0), rawFrameIndex).min(maxFrame)\n    const loopedFrame = rawFrameIndex.mod(animNumFrames)\n    const finalLocalFrame = mix(clampedFrame, loopedFrame, loopFlag)\n\n    const frameIndex = animFrameOffset.add(finalLocalFrame)\n\n    const uvTileWidth = float(1.0).div(this.sheetNumFramesUniform)\n    const uvOffset = vec2(frameIndex.mul(uvTileWidth), float(0))\n    const uvSize = vec2(uvTileWidth, float(1.0))\n\n    const baseUV = uv()\n    const finalUV = baseUV.mul(uvSize).add(uvOffset)\n\n    const mapNode = tslTexture(this.texture!)\n    const sampledColor = mapNode.sample(finalUV)\n\n    this.material = materialFactory()\n\n    const finalColor = vec4(sampledColor.rgb, sampledColor.a.mul(opacity))\n    this.material.colorNode = finalColor\n    this.material.positionNode = finalPosition\n\n    return this.material\n  }\n\n  private _resolveCurrentOrigin(originsArray: THREE.Vector3[]): THREE.Vector3 {\n    const currentOrigin = originsArray[this._currentOriginIndex]\n    this._currentOriginIndex = (this._currentOriginIndex + 1) % originsArray.length\n    return currentOrigin\n  }\n\n  public getActiveParticleCount(): number {\n    return this.particleSlots.filter((slot) => slot.isActive).length\n  }\n\n  private _resolveSpawnRadius(spawnRadius: number | THREE.Vector3): THREE.Vector3 {\n    return typeof spawnRadius === \"number\" ? new THREE.Vector3(spawnRadius, spawnRadius, spawnRadius) : spawnRadius\n  }\n\n  private _spawnParticle(effectiveParams: Readonly<ParticleEffectParameters>, spawnRadiusVec: THREE.Vector3): void {\n    if (!this.instanceManager?.hasFreeIndices) return\n\n    const index = this.instanceManager!.acquireInstanceSlot()\n    const particleOrigin = this._resolveCurrentOrigin(effectiveParams.origins)\n\n    const spawnOffset = new THREE.Vector3(\n      (Math.random() - 0.5) * 2 * spawnRadiusVec.x,\n      (Math.random() - 0.5) * 2 * spawnRadiusVec.y,\n      (Math.random() - 0.5) * 2 * spawnRadiusVec.z,\n    )\n    const initialPosition = new THREE.Vector3().copy(particleOrigin).add(spawnOffset)\n\n    const velocity = new THREE.Vector3(\n      THREE.MathUtils.randFloat(effectiveParams.initialVelocityMin.x, effectiveParams.initialVelocityMax.x),\n      THREE.MathUtils.randFloat(effectiveParams.initialVelocityMin.y, effectiveParams.initialVelocityMax.y),\n      THREE.MathUtils.randFloat(effectiveParams.initialVelocityMin.z, effectiveParams.initialVelocityMax.z),\n    )\n\n    const angularVelocity = new THREE.Vector3(\n      THREE.MathUtils.randFloat(effectiveParams.angularVelocityMin.x, effectiveParams.angularVelocityMax.x),\n      THREE.MathUtils.randFloat(effectiveParams.angularVelocityMin.y, effectiveParams.angularVelocityMax.y),\n      THREE.MathUtils.randFloat(effectiveParams.angularVelocityMin.z, effectiveParams.angularVelocityMax.z),\n    )\n\n    const lifespan = THREE.MathUtils.randFloat(effectiveParams.lifetimeMsMin, effectiveParams.lifetimeMsMax) / 1000\n\n    let gravityFactor = 1.0\n    if (effectiveParams.randomGravityFactorMinMax) {\n      gravityFactor = THREE.MathUtils.randFloat(\n        effectiveParams.randomGravityFactorMinMax.x,\n        effectiveParams.randomGravityFactorMinMax.y,\n      )\n    }\n\n    const initialScale = effectiveParams.scale ?? 1.0\n    let scaleMin = initialScale\n    let scaleMax = initialScale\n    if (effectiveParams.scaleOverLifeMinMax) {\n      scaleMin = initialScale * effectiveParams.scaleOverLifeMinMax.x\n      scaleMax = initialScale * effectiveParams.scaleOverLifeMinMax.y\n    }\n\n    this.particleDataAttribute!.setXYZW(\n      index,\n      initialPosition.x,\n      initialPosition.y,\n      initialPosition.z,\n      this.currentTime,\n    )\n    this.velocityAttribute!.setXYZW(index, velocity.x, velocity.y, velocity.z, gravityFactor)\n    this.angularVelAttribute!.setXYZW(index, angularVelocity.x, angularVelocity.y, angularVelocity.z, lifespan)\n    this.scaleDataAttribute!.setXYZW(index, initialScale, scaleMin, scaleMax, Math.random())\n\n    this.particleSlots[index] = {\n      isActive: true,\n      spawnTime: this.currentTime,\n      lifespan: lifespan,\n    }\n\n    this.particleDataAttribute!.needsUpdate = true\n    this.velocityAttribute!.needsUpdate = true\n    this.angularVelAttribute!.needsUpdate = true\n    this.scaleDataAttribute!.needsUpdate = true\n  }\n\n  public async spawnParticles(count: number, overrides: Partial<ParticleEffectParameters> = {}): Promise<void> {\n    await this._ensureInitialized()\n    if (count <= 0) return\n\n    const finalParams: ParticleEffectParameters = {\n      ...this.baseConfig,\n      ...overrides,\n    }\n\n    const spawnRadiusVec = this._resolveSpawnRadius(finalParams.spawnRadius)\n\n    for (let i = 0; i < count; i++) {\n      this._spawnParticle(finalParams, spawnRadiusVec)\n    }\n  }\n\n  public setAutoSpawn(ratePerSecond: number, autoSpawnParamOverrides: Partial<ParticleEffectParameters> = {}): void {\n    if (ratePerSecond <= 0) {\n      this.stopAutoSpawn()\n      return\n    }\n    const originalOverridesToStore =\n      Object.keys(autoSpawnParamOverrides).length > 0 ? { ...autoSpawnParamOverrides } : undefined\n\n    this.autoSpawnConfig = {\n      resolvedParams: { ...this.baseConfig, ...autoSpawnParamOverrides },\n      originalOverrides: originalOverridesToStore,\n      ratePerSecond: ratePerSecond,\n      accumulator: 0,\n    }\n  }\n\n  public hasAutoSpawn(): boolean {\n    return this.autoSpawnConfig !== null\n  }\n\n  public stopAutoSpawn(): void {\n    this.autoSpawnConfig = null\n  }\n\n  public async update(deltaTimeMs: number): Promise<void> {\n    await this._ensureInitialized()\n\n    this.currentTime += deltaTimeMs / 1000\n    this.timeUniform.value = this.currentTime\n\n    if (this.autoSpawnConfig) {\n      this.autoSpawnConfig.accumulator += deltaTimeMs\n      const particlesToSpawnThisFrame = Math.floor(\n        this.autoSpawnConfig.accumulator * (this.autoSpawnConfig.ratePerSecond / 1000),\n      )\n\n      if (particlesToSpawnThisFrame > 0) {\n        const spawnRadiusVec = this._resolveSpawnRadius(this.autoSpawnConfig.resolvedParams.spawnRadius)\n        for (let i = 0; i < particlesToSpawnThisFrame; i++) {\n          this._spawnParticle(this.autoSpawnConfig.resolvedParams, spawnRadiusVec)\n        }\n        this.autoSpawnConfig.accumulator -= (particlesToSpawnThisFrame * 1000) / this.autoSpawnConfig.ratePerSecond\n      }\n    }\n\n    for (let i = 0; i < this.particleSlots.length; i++) {\n      const slot = this.particleSlots[i]\n      if (slot.isActive && this.currentTime - slot.spawnTime >= slot.lifespan) {\n        slot.isActive = false\n        this.particleDataAttribute!.setW(i, -1)\n        this.instanceManager!.releaseInstanceSlot(i)\n        this.particleDataAttribute!.needsUpdate = true\n      }\n    }\n  }\n\n  public dispose(): void {\n    if (this.instanceManager) {\n      this.instanceManager.dispose()\n      this.material?.dispose()\n    }\n    this.stopAutoSpawn()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/canvas.ts",
    "content": "import { GPUCanvasContextMock } from \"bun-webgpu\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { SuperSampleType } from \"./WGPURenderer.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport { toArrayBuffer } from \"bun:ffi\"\nimport { Jimp } from \"jimp\"\n\n// @ts-ignore\nimport shaderTemplate from \"./shaders/supersampling.wgsl\" with { type: \"text\" }\n\nconst WORKGROUP_SIZE = 4\nconst SUPERSAMPLING_COMPUTE_SHADER = shaderTemplate.replace(/\\${WORKGROUP_SIZE}/g, WORKGROUP_SIZE.toString())\n\nexport enum SuperSampleAlgorithm {\n  STANDARD = 0,\n  PRE_SQUEEZED = 1,\n}\n\nexport class CLICanvas {\n  private device: GPUDevice\n  private readbackBuffer: GPUBuffer | null = null\n  private width: number\n  private height: number\n  private gpuCanvasContext: GPUCanvasContextMock\n\n  public superSampleDrawTimeMs: number = 0\n  public mapAsyncTimeMs: number = 0\n  public superSample: SuperSampleType = SuperSampleType.GPU\n\n  // Compute shader super sampling\n  private computePipeline: GPUComputePipeline | null = null\n  private computeBindGroupLayout: GPUBindGroupLayout | null = null\n  private computeOutputBuffer: GPUBuffer | null = null\n  private computeParamsBuffer: GPUBuffer | null = null\n  private computeReadbackBuffer: GPUBuffer | null = null\n  private updateScheduled: boolean = false\n  private screenshotGPUBuffer: GPUBuffer | null = null\n  private superSampleAlgorithm: SuperSampleAlgorithm = SuperSampleAlgorithm.STANDARD\n  private destroyed: boolean = false\n\n  constructor(\n    device: GPUDevice,\n    width: number,\n    height: number,\n    superSample: SuperSampleType,\n    sampleAlgo: SuperSampleAlgorithm = SuperSampleAlgorithm.STANDARD,\n  ) {\n    this.device = device\n    this.width = width\n    this.height = height\n    this.superSample = superSample\n    this.gpuCanvasContext = new GPUCanvasContextMock(this as unknown as HTMLCanvasElement, width, height)\n    this.superSampleAlgorithm = sampleAlgo\n  }\n\n  public destroy(): void {\n    this.destroyed = true\n  }\n\n  public setSuperSampleAlgorithm(superSampleAlgorithm: SuperSampleAlgorithm): void {\n    this.superSampleAlgorithm = superSampleAlgorithm\n    this.scheduleUpdateComputeBuffers()\n  }\n\n  public getSuperSampleAlgorithm(): SuperSampleAlgorithm {\n    return this.superSampleAlgorithm\n  }\n\n  getContext(type: string, attrs?: WebGLContextAttributes) {\n    if (type === \"webgpu\") {\n      this.updateReadbackBuffer(this.width, this.height)\n      this.updateComputeBuffers(this.width, this.height)\n      return this.gpuCanvasContext\n    }\n    throw new Error(`getContext not implemented: ${type}`)\n  }\n\n  setSize(width: number, height: number) {\n    this.width = width\n    this.height = height\n    this.gpuCanvasContext.setSize(width, height)\n    this.updateReadbackBuffer(width, height)\n    this.scheduleUpdateComputeBuffers()\n  }\n\n  addEventListener(event: string, listener: any, options?: any) {\n    console.error(\"addEventListener mockCanvas\", event, listener, options)\n  }\n\n  removeEventListener(event: string, listener: any, options?: any) {\n    console.error(\"removeEventListener mockCanvas\", event, listener, options)\n  }\n\n  dispatchEvent(event: Event) {\n    console.error(\"dispatchEvent mockCanvas\", event)\n  }\n\n  public setSuperSample(superSample: SuperSampleType): void {\n    this.superSample = superSample\n  }\n\n  public async saveToFile(filePath: string): Promise<void> {\n    const bytesPerPixel = 4 // RGBA\n    const unalignedBytesPerRow = this.width * bytesPerPixel\n    const alignedBytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256\n    const textureBufferSize = alignedBytesPerRow * this.height\n\n    if (!this.screenshotGPUBuffer || this.screenshotGPUBuffer.size !== textureBufferSize) {\n      if (this.screenshotGPUBuffer) {\n        this.screenshotGPUBuffer.destroy()\n      }\n      this.screenshotGPUBuffer = this.device.createBuffer({\n        label: \"Screenshot GPU Buffer\",\n        size: textureBufferSize,\n        usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,\n      })\n    }\n\n    const texture = this.gpuCanvasContext.getCurrentTexture()\n\n    const commandEncoder = this.device.createCommandEncoder({ label: \"Screenshot Command Encoder\" })\n    commandEncoder.copyTextureToBuffer(\n      { texture: texture },\n      { buffer: this.screenshotGPUBuffer, bytesPerRow: alignedBytesPerRow, rowsPerImage: this.height },\n      { width: this.width, height: this.height },\n    )\n    const commandBuffer = commandEncoder.finish()\n    this.device.queue.submit([commandBuffer])\n\n    await this.screenshotGPUBuffer.mapAsync(GPUMapMode.READ)\n\n    const resultBuffer = this.screenshotGPUBuffer.getMappedRange()\n    const pixelData = new Uint8Array(resultBuffer)\n    const contextFormat = texture.format\n    const isBGRA = contextFormat === \"bgra8unorm\"\n\n    // Handle row padding - extract only the actual image data\n    const imageData = new Uint8Array(this.width * this.height * 4)\n    for (let y = 0; y < this.height; y++) {\n      const srcOffset = y * alignedBytesPerRow\n      const dstOffset = y * this.width * 4\n\n      if (isBGRA) {\n        for (let x = 0; x < this.width; x++) {\n          const srcPixelOffset = srcOffset + x * 4\n          const dstPixelOffset = dstOffset + x * 4\n\n          imageData[dstPixelOffset] = pixelData[srcPixelOffset + 2]\n          imageData[dstPixelOffset + 1] = pixelData[srcPixelOffset + 1]\n          imageData[dstPixelOffset + 2] = pixelData[srcPixelOffset]\n          imageData[dstPixelOffset + 3] = pixelData[srcPixelOffset + 3]\n        }\n      } else {\n        imageData.set(pixelData.subarray(srcOffset, srcOffset + this.width * 4), dstOffset)\n      }\n    }\n\n    const image = new Jimp({\n      data: Buffer.from(imageData),\n      width: this.width,\n      height: this.height,\n    })\n\n    await image.write(filePath as `${string}.${string}`)\n    this.screenshotGPUBuffer.unmap()\n  }\n\n  private async initComputePipeline(): Promise<void> {\n    if (this.computePipeline) return\n\n    const shaderModule = this.device.createShaderModule({\n      label: \"SuperSampling Compute Shader\",\n      code: SUPERSAMPLING_COMPUTE_SHADER,\n    })\n\n    this.computeBindGroupLayout = this.device.createBindGroupLayout({\n      label: \"SuperSampling Bind Group Layout\",\n      entries: [\n        {\n          binding: 0,\n          visibility: GPUShaderStage.COMPUTE,\n          texture: { sampleType: \"float\", viewDimension: \"2d\" },\n        },\n        {\n          binding: 1,\n          visibility: GPUShaderStage.COMPUTE,\n          buffer: { type: \"storage\" },\n        },\n        {\n          binding: 2,\n          visibility: GPUShaderStage.COMPUTE,\n          buffer: { type: \"uniform\" },\n        },\n      ],\n    })\n\n    const pipelineLayout = this.device.createPipelineLayout({\n      label: \"SuperSampling Pipeline Layout\",\n      bindGroupLayouts: [this.computeBindGroupLayout],\n    })\n\n    this.computePipeline = this.device.createComputePipeline({\n      label: \"SuperSampling Compute Pipeline\",\n      layout: pipelineLayout,\n      compute: {\n        module: shaderModule,\n        entryPoint: \"main\",\n      },\n    })\n\n    // Create uniform buffer for parameters (8 bytes - 2 u32s: width, height)\n    this.computeParamsBuffer = this.device.createBuffer({\n      label: \"SuperSampling Params Buffer\",\n      size: 16,\n      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n    })\n\n    this.updateComputeParams()\n  }\n\n  private updateComputeParams(): void {\n    if (!this.computeParamsBuffer || this.superSample === SuperSampleType.NONE) return\n\n    // Update uniform buffer with parameters\n    // Note: this.width/height are render dimensions (2x terminal size for super sampling)\n    const paramsData = new ArrayBuffer(16)\n    const uint32View = new Uint32Array(paramsData)\n\n    uint32View[0] = this.width\n    uint32View[1] = this.height\n    uint32View[2] = this.superSampleAlgorithm\n\n    this.device.queue.writeBuffer(this.computeParamsBuffer, 0, paramsData)\n  }\n\n  private scheduleUpdateComputeBuffers(): void {\n    this.updateScheduled = true\n  }\n\n  private updateComputeBuffers(width: number, height: number): void {\n    if (this.superSample === SuperSampleType.NONE) return\n    this.updateComputeParams()\n\n    // Calculate output buffer size (48 bytes per cell: 2 vec4f + u32 + 3 padding u32s)\n    // Must match WGSL calculation exactly: (params.width + 1u) / 2u\n    const cellBytesSize = 48 // 16 + 16 + 4 + 4 + 4 + 4 bytes (16-byte aligned)\n    const terminalWidthCells = Math.floor((width + 1) / 2)\n    const terminalHeightCells = Math.floor((height + 1) / 2)\n    const outputBufferSize = terminalWidthCells * terminalHeightCells * cellBytesSize\n\n    const oldOutputBuffer = this.computeOutputBuffer\n    const oldReadbackBuffer = this.computeReadbackBuffer\n\n    if (oldOutputBuffer) {\n      oldOutputBuffer.destroy()\n    }\n    if (oldReadbackBuffer) {\n      oldReadbackBuffer.destroy()\n    }\n\n    // Create new buffers\n    this.computeOutputBuffer = this.device.createBuffer({\n      label: \"SuperSampling Output Buffer\",\n      size: outputBufferSize,\n      usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,\n    })\n\n    this.computeReadbackBuffer = this.device.createBuffer({\n      label: \"SuperSampling Readback Buffer\",\n      size: outputBufferSize,\n      usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,\n    })\n  }\n\n  private async runComputeShaderSuperSampling(texture: GPUTexture, buffer: OptimizedBuffer): Promise<void> {\n    if (this.destroyed) {\n      return\n    }\n\n    if (this.updateScheduled) {\n      this.updateScheduled = false\n      await this.device.queue.onSubmittedWorkDone()\n      this.updateComputeBuffers(this.width, this.height)\n    }\n\n    await this.initComputePipeline()\n\n    if (\n      !this.computePipeline ||\n      !this.computeBindGroupLayout ||\n      !this.computeOutputBuffer ||\n      !this.computeParamsBuffer\n    ) {\n      throw new Error(\"Compute pipeline not initialized\")\n    }\n\n    const mapAsyncStart = performance.now()\n    const textureView = texture.createView({\n      label: \"SuperSampling Input Texture View\",\n    })\n\n    const bindGroup = this.device.createBindGroup({\n      label: \"SuperSampling Bind Group\",\n      layout: this.computeBindGroupLayout,\n      entries: [\n        { binding: 0, resource: textureView },\n        { binding: 1, resource: { buffer: this.computeOutputBuffer } },\n        { binding: 2, resource: { buffer: this.computeParamsBuffer } },\n      ],\n    })\n\n    const commandEncoder = this.device.createCommandEncoder({ label: \"SuperSampling Command Encoder\" })\n    const computePass = commandEncoder.beginComputePass({ label: \"SuperSampling Compute Pass\" })\n    computePass.setPipeline(this.computePipeline)\n    computePass.setBindGroup(0, bindGroup)\n\n    // Must match WGSL calculation exactly: (params.width + 1u) / 2u\n    const terminalWidthCells = Math.floor((this.width + 1) / 2)\n    const terminalHeightCells = Math.floor((this.height + 1) / 2)\n    const dispatchX = Math.ceil(terminalWidthCells / WORKGROUP_SIZE)\n    const dispatchY = Math.ceil(terminalHeightCells / WORKGROUP_SIZE)\n\n    computePass.dispatchWorkgroups(dispatchX, dispatchY, 1)\n    computePass.end()\n\n    commandEncoder.copyBufferToBuffer(\n      this.computeOutputBuffer,\n      0,\n      this.computeReadbackBuffer!,\n      0,\n      this.computeOutputBuffer.size,\n    )\n\n    const commandBuffer = commandEncoder.finish()\n    this.device.queue.submit([commandBuffer])\n\n    await this.computeReadbackBuffer!.mapAsync(GPUMapMode.READ)\n\n    if (this.destroyed) {\n      this.computeReadbackBuffer!.unmap()\n      return\n    }\n\n    const resultsPtr = this.computeReadbackBuffer!.getMappedRangePtr()\n    const size = this.computeReadbackBuffer!.size\n\n    this.mapAsyncTimeMs = performance.now() - mapAsyncStart\n\n    const ssStart = performance.now()\n    buffer.drawPackedBuffer(resultsPtr, size, 0, 0, terminalWidthCells, terminalHeightCells)\n    this.superSampleDrawTimeMs = performance.now() - ssStart\n\n    this.computeReadbackBuffer!.unmap()\n  }\n\n  private updateReadbackBuffer(renderWidth: number, renderHeight: number): void {\n    if (this.readbackBuffer) {\n      this.readbackBuffer.destroy()\n    }\n    const bytesPerPixel = 4 // Assuming RGBA8 or BGRA8\n    const unalignedBytesPerRow = renderWidth * bytesPerPixel\n    const alignedBytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256\n    const textureBufferSize = alignedBytesPerRow * renderHeight\n    this.readbackBuffer = this.device!.createBuffer({\n      label: \"Readback Buffer\",\n      size: textureBufferSize,\n      usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,\n    })\n  }\n\n  async readPixelsIntoBuffer(buffer: OptimizedBuffer): Promise<void> {\n    if (this.destroyed) {\n      return\n    }\n\n    const texture = this.gpuCanvasContext.getCurrentTexture()\n    this.gpuCanvasContext.switchTextures()\n\n    if (this.superSample === SuperSampleType.GPU) {\n      await this.runComputeShaderSuperSampling(texture, buffer)\n      return\n    }\n\n    const textureBuffer = this.readbackBuffer\n    if (!textureBuffer) {\n      throw new Error(\"Readback buffer not found\")\n    }\n\n    try {\n      const bytesPerPixel = 4 // Assuming RGBA8 or BGRA8\n      const unalignedBytesPerRow = this.width * bytesPerPixel\n      const alignedBytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256\n      const contextFormat = texture.format\n\n      const commandEncoder = this.device.createCommandEncoder({ label: \"Readback Command Encoder\" })\n      commandEncoder.copyTextureToBuffer(\n        { texture: texture },\n        { buffer: textureBuffer, bytesPerRow: alignedBytesPerRow, rowsPerImage: this.height },\n        {\n          width: this.width,\n          height: this.height,\n        },\n      )\n      const commandBuffer = commandEncoder.finish()\n      this.device.queue.submit([commandBuffer])\n\n      const mapStart = performance.now()\n      await textureBuffer.mapAsync(GPUMapMode.READ, 0, textureBuffer.size)\n      this.mapAsyncTimeMs = performance.now() - mapStart\n\n      if (this.destroyed) {\n        textureBuffer.unmap()\n        return\n      }\n\n      const mappedRangePtr = textureBuffer.getMappedRangePtr(0, textureBuffer.size)\n      const bufPtr = mappedRangePtr\n\n      if (this.superSample === SuperSampleType.CPU) {\n        const format = contextFormat === \"bgra8unorm\" ? \"bgra8unorm\" : \"rgba8unorm\"\n        const ssStart = performance.now()\n        buffer.drawSuperSampleBuffer(0, 0, bufPtr, textureBuffer.size, format, alignedBytesPerRow)\n        this.superSampleDrawTimeMs = performance.now() - ssStart\n      } else {\n        this.superSampleDrawTimeMs = 0\n        const pixelData = new Uint8Array(toArrayBuffer(bufPtr, 0, textureBuffer.size))\n        const isBGRA = contextFormat === \"bgra8unorm\"\n        const backgroundColor = RGBA.fromValues(0, 0, 0, 1)\n\n        for (let y = 0; y < this.height; y++) {\n          for (let x = 0; x < this.width; x++) {\n            const pixelIndexInPaddedRow = y * alignedBytesPerRow + x * bytesPerPixel\n\n            if (pixelIndexInPaddedRow + 3 >= pixelData.length) continue\n\n            let rByte, gByte, bByte // Alpha currently ignored\n\n            if (isBGRA) {\n              bByte = pixelData[pixelIndexInPaddedRow]\n              gByte = pixelData[pixelIndexInPaddedRow + 1]\n              rByte = pixelData[pixelIndexInPaddedRow + 2]\n            } else {\n              // Assume RGBA\n              rByte = pixelData[pixelIndexInPaddedRow]\n              gByte = pixelData[pixelIndexInPaddedRow + 1]\n              bByte = pixelData[pixelIndexInPaddedRow + 2]\n            }\n\n            // Convert to [0-1] range for RGB class\n            const r = rByte / 255.0\n            const g = gByte / 255.0\n            const b = bByte / 255.0\n\n            const cellColor = RGBA.fromValues(r, g, b, 1.0)\n\n            buffer.setCellWithAlphaBlending(x, y, \"█\", cellColor, backgroundColor)\n          }\n        }\n      }\n    } finally {\n      textureBuffer.unmap()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/index.ts",
    "content": "export * from \"./WGPURenderer.js\"\nexport * from \"./ThreeRenderable.js\"\nexport * from \"./TextureUtils.js\"\nexport * from \"./canvas.js\"\nexport * from \"./SpriteUtils.js\"\nexport * from \"./animation/SpriteAnimator.js\"\nexport * from \"./animation/ExplodingSpriteEffect.js\"\nexport * from \"./animation/SpriteParticleGenerator.js\"\nexport * from \"./animation/PhysicsExplodingSpriteEffect.js\"\nexport * from \"./physics/RapierPhysicsAdapter.js\"\nexport * from \"./physics/PlanckPhysicsAdapter.js\"\nexport * from \"./SpriteResourceManager.js\"\n"
  },
  {
    "path": "packages/core/src/3d/physics/PlanckPhysicsAdapter.ts",
    "content": "import * as planck from \"planck\"\nimport type {\n  PhysicsVector2,\n  PhysicsRigidBodyDesc,\n  PhysicsColliderDesc,\n  PhysicsRigidBody,\n  PhysicsWorld,\n} from \"./physics-interface.js\"\n\nexport class PlanckRigidBody implements PhysicsRigidBody {\n  constructor(private planckBody: planck.Body) {}\n\n  applyImpulse(force: PhysicsVector2): void {\n    this.planckBody.applyLinearImpulse(planck.Vec2(force.x, force.y), this.planckBody.getWorldCenter())\n  }\n\n  applyTorqueImpulse(torque: number): void {\n    this.planckBody.applyAngularImpulse(torque)\n  }\n\n  getTranslation(): PhysicsVector2 {\n    const pos = this.planckBody.getPosition()\n    return { x: pos.x, y: pos.y }\n  }\n\n  getRotation(): number {\n    return this.planckBody.getAngle()\n  }\n\n  get nativeBody(): planck.Body {\n    return this.planckBody\n  }\n}\n\nexport class PlanckPhysicsWorld implements PhysicsWorld {\n  constructor(private planckWorld: planck.World) {}\n\n  createRigidBody(desc: PhysicsRigidBodyDesc): PhysicsRigidBody {\n    const bodyDef: planck.BodyDef = {\n      type: \"dynamic\",\n      position: planck.Vec2(desc.translation.x, desc.translation.y),\n      linearDamping: desc.linearDamping,\n      angularDamping: desc.angularDamping,\n    }\n\n    const planckBody = this.planckWorld.createBody(bodyDef)\n    return new PlanckRigidBody(planckBody)\n  }\n\n  createCollider(colliderDesc: PhysicsColliderDesc, rigidBody: PhysicsRigidBody): void {\n    const shape = planck.Box(colliderDesc.width * 0.5, colliderDesc.height * 0.5)\n\n    const fixtureDef: planck.FixtureDef = {\n      shape: shape,\n      density: colliderDesc.density,\n      friction: colliderDesc.friction,\n      restitution: colliderDesc.restitution,\n    }\n\n    const planckRigidBody = (rigidBody as PlanckRigidBody).nativeBody\n    planckRigidBody.createFixture(fixtureDef)\n  }\n\n  removeRigidBody(rigidBody: PhysicsRigidBody): void {\n    const planckRigidBody = (rigidBody as PlanckRigidBody).nativeBody\n    this.planckWorld.destroyBody(planckRigidBody)\n  }\n\n  static createFromPlanckWorld(planckWorld: planck.World): PlanckPhysicsWorld {\n    return new PlanckPhysicsWorld(planckWorld)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/physics/RapierPhysicsAdapter.ts",
    "content": "import RAPIER from \"@dimforge/rapier2d-simd-compat\"\nimport type {\n  PhysicsVector2,\n  PhysicsRigidBodyDesc,\n  PhysicsColliderDesc,\n  PhysicsRigidBody,\n  PhysicsWorld,\n} from \"./physics-interface.js\"\n\nexport class RapierRigidBody implements PhysicsRigidBody {\n  constructor(private rapierBody: RAPIER.RigidBody) {}\n\n  applyImpulse(force: PhysicsVector2): void {\n    this.rapierBody.applyImpulse(force, true)\n  }\n\n  applyTorqueImpulse(torque: number): void {\n    this.rapierBody.applyTorqueImpulse(torque, true)\n  }\n\n  getTranslation(): PhysicsVector2 {\n    const pos = this.rapierBody.translation()\n    return { x: pos.x, y: pos.y }\n  }\n\n  getRotation(): number {\n    return this.rapierBody.rotation()\n  }\n\n  get nativeBody(): RAPIER.RigidBody {\n    return this.rapierBody\n  }\n}\n\nexport class RapierPhysicsWorld implements PhysicsWorld {\n  constructor(private rapierWorld: RAPIER.World) {}\n\n  createRigidBody(desc: PhysicsRigidBodyDesc): PhysicsRigidBody {\n    const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic()\n      .setTranslation(desc.translation.x, desc.translation.y)\n      .setLinearDamping(desc.linearDamping)\n      .setAngularDamping(desc.angularDamping)\n\n    const rapierBody = this.rapierWorld.createRigidBody(rigidBodyDesc)\n    return new RapierRigidBody(rapierBody)\n  }\n\n  createCollider(colliderDesc: PhysicsColliderDesc, rigidBody: PhysicsRigidBody): void {\n    const rapierColliderDesc = RAPIER.ColliderDesc.cuboid(colliderDesc.width * 0.5, colliderDesc.height * 0.5)\n      .setRestitution(colliderDesc.restitution)\n      .setFriction(colliderDesc.friction)\n      .setDensity(colliderDesc.density)\n\n    const rapierRigidBody = (rigidBody as RapierRigidBody).nativeBody\n    this.rapierWorld.createCollider(rapierColliderDesc, rapierRigidBody)\n  }\n\n  removeRigidBody(rigidBody: PhysicsRigidBody): void {\n    const rapierRigidBody = (rigidBody as RapierRigidBody).nativeBody\n    this.rapierWorld.removeRigidBody(rapierRigidBody)\n  }\n\n  static createFromRapierWorld(rapierWorld: RAPIER.World): RapierPhysicsWorld {\n    return new RapierPhysicsWorld(rapierWorld)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/3d/physics/physics-interface.ts",
    "content": "export interface PhysicsVector2 {\n  x: number\n  y: number\n}\n\nexport interface PhysicsRigidBodyDesc {\n  translation: PhysicsVector2\n  linearDamping: number\n  angularDamping: number\n}\n\nexport interface PhysicsColliderDesc {\n  width: number\n  height: number\n  restitution: number\n  friction: number\n  density: number\n}\n\nexport interface PhysicsRigidBody {\n  applyImpulse(force: PhysicsVector2): void\n  applyTorqueImpulse(torque: number): void\n  getTranslation(): PhysicsVector2\n  getRotation(): number\n}\n\nexport interface PhysicsWorld {\n  createRigidBody(desc: PhysicsRigidBodyDesc): PhysicsRigidBody\n  createCollider(colliderDesc: PhysicsColliderDesc, rigidBody: PhysicsRigidBody): void\n  removeRigidBody(rigidBody: PhysicsRigidBody): void\n}\n"
  },
  {
    "path": "packages/core/src/3d/shaders/supersampling.wgsl",
    "content": "struct CellResult {\n    bg: vec4<f32>,      // Background RGBA (16 bytes)\n    fg: vec4<f32>,      // Foreground RGBA (16 bytes)\n    char: u32,          // Unicode character code (4 bytes)\n    _padding1: u32,     // Padding (4 bytes)\n    _padding2: u32,     // Extra padding (4 bytes) \n    _padding3: u32,     // Extra padding (4 bytes) - total now 48 bytes (16-byte aligned)\n};\n\nstruct CellBuffer {\n    cells: array<CellResult>\n};\n\nstruct SuperSamplingParams {\n    width: u32,              // Canvas width in pixels\n    height: u32,             // Canvas height in pixels  \n    sampleAlgo: u32,         // 0 = standard 2x2, 1 = pre-squeezed horizontal blend\n    _padding: u32,           // Padding for 16-byte alignment\n};\n\n@group(0) @binding(0) var inputTexture: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> output: CellBuffer;\n@group(0) @binding(2) var<uniform> params: SuperSamplingParams;\n\n// Quadrant character lookup table (same as Zig implementation)\nconst quadrantChars = array<u32, 16>(\n    32u,      // ' '  - 0000\n    0x2597u,  // ▗   - 0001 BR\n    0x2596u,  // ▖   - 0010 BL  \n    0x2584u,  // ▄   - 0011 Lower Half Block\n    0x259Du,  // ▝   - 0100 TR\n    0x2590u,  // ▐   - 0101 Right Half Block\n    0x259Eu,  // ▞   - 0110 TR+BL\n    0x259Fu,  // ▟   - 0111 TR+BL+BR\n    0x2598u,  // ▘   - 1000 TL\n    0x259Au,  // ▚   - 1001 TL+BR\n    0x258Cu,  // ▌   - 1010 Left Half Block\n    0x2599u,  // ▙   - 1011 TL+BL+BR\n    0x2580u,  // ▀   - 1100 Upper Half Block\n    0x259Cu,  // ▜   - 1101 TL+TR+BR\n    0x259Bu,  // ▛   - 1110 TL+TR+BL\n    0x2588u   // █   - 1111 Full Block\n);\n\nconst inv_255: f32 = 1.0 / 255.0;\n\nfn getPixelColor(pixelX: u32, pixelY: u32) -> vec4<f32> {\n    if (pixelX >= params.width || pixelY >= params.height) {\n        return vec4<f32>(0.0, 0.0, 0.0, 1.0); // Black for out-of-bounds\n    }\n    \n    // textureLoad automatically handles format conversion to RGBA\n    return textureLoad(inputTexture, vec2<i32>(i32(pixelX), i32(pixelY)), 0);\n}\n\nfn colorDistance(a: vec4<f32>, b: vec4<f32>) -> f32 {\n    let diff = a.rgb - b.rgb;\n    return dot(diff, diff);\n}\n\nfn luminance(color: vec4<f32>) -> f32 {\n    return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;\n}\n\nfn closestColorIndex(pixel: vec4<f32>, candA: vec4<f32>, candB: vec4<f32>) -> u32 {\n    return select(1u, 0u, colorDistance(pixel, candA) <= colorDistance(pixel, candB));\n}\n\nfn averageColor(pixels: array<vec4<f32>, 4>) -> vec4<f32> {\n    return (pixels[0] + pixels[1] + pixels[2] + pixels[3]) * 0.25;\n}\n\nfn blendColors(color1: vec4<f32>, color2: vec4<f32>) -> vec4<f32> {\n    let a1 = color1.a;\n    let a2 = color2.a;\n    \n    if (a1 == 0.0 && a2 == 0.0) {\n        return vec4<f32>(0.0, 0.0, 0.0, 0.0);\n    }\n    \n    let outAlpha = a1 + a2 - a1 * a2;\n    if (outAlpha == 0.0) {\n        return vec4<f32>(0.0, 0.0, 0.0, 0.0);\n    }\n    \n    let rgb = (color1.rgb * a1 + color2.rgb * a2 * (1.0 - a1)) / outAlpha;\n    \n    return vec4<f32>(rgb, outAlpha);\n}\n\nfn averageColorsWithAlpha(pixels: array<vec4<f32>, 4>) -> vec4<f32> {\n    let blend1 = blendColors(pixels[0], pixels[1]);\n    let blend2 = blendColors(pixels[2], pixels[3]);\n    \n    return blendColors(blend1, blend2);\n}\n\nfn renderQuadrantBlock(pixels: array<vec4<f32>, 4>) -> CellResult {\n    var maxDist: f32 = colorDistance(pixels[0], pixels[1]);\n    var pIdxA: u32 = 0u;\n    var pIdxB: u32 = 1u;\n    \n    for (var i: u32 = 0u; i < 4u; i++) {\n        for (var j: u32 = i + 1u; j < 4u; j++) {\n            let dist = colorDistance(pixels[i], pixels[j]);\n            if (dist > maxDist) {\n                pIdxA = i;\n                pIdxB = j;\n                maxDist = dist;\n            }\n        }\n    }\n    \n    let pCandA = pixels[pIdxA];\n    let pCandB = pixels[pIdxB];\n    \n    var chosenDarkColor: vec4<f32>;\n    var chosenLightColor: vec4<f32>;\n    \n    if (luminance(pCandA) <= luminance(pCandB)) {\n        chosenDarkColor = pCandA;\n        chosenLightColor = pCandB;\n    } else {\n        chosenDarkColor = pCandB;\n        chosenLightColor = pCandA;\n    }\n    \n    var quadrantBits: u32 = 0u;\n    let bitValues = array<u32, 4>(8u, 4u, 2u, 1u); // TL, TR, BL, BR\n    \n    for (var i: u32 = 0u; i < 4u; i++) {\n        if (closestColorIndex(pixels[i], chosenDarkColor, chosenLightColor) == 0u) {\n            quadrantBits |= bitValues[i];\n        }\n    }\n    \n    // Construct result\n    var result: CellResult;\n    \n    if (quadrantBits == 0u) { // All light\n        result.char = 32u; // Space character\n        result.fg = chosenDarkColor;\n        result.bg = averageColorsWithAlpha(pixels);\n    } else if (quadrantBits == 15u) { // All dark  \n        result.char = quadrantChars[15]; // Full block\n        result.fg = averageColorsWithAlpha(pixels);\n        result.bg = chosenLightColor;\n    } else { // Mixed pattern\n        result.char = quadrantChars[quadrantBits];\n        result.fg = chosenDarkColor;\n        result.bg = chosenLightColor;\n    }\n    result._padding1 = 0u;\n    result._padding2 = 0u;\n    result._padding3 = 0u;\n    \n    return result;\n}\n\n@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1)\nfn main(@builtin(global_invocation_id) id: vec3<u32>) {\n    let cellX = id.x;\n    let cellY = id.y;\n    let bufferWidthCells = (params.width + 1u) / 2u;\n    let bufferHeightCells = (params.height + 1u) / 2u;\n    \n    if (cellX >= bufferWidthCells || cellY >= bufferHeightCells) {\n        return;\n    }\n    \n    let renderX = cellX * 2u;\n    let renderY = cellY * 2u;\n    \n    var pixelsRgba: array<vec4<f32>, 4>;\n    \n    if (params.sampleAlgo == 1u) {\n        let topColor = getPixelColor(renderX, renderY);\n        let topColor2 = getPixelColor(renderX + 1u, renderY);\n        \n        let blendedTop = blendColors(topColor, topColor2);\n        \n        let bottomColor = getPixelColor(renderX, renderY + 1u);\n        let bottomColor2 = getPixelColor(renderX + 1u, renderY + 1u);\n        let blendedBottom = blendColors(bottomColor, bottomColor2);\n        \n        pixelsRgba[0] = blendedTop;      // TL\n        pixelsRgba[1] = blendedTop;      // TR  \n        pixelsRgba[2] = blendedBottom;   // BL\n        pixelsRgba[3] = blendedBottom;   // BR\n    } else {\n        pixelsRgba[0] = getPixelColor(renderX, renderY);         // TL\n        pixelsRgba[1] = getPixelColor(renderX + 1u, renderY);   // TR  \n        pixelsRgba[2] = getPixelColor(renderX, renderY + 1u);   // BL\n        pixelsRgba[3] = getPixelColor(renderX + 1u, renderY + 1u); // BR\n    }\n    \n    let cellResult = renderQuadrantBlock(pixelsRgba);\n    \n    let outputIndex = cellY * bufferWidthCells + cellX;\n    output.cells[outputIndex] = cellResult;\n}"
  },
  {
    "path": "packages/core/src/3d.ts",
    "content": "// 3D module exports - requires optional dependencies\nexport * from \"./3d/index.js\"\nexport * as THREE from \"three\"\n"
  },
  {
    "path": "packages/core/src/NativeSpanFeed.ts",
    "content": "import { toArrayBuffer, type Pointer } from \"bun:ffi\"\nimport { resolveRenderLib } from \"./zig\"\nimport { SpanInfoStruct } from \"./zig-structs\"\nimport type { GrowthPolicy, NativeSpanFeedOptions, NativeSpanFeedStats } from \"./zig-structs\"\n\nexport type { GrowthPolicy, NativeSpanFeedOptions, NativeSpanFeedStats } from \"./zig-structs\"\n\nconst enum EventId {\n  ChunkAdded = 2,\n  Closed = 5,\n  Error = 6,\n  DataAvailable = 7,\n  StateBuffer = 8,\n}\n\nfunction toPointer(value: number | bigint): Pointer {\n  if (typeof value === \"bigint\") {\n    if (value > BigInt(Number.MAX_SAFE_INTEGER)) {\n      throw new Error(\"Pointer exceeds safe integer range\")\n    }\n    return Number(value) as Pointer\n  }\n  return value as Pointer\n}\n\nfunction toNumber(value: number | bigint): number {\n  return typeof value === \"bigint\" ? Number(value) : value\n}\n\ntype StreamEventHandler = (eventId: number, arg0: Pointer, arg1: number | bigint) => void\n\nexport type DataHandler = (data: Uint8Array) => void | Promise<void>\n\n/**\n * Zero-copy wrapper over Zig memory; not a full stream interface.\n */\nexport class NativeSpanFeed {\n  static create(options?: NativeSpanFeedOptions): NativeSpanFeed {\n    const lib = resolveRenderLib()\n    const streamPtr = lib.createNativeSpanFeed(options)\n    const stream = new NativeSpanFeed(streamPtr)\n\n    lib.registerNativeSpanFeedStream(streamPtr, stream.eventHandler)\n\n    const status = lib.attachNativeSpanFeed(streamPtr)\n    if (status !== 0) {\n      lib.unregisterNativeSpanFeedStream(streamPtr)\n      lib.destroyNativeSpanFeed(streamPtr)\n      throw new Error(`Failed to attach stream: ${status}`)\n    }\n\n    return stream\n  }\n\n  static attach(streamPtr: bigint | number, _options?: NativeSpanFeedOptions): NativeSpanFeed {\n    const lib = resolveRenderLib()\n    const ptr = toPointer(streamPtr)\n    const stream = new NativeSpanFeed(ptr)\n\n    lib.registerNativeSpanFeedStream(ptr, stream.eventHandler)\n\n    const status = lib.attachNativeSpanFeed(ptr)\n    if (status !== 0) {\n      lib.unregisterNativeSpanFeedStream(ptr)\n      throw new Error(`Failed to attach stream: ${status}`)\n    }\n\n    return stream\n  }\n\n  readonly streamPtr: Pointer\n  private readonly lib = resolveRenderLib()\n  private readonly eventHandler: StreamEventHandler\n  private chunkMap = new Map<Pointer, ArrayBuffer>()\n  private chunkSizes = new Map<Pointer, number>()\n  private dataHandlers = new Set<DataHandler>()\n  private errorHandlers = new Set<(code: number) => void>()\n  private drainBuffer: Uint8Array | null = null\n  private stateBuffer: Uint8Array | null = null\n  private closed = false\n  private destroyed = false\n  private draining = false\n  private pendingDataAvailable = false\n  private pendingClose = false\n  private closing = false\n  private pendingAsyncHandlers = 0\n  private inCallback = false\n  private closeQueued = false\n\n  private constructor(streamPtr: Pointer) {\n    this.streamPtr = streamPtr\n    this.eventHandler = (eventId, arg0, arg1) => {\n      this.handleEvent(eventId, arg0, arg1)\n    }\n    this.ensureDrainBuffer()\n  }\n\n  private ensureDrainBuffer(): void {\n    if (this.drainBuffer) return\n    const capacity = 256\n    this.drainBuffer = new Uint8Array(capacity * SpanInfoStruct.size)\n  }\n\n  onData(handler: DataHandler): () => void {\n    this.dataHandlers.add(handler)\n    if (this.pendingDataAvailable) {\n      this.pendingDataAvailable = false\n      this.drainAll()\n    }\n    return () => this.dataHandlers.delete(handler)\n  }\n\n  onError(handler: (code: number) => void): () => void {\n    this.errorHandlers.add(handler)\n    return () => this.errorHandlers.delete(handler)\n  }\n\n  close(): void {\n    if (this.destroyed) return\n    if (this.inCallback || this.draining || this.pendingAsyncHandlers > 0) {\n      this.pendingClose = true\n      if (!this.closeQueued) {\n        this.closeQueued = true\n        queueMicrotask(() => {\n          this.closeQueued = false\n          this.processPendingClose()\n        })\n      }\n      return\n    }\n    this.performClose()\n  }\n\n  private processPendingClose(): void {\n    if (!this.pendingClose || this.destroyed) return\n    if (this.inCallback || this.draining || this.pendingAsyncHandlers > 0) return\n    this.pendingClose = false\n    this.performClose()\n  }\n\n  private performClose(): void {\n    if (this.closing) return\n    this.closing = true\n    if (!this.closed) {\n      const status = this.lib.streamClose(this.streamPtr)\n      if (status !== 0) {\n        this.closing = false\n        return\n      }\n      this.closed = true\n    }\n    this.finalizeDestroy()\n  }\n\n  private finalizeDestroy(): void {\n    if (this.destroyed) return\n    this.lib.unregisterNativeSpanFeedStream(this.streamPtr)\n    this.lib.destroyNativeSpanFeed(this.streamPtr)\n    this.destroyed = true\n    this.chunkMap.clear()\n    this.chunkSizes.clear()\n    this.stateBuffer = null\n    this.drainBuffer = null\n    this.dataHandlers.clear()\n    this.errorHandlers.clear()\n    this.pendingDataAvailable = false\n  }\n\n  private handleEvent(eventId: number, arg0: Pointer, arg1: number | bigint): void {\n    this.inCallback = true\n    try {\n      switch (eventId) {\n        case EventId.StateBuffer: {\n          const len = toNumber(arg1)\n          if (len > 0 && arg0) {\n            // toArrayBuffer must alias Zig memory so refcount writes are visible.\n            const buffer = toArrayBuffer(arg0, 0, len)\n            this.stateBuffer = new Uint8Array(buffer)\n          }\n          break\n        }\n        case EventId.DataAvailable: {\n          if (this.closing) break\n          if (this.dataHandlers.size === 0) {\n            this.pendingDataAvailable = true\n            break\n          }\n          this.drainAll()\n          break\n        }\n        case EventId.ChunkAdded: {\n          const chunkLen = toNumber(arg1)\n          if (chunkLen > 0 && arg0) {\n            if (!this.chunkMap.has(arg0)) {\n              const buffer = toArrayBuffer(arg0, 0, chunkLen)\n              this.chunkMap.set(arg0, buffer)\n            }\n            this.chunkSizes.set(arg0, chunkLen)\n          }\n          break\n        }\n        case EventId.Error: {\n          const code = arg0\n          for (const handler of this.errorHandlers) handler(code)\n          break\n        }\n        case EventId.Closed: {\n          this.closed = true\n          break\n        }\n        default:\n          break\n      }\n    } finally {\n      this.inCallback = false\n    }\n  }\n\n  private decrementRefcount(chunkIndex: number): void {\n    if (this.stateBuffer && chunkIndex < this.stateBuffer.length) {\n      const prev = this.stateBuffer[chunkIndex]\n      this.stateBuffer[chunkIndex] = prev > 0 ? prev - 1 : 0\n    }\n  }\n\n  private drainOnce(): number {\n    if (!this.drainBuffer || this.draining || this.pendingClose) return 0\n    const capacity = Math.floor(this.drainBuffer.byteLength / SpanInfoStruct.size)\n    if (capacity === 0) return 0\n\n    const count = this.lib.streamDrainSpans(this.streamPtr, this.drainBuffer, capacity)\n    if (count === 0) return 0\n\n    this.draining = true\n    const spans = SpanInfoStruct.unpackList(this.drainBuffer.buffer, count)\n    let firstError: unknown = null\n\n    try {\n      for (const span of spans) {\n        if (span.len === 0) continue\n\n        let buffer = this.chunkMap.get(span.chunkPtr)\n        if (!buffer) {\n          const size = this.chunkSizes.get(span.chunkPtr)\n          if (!size) continue\n          buffer = toArrayBuffer(span.chunkPtr, 0, size)\n          this.chunkMap.set(span.chunkPtr, buffer)\n        }\n\n        if (span.offset + span.len > buffer.byteLength) continue\n\n        const slice = new Uint8Array(buffer, span.offset, span.len)\n        let asyncResults: Promise<void>[] | null = null\n\n        for (const handler of this.dataHandlers) {\n          try {\n            const result = handler(slice)\n            // Async handlers keep the chunk pinned until they settle.\n            if (result && typeof result.then === \"function\") {\n              asyncResults ??= []\n              asyncResults.push(result)\n            }\n          } catch (e) {\n            firstError ??= e\n          }\n        }\n\n        const shouldStopAfterThisSpan = this.pendingClose\n\n        if (asyncResults) {\n          // Use allSettled so rejections still release refcounts.\n          const chunkIndex = span.chunkIndex\n          this.pendingAsyncHandlers += 1\n          Promise.allSettled(asyncResults).then(() => {\n            this.decrementRefcount(chunkIndex)\n            this.pendingAsyncHandlers -= 1\n            this.processPendingClose()\n          })\n        } else {\n          this.decrementRefcount(span.chunkIndex)\n        }\n\n        if (shouldStopAfterThisSpan) break\n      }\n    } finally {\n      this.draining = false\n    }\n\n    if (firstError) throw firstError\n\n    return count\n  }\n\n  drainAll(): void {\n    let count = this.drainOnce()\n    while (count > 0) {\n      count = this.drainOnce()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/Renderable.ts",
    "content": "import { EventEmitter } from \"events\"\nimport Yoga, { Direction, Display, Edge, FlexDirection, type Config, type Node as YogaNode } from \"yoga-layout\"\nimport { OptimizedBuffer } from \"./buffer.js\"\nimport type { KeyEvent, PasteEvent } from \"./lib/KeyHandler.js\"\nimport type { MouseEventType } from \"./lib/parse.mouse.js\"\nimport type { Selection } from \"./lib/selection.js\"\nimport {\n  parseAlign,\n  parseAlignItems,\n  parseFlexDirection,\n  parseJustify,\n  parseOverflow,\n  parsePositionType,\n  parseWrap,\n  type AlignString,\n  type FlexDirectionString,\n  type JustifyString,\n  type OverflowString,\n  type PositionTypeString,\n  type WrapString,\n} from \"./lib/yoga.options.js\"\nimport { maybeMakeRenderable, type VNode } from \"./renderables/composition/vnode.js\"\nimport type { MouseEvent } from \"./renderer.js\"\nimport type { RenderContext } from \"./types.js\"\nimport {\n  validateOptions,\n  isPositionType,\n  isDimensionType,\n  isFlexBasisType,\n  isSizeType,\n  isMarginType,\n  isPaddingType,\n  isPositionTypeType,\n  isOverflowType,\n} from \"./lib/renderable.validations.js\"\n\nconst BrandedRenderable: unique symbol = Symbol.for(\"@opentui/core/Renderable\")\n\nexport enum LayoutEvents {\n  LAYOUT_CHANGED = \"layout-changed\",\n  ADDED = \"added\",\n  REMOVED = \"removed\",\n  RESIZED = \"resized\",\n}\n\nexport enum RenderableEvents {\n  FOCUSED = \"focused\",\n  BLURRED = \"blurred\",\n}\n\nexport interface Position {\n  top?: number | \"auto\" | `${number}%`\n  right?: number | \"auto\" | `${number}%`\n  bottom?: number | \"auto\" | `${number}%`\n  left?: number | \"auto\" | `${number}%`\n}\n\nexport interface BaseRenderableOptions {\n  id?: string\n}\n\nexport interface LayoutOptions extends BaseRenderableOptions {\n  flexGrow?: number\n  flexShrink?: number\n  flexDirection?: FlexDirectionString\n  flexWrap?: WrapString\n  alignItems?: AlignString\n  justifyContent?: JustifyString\n  alignSelf?: AlignString\n  flexBasis?: number | \"auto\" | undefined\n  position?: PositionTypeString\n  overflow?: OverflowString\n  top?: number | \"auto\" | `${number}%`\n  right?: number | \"auto\" | `${number}%`\n  bottom?: number | \"auto\" | `${number}%`\n  left?: number | \"auto\" | `${number}%`\n  minWidth?: number | \"auto\" | `${number}%`\n  minHeight?: number | \"auto\" | `${number}%`\n  maxWidth?: number | \"auto\" | `${number}%`\n  maxHeight?: number | \"auto\" | `${number}%`\n  margin?: number | \"auto\" | `${number}%`\n  marginX?: number | \"auto\" | `${number}%`\n  marginY?: number | \"auto\" | `${number}%`\n  marginTop?: number | \"auto\" | `${number}%`\n  marginRight?: number | \"auto\" | `${number}%`\n  marginBottom?: number | \"auto\" | `${number}%`\n  marginLeft?: number | \"auto\" | `${number}%`\n  padding?: number | `${number}%`\n  paddingX?: number | `${number}%`\n  paddingY?: number | `${number}%`\n  paddingTop?: number | `${number}%`\n  paddingRight?: number | `${number}%`\n  paddingBottom?: number | `${number}%`\n  paddingLeft?: number | `${number}%`\n  enableLayout?: boolean\n}\n\nexport interface RenderableOptions<T extends BaseRenderable = BaseRenderable> extends Partial<LayoutOptions> {\n  width?: number | \"auto\" | `${number}%`\n  height?: number | \"auto\" | `${number}%`\n  zIndex?: number\n  visible?: boolean\n  buffered?: boolean\n  live?: boolean\n  opacity?: number\n\n  // hooks for custom render logic\n  renderBefore?: (this: T, buffer: OptimizedBuffer, deltaTime: number) => void\n  renderAfter?: (this: T, buffer: OptimizedBuffer, deltaTime: number) => void\n\n  // catch all\n  onMouse?: (this: T, event: MouseEvent) => void\n\n  onMouseDown?: (this: T, event: MouseEvent) => void\n  onMouseUp?: (this: T, event: MouseEvent) => void\n  onMouseMove?: (this: T, event: MouseEvent) => void\n  onMouseDrag?: (this: T, event: MouseEvent) => void\n  onMouseDragEnd?: (this: T, event: MouseEvent) => void\n  onMouseDrop?: (this: T, event: MouseEvent) => void\n  onMouseOver?: (this: T, event: MouseEvent) => void\n  onMouseOut?: (this: T, event: MouseEvent) => void\n  onMouseScroll?: (this: T, event: MouseEvent) => void\n\n  onPaste?: (this: T, event: PasteEvent) => void\n\n  onKeyDown?: (key: KeyEvent) => void\n\n  onSizeChange?: (this: T) => void\n}\n\nexport function isRenderable(obj: any): obj is Renderable {\n  return !!obj?.[BrandedRenderable]\n}\n\nexport abstract class BaseRenderable extends EventEmitter {\n  [BrandedRenderable] = true\n\n  private static renderableNumber = 1\n  protected _id: string\n  public readonly num: number\n  protected _dirty: boolean = false\n  public parent: BaseRenderable | null = null\n  protected _visible: boolean = true\n\n  constructor(options: BaseRenderableOptions) {\n    super()\n    this.num = BaseRenderable.renderableNumber++\n    this._id = options.id ?? `renderable-${this.num}`\n  }\n\n  public abstract add(obj: BaseRenderable | unknown, index?: number): number\n  public abstract remove(id: string): void\n  public abstract insertBefore(obj: BaseRenderable | unknown, anchor: BaseRenderable | unknown): void\n  public abstract getChildren(): BaseRenderable[]\n  public abstract getChildrenCount(): number\n  public abstract getRenderable(id: string): BaseRenderable | undefined\n  public abstract requestRender(): void\n  public abstract findDescendantById(id: string): BaseRenderable | undefined\n\n  public get id(): string {\n    return this._id\n  }\n\n  public set id(value: string) {\n    this._id = value\n  }\n\n  public get isDirty(): boolean {\n    return this._dirty\n  }\n\n  protected markClean(): void {\n    this._dirty = false\n  }\n\n  protected markDirty(): void {\n    this._dirty = true\n  }\n\n  public destroy(): void {\n    // Default implementation: do nothing\n    // Override this method to provide custom removal logic\n  }\n\n  public destroyRecursively(): void {\n    // Default implementation: do nothing\n    // Override this method to provide custom destruction logic\n  }\n\n  public get visible(): boolean {\n    return this._visible\n  }\n\n  public set visible(value: boolean) {\n    this._visible = value\n  }\n}\n\nconst yogaConfig: Config = Yoga.Config.create()\nyogaConfig.setUseWebDefaults(false)\nyogaConfig.setPointScaleFactor(1)\n\nexport abstract class Renderable extends BaseRenderable {\n  static renderablesByNumber: Map<number, Renderable> = new Map()\n\n  protected _isDestroyed: boolean = false\n  protected _ctx: RenderContext\n  protected _translateX: number = 0\n  protected _translateY: number = 0\n  protected _x: number = 0\n  protected _y: number = 0\n  protected _width: number | \"auto\" | `${number}%`\n  protected _height: number | \"auto\" | `${number}%`\n  protected _widthValue: number = 0\n  protected _heightValue: number = 0\n  private _zIndex: number\n  public selectable: boolean = false\n  protected buffered: boolean\n  protected frameBuffer: OptimizedBuffer | null = null\n\n  protected _focusable: boolean = false\n  protected _focused: boolean = false\n  protected keypressHandler: ((key: KeyEvent) => void) | null = null\n  protected pasteHandler: ((event: PasteEvent) => void) | null = null\n\n  private _live: boolean = false\n  protected _liveCount: number = 0\n\n  private _sizeChangeListener: (() => void) | undefined = undefined\n  private _mouseListener: ((event: MouseEvent) => void) | null = null\n  private _mouseListeners: Partial<Record<MouseEventType, (event: MouseEvent) => void>> = {}\n  private _pasteListener: ((event: PasteEvent) => void) | undefined = undefined\n  private _keyListeners: Partial<Record<\"down\", (key: KeyEvent) => void>> = {}\n\n  protected yogaNode: YogaNode\n  protected _positionType: PositionTypeString = \"relative\"\n  protected _overflow: OverflowString = \"visible\"\n  protected _position: Position = {}\n  protected _opacity: number = 1.0\n  private _flexShrink: number = 1\n\n  private renderableMapById: Map<string, Renderable> = new Map()\n  protected _childrenInLayoutOrder: Renderable[] = []\n  protected _childrenInZIndexOrder: Renderable[] = []\n  private needsZIndexSort: boolean = false\n  public parent: Renderable | null = null\n\n  private childrenPrimarySortDirty: boolean = true\n  private childrenSortedByPrimaryAxis: Renderable[] = []\n  private _shouldUpdateBefore: Set<Renderable> = new Set()\n\n  public onLifecyclePass: (() => void) | null = null\n\n  public renderBefore?: (this: Renderable, buffer: OptimizedBuffer, deltaTime: number) => void\n  public renderAfter?: (this: Renderable, buffer: OptimizedBuffer, deltaTime: number) => void\n\n  constructor(ctx: RenderContext, options: RenderableOptions<any>) {\n    super(options)\n\n    this._ctx = ctx\n    Renderable.renderablesByNumber.set(this.num, this)\n\n    validateOptions(this.id, options)\n\n    this.renderBefore = options.renderBefore\n    this.renderAfter = options.renderAfter\n\n    this._width = options.width ?? \"auto\"\n    this._height = options.height ?? \"auto\"\n\n    if (typeof this._width === \"number\") {\n      this._widthValue = this._width\n    }\n    if (typeof this._height === \"number\") {\n      this._heightValue = this._height\n    }\n\n    this._zIndex = options.zIndex ?? 0\n    this._visible = options.visible !== false\n    this.buffered = options.buffered ?? false\n    this._live = options.live ?? false\n    this._liveCount = this._live && this._visible ? 1 : 0\n    this._opacity = options.opacity !== undefined ? Math.max(0, Math.min(1, options.opacity)) : 1.0\n\n    // TODO: use a global yoga config\n    this.yogaNode = Yoga.Node.create(yogaConfig)\n    this.yogaNode.setDisplay(this._visible ? Display.Flex : Display.None)\n    this.setupYogaProperties(options)\n\n    this.applyEventOptions(options)\n\n    if (this.buffered) {\n      this.createFrameBuffer()\n    }\n  }\n\n  public override get id() {\n    return this._id\n  }\n\n  public override set id(value: string) {\n    if (this.parent) {\n      this.parent.renderableMapById.delete(this.id)\n      this.parent.renderableMapById.set(value, this)\n    }\n    super.id = value\n  }\n\n  public get focusable(): boolean {\n    return this._focusable\n  }\n\n  public set focusable(value: boolean) {\n    this._focusable = value\n  }\n\n  public get ctx(): RenderContext {\n    return this._ctx\n  }\n\n  public get visible(): boolean {\n    return this._visible\n  }\n\n  public get primaryAxis(): \"row\" | \"column\" {\n    const dir = this.yogaNode.getFlexDirection()\n    return dir === 2 || dir === 3 ? \"row\" : \"column\"\n  }\n\n  public set visible(value: boolean) {\n    if (this._visible === value) return\n\n    const wasVisible = this._visible\n    this._visible = value\n    this.yogaNode.setDisplay(value ? Display.Flex : Display.None)\n\n    if (this._live) {\n      if (!wasVisible && value) {\n        this.propagateLiveCount(1)\n      } else if (wasVisible && !value) {\n        this.propagateLiveCount(-1)\n      }\n    }\n\n    if (this._focused) {\n      this.blur()\n    }\n    this.requestRender()\n  }\n\n  public get opacity(): number {\n    return this._opacity\n  }\n\n  public set opacity(value: number) {\n    const clamped = Math.max(0, Math.min(1, value))\n    if (this._opacity !== clamped) {\n      this._opacity = clamped\n      this.requestRender()\n    }\n  }\n\n  public hasSelection(): boolean {\n    return false\n  }\n\n  public onSelectionChanged(selection: Selection | null): boolean {\n    // Default implementation: do nothing\n    // Override this method to provide custom selection handling\n    return false\n  }\n\n  public getSelectedText(): string {\n    return \"\"\n  }\n\n  public shouldStartSelection(x: number, y: number): boolean {\n    return false\n  }\n\n  public focus(): void {\n    if (this._isDestroyed || this._focused || !this._focusable) return\n\n    this._ctx.focusRenderable(this)\n    this._focused = true\n    this.requestRender()\n\n    this.keypressHandler = (key: KeyEvent) => {\n      if (this._isDestroyed) return\n      this._keyListeners[\"down\"]?.(key)\n      // Check again after user listener - it might have destroyed the renderable\n      if (this._isDestroyed) return\n      if (!key.defaultPrevented && this.handleKeyPress) {\n        this.handleKeyPress(key)\n      }\n    }\n\n    this.pasteHandler = (event: PasteEvent) => {\n      if (this._isDestroyed) return\n      this._pasteListener?.call(this, event)\n      // Check again after user listener - it might have destroyed the renderable\n      if (this._isDestroyed) return\n      if (!event.defaultPrevented && this.handlePaste) {\n        this.handlePaste(event)\n      }\n    }\n\n    this.ctx._internalKeyInput.onInternal(\"keypress\", this.keypressHandler)\n    this.ctx._internalKeyInput.onInternal(\"paste\", this.pasteHandler)\n    this.emit(RenderableEvents.FOCUSED)\n  }\n\n  public blur(): void {\n    if (!this._focused || !this._focusable) return\n\n    this._focused = false\n    this.requestRender()\n\n    if (this.keypressHandler) {\n      this.ctx._internalKeyInput.offInternal(\"keypress\", this.keypressHandler)\n      this.keypressHandler = null\n    }\n\n    if (this.pasteHandler) {\n      this.ctx._internalKeyInput.offInternal(\"paste\", this.pasteHandler)\n      this.pasteHandler = null\n    }\n\n    this.emit(RenderableEvents.BLURRED)\n  }\n\n  public get focused(): boolean {\n    return this._focused\n  }\n\n  public get live(): boolean {\n    return this._live\n  }\n\n  public get liveCount(): number {\n    return this._liveCount\n  }\n\n  public set live(value: boolean) {\n    if (this._live === value) return\n\n    this._live = value\n\n    if (this._visible) {\n      const delta = value ? 1 : -1\n      this.propagateLiveCount(delta)\n    }\n  }\n\n  protected propagateLiveCount(delta: number): void {\n    this._liveCount += delta\n    this.parent?.propagateLiveCount(delta)\n  }\n\n  public handleKeyPress?(key: KeyEvent): boolean\n  public handlePaste?(event: PasteEvent): void\n\n  public findDescendantById(id: string): Renderable | undefined {\n    for (const child of this._childrenInLayoutOrder) {\n      if (child.id === id) return child\n      if (isRenderable(child)) {\n        const found = child.findDescendantById(id)\n        if (found) return found\n      }\n    }\n    return undefined\n  }\n\n  public requestRender() {\n    this.markDirty()\n    this._ctx.requestRender()\n  }\n\n  public get translateX(): number {\n    return this._translateX\n  }\n\n  public set translateX(value: number) {\n    if (this._translateX === value) return\n    this._translateX = value\n    if (this.parent) this.parent.childrenPrimarySortDirty = true\n    this.requestRender()\n  }\n\n  public get translateY(): number {\n    return this._translateY\n  }\n\n  public set translateY(value: number) {\n    if (this._translateY === value) return\n    this._translateY = value\n    if (this.parent) this.parent.childrenPrimarySortDirty = true\n    this.requestRender()\n  }\n\n  public get x(): number {\n    if (this.parent) {\n      return this.parent.x + this._x + this._translateX\n    }\n    return this._x + this._translateX\n  }\n\n  public set x(value: number) {\n    this.left = value\n  }\n\n  public get top(): number | \"auto\" | `${number}%` | undefined {\n    return this._position.top\n  }\n\n  public set top(value: number | \"auto\" | `${number}%` | undefined) {\n    if (isPositionType(value) || value === undefined) {\n      this.setPosition({ top: value })\n    }\n  }\n\n  public get right(): number | \"auto\" | `${number}%` | undefined {\n    return this._position.right\n  }\n\n  public set right(value: number | \"auto\" | `${number}%` | undefined) {\n    if (isPositionType(value) || value === undefined) {\n      this.setPosition({ right: value })\n    }\n  }\n\n  public get bottom(): number | \"auto\" | `${number}%` | undefined {\n    return this._position.bottom\n  }\n\n  public set bottom(value: number | \"auto\" | `${number}%` | undefined) {\n    if (isPositionType(value) || value === undefined) {\n      this.setPosition({ bottom: value })\n    }\n  }\n\n  public get left(): number | \"auto\" | `${number}%` | undefined {\n    return this._position.left\n  }\n\n  public set left(value: number | \"auto\" | `${number}%` | undefined) {\n    if (isPositionType(value) || value === undefined) {\n      this.setPosition({ left: value })\n    }\n  }\n\n  public get y(): number {\n    if (this.parent) {\n      return this.parent.y + this._y + this._translateY\n    }\n    return this._y + this._translateY\n  }\n\n  public set y(value: number) {\n    this.top = value\n  }\n\n  public get width(): number {\n    return this._widthValue\n  }\n\n  public set width(value: number | \"auto\" | `${number}%`) {\n    if (isDimensionType(value)) {\n      this._width = value\n      this.yogaNode.setWidth(value)\n\n      if (typeof value === \"number\" && this._flexShrink === 1) {\n        this._flexShrink = 0\n        this.yogaNode.setFlexShrink(0)\n      }\n\n      this.requestRender()\n    }\n  }\n\n  public get height(): number {\n    return this._heightValue\n  }\n\n  public set height(value: number | \"auto\" | `${number}%`) {\n    if (isDimensionType(value)) {\n      this._height = value\n      this.yogaNode.setHeight(value)\n\n      if (typeof value === \"number\" && this._flexShrink === 1) {\n        this._flexShrink = 0\n        this.yogaNode.setFlexShrink(0)\n      }\n\n      this.requestRender()\n    }\n  }\n\n  public get zIndex(): number {\n    return this._zIndex\n  }\n\n  public set zIndex(value: number) {\n    if (this._zIndex !== value) {\n      this._zIndex = value\n      this.parent?.requestZIndexSort()\n      this.requestRender()\n    }\n  }\n\n  private requestZIndexSort(): void {\n    this.needsZIndexSort = true\n  }\n\n  private ensureZIndexSorted(): void {\n    if (this.needsZIndexSort) {\n      this._childrenInZIndexOrder.sort((a, b) => (a.zIndex > b.zIndex ? 1 : a.zIndex < b.zIndex ? -1 : 0))\n      this.needsZIndexSort = false\n    }\n  }\n\n  public getChildrenSortedByPrimaryAxis(): Renderable[] {\n    if (\n      !this.childrenPrimarySortDirty &&\n      this.childrenSortedByPrimaryAxis.length === this._childrenInLayoutOrder.length\n    ) {\n      return this.childrenSortedByPrimaryAxis\n    }\n\n    const dir = this.yogaNode.getFlexDirection()\n    const axis: \"x\" | \"y\" = dir === 2 || dir === 3 ? \"x\" : \"y\"\n\n    const sorted = [...this._childrenInLayoutOrder]\n    sorted.sort((a, b) => {\n      const va = axis === \"y\" ? a.y : a.x\n      const vb = axis === \"y\" ? b.y : b.x\n      return va - vb\n    })\n\n    this.childrenSortedByPrimaryAxis = sorted\n    this.childrenPrimarySortDirty = false\n    return this.childrenSortedByPrimaryAxis\n  }\n\n  private setupYogaProperties(options: RenderableOptions<Renderable>): void {\n    const node = this.yogaNode\n\n    if (isFlexBasisType(options.flexBasis)) {\n      node.setFlexBasis(options.flexBasis)\n    }\n\n    if (isSizeType(options.minWidth)) {\n      node.setMinWidth(options.minWidth)\n    }\n    if (isSizeType(options.minHeight)) {\n      node.setMinHeight(options.minHeight)\n    }\n\n    if (options.flexGrow !== undefined) {\n      node.setFlexGrow(options.flexGrow)\n    } else {\n      node.setFlexGrow(0)\n    }\n\n    if (options.flexShrink !== undefined) {\n      this._flexShrink = options.flexShrink\n      node.setFlexShrink(options.flexShrink)\n    } else {\n      // If explicit numeric width is set, don't shrink by default\n      // Otherwise follow web default of 1\n      const hasExplicitWidth = typeof options.width === \"number\"\n      const hasExplicitHeight = typeof options.height === \"number\"\n      this._flexShrink = hasExplicitWidth || hasExplicitHeight ? 0 : 1\n      node.setFlexShrink(this._flexShrink)\n    }\n\n    node.setFlexDirection(parseFlexDirection(options.flexDirection))\n    node.setFlexWrap(parseWrap(options.flexWrap))\n    node.setAlignItems(parseAlignItems(options.alignItems))\n    node.setJustifyContent(parseJustify(options.justifyContent))\n    node.setAlignSelf(parseAlign(options.alignSelf))\n\n    if (isDimensionType(options.width)) {\n      this._width = options.width\n      this.yogaNode.setWidth(options.width)\n    }\n    if (isDimensionType(options.height)) {\n      this._height = options.height\n      this.yogaNode.setHeight(options.height)\n    }\n\n    this._positionType = options.position === \"absolute\" ? \"absolute\" : \"relative\"\n    if (this._positionType !== \"relative\") {\n      node.setPositionType(parsePositionType(this._positionType))\n    }\n\n    this._overflow = options.overflow === \"hidden\" ? \"hidden\" : options.overflow === \"scroll\" ? \"scroll\" : \"visible\"\n    if (this._overflow !== \"visible\") {\n      node.setOverflow(parseOverflow(this._overflow))\n    }\n\n    // TODO: flatten position properties internally as well\n    const hasPositionProps =\n      options.top !== undefined ||\n      options.right !== undefined ||\n      options.bottom !== undefined ||\n      options.left !== undefined\n    if (hasPositionProps) {\n      this._position = {\n        top: options.top,\n        right: options.right,\n        bottom: options.bottom,\n        left: options.left,\n      }\n      this.updateYogaPosition(this._position)\n    }\n\n    if (isSizeType(options.maxWidth)) {\n      node.setMaxWidth(options.maxWidth)\n    }\n    if (isSizeType(options.maxHeight)) {\n      node.setMaxHeight(options.maxHeight)\n    }\n\n    this.setupMarginAndPadding(options)\n  }\n\n  private setupMarginAndPadding(options: RenderableOptions<Renderable>): void {\n    const node = this.yogaNode\n\n    if (isMarginType(options.margin)) {\n      node.setMargin(Edge.All, options.margin)\n    }\n\n    if (isMarginType(options.marginX)) {\n      node.setMargin(Edge.Horizontal, options.marginX)\n    }\n    if (isMarginType(options.marginY)) {\n      node.setMargin(Edge.Vertical, options.marginY)\n    }\n    if (isMarginType(options.marginTop)) {\n      node.setMargin(Edge.Top, options.marginTop)\n    }\n    if (isMarginType(options.marginRight)) {\n      node.setMargin(Edge.Right, options.marginRight)\n    }\n    if (isMarginType(options.marginBottom)) {\n      node.setMargin(Edge.Bottom, options.marginBottom)\n    }\n    if (isMarginType(options.marginLeft)) {\n      node.setMargin(Edge.Left, options.marginLeft)\n    }\n\n    if (isPaddingType(options.padding)) {\n      node.setPadding(Edge.All, options.padding)\n    }\n\n    if (isPaddingType(options.paddingX)) {\n      node.setPadding(Edge.Horizontal, options.paddingX)\n    }\n    if (isPaddingType(options.paddingY)) {\n      node.setPadding(Edge.Vertical, options.paddingY)\n    }\n    if (isPaddingType(options.paddingTop)) {\n      node.setPadding(Edge.Top, options.paddingTop)\n    }\n    if (isPaddingType(options.paddingRight)) {\n      node.setPadding(Edge.Right, options.paddingRight)\n    }\n    if (isPaddingType(options.paddingBottom)) {\n      node.setPadding(Edge.Bottom, options.paddingBottom)\n    }\n    if (isPaddingType(options.paddingLeft)) {\n      node.setPadding(Edge.Left, options.paddingLeft)\n    }\n  }\n\n  set position(positionType: PositionTypeString | null | undefined) {\n    if (!isPositionTypeType(positionType) || this._positionType === positionType) return\n\n    this._positionType = positionType\n    this.yogaNode.setPositionType(parsePositionType(positionType))\n    this.requestRender()\n  }\n\n  get overflow(): OverflowString {\n    return this._overflow\n  }\n\n  set overflow(overflow: OverflowString | null | undefined) {\n    if (!isOverflowType(overflow) || this._overflow === overflow) return\n\n    this._overflow = overflow\n    this.yogaNode.setOverflow(parseOverflow(overflow))\n    this.requestRender()\n  }\n\n  public setPosition(position: Position): void {\n    this._position = { ...this._position, ...position }\n    this.updateYogaPosition(position)\n  }\n\n  private updateYogaPosition(position: Position): void {\n    const node = this.yogaNode\n    const { top, right, bottom, left } = position\n\n    if (isPositionType(top)) {\n      if (top === \"auto\") {\n        node.setPositionAuto(Edge.Top)\n      } else {\n        node.setPosition(Edge.Top, top)\n      }\n    }\n    if (isPositionType(right)) {\n      if (right === \"auto\") {\n        node.setPositionAuto(Edge.Right)\n      } else {\n        node.setPosition(Edge.Right, right)\n      }\n    }\n    if (isPositionType(bottom)) {\n      if (bottom === \"auto\") {\n        node.setPositionAuto(Edge.Bottom)\n      } else {\n        node.setPosition(Edge.Bottom, bottom)\n      }\n    }\n    if (isPositionType(left)) {\n      if (left === \"auto\") {\n        node.setPositionAuto(Edge.Left)\n      } else {\n        node.setPosition(Edge.Left, left)\n      }\n    }\n    this.requestRender()\n  }\n\n  public set flexGrow(grow: number | null | undefined) {\n    if (grow == null) {\n      this.yogaNode.setFlexGrow(0)\n    } else {\n      this.yogaNode.setFlexGrow(grow)\n    }\n    this.requestRender()\n  }\n\n  public set flexShrink(shrink: number | null | undefined) {\n    const value = shrink == null ? 1 : shrink\n    this._flexShrink = value\n    this.yogaNode.setFlexShrink(value)\n    this.requestRender()\n  }\n\n  public set flexDirection(direction: FlexDirectionString | null | undefined) {\n    this.yogaNode.setFlexDirection(parseFlexDirection(direction))\n    this.requestRender()\n  }\n\n  public set flexWrap(wrap: WrapString | null | undefined) {\n    this.yogaNode.setFlexWrap(parseWrap(wrap))\n    this.requestRender()\n  }\n\n  public set alignItems(alignItems: AlignString | null | undefined) {\n    this.yogaNode.setAlignItems(parseAlignItems(alignItems))\n    this.requestRender()\n  }\n\n  public set justifyContent(justifyContent: JustifyString | null | undefined) {\n    this.yogaNode.setJustifyContent(parseJustify(justifyContent))\n    this.requestRender()\n  }\n\n  public set alignSelf(alignSelf: AlignString | null | undefined) {\n    this.yogaNode.setAlignSelf(parseAlign(alignSelf))\n    this.requestRender()\n  }\n\n  public set flexBasis(basis: number | \"auto\" | null | undefined) {\n    if (isFlexBasisType(basis)) {\n      this.yogaNode.setFlexBasis(basis)\n      this.requestRender()\n    }\n  }\n\n  public set minWidth(minWidth: number | `${number}%` | null | undefined) {\n    if (isSizeType(minWidth)) {\n      this.yogaNode.setMinWidth(minWidth)\n      this.requestRender()\n    }\n  }\n\n  public set maxWidth(maxWidth: number | `${number}%` | null | undefined) {\n    if (isSizeType(maxWidth)) {\n      this.yogaNode.setMaxWidth(maxWidth)\n      this.requestRender()\n    }\n  }\n\n  public set minHeight(minHeight: number | `${number}%` | null | undefined) {\n    if (isSizeType(minHeight)) {\n      this.yogaNode.setMinHeight(minHeight)\n      this.requestRender()\n    }\n  }\n\n  public set maxHeight(maxHeight: number | `${number}%` | null | undefined) {\n    if (isSizeType(maxHeight)) {\n      this.yogaNode.setMaxHeight(maxHeight)\n      this.requestRender()\n    }\n  }\n\n  public set margin(margin: number | \"auto\" | `${number}%` | null | undefined) {\n    if (isMarginType(margin)) {\n      this.yogaNode.setMargin(Edge.All, margin)\n      this.requestRender()\n    }\n  }\n\n  public set marginX(marginX: number | \"auto\" | `${number}%` | null | undefined) {\n    if (isMarginType(marginX)) {\n      this.yogaNode.setMargin(Edge.Horizontal, marginX)\n      this.requestRender()\n    }\n  }\n\n  public set marginY(marginY: number | \"auto\" | `${number}%` | null | undefined) {\n    if (isMarginType(marginY)) {\n      this.yogaNode.setMargin(Edge.Vertical, marginY)\n      this.requestRender()\n    }\n  }\n\n  public set marginTop(margin: number | \"auto\" | `${number}%` | null | undefined) {\n    if (isMarginType(margin)) {\n      this.yogaNode.setMargin(Edge.Top, margin)\n      this.requestRender()\n    }\n  }\n\n  public set marginRight(margin: number | \"auto\" | `${number}%` | null | undefined) {\n    if (isMarginType(margin)) {\n      this.yogaNode.setMargin(Edge.Right, margin)\n      this.requestRender()\n    }\n  }\n\n  public set marginBottom(margin: number | \"auto\" | `${number}%` | null | undefined) {\n    if (isMarginType(margin)) {\n      this.yogaNode.setMargin(Edge.Bottom, margin)\n      this.requestRender()\n    }\n  }\n\n  public set marginLeft(margin: number | \"auto\" | `${number}%` | null | undefined) {\n    if (isMarginType(margin)) {\n      this.yogaNode.setMargin(Edge.Left, margin)\n      this.requestRender()\n    }\n  }\n\n  public set padding(padding: number | `${number}%` | null | undefined) {\n    if (isPaddingType(padding)) {\n      this.yogaNode.setPadding(Edge.All, padding)\n      this.requestRender()\n    }\n  }\n\n  public set paddingX(paddingX: number | `${number}%` | null | undefined) {\n    if (isPaddingType(paddingX)) {\n      this.yogaNode.setPadding(Edge.Horizontal, paddingX)\n      this.requestRender()\n    }\n  }\n\n  public set paddingY(paddingY: number | `${number}%` | null | undefined) {\n    if (isPaddingType(paddingY)) {\n      this.yogaNode.setPadding(Edge.Vertical, paddingY)\n      this.requestRender()\n    }\n  }\n\n  public set paddingTop(padding: number | `${number}%` | null | undefined) {\n    if (isPaddingType(padding)) {\n      this.yogaNode.setPadding(Edge.Top, padding)\n      this.requestRender()\n    }\n  }\n\n  public set paddingRight(padding: number | `${number}%` | null | undefined) {\n    if (isPaddingType(padding)) {\n      this.yogaNode.setPadding(Edge.Right, padding)\n      this.requestRender()\n    }\n  }\n\n  public set paddingBottom(padding: number | `${number}%` | null | undefined) {\n    if (isPaddingType(padding)) {\n      this.yogaNode.setPadding(Edge.Bottom, padding)\n      this.requestRender()\n    }\n  }\n\n  public set paddingLeft(padding: number | `${number}%` | null | undefined) {\n    if (isPaddingType(padding)) {\n      this.yogaNode.setPadding(Edge.Left, padding)\n      this.requestRender()\n    }\n  }\n\n  public getLayoutNode(): YogaNode {\n    return this.yogaNode\n  }\n\n  public updateFromLayout(): void {\n    const layout = this.yogaNode.getComputedLayout()\n\n    const oldX = this._x\n    const oldY = this._y\n    const oldWidth = this._widthValue\n    const oldHeight = this._heightValue\n\n    this._x = layout.left\n    this._y = layout.top\n\n    const newWidth = Math.max(layout.width, 1)\n    const newHeight = Math.max(layout.height, 1)\n    const sizeChanged = oldWidth !== newWidth || oldHeight !== newHeight\n\n    this._widthValue = newWidth\n    this._heightValue = newHeight\n\n    if (sizeChanged) {\n      this.onLayoutResize(newWidth, newHeight)\n    }\n\n    const positionChanged = oldX !== this._x || oldY !== this._y\n    if (positionChanged) {\n      if (this.parent) this.parent.childrenPrimarySortDirty = true\n    }\n  }\n\n  protected onLayoutResize(width: number, height: number): void {\n    if (this._visible) {\n      // TODO: Should probably .markDirty()\n      this.handleFrameBufferResize(width, height)\n      this.onResize(width, height)\n      this.requestRender()\n    }\n  }\n\n  protected handleFrameBufferResize(width: number, height: number): void {\n    if (!this.buffered) return\n\n    if (width <= 0 || height <= 0) {\n      return\n    }\n\n    if (this.frameBuffer) {\n      this.frameBuffer.resize(width, height)\n    } else {\n      this.createFrameBuffer()\n    }\n  }\n\n  protected createFrameBuffer(): void {\n    const w = this.width\n    const h = this.height\n\n    if (w <= 0 || h <= 0) {\n      return\n    }\n\n    try {\n      const widthMethod = this._ctx.widthMethod\n      this.frameBuffer = OptimizedBuffer.create(w, h, widthMethod, { respectAlpha: true, id: `framebuffer-${this.id}` })\n    } catch (error) {\n      console.error(`Failed to create frame buffer for ${this.id}:`, error)\n      this.frameBuffer = null\n    }\n  }\n\n  /**\n   * This will be called during a render pass.\n   * Requesting a render during a render pass will drop the requested render.\n   * If you need to request a render during a render pass, use process.nextTick.\n   */\n  protected onResize(width: number, height: number): void {\n    this.onSizeChange?.()\n    this.emit(\"resize\")\n    // Override in subclasses for additional resize logic\n  }\n\n  private replaceParent(obj: Renderable) {\n    if (obj.parent) {\n      obj.parent.remove(obj.id)\n    }\n    obj.parent = this\n  }\n\n  public add(obj: Renderable | VNode<any, any[]> | unknown, index?: number): number {\n    if (!obj) {\n      return -1\n    }\n\n    const renderable = maybeMakeRenderable(this._ctx, obj)\n    if (!renderable) {\n      return -1\n    }\n\n    if (renderable.isDestroyed) {\n      if (process.env.NODE_ENV !== \"production\") {\n        console.warn(`Renderable with id ${renderable.id} was already destroyed, skipping add`)\n      }\n      return -1\n    }\n\n    const anchorRenderable = index !== undefined ? this._childrenInLayoutOrder[index] : undefined\n\n    if (anchorRenderable) {\n      return this.insertBefore(renderable, anchorRenderable)\n    }\n\n    if (renderable.parent === this) {\n      this.yogaNode.removeChild(renderable.getLayoutNode())\n      this._childrenInLayoutOrder.splice(this._childrenInLayoutOrder.indexOf(renderable), 1)\n    } else {\n      this.replaceParent(renderable)\n      this.needsZIndexSort = true\n      this.renderableMapById.set(renderable.id, renderable)\n      this._childrenInZIndexOrder.push(renderable)\n\n      if (typeof renderable.onLifecyclePass === \"function\") {\n        this._ctx.registerLifecyclePass(renderable)\n      }\n\n      if (renderable._liveCount > 0) {\n        this.propagateLiveCount(renderable._liveCount)\n      }\n    }\n\n    const childLayoutNode = renderable.getLayoutNode()\n    const insertedIndex = this._childrenInLayoutOrder.length\n    this._childrenInLayoutOrder.push(renderable)\n    this.yogaNode.insertChild(childLayoutNode, insertedIndex)\n\n    this.childrenPrimarySortDirty = true\n    this._shouldUpdateBefore.add(renderable)\n\n    this.requestRender()\n\n    return insertedIndex\n  }\n\n  insertBefore(obj: Renderable | VNode<any, any[]> | unknown, anchor?: Renderable | unknown): number {\n    if (!anchor) {\n      return this.add(obj)\n    }\n\n    if (!obj) {\n      return -1\n    }\n\n    const renderable = maybeMakeRenderable(this._ctx, obj)\n    if (!renderable) {\n      return -1\n    }\n\n    if (renderable.isDestroyed) {\n      if (process.env.NODE_ENV !== \"production\") {\n        console.warn(`Renderable with id ${renderable.id} was already destroyed, skipping insertBefore`)\n      }\n      return -1\n    }\n\n    if (!isRenderable(anchor)) {\n      throw new Error(\"Anchor must be a Renderable\")\n    }\n\n    if (anchor.isDestroyed) {\n      if (process.env.NODE_ENV !== \"production\") {\n        console.warn(`Anchor with id ${anchor.id} was already destroyed, skipping insertBefore`)\n      }\n      return -1\n    }\n\n    if (!this.renderableMapById.has(anchor.id)) {\n      if (process.env.NODE_ENV !== \"production\") {\n        console.warn(`Anchor with id ${anchor.id} does not exist within the parent ${this.id}, skipping insertBefore`)\n      }\n      return -1\n    }\n\n    if (renderable === anchor || renderable.id === anchor.id) {\n      if (process.env.NODE_ENV !== \"production\") {\n        console.warn(`Anchor is the same as the node ${renderable.id} being inserted, skipping insertBefore`)\n      }\n      return -1\n    }\n\n    if (renderable.parent === this) {\n      this.yogaNode.removeChild(renderable.getLayoutNode())\n      this._childrenInLayoutOrder.splice(this._childrenInLayoutOrder.indexOf(renderable), 1)\n    } else {\n      this.replaceParent(renderable)\n      this.needsZIndexSort = true\n      this.renderableMapById.set(renderable.id, renderable)\n      this._childrenInZIndexOrder.push(renderable)\n\n      if (typeof renderable.onLifecyclePass === \"function\") {\n        this._ctx.registerLifecyclePass(renderable)\n      }\n\n      if (renderable._liveCount > 0) {\n        this.propagateLiveCount(renderable._liveCount)\n      }\n    }\n\n    this.childrenPrimarySortDirty = true\n\n    const anchorIndex = this._childrenInLayoutOrder.indexOf(anchor)\n    const insertedIndex = Math.max(0, Math.min(anchorIndex, this._childrenInLayoutOrder.length))\n\n    this._childrenInLayoutOrder.splice(insertedIndex, 0, renderable)\n    this.yogaNode.insertChild(renderable.getLayoutNode(), insertedIndex)\n\n    this._shouldUpdateBefore.add(renderable)\n\n    this.requestRender()\n\n    return insertedIndex\n  }\n\n  // TODO: that naming is meh\n  public getRenderable(id: string): Renderable | undefined {\n    return this.renderableMapById.get(id)\n  }\n\n  public remove(id: string): void {\n    if (!id) {\n      return\n    }\n\n    if (this.renderableMapById.has(id)) {\n      const obj = this.renderableMapById.get(id)\n      if (obj) {\n        if (obj._liveCount > 0) {\n          this.propagateLiveCount(-obj._liveCount)\n        }\n\n        const childLayoutNode = obj.getLayoutNode()\n        this.yogaNode.removeChild(childLayoutNode)\n        this.requestRender()\n\n        obj.onRemove()\n        obj.parent = null\n        this._ctx.unregisterLifecyclePass(obj)\n        this.renderableMapById.delete(id)\n\n        const index = this._childrenInLayoutOrder.findIndex((obj) => obj.id === id)\n        if (index !== -1) {\n          this._childrenInLayoutOrder.splice(index, 1)\n        }\n\n        const zIndexIndex = this._childrenInZIndexOrder.findIndex((obj) => obj.id === id)\n        if (zIndexIndex !== -1) {\n          this._childrenInZIndexOrder.splice(zIndexIndex, 1)\n        }\n\n        this.childrenPrimarySortDirty = true\n      }\n    }\n  }\n\n  protected onRemove(): void {\n    // Default implementation: do nothing\n    // Override this method to provide custom removal logic\n  }\n\n  public getChildren(): Renderable[] {\n    return [...this._childrenInLayoutOrder]\n  }\n\n  public getChildrenCount(): number {\n    return this._childrenInLayoutOrder.length\n  }\n\n  public updateLayout(deltaTime: number, renderList: RenderCommand[] = []): void {\n    if (!this.visible) return\n\n    this.onUpdate(deltaTime)\n\n    // If destroyed during onUpdate, don't add to render list\n    if (this._isDestroyed) return\n\n    // NOTE: worst case updateFromLayout is called throughout the whole tree,\n    // which currently still has yoga performance issues.\n    // This can be mitigated at some point when the layout tree moved to native,\n    // as in the native yoga tree we can use events during the calculateLayout phase,\n    // and anctually know if a child has changed or not.\n    // That would allow us to to generate optimised render commands,\n    // including the layout updates, in one pass.\n    this.updateFromLayout()\n\n    // Update newly added children before getting visible children\n    // This ensures their positions are current when culling happens\n    if (this._shouldUpdateBefore.size > 0) {\n      for (const child of this._shouldUpdateBefore) {\n        if (!child.isDestroyed) {\n          child.updateFromLayout()\n        }\n      }\n      this._shouldUpdateBefore.clear()\n    }\n\n    // Check again after updateFromLayout, which calls onResize/onSizeChange\n    if (this._isDestroyed) return\n\n    // Push opacity BEFORE rendering this element so it affects this element and all children\n    const shouldPushOpacity = this._opacity < 1.0\n    if (shouldPushOpacity) {\n      renderList.push({ action: \"pushOpacity\", opacity: this._opacity })\n    }\n\n    renderList.push({ action: \"render\", renderable: this })\n\n    this.ensureZIndexSorted()\n\n    const shouldPushScissor = this._overflow !== \"visible\" && this.width > 0 && this.height > 0\n    if (shouldPushScissor) {\n      const scissorRect = this.getScissorRect()\n      renderList.push({\n        action: \"pushScissorRect\",\n        x: scissorRect.x,\n        y: scissorRect.y,\n        width: scissorRect.width,\n        height: scissorRect.height,\n        screenX: this.x,\n        screenY: this.y,\n      })\n    }\n    const visibleChildren = this._getVisibleChildren()\n    for (const child of this._childrenInZIndexOrder) {\n      if (!visibleChildren.includes(child.num)) {\n        child.updateFromLayout()\n        continue\n      }\n      child.updateLayout(deltaTime, renderList)\n    }\n\n    if (shouldPushScissor) {\n      renderList.push({ action: \"popScissorRect\" })\n    }\n    if (shouldPushOpacity) {\n      renderList.push({ action: \"popOpacity\" })\n    }\n  }\n\n  public render(buffer: OptimizedBuffer, deltaTime: number): void {\n    let renderBuffer = buffer\n    if (this.buffered && this.frameBuffer) {\n      renderBuffer = this.frameBuffer\n    }\n\n    if (this.renderBefore) {\n      this.renderBefore.call(this, renderBuffer, deltaTime)\n    }\n\n    this.renderSelf(renderBuffer, deltaTime)\n\n    if (this.renderAfter) {\n      this.renderAfter.call(this, renderBuffer, deltaTime)\n    }\n\n    this.markClean()\n    this._ctx.addToHitGrid(this.x, this.y, this.width, this.height, this.num)\n\n    if (this.buffered && this.frameBuffer) {\n      buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer)\n    }\n  }\n\n  protected _getVisibleChildren(): number[] {\n    return this._childrenInZIndexOrder.map((child) => child.num)\n  }\n\n  protected onUpdate(deltaTime: number): void {\n    // Default implementation: do nothing\n    // Override this method to provide custom rendering\n  }\n\n  protected getScissorRect(): { x: number; y: number; width: number; height: number } {\n    return {\n      x: this.buffered ? 0 : this.x,\n      y: this.buffered ? 0 : this.y,\n      width: this.width,\n      height: this.height,\n    }\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    // Default implementation: do nothing\n    // Override this method to provide custom rendering\n  }\n\n  public get isDestroyed(): boolean {\n    return this._isDestroyed\n  }\n\n  public destroy(): void {\n    if (this._isDestroyed) {\n      return\n    }\n\n    this._isDestroyed = true\n\n    if (this.parent) {\n      this.parent.remove(this.id)\n    }\n\n    if (this.frameBuffer) {\n      this.frameBuffer.destroy()\n      this.frameBuffer = null\n    }\n\n    for (const child of this._childrenInLayoutOrder) {\n      this.remove(child.id)\n    }\n\n    this._childrenInLayoutOrder = []\n    this.renderableMapById.clear()\n    Renderable.renderablesByNumber.delete(this.num)\n\n    this.blur()\n    this.removeAllListeners()\n\n    this.destroySelf()\n\n    try {\n      this.yogaNode.free()\n    } catch (e) {\n      // Might be already freed and will throw an error if we try to free it again\n    }\n  }\n\n  public destroyRecursively(): void {\n    // Destroy children first to ensure removal as destroy clears child array\n    // Make a copy of the children array to avoid iteration issues when children are destroyed\n    const children = [...this._childrenInLayoutOrder]\n    for (const child of children) {\n      child.destroyRecursively()\n    }\n    this.destroy()\n  }\n\n  protected destroySelf(): void {\n    // Default implementation: do nothing else\n    // Override this method to provide custom cleanup\n  }\n\n  public processMouseEvent(event: MouseEvent): void {\n    this._mouseListener?.call(this, event)\n    this._mouseListeners[event.type]?.call(this, event)\n    this.onMouseEvent(event)\n\n    if (this.parent && !event.propagationStopped) {\n      this.parent.processMouseEvent(event)\n    }\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    // Default implementation: do nothing\n    // Override this method to provide custom event handling\n  }\n\n  public set onMouse(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListener = handler\n    else this._mouseListener = null\n  }\n\n  public set onMouseDown(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"down\"] = handler\n    else delete this._mouseListeners[\"down\"]\n  }\n\n  public set onMouseUp(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"up\"] = handler\n    else delete this._mouseListeners[\"up\"]\n  }\n\n  public set onMouseMove(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"move\"] = handler\n    else delete this._mouseListeners[\"move\"]\n  }\n\n  public set onMouseDrag(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"drag\"] = handler\n    else delete this._mouseListeners[\"drag\"]\n  }\n\n  public set onMouseDragEnd(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"drag-end\"] = handler\n    else delete this._mouseListeners[\"drag-end\"]\n  }\n\n  public set onMouseDrop(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"drop\"] = handler\n    else delete this._mouseListeners[\"drop\"]\n  }\n\n  public set onMouseOver(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"over\"] = handler\n    else delete this._mouseListeners[\"over\"]\n  }\n\n  public set onMouseOut(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"out\"] = handler\n    else delete this._mouseListeners[\"out\"]\n  }\n\n  public set onMouseScroll(handler: ((event: MouseEvent) => void) | undefined) {\n    if (handler) this._mouseListeners[\"scroll\"] = handler\n    else delete this._mouseListeners[\"scroll\"]\n  }\n\n  public set onPaste(handler: ((event: PasteEvent) => void) | undefined) {\n    this._pasteListener = handler\n  }\n  public get onPaste(): ((event: PasteEvent) => void) | undefined {\n    return this._pasteListener\n  }\n\n  public set onKeyDown(handler: ((key: KeyEvent) => void) | undefined) {\n    if (handler) this._keyListeners[\"down\"] = handler\n    else delete this._keyListeners[\"down\"]\n  }\n  public get onKeyDown(): ((key: KeyEvent) => void) | undefined {\n    return this._keyListeners[\"down\"]\n  }\n\n  public set onSizeChange(handler: (() => void) | undefined) {\n    this._sizeChangeListener = handler\n  }\n  public get onSizeChange(): (() => void) | undefined {\n    return this._sizeChangeListener\n  }\n\n  private applyEventOptions(options: RenderableOptions<Renderable>): void {\n    this.onMouse = options.onMouse\n    this.onMouseDown = options.onMouseDown\n    this.onMouseUp = options.onMouseUp\n    this.onMouseMove = options.onMouseMove\n    this.onMouseDrag = options.onMouseDrag\n    this.onMouseDragEnd = options.onMouseDragEnd\n    this.onMouseDrop = options.onMouseDrop\n    this.onMouseOver = options.onMouseOver\n    this.onMouseOut = options.onMouseOut\n    this.onMouseScroll = options.onMouseScroll\n    this.onPaste = options.onPaste\n    this.onKeyDown = options.onKeyDown\n    this.onSizeChange = options.onSizeChange\n  }\n}\n\ninterface RenderCommandBase {\n  action: \"render\" | \"pushScissorRect\" | \"popScissorRect\" | \"pushOpacity\" | \"popOpacity\"\n}\n\ninterface RenderCommandPushScissorRect extends RenderCommandBase {\n  action: \"pushScissorRect\"\n  x: number\n  y: number\n  width: number\n  height: number\n  screenX: number\n  screenY: number\n}\n\ninterface RenderCommandPopScissorRect extends RenderCommandBase {\n  action: \"popScissorRect\"\n}\n\ninterface RenderCommandRender extends RenderCommandBase {\n  action: \"render\"\n  renderable: Renderable\n}\n\ninterface RenderCommandPushOpacity extends RenderCommandBase {\n  action: \"pushOpacity\"\n  opacity: number\n}\n\ninterface RenderCommandPopOpacity extends RenderCommandBase {\n  action: \"popOpacity\"\n}\n\nexport type RenderCommand =\n  | RenderCommandPushScissorRect\n  | RenderCommandPopScissorRect\n  | RenderCommandRender\n  | RenderCommandPushOpacity\n  | RenderCommandPopOpacity\n\nexport class RootRenderable extends Renderable {\n  private renderList: RenderCommand[] = []\n\n  constructor(ctx: RenderContext) {\n    super(ctx, { id: \"__root__\", zIndex: 0, visible: true, width: ctx.width, height: ctx.height, enableLayout: true })\n\n    if (this.yogaNode) {\n      this.yogaNode.free()\n    }\n\n    this.yogaNode = Yoga.Node.create(yogaConfig)\n    this.yogaNode.setWidth(ctx.width)\n    this.yogaNode.setHeight(ctx.height)\n    this.yogaNode.setFlexDirection(FlexDirection.Column)\n\n    this.calculateLayout()\n  }\n\n  public render(buffer: OptimizedBuffer, deltaTime: number): void {\n    if (!this.visible) return\n\n    // 0. Run lifecycle pass\n    for (const renderable of this._ctx.getLifecyclePasses()) {\n      renderable.onLifecyclePass?.call(renderable)\n    }\n\n    // NOTE: Strictly speaking, this is a 3-pass rendering process:\n    // 1. Calculate layout from root\n    // 2. Update layout throughout the tree and collect render list\n    // 3. Render all collected renderables\n    // Should be 2-pass by hooking into the calculateLayout phase,\n    // but that's only possible if we move the layout tree to native.\n\n    // 1. Calculate layout from root\n    if (this.yogaNode.isDirty()) {\n      this.calculateLayout()\n    }\n\n    // 2. Update layout throughout the tree and collect render list\n    this.renderList.length = 0\n    this.updateLayout(deltaTime, this.renderList)\n\n    // 3. Render all collected renderables\n    this._ctx.clearHitGridScissorRects()\n    for (let i = 1; i < this.renderList.length; i++) {\n      const command = this.renderList[i]\n      switch (command.action) {\n        case \"render\":\n          // Skip if renderable was destroyed during a previous render callback\n          if (!command.renderable.isDestroyed) {\n            command.renderable.render(buffer, deltaTime)\n          }\n          break\n        case \"pushScissorRect\":\n          buffer.pushScissorRect(command.x, command.y, command.width, command.height)\n          this._ctx.pushHitGridScissorRect(command.screenX, command.screenY, command.width, command.height)\n          break\n        case \"popScissorRect\":\n          buffer.popScissorRect()\n          this._ctx.popHitGridScissorRect()\n          break\n        case \"pushOpacity\":\n          buffer.pushOpacity(command.opacity)\n          break\n        case \"popOpacity\":\n          buffer.popOpacity()\n          break\n      }\n    }\n  }\n\n  protected propagateLiveCount(delta: number): void {\n    const oldCount = this._liveCount\n    this._liveCount += delta\n\n    if (oldCount === 0 && this._liveCount > 0) {\n      this._ctx.requestLive()\n    } else if (oldCount > 0 && this._liveCount === 0) {\n      this._ctx.dropLive()\n    }\n  }\n\n  public calculateLayout(): void {\n    this.yogaNode.calculateLayout(this.width, this.height, Direction.LTR)\n    this.emit(LayoutEvents.LAYOUT_CHANGED)\n  }\n\n  public resize(width: number, height: number): void {\n    this.width = width\n    this.height = height\n\n    this.emit(LayoutEvents.RESIZED, { width, height })\n  }\n}\n"
  },
  {
    "path": "packages/core/src/__snapshots__/buffer.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`OptimizedBuffer snapshot tests with unicode encoding should render ASCII text correctly: ASCII text rendering 1`] = `\n\"Hello               \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`OptimizedBuffer snapshot tests with unicode encoding should render emoji text correctly: Emoji text rendering 1`] = `\n\"Hi 👋 🌍            \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`OptimizedBuffer snapshot tests with unicode encoding should handle multiline text with unicode: Multiline unicode rendering 1`] = `\n\"Hi 世界             \n🌟 Star             \n                    \n                    \n                    \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/animation/Timeline.test.ts",
    "content": "import { expect, describe, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTimeline, Timeline, type JSAnimation, engine, type EasingFunctions } from \"./Timeline.js\"\n\ndescribe(\"Timeline\", () => {\n  let timeline: Timeline\n  let target: { x: number; y: number; value: number }\n  let updateCallbacks: JSAnimation[]\n\n  beforeEach(() => {\n    target = { x: 0, y: 0, value: 0 }\n    updateCallbacks = []\n  })\n\n  afterEach(() => {\n    engine.clear()\n  })\n\n  describe(\"Basic Animation\", () => {\n    it(\"should animate a single property\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      timeline.add(target, {\n        x: 100,\n        duration: 1000,\n        onUpdate: (anim: JSAnimation) => updateCallbacks.push(anim),\n      })\n\n      timeline.play()\n\n      engine.update(0)\n      expect(target.x).toBe(0)\n\n      engine.update(500)\n      expect(target.x).toBe(50)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(updateCallbacks.length).toBeGreaterThan(0)\n    })\n\n    it(\"should animate multiple properties\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      timeline.add(target, {\n        x: 100,\n        y: 200,\n        duration: 1000,\n      })\n\n      timeline.play()\n      engine.update(500)\n\n      expect(target.x).toBe(50)\n      expect(target.y).toBe(100)\n    })\n\n    it(\"should handle easing functions\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      timeline.add(target, {\n        x: 100,\n        duration: 1000,\n        ease: \"linear\",\n      })\n\n      timeline.play()\n      engine.update(500)\n\n      expect(target.x).toBe(50)\n    })\n  })\n\n  describe(\"Timeline Control\", () => {\n    beforeEach(() => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n      timeline.add(target, { x: 100, duration: 1000 })\n    })\n\n    it(\"should start paused when autoplay is false\", () => {\n      engine.update(500)\n      expect(target.x).toBe(0)\n    })\n\n    it(\"should animate when played\", () => {\n      timeline.play()\n      engine.update(500)\n      expect(target.x).toBe(50)\n    })\n\n    it(\"should pause animation\", () => {\n      timeline.play()\n      engine.update(250)\n      expect(target.x).toBe(25)\n\n      timeline.pause()\n      engine.update(250)\n      expect(target.x).toBe(25)\n    })\n\n    it(\"should restart animation\", () => {\n      timeline.play()\n      engine.update(500)\n      expect(target.x).toBe(50)\n\n      timeline.restart()\n      engine.update(250)\n      expect(target.x).toBe(25)\n    })\n\n    it(\"should play again when calling play() on a finished non-looping timeline\", () => {\n      timeline.play()\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n      expect(timeline.isPlaying).toBe(false)\n\n      timeline.play()\n      expect(timeline.isPlaying).toBe(true)\n\n      engine.update(500)\n      expect(target.x).toBe(50)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(timeline.isPlaying).toBe(false)\n    })\n\n    it(\"should call onPause callback when timeline is paused\", () => {\n      let pauseCallCount = 0\n      timeline = createTimeline({\n        duration: 1000,\n        autoplay: false,\n        onPause: () => pauseCallCount++,\n      })\n      timeline.add(target, { x: 100, duration: 1000 })\n\n      timeline.play()\n      engine.update(500)\n      expect(target.x).toBe(50)\n      expect(pauseCallCount).toBe(0)\n\n      timeline.pause()\n      expect(pauseCallCount).toBe(1)\n      expect(timeline.isPlaying).toBe(false)\n\n      timeline.pause()\n      expect(pauseCallCount).toBe(2)\n\n      timeline.play()\n      timeline.pause()\n      expect(pauseCallCount).toBe(3)\n    })\n\n    it(\"should not call onPause callback when timeline is not initialized with one\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n      timeline.add(target, { x: 100, duration: 1000 })\n\n      timeline.play()\n      timeline.pause()\n\n      expect(timeline.isPlaying).toBe(false)\n    })\n\n    it(\"should not call onPause callback when timeline completes naturally\", () => {\n      let pauseCallCount = 0\n      let completeCallCount = 0\n      timeline = createTimeline({\n        duration: 1000,\n        autoplay: false,\n        onPause: () => pauseCallCount++,\n        onComplete: () => completeCallCount++,\n      })\n      timeline.add(target, { x: 100, duration: 500 })\n\n      timeline.play()\n      engine.update(1000)\n\n      expect(timeline.isPlaying).toBe(false)\n      expect(pauseCallCount).toBe(0)\n      expect(completeCallCount).toBe(1)\n    })\n  })\n\n  describe(\"Looping\", () => {\n    it(\"should loop timeline when loop is true\", () => {\n      timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n      timeline.add(target, { x: 100, duration: 1000 })\n\n      timeline.play()\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n\n      engine.update(500)\n      expect(target.x).toBe(50)\n    })\n\n    it(\"should not loop when loop is false\", () => {\n      timeline = createTimeline({ duration: 1000, loop: false, autoplay: false })\n      timeline.add(target, { x: 100, duration: 1000 })\n\n      timeline.play()\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n      expect(timeline.isPlaying).toBe(false)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n    })\n  })\n\n  describe(\"Individual Animation Loops\", () => {\n    it(\"should loop individual animation specified number of times\", () => {\n      timeline = createTimeline({ duration: 5000, autoplay: false })\n\n      let completionCount = 0\n      timeline.add(target, {\n        x: 100,\n        duration: 1000,\n        loop: 3,\n        onComplete: () => completionCount++,\n      })\n\n      timeline.play()\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n      expect(completionCount).toBe(0)\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n      expect(completionCount).toBe(0)\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n      expect(completionCount).toBe(1)\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n      expect(completionCount).toBe(1)\n    })\n\n    it(\"should handle loop delay\", () => {\n      timeline = createTimeline({ duration: 5000, autoplay: false })\n\n      timeline.add(target, {\n        x: 100,\n        duration: 1000,\n        loop: 2,\n        loopDelay: 500,\n      })\n\n      timeline.play()\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n\n      engine.update(250)\n      expect(target.x).toBe(100)\n\n      engine.update(250)\n      engine.update(500)\n      expect(target.x).toBe(50)\n    })\n  })\n\n  describe(\"Alternating Animations\", () => {\n    it(\"should alternate direction with each loop\", () => {\n      timeline = createTimeline({ duration: 5000, autoplay: false })\n\n      const values: number[] = []\n      timeline.add(target, {\n        x: 100,\n        duration: 1000,\n        loop: 3,\n        alternate: true,\n        onUpdate: (anim: JSAnimation) => {\n          values.push(anim.targets[0].x)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(500)\n      expect(target.x).toBe(50)\n      engine.update(500)\n      expect(target.x).toBe(100)\n\n      engine.update(500)\n      expect(target.x).toBe(50)\n      engine.update(500)\n      expect(target.x).toBe(0)\n\n      engine.update(500)\n      expect(target.x).toBe(50)\n      engine.update(500)\n      expect(target.x).toBe(100)\n    })\n\n    it(\"should handle alternating with loop delay\", () => {\n      timeline = createTimeline({ duration: 5000, autoplay: false })\n\n      timeline.add(target, {\n        x: 100,\n        duration: 1000,\n        loop: 2,\n        alternate: true,\n        loopDelay: 500,\n      })\n\n      timeline.play()\n\n      engine.update(1000)\n      expect(target.x).toBe(100)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n\n      engine.update(500)\n      expect(target.x).toBe(50)\n      engine.update(500)\n      expect(target.x).toBe(0)\n    })\n\n    it(\"should handle alternating animations with looping parent timeline\", () => {\n      timeline = createTimeline({ duration: 3000, loop: true, autoplay: false })\n\n      const animationValues: { time: number; value: number; loop: number }[] = []\n      let mainTimelineLoops = 0\n\n      timeline.add(\n        target,\n        {\n          x: 100,\n          duration: 1000,\n          loop: 2,\n          alternate: true,\n          onUpdate: (anim: JSAnimation) => {\n            animationValues.push({\n              time: timeline.currentTime,\n              value: anim.targets[0].x,\n              loop: mainTimelineLoops,\n            })\n          },\n        },\n        500,\n      )\n\n      timeline.play()\n\n      engine.update(500)\n      const firstLoopStartValue = target.x\n      engine.update(500)\n      engine.update(500)\n      engine.update(500)\n      engine.update(500)\n      engine.update(500)\n\n      mainTimelineLoops++\n\n      const secondLoopTime = timeline.currentTime\n      engine.update(500)\n      const secondLoopStartValue = target.x\n\n      expect(secondLoopTime).toBe(0)\n      expect(secondLoopStartValue).toBe(firstLoopStartValue)\n    })\n  })\n\n  describe(\"Timeline Sync\", () => {\n    it(\"should sync sub-timelines to main timeline\", () => {\n      const mainTimeline = createTimeline({ duration: 3000, autoplay: false })\n      const subTimeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const subTarget = { value: 0 }\n      subTimeline.add(subTarget, { value: 100, duration: 1000 })\n\n      mainTimeline.sync(subTimeline, 1000)\n      mainTimeline.play()\n\n      engine.update(500)\n      expect(subTarget.value).toBe(0)\n\n      engine.update(500)\n      expect(subTarget.value).toBe(0)\n\n      engine.update(500)\n      expect(subTarget.value).toBe(50)\n\n      engine.update(500)\n      expect(subTarget.value).toBe(100)\n\n      engine.update(500)\n      expect(subTarget.value).toBe(100)\n    })\n\n    it(\"should restart completed sub-timelines when main timeline loops\", () => {\n      const mainTimeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n      const subTimeline = createTimeline({ duration: 300, autoplay: false })\n\n      const subTarget = { value: 0 }\n      let subCompleteCount = 0\n\n      subTimeline.add(subTarget, {\n        value: 100,\n        duration: 300,\n        onComplete: () => subCompleteCount++,\n      })\n\n      mainTimeline.sync(subTimeline, 200)\n      mainTimeline.play()\n\n      engine.update(200)\n      expect(subTarget.value).toBe(0)\n\n      engine.update(150)\n      expect(subTarget.value).toBe(50)\n\n      engine.update(150)\n      expect(subTarget.value).toBe(100)\n      expect(subCompleteCount).toBe(1)\n      expect(subTimeline.isPlaying).toBe(false)\n\n      engine.update(500)\n\n      expect(mainTimeline.currentTime).toBe(0)\n      expect(subTarget.value).toBe(100)\n      expect(subTimeline.isPlaying).toBe(false)\n\n      engine.update(200)\n      expect(subTimeline.isPlaying).toBe(true)\n\n      engine.update(150)\n      expect(subTarget.value).toBe(50)\n\n      engine.update(150)\n      expect(subTarget.value).toBe(100)\n      expect(subCompleteCount).toBe(2)\n    })\n\n    it(\"should preserve initial values for looping sub-timeline when main timeline does not loop\", () => {\n      const mainTimeline = createTimeline({ duration: 5000, loop: false, autoplay: false })\n      const subTimeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n\n      const subTarget = { x: 10, y: 20 }\n      const capturedStates: Array<{ x: number; y: number; time: number; loop: number }> = []\n      let subLoopCount = 0\n\n      subTarget.x = 50\n      subTarget.y = 80\n\n      subTimeline.add(subTarget, {\n        x: 200,\n        y: 300,\n        duration: 1000,\n        onUpdate: (anim: JSAnimation) => {\n          capturedStates.push({\n            x: anim.targets[0].x,\n            y: anim.targets[0].y,\n            time: mainTimeline.currentTime,\n            loop: subLoopCount,\n          })\n        },\n        onComplete: () => {\n          subLoopCount++\n        },\n      })\n\n      mainTimeline.sync(subTimeline, 1500)\n      mainTimeline.play()\n\n      engine.update(1000)\n      expect(subTarget.x).toBe(50)\n      expect(subTarget.y).toBe(80)\n      expect(capturedStates).toHaveLength(0)\n\n      engine.update(750)\n      expect(capturedStates.length).toBeGreaterThan(0)\n\n      const firstLoopMidpoint = capturedStates.find((state) => state.loop === 0)\n      expect(firstLoopMidpoint).toBeDefined()\n      expect(firstLoopMidpoint!.x).toBeGreaterThan(50)\n      expect(firstLoopMidpoint!.x).toBeLessThan(200)\n      expect(firstLoopMidpoint!.y).toBeGreaterThan(80)\n      expect(firstLoopMidpoint!.y).toBeLessThan(300)\n\n      engine.update(750)\n      expect(subTarget.x).toBe(200)\n      expect(subTarget.y).toBe(300)\n      expect(subLoopCount).toBe(1)\n\n      engine.update(500)\n\n      const secondLoopMidpoint = capturedStates.find((state) => state.loop === 1 && state.time >= 2500)\n      expect(secondLoopMidpoint).toBeDefined()\n\n      expect(secondLoopMidpoint!.x).toBeGreaterThan(50)\n      expect(secondLoopMidpoint!.x).toBeLessThan(200)\n      expect(secondLoopMidpoint!.y).toBeGreaterThan(80)\n      expect(secondLoopMidpoint!.y).toBeLessThan(300)\n\n      engine.update(500)\n      expect(subTarget.x).toBe(200)\n      expect(subTarget.y).toBe(300)\n      expect(subLoopCount).toBe(2)\n\n      engine.update(500)\n\n      const thirdLoopMidpoint = capturedStates.find((state) => state.loop === 2 && state.time >= 3500)\n      expect(thirdLoopMidpoint).toBeDefined()\n\n      expect(thirdLoopMidpoint!.x).toBeGreaterThan(50)\n      expect(thirdLoopMidpoint!.x).toBeLessThan(200)\n      expect(thirdLoopMidpoint!.y).toBeGreaterThan(80)\n      expect(thirdLoopMidpoint!.y).toBeLessThan(300)\n\n      engine.update(1000)\n      expect(mainTimeline.isPlaying).toBe(false)\n      expect(subLoopCount).toBeGreaterThanOrEqual(2)\n    })\n\n    it(\"should pause sub-timelines when main timeline is paused\", () => {\n      const mainTimeline = createTimeline({ duration: 3000, autoplay: false })\n      const subTimeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const mainTarget = { x: 0 }\n      const subTarget = { value: 0 }\n\n      mainTimeline.add(mainTarget, { x: 100, duration: 2000 })\n      subTimeline.add(subTarget, { value: 50, duration: 800 })\n\n      mainTimeline.sync(subTimeline, 500)\n      mainTimeline.play()\n\n      engine.update(250)\n      expect(mainTarget.x).toBe(12.5)\n      expect(subTarget.value).toBe(0)\n      expect(mainTimeline.isPlaying).toBe(true)\n      expect(subTimeline.isPlaying).toBe(false)\n\n      engine.update(500)\n      expect(mainTarget.x).toBe(37.5)\n      expect(subTarget.value).toBe(15.625)\n      expect(mainTimeline.isPlaying).toBe(true)\n      expect(subTimeline.isPlaying).toBe(true)\n\n      mainTimeline.pause()\n      expect(mainTimeline.isPlaying).toBe(false)\n      expect(subTimeline.isPlaying).toBe(false)\n\n      engine.update(400)\n      expect(mainTarget.x).toBe(37.5)\n      expect(subTarget.value).toBe(15.625)\n      expect(subTimeline.isPlaying).toBe(false)\n\n      mainTimeline.play()\n      expect(mainTimeline.isPlaying).toBe(true)\n      expect(subTimeline.isPlaying).toBe(true)\n\n      engine.update(200)\n      expect(mainTarget.x).toBe(47.5)\n      expect(subTarget.value).toBe(28.125)\n      expect(subTimeline.isPlaying).toBe(true)\n    })\n  })\n\n  describe(\"Callbacks\", () => {\n    it(\"should execute call callbacks at specified times\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      const callTimes: number[] = []\n      timeline.call(() => callTimes.push(0), 0)\n      timeline.call(() => callTimes.push(1000), 1000)\n      timeline.call(() => callTimes.push(1500), 1500)\n\n      timeline.play()\n\n      engine.update(500)\n      expect(callTimes).toEqual([0])\n\n      engine.update(500)\n      expect(callTimes).toEqual([0, 1000])\n\n      engine.update(500)\n      expect(callTimes).toEqual([0, 1000, 1500])\n    })\n\n    it(\"should support string startTime parameters\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const callTimes: string[] = []\n      timeline.call(() => callTimes.push(\"start\"), \"start\")\n      timeline.add(target, { x: 100, duration: 1000 }, \"start\")\n\n      timeline.play()\n      engine.update(500)\n\n      expect(callTimes).toEqual([\"start\"])\n      expect(target.x).toBe(50)\n    })\n\n    it(\"should trigger onStart callback correctly\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n      let started = false\n      timeline.add(\n        target,\n        {\n          x: 100,\n          duration: 500,\n          onStart: () => {\n            started = true\n          },\n        },\n        200,\n      )\n\n      timeline.play()\n      expect(started).toBe(false)\n\n      engine.update(100)\n      expect(started).toBe(false)\n      expect(target.x).toBe(0)\n\n      engine.update(150)\n      expect(started).toBe(true)\n      expect(target.x).toBe(10)\n    })\n\n    it(\"should trigger onLoop callback correctly for individual animation loops\", () => {\n      timeline = createTimeline({ duration: 5000, autoplay: false })\n      let loopCount = 0\n      let completeCount = 0\n      timeline.add(target, {\n        x: 100,\n        duration: 500,\n        loop: 3,\n        loopDelay: 100,\n        onLoop: () => {\n          loopCount++\n        },\n        onComplete: () => {\n          completeCount++\n        },\n      })\n\n      timeline.play()\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(loopCount).toBe(0)\n      engine.update(100)\n      expect(loopCount).toBe(1)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(loopCount).toBe(1)\n      engine.update(100)\n      expect(loopCount).toBe(2)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(loopCount).toBe(2)\n      expect(completeCount).toBe(1)\n    })\n  })\n\n  describe(\"Complex Looping Scenarios\", () => {\n    it(\"should correctly reset and re-run finite-looped animation when parent timeline loops\", () => {\n      timeline = createTimeline({ duration: 2000, loop: true, autoplay: false })\n\n      let animLoopCount = 0\n      let animCompleteCount = 0\n      let animStartCount = 0\n\n      timeline.add(\n        target,\n        {\n          x: 100,\n          duration: 500,\n          loop: 2,\n          loopDelay: 100,\n          onStart: () => animStartCount++,\n          onLoop: () => animLoopCount++,\n          onComplete: () => animCompleteCount++,\n        },\n        500,\n      )\n\n      timeline.play()\n\n      engine.update(500)\n      expect(animStartCount).toBe(1)\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(animLoopCount).toBe(0)\n      engine.update(100)\n      expect(animLoopCount).toBe(1)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(animLoopCount).toBe(1)\n      expect(animCompleteCount).toBe(1)\n      engine.update(100)\n      expect(animLoopCount).toBe(1)\n      expect(animCompleteCount).toBe(1)\n\n      engine.update(300)\n      expect(target.x).toBe(100)\n      expect(animCompleteCount).toBe(1)\n\n      expect(timeline.currentTime).toBe(0)\n\n      engine.update(500)\n      expect(animStartCount).toBe(2)\n      expect(target.x).toBe(0)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(animLoopCount).toBe(1)\n      engine.update(100)\n      expect(animLoopCount).toBe(2)\n\n      engine.update(500)\n      expect(target.x).toBe(100)\n      expect(animLoopCount).toBe(2)\n      expect(animCompleteCount).toBe(2)\n    })\n  })\n\n  describe(\"Timing Precision\", () => {\n    describe(\"Animation Start Time Overshoot\", () => {\n      it(\"should account for overshoot when animation starts late\", () => {\n        timeline = createTimeline({ duration: 2000, autoplay: false })\n\n        timeline.add(\n          target,\n          {\n            x: 100,\n            duration: 1000,\n            ease: \"linear\",\n          },\n          50,\n        )\n\n        timeline.play()\n\n        engine.update(66)\n        expect(target.x).toBeCloseTo(1.6, 1)\n      })\n\n      it(\"should handle multiple animations with different start time overshoots\", () => {\n        timeline = createTimeline({ duration: 3000, autoplay: false })\n\n        const target1 = { x: 0 }\n        const target2 = { y: 0 }\n\n        timeline.add(target1, { x: 100, duration: 1000, ease: \"linear\" }, 30)\n        timeline.add(target2, { y: 200, duration: 1000, ease: \"linear\" }, 80)\n\n        timeline.play()\n        engine.update(100)\n\n        expect(target1.x).toBeCloseTo(7, 1)\n        expect(target2.y).toBeCloseTo(4, 1)\n      })\n\n      it(\"should handle zero duration animations with overshoot\", () => {\n        timeline = createTimeline({ duration: 1000, autoplay: false })\n\n        timeline.add(target, { x: 100, duration: 0 }, 50)\n\n        timeline.play()\n        engine.update(66)\n\n        expect(target.x).toBe(100)\n      })\n    })\n\n    describe(\"Loop Delay Precision\", () => {\n      it(\"should account for overshoot in loop delays\", () => {\n        timeline = createTimeline({ duration: 5000, autoplay: false })\n\n        const values: number[] = []\n        timeline.add(target, {\n          x: 100,\n          duration: 1000,\n          loop: 3,\n          loopDelay: 500,\n          ease: \"linear\",\n          onUpdate: (anim: JSAnimation) => values.push(anim.targets[0].x),\n        })\n\n        timeline.play()\n\n        engine.update(1000)\n        expect(target.x).toBe(100)\n\n        engine.update(516)\n        expect(target.x).toBeCloseTo(1.6, 1)\n      })\n\n      it(\"should handle multiple loop delay overshoots\", () => {\n        timeline = createTimeline({ duration: 10000, autoplay: false })\n\n        timeline.add(target, {\n          x: 100,\n          duration: 1000,\n          loop: 4,\n          loopDelay: 300,\n          ease: \"linear\",\n        })\n\n        timeline.play()\n\n        engine.update(1000)\n        expect(target.x).toBe(100)\n\n        engine.update(333)\n        expect(target.x).toBeCloseTo(3.3, 1)\n\n        engine.update(967)\n        expect(target.x).toBe(100)\n\n        engine.update(350)\n        expect(target.x).toBeCloseTo(5, 1)\n      })\n\n      it(\"should handle alternating animations with loop delay overshoot\", () => {\n        timeline = createTimeline({ duration: 8000, autoplay: false })\n\n        timeline.add(target, {\n          x: 100,\n          duration: 1000,\n          loop: 3,\n          alternate: true,\n          loopDelay: 400,\n          ease: \"linear\",\n        })\n\n        timeline.play()\n\n        engine.update(1000)\n        expect(target.x).toBe(100)\n\n        engine.update(450)\n        expect(target.x).toBe(95)\n\n        engine.update(950)\n        expect(target.x).toBe(0)\n\n        engine.update(425)\n        expect(target.x).toBe(2.5)\n      })\n    })\n\n    describe(\"Synced Timeline Precision\", () => {\n      it(\"should account for overshoot when starting synced timelines\", () => {\n        const mainTimeline = createTimeline({ duration: 3000, autoplay: false })\n        const subTimeline = createTimeline({ duration: 1000, autoplay: false })\n\n        const subTarget = { value: 0 }\n        subTimeline.add(subTarget, { value: 100, duration: 1000, ease: \"linear\" })\n\n        mainTimeline.sync(subTimeline, 500)\n        mainTimeline.play()\n\n        engine.update(533)\n        expect(subTarget.value).toBeCloseTo(3.3, 1)\n      })\n\n      it(\"should handle multiple synced timelines with different overshoot amounts\", () => {\n        const mainTimeline = createTimeline({ duration: 5000, autoplay: false })\n        const subTimeline1 = createTimeline({ duration: 1000, autoplay: false })\n        const subTimeline2 = createTimeline({ duration: 1500, autoplay: false })\n\n        const subTarget1 = { value: 0 }\n        const subTarget2 = { value: 0 }\n\n        subTimeline1.add(subTarget1, { value: 100, duration: 1000, ease: \"linear\" })\n        subTimeline2.add(subTarget2, { value: 200, duration: 1500, ease: \"linear\" })\n\n        mainTimeline.sync(subTimeline1, 300)\n        mainTimeline.sync(subTimeline2, 800)\n        mainTimeline.play()\n\n        engine.update(850)\n\n        expect(subTarget1.value).toBeCloseTo(55, 1)\n        expect(subTarget2.value).toBeCloseTo(6.67, 1)\n      })\n    })\n\n    describe(\"Complex Precision Scenarios\", () => {\n      it(\"should handle alternating animation with main timeline loop and overshoot\", () => {\n        timeline = createTimeline({ duration: 3000, loop: true, autoplay: false })\n\n        timeline.add(\n          target,\n          {\n            x: 100,\n            duration: 800,\n            loop: 2,\n            alternate: true,\n            loopDelay: 200,\n            ease: \"linear\",\n          },\n          500,\n        )\n\n        timeline.play()\n\n        engine.update(3100)\n\n        expect(target.x).toBe(0)\n\n        engine.update(450)\n        expect(target.x).toBe(6.25)\n\n        engine.update(750 + 250)\n        expect(target.x).toBe(93.75)\n      })\n\n      it(\"should maintain precision across multiple frame updates at 30fps\", () => {\n        timeline = createTimeline({ duration: 2000, autoplay: false })\n\n        const frameTime = 33.33\n        const values: number[] = []\n\n        timeline.add(\n          target,\n          {\n            x: 100,\n            duration: 1000,\n            ease: \"linear\",\n            onUpdate: (anim: JSAnimation) => values.push(anim.targets[0].x),\n          },\n          50,\n        )\n\n        timeline.play()\n\n        engine.update(frameTime)\n        expect(target.x).toBe(0)\n\n        engine.update(frameTime)\n        expect(target.x).toBeCloseTo(1.67, 1)\n\n        engine.update(frameTime)\n        expect(target.x).toBeCloseTo(5, 1)\n\n        for (let i = 0; i < 29; i++) {\n          engine.update(frameTime)\n        }\n\n        expect(target.x).toBeCloseTo(100, 0)\n      })\n    })\n  })\n\n  describe(\"Edge Cases\", () => {\n    it(\"should handle zero duration\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n      timeline.add(target, { x: 100, duration: 0 })\n\n      timeline.play()\n      engine.update(1)\n\n      expect(target.x).toBe(100)\n    })\n\n    it(\"should handle negative deltaTime gracefully\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n      timeline.add(target, { x: 100, duration: 1000 })\n\n      timeline.play()\n      engine.update(-100)\n\n      expect(target.x).toBe(0)\n    })\n\n    it(\"should handle very large deltaTime\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n      timeline.add(target, { x: 100, duration: 1000 })\n\n      timeline.play()\n      engine.update(10000)\n\n      expect(target.x).toBe(100)\n    })\n  })\n\n  describe(\"New Easing Function Tests\", () => {\n    const testCases: { name: EasingFunctions; midValue: number }[] = [\n      { name: \"inCirc\", midValue: 0.13397459621556135 },\n      { name: \"outCirc\", midValue: 0.8660254037844386 },\n      { name: \"inOutCirc\", midValue: 0.5 },\n      { name: \"inBack\", midValue: -0.0876975 },\n      { name: \"outBack\", midValue: 1.0876975 },\n      { name: \"inOutBack\", midValue: 0.5 },\n    ]\n\n    testCases.forEach((tc) => {\n      it(`should animate correctly with ${tc.name} easing`, () => {\n        timeline = createTimeline({ duration: 1000, autoplay: false })\n        timeline.add(target, { x: 100, duration: 1000, ease: tc.name })\n        timeline.play()\n\n        engine.update(0)\n        expect(target.x).toBeCloseTo(0, 5)\n\n        engine.update(500)\n        if (tc.name === \"inBack\") {\n          expect(target.x).toBeCloseTo(100 * tc.midValue, 5)\n        } else if (tc.name === \"outBack\") {\n          expect(target.x).toBeCloseTo(100 * tc.midValue, 5)\n        } else if (tc.name === \"inOutCirc\" || tc.name === \"inOutBack\") {\n          expect(target.x).toBeCloseTo(50, 5)\n        } else {\n          expect(target.x).toBeCloseTo(100 * tc.midValue, 5)\n        }\n\n        engine.update(500)\n        expect(target.x).toBeCloseTo(100, 5)\n      })\n    })\n  })\n\n  describe(\"DeltaTime in onUpdate Callbacks\", () => {\n    it(\"should provide correct deltaTime to onUpdate callbacks\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const deltaTimesReceived: number[] = []\n      timeline.add(target, {\n        x: 100,\n        duration: 1000,\n        onUpdate: (anim: JSAnimation) => {\n          deltaTimesReceived.push(anim.deltaTime)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(16)\n      expect(deltaTimesReceived[0]).toBe(16)\n\n      engine.update(33)\n      expect(deltaTimesReceived[1]).toBe(33)\n\n      engine.update(50)\n      expect(deltaTimesReceived[2]).toBe(50)\n    })\n\n    it(\"should support throttling patterns like the vignette example\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      let vignetteTime = 0\n      let vignetteUpdateCount = 0\n      const vignetteStrengthValues: number[] = []\n\n      timeline.add(target, {\n        strength: 1.0,\n        duration: 1000,\n        onUpdate: (values: JSAnimation) => {\n          vignetteTime += values.deltaTime\n          if (vignetteTime > 66) {\n            vignetteStrengthValues.push(values.targets[0].strength)\n            vignetteUpdateCount++\n            vignetteTime = 0\n          }\n        },\n      })\n\n      timeline.play()\n\n      for (let i = 0; i < 10; i++) {\n        engine.update(16.67)\n      }\n\n      expect(vignetteUpdateCount).toBeGreaterThan(0)\n      expect(vignetteUpdateCount).toBeLessThan(10)\n      expect(vignetteStrengthValues.length).toBe(vignetteUpdateCount)\n    })\n\n    it(\"should provide deltaTime across multiple animation loops\", () => {\n      timeline = createTimeline({ duration: 5000, autoplay: false })\n\n      const deltaTimesReceived: number[] = []\n      timeline.add(target, {\n        x: 100,\n        duration: 500,\n        loop: 3,\n        loopDelay: 100,\n        onUpdate: (anim: JSAnimation) => {\n          deltaTimesReceived.push(anim.deltaTime)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(25)\n      engine.update(30)\n      engine.update(445)\n      engine.update(35)\n      engine.update(65)\n      engine.update(40)\n\n      expect(deltaTimesReceived).toEqual([25, 30, 445, 35, 65, 40])\n    })\n\n    it(\"should provide deltaTime to synced sub-timeline animations\", () => {\n      const mainTimeline = createTimeline({ duration: 2000, autoplay: false })\n      const subTimeline = createTimeline({ duration: 500, autoplay: false })\n\n      const mainDeltaTimes: number[] = []\n      const subDeltaTimes: number[] = []\n\n      const subTarget = { value: 0 }\n\n      mainTimeline.add(target, {\n        x: 100,\n        duration: 1000,\n        onUpdate: (anim: JSAnimation) => {\n          mainDeltaTimes.push(anim.deltaTime)\n        },\n      })\n\n      subTimeline.add(subTarget, {\n        value: 50,\n        duration: 500,\n        onUpdate: (anim: JSAnimation) => {\n          subDeltaTimes.push(anim.deltaTime)\n        },\n      })\n\n      mainTimeline.sync(subTimeline, 300)\n      mainTimeline.play()\n\n      engine.update(200)\n      expect(mainDeltaTimes).toEqual([200])\n      expect(subDeltaTimes).toEqual([])\n\n      engine.update(150)\n      expect(mainDeltaTimes).toEqual([200, 150])\n      expect(subDeltaTimes).toEqual([50])\n\n      engine.update(100)\n      expect(mainDeltaTimes).toEqual([200, 150, 100])\n      expect(subDeltaTimes).toEqual([50, 100])\n    })\n\n    it(\"should handle deltaTime correctly when animation starts mid-frame\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const deltaTimesReceived: number[] = []\n      timeline.add(\n        target,\n        {\n          x: 100,\n          duration: 500,\n          onUpdate: (anim: JSAnimation) => {\n            deltaTimesReceived.push(anim.deltaTime)\n          },\n        },\n        250,\n      )\n\n      timeline.play()\n\n      engine.update(200)\n      expect(deltaTimesReceived).toEqual([])\n\n      engine.update(100)\n      expect(deltaTimesReceived).toEqual([100])\n\n      engine.update(150)\n      expect(deltaTimesReceived).toEqual([100, 150])\n    })\n\n    it(\"should provide correct deltaTime for zero duration animations\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const deltaTimesReceived: number[] = []\n      timeline.add(target, {\n        x: 100,\n        duration: 0,\n        onUpdate: (anim: JSAnimation) => {\n          deltaTimesReceived.push(anim.deltaTime)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(50)\n      expect(deltaTimesReceived).toEqual([50])\n      expect(target.x).toBe(100)\n\n      engine.update(25)\n      expect(deltaTimesReceived).toEqual([50])\n    })\n\n    it(\"should provide consistent deltaTime during alternating animations\", () => {\n      timeline = createTimeline({ duration: 3000, autoplay: false })\n\n      const deltaTimesReceived: number[] = []\n      const progressValues: number[] = []\n\n      timeline.add(target, {\n        x: 100,\n        duration: 500,\n        loop: 2,\n        alternate: true,\n        onUpdate: (anim: JSAnimation) => {\n          deltaTimesReceived.push(anim.deltaTime)\n          progressValues.push(anim.progress)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(250)\n      engine.update(250)\n\n      engine.update(125)\n      engine.update(375)\n\n      expect(deltaTimesReceived).toEqual([250, 250, 125, 375])\n\n      expect(progressValues[0]).toBe(0.5)\n      expect(progressValues[1]).toBe(1)\n      expect(progressValues[2]).toBe(0.25)\n      expect(progressValues[3]).toBe(1)\n    })\n  })\n\n  describe(\"onUpdate Callback Frequency and Correctness\", () => {\n    it(\"should provide correct progress values in onUpdate callbacks\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const progressValues: number[] = []\n      const targetValues: number[] = []\n\n      timeline.add(target, {\n        x: 100,\n        duration: 1000,\n        ease: \"linear\",\n        onUpdate: (anim: JSAnimation) => {\n          progressValues.push(anim.progress)\n          targetValues.push(anim.targets[0].x)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(0)\n      engine.update(250)\n      engine.update(250)\n      engine.update(250)\n      engine.update(250)\n\n      expect(progressValues).toEqual([0, 0.25, 0.5, 0.75, 1])\n      expect(targetValues).toEqual([0, 25, 50, 75, 100])\n    })\n\n    it(\"should call onUpdate for each animation in a looping scenario without duplicates\", () => {\n      timeline = createTimeline({ duration: 3000, autoplay: false })\n\n      let updateCount = 0\n      const progressHistory: number[] = []\n\n      timeline.add(target, {\n        x: 100,\n        duration: 500,\n        loop: 3,\n        onUpdate: (anim: JSAnimation) => {\n          updateCount++\n          progressHistory.push(anim.progress)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(250)\n      engine.update(250)\n\n      engine.update(250)\n      engine.update(250)\n\n      engine.update(250)\n      engine.update(250)\n\n      expect(updateCount).toBe(6)\n      expect(progressHistory).toEqual([0.5, 1, 0.5, 1, 0.5, 1])\n    })\n\n    it(\"should call onUpdate correctly for alternating animations\", () => {\n      timeline = createTimeline({ duration: 3000, autoplay: false })\n\n      let updateCount = 0\n      const targetValueHistory: number[] = []\n      const progressHistory: number[] = []\n\n      timeline.add(target, {\n        x: 100,\n        duration: 500,\n        loop: 3,\n        alternate: true,\n        onUpdate: (anim: JSAnimation) => {\n          updateCount++\n          targetValueHistory.push(anim.targets[0].x)\n          progressHistory.push(anim.progress)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(250)\n      engine.update(250)\n\n      engine.update(250)\n      engine.update(250)\n\n      engine.update(250)\n      engine.update(250)\n\n      expect(updateCount).toBe(6)\n      expect(targetValueHistory).toEqual([50, 100, 50, 0, 50, 100])\n      expect(progressHistory).toEqual([0.5, 1, 0.5, 1, 0.5, 1])\n    })\n\n    it(\"should provide correct deltaTime and timing information in onUpdate\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      const deltaTimeHistory: number[] = []\n      const currentTimeHistory: number[] = []\n\n      timeline.add(\n        target,\n        {\n          x: 100,\n          duration: 1000,\n          onUpdate: (anim: JSAnimation) => {\n            deltaTimeHistory.push(anim.deltaTime)\n            currentTimeHistory.push(anim.currentTime)\n          },\n        },\n        300,\n      )\n\n      timeline.play()\n\n      engine.update(200)\n      expect(deltaTimeHistory).toEqual([])\n\n      engine.update(150)\n      engine.update(200)\n      engine.update(300)\n      engine.update(450)\n\n      expect(deltaTimeHistory).toEqual([150, 200, 300, 450])\n      expect(currentTimeHistory).toEqual([350, 550, 850, 1300])\n    })\n\n    it(\"should not call onUpdate multiple times for zero duration animations\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      let updateCount = 0\n      const receivedValues: JSAnimation[] = []\n\n      timeline.add(target, {\n        x: 100,\n        duration: 0,\n        onUpdate: (anim: JSAnimation) => {\n          updateCount++\n          receivedValues.push(anim)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(50)\n      engine.update(100)\n      engine.update(200)\n\n      expect(updateCount).toBe(1)\n      expect(receivedValues[0].progress).toBe(1)\n      expect(receivedValues[0].targets[0].x).toBe(100)\n    })\n\n    it(\"should not call onUpdate after animation completes\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      let updateCallCount = 0\n      let completeCallCount = 0\n      const updateTimes: number[] = []\n\n      timeline.add(target, {\n        x: 100,\n        duration: 500,\n        onUpdate: (anim: JSAnimation) => {\n          updateCallCount++\n          updateTimes.push(timeline.currentTime)\n        },\n        onComplete: () => {\n          completeCallCount++\n        },\n      })\n\n      timeline.play()\n\n      engine.update(250)\n      expect(updateCallCount).toBe(1)\n      expect(completeCallCount).toBe(0)\n      expect(target.x).toBe(50)\n\n      engine.update(250)\n      expect(updateCallCount).toBe(2)\n      expect(completeCallCount).toBe(1)\n      expect(target.x).toBe(100)\n\n      engine.update(300)\n      engine.update(400)\n      engine.update(500)\n\n      expect(updateCallCount).toBe(2)\n      expect(completeCallCount).toBe(1)\n      expect(target.x).toBe(100)\n\n      expect(updateTimes).toEqual([250, 500])\n    })\n\n    it(\"should call onUpdate for multiple targets on same animation correctly\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const target1 = { x: 0, y: 0 }\n      const target2 = { x: 0, y: 0 }\n      let updateCount = 0\n      const allTargetsHistory: Array<{ x: number; y: number }[]> = []\n\n      timeline.add([target1, target2], {\n        x: 100,\n        y: 200,\n        duration: 1000,\n        onUpdate: (anim: JSAnimation) => {\n          updateCount++\n          allTargetsHistory.push(anim.targets.map((target) => ({ x: target.x, y: target.y })))\n        },\n      })\n\n      timeline.play()\n\n      engine.update(500)\n      engine.update(500)\n\n      expect(updateCount).toBe(2)\n\n      expect(allTargetsHistory[0]).toEqual([\n        { x: 50, y: 100 },\n        { x: 50, y: 100 },\n      ])\n\n      expect(allTargetsHistory[1]).toEqual([\n        { x: 100, y: 200 },\n        { x: 100, y: 200 },\n      ])\n    })\n  })\n\n  describe(\"Target Value Persistence Bug\", () => {\n    it(\"should not reset target values to initial values when animation hasnt started\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const testTarget = { x: 50, strength: 1.5 }\n\n      testTarget.x = 75\n      testTarget.strength = 2.0\n\n      timeline.add(\n        testTarget,\n        {\n          x: 100,\n          duration: 300,\n        },\n        500,\n      )\n\n      timeline.play()\n\n      engine.update(100)\n\n      expect(testTarget.x).toBe(75)\n      expect(testTarget.strength).toBe(2.0)\n\n      engine.update(200)\n      expect(testTarget.x).toBe(75)\n      expect(testTarget.strength).toBe(2.0)\n\n      engine.update(300)\n      expect(testTarget.x).toBeCloseTo(83.33, 2)\n      expect(testTarget.strength).toBe(2.0)\n    })\n\n    it(\"should not reset target values to initial values after onUpdate\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const testTarget = { x: 0, y: 50 }\n      let onUpdateCallCount = 0\n      let capturedValues: Array<{ x: number; y: number }> = []\n\n      timeline.add(testTarget, {\n        x: 100,\n        duration: 500,\n        onUpdate: (anim: JSAnimation) => {\n          onUpdateCallCount++\n          capturedValues.push({ x: testTarget.x, y: testTarget.y })\n        },\n      })\n\n      timeline.play()\n\n      engine.update(250)\n      expect(onUpdateCallCount).toBe(1)\n      expect(testTarget.x).toBe(50)\n      expect(testTarget.y).toBe(50)\n\n      engine.update(250)\n      expect(onUpdateCallCount).toBe(2)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n\n      engine.update(100)\n      engine.update(100)\n      engine.update(100)\n\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(onUpdateCallCount).toBe(2)\n\n      expect(capturedValues[0]).toEqual({ x: 50, y: 50 })\n      expect(capturedValues[1]).toEqual({ x: 100, y: 50 })\n    })\n\n    it(\"should preserve final values across timeline loops\", () => {\n      timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n\n      const testTarget = { value: 0 }\n      let updateCallCount = 0\n\n      timeline.add(testTarget, {\n        value: 100,\n        duration: 600,\n        onUpdate: () => updateCallCount++,\n      })\n\n      timeline.play()\n\n      engine.update(600)\n      expect(testTarget.value).toBe(100)\n      expect(updateCallCount).toBe(1)\n\n      engine.update(400)\n\n      expect(testTarget.value).toBe(100)\n      expect(updateCallCount).toBe(1)\n\n      engine.update(300)\n      expect(testTarget.value).toBe(50)\n      expect(updateCallCount).toBe(2)\n    })\n\n    it(\"should preserve original initial values across timeline loops\", () => {\n      timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n\n      const testTarget = { value: 0 }\n      let updateCallCount = 0\n\n      timeline.add(testTarget, {\n        value: 100,\n        duration: 600,\n        onUpdate: () => updateCallCount++,\n      })\n\n      timeline.play()\n\n      engine.update(600)\n      expect(testTarget.value).toBe(100)\n      expect(updateCallCount).toBe(1)\n\n      engine.update(400)\n\n      expect(testTarget.value).toBe(100)\n      expect(updateCallCount).toBe(1)\n\n      engine.update(300)\n      expect(testTarget.value).toBe(50)\n      expect(updateCallCount).toBe(2)\n    })\n  })\n\n  describe(\"Multiple Animations on Same Object\", () => {\n    it(\"should handle multiple animations on the same object\", () => {\n      timeline = createTimeline({ duration: 5000, autoplay: false })\n\n      const testTarget = { x: 0 }\n\n      timeline.add(\n        testTarget,\n        {\n          x: 100,\n          duration: 100,\n        },\n        0,\n      )\n\n      timeline.add(\n        testTarget,\n        {\n          x: 50,\n          duration: 100,\n        },\n        200,\n      )\n\n      timeline.play()\n\n      expect(testTarget.x).toBe(0)\n\n      engine.update(50)\n      expect(testTarget.x).toBe(50)\n\n      engine.update(50)\n      expect(testTarget.x).toBe(100)\n\n      engine.update(50)\n      expect(testTarget.x).toBe(100)\n\n      engine.update(100)\n      expect(testTarget.x).toBe(75)\n\n      engine.update(50)\n      expect(testTarget.x).toBe(50)\n    })\n\n    it(\"should handle multiple sequential animations on the same object\", () => {\n      timeline = createTimeline({ duration: 5000, autoplay: false })\n\n      const testTarget = { x: 0, y: 0, z: 0 }\n      const animationStates: Array<{ time: number; x: number; y: number; z: number }> = []\n\n      timeline.add(\n        testTarget,\n        {\n          x: 100,\n          duration: 1000,\n          onUpdate: () =>\n            animationStates.push({ time: timeline.currentTime, x: testTarget.x, y: testTarget.y, z: testTarget.z }),\n        },\n        0,\n      )\n\n      timeline.add(\n        testTarget,\n        {\n          y: 50,\n          duration: 500,\n          onUpdate: () =>\n            animationStates.push({ time: timeline.currentTime, x: testTarget.x, y: testTarget.y, z: testTarget.z }),\n        },\n        1500,\n      )\n\n      timeline.add(\n        testTarget,\n        {\n          z: 200,\n          duration: 1000,\n          onUpdate: () =>\n            animationStates.push({ time: timeline.currentTime, x: testTarget.x, y: testTarget.y, z: testTarget.z }),\n        },\n        3000,\n      )\n\n      timeline.play()\n\n      engine.update(0)\n      expect(testTarget.x).toBe(0)\n      expect(testTarget.y).toBe(0)\n      expect(testTarget.z).toBe(0)\n\n      engine.update(500)\n      expect(testTarget.x).toBe(50)\n      expect(testTarget.y).toBe(0)\n      expect(testTarget.z).toBe(0)\n\n      engine.update(500)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(0)\n      expect(testTarget.z).toBe(0)\n\n      engine.update(250)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(0)\n      expect(testTarget.z).toBe(0)\n\n      engine.update(250)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(0)\n      expect(testTarget.z).toBe(0)\n\n      engine.update(250)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(25)\n      expect(testTarget.z).toBe(0)\n\n      engine.update(250)\n      engine.update(500)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(testTarget.z).toBe(0)\n\n      engine.update(500)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(testTarget.z).toBe(0)\n\n      engine.update(500)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(testTarget.z).toBe(100)\n\n      engine.update(500)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(testTarget.z).toBe(200)\n\n      engine.update(1000)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(testTarget.z).toBe(200)\n\n      expect(animationStates.length).toBeGreaterThan(0)\n\n      engine.update(1000)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(testTarget.z).toBe(200)\n    })\n\n    it(\"should handle overlapping animations on different properties\", () => {\n      timeline = createTimeline({ duration: 3000, autoplay: false })\n\n      const testTarget = { x: 0, y: 0, scale: 1 }\n\n      timeline.add(\n        testTarget,\n        {\n          x: 100,\n          duration: 1000,\n        },\n        0,\n      )\n\n      timeline.add(\n        testTarget,\n        {\n          y: 50,\n          duration: 1000,\n        },\n        500,\n      )\n\n      timeline.add(\n        testTarget,\n        {\n          scale: 2,\n          duration: 1000,\n        },\n        800,\n      )\n\n      timeline.play()\n\n      engine.update(600)\n      expect(testTarget.x).toBe(60)\n      expect(testTarget.y).toBe(5)\n      expect(testTarget.scale).toBe(1)\n\n      engine.update(400)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(25)\n      expect(testTarget.scale).toBe(1.2)\n\n      engine.update(600)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(testTarget.scale).toBe(1.8)\n\n      engine.update(400)\n      expect(testTarget.x).toBe(100)\n      expect(testTarget.y).toBe(50)\n      expect(testTarget.scale).toBe(2)\n    })\n\n    it(\"should handle multiple animations with different easing functions\", () => {\n      timeline = createTimeline({ duration: 3000, autoplay: false })\n\n      const testTarget = { a: 0, b: 0, c: 0 }\n\n      timeline.add(\n        testTarget,\n        {\n          a: 100,\n          duration: 1000,\n          ease: \"linear\",\n        },\n        0,\n      )\n\n      timeline.add(\n        testTarget,\n        {\n          b: 100,\n          duration: 1000,\n          ease: \"inQuad\",\n        },\n        500,\n      )\n\n      timeline.add(\n        testTarget,\n        {\n          c: 100,\n          duration: 1000,\n          ease: \"inExpo\",\n        },\n        1000,\n      )\n\n      timeline.play()\n\n      engine.update(500)\n      expect(testTarget.a).toBe(50)\n      expect(testTarget.b).toBe(0)\n      expect(testTarget.c).toBe(0)\n\n      engine.update(500)\n      expect(testTarget.a).toBe(100)\n      expect(testTarget.b).toBe(25)\n      expect(testTarget.c).toBe(0)\n\n      engine.update(500)\n      expect(testTarget.a).toBe(100)\n      expect(testTarget.b).toBe(100)\n      expect(testTarget.c).toBeGreaterThan(0)\n      expect(testTarget.c).toBeLessThan(50)\n\n      engine.update(500)\n      expect(testTarget.a).toBe(100)\n      expect(testTarget.b).toBe(100)\n      expect(testTarget.c).toBe(100)\n    })\n  })\n\n  describe(\"JSAnimation targets Array Handling\", () => {\n    it(\"should provide single target as targets[0] in onUpdate callback\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      const brightnessEffect = { brightness: 0.5 }\n      const capturedTargets: any[][] = []\n      const capturedValues: number[] = []\n\n      timeline.add(brightnessEffect, {\n        brightness: 1.0,\n        ease: \"linear\",\n        duration: 1000,\n        onUpdate: (values: JSAnimation) => {\n          capturedTargets.push([...values.targets])\n          capturedValues.push(values.targets[0].brightness)\n        },\n      })\n\n      timeline.play()\n\n      engine.update(250)\n      expect(capturedValues[0]).toBe(0.625)\n      expect(capturedTargets[0]).toHaveLength(1)\n      expect(capturedTargets[0][0].brightness).toBe(0.625)\n\n      engine.update(250)\n      expect(capturedValues[1]).toBe(0.75)\n      expect(capturedTargets[1][0].brightness).toBe(0.75)\n\n      engine.update(500)\n      expect(capturedValues[2]).toBe(1.0)\n      expect(capturedTargets[2][0].brightness).toBe(1.0)\n\n      expect(brightnessEffect.brightness).toBe(1.0)\n    })\n\n    it(\"should provide multiple targets correctly in targets array\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const effect1 = { intensity: 0.0 }\n      const effect2 = { intensity: 0.0 }\n      const capturedTargets: any[][] = []\n\n      timeline.add([effect1, effect2], {\n        intensity: 2.0,\n        ease: \"linear\",\n        duration: 500,\n        onUpdate: (values: JSAnimation) => {\n          capturedTargets.push([...values.targets])\n        },\n      })\n\n      timeline.play()\n\n      engine.update(250)\n      expect(capturedTargets[0]).toHaveLength(2)\n      expect(capturedTargets[0][0].intensity).toBe(1.0)\n      expect(capturedTargets[0][1].intensity).toBe(1.0)\n\n      engine.update(250)\n      expect(capturedTargets[1]).toHaveLength(2)\n      expect(capturedTargets[1][0].intensity).toBe(2.0)\n      expect(capturedTargets[1][1].intensity).toBe(2.0)\n\n      expect(effect1.intensity).toBe(2.0)\n      expect(effect2.intensity).toBe(2.0)\n    })\n\n    it(\"should provide targets with complex object properties\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      const postProcessEffect = {\n        brightness: 0.8,\n        contrast: 1.0,\n        saturation: 0.9,\n        vignette: { strength: 0.2 },\n      }\n\n      const loggedValues: Array<{ brightness: number; contrast: number; saturation: number }> = []\n\n      timeline.add(postProcessEffect, {\n        brightness: 1.2,\n        contrast: 1.5,\n        saturation: 1.1,\n        ease: \"outExpo\",\n        duration: 500,\n        onUpdate: (values: JSAnimation) => {\n          const target = values.targets[0]\n          loggedValues.push({\n            brightness: target.brightness,\n            contrast: target.contrast,\n            saturation: target.saturation,\n          })\n        },\n      })\n\n      timeline.play()\n\n      engine.update(100)\n      engine.update(200)\n      engine.update(200)\n\n      expect(loggedValues).toHaveLength(3)\n\n      expect(loggedValues[0].brightness).toBeGreaterThan(1.0)\n      expect(loggedValues[0].contrast).toBeGreaterThan(1.2)\n      expect(loggedValues[0].saturation).toBeGreaterThan(1.0)\n\n      expect(loggedValues[2].brightness).toBe(1.2)\n      expect(loggedValues[2].contrast).toBe(1.5)\n      expect(loggedValues[2].saturation).toBe(1.1)\n\n      expect(postProcessEffect.vignette.strength).toBe(0.2)\n\n      expect(postProcessEffect.brightness).toBe(1.2)\n      expect(postProcessEffect.contrast).toBe(1.5)\n      expect(postProcessEffect.saturation).toBe(1.1)\n    })\n\n    it(\"should maintain targets array consistency with different animation properties\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      const multiPropTarget = { x: 0, y: 0, z: 0, scale: 1, rotation: 0 }\n      const allCapturedStates: any[] = []\n\n      timeline.add(multiPropTarget, {\n        x: 100,\n        scale: 2,\n        rotation: 360,\n        ease: \"linear\",\n        duration: 1000,\n        onUpdate: (values: JSAnimation) => {\n          allCapturedStates.push({ ...values.targets[0] })\n        },\n      })\n\n      timeline.play()\n\n      engine.update(500)\n      engine.update(500)\n\n      expect(allCapturedStates).toHaveLength(2)\n\n      expect(allCapturedStates[0].x).toBe(50)\n      expect(allCapturedStates[0].scale).toBe(1.5)\n      expect(allCapturedStates[0].rotation).toBe(180)\n      expect(allCapturedStates[0].y).toBe(0)\n      expect(allCapturedStates[0].z).toBe(0)\n\n      expect(allCapturedStates[1].x).toBe(100)\n      expect(allCapturedStates[1].scale).toBe(2)\n      expect(allCapturedStates[1].rotation).toBe(360)\n      expect(allCapturedStates[1].y).toBe(0)\n      expect(allCapturedStates[1].z).toBe(0)\n\n      expect(multiPropTarget.x).toBe(100)\n      expect(multiPropTarget.scale).toBe(2)\n      expect(multiPropTarget.rotation).toBe(360)\n      expect(multiPropTarget.y).toBe(0)\n      expect(multiPropTarget.z).toBe(0)\n    })\n\n    it(\"should handle class instances with getter/setter properties\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      class TestEffect {\n        private _brightness: number = 1.0\n        private _contrast: number = 1.0\n\n        get brightness(): number {\n          return this._brightness\n        }\n\n        set brightness(value: number) {\n          this._brightness = value\n        }\n\n        get contrast(): number {\n          return this._contrast\n        }\n\n        set contrast(value: number) {\n          this._contrast = value\n        }\n      }\n\n      const effectInstance = new TestEffect()\n      const capturedValues: Array<{ brightness: number; contrast: number }> = []\n\n      timeline.add(effectInstance, {\n        brightness: 2.0,\n        contrast: 1.5,\n        ease: \"linear\",\n        duration: 500,\n        onUpdate: (values: JSAnimation) => {\n          const target = values.targets[0]\n          capturedValues.push({\n            brightness: target.brightness,\n            contrast: target.contrast,\n          })\n        },\n      })\n\n      timeline.play()\n\n      engine.update(250)\n      engine.update(250)\n\n      expect(capturedValues).toHaveLength(2)\n\n      expect(capturedValues[0].brightness).toBe(1.5)\n      expect(capturedValues[0].contrast).toBe(1.25)\n\n      expect(capturedValues[1].brightness).toBe(2.0)\n      expect(capturedValues[1].contrast).toBe(1.5)\n\n      expect(effectInstance.brightness).toBe(2.0)\n      expect(effectInstance.contrast).toBe(1.5)\n    })\n  })\n\n  describe(\"Scene00 Reproduction Bug\", () => {\n    it(\"should execute callbacks at position 0 again when timeline loops\", () => {\n      timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n\n      let callbackExecutionCount = 0\n      const resetValue = { x: 0 }\n\n      timeline.call(() => {\n        callbackExecutionCount++\n        resetValue.x = 0\n      }, 0)\n\n      timeline.add(\n        resetValue,\n        {\n          x: 100,\n          duration: 500,\n        },\n        200,\n      )\n\n      timeline.play()\n\n      engine.update(0)\n      expect(callbackExecutionCount).toBe(1)\n      expect(resetValue.x).toBe(0)\n\n      engine.update(200)\n      expect(resetValue.x).toBe(0)\n\n      engine.update(250)\n      expect(resetValue.x).toBe(50)\n\n      engine.update(575)\n      expect(timeline.currentTime).toBe(25)\n\n      expect(callbackExecutionCount).toBe(2)\n      expect(resetValue.x).toBe(0)\n\n      engine.update(175)\n      expect(resetValue.x).toBe(0)\n\n      engine.update(250)\n      expect(resetValue.x).toBe(50)\n    })\n  })\n\n  it(\"should execute callbacks at position 0 again when timeline loops\", () => {\n    timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n\n    let callbackExecutionCount = 0\n    const resetValue = { x: 0 }\n\n    timeline.call(() => {\n      callbackExecutionCount++\n      resetValue.x = 0\n    }, 0)\n\n    timeline.add(\n      resetValue,\n      {\n        x: 100,\n        duration: 500,\n      },\n      200,\n    )\n\n    timeline.play()\n\n    engine.update(0)\n    expect(callbackExecutionCount).toBe(1)\n    expect(resetValue.x).toBe(0)\n\n    engine.update(200)\n    expect(resetValue.x).toBe(0)\n\n    engine.update(250)\n    expect(resetValue.x).toBe(50)\n\n    engine.update(575)\n    expect(timeline.currentTime).toBe(25)\n\n    expect(callbackExecutionCount).toBe(2)\n    expect(resetValue.x).toBe(0)\n\n    engine.update(175)\n    expect(resetValue.x).toBe(0)\n\n    engine.update(250)\n    expect(resetValue.x).toBe(50)\n  })\n\n  it(\"should execute callbacks at position 0 again when nested sub-timeline loops\", () => {\n    const mainTimeline = createTimeline({ duration: 3000, loop: false, autoplay: false })\n    const subTimeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n\n    let callbackExecutionCount = 0\n    const resetValue = { x: 0 }\n\n    subTimeline.call(() => {\n      callbackExecutionCount++\n      resetValue.x = 0\n    }, 0)\n\n    subTimeline.add(\n      resetValue,\n      {\n        x: 100,\n        duration: 500,\n      },\n      200,\n    )\n\n    mainTimeline.sync(subTimeline, 500)\n    mainTimeline.play()\n\n    engine.update(400)\n    expect(callbackExecutionCount).toBe(0)\n    expect(resetValue.x).toBe(0)\n\n    engine.update(100)\n    expect(callbackExecutionCount).toBe(1)\n    expect(resetValue.x).toBe(0)\n\n    engine.update(200)\n    expect(resetValue.x).toBe(0)\n\n    engine.update(250)\n    expect(resetValue.x).toBe(50)\n\n    engine.update(550)\n    engine.update(25)\n\n    expect(callbackExecutionCount).toBe(2)\n    expect(resetValue.x).toBe(0)\n\n    engine.update(200)\n    expect(resetValue.x).toBe(5)\n\n    engine.update(225)\n    expect(resetValue.x).toBe(50)\n  })\n\n  it(\"should restart animations at position 0 again when nested sub-timeline loops\", () => {\n    const mainTimeline = createTimeline({ duration: 3000, loop: false, autoplay: false })\n    const subTimeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n\n    const animationTarget = { value: 0 }\n    let animationStartCount = 0\n\n    subTimeline.add(\n      animationTarget,\n      {\n        value: 100,\n        duration: 500,\n        onStart: () => animationStartCount++,\n      },\n      0,\n    )\n\n    mainTimeline.sync(subTimeline, 500)\n    mainTimeline.play()\n\n    engine.update(400)\n    expect(animationStartCount).toBe(0)\n    expect(animationTarget.value).toBe(0)\n\n    engine.update(100)\n    expect(animationStartCount).toBe(1)\n    expect(animationTarget.value).toBe(0)\n\n    engine.update(250)\n    expect(animationTarget.value).toBe(50)\n\n    engine.update(250)\n    expect(animationTarget.value).toBe(100)\n\n    engine.update(500)\n    engine.update(25)\n\n    expect(animationStartCount).toBe(2)\n    expect(animationTarget.value).toBe(5)\n\n    engine.update(225)\n    expect(animationTarget.value).toBe(50)\n\n    engine.update(250)\n    expect(animationTarget.value).toBe(100)\n  })\n\n  describe(\"Timeline onComplete Callback\", () => {\n    it(\"should call onComplete when timeline finishes (non-looping)\", () => {\n      let completeCallCount = 0\n      timeline = createTimeline({\n        duration: 1000,\n        loop: false,\n        autoplay: false,\n        onComplete: () => completeCallCount++,\n      })\n\n      timeline.add(target, { x: 100, duration: 500 })\n      timeline.play()\n\n      engine.update(500)\n      expect(completeCallCount).toBe(0)\n      expect(timeline.isPlaying).toBe(true)\n\n      engine.update(500)\n      expect(completeCallCount).toBe(1)\n      expect(timeline.isPlaying).toBe(false)\n\n      engine.update(1000)\n      expect(completeCallCount).toBe(1)\n    })\n\n    it(\"should not call onComplete for looping timelines\", () => {\n      let completeCallCount = 0\n      timeline = createTimeline({\n        duration: 1000,\n        loop: true,\n        autoplay: false,\n        onComplete: () => completeCallCount++,\n      })\n\n      timeline.add(target, { x: 100, duration: 500 })\n      timeline.play()\n\n      engine.update(1000)\n      expect(completeCallCount).toBe(0)\n      expect(timeline.isPlaying).toBe(true)\n\n      engine.update(1000)\n      expect(completeCallCount).toBe(0)\n      expect(timeline.isPlaying).toBe(true)\n\n      engine.update(2000)\n      expect(completeCallCount).toBe(0)\n      expect(timeline.isPlaying).toBe(true)\n    })\n\n    it(\"should call onComplete again when timeline is restarted and completes\", () => {\n      let completeCallCount = 0\n      timeline = createTimeline({\n        duration: 1000,\n        loop: false,\n        autoplay: false,\n        onComplete: () => completeCallCount++,\n      })\n\n      timeline.add(target, { x: 100, duration: 800 })\n      timeline.play()\n\n      engine.update(1000)\n      expect(completeCallCount).toBe(1)\n      expect(timeline.isPlaying).toBe(false)\n\n      timeline.restart()\n      expect(timeline.isPlaying).toBe(true)\n\n      engine.update(1000)\n      expect(completeCallCount).toBe(2)\n      expect(timeline.isPlaying).toBe(false)\n    })\n\n    it(\"should not call onComplete when timeline is paused before completion\", () => {\n      let completeCallCount = 0\n      timeline = createTimeline({\n        duration: 1000,\n        loop: false,\n        autoplay: false,\n        onComplete: () => completeCallCount++,\n      })\n\n      timeline.add(target, { x: 100, duration: 800 })\n      timeline.play()\n\n      engine.update(500)\n      expect(completeCallCount).toBe(0)\n      expect(timeline.isPlaying).toBe(true)\n\n      timeline.pause()\n      engine.update(1000)\n      expect(completeCallCount).toBe(0)\n      expect(timeline.isPlaying).toBe(false)\n    })\n\n    it(\"should call onComplete when playing again after pause reaches completion\", () => {\n      let completeCallCount = 0\n      timeline = createTimeline({\n        duration: 1000,\n        loop: false,\n        autoplay: false,\n        onComplete: () => completeCallCount++,\n      })\n\n      timeline.add(target, { x: 100, duration: 800 })\n      timeline.play()\n\n      engine.update(500)\n      timeline.pause()\n      engine.update(1000)\n      expect(completeCallCount).toBe(0)\n\n      timeline.play()\n      engine.update(500)\n      expect(completeCallCount).toBe(1)\n      expect(timeline.isPlaying).toBe(false)\n    })\n\n    it(\"should call onComplete with correct timing when timeline has overshoot\", () => {\n      let completeCallCount = 0\n      let completionTime = 0\n      timeline = createTimeline({\n        duration: 1000,\n        loop: false,\n        autoplay: false,\n        onComplete: () => {\n          completeCallCount++\n          completionTime = timeline.currentTime\n        },\n      })\n\n      timeline.add(target, { x: 100, duration: 800 })\n      timeline.play()\n\n      engine.update(1200)\n      expect(completeCallCount).toBe(1)\n      // expect(completionTime).toBe(0);\n      expect(timeline.isPlaying).toBe(false)\n    })\n\n    it(\"should work correctly with synced sub-timelines\", () => {\n      let mainCompleteCount = 0\n      let subCompleteCount = 0\n\n      const mainTimeline = createTimeline({\n        duration: 2000,\n        loop: false,\n        autoplay: false,\n        onComplete: () => mainCompleteCount++,\n      })\n\n      const subTimeline = createTimeline({\n        duration: 1000,\n        loop: false,\n        autoplay: false,\n        onComplete: () => subCompleteCount++,\n      })\n\n      const subTarget = { value: 0 }\n      subTimeline.add(subTarget, { value: 100, duration: 800 })\n      mainTimeline.add(target, { x: 50, duration: 1500 })\n\n      mainTimeline.sync(subTimeline, 500)\n      mainTimeline.play()\n\n      engine.update(1300)\n      expect(subCompleteCount).toBe(0)\n      expect(mainCompleteCount).toBe(0)\n      expect(mainTimeline.isPlaying).toBe(true)\n\n      engine.update(700)\n      expect(subCompleteCount).toBe(1)\n      expect(mainCompleteCount).toBe(1)\n      expect(mainTimeline.isPlaying).toBe(false)\n    })\n\n    it(\"should handle onComplete with timeline that has only callbacks\", () => {\n      let completeCallCount = 0\n      let callbackExecuted = false\n\n      timeline = createTimeline({\n        duration: 500,\n        loop: false,\n        autoplay: false,\n        onComplete: () => completeCallCount++,\n      })\n\n      timeline.call(() => {\n        callbackExecuted = true\n      }, 200)\n      timeline.play()\n\n      engine.update(300)\n      expect(callbackExecuted).toBe(true)\n      expect(completeCallCount).toBe(0)\n      expect(timeline.isPlaying).toBe(true)\n\n      engine.update(200)\n      expect(completeCallCount).toBe(1)\n      expect(timeline.isPlaying).toBe(false)\n    })\n\n    it(\"should handle onComplete when timeline duration is shorter than animations\", () => {\n      let completeCallCount = 0\n      timeline = createTimeline({\n        duration: 800,\n        loop: false,\n        autoplay: false,\n        onComplete: () => completeCallCount++,\n      })\n\n      timeline.add(target, { x: 100, duration: 1000 })\n      timeline.play()\n\n      engine.update(800)\n      expect(completeCallCount).toBe(1)\n      expect(timeline.isPlaying).toBe(false)\n      expect(target.x).toBe(80)\n    })\n\n    it(\"should not call onComplete multiple times on same completion\", () => {\n      let completeCallCount = 0\n      timeline = createTimeline({\n        duration: 500,\n        loop: false,\n        autoplay: false,\n        onComplete: () => completeCallCount++,\n      })\n\n      timeline.add(target, { x: 100, duration: 300 })\n      timeline.play()\n\n      engine.update(500)\n      expect(completeCallCount).toBe(1)\n\n      engine.update(100)\n      engine.update(200)\n      engine.update(500)\n      expect(completeCallCount).toBe(1)\n    })\n  })\n\n  describe(\"Once Method\", () => {\n    it(\"should execute once animation immediately\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      timeline.play()\n      engine.update(500)\n\n      expect(target.x).toBe(0)\n      expect(timeline.items).toHaveLength(0)\n\n      timeline.once(target, { x: 100, duration: 500 })\n\n      expect(timeline.items).toHaveLength(1)\n      expect(target.x).toBe(0)\n\n      engine.update(250)\n      expect(target.x).toBe(50)\n      expect(timeline.items).toHaveLength(1)\n\n      engine.update(250)\n      expect(target.x).toBe(100)\n      expect(timeline.items).toHaveLength(0)\n    })\n\n    it(\"should remove once animation after completion\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      timeline.add(target, { y: 50, duration: 1000 })\n      timeline.play()\n\n      engine.update(300)\n      expect(timeline.items).toHaveLength(1)\n\n      timeline.once(target, { x: 100, duration: 200 })\n      expect(timeline.items).toHaveLength(2)\n\n      engine.update(200)\n      expect(target.x).toBe(100)\n      expect(target.y).toBe(25)\n      expect(timeline.items).toHaveLength(1)\n\n      engine.update(500)\n      expect(target.y).toBe(50)\n      expect(timeline.items).toHaveLength(1)\n\n      engine.update(200)\n      expect(target.y).toBe(50)\n      expect(timeline.items).toHaveLength(1)\n    })\n\n    it(\"should not re-execute once animation when timeline loops\", () => {\n      timeline = createTimeline({ duration: 1000, loop: true, autoplay: false })\n\n      let onceStartCount = 0\n      let onceCompleteCount = 0\n\n      timeline.play()\n      engine.update(200)\n\n      timeline.once(target, {\n        x: 100,\n        duration: 300,\n        onStart: () => onceStartCount++,\n        onComplete: () => onceCompleteCount++,\n      })\n\n      expect(timeline.items).toHaveLength(1)\n\n      engine.update(300)\n      expect(target.x).toBe(100)\n      expect(onceStartCount).toBe(1)\n      expect(onceCompleteCount).toBe(1)\n      expect(timeline.items).toHaveLength(0)\n\n      engine.update(500)\n      expect(timeline.currentTime).toBe(0)\n      expect(target.x).toBe(100)\n      expect(onceStartCount).toBe(1)\n      expect(onceCompleteCount).toBe(1)\n      expect(timeline.items).toHaveLength(0)\n    })\n\n    it(\"should handle multiple once animations\", () => {\n      timeline = createTimeline({ duration: 2000, autoplay: false })\n\n      timeline.play()\n      engine.update(100)\n\n      const target1 = { value: 0 }\n      const target2 = { value: 0 }\n\n      timeline.once(target1, { value: 50, duration: 200 })\n      timeline.once(target2, { value: 100, duration: 300 })\n\n      expect(timeline.items).toHaveLength(2)\n\n      engine.update(200)\n      expect(target1.value).toBe(50)\n      expect(target2.value).toBeCloseTo(66.67, 1)\n      expect(timeline.items).toHaveLength(1)\n\n      engine.update(100)\n      expect(target1.value).toBe(50)\n      expect(target2.value).toBe(100)\n      expect(timeline.items).toHaveLength(0)\n    })\n\n    it(\"should handle once animations with different easing functions\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      timeline.play()\n      engine.update(200)\n\n      timeline.once(target, { x: 100, duration: 400, ease: \"linear\" })\n\n      engine.update(200)\n      expect(target.x).toBe(50)\n\n      engine.update(200)\n      expect(target.x).toBe(100)\n      expect(timeline.items).toHaveLength(0)\n    })\n\n    it(\"should trigger onUpdate callbacks for once animations\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      let updateCount = 0\n      const progressValues: number[] = []\n\n      timeline.play()\n      engine.update(100)\n\n      timeline.once(target, {\n        x: 100,\n        duration: 400,\n        onUpdate: (anim: JSAnimation) => {\n          updateCount++\n          progressValues.push(anim.progress)\n        },\n      })\n\n      engine.update(200)\n      expect(updateCount).toBe(1)\n      expect(progressValues[0]).toBe(0.5)\n\n      engine.update(200)\n      expect(updateCount).toBe(2)\n      expect(progressValues[1]).toBe(1)\n      expect(timeline.items).toHaveLength(0)\n    })\n\n    it(\"should handle zero duration once animations\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      timeline.play()\n      engine.update(200)\n\n      timeline.once(target, { x: 100, duration: 0 })\n\n      expect(timeline.items).toHaveLength(1)\n\n      engine.update(1)\n      expect(target.x).toBe(100)\n      expect(timeline.items).toHaveLength(0)\n    })\n\n    it(\"should handle once animations added while timeline is paused\", () => {\n      timeline = createTimeline({ duration: 1000, autoplay: false })\n\n      timeline.play()\n      engine.update(300)\n      timeline.pause()\n\n      timeline.once(target, { x: 100, duration: 200 })\n\n      expect(timeline.items).toHaveLength(1)\n      expect(target.x).toBe(0)\n\n      engine.update(100)\n      expect(target.x).toBe(0)\n      expect(timeline.items).toHaveLength(1)\n\n      timeline.play()\n      engine.update(100)\n      expect(target.x).toBe(50)\n\n      engine.update(100)\n      expect(target.x).toBe(100)\n      expect(timeline.items).toHaveLength(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/animation/Timeline.ts",
    "content": "import type { CliRenderer } from \"../renderer.js\"\n\nexport interface TimelineOptions {\n  duration?: number\n  loop?: boolean\n  autoplay?: boolean\n  onComplete?: () => void\n  onPause?: () => void\n}\n\nexport interface AnimationOptions {\n  duration: number\n  ease?: EasingFunctions\n  onUpdate?: (animation: JSAnimation) => void\n  onComplete?: () => void\n  onStart?: () => void\n  onLoop?: () => void\n  loop?: boolean | number\n  loopDelay?: number\n  alternate?: boolean\n  once?: boolean\n  [key: string]: any\n}\n\nexport interface JSAnimation {\n  targets: any[]\n  deltaTime: number\n  progress: number\n  currentTime: number\n}\n\ninterface TimelineItem {\n  type: \"animation\" | \"callback\" | \"timeline\"\n  startTime: number\n}\n\ninterface TimelineTimelineItem extends TimelineItem {\n  type: \"timeline\"\n  timeline: Timeline\n  timelineStarted?: boolean\n}\n\ninterface TimelineCallbackItem extends TimelineItem {\n  type: \"callback\"\n  callback: () => void\n  executed: boolean\n}\n\ninterface TimelineAnimationItem extends TimelineItem {\n  type: \"animation\"\n\n  target: any[]\n  properties?: Record<string, number>\n  initialValues?: Record<string, number>[]\n  duration?: number\n  ease?: keyof typeof easingFunctions\n  loop?: boolean | number\n  loopDelay?: number\n  alternate?: boolean\n  onUpdate?: (animation: JSAnimation) => void\n  onComplete?: () => void\n  onStart?: () => void\n  onLoop?: () => void\n  completed?: boolean\n  started?: boolean\n  currentLoop?: number\n  once?: boolean\n}\n\nexport type EasingFunctions = keyof typeof easingFunctions\n\nconst easingFunctions = {\n  linear: (t: number) => t,\n  inQuad: (t: number) => t * t,\n  outQuad: (t: number) => t * (2 - t),\n  inOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),\n  inExpo: (t: number) => (t === 0 ? 0 : Math.pow(2, 10 * (t - 1))),\n  outExpo: (t: number) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),\n  inOutSine: (t: number) => -(Math.cos(Math.PI * t) - 1) / 2,\n  outBounce: (t: number) => {\n    const n1 = 7.5625\n    const d1 = 2.75\n    if (t < 1 / d1) {\n      return n1 * t * t\n    } else if (t < 2 / d1) {\n      return n1 * (t -= 1.5 / d1) * t + 0.75\n    } else if (t < 2.5 / d1) {\n      return n1 * (t -= 2.25 / d1) * t + 0.9375\n    } else {\n      return n1 * (t -= 2.625 / d1) * t + 0.984375\n    }\n  },\n  outElastic: (t: number) => {\n    const c4 = (2 * Math.PI) / 3\n    return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1\n  },\n  inBounce: (t: number) => 1 - easingFunctions.outBounce(1 - t),\n  inCirc: (t: number) => 1 - Math.sqrt(1 - t * t),\n  outCirc: (t: number) => Math.sqrt(1 - Math.pow(t - 1, 2)),\n  inOutCirc: (t: number) => {\n    if ((t *= 2) < 1) return -0.5 * (Math.sqrt(1 - t * t) - 1)\n    return 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1)\n  },\n  inBack: (t: number, s: number = 1.70158) => t * t * ((s + 1) * t - s),\n  outBack: (t: number, s: number = 1.70158) => --t * t * ((s + 1) * t + s) + 1,\n  inOutBack: (t: number, s: number = 1.70158) => {\n    s *= 1.525\n    if ((t *= 2) < 1) return 0.5 * (t * t * ((s + 1) * t - s))\n    return 0.5 * ((t -= 2) * t * ((s + 1) * t + s) + 2)\n  },\n}\n\nfunction captureInitialValues(item: TimelineAnimationItem): void {\n  if (!item.properties) return\n  if (!item.initialValues || item.initialValues.length === 0) {\n    const initialValues: Record<string, number>[] = []\n\n    for (let i = 0; i < item.target.length; i++) {\n      const target = item.target[i]\n      const targetInitialValues: Record<string, number> = {}\n\n      for (const key of Object.keys(item.properties)) {\n        if (typeof target[key] === \"number\") {\n          targetInitialValues[key] = target[key]\n        }\n      }\n\n      initialValues.push(targetInitialValues)\n    }\n\n    item.initialValues = initialValues\n  }\n}\n\nfunction applyAnimationAtProgress(\n  item: TimelineAnimationItem,\n  progress: number,\n  reversed: boolean,\n  timelineTime: number,\n  deltaTime: number = 0,\n): void {\n  if (!item.properties || !item.initialValues) return\n\n  const easingFn = easingFunctions[item.ease || \"linear\"] || easingFunctions.linear\n  const easedProgress = easingFn(Math.max(0, Math.min(1, progress)))\n  const finalProgress = reversed ? 1 - easedProgress : easedProgress\n\n  for (let i = 0; i < item.target.length; i++) {\n    const target = item.target[i]\n    const targetInitialValues = item.initialValues[i]\n\n    if (!targetInitialValues) continue\n\n    for (const [key, endValue] of Object.entries(item.properties)) {\n      const startValue = targetInitialValues[key]\n      const newValue = startValue + (endValue - startValue) * finalProgress\n      target[key] = newValue\n    }\n  }\n\n  if (item.onUpdate) {\n    const animation: JSAnimation = {\n      targets: item.target,\n      progress: easedProgress,\n      currentTime: timelineTime,\n      deltaTime: deltaTime,\n    }\n    item.onUpdate(animation)\n  }\n}\n\nfunction evaluateAnimation(item: TimelineAnimationItem, timelineTime: number, deltaTime: number = 0): void {\n  if (timelineTime < item.startTime) {\n    return\n  }\n\n  const animationTime = timelineTime - item.startTime\n  const duration = item.duration || 0\n\n  if (timelineTime >= item.startTime && !item.started) {\n    captureInitialValues(item)\n    if (item.onStart) {\n      item.onStart()\n    }\n    item.started = true\n  }\n\n  if (duration === 0) {\n    if (!item.completed) {\n      applyAnimationAtProgress(item, 1, false, timelineTime, deltaTime)\n      if (item.onComplete) {\n        item.onComplete()\n      }\n      item.completed = true\n    }\n    return\n  }\n\n  // Unified looping logic - single execution is just maxLoops = 1\n  const maxLoops = !item.loop || item.loop === 1 ? 1 : typeof item.loop === \"number\" ? item.loop : Infinity\n  const loopDelay = item.loopDelay || 0\n  const cycleTime = duration + loopDelay\n  let currentCycle = Math.floor(animationTime / cycleTime)\n  let timeInCycle = animationTime % cycleTime\n\n  // Trigger onLoop if a loop cycle (not the final one) completes\n  if (item.onLoop && item.currentLoop !== undefined && currentCycle > item.currentLoop && currentCycle < maxLoops) {\n    item.onLoop()\n  }\n  item.currentLoop = currentCycle\n\n  // Check if the animation part of the *final loop* has just completed\n  if (item.onComplete && !item.completed && currentCycle === maxLoops - 1 && timeInCycle >= duration) {\n    const finalLoopReversed = (item.alternate || false) && currentCycle % 2 === 1\n    applyAnimationAtProgress(item, 1, finalLoopReversed, timelineTime, deltaTime)\n\n    item.onComplete()\n    item.completed = true\n    return\n  }\n\n  if (currentCycle >= maxLoops) {\n    if (!item.completed) {\n      const finalReversed = (item.alternate || false) && (maxLoops - 1) % 2 === 1\n      applyAnimationAtProgress(item, 1, finalReversed, timelineTime, deltaTime)\n\n      if (item.onComplete) {\n        item.onComplete()\n      }\n      item.completed = true\n    }\n    return\n  }\n\n  if (timeInCycle === 0 && animationTime > 0 && currentCycle < maxLoops) {\n    currentCycle = currentCycle - 1\n    timeInCycle = cycleTime\n  }\n\n  if (timeInCycle >= duration) {\n    const isReversed = (item.alternate || false) && currentCycle % 2 === 1\n    applyAnimationAtProgress(item, 1, isReversed, timelineTime, deltaTime)\n    return\n  }\n\n  const progress = timeInCycle / duration\n  const isReversed = (item.alternate || false) && currentCycle % 2 === 1\n  applyAnimationAtProgress(item, progress, isReversed, timelineTime, deltaTime)\n}\n\nfunction evaluateCallback(item: TimelineCallbackItem, timelineTime: number): void {\n  if (!item.executed && timelineTime >= item.startTime && item.callback) {\n    item.callback()\n    item.executed = true\n  }\n}\n\nfunction evaluateTimelineSync(item: TimelineTimelineItem, timelineTime: number, deltaTime: number = 0): void {\n  if (!item.timeline) return\n  if (timelineTime < item.startTime) {\n    return\n  }\n\n  if (!item.timelineStarted) {\n    item.timelineStarted = true\n    item.timeline.play()\n\n    const overshoot = timelineTime - item.startTime\n    item.timeline.update(overshoot)\n    return\n  }\n\n  item.timeline.update(deltaTime)\n}\n\nfunction evaluateItem(item: TimelineItem, timelineTime: number, deltaTime: number = 0): void {\n  if (item.type === \"animation\") {\n    evaluateAnimation(item as TimelineAnimationItem, timelineTime, deltaTime)\n  } else if (item.type === \"callback\") {\n    evaluateCallback(item as TimelineCallbackItem, timelineTime)\n  }\n}\n\nexport class Timeline {\n  public items: (TimelineAnimationItem | TimelineCallbackItem)[] = []\n  public subTimelines: TimelineTimelineItem[] = []\n  public currentTime: number = 0\n  public isPlaying: boolean = false\n  public isComplete: boolean = false\n  public duration: number\n  public loop: boolean\n  public synced: boolean = false\n  private autoplay: boolean\n  private onComplete?: () => void\n  private onPause?: () => void\n  private stateChangeListeners: ((timeline: Timeline) => void)[] = []\n\n  constructor(options: TimelineOptions = {}) {\n    this.duration = options.duration || 1000\n    this.loop = options.loop === true\n    this.autoplay = options.autoplay !== false\n    this.onComplete = options.onComplete\n    this.onPause = options.onPause\n  }\n\n  public addStateChangeListener(listener: (timeline: Timeline) => void): void {\n    this.stateChangeListeners.push(listener)\n  }\n\n  public removeStateChangeListener(listener: (timeline: Timeline) => void): void {\n    this.stateChangeListeners = this.stateChangeListeners.filter((l) => l !== listener)\n  }\n\n  private notifyStateChange(): void {\n    for (const listener of this.stateChangeListeners) {\n      listener(this)\n    }\n  }\n\n  add(target: any, properties: AnimationOptions, startTime: number | string = 0): this {\n    const resolvedStartTime = typeof startTime === \"string\" ? 0 : startTime\n\n    const animationProperties: Record<string, number> = {}\n\n    // Extract animation properties (don't capture initial values here)\n    for (const key in properties) {\n      if (\n        ![\"duration\", \"ease\", \"onUpdate\", \"onComplete\", \"onStart\", \"onLoop\", \"loop\", \"loopDelay\", \"alternate\"].includes(\n          key,\n        )\n      ) {\n        if (typeof properties[key] === \"number\") {\n          animationProperties[key] = properties[key]\n        }\n      }\n    }\n\n    this.items.push({\n      type: \"animation\",\n      startTime: resolvedStartTime,\n      target: Array.isArray(target) ? target : [target],\n      properties: animationProperties,\n      initialValues: [], // Will be captured when animation starts\n      duration: properties.duration !== undefined ? properties.duration : 1000,\n      ease: properties.ease || \"linear\",\n      loop: properties.loop,\n      loopDelay: properties.loopDelay || 0,\n      alternate: properties.alternate || false,\n      onUpdate: properties.onUpdate,\n      onComplete: properties.onComplete,\n      onStart: properties.onStart,\n      onLoop: properties.onLoop,\n      completed: false,\n      started: false,\n      currentLoop: 0,\n      once: properties.once ?? false,\n    })\n\n    return this\n  }\n\n  once(target: any, properties: AnimationOptions): this {\n    this.add(\n      target,\n      {\n        ...properties,\n        once: true,\n      },\n      this.currentTime,\n    )\n\n    return this\n  }\n\n  call(callback: () => void, startTime: number | string = 0): this {\n    const resolvedStartTime = typeof startTime === \"string\" ? 0 : startTime\n\n    this.items.push({\n      type: \"callback\",\n      startTime: resolvedStartTime,\n      callback,\n      executed: false,\n    })\n\n    return this\n  }\n\n  sync(timeline: Timeline, startTime: number = 0): this {\n    if (timeline.synced) {\n      throw new Error(\"Timeline already synced\")\n    }\n    this.subTimelines.push({\n      type: \"timeline\",\n      startTime,\n      timeline,\n    })\n    timeline.synced = true\n\n    return this\n  }\n\n  play(): this {\n    if (this.isComplete) {\n      return this.restart()\n    }\n    this.subTimelines.forEach((subTimeline) => {\n      if (subTimeline.timelineStarted) {\n        subTimeline.timeline.play()\n      }\n    })\n    this.isPlaying = true\n    this.notifyStateChange()\n    return this\n  }\n\n  pause(): this {\n    this.subTimelines.forEach((subTimeline) => {\n      subTimeline.timeline.pause()\n    })\n    this.isPlaying = false\n    if (this.onPause) {\n      this.onPause()\n    }\n    this.notifyStateChange()\n    return this\n  }\n\n  resetItems() {\n    this.items.forEach((item) => {\n      if (item.type === \"callback\") {\n        item.executed = false\n      } else if (item.type === \"animation\") {\n        item.completed = false\n        item.started = false\n        item.currentLoop = 0\n      }\n    })\n    this.subTimelines.forEach((subTimeline) => {\n      subTimeline.timelineStarted = false\n      if (subTimeline.timeline) {\n        subTimeline.timeline.restart()\n        subTimeline.timeline.pause()\n      }\n    })\n  }\n\n  restart(): this {\n    this.isComplete = false\n    this.currentTime = 0\n    this.isPlaying = true\n    this.resetItems()\n    this.notifyStateChange()\n\n    return this\n  }\n\n  update(deltaTime: number): void {\n    for (const subTimeline of this.subTimelines) {\n      evaluateTimelineSync(subTimeline, this.currentTime + deltaTime, deltaTime)\n    }\n\n    if (!this.isPlaying) return\n\n    this.currentTime += deltaTime\n\n    for (const item of this.items) {\n      evaluateItem(item, this.currentTime, deltaTime)\n    }\n\n    // Remove completed \"once\" items (iterate backwards to avoid index shifting)\n    for (let i = this.items.length - 1; i >= 0; i--) {\n      const item = this.items[i]\n      if (item.type === \"animation\" && item.once && item.completed) {\n        this.items.splice(i, 1)\n      }\n    }\n\n    if (this.loop && this.currentTime >= this.duration) {\n      const overshoot = this.currentTime % this.duration\n\n      this.resetItems()\n      this.currentTime = 0\n\n      if (overshoot > 0) {\n        this.update(overshoot)\n      }\n    } else if (!this.loop && this.currentTime >= this.duration) {\n      this.currentTime = this.duration\n      this.isPlaying = false\n      this.isComplete = true\n\n      if (this.onComplete) {\n        this.onComplete()\n      }\n      this.notifyStateChange()\n    }\n  }\n}\n\nclass TimelineEngine {\n  private timelines: Set<Timeline> = new Set()\n  private renderer: CliRenderer | null = null\n  private frameCallback: ((deltaTime: number) => Promise<void>) | null = null\n  private isLive: boolean = false\n  public defaults = {\n    frameRate: 60,\n  }\n\n  attach(renderer: CliRenderer): void {\n    if (this.renderer) {\n      this.detach()\n    }\n\n    this.renderer = renderer\n    this.frameCallback = async (deltaTime: number) => {\n      this.update(deltaTime)\n    }\n\n    renderer.setFrameCallback(this.frameCallback)\n  }\n\n  detach(): void {\n    if (this.renderer && this.frameCallback) {\n      this.renderer.removeFrameCallback(this.frameCallback)\n      if (this.isLive) {\n        this.renderer.dropLive()\n        this.isLive = false\n      }\n    }\n    this.renderer = null\n    this.frameCallback = null\n  }\n\n  private updateLiveState(): void {\n    if (!this.renderer) return\n\n    const hasRunningTimelines = Array.from(this.timelines).some(\n      (timeline) => !timeline.synced && timeline.isPlaying && !timeline.isComplete,\n    )\n\n    if (hasRunningTimelines && !this.isLive) {\n      this.renderer.requestLive()\n      this.isLive = true\n    } else if (!hasRunningTimelines && this.isLive) {\n      this.renderer.dropLive()\n      this.isLive = false\n    }\n  }\n\n  private onTimelineStateChange = (timeline: Timeline): void => {\n    this.updateLiveState()\n  }\n\n  register(timeline: Timeline): void {\n    if (!this.timelines.has(timeline)) {\n      this.timelines.add(timeline)\n      timeline.addStateChangeListener(this.onTimelineStateChange)\n      this.updateLiveState()\n    }\n  }\n\n  unregister(timeline: Timeline): void {\n    if (this.timelines.has(timeline)) {\n      this.timelines.delete(timeline)\n      timeline.removeStateChangeListener(this.onTimelineStateChange)\n      this.updateLiveState()\n    }\n  }\n\n  clear(): void {\n    for (const timeline of this.timelines) {\n      timeline.removeStateChangeListener(this.onTimelineStateChange)\n    }\n    this.timelines.clear()\n    this.updateLiveState()\n  }\n\n  update(deltaTime: number): void {\n    for (const timeline of this.timelines) {\n      if (!timeline.synced) {\n        timeline.update(deltaTime)\n      }\n    }\n  }\n}\n\nexport const engine = new TimelineEngine()\n\nexport function createTimeline(options: TimelineOptions = {}): Timeline {\n  const timeline = new Timeline(options)\n  if (options.autoplay !== false) {\n    timeline.play()\n  }\n\n  engine.register(timeline)\n\n  return timeline\n}\n"
  },
  {
    "path": "packages/core/src/ansi.ts",
    "content": "export const ANSI = {\n  switchToAlternateScreen: \"\\x1b[?1049h\",\n  switchToMainScreen: \"\\x1b[?1049l\",\n  reset: \"\\x1b[0m\",\n\n  scrollDown: (lines: number) => `\\x1b[${lines}T`,\n  scrollUp: (lines: number) => `\\x1b[${lines}S`,\n\n  moveCursor: (row: number, col: number) => `\\x1b[${row};${col}H`,\n  moveCursorAndClear: (row: number, col: number) => `\\x1b[${row};${col}H\\x1b[J`,\n\n  setRgbBackground: (r: number, g: number, b: number) => `\\x1b[48;2;${r};${g};${b}m`,\n  resetBackground: \"\\x1b[49m\",\n\n  // Bracketed paste mode\n  bracketedPasteStart: \"\\u001b[200~\",\n  bracketedPasteEnd: \"\\u001b[201~\",\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/.gitignore",
    "content": "*.json\n!latest-quick-bench-run.json\n!latest-default-bench-run.json\n!latest-large-bench-run.json\n!latest-all-bench-run.json\n!latest-async-bench-run.json\n\n"
  },
  {
    "path": "packages/core/src/benchmark/attenuation-benchmark.ts",
    "content": "#!/usr/bin/env bun\n\nimport { performance } from \"node:perf_hooks\"\nimport { OptimizedBuffer } from \"../buffer\"\nimport { VignetteEffect } from \"../post/effects\"\n\ntype Scenario = { width: number; height: number }\ntype ScenarioResult = {\n  size: string\n  cells: number\n  avgMs: number\n  avgNsPerCell: number\n  medianMs: number\n  p95Ms: number\n}\n\nconst ITERATIONS = 5000\nconst WARMUP_ITERATIONS = 100\nconst STRENGTH = 0.7\nconst baseScenarios: Array<{ width: number; height: number }> = [\n  { width: 40, height: 20 },\n  { width: 80, height: 24 },\n  { width: 120, height: 40 },\n  { width: 200, height: 60 },\n]\nconst scenarios: Scenario[] = baseScenarios\n\nfunction calculateStats(samples: number[]): { avgMs: number; medianMs: number; p95Ms: number } {\n  const sorted = [...samples].sort((a, b) => a - b)\n  const total = samples.reduce((sum, value) => sum + value, 0)\n  const avgMs = total / samples.length\n  const mid = Math.floor(sorted.length / 2)\n  const medianMs = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]\n  const p95Index = Math.floor(0.95 * (sorted.length - 1))\n  const p95Ms = sorted[p95Index]\n\n  return { avgMs, medianMs, p95Ms }\n}\n\nfunction formatMs(value: number): number {\n  return Number(value.toFixed(4))\n}\n\nfunction formatNs(value: number): number {\n  return Number(value.toFixed(2))\n}\n\nfunction runScenario({ width, height }: Scenario): ScenarioResult {\n  const buffer = OptimizedBuffer.create(width, height, \"unicode\", { id: `vignette-bench-${width}x${height}` })\n  const vignetteEffect = new VignetteEffect(STRENGTH)\n  const { fg, bg } = buffer.buffers\n  fg.fill(1)\n  bg.fill(1)\n\n  for (let i = 0; i < WARMUP_ITERATIONS; i++) {\n    vignetteEffect.apply(buffer)\n  }\n\n  const samples = new Array<number>(ITERATIONS)\n  for (let i = 0; i < ITERATIONS; i++) {\n    const start = performance.now()\n    vignetteEffect.apply(buffer)\n    samples[i] = performance.now() - start\n  }\n\n  buffer.destroy()\n\n  const stats = calculateStats(samples)\n  return {\n    size: `${width}x${height}`,\n    cells: width * height,\n    avgMs: formatMs(stats.avgMs),\n    avgNsPerCell: formatNs((stats.avgMs * 1_000_000) / (width * height)),\n    medianMs: formatMs(stats.medianMs),\n    p95Ms: formatMs(stats.p95Ms),\n  }\n}\n\nconsole.log(`Vignette Effect Benchmark (${ITERATIONS} iterations per scenario)`)\nconst results = scenarios.map(runScenario)\nconsole.table(results)\n"
  },
  {
    "path": "packages/core/src/benchmark/colormatrix-benchmark.ts",
    "content": "#!/usr/bin/env bun\n\nimport { performance } from \"node:perf_hooks\"\nimport { OptimizedBuffer } from \"../buffer\"\n\ntype Scenario = { width: number; height: number; mode: \"uniform\" | \"mask25\" | \"mask100\" }\ntype ScenarioResult = {\n  size: string\n  cells: number\n  mode: \"uniform\" | \"mask25\" | \"mask100\"\n  avgMs: number\n  avgNsPerCell: number\n  medianMs: number\n  p95Ms: number\n}\n\nconst sepiaMatrix = new Float32Array([\n  0.393, 0.769, 0.189, 0, 0.349, 0.686, 0.168, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 1,\n])\n\nconst ITERATIONS = 1000\nconst WARMUP_ITERATIONS = 100\nconst baseScenarios: Array<{ width: number; height: number }> = [\n  { width: 80, height: 24 },\n  { width: 120, height: 40 },\n  { width: 200, height: 60 },\n]\nconst scenarios: Scenario[] = baseScenarios.flatMap((scenario) => [\n  { ...scenario, mode: \"uniform\" },\n  { ...scenario, mode: \"mask25\" },\n  { ...scenario, mode: \"mask100\" },\n])\n\nfunction generateCellMask(width: number, height: number, density: number): Float32Array {\n  const totalCells = width * height\n  const numCells = Math.floor(totalCells * density)\n  const mask = new Float32Array(numCells * 3)\n\n  for (let i = 0; i < numCells; i++) {\n    mask[i * 3] = i % width\n    mask[i * 3 + 1] = Math.floor(i / width)\n    mask[i * 3 + 2] = 1\n  }\n\n  return mask\n}\n\nfunction calculateStats(samples: number[]): { avgMs: number; medianMs: number; p95Ms: number } {\n  const sorted = [...samples].sort((a, b) => a - b)\n  const total = samples.reduce((sum, value) => sum + value, 0)\n  const avgMs = total / samples.length\n  const mid = Math.floor(sorted.length / 2)\n  const medianMs = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]\n  const p95Index = Math.floor(0.95 * (sorted.length - 1))\n  const p95Ms = sorted[p95Index]\n\n  return { avgMs, medianMs, p95Ms }\n}\n\nfunction formatMs(value: number): number {\n  return Number(value.toFixed(4))\n}\n\nfunction formatNs(value: number): number {\n  return Number(value.toFixed(2))\n}\n\nfunction fillBufferColors(buffer: OptimizedBuffer): void {\n  const { fg, bg } = buffer.buffers\n\n  for (let i = 0; i < fg.length; i += 4) {\n    fg[i] = Math.random()\n    fg[i + 1] = Math.random()\n    fg[i + 2] = Math.random()\n    fg[i + 3] = 1\n    bg[i] = Math.random()\n    bg[i + 1] = Math.random()\n    bg[i + 2] = Math.random()\n    bg[i + 3] = 1\n  }\n}\n\nfunction runScenario({ width, height, mode }: Scenario): ScenarioResult {\n  const buffer = OptimizedBuffer.create(width, height, \"unicode\", {\n    id: `colormatrix-bench-${mode}-${width}x${height}`,\n  })\n  const cellMask = mode === \"mask25\" ? generateCellMask(width, height, 0.25) : generateCellMask(width, height, 1)\n  const cellCount = mode === \"uniform\" ? width * height : cellMask.length / 3\n\n  fillBufferColors(buffer)\n\n  for (let i = 0; i < WARMUP_ITERATIONS; i++) {\n    if (mode === \"uniform\") {\n      buffer.colorMatrixUniform(sepiaMatrix, 1.0, 3)\n    } else {\n      buffer.colorMatrix(sepiaMatrix, cellMask, 1.0, 3)\n    }\n  }\n\n  const samples = new Array<number>(ITERATIONS)\n  for (let i = 0; i < ITERATIONS; i++) {\n    const start = performance.now()\n    if (mode === \"uniform\") {\n      buffer.colorMatrixUniform(sepiaMatrix, 1.0, 3)\n    } else {\n      buffer.colorMatrix(sepiaMatrix, cellMask, 1.0, 3)\n    }\n    samples[i] = performance.now() - start\n  }\n\n  buffer.destroy()\n\n  const stats = calculateStats(samples)\n\n  return {\n    size: `${width}x${height}`,\n    cells: cellCount,\n    mode,\n    avgMs: formatMs(stats.avgMs),\n    avgNsPerCell: formatNs((stats.avgMs * 1_000_000) / cellCount),\n    medianMs: formatMs(stats.medianMs),\n    p95Ms: formatMs(stats.p95Ms),\n  }\n}\n\nconsole.log(`ColorMatrix Benchmark (${ITERATIONS} iterations per scenario)`)\nconst results = scenarios.map(runScenario)\nconsole.table(results)\n"
  },
  {
    "path": "packages/core/src/benchmark/gain-benchmark.ts",
    "content": "#!/usr/bin/env bun\n\nimport { performance } from \"node:perf_hooks\"\nimport { OptimizedBuffer } from \"../buffer\"\nimport { applyGain } from \"../post/filters\"\n\ntype Scenario = { width: number; height: number }\ntype ScenarioResult = {\n  size: string\n  cells: number\n  avgMs: number\n  avgNsPerCell: number\n  medianMs: number\n  p95Ms: number\n}\n\nconst ITERATIONS = 5000\nconst WARMUP_ITERATIONS = 100\nconst GAIN_FACTOR = 1.3\nconst baseScenarios: Array<{ width: number; height: number }> = [\n  { width: 40, height: 20 },\n  { width: 80, height: 24 },\n  { width: 120, height: 40 },\n  { width: 200, height: 60 },\n]\nconst scenarios: Scenario[] = baseScenarios\n\nfunction calculateStats(samples: number[]): { avgMs: number; medianMs: number; p95Ms: number } {\n  const sorted = [...samples].sort((a, b) => a - b)\n  const total = samples.reduce((sum, value) => sum + value, 0)\n  const avgMs = total / samples.length\n  const mid = Math.floor(sorted.length / 2)\n  const medianMs = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]\n  const p95Index = Math.floor(0.95 * (sorted.length - 1))\n  const p95Ms = sorted[p95Index]\n\n  return { avgMs, medianMs, p95Ms }\n}\n\nfunction formatMs(value: number): number {\n  return Number(value.toFixed(4))\n}\n\nfunction formatNs(value: number): number {\n  return Number(value.toFixed(2))\n}\n\nfunction runScenario({ width, height }: Scenario): ScenarioResult {\n  const buffer = OptimizedBuffer.create(width, height, \"unicode\", { id: `gain-bench-${width}x${height}` })\n  const { fg, bg } = buffer.buffers\n  fg.fill(1)\n  bg.fill(1)\n\n  for (let i = 0; i < WARMUP_ITERATIONS; i++) {\n    applyGain(buffer, GAIN_FACTOR)\n  }\n\n  const samples = new Array<number>(ITERATIONS)\n  for (let i = 0; i < ITERATIONS; i++) {\n    const start = performance.now()\n    applyGain(buffer, GAIN_FACTOR)\n    samples[i] = performance.now() - start\n  }\n\n  buffer.destroy()\n\n  const stats = calculateStats(samples)\n  return {\n    size: `${width}x${height}`,\n    cells: width * height,\n    avgMs: formatMs(stats.avgMs),\n    avgNsPerCell: formatNs((stats.avgMs * 1_000_000) / (width * height)),\n    medianMs: formatMs(stats.medianMs),\n    p95Ms: formatMs(stats.p95Ms),\n  }\n}\n\nconsole.log(`Gain Benchmark (${ITERATIONS} iterations per scenario)`)\nconst results = scenarios.map(runScenario)\nconsole.table(results)\n"
  },
  {
    "path": "packages/core/src/benchmark/latest-all-bench-run.json",
    "content": "{\n  \"runId\": \"2026-02-11T12:21:31.262Z\",\n  \"suite\": \"all\",\n  \"args\": [\"--suite=all\", \"--json=src/benchmark/latest-all-bench-run.json\"],\n  \"results\": [\n    {\n      \"name\": \"ansi_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.002211,\n      \"medianMs\": 0.002083,\n      \"p95Ms\": 0.00275,\n      \"minMs\": 0.001667,\n      \"maxMs\": 0.464375,\n      \"throughputMBps\": 28267.165966,\n      \"elapsedMs\": 44.220917,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ansi\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/ansi_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005902,\n      \"medianMs\": 0.005875,\n      \"p95Ms\": 0.007,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.110166,\n      \"throughputMBps\": 10589.744839,\n      \"elapsedMs\": 118.038727,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ansi\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"ascii_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001928,\n      \"medianMs\": 0.001875,\n      \"p95Ms\": 0.00225,\n      \"minMs\": 0.001666,\n      \"maxMs\": 0.274,\n      \"throughputMBps\": 32420.804404,\n      \"elapsedMs\": 38.55549,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/ascii_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005723,\n      \"medianMs\": 0.005709,\n      \"p95Ms\": 0.00675,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.030375,\n      \"throughputMBps\": 10920.186072,\n      \"elapsedMs\": 114.466914,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"binary_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.002055,\n      \"medianMs\": 0.001959,\n      \"p95Ms\": 0.002333,\n      \"minMs\": 0.001667,\n      \"maxMs\": 0.607541,\n      \"throughputMBps\": 30412.396229,\n      \"elapsedMs\": 41.101661,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005805,\n      \"medianMs\": 0.00575,\n      \"p95Ms\": 0.006833,\n      \"minMs\": 0.003958,\n      \"maxMs\": 0.628708,\n      \"throughputMBps\": 10766.934424,\n      \"elapsedMs\": 116.096184,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"random_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001972,\n      \"medianMs\": 0.001958,\n      \"p95Ms\": 0.0025,\n      \"minMs\": 0.001666,\n      \"maxMs\": 0.013542,\n      \"throughputMBps\": 31687.224282,\n      \"elapsedMs\": 39.448075,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005738,\n      \"medianMs\": 0.00575,\n      \"p95Ms\": 0.006792,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.019208,\n      \"throughputMBps\": 10892.833256,\n      \"elapsedMs\": 114.75435,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"medium_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.016347,\n      \"medianMs\": 0.0145,\n      \"p95Ms\": 0.018209,\n      \"minMs\": 0.012541,\n      \"maxMs\": 0.721917,\n      \"throughputMBps\": 61171.455392,\n      \"elapsedMs\": 8.173747,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/medium_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.019131,\n      \"medianMs\": 0.017375,\n      \"p95Ms\": 0.019041,\n      \"minMs\": 0.014875,\n      \"maxMs\": 0.883917,\n      \"throughputMBps\": 52271.680176,\n      \"elapsedMs\": 9.565409,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"binary_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.014489,\n      \"medianMs\": 0.01425,\n      \"p95Ms\": 0.017875,\n      \"minMs\": 0.012416,\n      \"maxMs\": 0.025459,\n      \"throughputMBps\": 69019.399973,\n      \"elapsedMs\": 7.24434,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.017255,\n      \"medianMs\": 0.016958,\n      \"p95Ms\": 0.019584,\n      \"minMs\": 0.014917,\n      \"maxMs\": 0.02875,\n      \"throughputMBps\": 57955.593265,\n      \"elapsedMs\": 8.627295,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"random_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.014455,\n      \"medianMs\": 0.01425,\n      \"p95Ms\": 0.016083,\n      \"minMs\": 0.012708,\n      \"maxMs\": 0.024042,\n      \"throughputMBps\": 69178.252295,\n      \"elapsedMs\": 7.227705,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.016948,\n      \"medianMs\": 0.017042,\n      \"p95Ms\": 0.018458,\n      \"minMs\": 0.014833,\n      \"maxMs\": 0.021875,\n      \"throughputMBps\": 59003.768571,\n      \"elapsedMs\": 8.474035,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"commit_4k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"262144\",\n      \"iters\": 1000,\n      \"bytesTotal\": \"262144000\",\n      \"avgMs\": 0.003711,\n      \"medianMs\": 0.003708,\n      \"p95Ms\": 0.004375,\n      \"minMs\": 0.003208,\n      \"maxMs\": 0.006834,\n      \"throughputMBps\": 67360.207362,\n      \"elapsedMs\": 3.71139,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": false,\n        \"commitEvery\": 4096,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/commit_4k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"262144\",\n      \"iters\": 1000,\n      \"bytesTotal\": \"262144000\",\n      \"avgMs\": 0.022511,\n      \"medianMs\": 0.020792,\n      \"p95Ms\": 0.026041,\n      \"minMs\": 0.017584,\n      \"maxMs\": 1.077,\n      \"throughputMBps\": 11105.825725,\n      \"elapsedMs\": 22.510708,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": false,\n        \"commitEvery\": 4096,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"large_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.323248,\n      \"medianMs\": 0.313833,\n      \"p95Ms\": 0.364875,\n      \"minMs\": 0.281583,\n      \"maxMs\": 0.826125,\n      \"throughputMBps\": 98995.088482,\n      \"elapsedMs\": 32.324836,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/large_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.345124,\n      \"medianMs\": 0.329667,\n      \"p95Ms\": 0.38,\n      \"minMs\": 0.290875,\n      \"maxMs\": 0.991666,\n      \"throughputMBps\": 92720.356737,\n      \"elapsedMs\": 34.512378,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"binary_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.341795,\n      \"medianMs\": 0.320834,\n      \"p95Ms\": 0.468,\n      \"minMs\": 0.290917,\n      \"maxMs\": 0.982041,\n      \"throughputMBps\": 93623.370734,\n      \"elapsedMs\": 34.1795,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.333031,\n      \"medianMs\": 0.329917,\n      \"p95Ms\": 0.373125,\n      \"minMs\": 0.297417,\n      \"maxMs\": 0.391792,\n      \"throughputMBps\": 96087.090456,\n      \"elapsedMs\": 33.303121,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"random_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.329251,\n      \"medianMs\": 0.325792,\n      \"p95Ms\": 0.362083,\n      \"minMs\": 0.30425,\n      \"maxMs\": 0.378292,\n      \"throughputMBps\": 97190.345561,\n      \"elapsedMs\": 32.925081,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.331988,\n      \"medianMs\": 0.33025,\n      \"p95Ms\": 0.374125,\n      \"minMs\": 0.289625,\n      \"maxMs\": 0.385334,\n      \"throughputMBps\": 96389.06095,\n      \"elapsedMs\": 33.198788,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"single_chunk_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.283677,\n      \"medianMs\": 0.268084,\n      \"p95Ms\": 0.298125,\n      \"minMs\": 0.246917,\n      \"maxMs\": 1.727,\n      \"throughputMBps\": 112804.29178,\n      \"elapsedMs\": 28.367715,\n      \"options\": {\n        \"chunkSize\": 33554432,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/single_chunk_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.276825,\n      \"medianMs\": 0.277375,\n      \"p95Ms\": 0.29725,\n      \"minMs\": 0.251209,\n      \"maxMs\": 0.322041,\n      \"throughputMBps\": 115596.663013,\n      \"elapsedMs\": 27.68246,\n      \"options\": {\n        \"chunkSize\": 33554432,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"huge_chunk_8mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"67108864\",\n      \"iters\": 100,\n      \"bytesTotal\": \"6710886400\",\n      \"avgMs\": 0.895062,\n      \"medianMs\": 0.852208,\n      \"p95Ms\": 1.153083,\n      \"minMs\": 0.697791,\n      \"maxMs\": 1.4935,\n      \"throughputMBps\": 71503.458533,\n      \"elapsedMs\": 89.50616,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/huge_chunk_8mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"67108864\",\n      \"iters\": 100,\n      \"bytesTotal\": \"6710886400\",\n      \"avgMs\": 0.859035,\n      \"medianMs\": 0.8215,\n      \"p95Ms\": 1.210875,\n      \"minMs\": 0.693125,\n      \"maxMs\": 1.474875,\n      \"throughputMBps\": 74502.235143,\n      \"elapsedMs\": 85.903463,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"very_large_128mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"134217728\",\n      \"iters\": 100,\n      \"bytesTotal\": \"13421772800\",\n      \"avgMs\": 1.474937,\n      \"medianMs\": 1.425,\n      \"p95Ms\": 1.820208,\n      \"minMs\": 1.301,\n      \"maxMs\": 3.190084,\n      \"throughputMBps\": 86783.361224,\n      \"elapsedMs\": 147.493711,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/very_large_128mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"134217728\",\n      \"iters\": 100,\n      \"bytesTotal\": \"13421772800\",\n      \"avgMs\": 1.488218,\n      \"medianMs\": 1.451709,\n      \"p95Ms\": 1.696209,\n      \"minMs\": 1.279375,\n      \"maxMs\": 3.401042,\n      \"throughputMBps\": 86008.885538,\n      \"elapsedMs\": 148.821833,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/latest-async-bench-run.json",
    "content": "{\n  \"runId\": \"2026-02-11T12:21:35.239Z\",\n  \"suite\": \"async\",\n  \"args\": [\"--json=src/benchmark/latest-async-bench-run.json\"],\n  \"results\": [\n    {\n      \"name\": \"async/small_64k_fast\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 50,\n      \"bytesTotal\": \"3276800\",\n      \"elapsedMs\": 90.32408000000005,\n      \"throughputMBps\": 34.597639964890845,\n      \"avgIterMs\": 1.806481600000001,\n      \"medianIterMs\": 2.267916000000014,\n      \"p95IterMs\": 2.3155410000000103,\n      \"minIterMs\": 1.122000000000014,\n      \"maxIterMs\": 3.7874999999999943,\n      \"asyncDelay\": {\n        \"minMs\": 1,\n        \"maxMs\": 3\n      },\n      \"peakInFlightSpans\": 1,\n      \"peakChunks\": 0,\n      \"memory\": {\n        \"start\": {\n          \"rss\": 85508096,\n          \"heapTotal\": 3651584,\n          \"heapUsed\": 36386529,\n          \"external\": 34698241,\n          \"arrayBuffers\": 16777328\n        },\n        \"end\": {\n          \"rss\": 106184704,\n          \"heapTotal\": 3904512,\n          \"heapUsed\": 42669399,\n          \"external\": 41696337,\n          \"arrayBuffers\": 23667428\n        },\n        \"peak\": {\n          \"rss\": 106184704,\n          \"heapTotal\": 3904512,\n          \"heapUsed\": 42669399,\n          \"external\": 41696337,\n          \"arrayBuffers\": 23667428\n        }\n      },\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2\n      }\n    },\n    {\n      \"name\": \"async/small_64k_slow\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 50,\n      \"bytesTotal\": \"3276800\",\n      \"elapsedMs\": 398.99471300000016,\n      \"throughputMBps\": 7.832183981846393,\n      \"avgIterMs\": 7.979894260000004,\n      \"medianIterMs\": 8.026458999999988,\n      \"p95IterMs\": 10.204833000000008,\n      \"minIterMs\": 5.447375000000022,\n      \"maxIterMs\": 10.733834000000002,\n      \"asyncDelay\": {\n        \"minMs\": 5,\n        \"maxMs\": 10\n      },\n      \"peakInFlightSpans\": 1,\n      \"peakChunks\": 0,\n      \"memory\": {\n        \"start\": {\n          \"rss\": 106217472,\n          \"heapTotal\": 3904512,\n          \"heapUsed\": 42669399,\n          \"external\": 41696337,\n          \"arrayBuffers\": 23667428\n        },\n        \"end\": {\n          \"rss\": 107298816,\n          \"heapTotal\": 3938304,\n          \"heapUsed\": 50755265,\n          \"external\": 48594555,\n          \"arrayBuffers\": 30557528\n        },\n        \"peak\": {\n          \"rss\": 107298816,\n          \"heapTotal\": 3938304,\n          \"heapUsed\": 50755265,\n          \"external\": 48594555,\n          \"arrayBuffers\": 30557528\n        }\n      },\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2\n      }\n    },\n    {\n      \"name\": \"async/medium_256k_mixed\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"262144\",\n      \"iters\": 50,\n      \"bytesTotal\": \"13107200\",\n      \"elapsedMs\": 426.60487300000034,\n      \"throughputMBps\": 29.301118648965808,\n      \"avgIterMs\": 8.532097460000006,\n      \"medianIterMs\": 9.055917000000022,\n      \"p95IterMs\": 9.720542000000023,\n      \"minIterMs\": 5.085083000000054,\n      \"maxIterMs\": 9.963625000000093,\n      \"asyncDelay\": {\n        \"minMs\": 1,\n        \"maxMs\": 10\n      },\n      \"peakInFlightSpans\": 4,\n      \"peakChunks\": 0,\n      \"memory\": {\n        \"start\": {\n          \"rss\": 107298816,\n          \"heapTotal\": 3954688,\n          \"heapUsed\": 50755265,\n          \"external\": 48594555,\n          \"arrayBuffers\": 30557528\n        },\n        \"end\": {\n          \"rss\": 109232128,\n          \"heapTotal\": 4008960,\n          \"heapUsed\": 63963835,\n          \"external\": 62095303,\n          \"arrayBuffers\": 44018228\n        },\n        \"peak\": {\n          \"rss\": 109232128,\n          \"heapTotal\": 3992576,\n          \"heapUsed\": 63963835,\n          \"external\": 62095303,\n          \"arrayBuffers\": 44018228\n        }\n      },\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2\n      }\n    },\n    {\n      \"name\": \"async/large_1mb_fast\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 10,\n      \"bytesTotal\": \"10485760\",\n      \"elapsedMs\": 24.028251999999952,\n      \"throughputMBps\": 416.17675726057894,\n      \"avgIterMs\": 2.402825199999995,\n      \"medianIterMs\": 2.420042000000194,\n      \"p95IterMs\": 3.1322499999998854,\n      \"minIterMs\": 2.0450419999999667,\n      \"maxIterMs\": 3.1322499999998854,\n      \"asyncDelay\": {\n        \"minMs\": 1,\n        \"maxMs\": 3\n      },\n      \"peakInFlightSpans\": 16,\n      \"peakChunks\": 0,\n      \"memory\": {\n        \"start\": {\n          \"rss\": 109232128,\n          \"heapTotal\": 4025344,\n          \"heapUsed\": 63963835,\n          \"external\": 62095303,\n          \"arrayBuffers\": 44018228\n        },\n        \"end\": {\n          \"rss\": 112050176,\n          \"heapTotal\": 4176896,\n          \"heapUsed\": 75115555,\n          \"external\": 72681683,\n          \"arrayBuffers\": 54590608\n        },\n        \"peak\": {\n          \"rss\": 112050176,\n          \"heapTotal\": 4176896,\n          \"heapUsed\": 75115555,\n          \"external\": 72681683,\n          \"arrayBuffers\": 54590608\n        }\n      },\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2\n      }\n    },\n    {\n      \"name\": \"async/large_1mb_slow\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 10,\n      \"bytesTotal\": \"10485760\",\n      \"elapsedMs\": 92.5455830000003,\n      \"throughputMBps\": 108.05485984133858,\n      \"avgIterMs\": 9.254558300000031,\n      \"medianIterMs\": 9.197959000000083,\n      \"p95IterMs\": 9.66429200000016,\n      \"minIterMs\": 8.976333000000068,\n      \"maxIterMs\": 9.66429200000016,\n      \"asyncDelay\": {\n        \"minMs\": 5,\n        \"maxMs\": 10\n      },\n      \"peakInFlightSpans\": 16,\n      \"peakChunks\": 0,\n      \"memory\": {\n        \"start\": {\n          \"rss\": 112050176,\n          \"heapTotal\": 4176896,\n          \"heapUsed\": 75115555,\n          \"external\": 72681683,\n          \"arrayBuffers\": 54590608\n        },\n        \"end\": {\n          \"rss\": 115736576,\n          \"heapTotal\": 4176896,\n          \"heapUsed\": 83616492,\n          \"external\": 83261768,\n          \"arrayBuffers\": 65162988\n        },\n        \"peak\": {\n          \"rss\": 115736576,\n          \"heapTotal\": 4176896,\n          \"heapUsed\": 83616492,\n          \"external\": 83261768,\n          \"arrayBuffers\": 65162988\n        }\n      },\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2\n      }\n    },\n    {\n      \"name\": \"async/write_256k_mixed\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"262144\",\n      \"iters\": 50,\n      \"bytesTotal\": \"13107200\",\n      \"elapsedMs\": 415.5097070000011,\n      \"throughputMBps\": 30.083533042466247,\n      \"avgIterMs\": 8.310194140000021,\n      \"medianIterMs\": 8.492375000000038,\n      \"p95IterMs\": 9.870334000000184,\n      \"minIterMs\": 5.02412500000014,\n      \"maxIterMs\": 10.184166000000005,\n      \"asyncDelay\": {\n        \"minMs\": 1,\n        \"maxMs\": 10\n      },\n      \"peakInFlightSpans\": 4,\n      \"peakChunks\": 0,\n      \"memory\": {\n        \"start\": {\n          \"rss\": 115736576,\n          \"heapTotal\": 4176896,\n          \"heapUsed\": 83616492,\n          \"external\": 83261768,\n          \"arrayBuffers\": 65162988\n        },\n        \"end\": {\n          \"rss\": 116080640,\n          \"heapTotal\": 4230144,\n          \"heapUsed\": 99294516,\n          \"external\": 96723492,\n          \"arrayBuffers\": 78623688\n        },\n        \"peak\": {\n          \"rss\": 116080640,\n          \"heapTotal\": 4231168,\n          \"heapUsed\": 99294516,\n          \"external\": 96723492,\n          \"arrayBuffers\": 78623688\n        }\n      },\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2\n      }\n    },\n    {\n      \"name\": \"async/tiny_chunks_slow\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 50,\n      \"bytesTotal\": \"3276800\",\n      \"elapsedMs\": 462.2234600000004,\n      \"throughputMBps\": 6.7607992030521284,\n      \"avgIterMs\": 9.244469200000008,\n      \"medianIterMs\": 9.15825000000018,\n      \"p95IterMs\": 9.618750000000091,\n      \"minIterMs\": 8.903291999999965,\n      \"maxIterMs\": 12.277291000000105,\n      \"asyncDelay\": {\n        \"minMs\": 5,\n        \"maxMs\": 10\n      },\n      \"peakInFlightSpans\": 16,\n      \"peakChunks\": 0,\n      \"memory\": {\n        \"start\": {\n          \"rss\": 116080640,\n          \"heapTotal\": 4246528,\n          \"heapUsed\": 99294516,\n          \"external\": 96723492,\n          \"arrayBuffers\": 78623688\n        },\n        \"end\": {\n          \"rss\": 118767616,\n          \"heapTotal\": 4505600,\n          \"heapUsed\": 103304662,\n          \"external\": 100442086,\n          \"arrayBuffers\": 82339238\n        },\n        \"peak\": {\n          \"rss\": 118767616,\n          \"heapTotal\": 4505600,\n          \"heapUsed\": 103304662,\n          \"external\": 100442086,\n          \"arrayBuffers\": 82339238\n        }\n      },\n      \"options\": {\n        \"chunkSize\": 4096,\n        \"initialChunks\": 1\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/latest-default-bench-run.json",
    "content": "{\n  \"runId\": \"2026-02-11T12:21:27.641Z\",\n  \"suite\": \"default\",\n  \"args\": [\"--suite=default\", \"--json=src/benchmark/latest-default-bench-run.json\"],\n  \"results\": [\n    {\n      \"name\": \"ansi_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.00224,\n      \"medianMs\": 0.002042,\n      \"p95Ms\": 0.002709,\n      \"minMs\": 0.001708,\n      \"maxMs\": 0.900208,\n      \"throughputMBps\": 27902.696289,\n      \"elapsedMs\": 44.798538,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ansi\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/ansi_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005808,\n      \"medianMs\": 0.005708,\n      \"p95Ms\": 0.006959,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.261917,\n      \"throughputMBps\": 10761.933807,\n      \"elapsedMs\": 116.150129,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ansi\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"ascii_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001991,\n      \"medianMs\": 0.001917,\n      \"p95Ms\": 0.002416,\n      \"minMs\": 0.001666,\n      \"maxMs\": 0.595709,\n      \"throughputMBps\": 31395.354211,\n      \"elapsedMs\": 39.814808,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/ascii_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005936,\n      \"medianMs\": 0.005834,\n      \"p95Ms\": 0.007125,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.781625,\n      \"throughputMBps\": 10528.991173,\n      \"elapsedMs\": 118.719826,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"binary_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001989,\n      \"medianMs\": 0.001875,\n      \"p95Ms\": 0.002458,\n      \"minMs\": 0.001666,\n      \"maxMs\": 0.581458,\n      \"throughputMBps\": 31415.936621,\n      \"elapsedMs\": 39.788723,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005774,\n      \"medianMs\": 0.005667,\n      \"p95Ms\": 0.007041,\n      \"minMs\": 0.003959,\n      \"maxMs\": 0.022416,\n      \"throughputMBps\": 10825.161253,\n      \"elapsedMs\": 115.471721,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"random_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001953,\n      \"medianMs\": 0.001917,\n      \"p95Ms\": 0.002292,\n      \"minMs\": 0.001666,\n      \"maxMs\": 0.606041,\n      \"throughputMBps\": 31995.902067,\n      \"elapsedMs\": 39.067503,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005528,\n      \"medianMs\": 0.005334,\n      \"p95Ms\": 0.006583,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.645333,\n      \"throughputMBps\": 11305.272381,\n      \"elapsedMs\": 110.56788,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"medium_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.014789,\n      \"medianMs\": 0.014292,\n      \"p95Ms\": 0.017709,\n      \"minMs\": 0.012459,\n      \"maxMs\": 0.04375,\n      \"throughputMBps\": 67619.762714,\n      \"elapsedMs\": 7.394288,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/medium_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.016742,\n      \"medianMs\": 0.01675,\n      \"p95Ms\": 0.018792,\n      \"minMs\": 0.01475,\n      \"maxMs\": 0.028792,\n      \"throughputMBps\": 59729.057051,\n      \"elapsedMs\": 8.371135,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"binary_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.013787,\n      \"medianMs\": 0.014,\n      \"p95Ms\": 0.01525,\n      \"minMs\": 0.012291,\n      \"maxMs\": 0.019917,\n      \"throughputMBps\": 72533.073994,\n      \"elapsedMs\": 6.893407,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.016692,\n      \"medianMs\": 0.016875,\n      \"p95Ms\": 0.01775,\n      \"minMs\": 0.01475,\n      \"maxMs\": 0.022166,\n      \"throughputMBps\": 59907.739685,\n      \"elapsedMs\": 8.346167,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"random_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.014075,\n      \"medianMs\": 0.014084,\n      \"p95Ms\": 0.015291,\n      \"minMs\": 0.012292,\n      \"maxMs\": 0.023083,\n      \"throughputMBps\": 71047.947276,\n      \"elapsedMs\": 7.037501,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.0183,\n      \"medianMs\": 0.016792,\n      \"p95Ms\": 0.017917,\n      \"minMs\": 0.014791,\n      \"maxMs\": 0.773,\n      \"throughputMBps\": 54645.232766,\n      \"elapsedMs\": 9.149929,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"commit_4k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"262144\",\n      \"iters\": 1000,\n      \"bytesTotal\": \"262144000\",\n      \"avgMs\": 0.003716,\n      \"medianMs\": 0.003708,\n      \"p95Ms\": 0.004334,\n      \"minMs\": 0.003166,\n      \"maxMs\": 0.012375,\n      \"throughputMBps\": 67271.645055,\n      \"elapsedMs\": 3.716276,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": false,\n        \"commitEvery\": 4096,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/commit_4k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"262144\",\n      \"iters\": 1000,\n      \"bytesTotal\": \"262144000\",\n      \"avgMs\": 0.021818,\n      \"medianMs\": 0.020584,\n      \"p95Ms\": 0.025625,\n      \"minMs\": 0.017583,\n      \"maxMs\": 0.473125,\n      \"throughputMBps\": 11458.455079,\n      \"elapsedMs\": 21.81795,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": false,\n        \"commitEvery\": 4096,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"large_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.314456,\n      \"medianMs\": 0.310625,\n      \"p95Ms\": 0.363083,\n      \"minMs\": 0.283791,\n      \"maxMs\": 0.480417,\n      \"throughputMBps\": 101762.970319,\n      \"elapsedMs\": 31.445623,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/large_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.3235,\n      \"medianMs\": 0.321792,\n      \"p95Ms\": 0.368,\n      \"minMs\": 0.288084,\n      \"maxMs\": 0.384708,\n      \"throughputMBps\": 98918.202714,\n      \"elapsedMs\": 32.349961,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"binary_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.315446,\n      \"medianMs\": 0.31475,\n      \"p95Ms\": 0.358458,\n      \"minMs\": 0.274083,\n      \"maxMs\": 0.379292,\n      \"throughputMBps\": 101443.71847,\n      \"elapsedMs\": 31.544585,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.320364,\n      \"medianMs\": 0.31725,\n      \"p95Ms\": 0.351125,\n      \"minMs\": 0.281292,\n      \"maxMs\": 0.483208,\n      \"throughputMBps\": 99886.335593,\n      \"elapsedMs\": 32.036414,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"random_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.318342,\n      \"medianMs\": 0.3145,\n      \"p95Ms\": 0.358417,\n      \"minMs\": 0.277333,\n      \"maxMs\": 0.38525,\n      \"throughputMBps\": 100520.807729,\n      \"elapsedMs\": 31.834205,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.330148,\n      \"medianMs\": 0.324875,\n      \"p95Ms\": 0.374,\n      \"minMs\": 0.304584,\n      \"maxMs\": 0.508042,\n      \"throughputMBps\": 96926.118399,\n      \"elapsedMs\": 33.014837,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"single_chunk_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.311759,\n      \"medianMs\": 0.264959,\n      \"p95Ms\": 0.47725,\n      \"minMs\": 0.246416,\n      \"maxMs\": 2.159916,\n      \"throughputMBps\": 102643.473604,\n      \"elapsedMs\": 31.175874,\n      \"options\": {\n        \"chunkSize\": 33554432,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/single_chunk_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.306392,\n      \"medianMs\": 0.275958,\n      \"p95Ms\": 0.475083,\n      \"minMs\": 0.251125,\n      \"maxMs\": 0.784,\n      \"throughputMBps\": 104441.345365,\n      \"elapsedMs\": 30.639207,\n      \"options\": {\n        \"chunkSize\": 33554432,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"huge_chunk_8mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"67108864\",\n      \"iters\": 100,\n      \"bytesTotal\": \"6710886400\",\n      \"avgMs\": 0.843045,\n      \"medianMs\": 0.821875,\n      \"p95Ms\": 1.179208,\n      \"minMs\": 0.693583,\n      \"maxMs\": 1.368125,\n      \"throughputMBps\": 75915.246369,\n      \"elapsedMs\": 84.304541,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/huge_chunk_8mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"67108864\",\n      \"iters\": 100,\n      \"bytesTotal\": \"6710886400\",\n      \"avgMs\": 0.88421,\n      \"medianMs\": 0.859,\n      \"p95Ms\": 1.215625,\n      \"minMs\": 0.685417,\n      \"maxMs\": 1.415375,\n      \"throughputMBps\": 72380.99219,\n      \"elapsedMs\": 88.421004,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/latest-large-bench-run.json",
    "content": "{\n  \"runId\": \"2026-02-11T12:21:29.304Z\",\n  \"suite\": \"large\",\n  \"args\": [\"--suite=large\", \"--json=src/benchmark/latest-large-bench-run.json\"],\n  \"results\": [\n    {\n      \"name\": \"ansi_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.002221,\n      \"medianMs\": 0.002042,\n      \"p95Ms\": 0.00275,\n      \"minMs\": 0.001708,\n      \"maxMs\": 0.437333,\n      \"throughputMBps\": 28141.540331,\n      \"elapsedMs\": 44.418322,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ansi\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/ansi_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005636,\n      \"medianMs\": 0.005584,\n      \"p95Ms\": 0.006542,\n      \"minMs\": 0.004041,\n      \"maxMs\": 0.060875,\n      \"throughputMBps\": 11089.90406,\n      \"elapsedMs\": 112.715132,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ansi\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"ascii_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001947,\n      \"medianMs\": 0.001917,\n      \"p95Ms\": 0.002416,\n      \"minMs\": 0.001625,\n      \"maxMs\": 0.064958,\n      \"throughputMBps\": 32098.83112,\n      \"elapsedMs\": 38.942228,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/ascii_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005491,\n      \"medianMs\": 0.005458,\n      \"p95Ms\": 0.006334,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.019542,\n      \"throughputMBps\": 11382.521104,\n      \"elapsedMs\": 109.817499,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"binary_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001937,\n      \"medianMs\": 0.001917,\n      \"p95Ms\": 0.00225,\n      \"minMs\": 0.001666,\n      \"maxMs\": 0.016125,\n      \"throughputMBps\": 32274.273281,\n      \"elapsedMs\": 38.730539,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005495,\n      \"medianMs\": 0.005417,\n      \"p95Ms\": 0.006375,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.019042,\n      \"throughputMBps\": 11374.657062,\n      \"elapsedMs\": 109.893423,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"random_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.00198,\n      \"medianMs\": 0.001958,\n      \"p95Ms\": 0.002458,\n      \"minMs\": 0.001625,\n      \"maxMs\": 0.045708,\n      \"throughputMBps\": 31563.29968,\n      \"elapsedMs\": 39.602957,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005517,\n      \"medianMs\": 0.005458,\n      \"p95Ms\": 0.006416,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.028625,\n      \"throughputMBps\": 11329.447285,\n      \"elapsedMs\": 110.331949,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"medium_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.016012,\n      \"medianMs\": 0.014833,\n      \"p95Ms\": 0.018708,\n      \"minMs\": 0.012416,\n      \"maxMs\": 0.400333,\n      \"throughputMBps\": 62451.529806,\n      \"elapsedMs\": 8.006209,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/medium_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.018703,\n      \"medianMs\": 0.016917,\n      \"p95Ms\": 0.019583,\n      \"minMs\": 0.015625,\n      \"maxMs\": 0.734875,\n      \"throughputMBps\": 53468.244408,\n      \"elapsedMs\": 9.351345,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"binary_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.014773,\n      \"medianMs\": 0.014166,\n      \"p95Ms\": 0.01525,\n      \"minMs\": 0.012334,\n      \"maxMs\": 0.388708,\n      \"throughputMBps\": 67690.233247,\n      \"elapsedMs\": 7.38659,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.018841,\n      \"medianMs\": 0.0175,\n      \"p95Ms\": 0.018833,\n      \"minMs\": 0.015333,\n      \"maxMs\": 0.743542,\n      \"throughputMBps\": 53076.691254,\n      \"elapsedMs\": 9.420331,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"random_1mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.013879,\n      \"medianMs\": 0.014,\n      \"p95Ms\": 0.014959,\n      \"minMs\": 0.012292,\n      \"maxMs\": 0.019959,\n      \"throughputMBps\": 72051.684691,\n      \"elapsedMs\": 6.939463,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_1mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"1048576\",\n      \"iters\": 500,\n      \"bytesTotal\": \"524288000\",\n      \"avgMs\": 0.018285,\n      \"medianMs\": 0.017,\n      \"p95Ms\": 0.018,\n      \"minMs\": 0.014792,\n      \"maxMs\": 0.757667,\n      \"throughputMBps\": 54688.876622,\n      \"elapsedMs\": 9.142627,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"commit_4k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"262144\",\n      \"iters\": 1000,\n      \"bytesTotal\": \"262144000\",\n      \"avgMs\": 0.003647,\n      \"medianMs\": 0.003666,\n      \"p95Ms\": 0.004208,\n      \"minMs\": 0.003166,\n      \"maxMs\": 0.007041,\n      \"throughputMBps\": 68548.252212,\n      \"elapsedMs\": 3.647066,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": false,\n        \"commitEvery\": 4096,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"write/commit_4k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"262144\",\n      \"iters\": 1000,\n      \"bytesTotal\": \"262144000\",\n      \"avgMs\": 0.022465,\n      \"medianMs\": 0.0205,\n      \"p95Ms\": 0.025583,\n      \"minMs\": 0.017666,\n      \"maxMs\": 0.913875,\n      \"throughputMBps\": 11128.457161,\n      \"elapsedMs\": 22.464929,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": false,\n        \"commitEvery\": 4096,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 1024\n        }\n      }\n    },\n    {\n      \"name\": \"large_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.3181,\n      \"medianMs\": 0.314625,\n      \"p95Ms\": 0.357625,\n      \"minMs\": 0.282167,\n      \"maxMs\": 0.385875,\n      \"throughputMBps\": 100597.29961,\n      \"elapsedMs\": 31.809999,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/large_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.323498,\n      \"medianMs\": 0.319417,\n      \"p95Ms\": 0.362292,\n      \"minMs\": 0.286916,\n      \"maxMs\": 0.387167,\n      \"throughputMBps\": 98918.829558,\n      \"elapsedMs\": 32.349756,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"binary_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.325211,\n      \"medianMs\": 0.320084,\n      \"p95Ms\": 0.374833,\n      \"minMs\": 0.287333,\n      \"maxMs\": 0.388375,\n      \"throughputMBps\": 98397.701061,\n      \"elapsedMs\": 32.521085,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.320437,\n      \"medianMs\": 0.316834,\n      \"p95Ms\": 0.362709,\n      \"minMs\": 0.284709,\n      \"maxMs\": 0.422709,\n      \"throughputMBps\": 99863.570759,\n      \"elapsedMs\": 32.043717,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"random_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.323457,\n      \"medianMs\": 0.319708,\n      \"p95Ms\": 0.370833,\n      \"minMs\": 0.286041,\n      \"maxMs\": 0.379708,\n      \"throughputMBps\": 98931.212108,\n      \"elapsedMs\": 32.345707,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.327069,\n      \"medianMs\": 0.324125,\n      \"p95Ms\": 0.377125,\n      \"minMs\": 0.278417,\n      \"maxMs\": 0.393333,\n      \"throughputMBps\": 97838.62253,\n      \"elapsedMs\": 32.70692,\n      \"options\": {\n        \"chunkSize\": 1048576,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"single_chunk_32mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.285571,\n      \"medianMs\": 0.271583,\n      \"p95Ms\": 0.297583,\n      \"minMs\": 0.246792,\n      \"maxMs\": 1.753625,\n      \"throughputMBps\": 112056.286433,\n      \"elapsedMs\": 28.557077,\n      \"options\": {\n        \"chunkSize\": 33554432,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/single_chunk_32mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"33554432\",\n      \"iters\": 100,\n      \"bytesTotal\": \"3355443200\",\n      \"avgMs\": 0.272651,\n      \"medianMs\": 0.272792,\n      \"p95Ms\": 0.299125,\n      \"minMs\": 0.250459,\n      \"maxMs\": 0.319875,\n      \"throughputMBps\": 117366.031901,\n      \"elapsedMs\": 27.265129,\n      \"options\": {\n        \"chunkSize\": 33554432,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"huge_chunk_8mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"67108864\",\n      \"iters\": 100,\n      \"bytesTotal\": \"6710886400\",\n      \"avgMs\": 0.831225,\n      \"medianMs\": 0.8115,\n      \"p95Ms\": 1.012959,\n      \"minMs\": 0.690917,\n      \"maxMs\": 1.299166,\n      \"throughputMBps\": 76994.794983,\n      \"elapsedMs\": 83.122502,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/huge_chunk_8mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"67108864\",\n      \"iters\": 100,\n      \"bytesTotal\": \"6710886400\",\n      \"avgMs\": 0.836986,\n      \"medianMs\": 0.8235,\n      \"p95Ms\": 1.081541,\n      \"minMs\": 0.684583,\n      \"maxMs\": 1.349,\n      \"throughputMBps\": 76464.816477,\n      \"elapsedMs\": 83.698625,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"very_large_128mb\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"134217728\",\n      \"iters\": 100,\n      \"bytesTotal\": \"13421772800\",\n      \"avgMs\": 1.454775,\n      \"medianMs\": 1.419458,\n      \"p95Ms\": 1.842208,\n      \"minMs\": 1.25225,\n      \"maxMs\": 2.212791,\n      \"throughputMBps\": 87986.142513,\n      \"elapsedMs\": 145.477454,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    },\n    {\n      \"name\": \"write/very_large_128mb\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"134217728\",\n      \"iters\": 100,\n      \"bytesTotal\": \"13421772800\",\n      \"avgMs\": 1.463544,\n      \"medianMs\": 1.422625,\n      \"p95Ms\": 1.853,\n      \"minMs\": 1.270916,\n      \"maxMs\": 2.295958,\n      \"throughputMBps\": 87458.923943,\n      \"elapsedMs\": 146.354419,\n      \"options\": {\n        \"chunkSize\": 8388608,\n        \"initialChunks\": 1,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 32768\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/latest-quick-bench-run.json",
    "content": "{\n  \"runId\": \"2026-02-11T12:21:26.541Z\",\n  \"suite\": \"quick\",\n  \"args\": [\"--suite=quick\", \"--json=src/benchmark/latest-quick-bench-run.json\"],\n  \"results\": [\n    {\n      \"name\": \"ansi_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.002408,\n      \"medianMs\": 0.002,\n      \"p95Ms\": 0.002709,\n      \"minMs\": 0.001708,\n      \"maxMs\": 0.900458,\n      \"throughputMBps\": 25949.773653,\n      \"elapsedMs\": 48.169977,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ansi\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/ansi_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.00543,\n      \"medianMs\": 0.005291,\n      \"p95Ms\": 0.00675,\n      \"minMs\": 0.004,\n      \"maxMs\": 0.064833,\n      \"throughputMBps\": 11510.384134,\n      \"elapsedMs\": 108.597592,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ansi\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"ascii_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001904,\n      \"medianMs\": 0.001833,\n      \"p95Ms\": 0.002416,\n      \"minMs\": 0.001625,\n      \"maxMs\": 0.016792,\n      \"throughputMBps\": 32821.490504,\n      \"elapsedMs\": 38.084803,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/ascii_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005445,\n      \"medianMs\": 0.005334,\n      \"p95Ms\": 0.006625,\n      \"minMs\": 0.003958,\n      \"maxMs\": 0.030542,\n      \"throughputMBps\": 11477.632101,\n      \"elapsedMs\": 108.907481,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"ascii\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"binary_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001943,\n      \"medianMs\": 0.001875,\n      \"p95Ms\": 0.0025,\n      \"minMs\": 0.001625,\n      \"maxMs\": 0.04125,\n      \"throughputMBps\": 32169.875886,\n      \"elapsedMs\": 38.856227,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/binary_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005312,\n      \"medianMs\": 0.005209,\n      \"p95Ms\": 0.006458,\n      \"minMs\": 0.003958,\n      \"maxMs\": 0.023334,\n      \"throughputMBps\": 11766.781598,\n      \"elapsedMs\": 106.231257,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"binary\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"random_64k\",\n      \"producerAPI\": \"reserve\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.001911,\n      \"medianMs\": 0.001916,\n      \"p95Ms\": 0.00225,\n      \"minMs\": 0.001666,\n      \"maxMs\": 0.011625,\n      \"throughputMBps\": 32712.320889,\n      \"elapsedMs\": 38.211902,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 64\n        }\n      }\n    },\n    {\n      \"name\": \"write/random_64k\",\n      \"producerAPI\": \"write\",\n      \"bytesPerIter\": \"65536\",\n      \"iters\": 20000,\n      \"bytesTotal\": \"1310720000\",\n      \"avgMs\": 0.005514,\n      \"medianMs\": 0.005416,\n      \"p95Ms\": 0.006666,\n      \"minMs\": 0.003959,\n      \"maxMs\": 0.063042,\n      \"throughputMBps\": 11335.096548,\n      \"elapsedMs\": 110.276961,\n      \"options\": {\n        \"chunkSize\": 65536,\n        \"initialChunks\": 2,\n        \"autoCommitOnFull\": true,\n        \"commitEvery\": 0,\n        \"reuseStream\": false,\n        \"pattern\": {\n          \"type\": \"random\",\n          \"size\": 64\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/markdown-benchmark.ts",
    "content": "#!/usr/bin/env bun\n\nimport { MarkdownRenderable, SyntaxStyle, createCliRenderer, parseColor } from \"../index\"\nimport { resolveRenderLib } from \"../zig\"\nimport { Command } from \"commander\"\nimport path from \"node:path\"\nimport { existsSync } from \"node:fs\"\nimport { mkdir, mkdtemp, readFile, unlink } from \"node:fs/promises\"\nimport { tmpdir } from \"node:os\"\n\nconst realStdoutWrite = process.stdout.write.bind(process.stdout)\nconst nativeLib = resolveRenderLib()\nconst nativeBuildOptions = nativeLib.getBuildOptions()\n\nconst WORDS = [\n  \"alpha\",\n  \"bravo\",\n  \"charlie\",\n  \"delta\",\n  \"echo\",\n  \"foxtrot\",\n  \"golf\",\n  \"hotel\",\n  \"india\",\n  \"juliet\",\n  \"kilo\",\n  \"lima\",\n  \"mango\",\n  \"nectar\",\n  \"oscar\",\n  \"papa\",\n  \"quartz\",\n  \"romeo\",\n  \"sierra\",\n  \"tango\",\n  \"uniform\",\n  \"vector\",\n  \"whiskey\",\n  \"xray\",\n  \"yankee\",\n  \"zulu\",\n  \"matrix\",\n  \"signal\",\n  \"tensor\",\n  \"render\",\n  \"schema\",\n  \"buffer\",\n  \"layout\",\n  \"stream\",\n  \"parser\",\n  \"syntax\",\n  \"viewport\",\n  \"cursor\",\n]\n\nconst CODE_WORDS = [\n  \"buffer\",\n  \"column\",\n  \"row\",\n  \"token\",\n  \"state\",\n  \"table\",\n  \"stream\",\n  \"render\",\n  \"result\",\n  \"index\",\n  \"value\",\n  \"output\",\n  \"content\",\n]\n\ntype MemorySample = {\n  rss: number\n  heapTotal: number\n  heapUsed: number\n  external: number\n  arrayBuffers: number\n}\n\ntype MemoryFieldStats = {\n  min: number\n  max: number\n  avg: number\n  median: number\n}\n\ntype MemoryStats = {\n  samples: number\n  start: MemorySample\n  end: MemorySample\n  delta: MemorySample\n  peak: MemorySample\n  fields: {\n    rss: MemoryFieldStats\n    heapTotal: MemoryFieldStats\n    heapUsed: MemoryFieldStats\n    external: MemoryFieldStats\n    arrayBuffers: MemoryFieldStats\n  }\n}\n\ntype NativeMemorySample = {\n  totalRequestedBytes: number\n  activeAllocations: number\n  smallAllocations: number\n  largeAllocations: number\n  requestedBytesValid: boolean\n}\n\ntype NativeMemoryStats = {\n  samples: number\n  start: NativeMemorySample\n  end: NativeMemorySample\n  delta: NativeMemorySample\n  peak: NativeMemorySample\n  requestedBytesReliable: boolean\n  fields: {\n    totalRequestedBytes: MemoryFieldStats\n    activeAllocations: MemoryFieldStats\n    smallAllocations: MemoryFieldStats\n    largeAllocations: MemoryFieldStats\n  }\n}\n\ntype TimingStats = {\n  count: number\n  averageMs: number\n  medianMs: number\n  p95Ms: number\n  minMs: number\n  maxMs: number\n  stdDevMs: number\n}\n\ntype ScenarioResult = {\n  name: string\n  description: string\n  iterations: number\n  warmupIterations: number\n  elapsedMs: number\n  category: \"parse\" | \"incremental\" | \"style\"\n  timingMode: \"content-set\" | \"style-refresh\"\n  updateStats: TimingStats\n  memoryStats?: MemoryStats\n  nativeMemoryStats?: NativeMemoryStats\n  contentStats: {\n    initialChars: number\n    finalChars: number\n    maxChars: number\n    updates: number\n    appendedChars: number\n  }\n  settings: Record<string, unknown>\n}\n\ntype StaticScenarioPlan = {\n  kind: \"static\"\n  name: string\n  description: string\n  iterations: number\n  warmupIterations: number\n  content: string\n  contentStats: Record<string, number>\n}\n\ntype StreamingScenarioPlan = {\n  kind: \"streaming\"\n  name: string\n  description: string\n  iterations: number\n  warmupIterations: number\n  baseContent: string\n  chunks: string[]\n  repeat: boolean\n  contentStats: Record<string, number>\n}\n\ntype StyleScenarioPlan = {\n  kind: \"style\"\n  name: string\n  description: string\n  iterations: number\n  warmupIterations: number\n  content: string\n  contentStats: Record<string, number>\n}\n\ntype ScenarioPlan = StaticScenarioPlan | StreamingScenarioPlan | StyleScenarioPlan\n\ntype StreamState = {\n  content: string\n  chunks: string[]\n  cursor: number\n  repeat: boolean\n  maxChars: number\n  maxContentChars: number\n  done: boolean\n}\n\ntype RunContext = {\n  renderer: Awaited<ReturnType<typeof createCliRenderer>>\n  markdown: MarkdownRenderable\n  syntaxStyleA: SyntaxStyle\n  syntaxStyleB: SyntaxStyle\n  streamIntervalMs: number\n  chunkLines: number\n  maxChars: number\n  memInterval: number\n  memSampleEvery: number\n}\n\ntype SuiteConfig = {\n  iterations: number\n  warmupIterations: number\n  longIterations: number\n  scale: number\n}\n\ntype MemorySampler = {\n  jsSamples: MemorySample[]\n  nativeSamples: NativeMemorySample[]\n  recordIteration: (iteration: number) => void\n  stop: () => void\n}\n\ntype StaticScenarioConfig = {\n  title: string\n  sections: number\n  paragraphsPerSection: number\n  sentencesPerParagraph: number\n  lists: number\n  listItems: number\n  tables: number\n  tableRows: number\n  tableCols: number\n  codeBlocks: number\n  codeLines: number\n}\n\ntype StreamingScenarioConfig = StaticScenarioConfig & {\n  repeat: boolean\n}\n\ntype OutputMeta = {\n  suiteName: string\n  targetFps: number\n  maxFps: number\n  iterations: number\n  warmupIterations: number\n  longIterations: number\n  streamIntervalMs: number\n  chunkLines: number\n  maxChars: number\n  scale: number\n  seed: number\n  memInterval: number\n  memSampleEvery: number\n  gpaSafeStats: boolean\n  gpaMemoryLimitTracking: boolean\n}\n\nconst program = new Command()\nprogram\n  .name(\"markdown-benchmark\")\n  .description(\"MarkdownRenderable benchmark scenarios (frame-independent)\")\n  .option(\"-s, --suite <name>\", \"benchmark suite: quick, default, long\", \"default\")\n  .option(\"-i, --iterations <count>\", \"iterations per scenario\", \"1000\")\n  .option(\"--warmup-iterations <count>\", \"warmup iterations per scenario\", \"50\")\n  .option(\"--long-iterations <count>\", \"iterations for long streaming scenario\", \"3000\")\n  .option(\"--target-fps <fps>\", \"renderer target fps\", \"60\")\n  .option(\"--max-fps <fps>\", \"renderer max fps\", \"60\")\n  .option(\"--mem-interval <ms>\", \"time-based memory sampling in ms (0 disables)\", \"0\")\n  .option(\"--mem-sample-every <count>\", \"sample memory every N iterations (0 disables)\", \"10\")\n  .option(\"--stream-interval <ms>\", \"stream update interval in ms\", \"0\")\n  .option(\"--chunk-lines <count>\", \"lines appended per stream tick\", \"4\")\n  .option(\"--max-chars <count>\", \"max streaming content size before stopping growth\", \"5000000\")\n  .option(\"--scale <n>\", \"scale content size\", \"1\")\n  .option(\"--seed <n>\", \"seed for deterministic content\", \"1337\")\n  .option(\"--json [path]\", \"write JSON results to file\")\n  .option(\"--no-testing\", \"use production renderer (outputs to terminal)\")\n  .option(\"--scenario <name>\", \"run a single scenario\")\n  .option(\"--no-spawn-per-scenario\", \"run all scenarios in a single process\")\n  .option(\"--no-output\", \"suppress stdout output\")\n  .parse(process.argv)\n\nconst options = program.opts()\n\nconst suiteName = String(options.suite)\nconst iterations = Math.max(1, Math.floor(toNumber(options.iterations, 1000)))\nconst warmupIterations = Math.max(0, Math.floor(toNumber(options.warmupIterations, 50)))\nconst longIterations = Math.max(iterations, Math.floor(toNumber(options.longIterations, 3000)))\nconst targetFps = toNumber(options.targetFps, 60)\nconst maxFps = toNumber(options.maxFps, 60)\nconst memInterval = Math.max(0, Math.floor(toNumber(options.memInterval, 0)))\nconst memSampleEvery = Math.max(0, Math.floor(toNumber(options.memSampleEvery, 10)))\nconst streamIntervalMs = Math.max(0, Math.floor(toNumber(options.streamInterval, 0)))\nconst chunkLines = Math.max(1, Math.floor(toNumber(options.chunkLines, 4)))\nconst maxChars = Math.max(0, Math.floor(toNumber(options.maxChars, 5000000)))\nconst scale = Math.max(0.25, toNumber(options.scale, 1))\nconst seed = Math.max(1, Math.floor(toNumber(options.seed, 1337)))\nconst testing = options.testing !== false\nconst outputEnabled = options.output !== false\nconst scenarioFilter = options.scenario ? String(options.scenario) : null\nconst spawnPerScenario = options.spawnPerScenario !== false\n\nconst jsonArg = options.json\nconst jsonPath =\n  typeof jsonArg === \"string\"\n    ? path.resolve(process.cwd(), jsonArg)\n    : jsonArg\n      ? path.resolve(process.cwd(), \"latest-markdown-bench-run.json\")\n      : null\n\nif (jsonPath) {\n  const dir = path.dirname(jsonPath)\n  if (!existsSync(dir)) {\n    await mkdir(dir, { recursive: true })\n  }\n  if (existsSync(jsonPath)) {\n    console.error(`Error: output file already exists: ${jsonPath}`)\n    process.exit(1)\n  }\n}\n\nconst scenarios = createScenarios(\n  suiteName,\n  {\n    iterations,\n    warmupIterations,\n    longIterations,\n    scale,\n  },\n  seed,\n)\n\nconst filteredScenarios = scenarioFilter ? scenarios.filter((scenario) => scenario.name === scenarioFilter) : scenarios\n\nif (scenarioFilter && filteredScenarios.length === 0) {\n  writeLine(`Unknown scenario: ${scenarioFilter}`)\n  process.exit(1)\n}\n\nif (filteredScenarios.length === 0) {\n  console.error(`Unknown suite: ${suiteName}`)\n  process.exit(1)\n}\n\nif (spawnPerScenario && !scenarioFilter) {\n  await runSpawnedScenarios(filteredScenarios)\n  process.exit(0)\n}\n\nprocess.env.OTUI_OVERRIDE_STDOUT = \"false\"\nprocess.env.OTUI_USE_ALTERNATE_SCREEN = \"false\"\n\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: true,\n  targetFps,\n  maxFps,\n  testing,\n  useAlternateScreen: false,\n  useConsole: false,\n  useMouse: false,\n})\n\nrenderer.disableStdoutInterception()\n\nrenderer.requestRender = () => {}\n\nconst syntaxStyleA = SyntaxStyle.fromStyles({\n  default: { fg: parseColor(\"#E6EDF3\") },\n  \"markup.heading\": { fg: parseColor(\"#88C0D0\"), bold: true },\n  \"markup.heading.1\": { fg: parseColor(\"#8FBCBB\"), bold: true },\n  \"markup.heading.2\": { fg: parseColor(\"#81A1C1\"), bold: true },\n  \"markup.heading.3\": { fg: parseColor(\"#5E81AC\"), bold: true },\n  \"markup.bold\": { fg: parseColor(\"#ECEFF4\"), bold: true },\n  \"markup.strong\": { fg: parseColor(\"#ECEFF4\"), bold: true },\n  \"markup.italic\": { fg: parseColor(\"#E5E9F0\"), italic: true },\n  \"markup.list\": { fg: parseColor(\"#B48EAD\") },\n  \"markup.raw\": { fg: parseColor(\"#A3BE8C\") },\n  \"markup.raw.block\": { fg: parseColor(\"#A3BE8C\") },\n  \"markup.raw.inline\": { fg: parseColor(\"#A3BE8C\") },\n  \"markup.link\": { fg: parseColor(\"#81A1C1\"), underline: true },\n  \"markup.link.label\": { fg: parseColor(\"#88C0D0\"), underline: true },\n  \"markup.link.url\": { fg: parseColor(\"#88C0D0\"), underline: true },\n  \"punctuation.special\": { fg: parseColor(\"#616E88\") },\n  conceal: { fg: parseColor(\"#4C566A\") },\n})\n\nconst syntaxStyleB = SyntaxStyle.fromStyles({\n  default: { fg: parseColor(\"#F8F8F2\") },\n  \"markup.heading\": { fg: parseColor(\"#A6E22E\"), bold: true },\n  \"markup.heading.1\": { fg: parseColor(\"#F92672\"), bold: true },\n  \"markup.heading.2\": { fg: parseColor(\"#66D9EF\"), bold: true },\n  \"markup.heading.3\": { fg: parseColor(\"#E6DB74\") },\n  \"markup.bold\": { fg: parseColor(\"#F8F8F2\"), bold: true },\n  \"markup.strong\": { fg: parseColor(\"#F8F8F2\"), bold: true },\n  \"markup.italic\": { fg: parseColor(\"#F8F8F2\"), italic: true },\n  \"markup.list\": { fg: parseColor(\"#F92672\") },\n  \"markup.raw\": { fg: parseColor(\"#E6DB74\") },\n  \"markup.raw.block\": { fg: parseColor(\"#E6DB74\") },\n  \"markup.raw.inline\": { fg: parseColor(\"#E6DB74\") },\n  \"markup.link\": { fg: parseColor(\"#66D9EF\"), underline: true },\n  \"markup.link.label\": { fg: parseColor(\"#E6DB74\"), underline: true },\n  \"markup.link.url\": { fg: parseColor(\"#66D9EF\"), underline: true },\n  \"punctuation.special\": { fg: parseColor(\"#75715E\") },\n  conceal: { fg: parseColor(\"#75715E\") },\n})\n\nconst markdown = new MarkdownRenderable(renderer, {\n  id: \"markdown-bench\",\n  content: \"\",\n  syntaxStyle: syntaxStyleA,\n  conceal: true,\n})\n\nconst ctx: RunContext = {\n  renderer,\n  markdown,\n  syntaxStyleA,\n  syntaxStyleB,\n  streamIntervalMs,\n  chunkLines,\n  maxChars,\n  memInterval,\n  memSampleEvery,\n}\n\nconst results: ScenarioResult[] = []\nconst scenarioLines: string[] = []\n\ntry {\n  for (let i = 0; i < filteredScenarios.length; i += 1) {\n    const plan = filteredScenarios[i]\n    const result = await runScenario(plan, ctx)\n    scenarioLines.push(formatScenarioResult(result))\n    results.push(result)\n  }\n} finally {\n  renderer.destroy()\n}\n\nawait outputResults(\n  {\n    suiteName,\n    targetFps,\n    maxFps,\n    iterations,\n    warmupIterations,\n    longIterations,\n    streamIntervalMs,\n    chunkLines,\n    maxChars,\n    scale,\n    seed,\n    memInterval,\n    memSampleEvery,\n    gpaSafeStats: nativeBuildOptions.gpaSafeStats,\n    gpaMemoryLimitTracking: nativeBuildOptions.gpaMemoryLimitTracking,\n  },\n  results,\n  scenarioLines,\n  outputEnabled,\n  jsonPath,\n)\n\nfunction createScenarios(suite: string, config: SuiteConfig, runSeed: number): ScenarioPlan[] {\n  const rng = createRng(runSeed)\n  const baseIterations = config.iterations\n  const baseWarmup = config.warmupIterations\n  const longIterations = config.longIterations\n\n  const staticSmallDoc = buildMarkdownDocument(rng, {\n    title: \"Markdown Static Small\",\n    sections: scaled(3, config.scale),\n    paragraphsPerSection: scaled(2, config.scale),\n    sentencesPerParagraph: 3,\n    lists: scaled(2, config.scale),\n    listItems: 6,\n    tables: scaled(2, config.scale),\n    tableRows: scaled(12, config.scale),\n    tableCols: 4,\n    codeBlocks: scaled(2, config.scale),\n    codeLines: 8,\n  })\n\n  const staticLargeDoc = buildMarkdownDocument(rng, {\n    title: \"Markdown Static Large\",\n    sections: scaled(6, config.scale),\n    paragraphsPerSection: scaled(3, config.scale),\n    sentencesPerParagraph: 4,\n    lists: scaled(3, config.scale),\n    listItems: 8,\n    tables: scaled(6, config.scale),\n    tableRows: scaled(40, config.scale),\n    tableCols: 6,\n    codeBlocks: scaled(4, config.scale),\n    codeLines: 14,\n  })\n\n  const tableOnlySmallDoc = buildTableOnlyDocument(rng, {\n    title: \"Markdown Tables Only Small\",\n    tables: scaled(3, config.scale),\n    rows: scaled(24, config.scale),\n    cols: 4,\n  })\n\n  const tableOnlyLargeDoc = buildTableOnlyDocument(rng, {\n    title: \"Markdown Tables Only Large\",\n    tables: scaled(8, config.scale),\n    rows: scaled(60, config.scale),\n    cols: 6,\n  })\n\n  const codeOnlySmallDoc = buildCodeOnlyDocument(rng, {\n    title: \"Markdown Code Only Small\",\n    blocks: scaled(8, config.scale),\n    lines: scaled(10, config.scale),\n  })\n\n  const codeOnlyLargeDoc = buildCodeOnlyDocument(rng, {\n    title: \"Markdown Code Only Large\",\n    blocks: scaled(24, config.scale),\n    lines: scaled(16, config.scale),\n  })\n\n  const headingsOnlyDoc = buildHeadingsOnlyDocument(rng, {\n    title: \"Markdown Headings Only\",\n    headings: scaled(200, config.scale),\n    depthMin: 2,\n    depthMax: 4,\n    wordsPerHeading: 4,\n  })\n\n  const streamMixedConfig = {\n    title: \"Markdown Stream Mixed\",\n    sections: scaled(5, config.scale),\n    paragraphsPerSection: scaled(2, config.scale),\n    sentencesPerParagraph: 3,\n    lists: scaled(2, config.scale),\n    listItems: 6,\n    tables: scaled(4, config.scale),\n    tableRows: scaled(18, config.scale),\n    tableCols: 5,\n    codeBlocks: scaled(3, config.scale),\n    codeLines: 10,\n    repeat: true,\n  }\n\n  const streamTablesConfig = {\n    title: \"Markdown Stream Tables Long\",\n    sections: scaled(4, config.scale),\n    paragraphsPerSection: scaled(1, config.scale),\n    sentencesPerParagraph: 2,\n    lists: scaled(1, config.scale),\n    listItems: 4,\n    tables: scaled(8, config.scale),\n    tableRows: scaled(50, config.scale),\n    tableCols: 6,\n    codeBlocks: scaled(2, config.scale),\n    codeLines: 8,\n    repeat: true,\n  }\n\n  const streamMixedBase = buildMarkdownDocument(rng, {\n    ...streamMixedConfig,\n    tables: Math.max(1, Math.floor(streamMixedConfig.tables / 2)),\n    tableRows: Math.max(6, Math.floor(streamMixedConfig.tableRows / 2)),\n    codeBlocks: Math.max(1, Math.floor(streamMixedConfig.codeBlocks / 2)),\n  })\n\n  const streamTablesBase = buildMarkdownDocument(rng, {\n    ...streamTablesConfig,\n    tables: Math.max(1, Math.floor(streamTablesConfig.tables / 2)),\n    tableRows: Math.max(6, Math.floor(streamTablesConfig.tableRows / 2)),\n    codeBlocks: Math.max(1, Math.floor(streamTablesConfig.codeBlocks / 2)),\n  })\n\n  const streamMixedChunks = buildStreamingChunks(rng, streamMixedConfig)\n  const streamTablesChunks = buildStreamingChunks(rng, streamTablesConfig)\n\n  const tableRowStreamCols = Math.max(3, scaled(6, config.scale))\n  const tableRowStreamBaseRows = 1\n  const tableRowStreamRows = Math.max(200, scaled(400, config.scale))\n  const tableRowStreamBaseLines = makeTableLines(rng, tableRowStreamCols, tableRowStreamBaseRows)\n  const tableRowStreamBaseContent = `# Stream Table Rows\\n\\n${tableRowStreamBaseLines.join(\"\\n\")}\\n`\n  const tableRowStreamChunks = buildTableRowChunks(rng, {\n    rows: tableRowStreamRows,\n    cols: tableRowStreamCols,\n  })\n\n  const codeBlockStreamBlocks = Math.max(40, scaled(80, config.scale))\n  const codeBlockStreamLines = Math.max(6, scaled(10, config.scale))\n  const codeBlockStreamBaseContent = \"# Stream Code Blocks\\n\\n\"\n  const codeBlockStreamChunks = buildCodeBlockChunks(rng, {\n    blocks: codeBlockStreamBlocks,\n    lines: codeBlockStreamLines,\n  })\n\n  const headingStreamCount = Math.max(200, scaled(400, config.scale))\n  const headingStreamBaseContent = \"# Stream Headings\\n\\n\"\n  const headingStreamChunks = buildHeadingChunks(rng, {\n    headings: headingStreamCount,\n    depthMin: 2,\n    depthMax: 4,\n    wordsPerHeading: 4,\n  })\n\n  const staticSmall: StaticScenarioPlan = {\n    kind: \"static\",\n    name: \"parse_small\",\n    description: \"Full parse/build on a small static document\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: staticSmallDoc.content,\n    contentStats: staticSmallDoc.stats,\n  }\n\n  const staticLarge: StaticScenarioPlan = {\n    kind: \"static\",\n    name: \"parse_large_tables\",\n    description: \"Full parse/build on a large, table-heavy document\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: staticLargeDoc.content,\n    contentStats: staticLargeDoc.stats,\n  }\n\n  const tableOnlySmall: StaticScenarioPlan = {\n    kind: \"static\",\n    name: \"parse_tables_only_small\",\n    description: \"Full parse/build on tables-only small document\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: tableOnlySmallDoc.content,\n    contentStats: tableOnlySmallDoc.stats,\n  }\n\n  const tableOnlyLarge: StaticScenarioPlan = {\n    kind: \"static\",\n    name: \"parse_tables_only_large\",\n    description: \"Full parse/build on tables-only large document\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: tableOnlyLargeDoc.content,\n    contentStats: tableOnlyLargeDoc.stats,\n  }\n\n  const codeOnlySmall: StaticScenarioPlan = {\n    kind: \"static\",\n    name: \"parse_code_only_small\",\n    description: \"Full parse/build on code-only document\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: codeOnlySmallDoc.content,\n    contentStats: codeOnlySmallDoc.stats,\n  }\n\n  const codeOnlyLarge: StaticScenarioPlan = {\n    kind: \"static\",\n    name: \"parse_code_only_large\",\n    description: \"Full parse/build on large code-only document\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: codeOnlyLargeDoc.content,\n    contentStats: codeOnlyLargeDoc.stats,\n  }\n\n  const headingsOnly: StaticScenarioPlan = {\n    kind: \"static\",\n    name: \"parse_headings_only\",\n    description: \"Full parse/build on headings-only document\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: headingsOnlyDoc.content,\n    contentStats: headingsOnlyDoc.stats,\n  }\n\n  const streamMixed: StreamingScenarioPlan = {\n    kind: \"streaming\",\n    name: \"incremental_mixed\",\n    description: \"Incremental parsing with mixed streamed content\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    baseContent: streamMixedBase.content,\n    chunks: streamMixedChunks,\n    repeat: true,\n    contentStats: {\n      ...streamMixedBase.stats,\n      streamTables: streamMixedConfig.tables,\n      streamTableRows: streamMixedConfig.tableRows,\n      streamTableCols: streamMixedConfig.tableCols,\n      streamLists: streamMixedConfig.lists,\n      streamListItems: streamMixedConfig.listItems,\n      streamCodeBlocks: streamMixedConfig.codeBlocks,\n      streamCodeLines: streamMixedConfig.codeLines,\n    },\n  }\n\n  const streamTablesLong: StreamingScenarioPlan = {\n    kind: \"streaming\",\n    name: \"incremental_tables_long\",\n    description: \"Long incremental run with large tables\",\n    iterations: longIterations,\n    warmupIterations: baseWarmup,\n    baseContent: streamTablesBase.content,\n    chunks: streamTablesChunks,\n    repeat: true,\n    contentStats: {\n      ...streamTablesBase.stats,\n      streamTables: streamTablesConfig.tables,\n      streamTableRows: streamTablesConfig.tableRows,\n      streamTableCols: streamTablesConfig.tableCols,\n      streamLists: streamTablesConfig.lists,\n      streamListItems: streamTablesConfig.listItems,\n      streamCodeBlocks: streamTablesConfig.codeBlocks,\n      streamCodeLines: streamTablesConfig.codeLines,\n    },\n  }\n\n  const streamTableRows: StreamingScenarioPlan = {\n    kind: \"streaming\",\n    name: \"incremental_table_rows\",\n    description: \"Incremental parsing on growing single table rows\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    baseContent: tableRowStreamBaseContent,\n    chunks: tableRowStreamChunks,\n    repeat: true,\n    contentStats: {\n      tableCols: tableRowStreamCols,\n      baseRows: tableRowStreamBaseRows,\n      streamRows: tableRowStreamRows,\n    },\n  }\n\n  const streamCodeBlocks: StreamingScenarioPlan = {\n    kind: \"streaming\",\n    name: \"incremental_code_blocks\",\n    description: \"Incremental parsing with appended code blocks\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    baseContent: codeBlockStreamBaseContent,\n    chunks: codeBlockStreamChunks,\n    repeat: true,\n    contentStats: {\n      codeBlocks: codeBlockStreamBlocks,\n      codeLines: codeBlockStreamLines,\n    },\n  }\n\n  const streamHeadings: StreamingScenarioPlan = {\n    kind: \"streaming\",\n    name: \"incremental_headings\",\n    description: \"Incremental parsing with appended headings\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    baseContent: headingStreamBaseContent,\n    chunks: headingStreamChunks,\n    repeat: true,\n    contentStats: {\n      headings: headingStreamCount,\n      depthMin: 2,\n      depthMax: 4,\n      wordsPerHeading: 4,\n    },\n  }\n\n  const styleSmall: StyleScenarioPlan = {\n    kind: \"style\",\n    name: \"style_rerender_small\",\n    description: \"Rerender blocks on style changes (small document)\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: staticSmallDoc.content,\n    contentStats: staticSmallDoc.stats,\n  }\n\n  const styleLarge: StyleScenarioPlan = {\n    kind: \"style\",\n    name: \"style_rerender_large\",\n    description: \"Rerender blocks on style changes (large document)\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: staticLargeDoc.content,\n    contentStats: staticLargeDoc.stats,\n  }\n\n  const styleTableSmall: StyleScenarioPlan = {\n    kind: \"style\",\n    name: \"style_rerender_tables_only\",\n    description: \"Rerender blocks on style changes (tables-only)\",\n    iterations: baseIterations,\n    warmupIterations: baseWarmup,\n    content: tableOnlySmallDoc.content,\n    contentStats: tableOnlySmallDoc.stats,\n  }\n\n  if (suite === \"quick\") return [staticSmall, tableOnlySmall, streamMixed, streamTableRows, styleSmall]\n  if (suite === \"long\")\n    return [\n      staticLarge,\n      tableOnlyLarge,\n      codeOnlyLarge,\n      streamMixed,\n      streamTableRows,\n      streamCodeBlocks,\n      streamHeadings,\n      streamTablesLong,\n      styleLarge,\n    ]\n  if (suite === \"default\")\n    return [\n      staticSmall,\n      tableOnlySmall,\n      codeOnlySmall,\n      headingsOnly,\n      staticLarge,\n      streamMixed,\n      streamTableRows,\n      streamCodeBlocks,\n      streamHeadings,\n      styleSmall,\n      styleTableSmall,\n      styleLarge,\n      streamTablesLong,\n    ]\n  return []\n}\n\nasync function runScenario(plan: ScenarioPlan, ctx: RunContext): Promise<ScenarioResult> {\n  if (plan.kind === \"static\") {\n    return runStaticScenario(plan, ctx)\n  }\n  if (plan.kind === \"style\") {\n    return runStyleScenario(plan, ctx)\n  }\n  return runStreamingScenario(plan, ctx)\n}\n\nasync function runStaticScenario(plan: StaticScenarioPlan, ctx: RunContext): Promise<ScenarioResult> {\n  ctx.markdown.streaming = false\n  ctx.markdown.content = \"\"\n  ctx.markdown.clearCache()\n\n  for (let i = 0; i < plan.warmupIterations; i += 1) {\n    ctx.markdown.content = \"\"\n    ctx.markdown.clearCache()\n    ctx.markdown.content = plan.content\n  }\n\n  const durations: number[] = []\n  const measurementStart = Date.now()\n  const memStart = shouldSampleMemory(ctx) ? readMemorySample() : null\n  const nativeMemStart = shouldSampleMemory(ctx) ? readNativeMemorySample() : null\n  const sampler = createMemorySampler(ctx)\n\n  for (let i = 0; i < plan.iterations; i += 1) {\n    ctx.markdown.content = \"\"\n    ctx.markdown.clearCache()\n    const start = performance.now()\n    ctx.markdown.content = plan.content\n    const elapsed = performance.now() - start\n    durations.push(elapsed)\n    sampler.recordIteration(i + 1)\n  }\n\n  const elapsedMs = Date.now() - measurementStart\n  const memEnd = shouldSampleMemory(ctx) ? readMemorySample() : null\n  const nativeMemEnd = shouldSampleMemory(ctx) ? readNativeMemorySample() : null\n  sampler.stop()\n\n  return {\n    name: plan.name,\n    description: plan.description,\n    iterations: plan.iterations,\n    warmupIterations: plan.warmupIterations,\n    elapsedMs,\n    category: \"parse\",\n    timingMode: \"content-set\",\n    updateStats: computeTimingStats(durations),\n    memoryStats: memStart && memEnd ? computeMemoryStats(sampler.jsSamples, memStart, memEnd) : undefined,\n    nativeMemoryStats:\n      nativeMemStart && nativeMemEnd\n        ? computeNativeMemoryStats(sampler.nativeSamples, nativeMemStart, nativeMemEnd)\n        : undefined,\n    contentStats: {\n      initialChars: plan.content.length,\n      finalChars: plan.content.length,\n      maxChars: plan.content.length,\n      updates: plan.iterations,\n      appendedChars: 0,\n    },\n    settings: {\n      ...plan.contentStats,\n      mode: \"static\",\n    },\n  }\n}\n\nasync function runStreamingScenario(plan: StreamingScenarioPlan, ctx: RunContext): Promise<ScenarioResult> {\n  ctx.markdown.streaming = true\n  const state = createStreamState({\n    content: plan.baseContent,\n    chunks: plan.chunks,\n    repeat: plan.repeat,\n    maxChars: ctx.maxChars,\n  })\n\n  ctx.markdown.content = state.content\n\n  if (plan.warmupIterations > 0) {\n    await runStreamingIterations(state, ctx, plan.warmupIterations, false)\n  }\n\n  const measurementStart = Date.now()\n  const memStart = shouldSampleMemory(ctx) ? readMemorySample() : null\n  const nativeMemStart = shouldSampleMemory(ctx) ? readNativeMemorySample() : null\n  const sampler = createMemorySampler(ctx)\n\n  const measured = await runStreamingIterations(state, ctx, plan.iterations, true, sampler)\n  if (measured.durations.length < plan.iterations) {\n    throw new Error(\n      `streaming scenario '${plan.name}' ended early (updates=${measured.durations.length}/${plan.iterations}). Increase --max-chars or reduce --iterations.`,\n    )\n  }\n\n  const elapsedMs = Date.now() - measurementStart\n  const memEnd = shouldSampleMemory(ctx) ? readMemorySample() : null\n  const nativeMemEnd = shouldSampleMemory(ctx) ? readNativeMemorySample() : null\n  sampler.stop()\n\n  return {\n    name: plan.name,\n    description: plan.description,\n    iterations: plan.iterations,\n    warmupIterations: plan.warmupIterations,\n    elapsedMs,\n    category: \"incremental\",\n    timingMode: \"content-set\",\n    updateStats: computeTimingStats(measured.durations),\n    memoryStats: memStart && memEnd ? computeMemoryStats(sampler.jsSamples, memStart, memEnd) : undefined,\n    nativeMemoryStats:\n      nativeMemStart && nativeMemEnd\n        ? computeNativeMemoryStats(sampler.nativeSamples, nativeMemStart, nativeMemEnd)\n        : undefined,\n    contentStats: {\n      initialChars: plan.baseContent.length,\n      finalChars: state.content.length,\n      maxChars: state.maxContentChars,\n      updates: measured.durations.length,\n      appendedChars: measured.appendedChars,\n    },\n    settings: {\n      ...plan.contentStats,\n      mode: \"streaming\",\n      streamIntervalMs: ctx.streamIntervalMs,\n      appendLinesPerTick: ctx.chunkLines,\n      maxChars: ctx.maxChars,\n      repeat: plan.repeat,\n    },\n  }\n}\n\nasync function runStyleScenario(plan: StyleScenarioPlan, ctx: RunContext): Promise<ScenarioResult> {\n  ctx.markdown.streaming = false\n  ctx.markdown.syntaxStyle = ctx.syntaxStyleA\n  ctx.markdown.conceal = true\n  ctx.markdown.content = plan.content\n\n  for (let i = 0; i < plan.warmupIterations; i += 1) {\n    ctx.markdown.conceal = !ctx.markdown.conceal\n    ctx.markdown.refreshStyles()\n  }\n\n  const durations: number[] = []\n  const measurementStart = Date.now()\n  const memStart = shouldSampleMemory(ctx) ? readMemorySample() : null\n  const nativeMemStart = shouldSampleMemory(ctx) ? readNativeMemorySample() : null\n  const sampler = createMemorySampler(ctx)\n\n  for (let i = 0; i < plan.iterations; i += 1) {\n    ctx.markdown.conceal = !ctx.markdown.conceal\n    ctx.markdown.syntaxStyle = i % 2 === 0 ? ctx.syntaxStyleA : ctx.syntaxStyleB\n    const start = performance.now()\n    ctx.markdown.refreshStyles()\n    const elapsed = performance.now() - start\n    durations.push(elapsed)\n    sampler.recordIteration(i + 1)\n  }\n\n  const elapsedMs = Date.now() - measurementStart\n  const memEnd = shouldSampleMemory(ctx) ? readMemorySample() : null\n  const nativeMemEnd = shouldSampleMemory(ctx) ? readNativeMemorySample() : null\n  sampler.stop()\n\n  return {\n    name: plan.name,\n    description: plan.description,\n    iterations: plan.iterations,\n    warmupIterations: plan.warmupIterations,\n    elapsedMs,\n    category: \"style\",\n    timingMode: \"style-refresh\",\n    updateStats: computeTimingStats(durations),\n    memoryStats: memStart && memEnd ? computeMemoryStats(sampler.jsSamples, memStart, memEnd) : undefined,\n    nativeMemoryStats:\n      nativeMemStart && nativeMemEnd\n        ? computeNativeMemoryStats(sampler.nativeSamples, nativeMemStart, nativeMemEnd)\n        : undefined,\n    contentStats: {\n      initialChars: plan.content.length,\n      finalChars: plan.content.length,\n      maxChars: plan.content.length,\n      updates: plan.iterations,\n      appendedChars: 0,\n    },\n    settings: {\n      ...plan.contentStats,\n      mode: \"style\",\n    },\n  }\n}\n\nasync function runStreamingIterations(\n  state: StreamState,\n  ctx: RunContext,\n  iterations: number,\n  record: boolean,\n  sampler?: MemorySampler,\n): Promise<{ durations: number[]; appendedChars: number }> {\n  const durations: number[] = []\n  let appendedChars = 0\n\n  for (let i = 0; i < iterations; i += 1) {\n    const update = appendStream(state, ctx.chunkLines)\n    if (!update.updated) break\n\n    const start = performance.now()\n    ctx.markdown.content = state.content\n    const elapsed = performance.now() - start\n\n    if (record) {\n      durations.push(elapsed)\n      appendedChars += update.appendedChars\n      sampler?.recordIteration(durations.length)\n    }\n\n    if (ctx.streamIntervalMs > 0) {\n      await Bun.sleep(ctx.streamIntervalMs)\n    }\n  }\n\n  return { durations, appendedChars }\n}\n\nfunction createStreamState(input: {\n  content: string\n  chunks: string[]\n  repeat: boolean\n  maxChars: number\n}): StreamState {\n  return {\n    content: input.content,\n    chunks: input.chunks,\n    cursor: 0,\n    repeat: input.repeat,\n    maxChars: input.maxChars,\n    maxContentChars: input.content.length,\n    done: false,\n  }\n}\n\nfunction appendStream(state: StreamState, linesPerTick: number): { updated: boolean; appendedChars: number } {\n  if (state.done) return { updated: false, appendedChars: 0 }\n  let appended = \"\"\n\n  for (let i = 0; i < linesPerTick; i += 1) {\n    if (state.cursor >= state.chunks.length) {\n      if (state.repeat) {\n        state.cursor = 0\n      } else {\n        state.done = true\n        break\n      }\n    }\n    appended += state.chunks[state.cursor]\n    state.cursor += 1\n  }\n\n  if (!appended) return { updated: false, appendedChars: 0 }\n\n  if (state.maxChars > 0 && state.content.length + appended.length > state.maxChars) {\n    state.done = true\n    return { updated: false, appendedChars: 0 }\n  }\n\n  state.content += appended\n  state.maxContentChars = Math.max(state.maxContentChars, state.content.length)\n\n  return { updated: true, appendedChars: appended.length }\n}\n\nfunction buildMarkdownDocument(rng: () => number, config: StaticScenarioConfig): { content: string; stats: any } {\n  const parts: string[] = []\n  parts.push(`# ${config.title}`)\n\n  for (let section = 0; section < config.sections; section += 1) {\n    parts.push(`## Section ${section + 1}`)\n\n    for (let p = 0; p < config.paragraphsPerSection; p += 1) {\n      parts.push(makeParagraph(rng, config.sentencesPerParagraph))\n    }\n\n    if (config.lists > 0) {\n      for (let l = 0; l < config.lists; l += 1) {\n        parts.push(makeList(rng, config.listItems))\n      }\n    }\n\n    if (config.tables > 0) {\n      for (let t = 0; t < config.tables; t += 1) {\n        const tableLines = makeTableLines(rng, config.tableCols, config.tableRows)\n        parts.push(tableLines.join(\"\\n\"))\n      }\n    }\n\n    if (config.codeBlocks > 0) {\n      for (let c = 0; c < config.codeBlocks; c += 1) {\n        parts.push(makeCodeBlock(rng, config.codeLines))\n      }\n    }\n  }\n\n  return {\n    content: parts.join(\"\\n\\n\") + \"\\n\",\n    stats: {\n      sections: config.sections,\n      paragraphsPerSection: config.paragraphsPerSection,\n      sentencesPerParagraph: config.sentencesPerParagraph,\n      listsPerSection: config.lists,\n      totalLists: config.sections * config.lists,\n      listItems: config.listItems,\n      tablesPerSection: config.tables,\n      totalTables: config.sections * config.tables,\n      tableRows: config.tableRows,\n      tableCols: config.tableCols,\n      codeBlocksPerSection: config.codeBlocks,\n      totalCodeBlocks: config.sections * config.codeBlocks,\n      codeLines: config.codeLines,\n      totalParagraphs: config.sections * config.paragraphsPerSection,\n    },\n  }\n}\n\nfunction buildTableOnlyDocument(\n  rng: () => number,\n  config: { title: string; tables: number; rows: number; cols: number },\n): { content: string; stats: any } {\n  const parts: string[] = []\n  parts.push(`# ${config.title}`)\n  for (let t = 0; t < config.tables; t += 1) {\n    const tableLines = makeTableLines(rng, config.cols, config.rows)\n    parts.push(tableLines.join(\"\\n\"))\n  }\n\n  return {\n    content: parts.join(\"\\n\\n\") + \"\\n\",\n    stats: {\n      tables: config.tables,\n      tableRows: config.rows,\n      tableCols: config.cols,\n    },\n  }\n}\n\nfunction buildCodeOnlyDocument(\n  rng: () => number,\n  config: { title: string; blocks: number; lines: number },\n): { content: string; stats: any } {\n  const parts: string[] = []\n  parts.push(`# ${config.title}`)\n  for (let b = 0; b < config.blocks; b += 1) {\n    parts.push(makeCodeBlock(rng, config.lines))\n  }\n\n  return {\n    content: parts.join(\"\\n\\n\") + \"\\n\",\n    stats: {\n      codeBlocks: config.blocks,\n      codeLines: config.lines,\n    },\n  }\n}\n\nfunction buildHeadingsOnlyDocument(\n  rng: () => number,\n  config: { title: string; headings: number; depthMin: number; depthMax: number; wordsPerHeading: number },\n): { content: string; stats: any } {\n  const parts: string[] = []\n  parts.push(`# ${config.title}`)\n  for (let i = 0; i < config.headings; i += 1) {\n    parts.push(makeHeadingLine(rng, config.depthMin, config.depthMax, config.wordsPerHeading))\n  }\n\n  return {\n    content: parts.join(\"\\n\") + \"\\n\",\n    stats: {\n      headings: config.headings,\n      depthMin: config.depthMin,\n      depthMax: config.depthMax,\n      wordsPerHeading: config.wordsPerHeading,\n    },\n  }\n}\n\nfunction buildHeadingChunks(\n  rng: () => number,\n  config: { headings: number; depthMin: number; depthMax: number; wordsPerHeading: number },\n): string[] {\n  const chunks: string[] = []\n  for (let i = 0; i < config.headings; i += 1) {\n    chunks.push(makeHeadingLine(rng, config.depthMin, config.depthMax, config.wordsPerHeading) + \"\\n\")\n  }\n  return chunks\n}\n\nfunction buildCodeBlockChunks(rng: () => number, config: { blocks: number; lines: number }): string[] {\n  const chunks: string[] = []\n  for (let b = 0; b < config.blocks; b += 1) {\n    const lines = makeCodeBlockLines(rng, config.lines)\n    for (const line of lines) {\n      chunks.push(`${line}\\n`)\n    }\n    chunks.push(\"\\n\")\n  }\n  return chunks\n}\n\nfunction buildTableRowChunks(rng: () => number, config: { rows: number; cols: number }): string[] {\n  const chunks: string[] = []\n  for (let r = 0; r < config.rows; r += 1) {\n    chunks.push(makeTableRowLine(rng, r, config.cols) + \"\\n\")\n  }\n  return chunks\n}\n\nfunction buildStreamingChunks(rng: () => number, config: StreamingScenarioConfig): string[] {\n  const chunks: string[] = []\n  for (let section = 0; section < config.sections; section += 1) {\n    pushLine(chunks, `### Stream Section ${section + 1}`)\n    pushLine(chunks, \"\")\n\n    for (let p = 0; p < config.paragraphsPerSection; p += 1) {\n      pushLine(chunks, makeParagraph(rng, config.sentencesPerParagraph))\n      pushLine(chunks, \"\")\n    }\n\n    for (let l = 0; l < config.lists; l += 1) {\n      const listLines = makeListLines(rng, config.listItems)\n      for (const line of listLines) {\n        pushLine(chunks, line)\n      }\n      pushLine(chunks, \"\")\n    }\n\n    for (let t = 0; t < config.tables; t += 1) {\n      const tableLines = makeTableLines(rng, config.tableCols, config.tableRows)\n      for (const line of tableLines) {\n        pushLine(chunks, line)\n      }\n      pushLine(chunks, \"\")\n    }\n\n    for (let c = 0; c < config.codeBlocks; c += 1) {\n      const codeLines = makeCodeBlockLines(rng, config.codeLines)\n      for (const line of codeLines) {\n        pushLine(chunks, line)\n      }\n      pushLine(chunks, \"\")\n    }\n  }\n\n  return chunks\n}\n\nfunction pushLine(chunks: string[], line: string): void {\n  chunks.push(`${line}\\n`)\n}\n\nfunction makeParagraph(rng: () => number, sentences: number): string {\n  const parts: string[] = []\n  for (let i = 0; i < sentences; i += 1) {\n    parts.push(makeSentence(rng, 6, 12))\n  }\n  return parts.join(\" \")\n}\n\nfunction makeSentence(rng: () => number, minWords: number, maxWords: number): string {\n  const count = minWords + Math.floor(rng() * (maxWords - minWords + 1))\n  const words: string[] = []\n  for (let i = 0; i < count; i += 1) {\n    let word = pick(rng, WORDS)\n    if (rng() < 0.25) {\n      word = wrapInline(rng, word)\n    }\n    words.push(word)\n  }\n  const sentence = words.join(\" \")\n  return sentence.charAt(0).toUpperCase() + sentence.slice(1) + \".\"\n}\n\nfunction wrapInline(rng: () => number, word: string): string {\n  const roll = rng()\n  if (roll < 0.08) return `**${word}**`\n  if (roll < 0.16) return `*${word}*`\n  if (roll < 0.22) return `\\`${word}\\``\n  if (roll < 0.26) return `[${word}](https://example.com/${word})`\n  return word\n}\n\nfunction makeList(rng: () => number, items: number): string {\n  return makeListLines(rng, items).join(\"\\n\")\n}\n\nfunction makeListLines(rng: () => number, items: number): string[] {\n  const lines: string[] = []\n  for (let i = 0; i < items; i += 1) {\n    lines.push(`- ${makeSentence(rng, 4, 9)}`)\n  }\n  return lines\n}\n\nfunction makeCodeBlock(rng: () => number, lines: number): string {\n  return makeCodeBlockLines(rng, lines).join(\"\\n\")\n}\n\nfunction makeCodeBlockLines(rng: () => number, lines: number): string[] {\n  const variable = pick(rng, CODE_WORDS)\n  const iterations = Math.max(2, Math.floor(lines / 2))\n  const targetLines = Math.max(6, lines)\n  const body: string[] = []\n  body.push(`const ${variable} = ${Math.floor(rng() * 1000)}`)\n  body.push(`let result = ${Math.floor(rng() * 10)}`)\n  body.push(`for (let i = 0; i < ${iterations}; i += 1) {`)\n  body.push(`  result += (${variable} + i) % 5`)\n  body.push(\"}\")\n  body.push(\"return result\")\n\n  let fillerIndex = 0\n  while (body.length < targetLines) {\n    body.splice(body.length - 1, 0, `result += ${Math.floor(rng() * 10)} + ${fillerIndex}`)\n    fillerIndex += 1\n  }\n\n  return [\"```typescript\", ...body, \"```\"]\n}\n\nfunction makeTableLines(rng: () => number, columns: number, rows: number): string[] {\n  const header: string[] = []\n  const align: string[] = []\n\n  for (let c = 0; c < columns; c += 1) {\n    header.push(`Column ${c + 1}`)\n    if (c % 3 === 0) align.push(\":---\")\n    else if (c % 3 === 1) align.push(\":---:\")\n    else align.push(\"---:\")\n  }\n\n  const lines: string[] = []\n  lines.push(`| ${header.join(\" | \")} |`)\n  lines.push(`| ${align.join(\" | \")} |`)\n\n  for (let r = 0; r < rows; r += 1) {\n    const cells: string[] = []\n    for (let c = 0; c < columns; c += 1) {\n      cells.push(makeCellText(rng, r, c))\n    }\n    lines.push(`| ${cells.join(\" | \")} |`)\n  }\n\n  return lines\n}\n\nfunction makeTableRowLine(rng: () => number, row: number, columns: number): string {\n  const cells: string[] = []\n  for (let c = 0; c < columns; c += 1) {\n    cells.push(makeCellText(rng, row, c))\n  }\n  return `| ${cells.join(\" | \")} |`\n}\n\nfunction makeHeadingLine(rng: () => number, depthMin: number, depthMax: number, words: number): string {\n  const depth = depthMin + Math.floor(rng() * Math.max(1, depthMax - depthMin + 1))\n  const tokens: string[] = []\n  for (let i = 0; i < words; i += 1) {\n    tokens.push(pick(rng, WORDS))\n  }\n  return `${\"#\".repeat(depth)} ${tokens.join(\" \")}`\n}\n\nfunction makeCellText(rng: () => number, row: number, col: number): string {\n  const base = `${pick(rng, WORDS)} ${pick(rng, WORDS)}`\n  const roll = rng()\n  if (roll < 0.2) return `**${base}**`\n  if (roll < 0.35) return `*${base}*`\n  if (roll < 0.45) return `\\`${base}\\``\n  if (roll < 0.55) return `${base} ${Math.floor(rng() * 100)}`\n  if (roll < 0.6) return `[${base}](https://example.com/r${row}c${col})`\n  return base\n}\n\nfunction pick<T>(rng: () => number, list: T[]): T {\n  return list[Math.floor(rng() * list.length)]\n}\n\nfunction scaled(value: number, scaleValue: number): number {\n  return Math.max(1, Math.round(value * scaleValue))\n}\n\nfunction createRng(initialSeed: number): () => number {\n  let state = initialSeed >>> 0\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0\n    return state / 0x100000000\n  }\n}\n\nfunction toNumber(value: unknown, fallback: number): number {\n  if (typeof value === \"number\" && Number.isFinite(value)) return value\n  if (typeof value === \"string\") {\n    const parsed = Number(value)\n    if (Number.isFinite(parsed)) return parsed\n  }\n  return fallback\n}\n\nfunction shouldSampleMemory(ctx: RunContext): boolean {\n  return ctx.memInterval > 0 || ctx.memSampleEvery > 0\n}\n\nfunction readMemorySample(): MemorySample {\n  const usage = process.memoryUsage()\n  return {\n    rss: usage.rss ?? 0,\n    heapTotal: usage.heapTotal ?? 0,\n    heapUsed: usage.heapUsed ?? 0,\n    external: usage.external ?? 0,\n    arrayBuffers: usage.arrayBuffers ?? 0,\n  }\n}\n\nfunction readNativeMemorySample(): NativeMemorySample {\n  const stats = nativeLib.getAllocatorStats()\n  return {\n    totalRequestedBytes: stats.totalRequestedBytes,\n    activeAllocations: stats.activeAllocations,\n    smallAllocations: stats.smallAllocations,\n    largeAllocations: stats.largeAllocations,\n    requestedBytesValid: stats.requestedBytesValid,\n  }\n}\n\nfunction createMemorySampler(ctx: RunContext): MemorySampler {\n  const jsSamples: MemorySample[] = []\n  const nativeSamples: NativeMemorySample[] = []\n\n  const pushSample = (): void => {\n    jsSamples.push(readMemorySample())\n    nativeSamples.push(readNativeMemorySample())\n  }\n\n  if (ctx.memInterval > 0) {\n    const timer = setInterval(() => {\n      pushSample()\n    }, ctx.memInterval)\n    return {\n      jsSamples,\n      nativeSamples,\n      recordIteration: () => {},\n      stop: () => clearInterval(timer),\n    }\n  }\n\n  if (ctx.memSampleEvery > 0) {\n    return {\n      jsSamples,\n      nativeSamples,\n      recordIteration: (iteration: number) => {\n        if (iteration % ctx.memSampleEvery === 0) {\n          pushSample()\n        }\n      },\n      stop: () => {},\n    }\n  }\n\n  return {\n    jsSamples,\n    nativeSamples,\n    recordIteration: () => {},\n    stop: () => {},\n  }\n}\n\nfunction computeMemoryStats(samples: MemorySample[], start: MemorySample, end: MemorySample): MemoryStats {\n  const all = [start, ...samples, end]\n  const peak = { ...start }\n  for (const sample of all) {\n    updatePeak(sample, peak)\n  }\n\n  return {\n    samples: all.length,\n    start,\n    end,\n    delta: diffMemory(start, end),\n    peak,\n    fields: {\n      rss: computeFieldStats(all.map((s) => s.rss)),\n      heapTotal: computeFieldStats(all.map((s) => s.heapTotal)),\n      heapUsed: computeFieldStats(all.map((s) => s.heapUsed)),\n      external: computeFieldStats(all.map((s) => s.external)),\n      arrayBuffers: computeFieldStats(all.map((s) => s.arrayBuffers)),\n    },\n  }\n}\n\nfunction computeNativeMemoryStats(\n  samples: NativeMemorySample[],\n  start: NativeMemorySample,\n  end: NativeMemorySample,\n): NativeMemoryStats {\n  const all = [start, ...samples, end]\n  const requestedBytesReliable = all.every((sample) => sample.requestedBytesValid)\n  const peak = { ...start }\n  for (const sample of all) {\n    updateNativePeak(sample, peak)\n  }\n\n  return {\n    samples: all.length,\n    start,\n    end,\n    delta: diffNativeMemory(start, end),\n    peak,\n    requestedBytesReliable,\n    fields: {\n      totalRequestedBytes: computeFieldStats(all.map((s) => s.totalRequestedBytes)),\n      activeAllocations: computeFieldStats(all.map((s) => s.activeAllocations)),\n      smallAllocations: computeFieldStats(all.map((s) => s.smallAllocations)),\n      largeAllocations: computeFieldStats(all.map((s) => s.largeAllocations)),\n    },\n  }\n}\n\nfunction updatePeak(sample: MemorySample, peak: MemorySample): void {\n  peak.rss = Math.max(peak.rss, sample.rss)\n  peak.heapTotal = Math.max(peak.heapTotal, sample.heapTotal)\n  peak.heapUsed = Math.max(peak.heapUsed, sample.heapUsed)\n  peak.external = Math.max(peak.external, sample.external)\n  peak.arrayBuffers = Math.max(peak.arrayBuffers, sample.arrayBuffers)\n}\n\nfunction updateNativePeak(sample: NativeMemorySample, peak: NativeMemorySample): void {\n  peak.totalRequestedBytes = Math.max(peak.totalRequestedBytes, sample.totalRequestedBytes)\n  peak.activeAllocations = Math.max(peak.activeAllocations, sample.activeAllocations)\n  peak.smallAllocations = Math.max(peak.smallAllocations, sample.smallAllocations)\n  peak.largeAllocations = Math.max(peak.largeAllocations, sample.largeAllocations)\n  peak.requestedBytesValid = peak.requestedBytesValid && sample.requestedBytesValid\n}\n\nfunction diffMemory(start: MemorySample, end: MemorySample): MemorySample {\n  return {\n    rss: end.rss - start.rss,\n    heapTotal: end.heapTotal - start.heapTotal,\n    heapUsed: end.heapUsed - start.heapUsed,\n    external: end.external - start.external,\n    arrayBuffers: end.arrayBuffers - start.arrayBuffers,\n  }\n}\n\nfunction diffNativeMemory(start: NativeMemorySample, end: NativeMemorySample): NativeMemorySample {\n  return {\n    totalRequestedBytes: end.totalRequestedBytes - start.totalRequestedBytes,\n    activeAllocations: end.activeAllocations - start.activeAllocations,\n    smallAllocations: end.smallAllocations - start.smallAllocations,\n    largeAllocations: end.largeAllocations - start.largeAllocations,\n    requestedBytesValid: start.requestedBytesValid && end.requestedBytesValid,\n  }\n}\n\nfunction computeFieldStats(values: number[]): MemoryFieldStats {\n  const sorted = [...values].sort((a, b) => a - b)\n  const min = sorted[0] ?? 0\n  const max = sorted[sorted.length - 1] ?? 0\n  const avg = sorted.length > 0 ? sorted.reduce((sum, v) => sum + v, 0) / sorted.length : 0\n  const median = sorted.length > 0 ? (sorted[Math.floor(sorted.length / 2)] ?? 0) : 0\n  return { min, max, avg, median }\n}\n\nfunction computeTimingStats(durations: number[]): TimingStats {\n  const sorted = [...durations].sort((a, b) => a - b)\n  const count = sorted.length\n  const sum = sorted.reduce((acc, value) => acc + value, 0)\n  const average = count > 0 ? sum / count : 0\n  const min = sorted[0] ?? 0\n  const max = sorted[count - 1] ?? 0\n  const median = count > 0 ? (sorted[Math.floor(count / 2)] ?? 0) : 0\n  const p95 = count > 0 ? (sorted[Math.floor(count * 0.95)] ?? 0) : 0\n  const stdDev = count > 0 ? Math.sqrt(sorted.reduce((acc, v) => acc + Math.pow(v - average, 2), 0) / count) : 0\n\n  return {\n    count,\n    averageMs: average,\n    medianMs: median,\n    p95Ms: p95,\n    minMs: min,\n    maxMs: max,\n    stdDevMs: stdDev,\n  }\n}\n\nasync function outputResults(\n  meta: OutputMeta,\n  resultsList: ScenarioResult[],\n  scenarioLines: string[],\n  outputEnabled: boolean,\n  outputPath: string | null,\n): Promise<void> {\n  const runId = new Date().toISOString()\n  const payload = {\n    runId,\n    suite: meta.suiteName,\n    renderer: {\n      targetFps: meta.targetFps,\n      maxFps: meta.maxFps,\n    },\n    config: {\n      iterations: meta.iterations,\n      warmupIterations: meta.warmupIterations,\n      longIterations: meta.longIterations,\n      streamIntervalMs: meta.streamIntervalMs,\n      chunkLines: meta.chunkLines,\n      maxChars: meta.maxChars,\n      scale: meta.scale,\n      seed: meta.seed,\n      memInterval: meta.memInterval,\n      memSampleEvery: meta.memSampleEvery,\n      gpaSafeStats: meta.gpaSafeStats,\n      gpaMemoryLimitTracking: meta.gpaMemoryLimitTracking,\n    },\n    results: resultsList,\n  }\n\n  if (outputEnabled) {\n    writeLine(\n      `markdown-benchmark suite=${meta.suiteName} timing=frame-independent iters=${meta.iterations} warmup=${meta.warmupIterations}`,\n    )\n    writeLine(`native-build gpaSafeStats=${meta.gpaSafeStats} gpaMemoryLimitTracking=${meta.gpaMemoryLimitTracking}`)\n    for (const line of scenarioLines) {\n      writeLine(line)\n    }\n  }\n\n  if (outputPath) {\n    try {\n      const json = JSON.stringify(payload, null, 2)\n      await Bun.write(outputPath, json)\n    } catch (error: any) {\n      writeLine(`Error writing results to ${outputPath}: ${error.message}`)\n    }\n  }\n}\n\nfunction formatBytes(value: number): string {\n  const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"]\n  const abs = Math.abs(value)\n\n  if (abs < 1024) {\n    return `${Math.trunc(value)}B`\n  }\n\n  let unitIndex = 0\n  let scaled = abs\n  while (scaled >= 1024 && unitIndex < units.length - 1) {\n    scaled /= 1024\n    unitIndex += 1\n  }\n\n  const sign = value < 0 ? \"-\" : \"\"\n  return `${sign}${scaled.toFixed(2)}${units[unitIndex]}`\n}\n\nfunction formatAllocs(value: number): string {\n  const intValue = Math.trunc(value)\n  const sign = intValue > 0 ? \"+\" : \"\"\n  return `${sign}${intValue.toLocaleString(\"en-US\")} allocs`\n}\n\nfunction formatScenarioResult(result: ScenarioResult): string {\n  const jsMem = result.memoryStats\n  const nativeMem = result.nativeMemoryStats\n\n  const jsMemSummary = jsMem\n    ? ` jsMemDeltaRss=${formatBytes(jsMem.delta.rss)}` +\n      ` jsMemDeltaHeap=${formatBytes(jsMem.delta.heapUsed)}` +\n      ` jsMemDeltaExt=${formatBytes(jsMem.delta.external)}` +\n      ` jsMemDeltaAB=${formatBytes(jsMem.delta.arrayBuffers)}` +\n      ` jsMemPeakRss=${formatBytes(jsMem.peak.rss)}`\n    : \"\"\n\n  const nativeMemSummary = nativeMem\n    ? ` nativeMemDeltaReq=${nativeMem.requestedBytesReliable ? formatBytes(nativeMem.delta.totalRequestedBytes) : \"invalid\"}` +\n      ` nativeMemDeltaReqBytes=${nativeMem.requestedBytesReliable ? `${Math.trunc(nativeMem.delta.totalRequestedBytes)}B` : \"invalid\"}` +\n      ` nativeMemDeltaActive=${formatAllocs(nativeMem.delta.activeAllocations)}` +\n      ` nativeMemDeltaSmall=${formatAllocs(nativeMem.delta.smallAllocations)}` +\n      ` nativeMemDeltaLarge=${formatAllocs(nativeMem.delta.largeAllocations)}` +\n      ` nativeMemPeakReq=${nativeMem.requestedBytesReliable ? formatBytes(nativeMem.peak.totalRequestedBytes) : \"invalid\"}` +\n      ` nativeMemPeakReqBytes=${nativeMem.requestedBytesReliable ? `${Math.trunc(nativeMem.peak.totalRequestedBytes)}B` : \"invalid\"}` +\n      ` nativeMemPeakActive=${formatAllocs(nativeMem.peak.activeAllocations)}` +\n      ` nativeMemReqReliable=${nativeMem.requestedBytesReliable}`\n    : \"\"\n\n  return `scenario=${result.name} category=${result.category} mode=${result.timingMode} iters=${result.updateStats.count} elapsedMs=${result.elapsedMs} avgMs=${result.updateStats.averageMs.toFixed(3)} medianMs=${result.updateStats.medianMs.toFixed(3)} p95Ms=${result.updateStats.p95Ms.toFixed(3)} minMs=${result.updateStats.minMs.toFixed(3)} maxMs=${result.updateStats.maxMs.toFixed(3)} chars=${result.contentStats.finalChars}${jsMemSummary}${nativeMemSummary}`\n}\n\nfunction writeLine(line: string): void {\n  realStdoutWrite(`${line}\\n`)\n}\n\nasync function runSpawnedScenarios(plans: ScenarioPlan[]): Promise<void> {\n  const tempDir = await mkdtemp(path.join(tmpdir(), \"opentui-markdown-bench-\"))\n  const scenarioLines: string[] = []\n  const results: ScenarioResult[] = []\n\n  for (const plan of plans) {\n    const jsonPath = path.join(tempDir, `scenario-${plan.name}.json`)\n    const args = buildChildArgs(process.argv.slice(2), plan.name, jsonPath)\n    const child = Bun.spawn([process.execPath, new URL(import.meta.url).pathname, ...args], {\n      stdout: \"inherit\",\n      stderr: \"inherit\",\n      env: {\n        ...process.env,\n        OTUI_OVERRIDE_STDOUT: \"false\",\n        OTUI_USE_ALTERNATE_SCREEN: \"false\",\n      },\n    })\n    const exitCode = await child.exited\n    if (exitCode !== 0) {\n      throw new Error(`Scenario ${plan.name} failed with exit code ${exitCode}`)\n    }\n\n    const json = await readFile(jsonPath, \"utf8\")\n    await unlink(jsonPath)\n    const payload = JSON.parse(json)\n    const result = payload.results?.[0] as ScenarioResult | undefined\n    if (!result) {\n      throw new Error(`Scenario ${plan.name} did not produce a result`)\n    }\n    results.push(result)\n    scenarioLines.push(formatScenarioResult(result))\n  }\n\n  await outputResults(\n    {\n      suiteName,\n      targetFps,\n      maxFps,\n      iterations,\n      warmupIterations,\n      longIterations,\n      streamIntervalMs,\n      chunkLines,\n      maxChars,\n      scale,\n      seed,\n      memInterval,\n      memSampleEvery,\n      gpaSafeStats: nativeBuildOptions.gpaSafeStats,\n      gpaMemoryLimitTracking: nativeBuildOptions.gpaMemoryLimitTracking,\n    },\n    results,\n    scenarioLines,\n    outputEnabled,\n    jsonPath,\n  )\n}\n\nfunction buildChildArgs(args: string[], scenarioName: string, jsonPath: string): string[] {\n  const filtered: string[] = []\n  for (const arg of args) {\n    if (arg === \"--no-spawn-per-scenario\") continue\n    if (arg.startsWith(\"--scenario\")) continue\n    if (arg === \"--json\" || arg.startsWith(\"--json=\")) continue\n    if (arg === \"--output\" || arg === \"--no-output\") continue\n    filtered.push(arg)\n  }\n  filtered.push(`--scenario=${scenarioName}`)\n  filtered.push(`--json=${jsonPath}`)\n  filtered.push(\"--no-output\")\n  filtered.push(\"--no-spawn-per-scenario\")\n  return filtered\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/native-span-feed-async-benchmark.ts",
    "content": "import { dlopen, FFIType, suffix } from \"bun:ffi\"\nimport { setRenderLibPath } from \"../zig\"\n\nif (!process.env.NATIVE_SPAN_FEED_LIB) {\n  process.env.NATIVE_SPAN_FEED_LIB = \"bench\"\n}\nconst { NativeSpanFeed } = await import(\"../NativeSpanFeed.ts\")\n\nconst args = process.argv.slice(2)\n\nfunction getArg(name: string): string | null {\n  const prefix = `--${name}=`\n  for (const arg of args) {\n    if (arg.startsWith(prefix)) return arg.slice(prefix.length)\n  }\n  return null\n}\n\nfunction hasFlag(name: string): boolean {\n  return args.includes(`--${name}`)\n}\n\nconst libVariant = process.env.NATIVE_SPAN_FEED_LIB\nconst libBase = libVariant === \"bench\" ? \"native_span_feed_bench\" : (libVariant ?? \"native_span_feed\")\nconst libPath = new URL(`../zig/zig-out/lib/lib${libBase}.${suffix}`, import.meta.url).pathname\n\nsetRenderLibPath(libPath)\n\nconst benchLib = dlopen(libPath, {\n  benchProduce: {\n    args: [FFIType.ptr, FFIType.u64, FFIType.ptr, FFIType.u64, FFIType.u32],\n    returns: FFIType.i32,\n  },\n  benchProduceWrite: {\n    args: [FFIType.ptr, FFIType.u64, FFIType.ptr, FFIType.u64, FFIType.u32],\n    returns: FFIType.i32,\n  },\n})\n\ntype MemorySample = {\n  rss: number\n  heapTotal: number\n  heapUsed: number\n  external: number\n  arrayBuffers: number\n}\n\nfunction readMemory(): MemorySample {\n  const u = process.memoryUsage()\n  return {\n    rss: u.rss ?? 0,\n    heapTotal: u.heapTotal ?? 0,\n    heapUsed: u.heapUsed ?? 0,\n    external: u.external ?? 0,\n    arrayBuffers: u.arrayBuffers ?? 0,\n  }\n}\n\nfunction formatMB(bytes: number): string {\n  return `${(bytes / (1024 * 1024)).toFixed(2)}MB`\n}\n\ntype AsyncScenario = {\n  name: string\n  bytesPerIter: bigint\n  iters: number\n  chunkSize: number\n  initialChunks: number\n  delayMinMs: number\n  delayMaxMs: number\n  producerAPI: \"reserve\" | \"write\"\n  patternSize: number\n}\n\ntype AsyncScenarioResult = {\n  name: string\n  producerAPI: string\n  bytesPerIter: bigint\n  iters: number\n  bytesTotal: bigint\n  elapsedMs: number\n  throughputMBps: number\n  avgIterMs: number\n  medianIterMs: number\n  p95IterMs: number\n  minIterMs: number\n  maxIterMs: number\n  asyncDelay: { minMs: number; maxMs: number }\n  peakInFlightSpans: number\n  peakChunks: number\n  memory: {\n    start: MemorySample\n    end: MemorySample\n    peak: MemorySample\n  }\n  options: {\n    chunkSize: number\n    initialChunks: number\n  }\n}\n\nasync function runAsyncScenario(scenario: AsyncScenario): Promise<AsyncScenarioResult> {\n  const memStart = readMemory()\n  const memPeak = { ...memStart }\n\n  let received = 0n\n  let inFlightSpans = 0\n  let peakInFlightSpans = 0\n  let peakChunks = 0\n\n  const pattern = new Uint8Array(scenario.patternSize)\n  for (let i = 0; i < scenario.patternSize; i++) pattern[i] = i & 0xff\n  const patternLen = BigInt(pattern.byteLength)\n\n  const durations: number[] = []\n  let totalElapsed = 0\n\n  for (let iter = 0; iter < scenario.iters; iter++) {\n    const stream = NativeSpanFeed.create({\n      chunkSize: scenario.chunkSize,\n      initialChunks: scenario.initialChunks,\n      autoCommitOnFull: true,\n    })\n\n    const pending: Promise<void>[] = []\n\n    stream.onData(async (data) => {\n      const len = data.byteLength\n      inFlightSpans++\n      if (inFlightSpans > peakInFlightSpans) peakInFlightSpans = inFlightSpans\n\n      const delay = scenario.delayMinMs + Math.random() * (scenario.delayMaxMs - scenario.delayMinMs)\n\n      const p = new Promise<void>((resolve) => {\n        setTimeout(() => {\n          received += BigInt(len)\n          inFlightSpans--\n          resolve()\n        }, delay)\n      })\n      pending.push(p)\n    })\n\n    const iterStart = performance.now()\n\n    const produceFn =\n      scenario.producerAPI === \"write\" ? benchLib.symbols.benchProduceWrite : benchLib.symbols.benchProduce\n    const status = produceFn(stream.streamPtr, scenario.bytesPerIter, pattern, patternLen, 0)\n\n    if (status !== 0) {\n      console.error(`produce failed: ${status} scenario=${scenario.name} iter=${iter}`)\n      process.exit(1)\n    }\n\n    await Promise.all(pending)\n\n    const iterElapsed = performance.now() - iterStart\n    durations.push(iterElapsed)\n    totalElapsed += iterElapsed\n\n    const mem = readMemory()\n    memPeak.rss = Math.max(memPeak.rss, mem.rss)\n    memPeak.heapTotal = Math.max(memPeak.heapTotal, mem.heapTotal)\n    memPeak.heapUsed = Math.max(memPeak.heapUsed, mem.heapUsed)\n    memPeak.external = Math.max(memPeak.external, mem.external)\n    memPeak.arrayBuffers = Math.max(memPeak.arrayBuffers, mem.arrayBuffers)\n\n    stream.close()\n  }\n\n  const memEnd = readMemory()\n  memPeak.rss = Math.max(memPeak.rss, memEnd.rss)\n\n  const sorted = [...durations].sort((a, b) => a - b)\n  const count = durations.length\n  const avg = count > 0 ? totalElapsed / count : 0\n  const median = count > 0 ? (sorted[Math.floor(count / 2)] ?? 0) : 0\n  const p95 = count > 0 ? (sorted[Math.floor(count * 0.95)] ?? 0) : 0\n  const min = count > 0 ? (sorted[0] ?? 0) : 0\n  const max = count > 0 ? (sorted[count - 1] ?? 0) : 0\n\n  const totalSec = totalElapsed / 1000\n  const totalMB = Number(received) / (1024 * 1024)\n  const mbps = totalSec > 0 ? totalMB / totalSec : 0\n\n  return {\n    name: scenario.name,\n    producerAPI: scenario.producerAPI,\n    bytesPerIter: scenario.bytesPerIter,\n    iters: scenario.iters,\n    bytesTotal: received,\n    elapsedMs: totalElapsed,\n    throughputMBps: mbps,\n    avgIterMs: avg,\n    medianIterMs: median,\n    p95IterMs: p95,\n    minIterMs: min,\n    maxIterMs: max,\n    asyncDelay: { minMs: scenario.delayMinMs, maxMs: scenario.delayMaxMs },\n    peakInFlightSpans,\n    peakChunks,\n    memory: { start: memStart, end: memEnd, peak: memPeak },\n    options: {\n      chunkSize: scenario.chunkSize,\n      initialChunks: scenario.initialChunks,\n    },\n  }\n}\n\nconst KB = 1024n\nconst kb = (v: number) => BigInt(v) * KB\n\nfunction makeAsyncScenarios(): AsyncScenario[] {\n  const iters = Number(getArg(\"iters\") ?? \"50\")\n  return [\n    {\n      name: \"async/small_64k_fast\",\n      bytesPerIter: kb(64),\n      iters,\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      delayMinMs: 1,\n      delayMaxMs: 3,\n      producerAPI: \"reserve\",\n      patternSize: 64,\n    },\n    {\n      name: \"async/small_64k_slow\",\n      bytesPerIter: kb(64),\n      iters,\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      delayMinMs: 5,\n      delayMaxMs: 10,\n      producerAPI: \"reserve\",\n      patternSize: 64,\n    },\n    {\n      name: \"async/medium_256k_mixed\",\n      bytesPerIter: kb(256),\n      iters,\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      delayMinMs: 1,\n      delayMaxMs: 10,\n      producerAPI: \"reserve\",\n      patternSize: 1024,\n    },\n    {\n      name: \"async/large_1mb_fast\",\n      bytesPerIter: kb(1024),\n      iters: Math.max(10, Math.floor(iters / 5)),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      delayMinMs: 1,\n      delayMaxMs: 3,\n      producerAPI: \"reserve\",\n      patternSize: 1024,\n    },\n    {\n      name: \"async/large_1mb_slow\",\n      bytesPerIter: kb(1024),\n      iters: Math.max(10, Math.floor(iters / 5)),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      delayMinMs: 5,\n      delayMaxMs: 10,\n      producerAPI: \"reserve\",\n      patternSize: 1024,\n    },\n    {\n      name: \"async/write_256k_mixed\",\n      bytesPerIter: kb(256),\n      iters,\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      delayMinMs: 1,\n      delayMaxMs: 10,\n      producerAPI: \"write\",\n      patternSize: 1024,\n    },\n    {\n      name: \"async/tiny_chunks_slow\",\n      bytesPerIter: kb(64),\n      iters,\n      chunkSize: 4096,\n      initialChunks: 1,\n      delayMinMs: 5,\n      delayMaxMs: 10,\n      producerAPI: \"reserve\",\n      patternSize: 64,\n    },\n  ]\n}\n\nconst jsonArg = getArg(\"json\")\nconst jsonDefault = \"latest-async-bench-run.json\"\nconst jsonPath = jsonArg ?? (hasFlag(\"json\") ? jsonDefault : null)\n\nconsole.log(\"=== Async Handler Benchmark Suite ===\")\nconsole.log(\"Measures throughput, memory growth, and backpressure with async data handlers\")\nconsole.log(\"that resolve after random delays (simulating I/O like file writes, network, etc.)\\n\")\n\nconst scenarios = makeAsyncScenarios()\nconst results: AsyncScenarioResult[] = []\n\nfor (const scenario of scenarios) {\n  const result = await runAsyncScenario(scenario)\n  results.push(result)\n\n  const memDelta = {\n    rss: result.memory.end.rss - result.memory.start.rss,\n    external: result.memory.end.external - result.memory.start.external,\n    arrayBuffers: result.memory.end.arrayBuffers - result.memory.start.arrayBuffers,\n  }\n\n  console.log(\n    `scenario=${result.name}` +\n      ` api=${result.producerAPI}` +\n      ` iters=${result.iters}` +\n      ` bytesPerIter=${result.bytesPerIter}` +\n      ` bytesTotal=${result.bytesTotal}` +\n      ` delay=${result.asyncDelay.minMs}-${result.asyncDelay.maxMs}ms` +\n      ` avgIterMs=${result.avgIterMs.toFixed(3)}` +\n      ` medianIterMs=${result.medianIterMs.toFixed(3)}` +\n      ` p95IterMs=${result.p95IterMs.toFixed(3)}` +\n      ` minIterMs=${result.minIterMs.toFixed(3)}` +\n      ` maxIterMs=${result.maxIterMs.toFixed(3)}` +\n      ` throughputMBps=${result.throughputMBps.toFixed(2)}` +\n      ` peakInFlight=${result.peakInFlightSpans}` +\n      ` memDeltaRss=${formatMB(memDelta.rss)}` +\n      ` memDeltaExt=${formatMB(memDelta.external)}` +\n      ` memDeltaAB=${formatMB(memDelta.arrayBuffers)}` +\n      ` memPeakRss=${formatMB(result.memory.peak.rss)}`,\n  )\n}\n\nif (jsonPath) {\n  const payload = {\n    runId: new Date().toISOString(),\n    suite: \"async\",\n    args: args.slice(),\n    results,\n  }\n  const json = JSON.stringify(\n    payload,\n    (_key, value) => {\n      if (typeof value === \"bigint\") return value.toString()\n      return value\n    },\n    2,\n  )\n  await Bun.write(jsonPath, json)\n  console.log(`\\nResults written to ${jsonPath}`)\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/native-span-feed-benchmark.md",
    "content": "# NativeSpanFeed Benchmarks\n\n## Benchmark\n\n### Build\n\nThe benchmark library (`libnative_span_feed_bench`) is built by the Zig bench-ffi step\nwith `ReleaseFast` by default. Override with `-Dbench-optimize=` if needed.\n\n```bash\ncd packages/core/src/zig\nzig build bench-ffi\n```\n\nThis installs `zig-out/lib/libnative_span_feed_bench.*`, which\n`src/benchmark/native-span-feed-benchmark.ts` loads by default.\n\nYou can also run `zig build bench` to build the bench runner and install the FFI bench library in one step.\n\n### Run\n\n```bash\ncd packages/core\nzig build bench-ffi\n```\n\n```bash\nbun bench:ts\n```\n\n```bash\nbun src/benchmark/native-span-feed-benchmark.ts --bytes=100000 --iters=1000 --chunk=65536 --initial=2\n```\n\n### Options\n\nDefaults are optimized (batch drain + reserve path + chunk release flags) with no\nadditional flags required.\n\n- `--bytes=<n>` total bytes produced by Zig per iteration (default: 100000)\n- `--iters=<n>` base iteration count (suite scenarios scale from this; defaults are optimized)\n- `--suite=<quick|default|large|all>` run a scenario suite\n- `--chunk=<n>` chunk size in bytes\n- `--initial=<n>` initial chunk count\n- `--auto=<0|1>` enable auto-commit on full chunks (default: 1)\n- `--commit=<n>` commit every N bytes (0 disables)\n- `--pattern=<str>` override the default ANSI pattern (single-run)\n- `--pattern-type=<ansi|ascii|binary|random>` choose pattern kind (single-run)\n- `--pattern-size=<n>` pattern size in bytes (single-run)\n- `--stdout` write received bytes to stdout\n- `--reuse` reuse a single stream across iterations (may grow memory)\n- `--mem` enable memory tracking\n- `--mem-sample=<n>` sample memory every N iterations (default: 1)\n- `--mem` enable memory tracking\n- `--mem-sample=<n>` sample memory every N iterations (default: 1)\n- `--json[=<path>]` write results to JSON (default: `latest-<suite>-bench-run.json` when `--suite` is set, otherwise `latest-bench-run.json`)\n"
  },
  {
    "path": "packages/core/src/benchmark/native-span-feed-benchmark.ts",
    "content": "import { dlopen, FFIType, suffix } from \"bun:ffi\"\nimport { setRenderLibPath } from \"../zig\"\n\nif (!process.env.NATIVE_SPAN_FEED_LIB) {\n  process.env.NATIVE_SPAN_FEED_LIB = \"bench\"\n}\nconst { NativeSpanFeed } = await import(\"../NativeSpanFeed.ts\")\n\nconst args = process.argv.slice(2)\n\nfunction getArg(name: string): string | null {\n  const prefix = `--${name}=`\n  for (const arg of args) {\n    if (arg.startsWith(prefix)) return arg.slice(prefix.length)\n  }\n  return null\n}\n\nfunction hasFlag(name: string): boolean {\n  return args.includes(`--${name}`)\n}\n\nconst totalBytes = BigInt(getArg(\"bytes\") ?? \"100000\")\nconst iterationsArg = getArg(\"iters\")\nconst iterations = Number(iterationsArg ?? \"1000\")\nconst commitEvery = Number(getArg(\"commit\") ?? \"0\")\nconst chunkSize = Number(getArg(\"chunk\") ?? String(64 * 1024))\nconst initialChunks = Number(getArg(\"initial\") ?? \"2\")\nconst autoArg = getArg(\"auto\")\nconst autoCommitOnFull = autoArg ? autoArg !== \"0\" : true\nconst writeStdout = hasFlag(\"stdout\")\nconst patternArg = getArg(\"pattern\")\nconst patternTypeArg = getArg(\"pattern-type\")\nconst patternSizeArg = getArg(\"pattern-size\")\nconst reuseStream = hasFlag(\"reuse\")\nconst suiteName = getArg(\"suite\")\nconst suiteIterations = suiteName === \"quick\" && iterationsArg === null ? 20000 : iterations\nconst memSampleArg = getArg(\"mem-sample\")\nconst memEnabled = hasFlag(\"mem\") || memSampleArg !== null\nconst memSampleEvery = memSampleArg ? Number(memSampleArg) : 1\nconst jsonArg = getArg(\"json\")\nconst jsonDefault = suiteName ? `latest-${suiteName}-bench-run.json` : \"latest-bench-run.json\"\nconst jsonPath = jsonArg ?? (hasFlag(\"json\") ? jsonDefault : null)\n\nconst libVariant = process.env.NATIVE_SPAN_FEED_LIB\nconst libBase = libVariant === \"bench\" ? \"native_span_feed_bench\" : (libVariant ?? \"native_span_feed\")\nconst libPath = new URL(`../zig/zig-out/lib/lib${libBase}.${suffix}`, import.meta.url).pathname\n\nsetRenderLibPath(libPath)\n\nconst benchLib = dlopen(libPath, {\n  benchProduce: {\n    args: [FFIType.ptr, FFIType.u64, FFIType.ptr, FFIType.u64, FFIType.u32],\n    returns: FFIType.i32,\n  },\n  benchProduceWrite: {\n    args: [FFIType.ptr, FFIType.u64, FFIType.ptr, FFIType.u64, FFIType.u32],\n    returns: FFIType.i32,\n  },\n})\n\nconst encoder = new TextEncoder()\n\ntype PatternSpec =\n  | { type: \"ansi\"; size?: number }\n  | { type: \"ascii\"; size?: number }\n  | { type: \"binary\"; size?: number }\n  | { type: \"random\"; size?: number }\n  | { type: \"string\"; value: string; size?: number }\n\n/**\n * Producer API: \"reserve\" (zero-copy) or \"write\" (copy).\n */\ntype ProducerAPI = \"reserve\" | \"write\"\n\ntype Scenario = {\n  name: string\n  bytes: bigint\n  iters: number\n  chunkSize: number\n  initialChunks: number\n  autoCommitOnFull: boolean\n  commitEvery: number\n  pattern?: PatternSpec\n  reuseStream: boolean\n  producerAPI: ProducerAPI\n}\n\ntype ScenarioResult = {\n  name: string\n  producerAPI: ProducerAPI\n  bytesPerIter: bigint\n  iters: number\n  bytesTotal: bigint\n  avgMs: number\n  medianMs: number\n  p95Ms: number\n  minMs: number\n  maxMs: number\n  throughputMBps: number\n  elapsedMs: number\n  memory?: {\n    start: MemorySample\n    end: MemorySample\n    delta: MemorySample\n    peak: MemorySample\n    samples: number\n  }\n  options: {\n    chunkSize: number\n    initialChunks: number\n    autoCommitOnFull: boolean\n    commitEvery: number\n    reuseStream: boolean\n    pattern?: PatternSpec\n  }\n}\n\ntype MemorySample = {\n  rss: number\n  heapTotal: number\n  heapUsed: number\n  external: number\n  arrayBuffers: number\n}\n\nconst KB = 1024n\nconst MB = 1024n * KB\nconst kb = (value: number) => BigInt(value) * KB\nconst mb = (value: number) => BigInt(value) * MB\nconst patternSizeSmall = 64\nconst patternSizeMedium = 1024\nconst patternSizeLarge = 32768\n\nfunction repeatPattern(base: Uint8Array, size: number): Uint8Array {\n  if (size <= base.byteLength) return base.subarray(0, size)\n  const out = new Uint8Array(size)\n  let offset = 0\n  while (offset < size) {\n    const slice = base.subarray(0, Math.min(base.byteLength, size - offset))\n    out.set(slice, offset)\n    offset += slice.byteLength\n  }\n  return out\n}\n\nfunction buildPattern(spec?: PatternSpec): Uint8Array | null {\n  if (!spec) return null\n  const size = spec.size ?? 0\n  switch (spec.type) {\n    case \"ansi\": {\n      const base = encoder.encode(\"\\x1b[32mnative-span-feed\\x1b[0m\\n\")\n      return size > 0 ? repeatPattern(base, size) : base\n    }\n    case \"ascii\": {\n      const base = encoder.encode(\"native-span-feed\\n\")\n      return size > 0 ? repeatPattern(base, size) : base\n    }\n    case \"string\": {\n      const base = encoder.encode(spec.value)\n      return size > 0 ? repeatPattern(base, size) : base\n    }\n    case \"binary\": {\n      const len = size > 0 ? size : 4096\n      const out = new Uint8Array(len)\n      for (let i = 0; i < len; i += 1) out[i] = i & 0xff\n      return out\n    }\n    case \"random\": {\n      const len = size > 0 ? size : 4096\n      const out = new Uint8Array(len)\n      crypto.getRandomValues(out)\n      return out\n    }\n  }\n}\n\nfunction readMemory(): MemorySample {\n  const usage = process.memoryUsage()\n  return {\n    rss: usage.rss ?? 0,\n    heapTotal: usage.heapTotal ?? 0,\n    heapUsed: usage.heapUsed ?? 0,\n    external: usage.external ?? 0,\n    arrayBuffers: usage.arrayBuffers ?? 0,\n  }\n}\n\nfunction updatePeak(current: MemorySample, peak: MemorySample): void {\n  peak.rss = Math.max(peak.rss, current.rss)\n  peak.heapTotal = Math.max(peak.heapTotal, current.heapTotal)\n  peak.heapUsed = Math.max(peak.heapUsed, current.heapUsed)\n  peak.external = Math.max(peak.external, current.external)\n  peak.arrayBuffers = Math.max(peak.arrayBuffers, current.arrayBuffers)\n}\n\nfunction diffMemory(start: MemorySample, end: MemorySample): MemorySample {\n  return {\n    rss: end.rss - start.rss,\n    heapTotal: end.heapTotal - start.heapTotal,\n    heapUsed: end.heapUsed - start.heapUsed,\n    external: end.external - start.external,\n    arrayBuffers: end.arrayBuffers - start.arrayBuffers,\n  }\n}\n\nfunction formatBytes(value: number): string {\n  const mb = value / (1024 * 1024)\n  return `${mb.toFixed(2)}MB`\n}\n\nfunction createStreamForScenario(\n  scenario: Scenario,\n  onDataBytes: (len: number) => void,\n): ReturnType<typeof NativeSpanFeed.create> {\n  const stream = NativeSpanFeed.create({\n    chunkSize: scenario.chunkSize,\n    initialChunks: scenario.initialChunks,\n    autoCommitOnFull: scenario.autoCommitOnFull,\n  })\n\n  if (writeStdout) {\n    stream.onData((data) => {\n      process.stdout.write(data)\n    })\n  }\n\n  stream.onData((data) => {\n    onDataBytes(data.byteLength)\n  })\n\n  return stream\n}\n\ntype BaseScenario = Omit<Scenario, \"producerAPI\">\n\nfunction withAPIs(bases: BaseScenario[]): Scenario[] {\n  const out: Scenario[] = []\n  for (const base of bases) {\n    out.push({ ...base, producerAPI: \"reserve\" })\n    out.push({ ...base, name: `write/${base.name}`, producerAPI: \"write\" })\n  }\n  return out\n}\n\nfunction makeScenarios(baseIters: number, reuse: boolean): Scenario[] {\n  const quick: BaseScenario[] = [\n    {\n      name: \"ansi_64k\",\n      bytes: kb(64),\n      iters: Math.max(20000, baseIters),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"ansi\", size: patternSizeSmall },\n      reuseStream: reuse,\n    },\n    {\n      name: \"ascii_64k\",\n      bytes: kb(64),\n      iters: Math.max(20000, baseIters),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"ascii\", size: patternSizeSmall },\n      reuseStream: reuse,\n    },\n    {\n      name: \"binary_64k\",\n      bytes: kb(64),\n      iters: Math.max(20000, baseIters),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"binary\", size: patternSizeSmall },\n      reuseStream: reuse,\n    },\n    {\n      name: \"random_64k\",\n      bytes: kb(64),\n      iters: Math.max(20000, baseIters),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"random\", size: patternSizeSmall },\n      reuseStream: reuse,\n    },\n  ]\n\n  const defaultBase: BaseScenario[] = [\n    ...quick,\n    {\n      name: \"medium_1mb\",\n      bytes: mb(1),\n      iters: Math.max(500, Math.floor(baseIters / 10)),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"ascii\", size: patternSizeMedium },\n      reuseStream: reuse,\n    },\n    {\n      name: \"binary_1mb\",\n      bytes: mb(1),\n      iters: Math.max(500, Math.floor(baseIters / 10)),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"binary\", size: patternSizeMedium },\n      reuseStream: reuse,\n    },\n    {\n      name: \"random_1mb\",\n      bytes: mb(1),\n      iters: Math.max(500, Math.floor(baseIters / 10)),\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"random\", size: patternSizeMedium },\n      reuseStream: reuse,\n    },\n    {\n      name: \"commit_4k\",\n      bytes: kb(256),\n      iters: baseIters,\n      chunkSize: 64 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: false,\n      commitEvery: 4096,\n      pattern: { type: \"ascii\", size: patternSizeMedium },\n      reuseStream: reuse,\n    },\n    {\n      name: \"large_32mb\",\n      bytes: mb(32),\n      iters: Math.max(100, Math.floor(baseIters / 50)),\n      chunkSize: 1024 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"ascii\", size: patternSizeLarge },\n      reuseStream: reuse,\n    },\n    {\n      name: \"binary_32mb\",\n      bytes: mb(32),\n      iters: Math.max(100, Math.floor(baseIters / 50)),\n      chunkSize: 1024 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"binary\", size: patternSizeLarge },\n      reuseStream: reuse,\n    },\n    {\n      name: \"random_32mb\",\n      bytes: mb(32),\n      iters: Math.max(100, Math.floor(baseIters / 50)),\n      chunkSize: 1024 * 1024,\n      initialChunks: 2,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"random\", size: patternSizeLarge },\n      reuseStream: reuse,\n    },\n    {\n      name: \"single_chunk_32mb\",\n      bytes: mb(32),\n      iters: Math.max(100, Math.floor(baseIters / 100)),\n      chunkSize: 32 * 1024 * 1024,\n      initialChunks: 1,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"ascii\", size: patternSizeLarge },\n      reuseStream: reuse,\n    },\n    {\n      name: \"huge_chunk_8mb\",\n      bytes: mb(64),\n      iters: Math.max(100, Math.floor(baseIters / 200)),\n      chunkSize: 8 * 1024 * 1024,\n      initialChunks: 1,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"ascii\", size: patternSizeLarge },\n      reuseStream: reuse,\n    },\n  ]\n\n  const largeBase: BaseScenario[] = [\n    ...defaultBase,\n    {\n      name: \"very_large_128mb\",\n      bytes: mb(128),\n      iters: Math.max(100, Math.floor(baseIters / 500)),\n      chunkSize: 8 * 1024 * 1024,\n      initialChunks: 1,\n      autoCommitOnFull: true,\n      commitEvery: 0,\n      pattern: { type: \"ascii\", size: patternSizeLarge },\n      reuseStream: reuse,\n    },\n  ]\n\n  if (suiteName === \"quick\") return withAPIs(quick)\n  if (suiteName === \"large\") return withAPIs(largeBase)\n  if (suiteName === \"all\") return withAPIs(largeBase)\n  return withAPIs(defaultBase)\n}\n\nfunction buildPatternSpec(): PatternSpec | undefined {\n  if (patternTypeArg) {\n    const size = patternSizeArg ? Number(patternSizeArg) : undefined\n    if (patternTypeArg === \"ansi\") return { type: \"ansi\", size }\n    if (patternTypeArg === \"ascii\") return { type: \"ascii\", size }\n    if (patternTypeArg === \"binary\") return { type: \"binary\", size }\n    if (patternTypeArg === \"random\") return { type: \"random\", size }\n  }\n  if (patternArg) {\n    const size = patternSizeArg ? Number(patternSizeArg) : undefined\n    return { type: \"string\", value: patternArg, size }\n  }\n  return undefined\n}\n\nfunction runScenario(scenario: Scenario): ScenarioResult {\n  let received = 0n\n  const memSamples: MemorySample[] = []\n  const memStart = memEnabled ? readMemory() : null\n  const memPeak = memStart ? { ...memStart } : null\n  const onDataBytes = (len: number) => {\n    received += BigInt(len)\n  }\n  let stream: ReturnType<typeof NativeSpanFeed.create> | null = scenario.reuseStream\n    ? createStreamForScenario(scenario, onDataBytes)\n    : null\n  const pattern = buildPattern(scenario.pattern)\n  const patternPtr = pattern ?? null\n  const patternLen = pattern ? BigInt(pattern.byteLength) : 0n\n  const durations: number[] = []\n  let totalElapsed = 0\n\n  for (let i = 0; i < scenario.iters; i += 1) {\n    if (!stream) stream = createStreamForScenario(scenario, onDataBytes)\n    const before = received\n    const start = performance.now()\n    const produceFn =\n      scenario.producerAPI === \"write\" ? benchLib.symbols.benchProduceWrite : benchLib.symbols.benchProduce\n    const status = produceFn(stream.streamPtr, scenario.bytes, patternPtr, patternLen, scenario.commitEvery)\n    const elapsedMs = performance.now() - start\n    totalElapsed += elapsedMs\n    durations.push(elapsedMs)\n\n    if (status !== 0) {\n      console.error(\n        `${scenario.producerAPI === \"write\" ? \"benchProduceWrite\" : \"benchProduce\"} failed: ${status} scenario=${scenario.name}`,\n      )\n      process.exit(1)\n    }\n\n    const produced = received - before\n    if (produced !== scenario.bytes) {\n      console.error(`unexpected byte count scenario=${scenario.name} got=${produced} expected=${scenario.bytes}`)\n    }\n\n    if (!scenario.reuseStream && stream) {\n      stream.close()\n      stream = null\n    }\n\n    if (memEnabled && memSampleEvery > 0 && i % memSampleEvery === 0) {\n      const sample = readMemory()\n      memSamples.push(sample)\n      if (memPeak) updatePeak(sample, memPeak)\n    }\n  }\n\n  if (stream) stream.close()\n\n  const memEnd = memEnabled ? readMemory() : null\n  if (memEnd && memPeak) updatePeak(memEnd, memPeak)\n\n  const sorted = [...durations].sort((a, b) => a - b)\n  const count = durations.length\n  const avg = count > 0 ? totalElapsed / count : 0\n  const median = count > 0 ? (sorted[Math.floor(count / 2)] ?? 0) : 0\n  const p95 = count > 0 ? (sorted[Math.floor(count * 0.95)] ?? 0) : 0\n  const min = count > 0 ? (sorted[0] ?? 0) : 0\n  const max = count > 0 ? (sorted[count - 1] ?? 0) : 0\n\n  const totalSeconds = totalElapsed / 1000\n  const totalBytesAll = received\n  const totalMb = Number(totalBytesAll) / (1024 * 1024)\n  const mbps = totalSeconds > 0 ? totalMb / totalSeconds : 0\n\n  let memSummary = \"\"\n  if (memStart && memEnd && memPeak) {\n    const delta = diffMemory(memStart, memEnd)\n    memSummary =\n      ` memDeltaRss=${formatBytes(delta.rss)}` +\n      ` memDeltaHeap=${formatBytes(delta.heapUsed)}` +\n      ` memDeltaExt=${formatBytes(delta.external)}` +\n      ` memDeltaAB=${formatBytes(delta.arrayBuffers)}` +\n      ` memPeakRss=${formatBytes(memPeak.rss)}`\n  }\n\n  console.log(\n    `scenario=${scenario.name} api=${scenario.producerAPI} iters=${scenario.iters} bytesPerIter=${scenario.bytes} bytesTotal=${totalBytesAll} avgMs=${avg.toFixed(3)} medianMs=${median.toFixed(3)} p95Ms=${p95.toFixed(3)} minMs=${min.toFixed(3)} maxMs=${max.toFixed(3)} throughputMBps=${mbps.toFixed(2)}${memSummary}`,\n  )\n\n  return {\n    name: scenario.name,\n    producerAPI: scenario.producerAPI,\n    bytesPerIter: scenario.bytes,\n    iters: scenario.iters,\n    bytesTotal: totalBytesAll,\n    avgMs: Number(avg.toFixed(6)),\n    medianMs: Number(median.toFixed(6)),\n    p95Ms: Number(p95.toFixed(6)),\n    minMs: Number(min.toFixed(6)),\n    maxMs: Number(max.toFixed(6)),\n    throughputMBps: Number(mbps.toFixed(6)),\n    elapsedMs: Number(totalElapsed.toFixed(6)),\n    memory:\n      memStart && memEnd && memPeak\n        ? {\n            start: memStart,\n            end: memEnd,\n            delta: diffMemory(memStart, memEnd),\n            peak: memPeak,\n            samples: memSamples.length,\n          }\n        : undefined,\n    options: {\n      chunkSize: scenario.chunkSize,\n      initialChunks: scenario.initialChunks,\n      autoCommitOnFull: scenario.autoCommitOnFull,\n      commitEvery: scenario.commitEvery,\n      reuseStream: scenario.reuseStream,\n      pattern: scenario.pattern,\n    },\n  }\n}\n\nfunction createSingleScenario(): Scenario {\n  const apiArg = getArg(\"api\")\n  return {\n    name: \"custom\",\n    bytes: totalBytes,\n    iters: iterations,\n    chunkSize,\n    initialChunks,\n    autoCommitOnFull,\n    commitEvery,\n    pattern: buildPatternSpec(),\n    reuseStream,\n    producerAPI: apiArg === \"write\" ? \"write\" : \"reserve\",\n  }\n}\n\nconst results: ScenarioResult[] = []\nconst runId = new Date().toISOString()\n\nif (suiteName) {\n  const scenarios = makeScenarios(suiteIterations, reuseStream)\n  for (const scenario of scenarios) {\n    results.push(runScenario(scenario))\n  }\n} else {\n  results.push(runScenario(createSingleScenario()))\n}\n\nif (jsonPath) {\n  const payload = {\n    runId,\n    suite: suiteName ?? \"custom\",\n    args: args.slice(),\n    results,\n  }\n  const json = JSON.stringify(\n    payload,\n    (_key, value) => {\n      if (typeof value === \"bigint\") return value.toString()\n      return value\n    },\n    2,\n  )\n  await Bun.write(jsonPath, json)\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/native-span-feed-compare.ts",
    "content": "import { readFileSync, writeFileSync } from \"node:fs\"\nimport { basename } from \"node:path\"\n\ntype ScenarioResult = {\n  name: string\n  bytesPerIter?: string | number\n  iters?: number\n  bytesTotal?: string | number\n  avgMs?: number\n  medianMs?: number\n  p95Ms?: number\n  minMs?: number\n  maxMs?: number\n  throughputMBps?: number\n  elapsedMs?: number\n  memory?: {\n    start: MemorySample\n    end: MemorySample\n    delta: MemorySample\n    peak: MemorySample\n    samples: number\n  }\n  options?: {\n    chunkSize?: number\n    initialChunks?: number\n    autoCommitOnFull?: boolean\n    commitEvery?: number\n    reuseStream?: boolean\n    pattern?: unknown\n  }\n}\n\ntype MemorySample = {\n  rss: number\n  heapTotal: number\n  heapUsed: number\n  external: number\n  arrayBuffers: number\n}\n\ntype BenchRun = {\n  runId?: string\n  suite?: string\n  args?: string[]\n  results?: ScenarioResult[]\n}\n\ntype MetricDelta = {\n  baseline: number\n  current: number\n  delta: number\n  deltaPct: number | null\n}\n\ntype ScenarioDiff = {\n  name: string\n  status: \"ok\" | \"missing_baseline\" | \"missing_current\"\n  bytesPerIter?: string\n  iters?: string\n  optionsDiff?: string[]\n  metrics?: {\n    avgMs?: MetricDelta\n    medianMs?: MetricDelta\n    p95Ms?: MetricDelta\n    throughputMBps?: MetricDelta\n  }\n  memory?: {\n    deltaRss?: MetricDelta\n    deltaExternal?: MetricDelta\n    deltaArrayBuffers?: MetricDelta\n    peakRss?: MetricDelta\n  }\n}\n\nconst args = process.argv.slice(2)\n\nfunction getArg(name: string): string | null {\n  const prefix = `--${name}=`\n  for (const arg of args) {\n    if (arg.startsWith(prefix)) return arg.slice(prefix.length)\n  }\n  return null\n}\n\nfunction hasFlag(name: string): boolean {\n  return args.includes(`--${name}`)\n}\n\nconst jsonPath = getArg(\"json\")\nconst positional = args.filter((arg) => !arg.startsWith(\"--\"))\n\nif (positional.length < 2) {\n  console.error(\"usage: bun src/benchmark/native-span-feed-compare.ts <baseline.json> <current.json> [--json=<path>]\")\n  process.exit(1)\n}\n\nconst baselinePath = positional[0]\nconst currentPath = positional[1]\n\nfunction readBench(path: string): BenchRun {\n  const text = readFileSync(path, \"utf8\")\n  return JSON.parse(text) as BenchRun\n}\n\nfunction toBigInt(value: string | number | undefined): bigint | null {\n  if (value === undefined) return null\n  if (typeof value === \"number\") return BigInt(value)\n  try {\n    return BigInt(value)\n  } catch {\n    return null\n  }\n}\n\nfunction formatBigPair(baseline?: string | number, current?: string | number): string {\n  const base = toBigInt(baseline)\n  const curr = toBigInt(current)\n  if (base == null || curr == null) return \"n/a\"\n  if (base === curr) return base.toString()\n  return `${base.toString()} -> ${curr.toString()}`\n}\n\nfunction formatPair(base: number, curr: number, digits = 3): string {\n  const delta = curr - base\n  const sign = delta >= 0 ? \"+\" : \"\"\n  const pct = base !== 0 ? (delta / base) * 100 : null\n  const pctStr = pct === null ? \"n/a\" : `${sign}${pct.toFixed(2)}%`\n  return `${base.toFixed(digits)} -> ${curr.toFixed(digits)} (${sign}${delta.toFixed(digits)}, ${pctStr})`\n}\n\nfunction formatBytes(value: number): string {\n  const mb = value / (1024 * 1024)\n  return `${mb.toFixed(2)}MB`\n}\n\nfunction formatBytesPair(base: number, curr: number): string {\n  const delta = curr - base\n  const sign = delta >= 0 ? \"+\" : \"\"\n  const pct = base !== 0 ? (delta / base) * 100 : null\n  const pctStr = pct === null ? \"n/a\" : `${sign}${pct.toFixed(2)}%`\n  return `${formatBytes(base)} -> ${formatBytes(curr)} (${sign}${formatBytes(delta)}, ${pctStr})`\n}\n\nfunction metricDelta(base?: number, curr?: number): MetricDelta | undefined {\n  if (base === undefined || curr === undefined) return undefined\n  const delta = curr - base\n  const deltaPct = base !== 0 ? (delta / base) * 100 : null\n  return { baseline: base, current: curr, delta, deltaPct }\n}\n\nfunction diffOptions(base?: ScenarioResult[\"options\"], curr?: ScenarioResult[\"options\"]): string[] | undefined {\n  if (!base || !curr) return undefined\n  const diffs: string[] = []\n  const keys: (keyof NonNullable<ScenarioResult[\"options\"]>)[] = [\n    \"chunkSize\",\n    \"initialChunks\",\n    \"autoCommitOnFull\",\n    \"commitEvery\",\n    \"reuseStream\",\n    \"pattern\",\n  ]\n  for (const key of keys) {\n    const b = base[key]\n    const c = curr[key]\n    if (JSON.stringify(b) !== JSON.stringify(c)) {\n      diffs.push(`${String(key)}:${JSON.stringify(b)}->${JSON.stringify(c)}`)\n    }\n  }\n  return diffs.length > 0 ? diffs : undefined\n}\n\nconst baseline = readBench(baselinePath)\nconst current = readBench(currentPath)\n\nconst baselineMap = new Map<string, ScenarioResult>()\nconst currentMap = new Map<string, ScenarioResult>()\n\nfor (const result of baseline.results ?? []) {\n  baselineMap.set(result.name, result)\n}\nfor (const result of current.results ?? []) {\n  currentMap.set(result.name, result)\n}\n\nconst scenarioNames = Array.from(new Set([...baselineMap.keys(), ...currentMap.keys()])).sort()\n\nconst diffs: ScenarioDiff[] = []\nlet missingBaseline = 0\nlet missingCurrent = 0\n\nfor (const name of scenarioNames) {\n  const base = baselineMap.get(name)\n  const curr = currentMap.get(name)\n  if (!base) {\n    diffs.push({ name, status: \"missing_baseline\" })\n    missingBaseline += 1\n    continue\n  }\n  if (!curr) {\n    diffs.push({ name, status: \"missing_current\" })\n    missingCurrent += 1\n    continue\n  }\n\n  const avg = metricDelta(base.avgMs, curr.avgMs)\n  const med = metricDelta(base.medianMs, curr.medianMs)\n  const p95 = metricDelta(base.p95Ms, curr.p95Ms)\n  const thr = metricDelta(base.throughputMBps, curr.throughputMBps)\n\n  const memDeltaRss = metricDelta(base.memory?.delta?.rss, curr.memory?.delta?.rss)\n  const memDeltaExternal = metricDelta(base.memory?.delta?.external, curr.memory?.delta?.external)\n  const memDeltaArrayBuffers = metricDelta(base.memory?.delta?.arrayBuffers, curr.memory?.delta?.arrayBuffers)\n  const memPeakRss = metricDelta(base.memory?.peak?.rss, curr.memory?.peak?.rss)\n\n  diffs.push({\n    name,\n    status: \"ok\",\n    bytesPerIter: formatBigPair(base.bytesPerIter, curr.bytesPerIter),\n    iters: formatBigPair(base.iters, curr.iters),\n    optionsDiff: diffOptions(base.options, curr.options),\n    metrics: {\n      avgMs: avg,\n      medianMs: med,\n      p95Ms: p95,\n      throughputMBps: thr,\n    },\n    memory: {\n      deltaRss: memDeltaRss,\n      deltaExternal: memDeltaExternal,\n      deltaArrayBuffers: memDeltaArrayBuffers,\n      peakRss: memPeakRss,\n    },\n  })\n}\n\nconst baseLabel = `${baseline.suite ?? \"unknown\"}:${basename(baselinePath)}`\nconst currentLabel = `${current.suite ?? \"unknown\"}:${basename(currentPath)}`\n\nconsole.log(`baseline=${baseLabel}`)\nconsole.log(`current=${currentLabel}`)\nconsole.log(`scenarios=${scenarioNames.length} missingBaseline=${missingBaseline} missingCurrent=${missingCurrent}`)\n\nfor (const diff of diffs) {\n  if (diff.status !== \"ok\") {\n    console.log(`scenario=${diff.name} status=${diff.status}`)\n    continue\n  }\n\n  const metrics = diff.metrics ?? {}\n  const avg = metrics.avgMs ? formatPair(metrics.avgMs.baseline, metrics.avgMs.current) : \"n/a\"\n  const median = metrics.medianMs ? formatPair(metrics.medianMs.baseline, metrics.medianMs.current) : \"n/a\"\n  const p95 = metrics.p95Ms ? formatPair(metrics.p95Ms.baseline, metrics.p95Ms.current) : \"n/a\"\n  const throughput = metrics.throughputMBps\n    ? formatPair(metrics.throughputMBps.baseline, metrics.throughputMBps.current)\n    : \"n/a\"\n  const mem = diff.memory\n  const memDeltaRss = mem?.deltaRss ? formatBytesPair(mem.deltaRss.baseline, mem.deltaRss.current) : \"n/a\"\n  const memDeltaExt = mem?.deltaExternal\n    ? formatBytesPair(mem.deltaExternal.baseline, mem.deltaExternal.current)\n    : \"n/a\"\n  const memDeltaAB = mem?.deltaArrayBuffers\n    ? formatBytesPair(mem.deltaArrayBuffers.baseline, mem.deltaArrayBuffers.current)\n    : \"n/a\"\n  const memPeak = mem?.peakRss ? formatBytesPair(mem.peakRss.baseline, mem.peakRss.current) : \"n/a\"\n  const opts = diff.optionsDiff ? ` opts=${diff.optionsDiff.join(\",\")}` : \"\"\n\n  console.log(\n    `scenario=${diff.name} bytesPerIter=${diff.bytesPerIter} iters=${diff.iters} avgMs=${avg} medianMs=${median} p95Ms=${p95} throughputMBps=${throughput} memDeltaRss=${memDeltaRss} memDeltaExt=${memDeltaExt} memDeltaAB=${memDeltaAB} memPeakRss=${memPeak}${opts}`,\n  )\n}\n\nif (jsonPath) {\n  const payload = {\n    baseline: { path: baselinePath, runId: baseline.runId, suite: baseline.suite },\n    current: { path: currentPath, runId: current.runId, suite: current.suite },\n    scenarios: diffs,\n  }\n  const json = JSON.stringify(payload, null, 2)\n  writeFileSync(jsonPath, json)\n}\n"
  },
  {
    "path": "packages/core/src/benchmark/renderer-benchmark.ts",
    "content": "#!/usr/bin/env bun\n\nimport { createCliRenderer, RGBA, TextRenderable, BoxRenderable, FrameBufferRenderable } from \"../index.js\"\nimport { ThreeCliRenderer } from \"../3d/WGPURenderer.js\"\nimport { TextureUtils } from \"../3d/TextureUtils.js\"\nimport {\n  Scene as ThreeScene,\n  Mesh as ThreeMesh,\n  PerspectiveCamera,\n  Color,\n  Vector2 as ThreeVector2,\n  DirectionalLight as ThreeDirectionalLight,\n  PointLight as ThreePointLight,\n  MeshPhongMaterial,\n  BoxGeometry,\n  SpotLight as ThreeSpotLight,\n  AmbientLight as ThreeAmbientLight,\n} from \"three\"\nimport { MeshPhongNodeMaterial } from \"three/webgpu\"\nimport { lights } from \"three/tsl\"\nimport { Command } from \"commander\"\nimport { existsSync, writeFileSync } from \"node:fs\"\nimport path, { dirname } from \"node:path\"\nimport { mkdir } from \"node:fs/promises\"\n\ntype MemorySnapshot = { heapUsed: number; heapTotal: number; arrayBuffers: number }\n\n// @ts-ignore\nimport cratePath from \"../examples/assets/crate.png\" with { type: \"image/png\" }\n// @ts-ignore\nimport crateEmissivePath from \"../examples/assets/crate_emissive.png\" with { type: \"image/png\" }\n\n// Setup command line options\nconst program = new Command()\nprogram\n  .name(\"renderer-benchmark\")\n  .description(\"3D renderer benchmark for terminal\")\n  .option(\"-d, --duration <ms>\", \"duration of each scenario in milliseconds\", \"10000\")\n  .option(\"-o, --output <path>\", \"path to save benchmark results as JSON\")\n  .option(\"--debug\", \"enable debug mode with culling stats\")\n  .option(\"--no-culling\", \"disable frustum culling for testing\")\n  .parse(process.argv)\n\nconst options = program.opts()\n\nconst SCENARIO_DURATION_MS = parseInt(options.duration)\n\nlet outputPath = options.output\nif (outputPath) {\n  outputPath = path.resolve(process.cwd(), outputPath)\n  if (existsSync(outputPath)) {\n    console.error(`Error: Output file already exists: ${outputPath}`)\n    process.exit(1)\n  }\n\n  try {\n    const dir = dirname(outputPath)\n    if (!existsSync(dir)) {\n      mkdir(dir, { recursive: true })\n    }\n  } catch (error: any) {\n    console.error(`Error: Cannot access output directory: ${error.message}`)\n    process.exit(1)\n  }\n}\n\nenum BenchmarkScenario {\n  SingleCube = 0,\n  MultipleCubes = 1,\n  TexturedCubes = 2,\n  Complete = 3,\n}\n\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: true,\n  targetFps: 60,\n  gatherStats: true,\n  memorySnapshotInterval: 1000,\n})\n\nconst WIDTH = renderer.terminalWidth\nconst HEIGHT = renderer.terminalHeight\n\nconst fbRenderable = new FrameBufferRenderable(renderer, {\n  id: \"main\",\n  width: WIDTH,\n  height: HEIGHT,\n  zIndex: 10,\n})\nrenderer.root.add(fbRenderable)\nconst { frameBuffer: framebuffer } = fbRenderable\n\nconst engine = new ThreeCliRenderer(renderer, {\n  width: WIDTH,\n  height: HEIGHT,\n  focalLength: 8,\n  backgroundColor: RGBA.fromInts(0, 0, 0, 255),\n})\nawait engine.init()\n\nconst sceneRoot = new ThreeScene()\nsceneRoot.name = \"scene_root\"\n\nconst mainLightNode = new ThreeDirectionalLight(new Color(1.0, 1.0, 1.0), 0.8)\nmainLightNode.position.set(-2, -3, 1)\nmainLightNode.target.position.set(0, 0, 0)\nmainLightNode.name = \"main_light\"\nsceneRoot.add(mainLightNode)\nsceneRoot.add(mainLightNode.target)\n\nconst ambientLight = new ThreeAmbientLight(new Color(0.3, 0.3, 0.4), 0.4)\nambientLight.name = \"ambient_light\"\nsceneRoot.add(ambientLight)\n\nconst pointLightNode = new ThreePointLight(new Color(255 / 255, 220 / 255, 180 / 255), 2.0, 300)\npointLightNode.position.set(1.5, 0, -0.5)\npointLightNode.name = \"point_light\"\nsceneRoot.add(pointLightNode)\n\nconst redLightNode = new ThreePointLight(new Color(1.0, 0.2, 0.2), 1.5, 12)\nredLightNode.position.set(-1.5, 1.0, -1.0)\nredLightNode.name = \"red_point_light\"\nsceneRoot.add(redLightNode)\n\nconst blueLightNode = new ThreePointLight(new Color(0.2, 0.2, 1.0), 1.5, 12)\nblueLightNode.position.set(1.5, 2.0, -1.0)\nblueLightNode.name = \"blue_point_light\"\nsceneRoot.add(blueLightNode)\n\nconst spotLightNode = new ThreeSpotLight(new Color(1.0, 0.9, 0.8), 1.2, 25, Math.PI / 3, 0.3, 1)\nspotLightNode.position.set(-8, -6, -3)\nspotLightNode.target.position.set(0, 0, 0)\nspotLightNode.name = \"bottom_left_spotlight\"\nsceneRoot.add(spotLightNode)\nsceneRoot.add(spotLightNode.target)\n\nconst cameraNode = new PerspectiveCamera(45, engine.aspectRatio, 1.0, 150.0)\ncameraNode.position.set(0, 0, -4)\ncameraNode.up.set(0, 1, 0)\ncameraNode.lookAt(0, 0, 0)\ncameraNode.updateMatrixWorld()\ncameraNode.name = \"main_camera\"\nsceneRoot.add(cameraNode)\n\nconst cameraLight = new ThreeDirectionalLight(new Color(0.8, 0.8, 0.7), 0.7)\ncameraLight.position.set(0, 0, -4)\ncameraLight.target.position.set(0, 0, 1)\ncameraLight.name = \"camera_light\"\nsceneRoot.add(cameraLight)\n\nengine.setActiveCamera(cameraNode)\n\nconst TEST_CUBE_COUNT = 300\n\nconst uiContainer = new BoxRenderable(renderer, {\n  id: \"ui-container\",\n  zIndex: 15,\n})\nrenderer.root.add(uiContainer)\n\nconst benchmarkStatus = new TextRenderable(renderer, {\n  id: \"benchmark\",\n  content: \"Initializing benchmark...\",\n  zIndex: 20,\n})\nuiContainer.add(benchmarkStatus)\n\nconst cubeCountStatus = new TextRenderable(renderer, {\n  id: \"cube-count\",\n  content: `Test cubes outside view: ${TEST_CUBE_COUNT}`,\n  position: \"absolute\",\n  left: 0,\n  top: 1,\n  zIndex: 20,\n})\nuiContainer.add(cubeCountStatus)\n\nif (options.debug) {\n  const debugStatus = new TextRenderable(renderer, {\n    id: \"debug\",\n    content: `Culling: ${options.culling !== false ? \"ON\" : \"OFF\"}`,\n    position: \"absolute\",\n    left: 0,\n    top: HEIGHT - 1,\n    zIndex: 20,\n  })\n  uiContainer.add(debugStatus)\n}\n\ntype ScenarioResult = {\n  name: string\n  frameCount: number\n  fps: number\n  averageFrameTime: number\n  minFrameTime: number\n  maxFrameTime: number\n  stdDev: number\n  memorySnapshots?: MemorySnapshot[]\n}\n\n// Benchmark state\nlet time = 0\nlet currentScenario = BenchmarkScenario.SingleCube\nlet benchmarkStartTime = 0\nlet benchmarkActive = true\nconst results: ScenarioResult[] = []\nlet currentMemorySnapshots: MemorySnapshot[] = []\nlet cubeMeshNodes: ThreeMesh[] = []\nconst RADIUS = 1\nconst MULTIPLE_CUBES_COUNT = 8\n\nconst singleCubeGeometry = new BoxGeometry(2.0, 2.0, 2.0)\nsingleCubeGeometry.computeBoundingSphere()\nconst multiCubeGeometry = new BoxGeometry(1.0, 1.0, 1.0)\nmultiCubeGeometry.computeBoundingSphere()\nconst cullingCubeGeometry = new BoxGeometry(0.5, 0.5, 0.5)\ncullingCubeGeometry.computeBoundingSphere()\n\nconst normalMapTexture = TextureUtils.createNoise(128, 2, 3, new Color(0.5, 0.5, 1), new Color(0.5, 0.5, 0.5))\n\nconst singleCubeMaterial = new MeshPhongMaterial({\n  color: 0xffffff,\n  shininess: 15,\n  specular: new Color(0.4, 0.4, 0.4),\n  normalMap: normalMapTexture,\n  normalScale: new ThreeVector2(0.3, 0.3),\n})\n\nconst cullingCubeMaterial = new MeshPhongMaterial({ color: 0x555555, shininess: 10 })\nlet texturedMaterial: MeshPhongNodeMaterial | null = null\nlet multiCubeMaterials: MeshPhongMaterial[] = []\nfor (let i = 0; i < MULTIPLE_CUBES_COUNT; i++) {\n  const baseColor = new Color()\n  const hue = i / MULTIPLE_CUBES_COUNT\n  baseColor.setHSL(hue, 0.6, 0.9)\n  multiCubeMaterials.push(new MeshPhongMaterial({ color: baseColor, shininess: 30, reflectivity: 1.5 }))\n}\n\nfunction clearPreviousCubes() {\n  for (const node of cubeMeshNodes) {\n    sceneRoot.remove(node)\n  }\n  cubeMeshNodes = []\n}\n\nfunction updateTextContent(textId: string, content: string) {\n  const textObj = uiContainer.getRenderable(textId) as TextRenderable\n  if (textObj) {\n    textObj.content = content\n  }\n}\n\nasync function setupScenario(scenario: BenchmarkScenario) {\n  clearPreviousCubes()\n  currentMemorySnapshots = []\n  renderer.resetStats()\n\n  switch (scenario) {\n    case BenchmarkScenario.SingleCube:\n      createSingleCubeScenario()\n      break\n    case BenchmarkScenario.MultipleCubes:\n      createMultipleCubesScenario()\n      break\n    case BenchmarkScenario.TexturedCubes:\n      await createTexturedCubesScenario()\n      break\n  }\n\n  addOutOfViewCubes()\n}\n\n// Scenario 1: Single fast-rotating cube\nfunction createSingleCubeScenario() {\n  updateTextContent(\"benchmark\", `Running Scenario 1/3: Single Fast Cube (${SCENARIO_DURATION_MS / 1000}s)`)\n\n  const cubeMesh = new ThreeMesh(singleCubeGeometry, singleCubeMaterial)\n  cubeMesh.name = \"cube_1\"\n\n  cubeMesh.position.set(0, 0, 0)\n  cubeMesh.rotation.set(0, 0, 0)\n  cubeMesh.scale.set(1.0, 1.0, 1.0)\n\n  sceneRoot.add(cubeMesh)\n  cubeMeshNodes.push(cubeMesh)\n}\n\n// Scenario 2: Multiple moving and spinning cubes\nfunction createMultipleCubesScenario() {\n  updateTextContent(\"benchmark\", `Running Scenario 2/3: Multiple Moving Cubes (${SCENARIO_DURATION_MS / 1000}s)`)\n\n  blueLightNode.position.set(0, 0, 0)\n  redLightNode.position.set(-1, 0, -1)\n  cameraLight.intensity = 1.5\n  pointLightNode.position.set(-1, 0, 0)\n  pointLightNode.intensity = 3.5\n\n  for (let i = 0; i < MULTIPLE_CUBES_COUNT; i++) {\n    const angle = (i / MULTIPLE_CUBES_COUNT) * Math.PI * 2\n    const x = Math.cos(angle) * RADIUS\n    const y = Math.sin(angle) * RADIUS\n\n    const cubeMesh = new ThreeMesh(multiCubeGeometry, multiCubeMaterials[i])\n    cubeMesh.name = `cube_${i + 1}`\n\n    cubeMesh.position.set(x, y, 0)\n    cubeMesh.rotation.set(i * 0.2, i * 0.3, i * 0.1)\n    cubeMesh.scale.set(0.8, 0.8, 0.8)\n\n    sceneRoot.add(cubeMesh)\n    cubeMeshNodes.push(cubeMesh)\n  }\n}\n\n// Scenario 3: Textured cubes with emissive maps\nasync function createTexturedCubesScenario() {\n  updateTextContent(\"benchmark\", `Running Scenario 3/3: Textured Cubes (${SCENARIO_DURATION_MS / 1000}s)`)\n\n  blueLightNode.position.set(1, 0, -2)\n  redLightNode.position.set(-1, 0, -3)\n  cameraLight.intensity = 3.0\n  mainLightNode.intensity = 2.0\n  pointLightNode.power = 1000\n  redLightNode.power = 800\n  blueLightNode.power = 800\n  spotLightNode.intensity = 2.5\n\n  const allLightsNode = lights([\n    mainLightNode,\n    pointLightNode,\n    redLightNode,\n    blueLightNode,\n    spotLightNode,\n    cameraLight,\n    ambientLight,\n  ])\n\n  if (!texturedMaterial) {\n    const imagePath = cratePath\n    const emissivePath = crateEmissivePath\n    const textureMap = await TextureUtils.fromFile(imagePath)\n    const emissiveMap = await TextureUtils.fromFile(emissivePath)\n\n    if (!textureMap || !emissiveMap) {\n      console.error(\"Failed to load texture or emissive map. Skipping textured scenario.\")\n      createMultipleCubesScenario()\n      updateTextContent(\"benchmark\", `Scenario 3/3 SKIPPED (Texture Load Fail). Using Multi-Cube.`)\n      return\n    }\n\n    texturedMaterial = new MeshPhongNodeMaterial({\n      map: textureMap,\n      emissiveMap: emissiveMap,\n      emissive: new Color(0x000000),\n      emissiveIntensity: 0.5,\n      shininess: 30,\n    })\n    texturedMaterial.lightsNode = allLightsNode\n  }\n\n  // Create 8 cubes in a pattern using the textured material\n  for (let i = 0; i < MULTIPLE_CUBES_COUNT; i++) {\n    const angle = (i / MULTIPLE_CUBES_COUNT) * Math.PI * 2\n    const x = Math.cos(angle) * RADIUS\n    const y = Math.sin(angle) * RADIUS\n\n    const cubeMesh = new ThreeMesh(multiCubeGeometry, texturedMaterial)\n    cubeMesh.name = `cube_${i + 1}`\n\n    cubeMesh.position.set(x, y, 0)\n    cubeMesh.rotation.set(i * 0.2, i * 0.3, i * 0.1)\n    cubeMesh.scale.set(0.8, 0.8, 0.8)\n\n    sceneRoot.add(cubeMesh)\n    cubeMeshNodes.push(cubeMesh)\n  }\n}\n\nfunction addOutOfViewCubes() {\n  const fov = 45 * (Math.PI / 180)\n  const distance = 50\n  const viewHeight = 2 * distance * Math.tan(fov / 2)\n  const viewWidth = viewHeight * engine.aspectRatio\n  const margin = 3.0\n  const boundWidth = viewWidth * margin\n  const boundHeight = viewHeight * margin\n\n  for (let i = 0; i < TEST_CUBE_COUNT; i++) {\n    let x, y, z\n    const placement = i % 7\n    const distMultiplier = 1 + Math.floor(i / 50)\n    if (placement === 0) {\n      x = -boundWidth * distMultiplier - Math.random() * 50\n      y = (Math.random() * 2 - 1) * boundHeight * distMultiplier\n      z = Math.random() * 30 - 15\n    } else if (placement === 1) {\n      x = boundWidth * distMultiplier + Math.random() * 50\n      y = (Math.random() * 2 - 1) * boundHeight * distMultiplier\n      z = Math.random() * 30 - 15\n    } else if (placement === 2) {\n      x = (Math.random() * 2 - 1) * boundWidth * distMultiplier\n      y = boundHeight * distMultiplier + Math.random() * 50\n      z = Math.random() * 30 - 15\n    } else if (placement === 3) {\n      x = (Math.random() * 2 - 1) * boundWidth * distMultiplier\n      y = -boundHeight * distMultiplier - Math.random() * 50\n      z = Math.random() * 30 - 15\n    } else if (placement === 4) {\n      x = (Math.random() * 2 - 1) * boundWidth\n      y = (Math.random() * 2 - 1) * boundHeight\n      z = -100 * distMultiplier - Math.random() * 100\n    } else if (placement === 5) {\n      x = (Math.random() * 2 - 1) * boundWidth\n      y = (Math.random() * 2 - 1) * boundHeight\n      z = 160 * distMultiplier + Math.random() * 100\n    } else {\n      x = (Math.random() > 0.5 ? 1 : -1) * boundWidth * distMultiplier\n      y = (Math.random() > 0.5 ? 1 : -1) * boundHeight * distMultiplier\n      z = (Math.random() > 0.5 ? 160 : -100) * distMultiplier\n    }\n\n    const cubeMesh = new ThreeMesh(cullingCubeGeometry, cullingCubeMaterial)\n    cubeMesh.name = `culling_test_cube_${i}`\n    cubeMesh.position.set(x, y, z)\n\n    cubeMesh.rotation.set(Math.random() * Math.PI * 2, Math.random() * Math.PI * 2, Math.random() * Math.PI * 2)\n\n    const scaleVal = 0.3 + Math.random() * 0.4\n    cubeMesh.scale.set(scaleVal, scaleVal, scaleVal)\n\n    sceneRoot.add(cubeMesh)\n    cubeMeshNodes.push(cubeMesh)\n  }\n}\n\n// Setup first scenario\nawait setupScenario(currentScenario)\n\nrenderer.setFrameCallback(async (deltaMs) => {\n  const deltaTime = deltaMs / 1000\n\n  if (benchmarkStartTime === 0) {\n    benchmarkStartTime = Date.now()\n    renderer.resetStats()\n  }\n\n  time += deltaTime\n  const elapsedTime = Date.now() - benchmarkStartTime\n\n  switch (currentScenario) {\n    case BenchmarkScenario.SingleCube:\n      if (cubeMeshNodes.length > 0) {\n        const mesh = cubeMeshNodes[0]\n        mesh.rotation.x += 1.5 * deltaTime\n        mesh.rotation.y += 2.0 * deltaTime\n        mesh.rotation.z += 0.8 * deltaTime\n        if (mesh.material instanceof MeshPhongMaterial) {\n          const hue = (time * 0.1) % 1\n          mesh.material.color.setHSL(hue, 0.7, 0.8)\n        }\n      }\n      break\n\n    case BenchmarkScenario.MultipleCubes:\n    case BenchmarkScenario.TexturedCubes:\n      for (let i = 0; i < MULTIPLE_CUBES_COUNT; i++) {\n        if (i >= cubeMeshNodes.length) continue\n        const mesh = cubeMeshNodes[i]\n\n        mesh.rotation.x += (0.5 + i * 0.1) * deltaTime\n        mesh.rotation.y += (0.8 + i * 0.1) * deltaTime\n        mesh.rotation.z += (0.3 + i * 0.1) * deltaTime\n\n        const angle = time + (i / MULTIPLE_CUBES_COUNT) * Math.PI * 2\n        mesh.position.x = Math.cos(angle) * RADIUS\n        mesh.position.y = Math.sin(angle) * RADIUS\n        mesh.position.z = Math.sin(angle * 0.5) * 2\n\n        if (currentScenario === BenchmarkScenario.MultipleCubes && mesh.material instanceof MeshPhongMaterial) {\n          const hue = (i / MULTIPLE_CUBES_COUNT + time * 0.05) % 1\n          mesh.material.color.setHSL(hue, 0.6, 0.85)\n        }\n      }\n      break\n  }\n\n  engine.drawScene(sceneRoot, framebuffer, deltaTime)\n\n  if (benchmarkActive && elapsedTime >= SCENARIO_DURATION_MS) {\n    const stats = renderer.getStats()\n    let stdDev = 0\n    if (stats.frameTimes.length > 0) {\n      let variance = 0\n      for (const ft of stats.frameTimes) {\n        variance += Math.pow(ft - stats.averageFrameTime, 2)\n      }\n      stdDev = Math.sqrt(variance / stats.frameTimes.length)\n    }\n    results.push({\n      name: getScenarioName(currentScenario),\n      frameCount: stats.frameCount,\n      fps: stats.fps,\n      averageFrameTime: stats.averageFrameTime,\n      minFrameTime: stats.minFrameTime,\n      maxFrameTime: stats.maxFrameTime,\n      stdDev: stdDev,\n      memorySnapshots: [...currentMemorySnapshots],\n    })\n    currentScenario++\n    if (currentScenario < BenchmarkScenario.Complete) {\n      setupScenario(currentScenario).then(() => {\n        benchmarkStartTime = Date.now()\n      })\n    } else {\n      benchmarkActive = false\n      displayBenchmarkResults()\n      renderer.pause()\n    }\n  }\n})\n\nfunction getScenarioName(scenario: BenchmarkScenario): string {\n  switch (scenario) {\n    case BenchmarkScenario.SingleCube:\n      return \"Single Fast Cube\"\n    case BenchmarkScenario.MultipleCubes:\n      return \"Multiple Moving Cubes\"\n    case BenchmarkScenario.TexturedCubes:\n      return \"Textured Cubes with Emissive Maps\"\n    default:\n      return \"Unknown\"\n  }\n}\n\nfunction displayBenchmarkResults(): void {\n  const resultsBox = new BoxRenderable(renderer, {\n    id: \"results-box\",\n    position: \"absolute\",\n    left: Math.floor(WIDTH / 6),\n    top: Math.floor(HEIGHT / 6),\n    width: Math.floor((WIDTH * 2) / 3),\n    height: Math.floor((HEIGHT * 2) / 3),\n    backgroundColor: RGBA.fromInts(10, 10, 40),\n    zIndex: 30,\n  })\n  uiContainer.add(resultsBox)\n\n  const resultsTitle = new TextRenderable(renderer, {\n    id: \"results-title\",\n    position: \"absolute\",\n    left: Math.floor(WIDTH / 6) + 2,\n    top: Math.floor(HEIGHT / 6) + 1,\n    content: \"📊 BENCHMARK RESULTS 📊\",\n    zIndex: 31,\n  })\n  uiContainer.add(resultsTitle)\n  let y = Math.floor(HEIGHT / 6) + 3\n  for (let i = 0; i < results.length; i++) {\n    const result = results[i]\n    const resultHeader = new TextRenderable(renderer, {\n      id: `result-header-${i}`,\n      position: \"absolute\",\n      left: Math.floor(WIDTH / 6) + 2,\n      top: y++,\n      content: `Scenario ${i + 1}: ${result.name}`,\n      zIndex: 31,\n    })\n    uiContainer.add(resultHeader)\n\n    const statLines = [\n      `  • Frames: ${result.frameCount} | FPS: ${result.fps}`,\n      `  • Frame Time: ${result.averageFrameTime.toFixed(2)}ms (min: ${result.minFrameTime.toFixed(2)}ms, max: ${result.maxFrameTime.toFixed(2)}ms)`,\n      `  • Standard Deviation: ${result.stdDev.toFixed(2)}ms`,\n    ]\n    for (let j = 0; j < statLines.length; j++) {\n      const statText = new TextRenderable(renderer, {\n        id: `result-stat-${i}-${j}`,\n        position: \"absolute\",\n        left: Math.floor(WIDTH / 6) + 2,\n        top: y + j,\n        content: statLines[j],\n        zIndex: 31,\n      })\n      uiContainer.add(statText)\n    }\n    y += statLines.length\n\n    if (result.memorySnapshots && result.memorySnapshots.length > 0) {\n      const heapUsedValues = result.memorySnapshots.map((s) => s.heapUsed).sort((a, b) => a - b)\n      const minMem = heapUsedValues[0]\n      const maxMem = heapUsedValues[heapUsedValues.length - 1]\n      const avgMem = heapUsedValues.reduce((sum, val) => sum + val, 0) / heapUsedValues.length\n      const midIndex = Math.floor(heapUsedValues.length / 2)\n      const medianMem =\n        heapUsedValues.length % 2 === 0\n          ? (heapUsedValues[midIndex - 1] + heapUsedValues[midIndex]) / 2\n          : heapUsedValues[midIndex]\n\n      const arrayBufferValues = result.memorySnapshots.map((s) => s.arrayBuffers).sort((a, b) => a - b)\n      const minAB = arrayBufferValues[0]\n      const maxAB = arrayBufferValues[arrayBufferValues.length - 1]\n      const avgAB = arrayBufferValues.reduce((sum, val) => sum + val, 0) / arrayBufferValues.length\n      const midABIndex = Math.floor(arrayBufferValues.length / 2)\n      const medianAB =\n        arrayBufferValues.length % 2 === 0\n          ? (arrayBufferValues[midABIndex - 1] + arrayBufferValues[midABIndex]) / 2\n          : arrayBufferValues[midABIndex]\n\n      const memStatLines = [\n        `  • Heap Used: ${(avgMem / 1024 / 1024).toFixed(2)}MB avg`,\n        `    (min: ${(minMem / 1024 / 1024).toFixed(2)}MB, max: ${(maxMem / 1024 / 1024).toFixed(2)}MB, median: ${(medianMem / 1024 / 1024).toFixed(2)}MB)`,\n        `  • ArrayBuffers: ${(avgAB / 1024 / 1024).toFixed(2)}MB avg`,\n        `    (min: ${(minAB / 1024 / 1024).toFixed(2)}MB, max: ${(maxAB / 1024 / 1024).toFixed(2)}MB, median: ${(medianAB / 1024 / 1024).toFixed(2)}MB)`,\n      ]\n      for (let j = 0; j < memStatLines.length; j++) {\n        const memStatText = new TextRenderable(renderer, {\n          id: `result-mem-stat-${i}-${j}`,\n          position: \"absolute\",\n          left: Math.floor(WIDTH / 6) + 2,\n          top: y + j,\n          content: memStatLines[j],\n          zIndex: 31,\n        })\n        uiContainer.add(memStatText)\n      }\n      y += memStatLines.length\n    }\n    y++\n  }\n  if (results.length > 1) {\n    const comparisonTitle = new TextRenderable(renderer, {\n      id: \"results-comparison\",\n      position: \"absolute\",\n      left: Math.floor(WIDTH / 6) + 2,\n      top: y++,\n      content: \"Performance Comparison:\",\n      zIndex: 31,\n    })\n    uiContainer.add(comparisonTitle)\n\n    for (let i = 1; i < results.length; i++) {\n      const basePerf = results[0].averageFrameTime\n      const currentPerf = results[i].averageFrameTime\n      const ratio = currentPerf / basePerf\n      const percent = ((ratio - 1) * 100).toFixed(1)\n      const compareText = `  • ${results[i].name}: ${ratio > 1 ? \"+\" : \"\"}${percent}% frame time vs. baseline`\n      const compareTextObj = new TextRenderable(renderer, {\n        id: `result-compare-${i}`,\n        position: \"absolute\",\n        left: Math.floor(WIDTH / 6) + 2,\n        top: y++,\n        content: compareText,\n        zIndex: 31,\n      })\n      uiContainer.add(compareTextObj)\n    }\n  }\n\n  const resultsFooter = new TextRenderable(renderer, {\n    id: \"results-footer\",\n    position: \"absolute\",\n    left: Math.floor(WIDTH / 6) + 2,\n    top: Math.floor((HEIGHT * 5) / 6) - 2,\n    content: \"Press Ctrl+C to exit\",\n    zIndex: 31,\n  })\n  uiContainer.add(resultsFooter)\n\n  if (outputPath) {\n    try {\n      const jsonResults = {\n        date: new Date().toISOString(),\n        scenarios: results,\n        comparison:\n          results.length > 1\n            ? results.slice(1).map((result) => {\n                const basePerf = results[0].averageFrameTime\n                const currentPerf = result.averageFrameTime\n                const ratio = currentPerf / basePerf\n                const percent = (ratio - 1) * 100\n                return { name: result.name, ratio, percentDifference: percent }\n              })\n            : [],\n      }\n\n      for (const result of jsonResults.scenarios) {\n        const scenarioResult = result as ScenarioResult\n        if (scenarioResult.memorySnapshots && scenarioResult.memorySnapshots.length > 0) {\n          const heapUsedValues = scenarioResult.memorySnapshots.map((s) => s.heapUsed).sort((a, b) => a - b)\n          const minMem = heapUsedValues[0]\n          const maxMem = heapUsedValues[heapUsedValues.length - 1]\n          const avgMem = heapUsedValues.reduce((sum, val) => sum + val, 0) / heapUsedValues.length\n          const midIndex = Math.floor(heapUsedValues.length / 2)\n          const medianMem =\n            heapUsedValues.length % 2 === 0\n              ? (heapUsedValues[midIndex - 1] + heapUsedValues[midIndex]) / 2\n              : heapUsedValues[midIndex]\n\n          const arrayBufferValues = scenarioResult.memorySnapshots.map((s) => s.arrayBuffers).sort((a, b) => a - b)\n          const minAB = arrayBufferValues[0]\n          const maxAB = arrayBufferValues[arrayBufferValues.length - 1]\n          const avgAB = arrayBufferValues.reduce((sum, val) => sum + val, 0) / arrayBufferValues.length\n          const midABIndex = Math.floor(arrayBufferValues.length / 2)\n          const medianAB =\n            arrayBufferValues.length % 2 === 0\n              ? (arrayBufferValues[midABIndex - 1] + arrayBufferValues[midABIndex]) / 2\n              : arrayBufferValues[midABIndex]\n\n          ;(scenarioResult as any).memoryStats = {\n            minHeapUsedMB: minMem / 1024 / 1024,\n            maxHeapUsedMB: maxMem / 1024 / 1024,\n            averageHeapUsedMB: avgMem / 1024 / 1024,\n            medianHeapUsedMB: medianMem / 1024 / 1024,\n            minArrayBuffersMB: minAB / 1024 / 1024,\n            maxArrayBuffersMB: maxAB / 1024 / 1024,\n            averageArrayBuffersMB: avgAB / 1024 / 1024,\n            medianArrayBuffersMB: medianAB / 1024 / 1024,\n          }\n        }\n        delete (scenarioResult as Partial<ScenarioResult>).memorySnapshots\n      }\n\n      writeFileSync(outputPath, JSON.stringify(jsonResults, null, 2))\n    } catch (error: any) {\n      console.error(`Error saving results to ${outputPath}: ${error.message}`)\n    }\n  }\n}\n\nrenderer.on(\"resize\", (width, height) => {\n  framebuffer.resize(width, height)\n  if (cameraNode) {\n    cameraNode.aspect = engine.aspectRatio\n    cameraNode.updateProjectionMatrix()\n  }\n})\n\nrenderer.on(\"memory:snapshot\", (snapshot: MemorySnapshot) => {\n  if (benchmarkActive) {\n    currentMemorySnapshots.push(snapshot)\n  }\n})\n\nrenderer.toggleDebugOverlay()\n\nprocess.stdin.on(\"data\", (key: Buffer) => {\n  const keyStr = key.toString()\n\n  if (keyStr === \"`\") {\n    renderer.console.toggle()\n  }\n})\n\nrenderer.start()\n"
  },
  {
    "path": "packages/core/src/benchmark/text-table-benchmark.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  TextTableRenderable,\n  type TextTableCellContent,\n  type TextTableColumnFitter,\n  type TextTableColumnWidthMode,\n  type TextTableContent,\n  type CliRenderer,\n} from \"../index\"\nimport { createTestRenderer } from \"../testing\"\nimport { Command } from \"commander\"\nimport { existsSync } from \"node:fs\"\nimport { mkdir } from \"node:fs/promises\"\nimport path from \"node:path\"\n\nconst realStdoutWrite = process.stdout.write.bind(process.stdout)\n\nconst WORDS = [\n  \"alpha\",\n  \"bravo\",\n  \"charlie\",\n  \"delta\",\n  \"echo\",\n  \"foxtrot\",\n  \"golf\",\n  \"hotel\",\n  \"india\",\n  \"juliet\",\n  \"kilo\",\n  \"lima\",\n  \"mango\",\n  \"nectar\",\n  \"oscar\",\n  \"papa\",\n  \"quartz\",\n  \"romeo\",\n  \"sierra\",\n  \"tango\",\n  \"uniform\",\n  \"vector\",\n  \"whiskey\",\n  \"xray\",\n  \"yankee\",\n  \"zulu\",\n  \"matrix\",\n  \"signal\",\n  \"tensor\",\n  \"render\",\n  \"schema\",\n  \"buffer\",\n  \"layout\",\n  \"stream\",\n  \"parser\",\n  \"syntax\",\n  \"viewport\",\n  \"cursor\",\n]\n\ntype MemorySample = {\n  rss: number\n  heapTotal: number\n  heapUsed: number\n  external: number\n  arrayBuffers: number\n}\n\ntype MemoryStats = {\n  samples: number\n  start: MemorySample\n  end: MemorySample\n  delta: MemorySample\n  peak: MemorySample\n}\n\ntype TimingStats = {\n  count: number\n  averageMs: number\n  medianMs: number\n  p95Ms: number\n  minMs: number\n  maxMs: number\n  stdDevMs: number\n}\n\ntype ScenarioResult = {\n  name: string\n  description: string\n  category: \"replace\" | \"incremental\" | \"selection\"\n  timingMode: \"content-set-and-render\" | \"selection-update-and-render\"\n  iterations: number\n  warmupIterations: number\n  elapsedMs: number\n  updateStats: TimingStats\n  memoryStats?: MemoryStats\n  tableStats: {\n    initialRows: number\n    finalRows: number\n    maxRows: number\n    columns: number\n    updates: number\n    datasetVariants: number\n  }\n  settings: Record<string, unknown>\n}\n\ntype ReplaceScenarioPlan = {\n  kind: \"replace\"\n  name: string\n  description: string\n  iterations: number\n  warmupIterations: number\n  rows: number\n  cols: number\n  variants: TextTableContent[]\n  tableConfig: BenchmarkTableConfig\n}\n\ntype IncrementalScenarioPlan = {\n  kind: \"incremental\"\n  name: string\n  description: string\n  iterations: number\n  warmupIterations: number\n  cols: number\n  header: TextTableCellContent[]\n  baseRows: TextTableCellContent[][]\n  rowPool: TextTableCellContent[][]\n  maxRows: number\n  tableConfig: BenchmarkTableConfig\n}\n\ntype SelectionScenarioPlan = {\n  kind: \"selection\"\n  name: string\n  description: string\n  iterations: number\n  warmupIterations: number\n  rows: number\n  cols: number\n  content: TextTableContent\n  dragSteps: number\n  tableConfig: BenchmarkTableConfig\n}\n\ntype ScenarioPlan = ReplaceScenarioPlan | IncrementalScenarioPlan | SelectionScenarioPlan\n\ntype BenchmarkTableConfig = {\n  wrapMode: \"none\" | \"char\" | \"word\"\n  columnWidthMode: TextTableColumnWidthMode\n  columnFitter: TextTableColumnFitter\n}\n\ntype RunContext = {\n  renderer: CliRenderer\n  table: TextTableRenderable\n  renderOnce: () => Promise<void>\n  memSampleEvery: number\n}\n\ntype SuiteConfig = {\n  iterations: number\n  warmupIterations: number\n  longIterations: number\n  scale: number\n}\n\ntype OutputMeta = {\n  suiteName: string\n  width: number\n  height: number\n  iterations: number\n  warmupIterations: number\n  longIterations: number\n  scale: number\n  seed: number\n  memSampleEvery: number\n}\n\ntype IncrementalState = {\n  rows: TextTableCellContent[][]\n  cursor: number\n  maxRowsSeen: number\n}\n\nconst program = new Command()\nprogram\n  .name(\"text-table-benchmark\")\n  .description(\"TextTableRenderable benchmark scenarios\")\n  .option(\"-s, --suite <name>\", \"benchmark suite: quick, default, long\", \"default\")\n  .option(\"-i, --iterations <count>\", \"iterations per scenario\", \"800\")\n  .option(\"--warmup-iterations <count>\", \"warmup iterations per scenario\", \"80\")\n  .option(\"--long-iterations <count>\", \"iterations for long suite\", \"3000\")\n  .option(\"--scale <n>\", \"scale rows and dataset size\", \"1\")\n  .option(\"--seed <n>\", \"seed for deterministic content\", \"1337\")\n  .option(\"--width <n>\", \"test renderer width\", \"140\")\n  .option(\"--height <n>\", \"test renderer height\", \"48\")\n  .option(\"--mem-sample-every <count>\", \"sample memory every N iterations (0 disables)\", \"10\")\n  .option(\"--scenario <name>\", \"run a single scenario\")\n  .option(\"--json [path]\", \"write JSON results to file\")\n  .option(\"--no-output\", \"suppress stdout output\")\n  .parse(process.argv)\n\nconst options = program.opts()\n\nconst suiteName = String(options.suite)\nconst iterations = Math.max(1, Math.floor(toNumber(options.iterations, 800)))\nconst warmupIterations = Math.max(0, Math.floor(toNumber(options.warmupIterations, 80)))\nconst longIterations = Math.max(iterations, Math.floor(toNumber(options.longIterations, 3000)))\nconst scale = Math.max(0.25, toNumber(options.scale, 1))\nconst seed = Math.max(1, Math.floor(toNumber(options.seed, 1337)))\nconst width = Math.max(40, Math.floor(toNumber(options.width, 140)))\nconst height = Math.max(12, Math.floor(toNumber(options.height, 48)))\nconst memSampleEvery = Math.max(0, Math.floor(toNumber(options.memSampleEvery, 10)))\nconst scenarioFilter = options.scenario ? String(options.scenario) : null\nconst outputEnabled = options.output !== false\n\nconst PROPORTIONAL_TABLE_CONFIG: BenchmarkTableConfig = {\n  wrapMode: \"word\",\n  columnWidthMode: \"full\",\n  columnFitter: \"proportional\",\n}\n\nconst BALANCED_TABLE_CONFIG: BenchmarkTableConfig = {\n  wrapMode: \"word\",\n  columnWidthMode: \"full\",\n  columnFitter: \"balanced\",\n}\n\nconst jsonArg = options.json\nconst jsonPath =\n  typeof jsonArg === \"string\"\n    ? path.resolve(process.cwd(), jsonArg)\n    : jsonArg\n      ? path.resolve(process.cwd(), \"latest-text-table-bench-run.json\")\n      : null\n\nif (jsonPath) {\n  const dir = path.dirname(jsonPath)\n  if (!existsSync(dir)) {\n    await mkdir(dir, { recursive: true })\n  }\n  if (existsSync(jsonPath)) {\n    console.error(`Error: output file already exists: ${jsonPath}`)\n    process.exit(1)\n  }\n}\n\nconst scenarios = createScenarios(\n  suiteName,\n  {\n    iterations,\n    warmupIterations,\n    longIterations,\n    scale,\n  },\n  seed,\n)\n\nif (scenarios.length === 0) {\n  console.error(`Unknown suite: ${suiteName}`)\n  process.exit(1)\n}\n\nconst filteredScenarios = scenarioFilter ? scenarios.filter((scenario) => scenario.name === scenarioFilter) : scenarios\n\nif (scenarioFilter && filteredScenarios.length === 0) {\n  writeLine(`Unknown scenario: ${scenarioFilter}`)\n  process.exit(1)\n}\n\nconst { renderer, renderOnce } = await createTestRenderer({\n  width,\n  height,\n  useAlternateScreen: false,\n  useConsole: false,\n})\n\nrenderer.requestRender = () => {}\n\nconst table = new TextTableRenderable(renderer, {\n  id: \"text-table-bench\",\n  width: \"100%\",\n  wrapMode: PROPORTIONAL_TABLE_CONFIG.wrapMode,\n  columnWidthMode: PROPORTIONAL_TABLE_CONFIG.columnWidthMode,\n  columnFitter: PROPORTIONAL_TABLE_CONFIG.columnFitter,\n  content: [],\n})\n\nrenderer.root.add(table)\nawait renderOnce()\n\nconst ctx: RunContext = {\n  renderer,\n  table,\n  renderOnce,\n  memSampleEvery,\n}\n\nconst results: ScenarioResult[] = []\nconst scenarioLines: string[] = []\n\ntry {\n  for (const plan of filteredScenarios) {\n    const result = await runScenario(plan, ctx)\n    results.push(result)\n    scenarioLines.push(formatScenarioResult(result))\n  }\n} finally {\n  renderer.destroy()\n}\n\nawait outputResults(\n  {\n    suiteName,\n    width,\n    height,\n    iterations,\n    warmupIterations,\n    longIterations,\n    scale,\n    seed,\n    memSampleEvery,\n  },\n  results,\n  scenarioLines,\n  outputEnabled,\n  jsonPath,\n)\n\nfunction createScenarios(suite: string, config: SuiteConfig, runSeed: number): ScenarioPlan[] {\n  const quick = {\n    replaceRows: scaled(24, config.scale),\n    replaceCols: 4,\n    replaceVariants: scaled(6, config.scale),\n    incrementalCols: 4,\n    incrementalBaseRows: scaled(8, config.scale),\n    incrementalPoolRows: scaled(220, config.scale),\n    incrementalMaxRows: scaled(120, config.scale),\n  }\n\n  const defaultSuite = {\n    replaceRows: scaled(72, config.scale),\n    replaceCols: 6,\n    replaceVariants: scaled(10, config.scale),\n    incrementalCols: 6,\n    incrementalBaseRows: scaled(16, config.scale),\n    incrementalPoolRows: scaled(480, config.scale),\n    incrementalMaxRows: scaled(320, config.scale),\n  }\n\n  const long = {\n    replaceRows: scaled(140, config.scale),\n    replaceCols: 8,\n    replaceVariants: scaled(14, config.scale),\n    incrementalCols: 8,\n    incrementalBaseRows: scaled(24, config.scale),\n    incrementalPoolRows: scaled(960, config.scale),\n    incrementalMaxRows: scaled(720, config.scale),\n  }\n\n  let shape: typeof quick\n  let runIterations = config.iterations\n\n  if (suite === \"quick\") {\n    shape = quick\n  } else if (suite === \"default\") {\n    shape = defaultSuite\n  } else if (suite === \"long\") {\n    shape = long\n    runIterations = config.longIterations\n  } else {\n    return []\n  }\n\n  const replaceRng = createRng((runSeed ^ 0x9e3779b9) >>> 0)\n  const variants: TextTableContent[] = []\n  for (let i = 0; i < shape.replaceVariants; i += 1) {\n    variants.push(buildTableContent(replaceRng, shape.replaceRows, shape.replaceCols))\n  }\n\n  const incrementalRng = createRng((runSeed ^ 0x85ebca6b) >>> 0)\n  const header = makeHeader(shape.incrementalCols)\n  const baseRows = buildRows(incrementalRng, shape.incrementalBaseRows, shape.incrementalCols, 0)\n  const rowPool = buildRows(\n    incrementalRng,\n    Math.max(shape.incrementalPoolRows, shape.incrementalBaseRows + 1),\n    shape.incrementalCols,\n    shape.incrementalBaseRows,\n  )\n\n  const replaceScenario: ReplaceScenarioPlan = {\n    kind: \"replace\",\n    name: \"replace_tables\",\n    description: \"Replace full table content with prebuilt variants\",\n    iterations: runIterations,\n    warmupIterations: config.warmupIterations,\n    rows: shape.replaceRows,\n    cols: shape.replaceCols,\n    variants,\n    tableConfig: PROPORTIONAL_TABLE_CONFIG,\n  }\n\n  const balancedFitterReplaceScenario: ReplaceScenarioPlan = {\n    kind: \"replace\",\n    name: \"replace_tables_balanced_fitter\",\n    description: \"Replace full table content with prebuilt variants (balanced fitter)\",\n    iterations: runIterations,\n    warmupIterations: config.warmupIterations,\n    rows: shape.replaceRows,\n    cols: shape.replaceCols,\n    variants,\n    tableConfig: BALANCED_TABLE_CONFIG,\n  }\n\n  const incrementalScenario: IncrementalScenarioPlan = {\n    kind: \"incremental\",\n    name: \"incremental_table_rows\",\n    description: \"Append table rows and periodically reset to base size\",\n    iterations: runIterations,\n    warmupIterations: config.warmupIterations,\n    cols: shape.incrementalCols,\n    header,\n    baseRows,\n    rowPool,\n    maxRows: Math.max(shape.incrementalMaxRows, shape.incrementalBaseRows + 1),\n    tableConfig: PROPORTIONAL_TABLE_CONFIG,\n  }\n\n  const balancedFitterIncrementalScenario: IncrementalScenarioPlan = {\n    kind: \"incremental\",\n    name: \"incremental_table_rows_balanced_fitter\",\n    description: \"Append table rows and periodically reset to base size (balanced fitter)\",\n    iterations: runIterations,\n    warmupIterations: config.warmupIterations,\n    cols: shape.incrementalCols,\n    header,\n    baseRows,\n    rowPool,\n    maxRows: Math.max(shape.incrementalMaxRows, shape.incrementalBaseRows + 1),\n    tableConfig: BALANCED_TABLE_CONFIG,\n  }\n\n  const selectionRng = createRng((runSeed ^ 0xa2f9c6d1) >>> 0)\n  const selectionContent = buildTableContent(selectionRng, shape.replaceRows, shape.replaceCols)\n\n  const selectionScenario: SelectionScenarioPlan = {\n    kind: \"selection\",\n    name: \"selection_update\",\n    description: \"Update selection focus across rows and render\",\n    iterations: runIterations,\n    warmupIterations: config.warmupIterations,\n    rows: shape.replaceRows,\n    cols: shape.replaceCols,\n    content: selectionContent,\n    dragSteps: 5,\n    tableConfig: PROPORTIONAL_TABLE_CONFIG,\n  }\n\n  const balancedFitterSelectionScenario: SelectionScenarioPlan = {\n    kind: \"selection\",\n    name: \"selection_update_balanced_fitter\",\n    description: \"Update selection focus across rows and render (balanced fitter)\",\n    iterations: runIterations,\n    warmupIterations: config.warmupIterations,\n    rows: shape.replaceRows,\n    cols: shape.replaceCols,\n    content: selectionContent,\n    dragSteps: 5,\n    tableConfig: BALANCED_TABLE_CONFIG,\n  }\n\n  return [\n    replaceScenario,\n    balancedFitterReplaceScenario,\n    incrementalScenario,\n    balancedFitterIncrementalScenario,\n    selectionScenario,\n    balancedFitterSelectionScenario,\n  ]\n}\n\nasync function runScenario(plan: ScenarioPlan, ctx: RunContext): Promise<ScenarioResult> {\n  if (plan.kind === \"replace\") {\n    return runReplaceScenario(plan, ctx)\n  }\n  if (plan.kind === \"incremental\") {\n    return runIncrementalScenario(plan, ctx)\n  }\n  return runSelectionScenario(plan, ctx)\n}\n\nfunction applyTableConfig(table: TextTableRenderable, config: BenchmarkTableConfig): void {\n  table.wrapMode = config.wrapMode\n  table.columnWidthMode = config.columnWidthMode\n  table.columnFitter = config.columnFitter\n}\n\nasync function runReplaceScenario(plan: ReplaceScenarioPlan, ctx: RunContext): Promise<ScenarioResult> {\n  applyTableConfig(ctx.table, plan.tableConfig)\n\n  for (let i = 0; i < plan.warmupIterations; i += 1) {\n    const variant = plan.variants[i % plan.variants.length]\n    ctx.table.content = variant\n    await ctx.renderOnce()\n  }\n\n  const durations: number[] = []\n  const measurementStart = Date.now()\n  const memStart = shouldSampleMemory(ctx.memSampleEvery) ? readMemorySample() : null\n  const memSamples: MemorySample[] = []\n\n  for (let i = 0; i < plan.iterations; i += 1) {\n    const variant = plan.variants[i % plan.variants.length]\n    const start = performance.now()\n    ctx.table.content = variant\n    await ctx.renderOnce()\n    durations.push(performance.now() - start)\n\n    if (ctx.memSampleEvery > 0 && (i + 1) % ctx.memSampleEvery === 0) {\n      memSamples.push(readMemorySample())\n    }\n  }\n\n  const elapsedMs = Date.now() - measurementStart\n  const memEnd = shouldSampleMemory(ctx.memSampleEvery) ? readMemorySample() : null\n\n  return {\n    name: plan.name,\n    description: plan.description,\n    category: \"replace\",\n    timingMode: \"content-set-and-render\",\n    iterations: plan.iterations,\n    warmupIterations: plan.warmupIterations,\n    elapsedMs,\n    updateStats: computeTimingStats(durations),\n    memoryStats: memStart && memEnd ? computeMemoryStats(memSamples, memStart, memEnd) : undefined,\n    tableStats: {\n      initialRows: plan.rows,\n      finalRows: plan.rows,\n      maxRows: plan.rows,\n      columns: plan.cols,\n      updates: plan.iterations,\n      datasetVariants: plan.variants.length,\n    },\n    settings: {\n      rows: plan.rows,\n      cols: plan.cols,\n      variants: plan.variants.length,\n      mode: \"replace\",\n      wrapMode: plan.tableConfig.wrapMode,\n      columnWidthMode: plan.tableConfig.columnWidthMode,\n      columnFitter: plan.tableConfig.columnFitter,\n    },\n  }\n}\n\nasync function runIncrementalScenario(plan: IncrementalScenarioPlan, ctx: RunContext): Promise<ScenarioResult> {\n  applyTableConfig(ctx.table, plan.tableConfig)\n\n  const state: IncrementalState = {\n    rows: [...plan.baseRows],\n    cursor: 0,\n    maxRowsSeen: plan.baseRows.length,\n  }\n\n  ctx.table.content = [plan.header, ...state.rows]\n  await ctx.renderOnce()\n\n  for (let i = 0; i < plan.warmupIterations; i += 1) {\n    const next = nextIncrementalContent(plan, state)\n    ctx.table.content = next\n    await ctx.renderOnce()\n  }\n\n  const durations: number[] = []\n  const measurementStart = Date.now()\n  const memStart = shouldSampleMemory(ctx.memSampleEvery) ? readMemorySample() : null\n  const memSamples: MemorySample[] = []\n\n  for (let i = 0; i < plan.iterations; i += 1) {\n    const next = nextIncrementalContent(plan, state)\n\n    const start = performance.now()\n    ctx.table.content = next\n    await ctx.renderOnce()\n    durations.push(performance.now() - start)\n\n    if (ctx.memSampleEvery > 0 && (i + 1) % ctx.memSampleEvery === 0) {\n      memSamples.push(readMemorySample())\n    }\n  }\n\n  const elapsedMs = Date.now() - measurementStart\n  const memEnd = shouldSampleMemory(ctx.memSampleEvery) ? readMemorySample() : null\n\n  return {\n    name: plan.name,\n    description: plan.description,\n    category: \"incremental\",\n    timingMode: \"content-set-and-render\",\n    iterations: plan.iterations,\n    warmupIterations: plan.warmupIterations,\n    elapsedMs,\n    updateStats: computeTimingStats(durations),\n    memoryStats: memStart && memEnd ? computeMemoryStats(memSamples, memStart, memEnd) : undefined,\n    tableStats: {\n      initialRows: plan.baseRows.length,\n      finalRows: state.rows.length,\n      maxRows: state.maxRowsSeen,\n      columns: plan.cols,\n      updates: plan.iterations,\n      datasetVariants: plan.rowPool.length,\n    },\n    settings: {\n      cols: plan.cols,\n      baseRows: plan.baseRows.length,\n      rowPool: plan.rowPool.length,\n      maxRows: plan.maxRows,\n      mode: \"incremental\",\n      wrapMode: plan.tableConfig.wrapMode,\n      columnWidthMode: plan.tableConfig.columnWidthMode,\n      columnFitter: plan.tableConfig.columnFitter,\n    },\n  }\n}\n\nasync function runSelectionScenario(plan: SelectionScenarioPlan, ctx: RunContext): Promise<ScenarioResult> {\n  applyTableConfig(ctx.table, plan.tableConfig)\n  ctx.table.content = plan.content\n  await ctx.renderOnce()\n\n  const tableX = ctx.table.x\n  const tableY = ctx.table.y\n  const tableH = ctx.table.height\n\n  const anchorX = tableX + 2\n  const anchorY = tableY + 2\n\n  const maxFocusY = tableY + tableH - 2\n  const focusRange = Math.max(1, maxFocusY - anchorY)\n\n  for (let i = 0; i < plan.warmupIterations; i += 1) {\n    const focusY = anchorY + (i % focusRange)\n    ctx.renderer.startSelection(ctx.table, anchorX, anchorY)\n    for (let step = 1; step <= plan.dragSteps; step += 1) {\n      const stepY = anchorY + Math.round(((focusY - anchorY) * step) / plan.dragSteps)\n      ctx.renderer.updateSelection(ctx.table, anchorX + 4, stepY)\n    }\n    await ctx.renderOnce()\n    ctx.renderer.clearSelection()\n    await ctx.renderOnce()\n  }\n\n  const durations: number[] = []\n  const measurementStart = Date.now()\n  const memStart = shouldSampleMemory(ctx.memSampleEvery) ? readMemorySample() : null\n  const memSamples: MemorySample[] = []\n\n  for (let i = 0; i < plan.iterations; i += 1) {\n    const focusY = anchorY + (i % focusRange)\n\n    const start = performance.now()\n\n    ctx.renderer.startSelection(ctx.table, anchorX, anchorY)\n    for (let step = 1; step <= plan.dragSteps; step += 1) {\n      const stepY = anchorY + Math.round(((focusY - anchorY) * step) / plan.dragSteps)\n      ctx.renderer.updateSelection(ctx.table, anchorX + 4, stepY)\n    }\n    await ctx.renderOnce()\n    ctx.renderer.clearSelection()\n    await ctx.renderOnce()\n\n    durations.push(performance.now() - start)\n\n    if (ctx.memSampleEvery > 0 && (i + 1) % ctx.memSampleEvery === 0) {\n      memSamples.push(readMemorySample())\n    }\n  }\n\n  const elapsedMs = Date.now() - measurementStart\n  const memEnd = shouldSampleMemory(ctx.memSampleEvery) ? readMemorySample() : null\n\n  return {\n    name: plan.name,\n    description: plan.description,\n    category: \"selection\",\n    timingMode: \"selection-update-and-render\",\n    iterations: plan.iterations,\n    warmupIterations: plan.warmupIterations,\n    elapsedMs,\n    updateStats: computeTimingStats(durations),\n    memoryStats: memStart && memEnd ? computeMemoryStats(memSamples, memStart, memEnd) : undefined,\n    tableStats: {\n      initialRows: plan.rows,\n      finalRows: plan.rows,\n      maxRows: plan.rows,\n      columns: plan.cols,\n      updates: plan.iterations * (plan.dragSteps + 1),\n      datasetVariants: 1,\n    },\n    settings: {\n      rows: plan.rows,\n      cols: plan.cols,\n      dragSteps: plan.dragSteps,\n      mode: \"selection\",\n      wrapMode: plan.tableConfig.wrapMode,\n      columnWidthMode: plan.tableConfig.columnWidthMode,\n      columnFitter: plan.tableConfig.columnFitter,\n    },\n  }\n}\n\nfunction nextIncrementalContent(plan: IncrementalScenarioPlan, state: IncrementalState): TextTableContent {\n  if (state.rows.length >= plan.maxRows) {\n    state.rows = [...plan.baseRows]\n  }\n\n  const fallbackRow = plan.rowPool[0] ?? makeDataRow(createRng(1), 0, plan.cols)\n  const nextRow = plan.rowPool[state.cursor] ?? fallbackRow\n\n  state.cursor += 1\n  if (state.cursor >= plan.rowPool.length) {\n    state.cursor = 0\n  }\n\n  state.rows = [...state.rows, nextRow]\n  state.maxRowsSeen = Math.max(state.maxRowsSeen, state.rows.length)\n\n  return [plan.header, ...state.rows]\n}\n\nfunction makeHeader(cols: number): TextTableCellContent[] {\n  const header: TextTableCellContent[] = []\n  for (let c = 0; c < cols; c += 1) {\n    header.push(chunkCell(`Column ${c + 1}`))\n  }\n  return header\n}\n\nfunction buildTableContent(rng: () => number, rows: number, cols: number): TextTableContent {\n  return [makeHeader(cols), ...buildRows(rng, rows, cols, 0)]\n}\n\nfunction buildRows(rng: () => number, rows: number, cols: number, rowOffset: number): TextTableCellContent[][] {\n  const out: TextTableCellContent[][] = []\n  for (let r = 0; r < rows; r += 1) {\n    out.push(makeDataRow(rng, rowOffset + r, cols))\n  }\n  return out\n}\n\nfunction makeDataRow(rng: () => number, rowIndex: number, cols: number): TextTableCellContent[] {\n  const row: TextTableCellContent[] = []\n  for (let c = 0; c < cols; c += 1) {\n    row.push(chunkCell(makeCellText(rng, rowIndex, c)))\n  }\n  return row\n}\n\nfunction chunkCell(text: string): TextTableCellContent {\n  return [\n    {\n      __isChunk: true,\n      text,\n    },\n  ]\n}\n\nfunction makeCellText(rng: () => number, row: number, col: number): string {\n  const a = pick(rng, WORDS)\n  const b = pick(rng, WORDS)\n  const roll = rng()\n\n  if (roll < 0.2) {\n    return `${a}-${b}-${row + col}`\n  }\n  if (roll < 0.4) {\n    return `${a} ${Math.floor(rng() * 1000)}`\n  }\n  if (roll < 0.6) {\n    return `${a} ${b} ${pick(rng, WORDS)}`\n  }\n  if (roll < 0.8) {\n    return `${a}_${b}_${Math.floor(rng() * 100)}`\n  }\n  return `${a} ${b} r${row}c${col}`\n}\n\nfunction pick<T>(rng: () => number, list: T[]): T {\n  return list[Math.floor(rng() * list.length)]\n}\n\nfunction createRng(initialSeed: number): () => number {\n  let state = initialSeed >>> 0\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0\n    return state / 0x100000000\n  }\n}\n\nfunction scaled(value: number, scaleValue: number): number {\n  return Math.max(1, Math.round(value * scaleValue))\n}\n\nfunction toNumber(value: unknown, fallback: number): number {\n  if (typeof value === \"number\" && Number.isFinite(value)) return value\n  if (typeof value === \"string\") {\n    const parsed = Number(value)\n    if (Number.isFinite(parsed)) return parsed\n  }\n  return fallback\n}\n\nfunction shouldSampleMemory(memSampleEvery: number): boolean {\n  return memSampleEvery > 0\n}\n\nfunction readMemorySample(): MemorySample {\n  const usage = process.memoryUsage()\n  return {\n    rss: usage.rss ?? 0,\n    heapTotal: usage.heapTotal ?? 0,\n    heapUsed: usage.heapUsed ?? 0,\n    external: usage.external ?? 0,\n    arrayBuffers: usage.arrayBuffers ?? 0,\n  }\n}\n\nfunction computeMemoryStats(samples: MemorySample[], start: MemorySample, end: MemorySample): MemoryStats {\n  const all = [start, ...samples, end]\n  const peak = { ...start }\n\n  for (const sample of all) {\n    peak.rss = Math.max(peak.rss, sample.rss)\n    peak.heapTotal = Math.max(peak.heapTotal, sample.heapTotal)\n    peak.heapUsed = Math.max(peak.heapUsed, sample.heapUsed)\n    peak.external = Math.max(peak.external, sample.external)\n    peak.arrayBuffers = Math.max(peak.arrayBuffers, sample.arrayBuffers)\n  }\n\n  return {\n    samples: all.length,\n    start,\n    end,\n    delta: diffMemory(start, end),\n    peak,\n  }\n}\n\nfunction diffMemory(start: MemorySample, end: MemorySample): MemorySample {\n  return {\n    rss: end.rss - start.rss,\n    heapTotal: end.heapTotal - start.heapTotal,\n    heapUsed: end.heapUsed - start.heapUsed,\n    external: end.external - start.external,\n    arrayBuffers: end.arrayBuffers - start.arrayBuffers,\n  }\n}\n\nfunction computeTimingStats(durations: number[]): TimingStats {\n  const sorted = [...durations].sort((a, b) => a - b)\n  const count = sorted.length\n  const sum = sorted.reduce((acc, value) => acc + value, 0)\n  const average = count > 0 ? sum / count : 0\n  const min = sorted[0] ?? 0\n  const max = sorted[count - 1] ?? 0\n  const median = count > 0 ? (sorted[Math.floor(count / 2)] ?? 0) : 0\n  const p95 = count > 0 ? (sorted[Math.floor(count * 0.95)] ?? 0) : 0\n  const stdDev = count > 0 ? Math.sqrt(sorted.reduce((acc, v) => acc + Math.pow(v - average, 2), 0) / count) : 0\n\n  return {\n    count,\n    averageMs: average,\n    medianMs: median,\n    p95Ms: p95,\n    minMs: min,\n    maxMs: max,\n    stdDevMs: stdDev,\n  }\n}\n\nasync function outputResults(\n  meta: OutputMeta,\n  results: ScenarioResult[],\n  scenarioLines: string[],\n  outputEnabled: boolean,\n  outputPath: string | null,\n): Promise<void> {\n  const runId = new Date().toISOString()\n  const payload = {\n    runId,\n    suite: meta.suiteName,\n    config: {\n      width: meta.width,\n      height: meta.height,\n      iterations: meta.iterations,\n      warmupIterations: meta.warmupIterations,\n      longIterations: meta.longIterations,\n      scale: meta.scale,\n      seed: meta.seed,\n      memSampleEvery: meta.memSampleEvery,\n    },\n    results,\n  }\n\n  if (outputEnabled) {\n    writeLine(\n      `text-table-benchmark suite=${meta.suiteName} mode=content-set-and-render iters=${meta.iterations} warmup=${meta.warmupIterations}`,\n    )\n    for (const line of scenarioLines) {\n      writeLine(line)\n    }\n  }\n\n  if (outputPath) {\n    try {\n      const json = JSON.stringify(payload, null, 2)\n      await Bun.write(outputPath, json)\n    } catch (error: any) {\n      writeLine(`Error writing results to ${outputPath}: ${error.message}`)\n    }\n  }\n}\n\nfunction formatBytes(value: number): string {\n  return `${(value / (1024 * 1024)).toFixed(2)}MB`\n}\n\nfunction formatScenarioResult(result: ScenarioResult): string {\n  const mem = result.memoryStats\n  const memSummary = mem\n    ? ` memDeltaRss=${formatBytes(mem.delta.rss)}` +\n      ` memDeltaHeap=${formatBytes(mem.delta.heapUsed)}` +\n      ` memDeltaExt=${formatBytes(mem.delta.external)}` +\n      ` memDeltaAB=${formatBytes(mem.delta.arrayBuffers)}` +\n      ` memPeakRss=${formatBytes(mem.peak.rss)}`\n    : \"\"\n\n  const fitter = typeof result.settings.columnFitter === \"string\" ? result.settings.columnFitter : \"unknown\"\n\n  return `scenario=${result.name} category=${result.category} mode=${result.timingMode} fitter=${fitter} iters=${result.updateStats.count} elapsedMs=${result.elapsedMs} avgMs=${result.updateStats.averageMs.toFixed(3)} medianMs=${result.updateStats.medianMs.toFixed(3)} p95Ms=${result.updateStats.p95Ms.toFixed(3)} minMs=${result.updateStats.minMs.toFixed(3)} maxMs=${result.updateStats.maxMs.toFixed(3)} rows=${result.tableStats.finalRows} maxRows=${result.tableStats.maxRows} cols=${result.tableStats.columns}${memSummary}`\n}\n\nfunction writeLine(line: string): void {\n  realStdoutWrite(`${line}\\n`)\n}\n"
  },
  {
    "path": "packages/core/src/buffer.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { OptimizedBuffer } from \"./buffer.js\"\nimport { RGBA } from \"./lib/RGBA.js\"\n\ndescribe(\"OptimizedBuffer\", () => {\n  let buffer: OptimizedBuffer\n\n  beforeEach(() => {\n    buffer = OptimizedBuffer.create(20, 5, \"unicode\", { id: \"test-buffer\" })\n  })\n\n  afterEach(() => {\n    buffer.destroy()\n  })\n\n  describe(\"encodeUnicode\", () => {\n    it(\"should encode simple ASCII text\", () => {\n      const encoded = buffer.encodeUnicode(\"Hello\")\n      expect(encoded).not.toBeNull()\n      expect(encoded!.data.length).toBe(5)\n      expect(encoded!.data[0]).toEqual({ width: 1, char: 72 }) // 'H'\n      expect(encoded!.data[1]).toEqual({ width: 1, char: 101 }) // 'e'\n      expect(encoded!.data[2]).toEqual({ width: 1, char: 108 }) // 'l'\n      expect(encoded!.data[3]).toEqual({ width: 1, char: 108 }) // 'l'\n      expect(encoded!.data[4]).toEqual({ width: 1, char: 111 }) // 'o'\n\n      buffer.freeUnicode(encoded!)\n    })\n\n    it(\"should encode emoji with correct width\", () => {\n      const encoded = buffer.encodeUnicode(\"👋\")\n      expect(encoded).not.toBeNull()\n      expect(encoded!.data.length).toBe(1)\n      expect(encoded!.data[0].width).toBe(2)\n      // Should be a packed grapheme (has high bit set)\n      expect(encoded!.data[0].char).toBeGreaterThan(0x80000000)\n\n      buffer.freeUnicode(encoded!)\n    })\n\n    it(\"should encode mixed ASCII and emoji\", () => {\n      const encoded = buffer.encodeUnicode(\"Hi 👋 World\")\n      expect(encoded).not.toBeNull()\n      expect(encoded!.data.length).toBe(10) // H, i, space, emoji, space, W, o, r, l, d\n\n      // Check ASCII chars\n      expect(encoded!.data[0].width).toBe(1)\n      expect(encoded!.data[0].char).toBe(72) // 'H'\n\n      // Check emoji\n      expect(encoded!.data[3].width).toBe(2)\n      expect(encoded!.data[3].char).toBeGreaterThan(0x80000000)\n\n      buffer.freeUnicode(encoded!)\n    })\n\n    it(\"should handle empty string\", () => {\n      const encoded = buffer.encodeUnicode(\"\")\n      expect(encoded).not.toBeNull()\n      expect(encoded!.data.length).toBe(0)\n\n      buffer.freeUnicode(encoded!)\n    })\n\n    it(\"should encode monkey emoji frames and draw in a line\", () => {\n      const frames = [\"🙈 \", \"🙈 \", \"🙉 \", \"🙊 \"]\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      buffer.clear(bg)\n\n      let x = 0\n      for (const frame of frames) {\n        const encoded = buffer.encodeUnicode(frame)\n        expect(encoded).not.toBeNull()\n\n        for (const encodedChar of encoded!.data) {\n          buffer.drawChar(encodedChar.char, x, 0, fg, bg)\n          x += encodedChar.width\n        }\n\n        buffer.freeUnicode(encoded!)\n      }\n\n      const frameBytes = buffer.getRealCharBytes(false)\n      const frameText = new TextDecoder().decode(frameBytes)\n      expect(frameText).toContain(\"🙈\")\n      expect(frameText).toContain(\"🙉\")\n      expect(frameText).toContain(\"🙊\")\n    })\n  })\n\n  describe(\"drawChar\", () => {\n    it(\"should draw a simple ASCII character\", () => {\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      buffer.drawChar(72, 0, 0, fg, bg) // 'H'\n\n      const chars = buffer.buffers.char\n      expect(chars[0]).toBe(72)\n    })\n\n    it(\"should draw encoded characters from encodeUnicode\", () => {\n      const encoded = buffer.encodeUnicode(\"Hello\")\n      expect(encoded).not.toBeNull()\n\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      // Draw each character\n      for (let i = 0; i < encoded!.data.length; i++) {\n        buffer.drawChar(encoded!.data[i].char, i, 0, fg, bg)\n      }\n\n      // Verify buffer content\n      const frameBytes = buffer.getRealCharBytes(false)\n      const frameText = new TextDecoder().decode(frameBytes)\n      expect(frameText).toContain(\"Hello\")\n\n      buffer.freeUnicode(encoded!)\n    })\n\n    it(\"should draw emoji using encoded char\", () => {\n      const encoded = buffer.encodeUnicode(\"👋\")\n      expect(encoded).not.toBeNull()\n\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      buffer.drawChar(encoded!.data[0].char, 0, 0, fg, bg)\n\n      const frameBytes = buffer.getRealCharBytes(false)\n      const frameText = new TextDecoder().decode(frameBytes)\n      expect(frameText).toContain(\"👋\")\n\n      buffer.freeUnicode(encoded!)\n    })\n  })\n\n  describe(\"snapshot tests with unicode encoding\", () => {\n    it(\"should render ASCII text correctly\", () => {\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n\n      const encoded = buffer.encodeUnicode(\"Hello\")\n      expect(encoded).not.toBeNull()\n\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      let x = 0\n      for (const encodedChar of encoded!.data) {\n        buffer.drawChar(encodedChar.char, x, 0, fg, bg)\n        x += encodedChar.width\n      }\n\n      const frameBytes = buffer.getRealCharBytes(true)\n      const frameText = new TextDecoder().decode(frameBytes)\n      expect(frameText).toMatchSnapshot(\"ASCII text rendering\")\n\n      buffer.freeUnicode(encoded!)\n    })\n\n    it(\"should render emoji text correctly\", () => {\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n\n      const encoded = buffer.encodeUnicode(\"Hi 👋 🌍\")\n      expect(encoded).not.toBeNull()\n\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      let x = 0\n      for (const encodedChar of encoded!.data) {\n        buffer.drawChar(encodedChar.char, x, 0, fg, bg)\n        x += encodedChar.width\n      }\n\n      const frameBytes = buffer.getRealCharBytes(true)\n      const frameText = new TextDecoder().decode(frameBytes)\n      expect(frameText).toMatchSnapshot(\"Emoji text rendering\")\n\n      buffer.freeUnicode(encoded!)\n    })\n\n    it(\"should handle multiline text with unicode\", () => {\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n\n      const lines = [\"Hi 世界\", \"🌟 Star\"]\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      for (let y = 0; y < lines.length; y++) {\n        const encoded = buffer.encodeUnicode(lines[y])\n        expect(encoded).not.toBeNull()\n\n        let x = 0\n        for (const encodedChar of encoded!.data) {\n          buffer.drawChar(encodedChar.char, x, y, fg, bg)\n          x += encodedChar.width\n        }\n\n        buffer.freeUnicode(encoded!)\n      }\n\n      const frameBytes = buffer.getRealCharBytes(true)\n      const frameText = new TextDecoder().decode(frameBytes)\n      expect(frameText).toMatchSnapshot(\"Multiline unicode rendering\")\n    })\n\n    it(\"should respect character widths in positioning\", () => {\n      const encoded = buffer.encodeUnicode(\"A👋B\")\n      expect(encoded).not.toBeNull()\n\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      // 'A' at x=0, emoji at x=1 (width 2), 'B' at x=3\n      buffer.drawChar(encoded!.data[0].char, 0, 0, fg, bg) // 'A'\n      buffer.drawChar(encoded!.data[1].char, 1, 0, fg, bg) // emoji\n      buffer.drawChar(encoded!.data[2].char, 3, 0, fg, bg) // 'B'\n\n      const frameBytes = buffer.getRealCharBytes(false)\n      const frameText = new TextDecoder().decode(frameBytes)\n      expect(frameText).toContain(\"A👋B\")\n\n      buffer.freeUnicode(encoded!)\n    })\n  })\n\n  describe(\"drawChar with alpha blending\", () => {\n    it(\"should blend semi-transparent foreground\", () => {\n      const fg = RGBA.fromValues(1, 0, 0, 0.5)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      buffer.drawChar(65, 0, 0, fg, bg) // 'A'\n\n      const fgBuffer = buffer.buffers.fg\n      // Should have blended the color\n      expect(fgBuffer[0]).toBeLessThan(1.0)\n    })\n\n    it(\"should blend semi-transparent background\", () => {\n      buffer.setRespectAlpha(true)\n\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(1, 0, 0, 0.5)\n\n      buffer.drawChar(65, 0, 0, fg, bg) // 'A'\n\n      const bgBuffer = buffer.buffers.bg\n      // Background should reflect the alpha\n      expect(bgBuffer[3]).toBeLessThan(1.0)\n    })\n  })\n\n  describe(\"grapheme pool churn across drawFrameBuffer\", () => {\n    it(\"should not crash with WrongGeneration after many grapheme alloc cycles\", () => {\n      const parent = OptimizedBuffer.create(40, 5, \"unicode\", { id: \"parent\" })\n      const child = OptimizedBuffer.create(40, 5, \"unicode\", { id: \"child\", respectAlpha: true })\n\n      const fg = RGBA.fromValues(1, 1, 1, 1)\n      const bg = RGBA.fromValues(0, 0, 0, 1)\n\n      for (let cycle = 0; cycle < 50; cycle++) {\n        parent.clear(bg)\n\n        if (cycle % 2 === 0) {\n          child.drawText(\"╭────────────────────────────────────╮\", 0, 0, fg, bg)\n          child.drawText(\"│ ◇ Select Files ▫ src/ ▪ file.ts   │\", 0, 1, fg, bg)\n          child.drawText(\"│ ↑↓ navigate  ⏎ select  esc close  │\", 0, 2, fg, bg)\n          child.drawText(\"╰────────────────────────────────────╯\", 0, 3, fg, bg)\n        } else {\n          child.drawText(\"  Your Name                              \", 0, 0, fg, bg)\n          child.drawText(\"  John Doe                               \", 0, 1, fg, bg)\n          child.drawText(\"                                         \", 0, 2, fg, bg)\n          child.drawText(\"  Select Files                           \", 0, 3, fg, bg)\n        }\n\n        parent.drawFrameBuffer(0, 0, child)\n\n        const frameBytes = parent.getRealCharBytes(true)\n        const text = new TextDecoder().decode(frameBytes)\n        expect(text.length).toBeGreaterThan(0)\n      }\n\n      child.destroy()\n      parent.destroy()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/buffer.ts",
    "content": "import { RGBA } from \"./lib\"\nimport { resolveRenderLib, type RenderLib } from \"./zig\"\nimport { type Pointer, toArrayBuffer, ptr } from \"bun:ffi\"\nimport { type BorderStyle, type BorderSides, BorderCharArrays, parseBorderStyle } from \"./lib/index.js\"\nimport { TargetChannel, type WidthMethod, type CapturedSpan, type CapturedLine } from \"./types.js\"\nimport type { TextBufferView } from \"./text-buffer-view.js\"\nimport type { EditorView } from \"./editor-view.js\"\n\n// Pack drawing options into a single u32\n// bits 0-3: borderSides, bit 4: shouldFill, bits 5-6: titleAlignment\nfunction packDrawOptions(\n  border: boolean | BorderSides[],\n  shouldFill: boolean,\n  titleAlignment: \"left\" | \"center\" | \"right\",\n): number {\n  let packed = 0\n\n  if (border === true) {\n    packed |= 0b1111 // All sides\n  } else if (Array.isArray(border)) {\n    if (border.includes(\"top\")) packed |= 0b1000\n    if (border.includes(\"right\")) packed |= 0b0100\n    if (border.includes(\"bottom\")) packed |= 0b0010\n    if (border.includes(\"left\")) packed |= 0b0001\n  }\n\n  if (shouldFill) {\n    packed |= 1 << 4\n  }\n\n  const alignmentMap: Record<string, number> = {\n    left: 0,\n    center: 1,\n    right: 2,\n  }\n  const alignment = alignmentMap[titleAlignment]\n  packed |= alignment << 5\n\n  return packed\n}\n\nexport class OptimizedBuffer {\n  private static fbIdCounter = 0\n  public id: string\n  public lib: RenderLib\n  private bufferPtr: Pointer\n  private _width: number\n  private _height: number\n  private _widthMethod: WidthMethod\n  public respectAlpha: boolean = false\n  private _rawBuffers: {\n    char: Uint32Array\n    fg: Float32Array\n    bg: Float32Array\n    attributes: Uint32Array\n  } | null = null\n  private _destroyed: boolean = false\n\n  get ptr(): Pointer {\n    return this.bufferPtr\n  }\n\n  // Fail loud and clear\n  // Instead of trying to return values that could work or not,\n  // this at least will show a stack trace to know where the call to a destroyed Buffer was made\n  private guard(): void {\n    if (this._destroyed) throw new Error(`Buffer ${this.id} is destroyed`)\n  }\n\n  get buffers(): {\n    char: Uint32Array\n    fg: Float32Array\n    bg: Float32Array\n    attributes: Uint32Array\n  } {\n    this.guard()\n    if (this._rawBuffers === null) {\n      const size = this._width * this._height\n      const charPtr = this.lib.bufferGetCharPtr(this.bufferPtr)\n      const fgPtr = this.lib.bufferGetFgPtr(this.bufferPtr)\n      const bgPtr = this.lib.bufferGetBgPtr(this.bufferPtr)\n      const attributesPtr = this.lib.bufferGetAttributesPtr(this.bufferPtr)\n\n      this._rawBuffers = {\n        char: new Uint32Array(toArrayBuffer(charPtr, 0, size * 4)),\n        fg: new Float32Array(toArrayBuffer(fgPtr, 0, size * 4 * 4)),\n        bg: new Float32Array(toArrayBuffer(bgPtr, 0, size * 4 * 4)),\n        attributes: new Uint32Array(toArrayBuffer(attributesPtr, 0, size * 4)),\n      }\n    }\n\n    return this._rawBuffers\n  }\n\n  constructor(\n    lib: RenderLib,\n    ptr: Pointer,\n    width: number,\n    height: number,\n    options: { respectAlpha?: boolean; id?: string; widthMethod?: WidthMethod },\n  ) {\n    this.id = options.id || `fb_${OptimizedBuffer.fbIdCounter++}`\n    this.lib = lib\n    this.respectAlpha = options.respectAlpha || false\n    this._width = width\n    this._height = height\n    this._widthMethod = options.widthMethod || \"unicode\"\n    this.bufferPtr = ptr\n  }\n\n  static create(\n    width: number,\n    height: number,\n    widthMethod: WidthMethod,\n    options: { respectAlpha?: boolean; id?: string } = {},\n  ): OptimizedBuffer {\n    const lib = resolveRenderLib()\n    const respectAlpha = options.respectAlpha || false\n    const id = options.id && options.id.trim() !== \"\" ? options.id : \"unnamed buffer\"\n    const buffer = lib.createOptimizedBuffer(width, height, widthMethod, respectAlpha, id)\n    return buffer\n  }\n\n  public get widthMethod(): WidthMethod {\n    return this._widthMethod\n  }\n\n  public get width(): number {\n    return this._width\n  }\n\n  public get height(): number {\n    return this._height\n  }\n\n  public setRespectAlpha(respectAlpha: boolean): void {\n    this.guard()\n    this.lib.bufferSetRespectAlpha(this.bufferPtr, respectAlpha)\n    this.respectAlpha = respectAlpha\n  }\n\n  public getNativeId(): string {\n    this.guard()\n    return this.lib.bufferGetId(this.bufferPtr)\n  }\n\n  public getRealCharBytes(addLineBreaks: boolean = false): Uint8Array {\n    this.guard()\n    const realSize = this.lib.bufferGetRealCharSize(this.bufferPtr)\n    const outputBuffer = new Uint8Array(realSize)\n    const bytesWritten = this.lib.bufferWriteResolvedChars(this.bufferPtr, outputBuffer, addLineBreaks)\n    return outputBuffer.slice(0, bytesWritten)\n  }\n\n  public getSpanLines(): CapturedLine[] {\n    this.guard()\n    const { char, fg, bg, attributes } = this.buffers\n    const lines: CapturedLine[] = []\n\n    const CHAR_FLAG_CONTINUATION = 0xc0000000 | 0\n    const CHAR_FLAG_MASK = 0xc0000000 | 0\n\n    const realTextBytes = this.getRealCharBytes(true)\n    const realTextLines = new TextDecoder().decode(realTextBytes).split(\"\\n\")\n\n    for (let y = 0; y < this._height; y++) {\n      const spans: CapturedSpan[] = []\n      let currentSpan: CapturedSpan | null = null\n\n      const lineChars = [...(realTextLines[y] || \"\")]\n      let charIdx = 0\n\n      for (let x = 0; x < this._width; x++) {\n        const i = y * this._width + x\n        const cp = char[i]\n        const cellFg = RGBA.fromValues(fg[i * 4], fg[i * 4 + 1], fg[i * 4 + 2], fg[i * 4 + 3])\n        const cellBg = RGBA.fromValues(bg[i * 4], bg[i * 4 + 1], bg[i * 4 + 2], bg[i * 4 + 3])\n        const cellAttrs = attributes[i] & 0xff\n\n        // Continuation cells are placeholders for wide characters (emojis, CJK)\n        const isContinuation = (cp & CHAR_FLAG_MASK) === CHAR_FLAG_CONTINUATION\n        const cellChar = isContinuation ? \"\" : (lineChars[charIdx++] ?? \" \")\n\n        // Check if this cell continues the current span\n        if (\n          currentSpan &&\n          currentSpan.fg.equals(cellFg) &&\n          currentSpan.bg.equals(cellBg) &&\n          currentSpan.attributes === cellAttrs\n        ) {\n          currentSpan.text += cellChar\n          currentSpan.width += 1\n        } else {\n          // Start a new span\n          if (currentSpan) {\n            spans.push(currentSpan)\n          }\n          currentSpan = {\n            text: cellChar,\n            fg: cellFg,\n            bg: cellBg,\n            attributes: cellAttrs,\n            width: 1,\n          }\n        }\n      }\n\n      // Push the last span\n      if (currentSpan) {\n        spans.push(currentSpan)\n      }\n\n      lines.push({ spans })\n    }\n\n    return lines\n  }\n\n  public clear(bg: RGBA = RGBA.fromValues(0, 0, 0, 1)): void {\n    this.guard()\n    this.lib.bufferClear(this.bufferPtr, bg)\n  }\n\n  public setCell(x: number, y: number, char: string, fg: RGBA, bg: RGBA, attributes: number = 0): void {\n    this.guard()\n    this.lib.bufferSetCell(this.bufferPtr, x, y, char, fg, bg, attributes)\n  }\n\n  public setCellWithAlphaBlending(\n    x: number,\n    y: number,\n    char: string,\n    fg: RGBA,\n    bg: RGBA,\n    attributes: number = 0,\n  ): void {\n    this.guard()\n    this.lib.bufferSetCellWithAlphaBlending(this.bufferPtr, x, y, char, fg, bg, attributes)\n  }\n\n  public drawText(\n    text: string,\n    x: number,\n    y: number,\n    fg: RGBA,\n    bg?: RGBA,\n    attributes: number = 0,\n    selection?: { start: number; end: number; bgColor?: RGBA; fgColor?: RGBA } | null,\n  ): void {\n    this.guard()\n    if (!selection) {\n      this.lib.bufferDrawText(this.bufferPtr, text, x, y, fg, bg, attributes)\n      return\n    }\n\n    const { start, end } = selection\n\n    let selectionBg: RGBA\n    let selectionFg: RGBA\n\n    if (selection.bgColor) {\n      selectionBg = selection.bgColor\n      selectionFg = selection.fgColor || fg\n    } else {\n      const defaultBg = bg || RGBA.fromValues(0, 0, 0, 0)\n      selectionFg = defaultBg.a > 0 ? defaultBg : RGBA.fromValues(0, 0, 0, 1)\n      selectionBg = fg\n    }\n\n    if (start > 0) {\n      const beforeText = text.slice(0, start)\n      this.lib.bufferDrawText(this.bufferPtr, beforeText, x, y, fg, bg, attributes)\n    }\n\n    if (end > start) {\n      const selectedText = text.slice(start, end)\n      this.lib.bufferDrawText(this.bufferPtr, selectedText, x + start, y, selectionFg, selectionBg, attributes)\n    }\n\n    if (end < text.length) {\n      const afterText = text.slice(end)\n      this.lib.bufferDrawText(this.bufferPtr, afterText, x + end, y, fg, bg, attributes)\n    }\n  }\n\n  public fillRect(x: number, y: number, width: number, height: number, bg: RGBA): void {\n    this.lib.bufferFillRect(this.bufferPtr, x, y, width, height, bg)\n  }\n\n  public colorMatrix(\n    matrix: Float32Array,\n    cellMask: Float32Array,\n    strength: number = 1.0,\n    target: TargetChannel = TargetChannel.Both,\n  ): void {\n    this.guard()\n    if (matrix.length !== 16) throw new RangeError(`colorMatrix matrix must have length 16, got ${matrix.length}`)\n    const cellMaskCount = Math.floor(cellMask.length / 3)\n    this.lib.bufferColorMatrix(this.bufferPtr, ptr(matrix), ptr(cellMask), cellMaskCount, strength, target)\n  }\n\n  public colorMatrixUniform(\n    matrix: Float32Array,\n    strength: number = 1.0,\n    target: TargetChannel = TargetChannel.Both,\n  ): void {\n    this.guard()\n    if (matrix.length !== 16)\n      throw new RangeError(`colorMatrixUniform matrix must have length 16, got ${matrix.length}`)\n    if (strength === 0.0) return\n    this.lib.bufferColorMatrixUniform(this.bufferPtr, ptr(matrix), strength, target)\n  }\n\n  public drawFrameBuffer(\n    destX: number,\n    destY: number,\n    frameBuffer: OptimizedBuffer,\n    sourceX?: number,\n    sourceY?: number,\n    sourceWidth?: number,\n    sourceHeight?: number,\n  ): void {\n    this.guard()\n    this.lib.drawFrameBuffer(this.bufferPtr, destX, destY, frameBuffer.ptr, sourceX, sourceY, sourceWidth, sourceHeight)\n  }\n\n  public destroy(): void {\n    if (this._destroyed) return\n    this._destroyed = true\n    this.lib.destroyOptimizedBuffer(this.bufferPtr)\n  }\n\n  public drawTextBuffer(textBufferView: TextBufferView, x: number, y: number): void {\n    this.guard()\n    this.lib.bufferDrawTextBufferView(this.bufferPtr, textBufferView.ptr, x, y)\n  }\n\n  public drawEditorView(editorView: EditorView, x: number, y: number): void {\n    this.guard()\n    this.lib.bufferDrawEditorView(this.bufferPtr, editorView.ptr, x, y)\n  }\n\n  public drawSuperSampleBuffer(\n    x: number,\n    y: number,\n    pixelDataPtr: Pointer,\n    pixelDataLength: number,\n    format: \"bgra8unorm\" | \"rgba8unorm\",\n    alignedBytesPerRow: number,\n  ): void {\n    this.guard()\n    this.lib.bufferDrawSuperSampleBuffer(\n      this.bufferPtr,\n      x,\n      y,\n      pixelDataPtr,\n      pixelDataLength,\n      format,\n      alignedBytesPerRow,\n    )\n  }\n\n  public drawPackedBuffer(\n    dataPtr: Pointer,\n    dataLen: number,\n    posX: number,\n    posY: number,\n    terminalWidthCells: number,\n    terminalHeightCells: number,\n  ): void {\n    this.guard()\n    this.lib.bufferDrawPackedBuffer(\n      this.bufferPtr,\n      dataPtr,\n      dataLen,\n      posX,\n      posY,\n      terminalWidthCells,\n      terminalHeightCells,\n    )\n  }\n\n  public drawGrayscaleBuffer(\n    posX: number,\n    posY: number,\n    intensities: Float32Array,\n    srcWidth: number,\n    srcHeight: number,\n    fg: RGBA | null = null,\n    bg: RGBA | null = null,\n  ): void {\n    this.guard()\n    this.lib.bufferDrawGrayscaleBuffer(this.bufferPtr, posX, posY, ptr(intensities), srcWidth, srcHeight, fg, bg)\n  }\n\n  public drawGrayscaleBufferSupersampled(\n    posX: number,\n    posY: number,\n    intensities: Float32Array,\n    srcWidth: number,\n    srcHeight: number,\n    fg: RGBA | null = null,\n    bg: RGBA | null = null,\n  ): void {\n    this.guard()\n    this.lib.bufferDrawGrayscaleBufferSupersampled(\n      this.bufferPtr,\n      posX,\n      posY,\n      ptr(intensities),\n      srcWidth,\n      srcHeight,\n      fg,\n      bg,\n    )\n  }\n\n  public resize(width: number, height: number): void {\n    this.guard()\n    if (this._width === width && this._height === height) return\n\n    this._width = width\n    this._height = height\n    this._rawBuffers = null\n\n    this.lib.bufferResize(this.bufferPtr, width, height)\n  }\n\n  public drawBox(options: {\n    x: number\n    y: number\n    width: number\n    height: number\n    borderStyle?: BorderStyle\n    customBorderChars?: Uint32Array\n    border: boolean | BorderSides[]\n    borderColor: RGBA\n    backgroundColor: RGBA\n    shouldFill?: boolean\n    title?: string\n    titleAlignment?: \"left\" | \"center\" | \"right\"\n  }): void {\n    this.guard()\n    const style = parseBorderStyle(options.borderStyle, \"single\")\n    const borderChars: Uint32Array = options.customBorderChars ?? BorderCharArrays[style]\n\n    const packedOptions = packDrawOptions(options.border, options.shouldFill ?? false, options.titleAlignment || \"left\")\n\n    this.lib.bufferDrawBox(\n      this.bufferPtr,\n      options.x,\n      options.y,\n      options.width,\n      options.height,\n      borderChars,\n      packedOptions,\n      options.borderColor,\n      options.backgroundColor,\n      options.title ?? null,\n    )\n  }\n\n  public pushScissorRect(x: number, y: number, width: number, height: number): void {\n    this.guard()\n    this.lib.bufferPushScissorRect(this.bufferPtr, x, y, width, height)\n  }\n\n  public popScissorRect(): void {\n    this.guard()\n    this.lib.bufferPopScissorRect(this.bufferPtr)\n  }\n\n  public clearScissorRects(): void {\n    this.guard()\n    this.lib.bufferClearScissorRects(this.bufferPtr)\n  }\n\n  public pushOpacity(opacity: number): void {\n    this.guard()\n    this.lib.bufferPushOpacity(this.bufferPtr, Math.max(0, Math.min(1, opacity)))\n  }\n\n  public popOpacity(): void {\n    this.guard()\n    this.lib.bufferPopOpacity(this.bufferPtr)\n  }\n\n  public getCurrentOpacity(): number {\n    this.guard()\n    return this.lib.bufferGetCurrentOpacity(this.bufferPtr)\n  }\n\n  public clearOpacity(): void {\n    this.guard()\n    this.lib.bufferClearOpacity(this.bufferPtr)\n  }\n\n  public encodeUnicode(text: string): { ptr: Pointer; data: Array<{ width: number; char: number }> } | null {\n    this.guard()\n    return this.lib.encodeUnicode(text, this._widthMethod)\n  }\n\n  public freeUnicode(encoded: { ptr: Pointer; data: Array<{ width: number; char: number }> }): void {\n    this.guard()\n    this.lib.freeUnicode(encoded)\n  }\n\n  public drawGrid(options: {\n    borderChars: Uint32Array\n    borderFg: RGBA\n    borderBg: RGBA\n    columnOffsets: Int32Array\n    rowOffsets: Int32Array\n    drawInner: boolean\n    drawOuter: boolean\n  }): void {\n    this.guard()\n\n    const columnCount = Math.max(0, options.columnOffsets.length - 1)\n    const rowCount = Math.max(0, options.rowOffsets.length - 1)\n\n    this.lib.bufferDrawGrid(\n      this.bufferPtr,\n      options.borderChars,\n      options.borderFg,\n      options.borderBg,\n      options.columnOffsets,\n      columnCount,\n      options.rowOffsets,\n      rowCount,\n      {\n        drawInner: options.drawInner,\n        drawOuter: options.drawOuter,\n      },\n    )\n  }\n\n  public drawChar(char: number, x: number, y: number, fg: RGBA, bg: RGBA, attributes: number = 0): void {\n    this.guard()\n    this.lib.bufferDrawChar(this.bufferPtr, char, x, y, fg, bg, attributes)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/console.test.ts",
    "content": "import { test, expect, describe, mock, beforeEach } from \"bun:test\"\nimport { TerminalConsole, ConsolePosition } from \"./console.js\"\nimport { MouseEvent } from \"./renderer.js\"\nimport { ManualClock } from \"./testing/manual-clock.js\"\n\ninterface MockRenderer {\n  terminalWidth: number\n  terminalHeight: number\n  width: number\n  height: number\n  isRunning: boolean\n  widthMethod: string\n  requestRender: () => void\n  keyInput: {\n    on: (event: string, handler: any) => void\n    off: (event: string, handler: any) => void\n  }\n}\n\n// Helper function to create MouseEvent objects for testing\nfunction createMouseEvent(\n  x: number,\n  y: number,\n  type: \"down\" | \"up\" | \"move\" | \"drag\" | \"scroll\",\n  button: number = 0,\n  scroll?: { direction: \"up\" | \"down\" | \"left\" | \"right\"; delta: number },\n): MouseEvent {\n  return new MouseEvent(null, {\n    type,\n    button,\n    x,\n    y,\n    modifiers: { shift: false, alt: false, ctrl: false },\n    scroll,\n  })\n}\n\ndescribe(\"TerminalConsole\", () => {\n  let mockRenderer: MockRenderer\n  let terminalConsole: TerminalConsole\n\n  beforeEach(() => {\n    mockRenderer = {\n      terminalWidth: 100,\n      terminalHeight: 30,\n      width: 100,\n      height: 30,\n      isRunning: false,\n      widthMethod: \"cell\",\n      requestRender: mock(() => {}),\n      keyInput: {\n        on: mock(() => {}),\n        off: mock(() => {}),\n      },\n    }\n  })\n\n  describe(\"resize\", () => {\n    test(\"should use provided width and height parameters\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      const initialWidth = terminalConsole[\"consoleWidth\"]\n      expect(initialWidth).toBe(100)\n\n      terminalConsole.resize(80, 50)\n\n      expect(terminalConsole[\"consoleWidth\"]).toBe(80)\n      expect(terminalConsole[\"consoleHeight\"]).toBe(15) // 30% of 50\n    })\n\n    test(\"should apply sizePercent correctly for different positions\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.TOP,\n        sizePercent: 40,\n      })\n\n      terminalConsole.resize(100, 50)\n\n      expect(terminalConsole[\"consoleHeight\"]).toBe(20) // 40% of 50\n      expect(terminalConsole[\"consoleY\"]).toBe(0) // TOP position\n    })\n\n    test(\"should position console correctly for BOTTOM position\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      terminalConsole.resize(100, 50)\n\n      const consoleHeight = terminalConsole[\"consoleHeight\"]\n      expect(terminalConsole[\"consoleY\"]).toBe(50 - consoleHeight)\n    })\n\n    test(\"should position console correctly for RIGHT position\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.RIGHT,\n        sizePercent: 30,\n      })\n\n      terminalConsole.resize(100, 50)\n\n      const consoleWidth = terminalConsole[\"consoleWidth\"]\n      expect(terminalConsole[\"consoleX\"]).toBe(100 - consoleWidth)\n    })\n\n    test(\"should enforce minimum dimension of 1\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 5,\n      })\n\n      terminalConsole.resize(100, 10)\n\n      expect(terminalConsole[\"consoleHeight\"]).toBeGreaterThanOrEqual(1)\n    })\n  })\n\n  describe(\"Console Selection\", () => {\n    test(\"should have no selection initially\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      expect(terminalConsole[\"hasSelection\"]()).toBe(false)\n      expect(terminalConsole[\"getSelectedText\"]()).toBe(\"\")\n    })\n\n    test(\"should set selection on mouse down in log area\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n      terminalConsole[\"isVisible\"] = true\n\n      terminalConsole[\"_displayLines\"] = [\n        { text: \"Hello World\", level: \"LOG\" as any, indent: false },\n        { text: \"Second Line\", level: \"LOG\" as any, indent: false },\n      ]\n\n      const bounds = terminalConsole.bounds\n      const mouseEvent = createMouseEvent(bounds.x + 5, bounds.y + 1, \"down\", 0)\n      terminalConsole.handleMouse(mouseEvent)\n\n      expect(terminalConsole[\"_selectionStart\"]).not.toBeNull()\n      expect(terminalConsole[\"_isDragging\"]).toBe(true)\n    })\n\n    test(\"should extend selection on drag\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n      terminalConsole[\"isVisible\"] = true\n\n      terminalConsole[\"_displayLines\"] = [\n        { text: \"Hello World\", level: \"LOG\" as any, indent: false },\n        { text: \"Second Line\", level: \"LOG\" as any, indent: false },\n      ]\n\n      const bounds = terminalConsole.bounds\n      const downEvent = createMouseEvent(bounds.x + 1, bounds.y + 1, \"down\", 0)\n      terminalConsole.handleMouse(downEvent)\n      const dragEvent = createMouseEvent(bounds.x + 10, bounds.y + 1, \"drag\", 0)\n      terminalConsole.handleMouse(dragEvent)\n\n      expect(terminalConsole[\"_selectionEnd\"]?.col).toBe(9)\n    })\n\n    test(\"should finalize selection on mouse up\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n      terminalConsole[\"isVisible\"] = true\n\n      terminalConsole[\"_displayLines\"] = [{ text: \"Hello World\", level: \"LOG\" as any, indent: false }]\n\n      const bounds = terminalConsole.bounds\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 1, \"down\", 0))\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 5, bounds.y + 1, \"drag\", 0))\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 5, bounds.y + 1, \"up\", 0))\n\n      expect(terminalConsole[\"_isDragging\"]).toBe(false)\n      expect(terminalConsole[\"hasSelection\"]()).toBe(true)\n    })\n\n    test(\"should normalize reverse selection\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      terminalConsole[\"_selectionStart\"] = { line: 5, col: 10 }\n      terminalConsole[\"_selectionEnd\"] = { line: 2, col: 5 }\n\n      const normalized = terminalConsole[\"normalizeSelection\"]()\n\n      expect(normalized?.startLine).toBe(2)\n      expect(normalized?.startCol).toBe(5)\n      expect(normalized?.endLine).toBe(5)\n      expect(normalized?.endCol).toBe(10)\n    })\n\n    test(\"should extract correct text for single-line selection\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      terminalConsole[\"_displayLines\"] = [{ text: \"Hello World Test\", level: \"LOG\" as any, indent: false }]\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 0, col: 5 }\n\n      expect(terminalConsole[\"getSelectedText\"]()).toBe(\"Hello\")\n    })\n\n    test(\"should extract correct text for multi-line selection\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      terminalConsole[\"_displayLines\"] = [\n        { text: \"First Line\", level: \"LOG\" as any, indent: false },\n        { text: \"Second Line\", level: \"LOG\" as any, indent: false },\n        { text: \"Third Line\", level: \"LOG\" as any, indent: false },\n      ]\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 6 }\n      terminalConsole[\"_selectionEnd\"] = { line: 2, col: 5 }\n\n      const text = terminalConsole[\"getSelectedText\"]()\n      expect(text).toBe(\"Line\\nSecond Line\\nThird\")\n    })\n\n    test(\"should clear selection on clearSelection\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 0, col: 5 }\n      terminalConsole[\"_isDragging\"] = true\n\n      terminalConsole[\"clearSelection\"]()\n\n      expect(terminalConsole[\"_selectionStart\"]).toBeNull()\n      expect(terminalConsole[\"_selectionEnd\"]).toBeNull()\n      expect(terminalConsole[\"_isDragging\"]).toBe(false)\n    })\n  })\n\n  describe(\"Copy Button\", () => {\n    test(\"should trigger onCopySelection callback on click when selection exists\", () => {\n      const onCopy = mock(() => {})\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n        onCopySelection: onCopy,\n      })\n      terminalConsole[\"isVisible\"] = true\n      terminalConsole[\"_copyButtonBounds\"] = { x: 93, y: 0, width: 6, height: 1 }\n\n      terminalConsole[\"_displayLines\"] = [{ text: \"Hello World\", level: \"LOG\" as any, indent: false }]\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 0, col: 5 }\n\n      const bounds = terminalConsole.bounds\n      const copyButtonX = bounds.x + terminalConsole[\"_copyButtonBounds\"].x\n      const mouseEvent = createMouseEvent(copyButtonX, bounds.y, \"down\", 0)\n      terminalConsole.handleMouse(mouseEvent)\n\n      expect(onCopy).toHaveBeenCalledWith(\"Hello\")\n    })\n\n    test(\"should not trigger callback when no selection\", () => {\n      const onCopy = mock(() => {})\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n        onCopySelection: onCopy,\n      })\n      terminalConsole[\"isVisible\"] = true\n      terminalConsole[\"_copyButtonBounds\"] = { x: 93, y: 0, width: 6, height: 1 }\n\n      const bounds = terminalConsole.bounds\n      const copyButtonX = bounds.x + terminalConsole[\"_copyButtonBounds\"].x\n      const mouseEvent = createMouseEvent(copyButtonX, bounds.y, \"down\", 0)\n      terminalConsole.handleMouse(mouseEvent)\n\n      expect(onCopy).not.toHaveBeenCalled()\n    })\n  })\n\n  describe(\"Copy Keyboard Shortcut\", () => {\n    test(\"should trigger copy on Ctrl+Shift+C when focused with selection\", () => {\n      const onCopy = mock(() => {})\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n        onCopySelection: onCopy,\n      })\n\n      terminalConsole[\"_displayLines\"] = [{ text: \"Hello World\", level: \"LOG\" as any, indent: false }]\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 0, col: 5 }\n\n      terminalConsole[\"handleKeyPress\"]({ name: \"c\", ctrl: true, shift: true, meta: false } as any)\n\n      expect(onCopy).toHaveBeenCalledWith(\"Hello\")\n    })\n\n    test(\"should not trigger when no selection\", () => {\n      const onCopy = mock(() => {})\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n        onCopySelection: onCopy,\n      })\n\n      terminalConsole[\"handleKeyPress\"]({ name: \"c\", ctrl: true, shift: true, meta: false } as any)\n\n      expect(onCopy).not.toHaveBeenCalled()\n    })\n\n    test(\"should respect custom key bindings\", () => {\n      const onCopy = mock(() => {})\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n        onCopySelection: onCopy,\n        keyBindings: [{ name: \"y\", ctrl: true, action: \"copy-selection\" }],\n      })\n\n      terminalConsole[\"_displayLines\"] = [{ text: \"Test\", level: \"LOG\" as any, indent: false }]\n\n      // Test default binding (Ctrl+Shift+C) still works\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 0, col: 4 }\n      terminalConsole[\"handleKeyPress\"]({ name: \"c\", ctrl: true, shift: true, meta: false } as any)\n      expect(onCopy).toHaveBeenCalledWith(\"Test\")\n      expect(terminalConsole[\"hasSelection\"]()).toBe(false) // Selection cleared after copy\n      onCopy.mockClear()\n\n      // Test custom binding (Ctrl+Y) also works\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 0, col: 4 }\n      terminalConsole[\"handleKeyPress\"]({ name: \"y\", ctrl: true, shift: false, meta: false } as any)\n      expect(onCopy).toHaveBeenCalledWith(\"Test\")\n      expect(terminalConsole[\"hasSelection\"]()).toBe(false) // Selection cleared after copy\n    })\n\n    test(\"should update copy button label when key bindings change\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      // Check default label (lowercase)\n      const defaultLabel = terminalConsole[\"getCopyButtonLabel\"]()\n      expect(defaultLabel).toContain(\"ctrl+shift+c\")\n\n      // Update key bindings - last binding for the action wins in the label\n      terminalConsole.keyBindings = [{ name: \"y\", ctrl: true, action: \"copy-selection\" }]\n\n      // Check updated label - should show the last binding for copy-selection\n      const updatedLabel = terminalConsole[\"getCopyButtonLabel\"]()\n      expect(updatedLabel).toContain(\"ctrl+y\")\n      expect(updatedLabel).not.toContain(\"ctrl+shift+c\")\n    })\n  })\n\n  describe(\"Mouse Event Bounds\", () => {\n    test(\"should handle mouse events based on console bounds\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n      terminalConsole[\"isVisible\"] = true\n\n      // Outside bounds\n      const outsideEvent = createMouseEvent(0, 0, \"down\", 0)\n      expect(terminalConsole.handleMouse(outsideEvent)).toBe(false)\n\n      // Inside bounds\n      const bounds = terminalConsole.bounds\n      const insideEvent = createMouseEvent(bounds.x + 1, bounds.y + 1, \"down\", 0)\n      expect(terminalConsole.handleMouse(insideEvent)).toBe(true)\n    })\n\n    test(\"should not start selection on right-click\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n      terminalConsole[\"isVisible\"] = true\n      terminalConsole[\"_displayLines\"] = [{ text: \"Test\", level: \"LOG\" as any, indent: false }]\n\n      const bounds = terminalConsole.bounds\n      const rightClickEvent = createMouseEvent(bounds.x + 1, bounds.y + 1, \"down\", 2)\n      terminalConsole.handleMouse(rightClickEvent)\n\n      expect(terminalConsole[\"_isDragging\"]).toBe(false)\n      expect(terminalConsole[\"_selectionStart\"]).toBeNull()\n    })\n  })\n\n  describe(\"Auto-scroll during selection\", () => {\n    test(\"should auto-scroll up when dragging at top edge\", async () => {\n      const clock = new ManualClock()\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n        clock,\n      })\n      terminalConsole[\"isVisible\"] = true\n\n      // Create many display lines\n      const lines = []\n      for (let i = 0; i < 50; i++) {\n        lines.push({ text: `Line ${i}`, level: \"LOG\" as any, indent: false })\n      }\n      terminalConsole[\"_displayLines\"] = lines\n\n      // Scroll to middle\n      terminalConsole[\"scrollTopIndex\"] = 20\n\n      const bounds = terminalConsole.bounds\n      // Start selection in middle\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 5, \"down\", 0))\n\n      // Drag to top edge\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 1, \"drag\", 0))\n\n      // Check that auto-scroll interval was started\n      expect(terminalConsole[\"_autoScrollInterval\"]).not.toBeNull()\n\n      // Advance clock to trigger auto-scroll (interval is 50ms)\n      clock.advance(100)\n\n      // Should have scrolled up\n      expect(terminalConsole[\"scrollTopIndex\"]).toBeLessThan(20)\n\n      // Stop selecting\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 1, \"up\", 0))\n\n      // Auto-scroll should be stopped\n      expect(terminalConsole[\"_autoScrollInterval\"]).toBeNull()\n    })\n\n    test(\"should auto-scroll down when dragging at bottom edge\", async () => {\n      const clock = new ManualClock()\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n        clock,\n      })\n      terminalConsole[\"isVisible\"] = true\n\n      // Create many display lines\n      const lines = []\n      for (let i = 0; i < 50; i++) {\n        lines.push({ text: `Line ${i}`, level: \"LOG\" as any, indent: false })\n      }\n      terminalConsole[\"_displayLines\"] = lines\n\n      // Scroll to beginning\n      terminalConsole[\"scrollTopIndex\"] = 0\n\n      const bounds = terminalConsole.bounds\n      const logAreaHeight = Math.max(1, bounds.height - 1)\n\n      // Start selection in middle\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 5, \"down\", 0))\n\n      // Drag to bottom edge\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + logAreaHeight, \"drag\", 0))\n\n      // Check that auto-scroll interval was started\n      expect(terminalConsole[\"_autoScrollInterval\"]).not.toBeNull()\n\n      // Advance clock to trigger auto-scroll (interval is 50ms)\n      clock.advance(100)\n\n      // Should have scrolled down\n      expect(terminalConsole[\"scrollTopIndex\"]).toBeGreaterThan(0)\n\n      // Stop selecting\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + logAreaHeight, \"up\", 0))\n\n      // Auto-scroll should be stopped\n      expect(terminalConsole[\"_autoScrollInterval\"]).toBeNull()\n    })\n\n    test(\"should stop auto-scroll when dragging away from edge\", async () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n      terminalConsole[\"isVisible\"] = true\n\n      // Create many display lines\n      const lines = []\n      for (let i = 0; i < 50; i++) {\n        lines.push({ text: `Line ${i}`, level: \"LOG\" as any, indent: false })\n      }\n      terminalConsole[\"_displayLines\"] = lines\n      terminalConsole[\"scrollTopIndex\"] = 20\n\n      const bounds = terminalConsole.bounds\n\n      // Start selection\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 5, \"down\", 0))\n\n      // Drag to top edge to start auto-scroll\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 1, \"drag\", 0))\n      expect(terminalConsole[\"_autoScrollInterval\"]).not.toBeNull()\n\n      // Drag away from edge\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 5, \"drag\", 0))\n\n      // Auto-scroll should be stopped\n      expect(terminalConsole[\"_autoScrollInterval\"]).toBeNull()\n    })\n\n    test(\"should extend selection as auto-scroll happens\", async () => {\n      const clock = new ManualClock()\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n        clock,\n      })\n      terminalConsole[\"isVisible\"] = true\n\n      // Create many display lines\n      const lines = []\n      for (let i = 0; i < 50; i++) {\n        lines.push({ text: `Line ${i}`, level: \"LOG\" as any, indent: false })\n      }\n      terminalConsole[\"_displayLines\"] = lines\n      terminalConsole[\"scrollTopIndex\"] = 20\n\n      const bounds = terminalConsole.bounds\n\n      // Start selection\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 5, \"down\", 0))\n      const initialStartLine = terminalConsole[\"_selectionStart\"]?.line\n\n      // Drag to top edge\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 1, \"drag\", 0))\n\n      // Advance clock to trigger auto-scroll (interval is 50ms)\n      clock.advance(100)\n\n      // Selection end should have moved with the scroll\n      const endLine = terminalConsole[\"_selectionEnd\"]?.line\n      expect(endLine).toBeLessThan(initialStartLine!)\n\n      // Stop selecting\n      terminalConsole.handleMouse(createMouseEvent(bounds.x + 1, bounds.y + 1, \"up\", 0))\n    })\n  })\n\n  describe(\"Edge Cases\", () => {\n    test(\"should extract correct text for indented line selection\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      terminalConsole[\"_displayLines\"] = [\n        { text: \"Parent\", level: \"LOG\" as any, indent: false },\n        { text: \"Child\", level: \"LOG\" as any, indent: true },\n      ]\n      terminalConsole[\"_selectionStart\"] = { line: 1, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 1, col: 7 }\n\n      expect(terminalConsole[\"getSelectedText\"]()).toBe(\"  Child\")\n    })\n\n    test(\"should handle selection extending beyond display lines\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      terminalConsole[\"_displayLines\"] = [{ text: \"Only Line\", level: \"LOG\" as any, indent: false }]\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 5, col: 10 }\n\n      expect(terminalConsole[\"getSelectedText\"]()).toBe(\"Only Line\")\n    })\n\n    test(\"should not crash when onCopySelection is not provided\", () => {\n      terminalConsole = new TerminalConsole(mockRenderer as any, {\n        position: ConsolePosition.BOTTOM,\n        sizePercent: 30,\n      })\n\n      terminalConsole[\"_displayLines\"] = [{ text: \"Test\", level: \"LOG\" as any, indent: false }]\n      terminalConsole[\"_selectionStart\"] = { line: 0, col: 0 }\n      terminalConsole[\"_selectionEnd\"] = { line: 0, col: 4 }\n\n      expect(() => terminalConsole[\"triggerCopy\"]()).not.toThrow()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/console.ts",
    "content": "import { EventEmitter } from \"events\"\nimport { Console } from \"node:console\"\nimport fs from \"node:fs\"\nimport path from \"node:path\"\nimport util from \"node:util\"\nimport type { CliRenderer, ColorInput, MouseEvent } from \"./index.js\"\nimport { OptimizedBuffer } from \"./buffer.js\"\nimport { type Clock, SystemClock } from \"./lib/clock.js\"\nimport { Capture, CapturedWritableStream } from \"./lib/output.capture.js\"\nimport { parseColor, RGBA } from \"./lib/RGBA.js\"\nimport { singleton } from \"./lib/singleton.js\"\nimport { env, registerEnvVar } from \"./lib/env.js\"\nimport type { KeyEvent } from \"./lib/KeyHandler.js\"\nimport {\n  type KeyBinding as BaseKeyBinding,\n  mergeKeyBindings,\n  getKeyBindingKey,\n  buildKeyBindingsMap,\n  type KeyAliasMap,\n  defaultKeyAliases,\n  mergeKeyAliases,\n  keyBindingToString,\n} from \"./lib/keymapping.js\"\n\ninterface CallerInfo {\n  functionName: string\n  fullPath: string\n  fileName: string\n  lineNumber: number\n  columnNumber: number\n}\n\nfunction getCallerInfo(): CallerInfo | null {\n  const err = new Error()\n  const stackLines = err.stack?.split(\"\\n\").slice(5) || []\n  if (!stackLines.length) return null\n\n  const callerLine = stackLines[0].trim()\n\n  const regex = /at\\s+(?:([\\w$.<>]+)\\s+\\()?((?:\\/|[A-Za-z]:\\\\)[^:]+):(\\d+):(\\d+)\\)?/\n  const match = callerLine.match(regex)\n\n  if (!match) return null\n\n  // Extract details from the match.\n  const functionName = match[1] || \"<anonymous>\"\n  const fullPath = match[2]\n  const fileName = fullPath.split(/[\\\\/]/).pop() || \"<unknown>\"\n  const lineNumber = parseInt(match[3], 10) || 0\n  const columnNumber = parseInt(match[4], 10) || 0\n\n  return { functionName, fullPath, fileName, lineNumber, columnNumber }\n}\n\nenum LogLevel {\n  LOG = \"LOG\",\n  INFO = \"INFO\",\n  WARN = \"WARN\",\n  ERROR = \"ERROR\",\n  DEBUG = \"DEBUG\",\n}\n\nexport const capture = singleton(\"ConsoleCapture\", () => new Capture())\n\nregisterEnvVar({\n  name: \"OTUI_USE_CONSOLE\",\n  description: \"Whether to use the console. Will not capture console output if set to false.\",\n  type: \"boolean\",\n  default: true,\n})\n\nregisterEnvVar({\n  name: \"SHOW_CONSOLE\",\n  description: \"Show the console at startup if set to true.\",\n  type: \"boolean\",\n  default: false,\n})\n\nclass TerminalConsoleCache extends EventEmitter {\n  private _cachedLogs: [Date, LogLevel, any[], CallerInfo | null][] = []\n  private readonly MAX_CACHE_SIZE = 1000\n  private _collectCallerInfo: boolean = false\n  private _cachingEnabled: boolean = true\n  private _originalConsole: typeof console | null = null\n\n  get cachedLogs(): [Date, LogLevel, any[], CallerInfo | null][] {\n    return this._cachedLogs\n  }\n\n  constructor() {\n    super()\n\n    // Note: Console activation will be handled by the renderer when needed\n    // Don't activate on import to avoid hiding console.log globally\n  }\n\n  public activate(): void {\n    if (!this._originalConsole) {\n      this._originalConsole = global.console\n    }\n    this.setupConsoleCapture()\n    this.overrideConsoleMethods()\n  }\n\n  private setupConsoleCapture(): void {\n    if (!env.OTUI_USE_CONSOLE) return\n\n    const mockStdout = new CapturedWritableStream(\"stdout\", capture)\n    const mockStderr = new CapturedWritableStream(\"stderr\", capture)\n\n    // TODO: The Console constructor doesn't return a full Console interface implementation,\n    // it only provides a subset of methods (log, info, warn, error, debug, etc.).\n    // TypeScript's Console interface requires all methods (assert, clear, count, etc.).\n    // Using 'as any' as a workaround since we override the methods we use immediately after.\n    global.console = new Console({\n      stdout: mockStdout,\n      stderr: mockStderr,\n      colorMode: true,\n      inspectOptions: {\n        compact: false,\n        breakLength: 80,\n        depth: 2,\n      },\n    }) as any\n  }\n\n  private overrideConsoleMethods(): void {\n    console.log = (...args: any[]) => {\n      this.appendToConsole(LogLevel.LOG, ...args)\n    }\n\n    console.info = (...args: any[]) => {\n      this.appendToConsole(LogLevel.INFO, ...args)\n    }\n\n    console.warn = (...args: any[]) => {\n      this.appendToConsole(LogLevel.WARN, ...args)\n    }\n\n    console.error = (...args: any[]) => {\n      this.appendToConsole(LogLevel.ERROR, ...args)\n    }\n\n    console.debug = (...args: any[]) => {\n      this.appendToConsole(LogLevel.DEBUG, ...args)\n    }\n  }\n\n  public setCollectCallerInfo(enabled: boolean): void {\n    this._collectCallerInfo = enabled\n  }\n\n  public clearConsole(): void {\n    this._cachedLogs = []\n  }\n\n  public setCachingEnabled(enabled: boolean): void {\n    this._cachingEnabled = enabled\n  }\n\n  public deactivate(): void {\n    this.restoreOriginalConsole()\n  }\n\n  private restoreOriginalConsole(): void {\n    if (this._originalConsole) {\n      global.console = this._originalConsole\n    }\n\n    this.setupConsoleCapture()\n  }\n\n  public addLogEntry(level: LogLevel, ...args: any[]) {\n    const callerInfo = this._collectCallerInfo ? getCallerInfo() : null\n    const logEntry: [Date, LogLevel, any[], CallerInfo | null] = [new Date(), level, args, callerInfo]\n\n    if (this._cachingEnabled) {\n      if (this._cachedLogs.length >= this.MAX_CACHE_SIZE) {\n        this._cachedLogs.shift()\n      }\n      this._cachedLogs.push(logEntry)\n    }\n\n    return logEntry\n  }\n\n  private appendToConsole(level: LogLevel, ...args: any[]): void {\n    if (this._cachedLogs.length >= this.MAX_CACHE_SIZE) {\n      this._cachedLogs.shift()\n    }\n    const entry = this.addLogEntry(level, ...args)\n    this.emit(\"entry\", entry)\n  }\n\n  public destroy(): void {\n    this.deactivate()\n  }\n}\n\nconst terminalConsoleCache = singleton(\"TerminalConsoleCache\", () => {\n  const terminalConsoleCache = new TerminalConsoleCache()\n  process.on(\"exit\", () => {\n    terminalConsoleCache.destroy()\n  })\n  return terminalConsoleCache\n})\n\nexport enum ConsolePosition {\n  TOP = \"top\",\n  BOTTOM = \"bottom\",\n  LEFT = \"left\",\n  RIGHT = \"right\",\n}\n\ninterface ConsoleSelection {\n  startLine: number\n  startCol: number\n  endLine: number\n  endCol: number\n}\n\nexport type ConsoleAction =\n  | \"scroll-up\"\n  | \"scroll-down\"\n  | \"scroll-to-top\"\n  | \"scroll-to-bottom\"\n  | \"position-previous\"\n  | \"position-next\"\n  | \"size-increase\"\n  | \"size-decrease\"\n  | \"save-logs\"\n  | \"copy-selection\"\n\nexport type ConsoleKeyBinding = BaseKeyBinding<ConsoleAction>\n\nconst defaultConsoleKeybindings: ConsoleKeyBinding[] = [\n  { name: \"up\", action: \"scroll-up\" },\n  { name: \"down\", action: \"scroll-down\" },\n  { name: \"up\", shift: true, action: \"scroll-to-top\" },\n  { name: \"down\", shift: true, action: \"scroll-to-bottom\" },\n  { name: \"p\", ctrl: true, action: \"position-previous\" },\n  { name: \"o\", ctrl: true, action: \"position-next\" },\n  { name: \"+\", action: \"size-increase\" },\n  { name: \"=\", shift: true, action: \"size-increase\" },\n  { name: \"-\", action: \"size-decrease\" },\n  { name: \"s\", ctrl: true, action: \"save-logs\" },\n  { name: \"c\", ctrl: true, shift: true, action: \"copy-selection\" },\n]\n\nexport interface ConsoleOptions {\n  position?: ConsolePosition\n  sizePercent?: number\n  zIndex?: number\n  colorInfo?: ColorInput\n  colorWarn?: ColorInput\n  colorError?: ColorInput\n  colorDebug?: ColorInput\n  colorDefault?: ColorInput\n  backgroundColor?: ColorInput\n  startInDebugMode?: boolean\n  title?: string\n  titleBarColor?: ColorInput\n  titleBarTextColor?: ColorInput\n  cursorColor?: ColorInput\n  maxStoredLogs?: number\n  maxDisplayLines?: number\n  onCopySelection?: (text: string) => void\n  keyBindings?: ConsoleKeyBinding[]\n  keyAliasMap?: KeyAliasMap\n  selectionColor?: ColorInput\n  copyButtonColor?: ColorInput\n  clock?: Clock\n}\n\nconst DEFAULT_CONSOLE_OPTIONS: Required<\n  Omit<ConsoleOptions, \"onCopySelection\" | \"keyBindings\" | \"keyAliasMap\" | \"clock\">\n> & {\n  onCopySelection?: (text: string) => void\n  keyBindings?: ConsoleKeyBinding[]\n  keyAliasMap?: KeyAliasMap\n} = {\n  position: ConsolePosition.BOTTOM,\n  sizePercent: 30,\n  zIndex: Infinity,\n  colorInfo: \"#00FFFF\", // Cyan\n  colorWarn: \"#FFFF00\", // Yellow\n  colorError: \"#FF0000\", // Red\n  colorDebug: \"#808080\", // Gray\n  colorDefault: \"#FFFFFF\", // White\n  backgroundColor: RGBA.fromValues(0.1, 0.1, 0.1, 0.7),\n  startInDebugMode: false,\n  title: \"Console\",\n  titleBarColor: RGBA.fromValues(0.05, 0.05, 0.05, 0.7),\n  titleBarTextColor: \"#FFFFFF\",\n  cursorColor: \"#00A0FF\",\n  maxStoredLogs: 2000,\n  maxDisplayLines: 3000,\n  onCopySelection: undefined,\n  keyBindings: undefined,\n  keyAliasMap: undefined,\n  selectionColor: RGBA.fromValues(0.3, 0.5, 0.8, 0.5),\n  copyButtonColor: \"#00A0FF\",\n}\n\nconst INDENT_WIDTH = 2\n\ninterface DisplayLine {\n  text: string\n  level: LogLevel\n  indent: boolean\n}\n\nexport class TerminalConsole extends EventEmitter {\n  private isVisible: boolean = false\n  private isFocused: boolean = false\n  private renderer: CliRenderer\n  private keyHandler: (event: KeyEvent) => void\n  private options: Required<Omit<ConsoleOptions, \"onCopySelection\" | \"keyBindings\" | \"keyAliasMap\" | \"clock\">> & {\n    onCopySelection?: (text: string) => void\n    keyBindings?: ConsoleKeyBinding[]\n    keyAliasMap?: KeyAliasMap\n  }\n  private _debugModeEnabled: boolean = false\n\n  private frameBuffer: OptimizedBuffer | null = null\n  private consoleX: number = 0\n  private consoleY: number = 0\n  private consoleWidth: number = 0\n  private consoleHeight: number = 0\n  private scrollTopIndex: number = 0\n  private isScrolledToBottom: boolean = true\n  private currentLineIndex: number = 0\n  private _displayLines: DisplayLine[] = []\n  private _allLogEntries: [Date, LogLevel, any[], CallerInfo | null][] = []\n  private _needsFrameBufferUpdate: boolean = false\n  private _entryListener: (logEntry: [Date, LogLevel, any[], CallerInfo | null]) => void\n\n  private _selectionStart: { line: number; col: number } | null = null\n  private _selectionEnd: { line: number; col: number } | null = null\n  private _isDragging: boolean = false\n  private _copyButtonBounds: { x: number; y: number; width: number; height: number } = {\n    x: 0,\n    y: 0,\n    width: 0,\n    height: 0,\n  }\n  private _autoScrollInterval: ReturnType<Clock[\"setInterval\"]> | null = null\n  private readonly clock: Clock\n\n  private _keyBindingsMap: Map<string, ConsoleAction>\n  private _keyAliasMap: KeyAliasMap\n  private _keyBindings: ConsoleKeyBinding[]\n  private _mergedKeyBindings: ConsoleKeyBinding[]\n  private _actionHandlers: Map<ConsoleAction, () => boolean>\n\n  private markNeedsRerender(): void {\n    this._needsFrameBufferUpdate = true\n    this.renderer.requestRender()\n  }\n\n  private getCopyButtonLabel(): string {\n    const copyBindings = this._mergedKeyBindings.filter((b) => b.action === \"copy-selection\")\n    const copyBinding = copyBindings[copyBindings.length - 1]\n    if (copyBinding) {\n      const shortcut = keyBindingToString(copyBinding)\n      return `[Copy (${shortcut})]`\n    }\n    return \"[Copy]\"\n  }\n\n  private _rgbaInfo: RGBA\n  private _rgbaWarn: RGBA\n  private _rgbaError: RGBA\n  private _rgbaDebug: RGBA\n  private _rgbaDefault: RGBA\n  private backgroundColor: RGBA\n  private _rgbaTitleBar: RGBA\n  private _rgbaTitleBarText: RGBA\n  private _title: string\n  private _rgbaCursor: RGBA\n  private _rgbaSelection: RGBA\n  private _rgbaCopyButton: RGBA\n\n  private _positions: ConsolePosition[] = [\n    ConsolePosition.TOP,\n    ConsolePosition.RIGHT,\n    ConsolePosition.BOTTOM,\n    ConsolePosition.LEFT,\n  ]\n\n  constructor(renderer: CliRenderer, options: ConsoleOptions = {}) {\n    super()\n    this.renderer = renderer\n    this.clock = options.clock ?? new SystemClock()\n    this.options = { ...DEFAULT_CONSOLE_OPTIONS, ...options }\n    this.keyHandler = this.handleKeyPress.bind(this)\n    this._debugModeEnabled = this.options.startInDebugMode\n    terminalConsoleCache.setCollectCallerInfo(this._debugModeEnabled)\n\n    this._rgbaInfo = parseColor(this.options.colorInfo)\n    this._rgbaWarn = parseColor(this.options.colorWarn)\n    this._rgbaError = parseColor(this.options.colorError)\n    this._rgbaDebug = parseColor(this.options.colorDebug)\n    this._rgbaDefault = parseColor(this.options.colorDefault)\n    this.backgroundColor = parseColor(this.options.backgroundColor)\n    this._rgbaTitleBar = parseColor(this.options.titleBarColor)\n    this._rgbaTitleBarText = parseColor(this.options.titleBarTextColor || this.options.colorDefault)\n    this._title = this.options.title\n    this._rgbaCursor = parseColor(this.options.cursorColor)\n    this._rgbaSelection = parseColor(this.options.selectionColor)\n    this._rgbaCopyButton = parseColor(this.options.copyButtonColor)\n\n    this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {})\n    this._keyBindings = options.keyBindings || []\n    this._mergedKeyBindings = mergeKeyBindings(defaultConsoleKeybindings, this._keyBindings)\n    this._keyBindingsMap = buildKeyBindingsMap(this._mergedKeyBindings, this._keyAliasMap)\n    this._actionHandlers = this.buildActionHandlers()\n\n    this._updateConsoleDimensions()\n    this._scrollToBottom(true)\n\n    this._entryListener = (logEntry: [Date, LogLevel, any[], CallerInfo | null]) => {\n      this._handleNewLog(logEntry)\n    }\n    terminalConsoleCache.on(\"entry\", this._entryListener)\n\n    if (env.SHOW_CONSOLE) {\n      this.show()\n    }\n  }\n\n  private buildActionHandlers(): Map<ConsoleAction, () => boolean> {\n    return new Map([\n      [\"scroll-up\", () => this.scrollUp()],\n      [\"scroll-down\", () => this.scrollDown()],\n      [\"scroll-to-top\", () => this.scrollToTop()],\n      [\"scroll-to-bottom\", () => this.scrollToBottomAction()],\n      [\"position-previous\", () => this.positionPrevious()],\n      [\"position-next\", () => this.positionNext()],\n      [\"size-increase\", () => this.sizeIncrease()],\n      [\"size-decrease\", () => this.sizeDecrease()],\n      [\"save-logs\", () => this.saveLogsAction()],\n      [\"copy-selection\", () => this.triggerCopyAction()],\n    ])\n  }\n\n  public activate(): void {\n    terminalConsoleCache.activate()\n  }\n\n  public deactivate(): void {\n    terminalConsoleCache.deactivate()\n  }\n\n  // Handles a single new log entry *while the console is visible*\n  private _handleNewLog(logEntry: [Date, LogLevel, any[], CallerInfo | null]): void {\n    if (!this.isVisible) return\n\n    this._allLogEntries.push(logEntry)\n\n    if (this._allLogEntries.length > this.options.maxStoredLogs) {\n      this._allLogEntries.splice(0, this._allLogEntries.length - this.options.maxStoredLogs)\n    }\n\n    const newDisplayLines = this._processLogEntry(logEntry)\n    this._displayLines.push(...newDisplayLines)\n\n    if (this._displayLines.length > this.options.maxDisplayLines) {\n      this._displayLines.splice(0, this._displayLines.length - this.options.maxDisplayLines)\n      const linesRemoved = this._displayLines.length - this.options.maxDisplayLines\n      this.scrollTopIndex = Math.max(0, this.scrollTopIndex - linesRemoved)\n    }\n\n    if (this.isScrolledToBottom) {\n      this._scrollToBottom()\n    }\n    this.markNeedsRerender()\n  }\n\n  private _updateConsoleDimensions(termWidth?: number, termHeight?: number): void {\n    const width = termWidth ?? this.renderer.width\n    const height = termHeight ?? this.renderer.height\n    const sizePercent = this.options.sizePercent / 100\n\n    switch (this.options.position) {\n      case ConsolePosition.TOP:\n        this.consoleX = 0\n        this.consoleY = 0\n        this.consoleWidth = width\n        this.consoleHeight = Math.max(1, Math.floor(height * sizePercent))\n        break\n      case ConsolePosition.BOTTOM:\n        this.consoleHeight = Math.max(1, Math.floor(height * sizePercent))\n        this.consoleWidth = width\n        this.consoleX = 0\n        this.consoleY = height - this.consoleHeight\n        break\n      case ConsolePosition.LEFT:\n        this.consoleWidth = Math.max(1, Math.floor(width * sizePercent))\n        this.consoleHeight = height\n        this.consoleX = 0\n        this.consoleY = 0\n        break\n      case ConsolePosition.RIGHT:\n        this.consoleWidth = Math.max(1, Math.floor(width * sizePercent))\n        this.consoleHeight = height\n        this.consoleY = 0\n        this.consoleX = width - this.consoleWidth\n        break\n    }\n    this.currentLineIndex = Math.max(0, Math.min(this.currentLineIndex, this.consoleHeight - 1))\n  }\n\n  private handleKeyPress(event: KeyEvent): void {\n    if (event.name === \"escape\") {\n      this.blur()\n      return\n    }\n\n    const bindingKey = getKeyBindingKey({\n      name: event.name,\n      ctrl: event.ctrl,\n      shift: event.shift,\n      meta: event.meta,\n      super: event.super,\n      action: \"scroll-up\" as ConsoleAction,\n    })\n\n    const action = this._keyBindingsMap.get(bindingKey)\n\n    if (action) {\n      const handler = this._actionHandlers.get(action)\n      if (handler) {\n        handler()\n        return\n      }\n    }\n  }\n\n  private scrollUp(): boolean {\n    const logAreaHeight = Math.max(1, this.consoleHeight - 1)\n\n    if (this.currentLineIndex > 0) {\n      this.currentLineIndex--\n      this.markNeedsRerender()\n    } else if (this.scrollTopIndex > 0) {\n      this.scrollTopIndex--\n      this.isScrolledToBottom = false\n      this.markNeedsRerender()\n    }\n    return true\n  }\n\n  private scrollDown(): boolean {\n    const displayLineCount = this._displayLines.length\n    const logAreaHeight = Math.max(1, this.consoleHeight - 1)\n    const maxScrollTop = Math.max(0, displayLineCount - logAreaHeight)\n    const canCursorMoveDown =\n      this.currentLineIndex < logAreaHeight - 1 && this.scrollTopIndex + this.currentLineIndex < displayLineCount - 1\n\n    if (canCursorMoveDown) {\n      this.currentLineIndex++\n      this.markNeedsRerender()\n    } else if (this.scrollTopIndex < maxScrollTop) {\n      this.scrollTopIndex++\n      this.isScrolledToBottom = this.scrollTopIndex === maxScrollTop\n      this.markNeedsRerender()\n    }\n    return true\n  }\n\n  private scrollToTop(): boolean {\n    if (this.scrollTopIndex > 0 || this.currentLineIndex > 0) {\n      this.scrollTopIndex = 0\n      this.currentLineIndex = 0\n      this.isScrolledToBottom = this._displayLines.length <= Math.max(1, this.consoleHeight - 1)\n      this.markNeedsRerender()\n    }\n    return true\n  }\n\n  private scrollToBottomAction(): boolean {\n    const logAreaHeightForScroll = Math.max(1, this.consoleHeight - 1)\n    const maxScrollPossible = Math.max(0, this._displayLines.length - logAreaHeightForScroll)\n    if (this.scrollTopIndex < maxScrollPossible || !this.isScrolledToBottom) {\n      this._scrollToBottom(true)\n      this.markNeedsRerender()\n    }\n    return true\n  }\n\n  private positionPrevious(): boolean {\n    const currentPositionIndex = this._positions.indexOf(this.options.position)\n    const prevIndex = (currentPositionIndex - 1 + this._positions.length) % this._positions.length\n    this.options.position = this._positions[prevIndex]\n    this.resize(this.renderer.width, this.renderer.height)\n    return true\n  }\n\n  private positionNext(): boolean {\n    const currentPositionIndex = this._positions.indexOf(this.options.position)\n    const nextIndex = (currentPositionIndex + 1) % this._positions.length\n    this.options.position = this._positions[nextIndex]\n    this.resize(this.renderer.width, this.renderer.height)\n    return true\n  }\n\n  private sizeIncrease(): boolean {\n    this.options.sizePercent = Math.min(100, this.options.sizePercent + 5)\n    this.resize(this.renderer.width, this.renderer.height)\n    return true\n  }\n\n  private sizeDecrease(): boolean {\n    this.options.sizePercent = Math.max(10, this.options.sizePercent - 5)\n    this.resize(this.renderer.width, this.renderer.height)\n    return true\n  }\n\n  private saveLogsAction(): boolean {\n    this.saveLogsToFile()\n    return true\n  }\n\n  private triggerCopyAction(): boolean {\n    this.triggerCopy()\n    return true\n  }\n\n  private attachStdin(): void {\n    if (this.isFocused) return\n    this.renderer.keyInput.on(\"keypress\", this.keyHandler)\n    this.isFocused = true\n  }\n\n  private detachStdin(): void {\n    if (!this.isFocused) return\n    this.renderer.keyInput.off(\"keypress\", this.keyHandler)\n    this.isFocused = false\n  }\n\n  private formatTimestamp(date: Date): string {\n    return new Intl.DateTimeFormat(\"en-US\", {\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      second: \"2-digit\",\n      hour12: false,\n    }).format(date)\n  }\n\n  private formatArguments(args: any[]): string {\n    return args\n      .map((arg) => {\n        if (arg instanceof Error) {\n          const errorProps = arg\n          return `Error: ${errorProps.message}\\n` + (errorProps.stack ? `${errorProps.stack}\\n` : \"\")\n        }\n        if (typeof arg === \"object\" && arg !== null) {\n          try {\n            return util.inspect(arg, { depth: 2 })\n          } catch (e) {\n            return String(arg)\n          }\n        }\n        try {\n          return util.inspect(arg, { depth: 2 })\n        } catch (e) {\n          return String(arg)\n        }\n      })\n      .join(\" \")\n  }\n\n  public resize(width: number, height: number): void {\n    this._updateConsoleDimensions(width, height)\n\n    if (this.frameBuffer) {\n      this.frameBuffer.resize(this.consoleWidth, this.consoleHeight)\n\n      const displayLineCount = this._displayLines.length\n      const logAreaHeight = Math.max(1, this.consoleHeight - 1)\n      const maxScrollTop = Math.max(0, displayLineCount - logAreaHeight)\n      this.scrollTopIndex = Math.min(this.scrollTopIndex, maxScrollTop)\n      this.isScrolledToBottom = this.scrollTopIndex === maxScrollTop\n      const visibleLineCount = Math.min(logAreaHeight, displayLineCount - this.scrollTopIndex)\n      this.currentLineIndex = Math.max(0, Math.min(this.currentLineIndex, visibleLineCount - 1))\n\n      if (this.isVisible) {\n        this.markNeedsRerender()\n      }\n    }\n  }\n\n  public clear(): void {\n    terminalConsoleCache.clearConsole()\n    this._allLogEntries = []\n    this._displayLines = []\n    this.markNeedsRerender()\n  }\n\n  public toggle(): void {\n    if (this.isVisible) {\n      if (this.isFocused) {\n        this.hide()\n      } else {\n        this.focus()\n      }\n    } else {\n      this.show()\n    }\n    if (!this.renderer.isRunning) {\n      this.renderer.requestRender()\n    }\n  }\n\n  public focus(): void {\n    this.attachStdin()\n    this._scrollToBottom(true)\n    this.markNeedsRerender()\n  }\n\n  public blur(): void {\n    this.detachStdin()\n    this.markNeedsRerender()\n  }\n\n  public show(): void {\n    if (!this.isVisible) {\n      this.isVisible = true\n      this._processCachedLogs()\n      terminalConsoleCache.setCachingEnabled(false)\n\n      if (!this.frameBuffer) {\n        this.frameBuffer = OptimizedBuffer.create(this.consoleWidth, this.consoleHeight, this.renderer.widthMethod, {\n          respectAlpha: this.backgroundColor.a < 1,\n          id: \"console framebuffer\",\n        })\n      }\n      const logCount = terminalConsoleCache.cachedLogs.length\n      const visibleLogLines = Math.min(this.consoleHeight, logCount)\n      this.currentLineIndex = Math.max(0, visibleLogLines - 1)\n      this.scrollTopIndex = 0\n      this._scrollToBottom(true)\n\n      this.focus()\n      this.markNeedsRerender()\n    }\n  }\n\n  public hide(): void {\n    if (this.isVisible) {\n      this.isVisible = false\n      this.blur()\n      terminalConsoleCache.setCachingEnabled(true)\n    }\n  }\n\n  public destroy(): void {\n    this.stopAutoScroll()\n    this.hide()\n    this.deactivate()\n    terminalConsoleCache.off(\"entry\", this._entryListener)\n  }\n\n  public getCachedLogs(): string {\n    return terminalConsoleCache.cachedLogs\n      .map((logEntry) => logEntry[0].toISOString() + \" \" + logEntry.slice(1).join(\" \"))\n      .join(\"\\n\")\n  }\n\n  private updateFrameBuffer(): void {\n    if (!this.frameBuffer) return\n\n    this.frameBuffer.clear(this.backgroundColor)\n\n    const displayLines = this._displayLines\n    const displayLineCount = displayLines.length\n    const logAreaHeight = Math.max(1, this.consoleHeight - 1)\n\n    // --- Draw Title Bar ---\n    this.frameBuffer.fillRect(0, 0, this.consoleWidth, 1, this._rgbaTitleBar)\n    const dynamicTitle = `${this._title}${this.isFocused ? \" (Focused)\" : \"\"}`\n    const titleX = Math.max(0, Math.floor((this.consoleWidth - dynamicTitle.length) / 2))\n    this.frameBuffer.drawText(dynamicTitle, titleX, 0, this._rgbaTitleBarText, this._rgbaTitleBar)\n\n    // --- Draw [Copy] Button ---\n    const copyLabel = this.getCopyButtonLabel()\n    const copyButtonX = this.consoleWidth - copyLabel.length - 1\n    if (copyButtonX >= 0) {\n      const copyButtonEnabled = this.hasSelection()\n      const disabledColor = RGBA.fromInts(100, 100, 100, 255)\n      const copyColor = copyButtonEnabled ? this._rgbaCopyButton : disabledColor\n      this.frameBuffer.drawText(copyLabel, copyButtonX, 0, copyColor, this._rgbaTitleBar)\n      this._copyButtonBounds = { x: copyButtonX, y: 0, width: copyLabel.length, height: 1 }\n    } else {\n      this._copyButtonBounds = { x: -1, y: -1, width: 0, height: 0 }\n    }\n\n    const startIndex = this.scrollTopIndex\n    const endIndex = Math.min(startIndex + logAreaHeight, displayLineCount)\n    const visibleDisplayLines = displayLines.slice(startIndex, endIndex)\n\n    let lineY = 1\n    for (let i = 0; i < visibleDisplayLines.length; i++) {\n      if (lineY >= this.consoleHeight) break\n\n      const displayLine = visibleDisplayLines[i]\n      const absoluteLineIndex = startIndex + i\n\n      let levelColor = this._rgbaDefault\n      switch (displayLine.level) {\n        case LogLevel.INFO:\n          levelColor = this._rgbaInfo\n          break\n        case LogLevel.WARN:\n          levelColor = this._rgbaWarn\n          break\n        case LogLevel.ERROR:\n          levelColor = this._rgbaError\n          break\n        case LogLevel.DEBUG:\n          levelColor = this._rgbaDebug\n          break\n      }\n\n      const linePrefix = displayLine.indent ? \" \".repeat(INDENT_WIDTH) : \"\"\n      const textToDraw = displayLine.text\n      const textAvailableWidth = this.consoleWidth - 1 - (displayLine.indent ? INDENT_WIDTH : 0)\n      const showCursor = this.isFocused && lineY - 1 === this.currentLineIndex\n\n      if (showCursor) {\n        this.frameBuffer.drawText(\">\", 0, lineY, this._rgbaCursor, this.backgroundColor)\n      } else {\n        this.frameBuffer.drawText(\" \", 0, lineY, this._rgbaDefault, this.backgroundColor)\n      }\n\n      const fullText = `${linePrefix}${textToDraw.substring(0, textAvailableWidth)}`\n      const selectionRange = this.getLineSelectionRange(absoluteLineIndex)\n\n      if (selectionRange) {\n        const adjustedStart = Math.max(0, selectionRange.start)\n        const adjustedEnd = Math.min(fullText.length, selectionRange.end)\n\n        if (adjustedStart > 0) {\n          this.frameBuffer.drawText(fullText.substring(0, adjustedStart), 1, lineY, levelColor)\n        }\n\n        if (adjustedStart < adjustedEnd) {\n          this.frameBuffer.fillRect(1 + adjustedStart, lineY, adjustedEnd - adjustedStart, 1, this._rgbaSelection)\n          this.frameBuffer.drawText(\n            fullText.substring(adjustedStart, adjustedEnd),\n            1 + adjustedStart,\n            lineY,\n            levelColor,\n            this._rgbaSelection,\n          )\n        }\n\n        if (adjustedEnd < fullText.length) {\n          this.frameBuffer.drawText(fullText.substring(adjustedEnd), 1 + adjustedEnd, lineY, levelColor)\n        }\n      } else {\n        this.frameBuffer.drawText(fullText, 1, lineY, levelColor)\n      }\n\n      lineY++\n    }\n  }\n\n  public renderToBuffer(buffer: OptimizedBuffer): void {\n    if (!this.isVisible || !this.frameBuffer) return\n\n    if (this._needsFrameBufferUpdate) {\n      this.updateFrameBuffer()\n      this._needsFrameBufferUpdate = false\n    }\n\n    buffer.drawFrameBuffer(this.consoleX, this.consoleY, this.frameBuffer)\n  }\n\n  public setDebugMode(enabled: boolean): void {\n    this._debugModeEnabled = enabled\n    terminalConsoleCache.setCollectCallerInfo(enabled)\n    if (this.isVisible) {\n      this.markNeedsRerender()\n    }\n  }\n\n  public toggleDebugMode(): void {\n    this.setDebugMode(!this._debugModeEnabled)\n  }\n\n  public set keyBindings(bindings: ConsoleKeyBinding[]) {\n    this._keyBindings = bindings\n    this._mergedKeyBindings = mergeKeyBindings(defaultConsoleKeybindings, bindings)\n    this._keyBindingsMap = buildKeyBindingsMap(this._mergedKeyBindings, this._keyAliasMap)\n    this.markNeedsRerender()\n  }\n\n  public set keyAliasMap(aliases: KeyAliasMap) {\n    this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, aliases)\n    this._mergedKeyBindings = mergeKeyBindings(defaultConsoleKeybindings, this._keyBindings)\n    this._keyBindingsMap = buildKeyBindingsMap(this._mergedKeyBindings, this._keyAliasMap)\n    this.markNeedsRerender()\n  }\n\n  public set onCopySelection(callback: ((text: string) => void) | undefined) {\n    this.options.onCopySelection = callback\n  }\n\n  public get onCopySelection(): ((text: string) => void) | undefined {\n    return this.options.onCopySelection\n  }\n\n  private _scrollToBottom(forceCursorToLastLine: boolean = false): void {\n    const displayLineCount = this._displayLines.length\n    const logAreaHeight = Math.max(1, this.consoleHeight - 1)\n    const maxScrollTop = Math.max(0, displayLineCount - logAreaHeight)\n    this.scrollTopIndex = maxScrollTop\n    this.isScrolledToBottom = true\n\n    const visibleLineCount = Math.min(logAreaHeight, displayLineCount - this.scrollTopIndex)\n    if (forceCursorToLastLine || this.currentLineIndex >= visibleLineCount) {\n      this.currentLineIndex = Math.max(0, visibleLineCount - 1)\n    }\n  }\n\n  private _processLogEntry(logEntry: [Date, LogLevel, any[], CallerInfo | null]): DisplayLine[] {\n    const [date, level, args, callerInfo] = logEntry\n    const displayLines: DisplayLine[] = []\n\n    const timestamp = this.formatTimestamp(date)\n    const callerSource = callerInfo ? `${callerInfo.fileName}:${callerInfo.lineNumber}` : \"unknown\"\n    const prefix = `[${timestamp}] [${level}]` + (this._debugModeEnabled ? ` [${callerSource}]` : \"\") + \" \"\n\n    const formattedArgs = this.formatArguments(args)\n    const initialLines = formattedArgs.split(\"\\n\")\n\n    for (let i = 0; i < initialLines.length; i++) {\n      const lineText = initialLines[i]\n      const isFirstLineOfEntry = i === 0\n      const availableWidth = this.consoleWidth - 1 - (isFirstLineOfEntry ? 0 : INDENT_WIDTH)\n      const linePrefix = isFirstLineOfEntry ? prefix : \" \".repeat(INDENT_WIDTH)\n      const textToWrap = isFirstLineOfEntry ? linePrefix + lineText : lineText\n\n      let currentPos = 0\n      while (currentPos < textToWrap.length || (isFirstLineOfEntry && currentPos === 0 && textToWrap.length === 0)) {\n        const segment = textToWrap.substring(currentPos, currentPos + availableWidth)\n        const isFirstSegmentOfLine = currentPos === 0\n\n        displayLines.push({\n          text: isFirstSegmentOfLine && !isFirstLineOfEntry ? linePrefix + segment : segment,\n          level: level,\n          indent: !isFirstLineOfEntry || !isFirstSegmentOfLine,\n        })\n\n        currentPos += availableWidth\n        if (isFirstLineOfEntry && currentPos === 0 && textToWrap.length === 0) break\n      }\n    }\n\n    return displayLines\n  }\n\n  private _processCachedLogs(): void {\n    const logsToProcess = [...terminalConsoleCache.cachedLogs]\n    terminalConsoleCache.clearConsole()\n\n    this._allLogEntries.push(...logsToProcess)\n\n    if (this._allLogEntries.length > this.options.maxStoredLogs) {\n      this._allLogEntries.splice(0, this._allLogEntries.length - this.options.maxStoredLogs)\n    }\n\n    for (const logEntry of logsToProcess) {\n      const processed = this._processLogEntry(logEntry)\n      this._displayLines.push(...processed)\n    }\n\n    if (this._displayLines.length > this.options.maxDisplayLines) {\n      this._displayLines.splice(0, this._displayLines.length - this.options.maxDisplayLines)\n    }\n  }\n\n  private hasSelection(): boolean {\n    if (this._selectionStart === null || this._selectionEnd === null) return false\n\n    return this._selectionStart.line !== this._selectionEnd.line || this._selectionStart.col !== this._selectionEnd.col\n  }\n\n  private normalizeSelection(): ConsoleSelection | null {\n    if (!this._selectionStart || !this._selectionEnd) return null\n\n    const start = this._selectionStart\n    const end = this._selectionEnd\n\n    const startBeforeEnd = start.line < end.line || (start.line === end.line && start.col <= end.col)\n\n    if (startBeforeEnd) {\n      return {\n        startLine: start.line,\n        startCol: start.col,\n        endLine: end.line,\n        endCol: end.col,\n      }\n    } else {\n      return {\n        startLine: end.line,\n        startCol: end.col,\n        endLine: start.line,\n        endCol: start.col,\n      }\n    }\n  }\n\n  private getSelectedText(): string {\n    const selection = this.normalizeSelection()\n    if (!selection) return \"\"\n\n    const lines: string[] = []\n    for (let i = selection.startLine; i <= selection.endLine; i++) {\n      if (i < 0 || i >= this._displayLines.length) continue\n      const line = this._displayLines[i]\n      const linePrefix = line.indent ? \" \".repeat(INDENT_WIDTH) : \"\"\n      const textAvailableWidth = this.consoleWidth - 1 - (line.indent ? INDENT_WIDTH : 0)\n      const fullText = linePrefix + line.text.substring(0, textAvailableWidth)\n      let text = fullText\n\n      if (i === selection.startLine && i === selection.endLine) {\n        text = fullText.substring(selection.startCol, selection.endCol)\n      } else if (i === selection.startLine) {\n        text = fullText.substring(selection.startCol)\n      } else if (i === selection.endLine) {\n        text = fullText.substring(0, selection.endCol)\n      }\n\n      lines.push(text)\n    }\n\n    return lines.join(\"\\n\")\n  }\n\n  private clearSelection(): void {\n    this._selectionStart = null\n    this._selectionEnd = null\n    this._isDragging = false\n    this.stopAutoScroll()\n  }\n\n  private stopAutoScroll(): void {\n    if (this._autoScrollInterval !== null) {\n      this.clock.clearInterval(this._autoScrollInterval)\n      this._autoScrollInterval = null\n    }\n  }\n\n  private startAutoScroll(direction: \"up\" | \"down\"): void {\n    this.stopAutoScroll()\n    this._autoScrollInterval = this.clock.setInterval(() => {\n      if (direction === \"up\") {\n        if (this.scrollTopIndex > 0) {\n          this.scrollTopIndex--\n          this.isScrolledToBottom = false\n          if (this._selectionEnd) {\n            this._selectionEnd = {\n              line: this.scrollTopIndex,\n              col: this._selectionEnd.col,\n            }\n          }\n          this.markNeedsRerender()\n        } else {\n          this.stopAutoScroll()\n        }\n      } else {\n        const displayLineCount = this._displayLines.length\n        const logAreaHeight = Math.max(1, this.consoleHeight - 1)\n        const maxScrollTop = Math.max(0, displayLineCount - logAreaHeight)\n        if (this.scrollTopIndex < maxScrollTop) {\n          this.scrollTopIndex++\n          this.isScrolledToBottom = this.scrollTopIndex === maxScrollTop\n          if (this._selectionEnd) {\n            const maxLine = this.scrollTopIndex + logAreaHeight - 1\n            this._selectionEnd = {\n              line: Math.min(maxLine, displayLineCount - 1),\n              col: this._selectionEnd.col,\n            }\n          }\n          this.markNeedsRerender()\n        } else {\n          this.stopAutoScroll()\n        }\n      }\n    }, 50)\n  }\n\n  private triggerCopy(): void {\n    if (!this.hasSelection()) return\n    const text = this.getSelectedText()\n    if (text && this.options.onCopySelection) {\n      try {\n        this.options.onCopySelection(text)\n      } catch {}\n      this.clearSelection()\n      this.markNeedsRerender()\n    }\n  }\n\n  private getLineSelectionRange(lineIndex: number): { start: number; end: number } | null {\n    const selection = this.normalizeSelection()\n    if (!selection) return null\n\n    if (lineIndex < selection.startLine || lineIndex > selection.endLine) {\n      return null\n    }\n\n    const line = this._displayLines[lineIndex]\n    if (!line) return null\n\n    const linePrefix = line.indent ? \" \".repeat(INDENT_WIDTH) : \"\"\n    const textAvailableWidth = this.consoleWidth - 1 - (line.indent ? INDENT_WIDTH : 0)\n    const fullTextLength = linePrefix.length + Math.min(line.text.length, textAvailableWidth)\n\n    let start = 0\n    let end = fullTextLength\n\n    if (lineIndex === selection.startLine) {\n      start = Math.max(0, selection.startCol)\n    }\n    if (lineIndex === selection.endLine) {\n      end = Math.min(fullTextLength, selection.endCol)\n    }\n\n    if (start >= end) return null\n    return { start, end }\n  }\n\n  public handleMouse(event: MouseEvent): boolean {\n    if (!this.isVisible) return false\n\n    const localX = event.x - this.consoleX\n    const localY = event.y - this.consoleY\n\n    if (localX < 0 || localX >= this.consoleWidth || localY < 0 || localY >= this.consoleHeight) {\n      return false\n    }\n\n    if (event.type === \"scroll\" && event.scroll) {\n      if (event.scroll.direction === \"up\") {\n        this.scrollUp()\n      } else if (event.scroll.direction === \"down\") {\n        this.scrollDown()\n      }\n      return true\n    }\n\n    if (localY === 0) {\n      if (\n        event.type === \"down\" &&\n        event.button === 0 &&\n        localX >= this._copyButtonBounds.x &&\n        localX < this._copyButtonBounds.x + this._copyButtonBounds.width\n      ) {\n        this.triggerCopy()\n        return true\n      }\n      return true\n    }\n\n    const lineIndex = this.scrollTopIndex + (localY - 1)\n    const colIndex = Math.max(0, localX - 1)\n\n    if (event.type === \"down\" && event.button === 0) {\n      this.clearSelection()\n      this._selectionStart = { line: lineIndex, col: colIndex }\n      this._selectionEnd = { line: lineIndex, col: colIndex }\n      this._isDragging = true\n      this.markNeedsRerender()\n      return true\n    }\n\n    if (event.type === \"drag\" && this._isDragging) {\n      this._selectionEnd = { line: lineIndex, col: colIndex }\n\n      // Check if drag is at the edge and trigger auto-scroll\n      const logAreaHeight = Math.max(1, this.consoleHeight - 1)\n      const relativeY = localY - 1 // Subtract 1 for title bar\n\n      if (relativeY <= 0) {\n        // Dragging at top edge\n        this.startAutoScroll(\"up\")\n      } else if (relativeY >= logAreaHeight - 1) {\n        // Dragging at bottom edge\n        this.startAutoScroll(\"down\")\n      } else {\n        // Not at edge, stop auto-scrolling\n        this.stopAutoScroll()\n      }\n\n      this.markNeedsRerender()\n      return true\n    }\n\n    if (event.type === \"up\") {\n      if (this._isDragging) {\n        this._selectionEnd = { line: lineIndex, col: colIndex }\n        this._isDragging = false\n        this.stopAutoScroll()\n        this.markNeedsRerender()\n      }\n      return true\n    }\n\n    return true\n  }\n\n  public get visible(): boolean {\n    return this.isVisible\n  }\n\n  public get bounds(): { x: number; y: number; width: number; height: number } {\n    return {\n      x: this.consoleX,\n      y: this.consoleY,\n      width: this.consoleWidth,\n      height: this.consoleHeight,\n    }\n  }\n\n  private saveLogsToFile(): void {\n    try {\n      const timestamp = Date.now()\n      const filename = `_console_${timestamp}.log`\n      const filepath = path.join(process.cwd(), filename)\n\n      const allLogEntries = [...this._allLogEntries, ...terminalConsoleCache.cachedLogs]\n\n      const logLines: string[] = []\n\n      for (const [date, level, args, callerInfo] of allLogEntries) {\n        const timestampStr = this.formatTimestamp(date)\n        const callerSource = callerInfo ? `${callerInfo.fileName}:${callerInfo.lineNumber}` : \"unknown\"\n        const prefix = `[${timestampStr}] [${level}]` + (this._debugModeEnabled ? ` [${callerSource}]` : \"\") + \" \"\n        const formattedArgs = this.formatArguments(args)\n        logLines.push(prefix + formattedArgs)\n      }\n\n      const content = logLines.join(\"\\n\")\n      fs.writeFileSync(filepath, content, \"utf8\")\n\n      console.info(`Console logs saved to: ${filename}`)\n    } catch (error) {\n      console.error(`Failed to save console logs:`, error)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/edit-buffer.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { EditBuffer } from \"./edit-buffer.js\"\n\ndescribe(\"EditBuffer\", () => {\n  let buffer: EditBuffer\n\n  beforeEach(() => {\n    buffer = EditBuffer.create(\"wcwidth\")\n  })\n\n  afterEach(() => {\n    buffer.destroy()\n  })\n\n  describe(\"setText and getText\", () => {\n    it(\"should set and retrieve text content\", () => {\n      buffer.setText(\"Hello World\")\n      expect(buffer.getText()).toBe(\"Hello World\")\n    })\n\n    it(\"should handle empty text\", () => {\n      buffer.setText(\"\")\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should handle text with newlines\", () => {\n      const text = \"Line 1\\nLine 2\\nLine 3\"\n      buffer.setText(text)\n      expect(buffer.getText()).toBe(text)\n    })\n\n    it(\"should handle Unicode characters\", () => {\n      const text = \"Hello 世界 🌟\"\n      buffer.setText(text)\n      expect(buffer.getText()).toBe(text)\n    })\n  })\n\n  describe(\"cursor position\", () => {\n    it(\"should start cursor at beginning after setText\", () => {\n      buffer.setText(\"Hello World\")\n      const cursor = buffer.getCursorPosition()\n\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should track cursor position after movements\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.moveCursorRight()\n      let cursor = buffer.getCursorPosition()\n      expect(cursor.col).toBe(1)\n\n      buffer.moveCursorRight()\n      cursor = buffer.getCursorPosition()\n      expect(cursor.col).toBe(2)\n    })\n\n    it(\"should handle multi-line cursor positions\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.moveCursorDown()\n      let cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(1)\n\n      buffer.moveCursorDown()\n      cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(2)\n    })\n  })\n\n  describe(\"cursor movement\", () => {\n    it(\"should move cursor left and right\", () => {\n      buffer.setText(\"ABCDE\")\n\n      buffer.setCursorToLineCol(0, 5) // Move to end\n      expect(buffer.getCursorPosition().col).toBe(5)\n\n      buffer.moveCursorLeft()\n      expect(buffer.getCursorPosition().col).toBe(4)\n\n      buffer.moveCursorLeft()\n      expect(buffer.getCursorPosition().col).toBe(3)\n    })\n\n    it(\"should move cursor up and down\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.moveCursorDown()\n      expect(buffer.getCursorPosition().row).toBe(1)\n\n      buffer.moveCursorDown()\n      expect(buffer.getCursorPosition().row).toBe(2)\n\n      buffer.moveCursorUp()\n      expect(buffer.getCursorPosition().row).toBe(1)\n    })\n\n    it(\"should move to line start and end\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.setCursorToLineCol(0, 11) // Move to end\n      expect(buffer.getCursorPosition().col).toBe(11)\n\n      const cursor = buffer.getCursorPosition()\n      buffer.setCursor(cursor.row, 0)\n      expect(buffer.getCursorPosition().col).toBe(0)\n    })\n\n    it(\"should goto specific line\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.gotoLine(1)\n      expect(buffer.getCursorPosition().row).toBe(1)\n\n      buffer.gotoLine(2)\n      expect(buffer.getCursorPosition().row).toBe(2)\n    })\n\n    it(\"should handle Unicode grapheme movement correctly\", () => {\n      buffer.setText(\"A🌟B\")\n\n      expect(buffer.getCursorPosition().col).toBe(0)\n\n      buffer.moveCursorRight() // Move to emoji\n      expect(buffer.getCursorPosition().col).toBe(1)\n\n      buffer.moveCursorRight() // Move past emoji (2 cells wide)\n      expect(buffer.getCursorPosition().col).toBe(3)\n\n      buffer.moveCursorRight() // Move to B\n      expect(buffer.getCursorPosition().col).toBe(4)\n    })\n  })\n\n  describe(\"text insertion\", () => {\n    it(\"should insert single character\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.setCursorToLineCol(0, 11) // Move to end\n      buffer.insertChar(\"!\")\n\n      expect(buffer.getText()).toBe(\"Hello World!\")\n    })\n\n    it(\"should insert text at cursor\", () => {\n      buffer.setText(\"Hello\")\n\n      buffer.setCursorToLineCol(0, 5) // Move to end\n      buffer.insertText(\" World\")\n\n      expect(buffer.getText()).toBe(\"Hello World\")\n    })\n\n    it(\"should insert text in middle\", () => {\n      buffer.setText(\"HelloWorld\")\n\n      buffer.setCursorToLineCol(0, 5)\n      buffer.insertText(\" \")\n\n      expect(buffer.getText()).toBe(\"Hello World\")\n    })\n\n    it(\"should handle continuous typing (edit session)\", () => {\n      buffer.setText(\"\")\n\n      buffer.insertText(\"Hello\")\n      buffer.insertText(\" \")\n      buffer.insertText(\"World\")\n\n      expect(buffer.getText()).toBe(\"Hello World\")\n    })\n\n    it(\"should insert Unicode characters\", () => {\n      buffer.setText(\"Hello\")\n\n      buffer.setCursorToLineCol(0, 5) // Move to end\n      buffer.insertText(\" 世界 🌟\")\n\n      expect(buffer.getText()).toBe(\"Hello 世界 🌟\")\n    })\n\n    it(\"should handle newline insertion\", () => {\n      buffer.setText(\"HelloWorld\")\n\n      buffer.setCursorToLineCol(0, 5)\n      buffer.newLine()\n\n      expect(buffer.getText()).toBe(\"Hello\\nWorld\")\n    })\n  })\n\n  describe(\"text deletion\", () => {\n    it(\"should delete character at cursor\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.setCursorToLineCol(0, 6)\n      buffer.deleteChar()\n\n      expect(buffer.getText()).toBe(\"Hello orld\")\n    })\n\n    it(\"should delete character backward\", () => {\n      buffer.setText(\"\")\n\n      buffer.insertText(\"test\")\n      buffer.deleteCharBackward()\n\n      expect(buffer.getText()).toBe(\"tes\")\n    })\n\n    it(\"should delete range within a single line\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.deleteRange(0, 0, 0, 5)\n\n      expect(buffer.getText()).toBe(\" World\")\n    })\n\n    it(\"should delete range across multiple lines\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.deleteRange(0, 5, 2, 5)\n\n      expect(buffer.getText()).toBe(\"Line 3\")\n    })\n\n    it(\"should handle deleteRange with start equal to end (no-op)\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.deleteRange(0, 5, 0, 5)\n\n      expect(buffer.getText()).toBe(\"Hello World\")\n    })\n\n    it(\"should handle deleteRange with reversed start and end\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.deleteRange(0, 10, 0, 5)\n\n      expect(buffer.getText()).toBe(\"Hellod\")\n    })\n\n    it(\"should delete from middle of one line to middle of another\", () => {\n      buffer.setText(\"AAAA\\nBBBB\\nCCCC\")\n\n      buffer.deleteRange(0, 2, 2, 2)\n\n      expect(buffer.getText()).toBe(\"AACC\")\n    })\n\n    it(\"should delete entire content with deleteRange\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.deleteRange(0, 0, 0, 11)\n\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should handle deleteRange with Unicode characters\", () => {\n      buffer.setText(\"Hello 世界 🌟\")\n\n      buffer.deleteRange(0, 6, 0, 10)\n\n      expect(buffer.getText()).toBe(\"Hello  🌟\")\n    })\n\n    it(\"should delete entire line\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.gotoLine(1) // Go to Line 2\n      buffer.deleteLine()\n\n      expect(buffer.getText()).toBe(\"Line 1\\nLine 3\")\n    })\n\n    // TODO: Re-implement deleteToLineEnd as scripted method\n    it.skip(\"should delete to line end\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.setCursorToLineCol(0, 6)\n      // buffer.deleteToLineEnd()\n\n      expect(buffer.getText()).toBe(\"Hello \")\n    })\n\n    it(\"should handle backspace in active edit session\", () => {\n      buffer.setText(\"\")\n\n      buffer.insertText(\"test\")\n      buffer.deleteCharBackward()\n      buffer.deleteCharBackward()\n\n      expect(buffer.getText()).toBe(\"te\")\n    })\n  })\n\n  describe(\"complex editing scenarios\", () => {\n    it(\"should handle multiple edit operations in sequence\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.setCursorToLineCol(0, 11) // Move to end\n      buffer.insertText(\"!\")\n\n      buffer.setCursorToLineCol(0, 0) // Move to start\n      buffer.insertText(\">> \")\n\n      buffer.setCursorToLineCol(0, 99) // Move to end of line\n      buffer.newLine()\n      buffer.insertText(\"New line\")\n\n      expect(buffer.getText()).toBe(\">> Hello World!\\nNew line\")\n    })\n\n    it(\"should handle insert, delete, and cursor movement\", () => {\n      buffer.setText(\"AAAA\\nBBBB\\nCCCC\")\n\n      buffer.gotoLine(1)\n      buffer.setCursorToLineCol(1, 4) // Move to end of line 1\n      buffer.insertText(\"X\")\n\n      const text1 = buffer.getText()\n      expect(text1).toBe(\"AAAA\\nBBBBX\\nCCCC\")\n\n      // After insert, cursor is at end, deleteCharBackward will delete X\n      buffer.deleteCharBackward()\n\n      expect(buffer.getText()).toBe(\"AAAA\\nBBBB\\nCCCC\")\n    })\n\n    it(\"should handle line operations\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.gotoLine(1) // Go to Line 2\n      buffer.deleteLine()\n\n      // After deleting Line 2, we should have Line 1 and Line 3\n      const result = buffer.getText()\n      expect(result === \"Line 1\\nLine 3\" || result === \"Line 1\\nLine 3\\n\").toBe(true)\n    })\n  })\n\n  describe(\"setCursor methods\", () => {\n    it(\"should set cursor by line and byte offset\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.setCursor(0, 6)\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.col).toBe(6)\n    })\n\n    it(\"should set cursor by line and column\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.setCursorToLineCol(0, 5)\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.col).toBe(5)\n    })\n\n    it(\"should handle multi-line setCursorToLineCol\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.setCursorToLineCol(1, 3)\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(1)\n      expect(cursor.col).toBe(3)\n    })\n  })\n\n  describe(\"word boundary navigation\", () => {\n    it(\"should get next word boundary\", () => {\n      buffer.setText(\"hello world foo\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const nextBoundary = buffer.getNextWordBoundary()\n      expect(nextBoundary.col).toBeGreaterThan(0)\n    })\n\n    it(\"should get previous word boundary\", () => {\n      buffer.setText(\"hello world foo\")\n      buffer.setCursorToLineCol(0, 15)\n\n      const prevBoundary = buffer.getPrevWordBoundary()\n      expect(prevBoundary.col).toBeLessThan(15)\n    })\n\n    it(\"should handle word boundary at start\", () => {\n      buffer.setText(\"hello world\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const prevBoundary = buffer.getPrevWordBoundary()\n      expect(prevBoundary.row).toBe(0)\n      expect(prevBoundary.col).toBe(0)\n    })\n\n    it(\"should handle word boundary at end\", () => {\n      buffer.setText(\"hello world\")\n      buffer.setCursorToLineCol(0, 11)\n\n      const nextBoundary = buffer.getNextWordBoundary()\n      expect(nextBoundary.col).toBe(11)\n    })\n\n    it(\"should navigate across lines\", () => {\n      buffer.setText(\"hello\\nworld\")\n      buffer.setCursorToLineCol(0, 5)\n\n      const nextBoundary = buffer.getNextWordBoundary()\n      expect(nextBoundary.row).toBeGreaterThanOrEqual(0)\n    })\n\n    it(\"should handle punctuation boundaries\", () => {\n      buffer.setText(\"hello-world test\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const next1 = buffer.getNextWordBoundary()\n      expect(next1.col).toBeGreaterThan(0)\n    })\n\n    it(\"should handle word boundaries after CJK graphemes\", () => {\n      // \"你\" = 2 cols, \" \" = 1 col, \"好\" = 2 cols\n      buffer.setText(\"你 好\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const nextBoundary = buffer.getNextWordBoundary()\n      expect(nextBoundary.col).toBe(3)\n\n      buffer.setCursorToLineCol(0, 5)\n      const prevBoundary = buffer.getPrevWordBoundary()\n      expect(prevBoundary.col).toBe(3)\n    })\n\n    it(\"should handle word boundaries after emoji\", () => {\n      // \"🌟\" = 2 cols, \" \" = 1 col, \"ok\" = 2 cols\n      buffer.setText(\"🌟 ok\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const nextBoundary = buffer.getNextWordBoundary()\n      expect(nextBoundary.col).toBe(3)\n\n      buffer.setCursorToLineCol(0, 5)\n      const prevBoundary = buffer.getPrevWordBoundary()\n      expect(prevBoundary.col).toBe(3)\n    })\n\n    it(\"should handle word boundaries around tabs\", () => {\n      // tab = 2 cols\n      buffer.setText(\"Hello\\tWorld\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const nextBoundary = buffer.getNextWordBoundary()\n      expect(nextBoundary.col).toBe(7)\n\n      buffer.setCursorToLineCol(0, 12)\n      const prevBoundary = buffer.getPrevWordBoundary()\n      expect(prevBoundary.col).toBe(7)\n    })\n  })\n\n  describe(\"native coordinate conversion methods\", () => {\n    it(\"should convert offset to position\", () => {\n      buffer.setText(\"Hello\\nWorld\")\n\n      const pos0 = buffer.offsetToPosition(0)\n      expect(pos0).toEqual({ row: 0, col: 0 })\n\n      const pos5 = buffer.offsetToPosition(5)\n      expect(pos5).toEqual({ row: 0, col: 5 })\n\n      const pos6 = buffer.offsetToPosition(6)\n      expect(pos6).toEqual({ row: 1, col: 0 })\n\n      const pos11 = buffer.offsetToPosition(11)\n      expect(pos11).toEqual({ row: 1, col: 5 })\n    })\n\n    it(\"should convert position to offset\", () => {\n      buffer.setText(\"Hello\\nWorld\")\n\n      expect(buffer.positionToOffset(0, 0)).toBe(0)\n      expect(buffer.positionToOffset(0, 5)).toBe(5)\n      expect(buffer.positionToOffset(1, 0)).toBe(6)\n      expect(buffer.positionToOffset(1, 5)).toBe(11)\n    })\n\n    it(\"should get line start offset\", () => {\n      buffer.setText(\"Line1\\nLine2\\nLine3\")\n\n      expect(buffer.getLineStartOffset(0)).toBe(0)\n      expect(buffer.getLineStartOffset(1)).toBe(6)\n      expect(buffer.getLineStartOffset(2)).toBe(12)\n    })\n\n    it(\"should handle multiline text with varying lengths\", () => {\n      buffer.setText(\"AAA\\nBB\\nCCCC\")\n\n      expect(buffer.offsetToPosition(0)).toEqual({ row: 0, col: 0 })\n      expect(buffer.offsetToPosition(3)).toEqual({ row: 0, col: 3 })\n      expect(buffer.offsetToPosition(4)).toEqual({ row: 1, col: 0 })\n      expect(buffer.offsetToPosition(6)).toEqual({ row: 1, col: 2 })\n      expect(buffer.offsetToPosition(7)).toEqual({ row: 2, col: 0 })\n\n      expect(buffer.positionToOffset(0, 0)).toBe(0)\n      expect(buffer.positionToOffset(1, 0)).toBe(4)\n      expect(buffer.positionToOffset(2, 0)).toBe(7)\n    })\n\n    it(\"should return null for invalid offset\", () => {\n      buffer.setText(\"Hello\")\n      const result = buffer.offsetToPosition(1000)\n      expect(result).toBeNull()\n    })\n\n    it(\"should handle empty text\", () => {\n      buffer.setText(\"\")\n\n      const pos = buffer.offsetToPosition(0)\n      expect(pos).toEqual({ row: 0, col: 0 })\n\n      expect(buffer.positionToOffset(0, 0)).toBe(0)\n      expect(buffer.getLineStartOffset(0)).toBe(0)\n    })\n  })\n\n  describe(\"getEOL navigation\", () => {\n    it(\"should get end of line from start\", () => {\n      buffer.setText(\"Hello World\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const eol = buffer.getEOL()\n      expect(eol.row).toBe(0)\n      expect(eol.col).toBe(11)\n    })\n\n    it(\"should get end of line from middle\", () => {\n      buffer.setText(\"Hello World\")\n      buffer.setCursorToLineCol(0, 5)\n\n      const eol = buffer.getEOL()\n      expect(eol.row).toBe(0)\n      expect(eol.col).toBe(11)\n    })\n\n    it(\"should stay at end of line when already there\", () => {\n      buffer.setText(\"Hello\")\n      buffer.setCursorToLineCol(0, 5)\n\n      const eol = buffer.getEOL()\n      expect(eol.row).toBe(0)\n      expect(eol.col).toBe(5)\n    })\n\n    it(\"should handle multi-line text\", () => {\n      buffer.setText(\"Hello\\nWorld\\nTest\")\n      buffer.setCursorToLineCol(1, 0)\n\n      const eol = buffer.getEOL()\n      expect(eol.row).toBe(1)\n      expect(eol.col).toBe(5)\n    })\n\n    it(\"should handle empty lines\", () => {\n      buffer.setText(\"Hello\\n\\nWorld\")\n      buffer.setCursorToLineCol(1, 0)\n\n      const eol = buffer.getEOL()\n      expect(eol.row).toBe(1)\n      expect(eol.col).toBe(0)\n    })\n\n    it(\"should work on different lines\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.setCursorToLineCol(0, 0)\n      const eol0 = buffer.getEOL()\n      expect(eol0.row).toBe(0)\n      expect(eol0.col).toBe(6)\n\n      buffer.setCursorToLineCol(1, 0)\n      const eol1 = buffer.getEOL()\n      expect(eol1.row).toBe(1)\n      expect(eol1.col).toBe(6)\n\n      buffer.setCursorToLineCol(2, 0)\n      const eol2 = buffer.getEOL()\n      expect(eol2.row).toBe(2)\n      expect(eol2.col).toBe(6)\n    })\n  })\n\n  describe(\"error handling\", () => {\n    it(\"should throw error when using destroyed buffer\", () => {\n      buffer.setText(\"Test\")\n      buffer.destroy()\n\n      expect(() => buffer.getText()).toThrow(\"EditBuffer is destroyed\")\n      expect(() => buffer.insertText(\"x\")).toThrow(\"EditBuffer is destroyed\")\n      expect(() => buffer.moveCursorLeft()).toThrow(\"EditBuffer is destroyed\")\n    })\n  })\n\n  describe(\"line boundary operations\", () => {\n    it(\"should merge lines when backspacing at BOL\", () => {\n      buffer.setText(\"Line 1\\nLine 2\")\n      buffer.setCursorToLineCol(1, 0) // Start of line 2\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"Line 1Line 2\")\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(6)\n    })\n\n    it(\"should merge lines when deleting at EOL\", () => {\n      buffer.setText(\"Line 1\\nLine 2\")\n      buffer.setCursorToLineCol(0, 6) // End of line 1\n      buffer.deleteChar()\n      expect(buffer.getText()).toBe(\"Line 1Line 2\")\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(6)\n    })\n\n    it(\"should handle newline insertion at BOL\", () => {\n      buffer.setText(\"Hello\")\n      buffer.setCursorToLineCol(0, 0)\n      buffer.newLine()\n      expect(buffer.getText()).toBe(\"\\nHello\")\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(1)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should handle newline insertion at EOL\", () => {\n      buffer.setText(\"Hello\")\n      buffer.setCursorToLineCol(0, 5)\n      buffer.newLine()\n      expect(buffer.getText()).toBe(\"Hello\\n\")\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(1)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should handle CRLF in text\", () => {\n      // CRLF is detected as a line break during setText\n      buffer.setText(\"Line 1\\r\\nLine 2\")\n      // Both CR and LF are detected, so we get the text back\n      const text = buffer.getText()\n      // Verify we have two lines\n      buffer.setCursorToLineCol(1, 0)\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"Line 1Line 2\")\n    })\n\n    it(\"should handle multiple consecutive newlines\", () => {\n      buffer.setText(\"A\\n\\n\\nB\")\n      buffer.setCursorToLineCol(1, 0) // Empty line\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"A\\n\\nB\")\n    })\n  })\n\n  describe(\"wide character handling\", () => {\n    it(\"should handle tabs correctly in edits\", () => {\n      buffer.setText(\"A\\tB\")\n      // Tab has a display width of 2 columns (by default, rounded to multiple of 2)\n      // So \"A\\tB\" has positions: A at col 0-1, tab at col 1-2, B at col 2\n      // To insert after A, we use column 1\n      buffer.setCursorToLineCol(0, 1) // After A, at the tab position\n      // But since setCursorToLineCol might snap to grapheme boundaries,\n      // let's just verify the text remains intact when inserting at byte level\n      buffer.insertText(\"X\")\n      // The insert should happen at the cursor position\n      const text = buffer.getText()\n      // Either AX\\tB or A\\tXB depending on how cursor snaps\n      expect(text.includes(\"A\") && text.includes(\"B\") && text.includes(\"\\t\") && text.includes(\"X\")).toBe(true)\n    })\n\n    it(\"should handle CJK characters correctly\", () => {\n      buffer.setText(\"世界\")\n      buffer.setCursorToLineCol(0, 2) // After first character (2 columns wide)\n      buffer.insertText(\"X\")\n      expect(buffer.getText()).toBe(\"世X界\")\n    })\n\n    it(\"should handle emoji correctly\", () => {\n      buffer.setText(\"🌟\")\n      buffer.setCursorToLineCol(0, 0)\n      buffer.moveCursorRight()\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.col).toBe(2) // Emoji is 2 columns wide\n    })\n\n    it(\"should handle mixed width text correctly\", () => {\n      buffer.setText(\"A世🌟B\")\n      buffer.setCursorToLineCol(0, 1) // After A\n      buffer.moveCursorRight()\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.col).toBe(3) // A(1) + 世(2)\n    })\n  })\n\n  describe(\"multi-line insertion\", () => {\n    it(\"should insert multi-line text correctly\", () => {\n      buffer.setText(\"Start\")\n      buffer.setCursorToLineCol(0, 5)\n      buffer.insertText(\"\\nMiddle\\nEnd\")\n      expect(buffer.getText()).toBe(\"Start\\nMiddle\\nEnd\")\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(2)\n      expect(cursor.col).toBe(3)\n    })\n\n    it(\"should insert multi-line text in middle\", () => {\n      buffer.setText(\"StartEnd\")\n      buffer.setCursorToLineCol(0, 5)\n      buffer.insertText(\"\\nMiddle\\n\")\n      expect(buffer.getText()).toBe(\"Start\\nMiddle\\nEnd\")\n    })\n\n    it(\"should handle inserting text with various line endings\", () => {\n      buffer.setText(\"\")\n      buffer.insertText(\"Line 1\\nLine 2\\rLine 3\\r\\nLine 4\")\n      const text = buffer.getText()\n      // Line breaks are preserved in the buffer\n      // Just verify we have 4 lines\n      const lines = text.split(/\\r?\\n|\\r/)\n      expect(lines.length).toBe(4)\n      expect(lines[0]).toBe(\"Line 1\")\n      expect(lines[3]).toBe(\"Line 4\")\n    })\n  })\n})\n\ndescribe(\"EditBuffer Placeholder\", () => {\n  let buffer: EditBuffer\n\n  beforeEach(() => {\n    buffer = EditBuffer.create(\"wcwidth\")\n  })\n\n  afterEach(() => {\n    buffer.destroy()\n  })\n})\n\ndescribe(\"EditBuffer Events\", () => {\n  describe(\"events\", () => {\n    it(\"should emit cursor-changed event when cursor moves\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"cursor-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello World\")\n      testBuffer.moveCursorRight()\n\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(eventCount).toBeGreaterThan(1) // setText + moveCursorRight\n      testBuffer.destroy()\n    })\n\n    it(\"should emit cursor-changed event on setCursor\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"cursor-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello World\")\n      testBuffer.setCursorToLineCol(0, 5)\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(eventCount).toBeGreaterThan(1) // setText + setCursor\n      testBuffer.destroy()\n    })\n\n    it(\"should emit cursor-changed event on text insertion\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"cursor-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello\")\n      testBuffer.insertText(\" World\")\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(eventCount).toBeGreaterThan(1) // setText + insertText\n      testBuffer.destroy()\n    })\n\n    it(\"should emit cursor-changed event on deletion\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"cursor-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello World\")\n      const beforeDelete = eventCount\n      testBuffer.setCursorToLineCol(0, 5)\n      testBuffer.deleteChar()\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(eventCount).toBeGreaterThan(beforeDelete + 1) // setCursor + deleteChar\n      testBuffer.destroy()\n    })\n\n    it(\"should emit cursor-changed event on undo/redo\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"cursor-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Test\")\n      testBuffer.insertText(\" Hello\")\n\n      if (testBuffer.canUndo()) {\n        const beforeUndo = eventCount\n        testBuffer.undo()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(eventCount).toBeGreaterThan(beforeUndo)\n      }\n\n      if (testBuffer.canRedo()) {\n        const beforeRedo = eventCount\n        testBuffer.redo()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(eventCount).toBeGreaterThan(beforeRedo)\n      }\n\n      testBuffer.destroy()\n    })\n\n    it(\"should handle multiple event listeners\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let count1 = 0\n      let count2 = 0\n\n      testBuffer.on(\"cursor-changed\", () => {\n        count1++\n      })\n      testBuffer.on(\"cursor-changed\", () => {\n        count2++\n      })\n\n      testBuffer.setText(\"Hello\")\n      testBuffer.moveCursorRight()\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(count1).toBeGreaterThan(1)\n      expect(count2).toBeGreaterThan(1)\n      expect(count1).toBe(count2)\n\n      testBuffer.destroy()\n    })\n\n    it(\"should support removing event listeners\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n      testBuffer.setText(\"Hello\")\n\n      let eventCount = 0\n      const listener = () => {\n        eventCount++\n      }\n\n      testBuffer.on(\"cursor-changed\", listener)\n      testBuffer.moveCursorRight()\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      const firstCount = eventCount\n\n      testBuffer.off(\"cursor-changed\", listener)\n      testBuffer.moveCursorRight()\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      // Count should not have increased after removing listener\n      expect(eventCount).toBe(firstCount)\n\n      testBuffer.destroy()\n    })\n\n    it(\"should isolate events between different buffer instances\", async () => {\n      const testBuffer1 = EditBuffer.create(\"wcwidth\")\n      const testBuffer2 = EditBuffer.create(\"wcwidth\")\n\n      let count1 = 0\n      let count2 = 0\n\n      testBuffer1.on(\"cursor-changed\", () => {\n        count1++\n      })\n      testBuffer2.on(\"cursor-changed\", () => {\n        count2++\n      })\n\n      testBuffer1.setText(\"Buffer 1\")\n      await Bun.sleep(10)\n      const count1AfterSetText = count1\n      testBuffer1.moveCursorRight()\n      await Bun.sleep(10)\n\n      expect(count1).toBeGreaterThan(count1AfterSetText)\n      expect(count2).toBe(0)\n\n      testBuffer2.setText(\"Buffer 2\")\n      await Bun.sleep(10)\n      const count2AfterSetText = count2\n      testBuffer2.moveCursorRight()\n      await Bun.sleep(10)\n\n      expect(count1).toBe(count1AfterSetText + 1)\n      expect(count2).toBeGreaterThan(count2AfterSetText)\n\n      testBuffer1.destroy()\n      testBuffer2.destroy()\n    })\n\n    it(\"should not emit events after destroy\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"cursor-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello\")\n      testBuffer.moveCursorRight()\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      const countBeforeDestroy = eventCount\n\n      testBuffer.destroy()\n\n      // Trying to move cursor on destroyed buffer should throw\n      // So we can't test event emission, but we can verify the instance is removed from registry\n      expect(countBeforeDestroy).toBeGreaterThan(1) // setText + moveCursorRight\n    })\n  })\n\n  describe(\"content-changed events\", () => {\n    it(\"should emit content-changed event on setText\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello World\")\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(0)\n      testBuffer.destroy()\n    })\n\n    it(\"should emit content-changed event on insertText\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello\")\n      await Bun.sleep(10)\n      const countAfterSetText = eventCount\n\n      testBuffer.insertText(\" World\")\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(countAfterSetText)\n      testBuffer.destroy()\n    })\n\n    it(\"should emit content-changed event on deleteChar\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello World\")\n      await Bun.sleep(10)\n      const countAfterSetText = eventCount\n\n      testBuffer.setCursorToLineCol(0, 5)\n      testBuffer.deleteChar()\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(countAfterSetText)\n      testBuffer.destroy()\n    })\n\n    it(\"should emit content-changed event on deleteCharBackward\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello\")\n      await Bun.sleep(10)\n      const countAfterSetText = eventCount\n\n      testBuffer.setCursorToLineCol(0, 5)\n      testBuffer.deleteCharBackward()\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(countAfterSetText)\n      testBuffer.destroy()\n    })\n\n    it(\"should emit content-changed event on deleteLine\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      await Bun.sleep(10)\n      const countAfterSetText = eventCount\n\n      testBuffer.gotoLine(1)\n      testBuffer.deleteLine()\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(countAfterSetText)\n      testBuffer.destroy()\n    })\n\n    it(\"should emit content-changed event on newLine\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello\")\n      await Bun.sleep(10)\n      const countAfterSetText = eventCount\n\n      testBuffer.setCursorToLineCol(0, 5)\n      testBuffer.newLine()\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(countAfterSetText)\n      testBuffer.destroy()\n    })\n\n    it(\"should handle multiple content-changed listeners\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let count1 = 0\n      let count2 = 0\n\n      testBuffer.on(\"content-changed\", () => {\n        count1++\n      })\n      testBuffer.on(\"content-changed\", () => {\n        count2++\n      })\n\n      testBuffer.setText(\"Hello\")\n      await Bun.sleep(10)\n\n      expect(count1).toBeGreaterThan(0)\n      expect(count2).toBeGreaterThan(0)\n      expect(count1).toBe(count2)\n\n      testBuffer.destroy()\n    })\n\n    it(\"should support removing content-changed listeners\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n      testBuffer.setText(\"Hello\")\n      await Bun.sleep(10)\n\n      let eventCount = 0\n      const listener = () => {\n        eventCount++\n      }\n\n      testBuffer.on(\"content-changed\", listener)\n      testBuffer.insertText(\" World\")\n      await Bun.sleep(10)\n\n      const firstCount = eventCount\n\n      testBuffer.off(\"content-changed\", listener)\n      testBuffer.insertText(\"!\")\n      await Bun.sleep(10)\n\n      // Count should not have increased after removing listener\n      expect(eventCount).toBe(firstCount)\n\n      testBuffer.destroy()\n    })\n\n    it(\"should isolate content-changed events between different buffer instances\", async () => {\n      const testBuffer1 = EditBuffer.create(\"wcwidth\")\n      const testBuffer2 = EditBuffer.create(\"wcwidth\")\n\n      let count1 = 0\n      let count2 = 0\n\n      testBuffer1.on(\"content-changed\", () => {\n        count1++\n      })\n      testBuffer2.on(\"content-changed\", () => {\n        count2++\n      })\n\n      testBuffer1.setText(\"Buffer 1\")\n      await Bun.sleep(10)\n      const count1AfterSetText = count1\n\n      testBuffer1.insertText(\" updated\")\n      await Bun.sleep(10)\n\n      expect(count1).toBeGreaterThan(count1AfterSetText)\n      expect(count2).toBe(0)\n\n      testBuffer2.setText(\"Buffer 2\")\n      await Bun.sleep(10)\n      const count2AfterSetText = count2\n\n      testBuffer2.insertText(\" updated\")\n      await Bun.sleep(10)\n\n      expect(count1).toBe(count1AfterSetText + 1)\n      expect(count2).toBeGreaterThan(count2AfterSetText)\n\n      testBuffer1.destroy()\n      testBuffer2.destroy()\n    })\n\n    it(\"should not emit content-changed after destroy\", async () => {\n      const testBuffer = EditBuffer.create(\"wcwidth\")\n\n      let eventCount = 0\n      testBuffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      testBuffer.setText(\"Hello\")\n      await Bun.sleep(10)\n\n      const countBeforeDestroy = eventCount\n\n      testBuffer.destroy()\n\n      // Trying to modify destroyed buffer should throw\n      expect(countBeforeDestroy).toBeGreaterThan(0)\n    })\n  })\n})\n\ndescribe(\"EditBuffer History Management\", () => {\n  let buffer: EditBuffer\n\n  beforeEach(() => {\n    buffer = EditBuffer.create(\"wcwidth\")\n  })\n\n  afterEach(() => {\n    buffer.destroy()\n  })\n\n  describe(\"replaceText with history\", () => {\n    it(\"should create undo history when using replaceText\", () => {\n      buffer.replaceText(\"Initial text\")\n      expect(buffer.canUndo()).toBe(true)\n    })\n\n    it(\"should allow undo after replaceText\", () => {\n      buffer.replaceText(\"First text\")\n      expect(buffer.getText()).toBe(\"First text\")\n\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should allow redo after undo of replaceText\", () => {\n      buffer.replaceText(\"First text\")\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"\")\n\n      buffer.redo()\n      expect(buffer.getText()).toBe(\"First text\")\n    })\n\n    it(\"should maintain history across multiple replaceText calls\", () => {\n      buffer.replaceText(\"Text 1\")\n      buffer.replaceText(\"Text 2\")\n      buffer.replaceText(\"Text 3\")\n\n      expect(buffer.getText()).toBe(\"Text 3\")\n      expect(buffer.canUndo()).toBe(true)\n\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"Text 2\")\n\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"Text 1\")\n\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"\")\n    })\n  })\n\n  describe(\"replaceTextOwned with history\", () => {\n    it(\"should create undo history when using replaceTextOwned\", () => {\n      buffer.replaceTextOwned(\"Initial text\")\n      expect(buffer.canUndo()).toBe(true)\n    })\n\n    it(\"should allow undo after replaceTextOwned\", () => {\n      buffer.replaceTextOwned(\"First text\")\n      expect(buffer.getText()).toBe(\"First text\")\n\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should allow redo after undo of replaceTextOwned\", () => {\n      buffer.replaceTextOwned(\"First text\")\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"\")\n\n      buffer.redo()\n      expect(buffer.getText()).toBe(\"First text\")\n    })\n\n    it(\"should work correctly with Unicode text\", () => {\n      buffer.replaceTextOwned(\"Hello 世界 🌟\")\n      expect(buffer.getText()).toBe(\"Hello 世界 🌟\")\n      expect(buffer.canUndo()).toBe(true)\n\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"\")\n    })\n  })\n\n  describe(\"setTextOwned without history\", () => {\n    it(\"should not create undo history when using setTextOwned\", () => {\n      buffer.setTextOwned(\"Initial text\")\n      expect(buffer.canUndo()).toBe(false)\n    })\n\n    it(\"should work correctly with Unicode text\", () => {\n      buffer.setTextOwned(\"Hello 世界 🌟\")\n      expect(buffer.getText()).toBe(\"Hello 世界 🌟\")\n      expect(buffer.canUndo()).toBe(false)\n    })\n  })\n\n  describe(\"setText without history\", () => {\n    it(\"should not create undo history when using setText\", () => {\n      buffer.setText(\"Initial text\")\n      expect(buffer.canUndo()).toBe(false)\n    })\n\n    it(\"should set text content correctly\", () => {\n      buffer.setText(\"Test content\")\n      expect(buffer.getText()).toBe(\"Test content\")\n    })\n\n    it(\"should clear existing history\", () => {\n      buffer.replaceText(\"First text\")\n      expect(buffer.canUndo()).toBe(true)\n\n      buffer.setText(\"Second text\")\n      expect(buffer.getText()).toBe(\"Second text\")\n      // setText clears all history\n      expect(buffer.canUndo()).toBe(false)\n    })\n\n    it(\"should work with multi-line text\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      expect(buffer.getText()).toBe(\"Line 1\\nLine 2\\nLine 3\")\n      expect(buffer.canUndo()).toBe(false)\n    })\n\n    it(\"should work with Unicode text\", () => {\n      buffer.setText(\"Unicode 世界 🌟\")\n      expect(buffer.getText()).toBe(\"Unicode 世界 🌟\")\n      expect(buffer.canUndo()).toBe(false)\n    })\n\n    it(\"should work with empty text\", () => {\n      buffer.replaceText(\"Some text\")\n      buffer.setText(\"\")\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should reuse single memory slot on repeated calls\", () => {\n      // This tests the memory efficiency - each call should replace the previous\n      buffer.setText(\"Text 1\")\n      expect(buffer.getText()).toBe(\"Text 1\")\n\n      buffer.setText(\"Text 2\")\n      expect(buffer.getText()).toBe(\"Text 2\")\n\n      buffer.setText(\"Text 3\")\n      expect(buffer.getText()).toBe(\"Text 3\")\n\n      // Should not have created any history\n      expect(buffer.canUndo()).toBe(false)\n    })\n  })\n\n  describe(\"mixed operations\", () => {\n    it(\"should handle replaceText followed by insertText with full undo\", () => {\n      buffer.replaceText(\"Hello\")\n      // replaceText places cursor at (0,0), so move to end\n      buffer.setCursorToLineCol(0, 5) // Move to end\n      buffer.insertText(\" World\")\n      expect(buffer.getText()).toBe(\"Hello World\")\n\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"Hello\")\n\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should handle replaceText followed by insertText\", () => {\n      buffer.replaceText(\"Hello\")\n      // replaceText places cursor at (0,0)\n      buffer.setCursorToLineCol(0, 5) // Move to end\n      buffer.insertText(\" World\")\n      expect(buffer.getText()).toBe(\"Hello World\")\n\n      // Can undo the insertText\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"Hello\")\n\n      // Can undo replaceText since it preserved history\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should handle setText followed by insertText\", () => {\n      buffer.setText(\"Hello\")\n      // setText places cursor at (0,0)\n      buffer.setCursorToLineCol(0, 5) // Move to end\n      buffer.insertText(\" World\")\n      expect(buffer.getText()).toBe(\"Hello World\")\n\n      // Can undo the insertText\n      buffer.undo()\n      expect(buffer.getText()).toBe(\"Hello\")\n\n      // Cannot undo setText since it cleared history\n      expect(buffer.canUndo()).toBe(false)\n    })\n\n    it(\"should handle replaceText and setText together\", () => {\n      buffer.replaceText(\"Text 1\")\n      buffer.setText(\"Text 2\")\n      expect(buffer.getText()).toBe(\"Text 2\")\n\n      // Cannot undo because setText cleared history\n      expect(buffer.canUndo()).toBe(false)\n    })\n\n    it(\"should allow clearing history after replaceText\", () => {\n      buffer.replaceText(\"Text 1\")\n      buffer.replaceText(\"Text 2\")\n      expect(buffer.canUndo()).toBe(true)\n\n      buffer.clearHistory()\n      expect(buffer.canUndo()).toBe(false)\n      expect(buffer.getText()).toBe(\"Text 2\")\n    })\n  })\n\n  describe(\"events with different methods\", () => {\n    it(\"should emit content-changed for setText\", async () => {\n      let eventCount = 0\n      buffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      buffer.setText(\"Hello\")\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(0)\n    })\n\n    it(\"should emit content-changed for replaceText\", async () => {\n      let eventCount = 0\n      buffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      buffer.replaceText(\"Hello\")\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(0)\n    })\n\n    it(\"should emit content-changed for setTextOwned\", async () => {\n      let eventCount = 0\n      buffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      buffer.setTextOwned(\"Hello\")\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(0)\n    })\n  })\n})\n\ndescribe(\"EditBuffer Clear Method\", () => {\n  let buffer: EditBuffer\n\n  beforeEach(() => {\n    buffer = EditBuffer.create(\"wcwidth\")\n  })\n\n  afterEach(() => {\n    buffer.destroy()\n  })\n\n  describe(\"basic clear functionality\", () => {\n    it(\"should clear text content\", () => {\n      buffer.setText(\"Hello World\")\n      expect(buffer.getText()).toBe(\"Hello World\")\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should reset cursor to 0,0\", () => {\n      buffer.setText(\"Hello World\")\n      buffer.setCursorToLineCol(0, 5)\n      expect(buffer.getCursorPosition().col).toBe(5)\n\n      buffer.clear()\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n      expect(cursor.offset).toBe(0)\n    })\n\n    it(\"should clear multi-line text\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      expect(buffer.getText()).toBe(\"Line 1\\nLine 2\\nLine 3\")\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should clear Unicode text\", () => {\n      buffer.setText(\"Hello 世界 🌟\")\n      expect(buffer.getText()).toBe(\"Hello 世界 🌟\")\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should handle clearing already empty buffer\", () => {\n      buffer.setText(\"\")\n      expect(buffer.getText()).toBe(\"\")\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should handle clearing after multiple edits\", () => {\n      buffer.setText(\"Hello\")\n      buffer.setCursorToLineCol(0, 5) // Move to end\n      buffer.insertText(\" World\")\n      buffer.insertText(\"!\")\n      expect(buffer.getText()).toBe(\"Hello World!\")\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n    })\n  })\n\n  describe(\"clear with cursor positions\", () => {\n    it(\"should reset cursor from end of text\", () => {\n      buffer.setText(\"Hello World\")\n      buffer.setCursorToLineCol(0, 11) // End of text\n\n      buffer.clear()\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should reset cursor from middle of multi-line text\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      buffer.setCursorToLineCol(1, 3) // Middle of line 2\n\n      buffer.clear()\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should reset cursor from last line\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      buffer.gotoLine(2) // Last line\n\n      buffer.clear()\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n  })\n\n  describe(\"clear without placeholder\", () => {\n    it(\"should handle clear without placeholder\", () => {\n      buffer.setText(\"Hello World\")\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n    })\n  })\n\n  describe(\"clear with events\", () => {\n    it(\"should emit content-changed event on clear\", async () => {\n      let eventCount = 0\n      buffer.on(\"content-changed\", () => {\n        eventCount++\n      })\n\n      buffer.setText(\"Hello World\")\n      await Bun.sleep(10)\n      const countAfterSetText = eventCount\n\n      buffer.clear()\n      await Bun.sleep(10)\n\n      expect(eventCount).toBeGreaterThan(countAfterSetText)\n    })\n\n    it(\"should emit cursor-changed event on clear\", async () => {\n      let eventCount = 0\n      buffer.on(\"cursor-changed\", () => {\n        eventCount++\n      })\n\n      buffer.setText(\"Hello World\")\n      buffer.setCursorToLineCol(0, 5)\n      await Bun.sleep(10)\n      const countBeforeClear = eventCount\n\n      buffer.clear()\n      await Bun.sleep(10)\n\n      // Should emit cursor-changed when resetting cursor to 0,0\n      expect(eventCount).toBeGreaterThan(countBeforeClear)\n    })\n\n    it(\"should emit both events on clear\", async () => {\n      let contentChangedCount = 0\n      let cursorChangedCount = 0\n\n      buffer.on(\"content-changed\", () => {\n        contentChangedCount++\n      })\n      buffer.on(\"cursor-changed\", () => {\n        cursorChangedCount++\n      })\n\n      buffer.setText(\"Hello World\")\n      buffer.setCursorToLineCol(0, 5)\n      await Bun.sleep(10)\n\n      const contentCountBefore = contentChangedCount\n      const cursorCountBefore = cursorChangedCount\n\n      buffer.clear()\n      await Bun.sleep(10)\n\n      expect(contentChangedCount).toBeGreaterThan(contentCountBefore)\n      expect(cursorChangedCount).toBeGreaterThan(cursorCountBefore)\n    })\n  })\n\n  describe(\"clear and subsequent operations\", () => {\n    it(\"should allow inserting text after clear\", () => {\n      buffer.setText(\"Hello\")\n      buffer.clear()\n\n      buffer.insertText(\"World\")\n      expect(buffer.getText()).toBe(\"World\")\n    })\n\n    it(\"should allow setText after clear\", () => {\n      buffer.setText(\"Hello\")\n      buffer.clear()\n\n      buffer.setText(\"New Text\")\n      expect(buffer.getText()).toBe(\"New Text\")\n    })\n\n    it(\"should maintain correct cursor after clear and insert\", () => {\n      buffer.setText(\"Hello World\")\n      buffer.clear()\n\n      buffer.insertText(\"Test\")\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(4)\n    })\n\n    it(\"should allow multiple clear operations\", () => {\n      buffer.setText(\"Text 1\")\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n\n      buffer.setText(\"Text 2\")\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n\n      buffer.setText(\"Text 3\")\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n    })\n  })\n\n  describe(\"clear with complex scenarios\", () => {\n    it(\"should clear after edit session\", () => {\n      buffer.setText(\"Hello\")\n      buffer.setCursorToLineCol(0, 5) // Move to end\n      buffer.insertText(\" World\")\n      buffer.insertText(\"!\")\n      buffer.setCursorToLineCol(0, 0) // Move to start\n      buffer.insertText(\">> \")\n\n      expect(buffer.getText()).toBe(\">> Hello World!\")\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should clear after line operations\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      buffer.gotoLine(1)\n      buffer.deleteLine()\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should clear after range deletion\", () => {\n      buffer.setText(\"Hello World Test\")\n      buffer.deleteRange(0, 6, 0, 11)\n      expect(buffer.getText()).toBe(\"Hello  Test\")\n\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n    })\n\n    it(\"should handle clear with wide characters\", () => {\n      buffer.setText(\"A世🌟B\")\n      buffer.clear()\n      expect(buffer.getText()).toBe(\"\")\n\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n  })\n\n  describe(\"error handling\", () => {\n    it(\"should throw error when clearing destroyed buffer\", () => {\n      buffer.setText(\"Test\")\n      buffer.destroy()\n\n      expect(() => buffer.clear()).toThrow(\"EditBuffer is destroyed\")\n    })\n  })\n\n  describe(\"Regression Tests\", () => {\n    it(\"should handle moving left in a long line (potential BoundedArray overflow)\", () => {\n      const longText = \"a\".repeat(500)\n      buffer.setText(longText)\n\n      buffer.setCursorToLineCol(0, 500)\n      buffer.moveCursorLeft()\n\n      const cursor = buffer.getCursorPosition()\n      expect(cursor.col).toBe(499)\n    })\n  })\n})\n\ndescribe(\"EditBuffer Memory Registry Limits\", () => {\n  let buffer: EditBuffer\n\n  beforeEach(() => {\n    buffer = EditBuffer.create(\"wcwidth\")\n  })\n\n  afterEach(() => {\n    buffer.destroy()\n  })\n\n  describe(\"Memory buffer management\", () => {\n    it(\"should handle many setText calls without exceeding limit\", () => {\n      for (let i = 0; i < 300; i++) {\n        buffer.setText(`Text ${i}`)\n      }\n\n      expect(buffer.getText()).toBe(\"Text 299\")\n    })\n\n    it(\"should handle 1000 setText calls without memory registry errors\", () => {\n      for (let i = 0; i < 1000; i++) {\n        buffer.setText(`Text ${i}`)\n      }\n\n      expect(buffer.getText()).toBe(\"Text 999\")\n      expect(buffer.canUndo()).toBe(false)\n    })\n\n    it(\"should handle limited replaceText calls before hitting buffer limit\", () => {\n      for (let i = 0; i < 200; i++) {\n        buffer.replaceText(`Text ${i}`)\n      }\n\n      expect(buffer.getText()).toBe(\"Text 199\")\n    })\n\n    it(\"should handle mixed replaceText and setText calls\", () => {\n      for (let i = 0; i < 100; i++) {\n        buffer.replaceText(`With history ${i}`)\n      }\n\n      for (let i = 0; i < 300; i++) {\n        buffer.setText(`Without history ${i}`)\n      }\n\n      expect(buffer.getText()).toBe(\"Without history 299\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/edit-buffer.ts",
    "content": "import { resolveRenderLib, type LogicalCursor, type RenderLib } from \"./zig.js\"\nimport { type Pointer } from \"bun:ffi\"\nimport { type WidthMethod, type Highlight } from \"./types.js\"\nimport { RGBA } from \"./lib/RGBA.js\"\nimport { EventEmitter } from \"events\"\nimport type { SyntaxStyle } from \"./syntax-style.js\"\n\nexport type { LogicalCursor }\n\n/**\n * EditBuffer provides a text editing buffer with cursor management,\n * incremental editing, and grapheme-aware operations.\n */\nexport class EditBuffer extends EventEmitter {\n  private static registry = new Map<number, EditBuffer>()\n  private static nativeEventsSubscribed = false\n\n  private lib: RenderLib\n  private bufferPtr: Pointer\n  private textBufferPtr: Pointer\n  public readonly id: number\n  private _destroyed: boolean = false\n  private _textBytes: Uint8Array[] = []\n  private _singleTextBytes: Uint8Array | null = null\n  private _singleTextMemId: number | null = null\n  private _syntaxStyle?: SyntaxStyle\n\n  constructor(lib: RenderLib, ptr: Pointer) {\n    super()\n    this.lib = lib\n    this.bufferPtr = ptr\n    this.textBufferPtr = lib.editBufferGetTextBuffer(ptr)\n    this.id = lib.editBufferGetId(ptr)\n\n    EditBuffer.registry.set(this.id, this)\n    EditBuffer.subscribeToNativeEvents(lib)\n  }\n\n  static create(widthMethod: WidthMethod): EditBuffer {\n    const lib = resolveRenderLib()\n    const ptr = lib.createEditBuffer(widthMethod)\n    return new EditBuffer(lib, ptr)\n  }\n\n  private static subscribeToNativeEvents(lib: RenderLib): void {\n    if (EditBuffer.nativeEventsSubscribed) return\n    EditBuffer.nativeEventsSubscribed = true\n\n    lib.onAnyNativeEvent((name: string, data: ArrayBuffer) => {\n      const buffer = new Uint16Array(data)\n\n      if (name.startsWith(\"eb_\") && buffer.length >= 1) {\n        const id = buffer[0]\n        const instance = EditBuffer.registry.get(id)\n\n        if (instance) {\n          // Strip the \"eb_\" prefix and forward the event\n          const eventName = name.slice(3)\n          const eventData = data.slice(2)\n          instance.emit(eventName, eventData)\n        }\n      }\n    })\n  }\n\n  private guard(): void {\n    if (this._destroyed) throw new Error(\"EditBuffer is destroyed\")\n  }\n\n  public get ptr(): Pointer {\n    this.guard()\n    return this.bufferPtr\n  }\n\n  /**\n   * Set text and completely reset the buffer state (clears history, resets add_buffer).\n   * Use this for initial text setting or when you want a clean slate.\n   */\n  public setText(text: string): void {\n    this.guard()\n    const textBytes = this.lib.encoder.encode(text)\n\n    if (this._singleTextMemId !== null) {\n      this.lib.textBufferReplaceMemBuffer(this.textBufferPtr, this._singleTextMemId, textBytes, false)\n    } else {\n      this._singleTextMemId = this.lib.textBufferRegisterMemBuffer(this.textBufferPtr, textBytes, false)\n    }\n    this._singleTextBytes = textBytes\n    this.lib.editBufferSetTextFromMem(this.bufferPtr, this._singleTextMemId)\n  }\n\n  /**\n   * Set text using owned memory and completely reset the buffer state (clears history, resets add_buffer).\n   * The native code takes ownership of the memory.\n   */\n  public setTextOwned(text: string): void {\n    this.guard()\n    const textBytes = this.lib.encoder.encode(text)\n    this.lib.editBufferSetText(this.bufferPtr, textBytes)\n  }\n\n  /**\n   * Replace text while preserving undo history (creates an undo point).\n   * Use this when you want the setText operation to be undoable.\n   */\n  public replaceText(text: string): void {\n    this.guard()\n    const textBytes = this.lib.encoder.encode(text)\n    this._textBytes.push(textBytes)\n    const memId = this.lib.textBufferRegisterMemBuffer(this.textBufferPtr, textBytes, false)\n    this.lib.editBufferReplaceTextFromMem(this.bufferPtr, memId)\n  }\n\n  /**\n   * Replace text using owned memory while preserving undo history (creates an undo point).\n   * The native code takes ownership of the memory.\n   */\n  public replaceTextOwned(text: string): void {\n    this.guard()\n    const textBytes = this.lib.encoder.encode(text)\n    this.lib.editBufferReplaceText(this.bufferPtr, textBytes)\n  }\n\n  public getLineCount(): number {\n    this.guard()\n    return this.lib.textBufferGetLineCount(this.textBufferPtr)\n  }\n\n  public getText(): string {\n    this.guard()\n    // TODO: Use byte size of text buffer to get the actual size of the text\n    // actually native can stack alloc all the text and decode will alloc as js string then\n    const maxSize = 1024 * 1024 // 1MB max\n    const textBytes = this.lib.editBufferGetText(this.bufferPtr, maxSize)\n\n    if (!textBytes) return \"\"\n\n    return this.lib.decoder.decode(textBytes)\n  }\n\n  public insertChar(char: string): void {\n    this.guard()\n    this.lib.editBufferInsertChar(this.bufferPtr, char)\n  }\n\n  public insertText(text: string): void {\n    this.guard()\n    this.lib.editBufferInsertText(this.bufferPtr, text)\n  }\n\n  public deleteChar(): void {\n    this.guard()\n    this.lib.editBufferDeleteChar(this.bufferPtr)\n  }\n\n  public deleteCharBackward(): void {\n    this.guard()\n    this.lib.editBufferDeleteCharBackward(this.bufferPtr)\n  }\n\n  public deleteRange(startLine: number, startCol: number, endLine: number, endCol: number): void {\n    this.guard()\n    this.lib.editBufferDeleteRange(this.bufferPtr, startLine, startCol, endLine, endCol)\n  }\n\n  public newLine(): void {\n    this.guard()\n    this.lib.editBufferNewLine(this.bufferPtr)\n  }\n\n  public deleteLine(): void {\n    this.guard()\n    this.lib.editBufferDeleteLine(this.bufferPtr)\n  }\n\n  public moveCursorLeft(): void {\n    this.guard()\n    this.lib.editBufferMoveCursorLeft(this.bufferPtr)\n  }\n\n  public moveCursorRight(): void {\n    this.guard()\n    this.lib.editBufferMoveCursorRight(this.bufferPtr)\n  }\n\n  public moveCursorUp(): void {\n    this.guard()\n    this.lib.editBufferMoveCursorUp(this.bufferPtr)\n  }\n\n  public moveCursorDown(): void {\n    this.guard()\n    this.lib.editBufferMoveCursorDown(this.bufferPtr)\n  }\n\n  public gotoLine(line: number): void {\n    this.guard()\n    this.lib.editBufferGotoLine(this.bufferPtr, line)\n  }\n\n  public setCursor(line: number, col: number): void {\n    this.guard()\n    this.lib.editBufferSetCursor(this.bufferPtr, line, col)\n  }\n\n  public setCursorToLineCol(line: number, col: number): void {\n    this.guard()\n    this.lib.editBufferSetCursorToLineCol(this.bufferPtr, line, col)\n  }\n\n  public setCursorByOffset(offset: number): void {\n    this.guard()\n    this.lib.editBufferSetCursorByOffset(this.bufferPtr, offset)\n  }\n\n  public getCursorPosition(): LogicalCursor {\n    this.guard()\n    return this.lib.editBufferGetCursorPosition(this.bufferPtr)\n  }\n\n  public getNextWordBoundary(): LogicalCursor {\n    this.guard()\n    const boundary = this.lib.editBufferGetNextWordBoundary(this.bufferPtr)\n    return {\n      row: boundary.row,\n      col: boundary.col,\n      offset: boundary.offset,\n    }\n  }\n\n  public getPrevWordBoundary(): LogicalCursor {\n    this.guard()\n    const boundary = this.lib.editBufferGetPrevWordBoundary(this.bufferPtr)\n    return {\n      row: boundary.row,\n      col: boundary.col,\n      offset: boundary.offset,\n    }\n  }\n\n  public getEOL(): LogicalCursor {\n    this.guard()\n    const boundary = this.lib.editBufferGetEOL(this.bufferPtr)\n    return {\n      row: boundary.row,\n      col: boundary.col,\n      offset: boundary.offset,\n    }\n  }\n\n  public offsetToPosition(offset: number): { row: number; col: number } | null {\n    this.guard()\n    const result = this.lib.editBufferOffsetToPosition(this.bufferPtr, offset)\n    if (!result) return null\n    return { row: result.row, col: result.col }\n  }\n\n  public positionToOffset(row: number, col: number): number {\n    this.guard()\n    return this.lib.editBufferPositionToOffset(this.bufferPtr, row, col)\n  }\n\n  public getLineStartOffset(row: number): number {\n    this.guard()\n    return this.lib.editBufferGetLineStartOffset(this.bufferPtr, row)\n  }\n\n  public getTextRange(startOffset: number, endOffset: number): string {\n    this.guard()\n    if (startOffset >= endOffset) return \"\"\n\n    // TODO: Use actual expected size of the text\n    // like other methods native can just return a pointer and size\n    // and we immediately decode the text into a js string then the native stack\n    // can go out of scope\n    const maxSize = 1024 * 1024 // 1MB max\n    const textBytes = this.lib.editBufferGetTextRange(this.bufferPtr, startOffset, endOffset, maxSize)\n\n    if (!textBytes) return \"\"\n\n    return this.lib.decoder.decode(textBytes)\n  }\n\n  public getTextRangeByCoords(startRow: number, startCol: number, endRow: number, endCol: number): string {\n    this.guard()\n\n    const maxSize = 1024 * 1024 // 1MB max\n    const textBytes = this.lib.editBufferGetTextRangeByCoords(\n      this.bufferPtr,\n      startRow,\n      startCol,\n      endRow,\n      endCol,\n      maxSize,\n    )\n\n    if (!textBytes) return \"\"\n\n    return this.lib.decoder.decode(textBytes)\n  }\n\n  public debugLogRope(): void {\n    this.guard()\n    this.lib.editBufferDebugLogRope(this.bufferPtr)\n  }\n\n  public undo(): string | null {\n    this.guard()\n    const maxSize = 256\n    const metaBytes = this.lib.editBufferUndo(this.bufferPtr, maxSize)\n    if (!metaBytes) return null\n    return this.lib.decoder.decode(metaBytes)\n  }\n\n  public redo(): string | null {\n    this.guard()\n    const maxSize = 256\n    const metaBytes = this.lib.editBufferRedo(this.bufferPtr, maxSize)\n    if (!metaBytes) return null\n    return this.lib.decoder.decode(metaBytes)\n  }\n\n  public canUndo(): boolean {\n    this.guard()\n    return this.lib.editBufferCanUndo(this.bufferPtr)\n  }\n\n  public canRedo(): boolean {\n    this.guard()\n    return this.lib.editBufferCanRedo(this.bufferPtr)\n  }\n\n  public clearHistory(): void {\n    this.guard()\n    this.lib.editBufferClearHistory(this.bufferPtr)\n  }\n\n  public setDefaultFg(fg: RGBA | null): void {\n    this.guard()\n    this.lib.textBufferSetDefaultFg(this.textBufferPtr, fg)\n  }\n\n  public setDefaultBg(bg: RGBA | null): void {\n    this.guard()\n    this.lib.textBufferSetDefaultBg(this.textBufferPtr, bg)\n  }\n\n  public setDefaultAttributes(attributes: number | null): void {\n    this.guard()\n    this.lib.textBufferSetDefaultAttributes(this.textBufferPtr, attributes)\n  }\n\n  public resetDefaults(): void {\n    this.guard()\n    this.lib.textBufferResetDefaults(this.textBufferPtr)\n  }\n\n  public setSyntaxStyle(style: SyntaxStyle | null): void {\n    this.guard()\n    this._syntaxStyle = style ?? undefined\n    this.lib.textBufferSetSyntaxStyle(this.textBufferPtr, style?.ptr ?? null)\n  }\n\n  public getSyntaxStyle(): SyntaxStyle | null {\n    this.guard()\n    return this._syntaxStyle ?? null\n  }\n\n  public addHighlight(lineIdx: number, highlight: Highlight): void {\n    this.guard()\n    this.lib.textBufferAddHighlight(this.textBufferPtr, lineIdx, highlight)\n  }\n\n  public addHighlightByCharRange(highlight: Highlight): void {\n    this.guard()\n    this.lib.textBufferAddHighlightByCharRange(this.textBufferPtr, highlight)\n  }\n\n  public removeHighlightsByRef(hlRef: number): void {\n    this.guard()\n    this.lib.textBufferRemoveHighlightsByRef(this.textBufferPtr, hlRef)\n  }\n\n  public clearLineHighlights(lineIdx: number): void {\n    this.guard()\n    this.lib.textBufferClearLineHighlights(this.textBufferPtr, lineIdx)\n  }\n\n  public clearAllHighlights(): void {\n    this.guard()\n    this.lib.textBufferClearAllHighlights(this.textBufferPtr)\n  }\n\n  public getLineHighlights(lineIdx: number): Array<Highlight> {\n    this.guard()\n    return this.lib.textBufferGetLineHighlights(this.textBufferPtr, lineIdx)\n  }\n\n  public clear(): void {\n    this.guard()\n    this.lib.editBufferClear(this.bufferPtr)\n  }\n\n  public destroy(): void {\n    if (this._destroyed) return\n\n    this._destroyed = true\n    EditBuffer.registry.delete(this.id)\n    this.lib.destroyEditBuffer(this.bufferPtr)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/editor-view.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { EditBuffer } from \"./edit-buffer.js\"\nimport { EditorView } from \"./editor-view.js\"\nimport { RGBA } from \"./lib/RGBA.js\"\n\ndescribe(\"EditorView\", () => {\n  let buffer: EditBuffer\n  let view: EditorView\n\n  beforeEach(() => {\n    buffer = EditBuffer.create(\"wcwidth\")\n    view = EditorView.create(buffer, 40, 10)\n  })\n\n  afterEach(() => {\n    view.destroy()\n    buffer.destroy()\n  })\n\n  describe(\"initialization\", () => {\n    it(\"should create view with specified viewport dimensions\", () => {\n      const viewport = view.getViewport()\n      expect(viewport.width).toBe(40)\n      expect(viewport.height).toBe(10)\n      expect(viewport.offsetY).toBe(0)\n      expect(viewport.offsetX).toBe(0)\n    })\n\n    it(\"should start with wrap mode set to none\", () => {\n      expect(view.getVirtualLineCount()).toBeGreaterThanOrEqual(0)\n    })\n  })\n\n  describe(\"viewport management\", () => {\n    it(\"should update viewport size\", () => {\n      view.setViewportSize(80, 20)\n      const viewport = view.getViewport()\n      expect(viewport.width).toBe(80)\n      expect(viewport.height).toBe(20)\n    })\n\n    it(\"should set scroll margin\", () => {\n      view.setScrollMargin(0.2)\n      expect(true).toBe(true)\n    })\n\n    it(\"should return correct virtual line count for simple text\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      expect(view.getVirtualLineCount()).toBe(3)\n    })\n  })\n\n  describe(\"text wrapping\", () => {\n    it(\"should enable and disable wrapping via wrap mode\", () => {\n      buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRST\")\n\n      expect(view.getVirtualLineCount()).toBe(1)\n\n      view.setWrapMode(\"char\")\n      expect(view.getVirtualLineCount()).toBeGreaterThan(1)\n\n      view.setWrapMode(\"none\")\n      expect(view.getVirtualLineCount()).toBe(1)\n    })\n\n    it(\"should wrap at viewport width\", () => {\n      buffer.setText(\"ABCDEFGHIJKLMNOPQRST\")\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(10, 10)\n\n      expect(view.getVirtualLineCount()).toBe(2)\n\n      view.setViewportSize(5, 10)\n      expect(view.getVirtualLineCount()).toBe(4)\n\n      view.setViewportSize(20, 10)\n      expect(view.getVirtualLineCount()).toBe(1)\n    })\n\n    it(\"should change wrap mode\", () => {\n      buffer.setText(\"Hello wonderful world\")\n\n      view.setViewportSize(10, 10)\n\n      view.setWrapMode(\"char\")\n      const charCount = view.getVirtualLineCount()\n      expect(charCount).toBeGreaterThanOrEqual(2)\n\n      view.setWrapMode(\"word\")\n      const wordCount = view.getVirtualLineCount()\n      expect(wordCount).toBeGreaterThanOrEqual(2)\n\n      view.setWrapMode(\"none\")\n      const noneCount = view.getVirtualLineCount()\n      expect(noneCount).toBe(1)\n    })\n\n    it(\"should preserve newlines when wrapping\", () => {\n      buffer.setText(\"Short\\nAnother short line\\nLast\")\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(50, 10)\n\n      expect(view.getVirtualLineCount()).toBe(3)\n    })\n\n    it(\"should wrap long lines with wrapping enabled\", () => {\n      const longLine = \"This is a very long line that will definitely wrap when the viewport is narrow\"\n      buffer.setText(longLine)\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(20, 10)\n\n      const vlineCount = view.getVirtualLineCount()\n      expect(vlineCount).toBeGreaterThan(1)\n    })\n  })\n\n  describe(\"integration with EditBuffer\", () => {\n    it(\"should reflect edits made to EditBuffer\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      expect(view.getVirtualLineCount()).toBe(3)\n\n      buffer.gotoLine(9999)\n      buffer.newLine()\n      buffer.insertText(\"Line 4\")\n\n      expect(view.getVirtualLineCount()).toBe(4)\n    })\n\n    it(\"should update after text deletion\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n      expect(view.getVirtualLineCount()).toBe(3)\n\n      buffer.gotoLine(1)\n      buffer.deleteLine()\n\n      expect(view.getVirtualLineCount()).toBe(2)\n    })\n  })\n\n  describe(\"viewport with wrapping and editing\", () => {\n    it(\"should maintain wrapping after edits\", () => {\n      buffer.setText(\"Short line\")\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(20, 10)\n\n      expect(view.getVirtualLineCount()).toBe(1)\n\n      buffer.gotoLine(9999)\n      buffer.insertText(\" that becomes very long and should wrap now\")\n\n      expect(view.getVirtualLineCount()).toBeGreaterThan(1)\n    })\n\n    it(\"should handle viewport resize with wrapped content\", () => {\n      const longText = \"This is a very long line that will wrap when the viewport is narrow\"\n      buffer.setText(longText)\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(20, 10)\n\n      const count20 = view.getVirtualLineCount()\n      expect(count20).toBeGreaterThan(1)\n\n      view.setViewportSize(40, 10)\n      const count40 = view.getVirtualLineCount()\n      expect(count40).toBeLessThan(count20)\n    })\n  })\n\n  describe(\"selection\", () => {\n    it(\"should set and reset selection\", () => {\n      buffer.setText(\"Hello World\")\n\n      view.setSelection(0, 5)\n      expect(view.hasSelection()).toBe(true)\n\n      view.resetSelection()\n      expect(view.hasSelection()).toBe(false)\n    })\n\n    it(\"should set selection with colors\", () => {\n      buffer.setText(\"Hello World\")\n\n      const bgColor = RGBA.fromValues(0, 0, 1, 0.3)\n      const fgColor = RGBA.fromValues(1, 1, 1, 1)\n\n      view.setSelection(0, 5, bgColor, fgColor)\n      expect(view.hasSelection()).toBe(true)\n\n      const selection = view.getSelection()\n      expect(selection).toEqual({ start: 0, end: 5 })\n    })\n\n    it(\"should update selection end position\", () => {\n      buffer.setText(\"Hello World\")\n\n      view.setSelection(0, 5)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n\n      view.updateSelection(11)\n      expect(view.getSelectedText()).toBe(\"Hello World\")\n\n      const selection = view.getSelection()\n      expect(selection).toEqual({ start: 0, end: 11 })\n    })\n\n    it(\"should shrink selection with updateSelection\", () => {\n      buffer.setText(\"Hello World\")\n\n      view.setSelection(0, 11)\n      expect(view.getSelectedText()).toBe(\"Hello World\")\n\n      view.updateSelection(5)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n    })\n\n    it(\"should update local selection focus position\", () => {\n      buffer.setText(\"Hello World\")\n\n      const changed1 = view.setLocalSelection(0, 0, 5, 0)\n      expect(changed1).toBe(true)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n\n      const changed2 = view.updateLocalSelection(0, 0, 11, 0)\n      expect(changed2).toBe(true)\n      expect(view.getSelectedText()).toBe(\"Hello World\")\n    })\n\n    it(\"should update local selection across lines\", () => {\n      buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n      view.setLocalSelection(2, 0, 2, 0)\n\n      const changed = view.updateLocalSelection(2, 0, 4, 1)\n      expect(changed).toBe(true)\n\n      const selectedText = view.getSelectedText()\n      expect(selectedText).toContain(\"ne 1\")\n      expect(selectedText).toContain(\"Line\")\n    })\n\n    it(\"should fallback to setLocalSelection when updateLocalSelection called with no existing anchor\", () => {\n      buffer.setText(\"Hello World\")\n\n      const changed = view.updateLocalSelection(0, 0, 5, 0)\n      expect(changed).toBe(true)\n      expect(view.hasSelection()).toBe(true)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n    })\n\n    it(\"should preserve anchor when updating local selection\", () => {\n      buffer.setText(\"Hello World\")\n\n      view.setLocalSelection(0, 0, 5, 0)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n\n      view.updateLocalSelection(0, 0, 11, 0)\n      expect(view.getSelectedText()).toBe(\"Hello World\")\n\n      view.updateLocalSelection(0, 0, 3, 0)\n      expect(view.getSelectedText()).toBe(\"Hel\")\n    })\n\n    it(\"should handle backward selection with updateLocalSelection\", () => {\n      buffer.setText(\"Hello World\")\n\n      view.setLocalSelection(11, 0, 11, 0)\n\n      const changed = view.updateLocalSelection(11, 0, 6, 0)\n      expect(changed).toBe(true)\n      expect(view.getSelectedText()).toBe(\"World\")\n    })\n\n    it(\"should handle wrapped lines with updateLocalSelection\", () => {\n      buffer.setText(\"ABCDEFGHIJKLMNOPQRST\")\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(10, 10)\n\n      view.setLocalSelection(0, 0, 0, 0)\n\n      const changed = view.updateLocalSelection(0, 0, 5, 1)\n      expect(changed).toBe(true)\n      expect(view.getSelectedText()).toBe(\"ABCDEFGHIJKLMNO\")\n    })\n  })\n\n  describe(\"word boundary navigation\", () => {\n    it(\"should get next word boundary with visual cursor\", () => {\n      buffer.setText(\"hello world foo\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const nextBoundary = view.getNextWordBoundary()\n      expect(nextBoundary).toBeDefined()\n      expect(nextBoundary.visualCol).toBeGreaterThan(0)\n    })\n\n    it(\"should get previous word boundary with visual cursor\", () => {\n      buffer.setText(\"hello world foo\")\n      buffer.setCursorToLineCol(0, 15)\n\n      const prevBoundary = view.getPrevWordBoundary()\n      expect(prevBoundary).toBeDefined()\n      expect(prevBoundary.visualCol).toBeLessThan(15)\n    })\n\n    it(\"should handle word boundary at start\", () => {\n      buffer.setText(\"hello world\")\n      buffer.setCursorToLineCol(0, 0)\n\n      const prevBoundary = view.getPrevWordBoundary()\n      expect(prevBoundary.logicalRow).toBe(0)\n      expect(prevBoundary.visualCol).toBe(0)\n    })\n\n    it(\"should handle word boundary at end\", () => {\n      buffer.setText(\"hello world\")\n      buffer.setCursorToLineCol(0, 11)\n\n      const nextBoundary = view.getNextWordBoundary()\n      expect(nextBoundary.visualCol).toBe(11)\n    })\n\n    it(\"should navigate across lines with visual coordinates\", () => {\n      buffer.setText(\"hello\\nworld\")\n      buffer.setCursorToLineCol(0, 5)\n\n      const nextBoundary = view.getNextWordBoundary()\n      expect(nextBoundary.logicalRow).toBeGreaterThanOrEqual(0)\n    })\n\n    it(\"should handle wrapping when getting word boundaries\", () => {\n      buffer.setText(\"hello world test foo bar\")\n      view.setWrapMode(\"word\")\n      view.setViewportSize(10, 10)\n\n      buffer.setCursorToLineCol(0, 0)\n      const nextBoundary = view.getNextWordBoundary()\n\n      expect(nextBoundary).toBeDefined()\n      expect(nextBoundary.visualRow).toBeGreaterThanOrEqual(0)\n      expect(nextBoundary.logicalRow).toBeGreaterThanOrEqual(0)\n    })\n  })\n\n  describe(\"large content\", () => {\n    it(\"should handle many lines\", () => {\n      const lines = Array.from({ length: 100 }, (_, i) => `Line ${i}`).join(\"\\n\")\n      buffer.setText(lines)\n\n      expect(view.getTotalVirtualLineCount()).toBe(100)\n    })\n\n    it(\"should handle very long single line with wrapping\", () => {\n      const longLine = \"A\".repeat(1000)\n      buffer.setText(longLine)\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(80, 24)\n\n      const vlineCount = view.getVirtualLineCount()\n      expect(vlineCount).toBeGreaterThan(10)\n    })\n  })\n\n  describe(\"viewport slicing\", () => {\n    it(\"should show subset of content in viewport\", () => {\n      const lines = Array.from({ length: 20 }, (_, i) => `Line ${i}`).join(\"\\n\")\n      buffer.setText(lines)\n\n      const smallView = EditorView.create(buffer, 40, 5)\n\n      expect(smallView.getTotalVirtualLineCount()).toBe(20)\n\n      smallView.destroy()\n    })\n  })\n\n  describe(\"error handling\", () => {\n    it(\"should throw error when using destroyed view\", () => {\n      view.destroy()\n\n      expect(() => view.getVirtualLineCount()).toThrow(\"EditorView is destroyed\")\n      expect(() => view.setViewportSize(80, 24)).toThrow(\"EditorView is destroyed\")\n      expect(() => view.setWrapMode(\"char\")).toThrow(\"EditorView is destroyed\")\n    })\n  })\n\n  describe(\"Unicode edge cases\", () => {\n    it(\"should handle emoji with wrapping\", () => {\n      buffer.setText(\"🌟\".repeat(20))\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(10, 10)\n\n      expect(view.getVirtualLineCount()).toBeGreaterThan(1)\n    })\n\n    it(\"should handle CJK characters with wrapping\", () => {\n      buffer.setText(\"测试文字处理功能\")\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(10, 10)\n\n      const vlineCount = view.getVirtualLineCount()\n      expect(vlineCount).toBeGreaterThanOrEqual(1)\n    })\n\n    it(\"should handle mixed ASCII and wide characters\", () => {\n      buffer.setText(\"AB测试CD文字EF\")\n\n      view.setWrapMode(\"char\")\n      view.setViewportSize(8, 10)\n\n      expect(view.getVirtualLineCount()).toBeGreaterThanOrEqual(1)\n    })\n\n    it(\"should navigate visual cursor correctly through emoji and CJK\", () => {\n      buffer.setText(\"(emoji 🌟 and CJK 世界)\")\n\n      let cursor = view.getVisualCursor()\n      expect(cursor.visualRow).toBe(0)\n      expect(cursor.visualCol).toBe(0)\n      expect(cursor.offset).toBe(0)\n\n      for (let i = 0; i < 6; i++) {\n        buffer.moveCursorRight()\n      }\n      cursor = view.getVisualCursor()\n      expect(cursor.offset).toBe(6)\n\n      buffer.moveCursorRight()\n      cursor = view.getVisualCursor()\n      expect(cursor.offset).toBe(7)\n\n      buffer.moveCursorRight()\n      cursor = view.getVisualCursor()\n      expect(cursor.offset).toBe(9)\n\n      buffer.moveCursorLeft()\n      cursor = view.getVisualCursor()\n      expect(cursor.offset).toBe(7)\n\n      buffer.moveCursorLeft()\n      cursor = view.getVisualCursor()\n      expect(cursor.offset).toBe(6)\n    })\n\n    it(\"should handle vertical navigation through emoji cells correctly\", () => {\n      buffer.setText(\"1234567890123456789\\n(emoji 🌟 and CJK 世界)\\n1234567890123456789\")\n\n      buffer.setCursorToLineCol(0, 7)\n      let cursor = view.getVisualCursor()\n      expect(cursor.visualRow).toBe(0)\n      expect(cursor.visualCol).toBe(7)\n\n      view.moveDownVisual()\n      cursor = view.getVisualCursor()\n      expect(cursor.visualRow).toBe(1)\n      expect(cursor.visualCol).toBe(7)\n\n      buffer.moveCursorRight()\n      cursor = view.getVisualCursor()\n      expect(cursor.visualCol).toBe(9)\n\n      view.moveUpVisual()\n      cursor = view.getVisualCursor()\n      expect(cursor.visualRow).toBe(0)\n      expect(cursor.visualCol).toBe(9)\n\n      buffer.moveCursorLeft()\n      cursor = view.getVisualCursor()\n      expect(cursor.visualCol).toBe(8)\n\n      view.moveDownVisual()\n      cursor = view.getVisualCursor()\n      expect(cursor.visualRow).toBe(1)\n      expect(cursor.visualCol).toBe(8)\n\n      buffer.moveCursorLeft()\n      cursor = view.getVisualCursor()\n      expect(cursor.visualCol).toBe(6)\n    })\n  })\n\n  describe(\"cursor movement around multi-cell graphemes\", () => {\n    // These tests verify that the cursor correctly handles multi-cell graphemes like emojis (🌟)\n    // and CJK characters (世界). Multi-cell graphemes occupy 2 visual columns but are treated\n    // as a single logical unit for cursor movement and deletion.\n    //\n    // Key behaviors:\n    // - moveCursorRight/Left skips over entire graphemes (no intermediate positions)\n    // - deleteCharBackward deletes the entire grapheme, not individual cells\n    // - Visual column positions reflect the actual display width (2 cells per wide grapheme)\n    // - Logical column positions mark grapheme boundaries (skipping intermediate cell positions)\n\n    it(\"should understand logical vs visual cursor positions\", () => {\n      buffer.setText(\"a🌟b\")\n\n      buffer.setCursorToLineCol(0, 0)\n      expect(view.getVisualCursor().visualCol).toBe(0)\n\n      buffer.setCursorToLineCol(0, 1)\n      expect(view.getVisualCursor().visualCol).toBe(1)\n\n      buffer.setCursorToLineCol(0, 3)\n      expect(view.getVisualCursor().visualCol).toBe(3)\n\n      buffer.setCursorToLineCol(0, 4)\n      expect(view.getVisualCursor().visualCol).toBe(4)\n\n      buffer.setCursorToLineCol(0, 0)\n      buffer.moveCursorRight()\n      expect(buffer.getCursorPosition().col).toBe(1)\n\n      buffer.moveCursorRight()\n      expect(buffer.getCursorPosition().col).toBe(3)\n      expect(view.getVisualCursor().visualCol).toBe(3)\n\n      buffer.moveCursorRight()\n      expect(buffer.getCursorPosition().col).toBe(4)\n    })\n\n    it(\"should move cursor correctly around emoji (🌟) with visual positions\", () => {\n      buffer.setText(\"a🌟b\")\n\n      buffer.setCursorToLineCol(0, 1)\n      let visualCursor = view.getVisualCursor()\n      expect(visualCursor.visualCol).toBe(1)\n\n      buffer.moveCursorRight()\n      visualCursor = view.getVisualCursor()\n      expect(visualCursor.visualCol).toBe(3)\n\n      buffer.moveCursorRight()\n      visualCursor = view.getVisualCursor()\n      expect(visualCursor.visualCol).toBe(4)\n\n      buffer.moveCursorLeft()\n      visualCursor = view.getVisualCursor()\n      expect(visualCursor.visualCol).toBe(3)\n\n      buffer.moveCursorLeft()\n      visualCursor = view.getVisualCursor()\n      expect(visualCursor.visualCol).toBe(1)\n    })\n\n    it(\"should move cursor correctly around CJK characters (世界) with visual positions\", () => {\n      buffer.setText(\"a世界b\")\n\n      buffer.setCursorToLineCol(0, 0)\n      expect(view.getVisualCursor().visualCol).toBe(0)\n\n      buffer.moveCursorRight()\n      expect(view.getVisualCursor().visualCol).toBe(1)\n\n      buffer.moveCursorRight()\n      expect(view.getVisualCursor().visualCol).toBe(3)\n\n      buffer.moveCursorRight()\n      expect(view.getVisualCursor().visualCol).toBe(5)\n\n      buffer.moveCursorRight()\n      expect(view.getVisualCursor().visualCol).toBe(6)\n\n      buffer.moveCursorLeft()\n      expect(view.getVisualCursor().visualCol).toBe(5)\n\n      buffer.moveCursorLeft()\n      expect(view.getVisualCursor().visualCol).toBe(3)\n\n      buffer.moveCursorLeft()\n      expect(view.getVisualCursor().visualCol).toBe(1)\n    })\n\n    it(\"should handle backspace correctly after emoji\", () => {\n      buffer.setText(\"a🌟b\")\n\n      buffer.setCursorToLineCol(0, 3)\n      expect(view.getVisualCursor().visualCol).toBe(3)\n\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"ab\")\n      expect(view.getVisualCursor().visualCol).toBe(1)\n    })\n\n    it(\"should handle backspace correctly after CJK character\", () => {\n      buffer.setText(\"世界\")\n\n      buffer.setCursorToLineCol(0, 4)\n      expect(view.getVisualCursor().visualCol).toBe(4)\n\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"世\")\n      expect(view.getVisualCursor().visualCol).toBe(2)\n\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"\")\n      expect(view.getVisualCursor().visualCol).toBe(0)\n    })\n\n    it(\"should treat multi-cell graphemes as single units for cursor movement\", () => {\n      buffer.setText(\"🌟世界🎉\")\n\n      buffer.setCursorToLineCol(0, 0)\n      expect(view.getVisualCursor().visualCol).toBe(0)\n\n      buffer.moveCursorRight()\n      expect(view.getVisualCursor().visualCol).toBe(2)\n\n      buffer.moveCursorRight()\n      expect(view.getVisualCursor().visualCol).toBe(4)\n\n      buffer.moveCursorRight()\n      expect(view.getVisualCursor().visualCol).toBe(6)\n\n      buffer.moveCursorRight()\n      expect(view.getVisualCursor().visualCol).toBe(8)\n\n      buffer.moveCursorLeft()\n      expect(view.getVisualCursor().visualCol).toBe(6)\n\n      buffer.moveCursorLeft()\n      expect(view.getVisualCursor().visualCol).toBe(4)\n\n      buffer.moveCursorLeft()\n      expect(view.getVisualCursor().visualCol).toBe(2)\n\n      buffer.moveCursorLeft()\n      expect(view.getVisualCursor().visualCol).toBe(0)\n    })\n\n    it(\"should handle backspace through mixed multi-cell graphemes\", () => {\n      buffer.setText(\"a🌟b世c\")\n\n      buffer.setCursorToLineCol(0, 7)\n      expect(view.getVisualCursor().visualCol).toBe(7)\n\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"a🌟b世\")\n      expect(view.getVisualCursor().visualCol).toBe(6)\n\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"a🌟b\")\n      expect(view.getVisualCursor().visualCol).toBe(4)\n\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"a🌟\")\n      expect(view.getVisualCursor().visualCol).toBe(3)\n\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"a\")\n      expect(view.getVisualCursor().visualCol).toBe(1)\n\n      buffer.deleteCharBackward()\n      expect(buffer.getText()).toBe(\"\")\n      expect(view.getVisualCursor().visualCol).toBe(0)\n    })\n\n    it(\"should handle delete key correctly before multi-cell graphemes\", () => {\n      buffer.setText(\"a🌟b\")\n\n      buffer.setCursorToLineCol(0, 1)\n      expect(view.getVisualCursor().visualCol).toBe(1)\n\n      buffer.deleteChar()\n      expect(buffer.getText()).toBe(\"ab\")\n      expect(view.getVisualCursor().visualCol).toBe(1)\n\n      buffer.setCursorToLineCol(0, 0)\n\n      buffer.deleteChar()\n      expect(buffer.getText()).toBe(\"b\")\n      expect(view.getVisualCursor().visualCol).toBe(0)\n    })\n\n    it(\"should handle line start and end with multi-cell graphemes\", () => {\n      buffer.setText(\"🌟世界🎉\")\n\n      buffer.setCursorToLineCol(0, 0)\n      expect(view.getVisualCursor().visualCol).toBe(0)\n\n      const eol = view.getEOL()\n      buffer.setCursorToLineCol(eol.logicalRow, eol.logicalCol)\n      expect(view.getVisualCursor().visualCol).toBe(8)\n    })\n  })\n\n  describe(\"visual line navigation (SOL/EOL)\", () => {\n    describe(\"without wrapping\", () => {\n      it(\"should get visual SOL on single line\", () => {\n        buffer.setText(\"Hello World\")\n        buffer.setCursorToLineCol(0, 6) // Middle of line\n\n        const sol = view.getVisualSOL()\n        expect(sol.logicalRow).toBe(0)\n        expect(sol.logicalCol).toBe(0)\n        expect(sol.visualRow).toBe(0)\n        expect(sol.visualCol).toBe(0)\n        expect(sol.offset).toBe(0)\n      })\n\n      it(\"should get visual EOL on single line\", () => {\n        buffer.setText(\"Hello World\")\n        buffer.setCursorToLineCol(0, 6) // Middle of line\n\n        const eol = view.getVisualEOL()\n        expect(eol.logicalRow).toBe(0)\n        expect(eol.logicalCol).toBe(11)\n        expect(eol.visualRow).toBe(0)\n        expect(eol.visualCol).toBe(11)\n      })\n\n      it(\"should get visual SOL/EOL on multi-line text\", () => {\n        buffer.setText(\"Line 1\\nLine 2\\nLine 3\")\n\n        // Test on second line\n        buffer.setCursorToLineCol(1, 3)\n\n        const sol = view.getVisualSOL()\n        expect(sol.logicalRow).toBe(1)\n        expect(sol.logicalCol).toBe(0)\n        expect(sol.visualRow).toBe(1)\n        expect(sol.visualCol).toBe(0)\n\n        const eol = view.getVisualEOL()\n        expect(eol.logicalRow).toBe(1)\n        expect(eol.logicalCol).toBe(6)\n        expect(eol.visualRow).toBe(1)\n        expect(eol.visualCol).toBe(6)\n      })\n\n      it(\"should handle visual SOL/EOL at line boundaries\", () => {\n        buffer.setText(\"ABC\\nDEF\")\n\n        // At start of line 0\n        buffer.setCursorToLineCol(0, 0)\n        let sol = view.getVisualSOL()\n        expect(sol.logicalCol).toBe(0)\n\n        // At end of line 0\n        buffer.setCursorToLineCol(0, 3)\n        let eol = view.getVisualEOL()\n        expect(eol.logicalCol).toBe(3)\n\n        // At start of line 1\n        buffer.setCursorToLineCol(1, 0)\n        sol = view.getVisualSOL()\n        expect(sol.logicalRow).toBe(1)\n        expect(sol.logicalCol).toBe(0)\n      })\n    })\n\n    describe(\"with wrapping\", () => {\n      it(\"should get SOL of first wrapped line\", () => {\n        buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        // Cursor at position 0 (first visual line)\n        buffer.setCursorToLineCol(0, 0)\n\n        const sol = view.getVisualSOL()\n        expect(sol.logicalRow).toBe(0)\n        expect(sol.logicalCol).toBe(0)\n        expect(sol.visualRow).toBe(0)\n        expect(sol.visualCol).toBe(0)\n      })\n\n      it(\"should get EOL of first wrapped line\", () => {\n        buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        buffer.setCursorToLineCol(0, 5)\n\n        const eol = view.getVisualEOL()\n        expect(eol.logicalRow).toBe(0)\n        expect(eol.logicalCol).toBe(9)\n        expect(eol.visualRow).toBe(0)\n        expect(eol.visualCol).toBe(9)\n      })\n\n      it(\"should get SOL of second wrapped line\", () => {\n        buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        buffer.setCursorToLineCol(0, 15)\n\n        const sol = view.getVisualSOL()\n        expect(sol.logicalRow).toBe(0)\n        expect(sol.logicalCol).toBe(10)\n        expect(sol.visualRow).toBe(1)\n        expect(sol.visualCol).toBe(0)\n      })\n\n      it(\"should get EOL of second wrapped line\", () => {\n        buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        buffer.setCursorToLineCol(0, 15)\n\n        const eol = view.getVisualEOL()\n        expect(eol.logicalRow).toBe(0)\n        expect(eol.logicalCol).toBe(19)\n        expect(eol.visualRow).toBe(1)\n        expect(eol.visualCol).toBe(9)\n      })\n\n      it(\"should get EOL of last wrapped line (end of logical line)\", () => {\n        buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        buffer.setCursorToLineCol(0, 25)\n\n        const eol = view.getVisualEOL()\n        expect(eol.logicalRow).toBe(0)\n        expect(eol.logicalCol).toBe(26)\n        expect(eol.visualRow).toBe(2)\n        expect(eol.visualCol).toBe(6)\n      })\n\n      it(\"should handle word wrapping correctly\", () => {\n        buffer.setText(\"Hello wonderful world of text\")\n        view.setWrapMode(\"word\")\n        view.setViewportSize(15, 10)\n\n        buffer.setCursorToLineCol(0, 20)\n\n        const vcursor = view.getVisualCursor()\n        expect(vcursor.visualRow).toBeGreaterThan(0)\n\n        const sol = view.getVisualSOL()\n        expect(sol.visualRow).toBe(vcursor.visualRow)\n        expect(sol.visualCol).toBe(0)\n        expect(sol.logicalRow).toBe(0)\n        expect(sol.logicalCol).toBeGreaterThan(0)\n\n        const eol = view.getVisualEOL()\n        expect(eol.visualRow).toBe(vcursor.visualRow)\n        expect(eol.logicalRow).toBe(0)\n        expect(eol.logicalCol).toBeGreaterThan(sol.logicalCol)\n      })\n\n      it(\"should move cursor to END of current visual line, NOT start of next line\", () => {\n        buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        buffer.setCursorToLineCol(0, 5)\n        let vcursor = view.getVisualCursor()\n        expect(vcursor.visualRow).toBe(0)\n        expect(vcursor.logicalCol).toBe(5)\n\n        const eol = view.getVisualEOL()\n        buffer.setCursor(eol.logicalRow, eol.logicalCol)\n\n        const finalCursor = buffer.getCursorPosition()\n        const finalVCursor = view.getVisualCursor()\n\n        expect(finalVCursor.visualRow).toBe(0)\n        expect(finalCursor.col).toBe(9)\n      })\n\n      it(\"should navigate through multiple wrapped lines\", () => {\n        buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        const positions = [0, 10, 20, 30]\n\n        for (const pos of positions) {\n          buffer.setCursorToLineCol(0, pos)\n\n          const vcursor = view.getVisualCursor()\n          const sol = view.getVisualSOL()\n          const eol = view.getVisualEOL()\n\n          expect(sol.visualCol).toBe(0)\n          expect(sol.visualRow).toBe(vcursor.visualRow)\n\n          expect(eol.logicalCol).toBeGreaterThan(sol.logicalCol)\n          expect(eol.visualRow).toBe(vcursor.visualRow)\n        }\n      })\n    })\n\n    describe(\"with multi-byte characters\", () => {\n      it(\"should handle emoji in visual SOL/EOL\", () => {\n        buffer.setText(\"Hello 🌟 World\")\n        buffer.setCursorToLineCol(0, 8) // After emoji\n\n        const sol = view.getVisualSOL()\n        expect(sol.logicalCol).toBe(0)\n        expect(sol.visualCol).toBe(0)\n\n        const eol = view.getVisualEOL()\n        expect(eol.logicalCol).toBe(14)\n        expect(eol.visualCol).toBe(14) // Visual width of the line\n      })\n\n      it(\"should handle CJK characters in visual SOL/EOL\", () => {\n        buffer.setText(\"测试文字\")\n        buffer.setCursorToLineCol(0, 2) // Middle\n\n        const sol = view.getVisualSOL()\n        expect(sol.logicalCol).toBe(0)\n        expect(sol.visualCol).toBe(0)\n\n        const eol = view.getVisualEOL()\n        expect(eol.logicalRow).toBe(0)\n        expect(eol.logicalCol).toBe(8) // CJK text line width\n        expect(eol.visualCol).toBe(8) // Visual width\n      })\n\n      it(\"should handle wrapped emoji correctly\", () => {\n        buffer.setText(\"🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟\") // 10 emoji\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        // First wrapped line\n        buffer.setCursorToLineCol(0, 2)\n        let sol = view.getVisualSOL()\n        let eol = view.getVisualEOL()\n        let vcursor = view.getVisualCursor()\n\n        expect(vcursor.visualRow).toBe(0)\n        expect(sol.logicalCol).toBe(0)\n        expect(sol.visualCol).toBe(0)\n        expect(eol.logicalCol).toBeGreaterThan(0)\n        expect(eol.visualCol).toBeGreaterThan(0)\n\n        // Second wrapped line - need to be far enough to be on next visual line\n        buffer.setCursorToLineCol(0, 12) // Past first 5 emoji (10 logical cols)\n        vcursor = view.getVisualCursor()\n        sol = view.getVisualSOL()\n        eol = view.getVisualEOL()\n\n        expect(vcursor.visualRow).toBe(1) // Should be on second visual line\n        expect(sol.visualCol).toBe(0)\n        expect(sol.logicalCol).toBeGreaterThan(0)\n        expect(eol.logicalCol).toBe(20) // End of logical line\n      })\n\n      it(\"should handle mixed ASCII and CJK with wrapping\", () => {\n        buffer.setText(\"AB测试CD文字EF\") // Mixed width chars\n        view.setWrapMode(\"char\")\n        view.setViewportSize(8, 10)\n\n        buffer.setCursorToLineCol(0, 5)\n\n        const vcursor = view.getVisualCursor()\n        const sol = view.getVisualSOL()\n        const eol = view.getVisualEOL()\n\n        expect(sol.visualRow).toBe(vcursor.visualRow)\n        expect(sol.visualCol).toBe(0)\n        expect(eol.visualRow).toBe(vcursor.visualRow)\n        expect(eol.visualCol).toBeGreaterThan(0)\n      })\n    })\n\n    describe(\"edge cases\", () => {\n      it(\"should handle empty line\", () => {\n        buffer.setText(\"\\n\")\n        buffer.setCursorToLineCol(0, 0)\n\n        const sol = view.getVisualSOL()\n        const eol = view.getVisualEOL()\n\n        expect(sol.logicalRow).toBe(0)\n        expect(sol.logicalCol).toBe(0)\n        expect(eol.logicalRow).toBe(0)\n        expect(eol.logicalCol).toBe(0)\n      })\n\n      it(\"should handle cursor at exact wrap boundary\", () => {\n        buffer.setText(\"0123456789ABCDEFGHIJ\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        // Cursor at position 10 (start of second visual line)\n        buffer.setCursorToLineCol(0, 10)\n\n        const vcursor = view.getVisualCursor()\n        expect(vcursor.visualRow).toBe(1)\n\n        const sol = view.getVisualSOL()\n        expect(sol.logicalCol).toBe(10)\n        expect(sol.visualRow).toBe(1)\n        expect(sol.visualCol).toBe(0)\n\n        const eol = view.getVisualEOL()\n        expect(eol.logicalCol).toBe(20)\n        expect(eol.visualRow).toBe(1)\n      })\n\n      it(\"should handle single character line\", () => {\n        buffer.setText(\"X\")\n        buffer.setCursorToLineCol(0, 0)\n\n        const sol = view.getVisualSOL()\n        const eol = view.getVisualEOL()\n\n        expect(sol.logicalCol).toBe(0)\n        expect(eol.logicalCol).toBe(1)\n      })\n\n      it(\"should compare logical EOL vs visual EOL on wrapped line\", () => {\n        buffer.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n        view.setWrapMode(\"char\")\n        view.setViewportSize(10, 10)\n\n        buffer.setCursorToLineCol(0, 5)\n\n        const logicalEOL = view.getEOL()\n        const visualEOL = view.getVisualEOL()\n\n        expect(logicalEOL.logicalCol).toBe(26)\n        expect(visualEOL.logicalCol).toBe(9)\n        expect(visualEOL.visualRow).toBe(0)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/editor-view.ts",
    "content": "import { RGBA } from \"./lib/RGBA.js\"\nimport { resolveRenderLib, type RenderLib, type VisualCursor, type LineInfo } from \"./zig.js\"\nimport { type Pointer } from \"bun:ffi\"\nimport type { EditBuffer } from \"./edit-buffer.js\"\nimport { createExtmarksController } from \"./lib/index.js\"\n\nexport interface Viewport {\n  offsetY: number\n  offsetX: number\n  height: number\n  width: number\n}\n\nexport type { VisualCursor }\n\nexport class EditorView {\n  private lib: RenderLib\n  private viewPtr: Pointer\n  private editBuffer: EditBuffer\n  private _destroyed: boolean = false\n  private _extmarksController?: any\n  private _textBufferViewPtr?: Pointer\n\n  constructor(lib: RenderLib, ptr: Pointer, editBuffer: EditBuffer) {\n    this.lib = lib\n    this.viewPtr = ptr\n    this.editBuffer = editBuffer\n  }\n\n  static create(editBuffer: EditBuffer, viewportWidth: number, viewportHeight: number): EditorView {\n    const lib = resolveRenderLib()\n    const viewPtr = lib.createEditorView(editBuffer.ptr, viewportWidth, viewportHeight)\n    return new EditorView(lib, viewPtr, editBuffer)\n  }\n\n  private guard(): void {\n    if (this._destroyed) throw new Error(\"EditorView is destroyed\")\n  }\n\n  public get ptr(): Pointer {\n    this.guard()\n    return this.viewPtr\n  }\n\n  public setViewportSize(width: number, height: number): void {\n    this.guard()\n    this.lib.editorViewSetViewportSize(this.viewPtr, width, height)\n  }\n\n  public setViewport(x: number, y: number, width: number, height: number, moveCursor: boolean = true): void {\n    this.guard()\n    this.lib.editorViewSetViewport(this.viewPtr, x, y, width, height, moveCursor)\n  }\n\n  public getViewport(): Viewport {\n    this.guard()\n    return this.lib.editorViewGetViewport(this.viewPtr)\n  }\n\n  public setScrollMargin(margin: number): void {\n    this.guard()\n    this.lib.editorViewSetScrollMargin(this.viewPtr, margin)\n  }\n\n  public setWrapMode(mode: \"none\" | \"char\" | \"word\"): void {\n    this.guard()\n    this.lib.editorViewSetWrapMode(this.viewPtr, mode)\n  }\n\n  public getVirtualLineCount(): number {\n    this.guard()\n    return this.lib.editorViewGetVirtualLineCount(this.viewPtr)\n  }\n\n  public getTotalVirtualLineCount(): number {\n    this.guard()\n    return this.lib.editorViewGetTotalVirtualLineCount(this.viewPtr)\n  }\n\n  public setSelection(start: number, end: number, bgColor?: RGBA, fgColor?: RGBA): void {\n    this.guard()\n    this.lib.editorViewSetSelection(this.viewPtr, start, end, bgColor || null, fgColor || null)\n  }\n\n  public updateSelection(end: number, bgColor?: RGBA, fgColor?: RGBA): void {\n    this.guard()\n    this.lib.editorViewUpdateSelection(this.viewPtr, end, bgColor || null, fgColor || null)\n  }\n\n  public resetSelection(): void {\n    this.guard()\n    this.lib.editorViewResetSelection(this.viewPtr)\n  }\n\n  public getSelection(): { start: number; end: number } | null {\n    this.guard()\n    return this.lib.editorViewGetSelection(this.viewPtr)\n  }\n\n  public hasSelection(): boolean {\n    this.guard()\n    return this.getSelection() !== null\n  }\n\n  public setLocalSelection(\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor?: RGBA,\n    fgColor?: RGBA,\n    updateCursor?: boolean,\n    followCursor?: boolean,\n  ): boolean {\n    this.guard()\n    return this.lib.editorViewSetLocalSelection(\n      this.viewPtr,\n      anchorX,\n      anchorY,\n      focusX,\n      focusY,\n      bgColor || null,\n      fgColor || null,\n      updateCursor ?? false,\n      followCursor ?? false,\n    )\n  }\n\n  public updateLocalSelection(\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor?: RGBA,\n    fgColor?: RGBA,\n    updateCursor?: boolean,\n    followCursor?: boolean,\n  ): boolean {\n    this.guard()\n    return this.lib.editorViewUpdateLocalSelection(\n      this.viewPtr,\n      anchorX,\n      anchorY,\n      focusX,\n      focusY,\n      bgColor || null,\n      fgColor || null,\n      updateCursor ?? false,\n      followCursor ?? false,\n    )\n  }\n\n  public resetLocalSelection(): void {\n    this.guard()\n    this.lib.editorViewResetLocalSelection(this.viewPtr)\n  }\n\n  public getSelectedText(): string {\n    this.guard()\n    // TODO: native can stack alloc all the text and decode will alloc as js string then\n    const maxLength = 1024 * 1024 // 1MB should be enough for most selections\n    const selectedBytes = this.lib.editorViewGetSelectedTextBytes(this.viewPtr, maxLength)\n\n    if (!selectedBytes) return \"\"\n\n    return this.lib.decoder.decode(selectedBytes)\n  }\n\n  public getCursor(): { row: number; col: number } {\n    this.guard()\n    return this.lib.editorViewGetCursor(this.viewPtr)\n  }\n\n  public getText(): string {\n    this.guard()\n    const maxLength = 1024 * 1024 // 1MB buffer\n    const textBytes = this.lib.editorViewGetText(this.viewPtr, maxLength)\n    if (!textBytes) return \"\"\n    return this.lib.decoder.decode(textBytes)\n  }\n\n  public getVisualCursor(): VisualCursor {\n    this.guard()\n    return this.lib.editorViewGetVisualCursor(this.viewPtr)\n  }\n\n  public moveUpVisual(): void {\n    this.guard()\n    this.lib.editorViewMoveUpVisual(this.viewPtr)\n  }\n\n  public moveDownVisual(): void {\n    this.guard()\n    this.lib.editorViewMoveDownVisual(this.viewPtr)\n  }\n\n  public deleteSelectedText(): void {\n    this.guard()\n    this.lib.editorViewDeleteSelectedText(this.viewPtr)\n  }\n\n  public setCursorByOffset(offset: number): void {\n    this.guard()\n    this.lib.editorViewSetCursorByOffset(this.viewPtr, offset)\n  }\n\n  public getNextWordBoundary(): VisualCursor {\n    this.guard()\n    return this.lib.editorViewGetNextWordBoundary(this.viewPtr)\n  }\n\n  public getPrevWordBoundary(): VisualCursor {\n    this.guard()\n    return this.lib.editorViewGetPrevWordBoundary(this.viewPtr)\n  }\n\n  public getEOL(): VisualCursor {\n    this.guard()\n    return this.lib.editorViewGetEOL(this.viewPtr)\n  }\n\n  public getVisualSOL(): VisualCursor {\n    this.guard()\n    return this.lib.editorViewGetVisualSOL(this.viewPtr)\n  }\n\n  public getVisualEOL(): VisualCursor {\n    this.guard()\n    return this.lib.editorViewGetVisualEOL(this.viewPtr)\n  }\n\n  public getLineInfo(): LineInfo {\n    this.guard()\n    return this.lib.editorViewGetLineInfo(this.viewPtr)\n  }\n\n  public getLogicalLineInfo(): LineInfo {\n    this.guard()\n    return this.lib.editorViewGetLogicalLineInfo(this.viewPtr)\n  }\n\n  public get extmarks(): any {\n    if (!this._extmarksController) {\n      this._extmarksController = createExtmarksController(this.editBuffer, this)\n    }\n    return this._extmarksController\n  }\n\n  public setPlaceholderStyledText(chunks: { text: string; fg?: RGBA; bg?: RGBA; attributes?: number }[]): void {\n    this.guard()\n    this.lib.editorViewSetPlaceholderStyledText(this.viewPtr, chunks)\n  }\n\n  public setTabIndicator(indicator: string | number): void {\n    this.guard()\n    const codePoint = typeof indicator === \"string\" ? (indicator.codePointAt(0) ?? 0) : indicator\n    this.lib.editorViewSetTabIndicator(this.viewPtr, codePoint)\n  }\n\n  public setTabIndicatorColor(color: RGBA): void {\n    this.guard()\n    this.lib.editorViewSetTabIndicatorColor(this.viewPtr, color)\n  }\n\n  public measureForDimensions(width: number, height: number): { lineCount: number; widthColsMax: number } | null {\n    this.guard()\n    if (!this._textBufferViewPtr) {\n      this._textBufferViewPtr = this.lib.editorViewGetTextBufferView(this.viewPtr)\n    }\n    return this.lib.textBufferViewMeasureForDimensions(this._textBufferViewPtr, width, height)\n  }\n\n  public destroy(): void {\n    if (this._destroyed) return\n\n    if (this._extmarksController) {\n      this._extmarksController.destroy()\n      this._extmarksController = undefined\n    }\n\n    this._destroyed = true\n    this.lib.destroyEditorView(this.viewPtr)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/examples/ascii-font-selection-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport { CliRenderer, createCliRenderer, BoxRenderable, TextRenderable, RGBA } from \"../index.js\"\nimport { ASCIIFontRenderable } from \"../renderables/ASCIIFont.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet mainContainer: BoxRenderable | null = null\nlet fontGroup: BoxRenderable | null = null\nlet statusBox: BoxRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet selectionStartText: TextRenderable | null = null\nlet selectionMiddleText: TextRenderable | null = null\nlet selectionEndText: TextRenderable | null = null\nlet debugText: TextRenderable | null = null\nlet allFontRenderables: ASCIIFontRenderable[] = []\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"#0d1117\")\n\n  mainContainer = new BoxRenderable(renderer, {\n    id: \"mainContainer\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    width: 95,\n    height: 30,\n    backgroundColor: \"#161b22\",\n    zIndex: 1,\n    borderColor: \"#50565d\",\n    title: \"ASCII Font Selection Demo\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  renderer.root.add(mainContainer)\n\n  fontGroup = new BoxRenderable(renderer, {\n    id: \"fontGroup\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    zIndex: 10,\n  })\n  mainContainer.add(fontGroup)\n\n  const tinyFont = new ASCIIFontRenderable(renderer, {\n    id: \"tinyFont\",\n    text: \"TINY FONT DEMO\",\n    font: \"tiny\",\n    color: RGBA.fromInts(255, 255, 0, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    selectionBg: \"#4a5568\",\n    selectionFg: \"#ffffff\",\n    zIndex: 20,\n  })\n  fontGroup.add(tinyFont)\n  allFontRenderables.push(tinyFont)\n\n  const blockFont = new ASCIIFontRenderable(renderer, {\n    id: \"blockFont\",\n    text: \"opentui\",\n    font: \"block\",\n    color: [RGBA.fromInts(255, 100, 100, 255), RGBA.fromInts(100, 255, 100, 255)],\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    selectionBg: \"#4a5568\",\n    selectionFg: \"#ffffff\",\n    zIndex: 20,\n  })\n  fontGroup.add(blockFont)\n  allFontRenderables.push(blockFont)\n\n  const shadeFont = new ASCIIFontRenderable(renderer, {\n    id: \"shadeFont\",\n    text: \"SHADE\",\n    font: \"shade\",\n    color: [RGBA.fromInts(255, 200, 100, 255), RGBA.fromInts(100, 150, 200, 255)],\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    selectionBg: \"#4a5568\",\n    selectionFg: \"#ffffff\",\n    zIndex: 20,\n  })\n  fontGroup.add(shadeFont)\n  allFontRenderables.push(shadeFont)\n\n  const slickFont = new ASCIIFontRenderable(renderer, {\n    id: \"slickFont\",\n    text: \"SLICK\",\n    font: \"slick\",\n    color: [RGBA.fromInts(100, 255, 100, 255), RGBA.fromInts(255, 100, 255, 255)],\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    selectionBg: \"#4a5568\",\n    selectionFg: \"#ffffff\",\n    zIndex: 20,\n  })\n  fontGroup.add(slickFont)\n  allFontRenderables.push(slickFont)\n\n  const instructions = new TextRenderable(renderer, {\n    id: \"ascii-font-instructions\",\n    content: \"Click and drag to select text across any ASCII font elements. Press 'C' to clear selection.\",\n    left: 2,\n    top: 26,\n    zIndex: 2,\n    fg: \"#f0f6fc\",\n  })\n  mainContainer.add(instructions)\n\n  statusBox = new BoxRenderable(renderer, {\n    id: \"statusBox\",\n    position: \"absolute\",\n    left: 1,\n    top: 32,\n    width: 95,\n    height: 10,\n    backgroundColor: \"#0d1117\",\n    borderColor: \"#50565d\",\n    title: \"Selection Status\",\n    titleAlignment: \"left\",\n    border: true,\n  })\n  renderer.root.add(statusBox)\n\n  statusText = new TextRenderable(renderer, {\n    id: \"statusText\",\n    content: \"No selection - try selecting across different ASCII fonts\",\n    fg: \"#f0f6fc\",\n  })\n  statusBox.add(statusText)\n\n  selectionStartText = new TextRenderable(renderer, {\n    id: \"selectionStartText\",\n    content: \"\",\n    left: 3,\n    zIndex: 2,\n    fg: \"#7dd3fc\",\n  })\n  statusBox.add(selectionStartText)\n\n  selectionMiddleText = new TextRenderable(renderer, {\n    id: \"selectionMiddleText\",\n    content: \"\",\n    left: 3,\n    zIndex: 2,\n    fg: \"#94a3b8\",\n  })\n  statusBox.add(selectionMiddleText)\n\n  selectionEndText = new TextRenderable(renderer, {\n    id: \"selectionEndText\",\n    content: \"\",\n    left: 3,\n    zIndex: 2,\n    fg: \"#7dd3fc\",\n  })\n  statusBox.add(selectionEndText)\n\n  debugText = new TextRenderable(renderer, {\n    id: \"debugText\",\n    content: \"\",\n    left: 3,\n    zIndex: 2,\n    fg: \"#e6edf3\",\n  })\n  statusBox.add(debugText)\n\n  renderer.on(\"selection\", (selection) => {\n    if (selection && statusText && debugText && selectionStartText && selectionMiddleText && selectionEndText) {\n      const selectedText = selection.getSelectedText()\n\n      const selectedCount = allFontRenderables.filter((r) => r.hasSelection()).length\n      const container = renderer.getSelectionContainer()\n      const containerInfo = container ? `Container: ${container.id}` : \"Container: none\"\n      debugText.content = `Selected fonts: ${selectedCount}/${allFontRenderables.length} | ${containerInfo}`\n\n      if (selectedText) {\n        const lines = selectedText.split(\"\\n\")\n        const totalLength = selectedText.length\n\n        if (lines.length > 1) {\n          statusText.content = `Selected ${lines.length} lines (${totalLength} chars):`\n          selectionStartText.content = lines[0]\n          selectionMiddleText.content = \"...\"\n          selectionEndText.content = lines[lines.length - 1]\n        } else if (selectedText.length > 60) {\n          statusText.content = `Selected ${totalLength} chars:`\n          selectionStartText.content = selectedText.substring(0, 30)\n          selectionMiddleText.content = \"...\"\n          selectionEndText.content = selectedText.substring(selectedText.length - 30)\n        } else {\n          statusText.content = `Selected ${totalLength} chars:`\n          selectionStartText.content = `\"${selectedText}\"`\n          selectionMiddleText.content = \"\"\n          selectionEndText.content = \"\"\n        }\n      } else {\n        statusText.content = \"Empty selection\"\n        selectionStartText.content = \"\"\n        selectionMiddleText.content = \"\"\n        selectionEndText.content = \"\"\n      }\n    }\n  })\n\n  renderer.keyInput.on(\"keypress\", (event) => {\n    const key = event.sequence\n    if (key === \"c\" || key === \"C\") {\n      renderer.clearSelection()\n      if (statusText && debugText && selectionStartText && selectionMiddleText && selectionEndText) {\n        statusText.content = \"Selection cleared\"\n        selectionStartText.content = \"\"\n        selectionMiddleText.content = \"\"\n        selectionEndText.content = \"\"\n        debugText.content = \"\"\n      }\n    }\n  })\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  allFontRenderables = []\n\n  fontGroup?.destroyRecursively()\n  mainContainer?.destroyRecursively()\n  statusBox?.destroyRecursively()\n\n  fontGroup = null\n  mainContainer = null\n  statusBox = null\n  statusText = null\n  selectionStartText = null\n  selectionMiddleText = null\n  selectionEndText = null\n  debugText = null\n\n  renderer.clearSelection()\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    targetFps: 30,\n    enableMouseMovement: true,\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/assets/hast-example.json",
    "content": "{\n  \"type\": \"element\",\n  \"tagName\": \"pre\",\n  \"children\": [\n    {\n      \"type\": \"element\",\n      \"tagName\": \"code\",\n      \"children\": [\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"import\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"useState\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \",\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"useEffect\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"from\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"string\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"'react'\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n\" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"import\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"type\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"type\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"ReactNode\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"from\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"string\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"'react'\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n\\n\" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"interface\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"type\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"UserProfile\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"id\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"type\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"string\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"name\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"type\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"string\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"age\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"type\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"number\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"email\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"?\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"type\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"string\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n\" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n\\n\" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"comment\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"// Main application component\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n\" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"export\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"function\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"function\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"App\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"(\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \")\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"type\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"ReactNode\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"const\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"[\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"count\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \",\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"setCount\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"]\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"=\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"function\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"useState\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"(\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"number\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"0\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \")\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"const\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"user\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"type\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"UserProfile\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"=\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n    \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"id\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"string\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"'user-123'\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \",\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n    \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"name\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"string\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"'John Doe'\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \",\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n    \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"age\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"number\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"28\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \",\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n    \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"email\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \":\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"string\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"'john@example.com'\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"function\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"useEffect\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"(\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"(\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \")\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"=>\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n    \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"comment\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"// Log count changes\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n    \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"console\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \".\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"function\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"log\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"(\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"string\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"'Count changed:'\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \",\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"count\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \")\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \",\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"[\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"count\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"]\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \")\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"keyword\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"return\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"(\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n    \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"<\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"div\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \">\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n      \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"<\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"h1\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \">\" }]\n        },\n        { \"type\": \"text\", \"value\": \"Hello, \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"user\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \".\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"name\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        },\n        { \"type\": \"text\", \"value\": \"!\" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"</\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"h1\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \">\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n      \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"<\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"p\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \">\" }]\n        },\n        { \"type\": \"text\", \"value\": \"Count: \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"count\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"</\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"p\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \">\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n      \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"<\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"button\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"onClick\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"=\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"{\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"(\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \")\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"=>\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"function\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"setCount\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"(\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"count\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"+\" }]\n        },\n        { \"type\": \"text\", \"value\": \" \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"number\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"1\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \")\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \">\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n        Increment\\n      \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"</\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"button\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \">\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n    \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"</\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"variable\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"div\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"operator\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \">\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n  \" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \")\" }]\n        },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \";\" }]\n        },\n        { \"type\": \"text\", \"value\": \"\\n\" },\n        {\n          \"type\": \"element\",\n          \"tagName\": \"span\",\n          \"properties\": { \"className\": \"punctuation bracket\" },\n          \"children\": [{ \"type\": \"text\", \"value\": \"}\" }]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/core/src/examples/build.ts",
    "content": "#!/usr/bin/env bun\n\nimport { mkdirSync } from \"node:fs\"\nimport { readFile } from \"node:fs/promises\"\nimport { join, dirname } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = join(__dirname, \"..\", \"..\")\nconst examplesDir = join(rootDir, \"src\", \"examples\")\n\n// Supported platforms and architectures based on bun-webgpu and opentui native binaries\nconst targets = [\n  { platform: \"darwin\", arch: \"x64\" },\n  { platform: \"darwin\", arch: \"arm64\" },\n  { platform: \"linux\", arch: \"x64\" },\n  // dawn webgpu used by bun-webgpu is not supported on arm64 linux currently\n  // { platform: \"linux\", arch: \"arm64\" },\n  { platform: \"windows\", arch: \"x64\" },\n]\n\n// Ensure dist directory exists\nconst distDir = join(examplesDir, \"dist\")\nmkdirSync(distDir, { recursive: true })\n\n// Get version from package.json\nconst packageJson = JSON.parse(await readFile(join(rootDir, \"package.json\"), \"utf8\"))\nconst version = packageJson.version\n\n// Install bun-webgpu for all platforms to ensure cross-compilation works\nconsole.log(\"Installing bun-webgpu for all platforms...\")\nconst bunWebgpuVersion = packageJson.optionalDependencies?.[\"bun-webgpu\"]\nif (!bunWebgpuVersion) {\n  throw new Error(\"bun-webgpu is not installed\")\n}\nawait Bun.$`bun install --os=\"*\" --cpu=\"*\" bun-webgpu@${bunWebgpuVersion}`\nconsole.log(`✅ bun-webgpu@${bunWebgpuVersion} installed for all platforms`)\nconsole.log()\n\nconsole.log(`Building examples executable for all platforms...`)\nconsole.log(`Output directory: ${distDir}`)\nconsole.log()\n\nlet successCount = 0\nlet failCount = 0\n\nfor (const { platform: targetPlatform, arch: targetArch } of targets) {\n  const exeName = targetPlatform === \"windows\" ? \"opentui-examples.exe\" : \"opentui-examples\"\n  const outfile = join(distDir, `${targetPlatform}-${targetArch}`, exeName)\n  const outDir = dirname(outfile)\n\n  mkdirSync(outDir, { recursive: true })\n\n  console.log(`Building for ${targetPlatform}-${targetArch}...`)\n\n  try {\n    const buildResult = await Bun.build({\n      conditions: [\"browser\"],\n      tsconfig: join(rootDir, \"tsconfig.json\"),\n      sourcemap: \"external\",\n      compile: {\n        target: `bun-${targetPlatform}-${targetArch}` as any,\n        outfile,\n        execArgv: [`--user-agent=opentui-examples/${version}`, `--env-file=\"\"`, `--`],\n        windows: {},\n      },\n      entrypoints: [join(examplesDir, \"index.ts\")],\n      define: {\n        OPENCODE_VERSION: `'${version}'`,\n        OPENCODE_CHANNEL: `'dev'`,\n      },\n    })\n\n    if (buildResult.logs.length > 0) {\n      console.log(`  Build logs for ${targetPlatform}-${targetArch}:`)\n      buildResult.logs.forEach((log) => {\n        if (log.level === \"error\") {\n          console.error(\"  ERROR:\", log.message)\n        } else if (log.level === \"warning\") {\n          console.warn(\"  WARNING:\", log.message)\n        } else {\n          console.log(\"  INFO:\", log.message)\n        }\n      })\n    }\n\n    if (buildResult.success) {\n      console.log(`  ✅ Successfully built: ${outfile}`)\n\n      // Make it executable on Unix-like systems\n      if (targetPlatform !== \"windows\") {\n        await Bun.$`chmod +x ${outfile}`\n      }\n\n      successCount++\n    } else {\n      console.error(`  ❌ Build failed for ${targetPlatform}-${targetArch}`)\n      failCount++\n    }\n  } catch (error) {\n    console.error(`  ❌ Build error for ${targetPlatform}-${targetArch}:`, error)\n    failCount++\n  }\n\n  console.log()\n}\n\nconsole.log(\"=\".repeat(60))\nconsole.log(`Build complete: ${successCount} succeeded, ${failCount} failed`)\nconsole.log(`Output directory: ${distDir}`)\n\nif (failCount > 0) {\n  process.exit(1)\n}\n"
  },
  {
    "path": "packages/core/src/examples/code-demo.ts",
    "content": "import {\n  CliRenderer,\n  createCliRenderer,\n  CodeRenderable,\n  BoxRenderable,\n  TextRenderable,\n  type ParsedKey,\n  ScrollBoxRenderable,\n  LineNumberRenderable,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { parseColor } from \"../lib/RGBA.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\n\n// Code examples to cycle through\nconst examples = [\n  {\n    name: \"TypeScript\",\n    filetype: \"typescript\" as const,\n    code: `interface User {\n  name: string;\n  age: number;\n  email?: string;\n}\n\nclass UserManager {\n  private users: User[] = [];\n\n  constructor(initialUsers: User[] = []) {\n    this.users = initialUsers;\n  }\n\n  addUser(user: User): void {\n    if (!user.name || user.age < 0) {\n      throw new Error(\"Invalid user data\");\n    }\n    this.users.push(user);\n  }\n\n  findUser(name: string): User | undefined {\n    return this.users.find(u => u.name === name);\n  }\n\n  getUserCount(): number {\n    return this.users.length;\n  }\n\n  // Get users over a certain age\n  getAdults(minAge: number = 18): User[] {\n    return this.users.filter(user => user.age >= minAge);\n  }\n}\n\n// Usage example\nconst manager = new UserManager();\nmanager.addUser({ name: \"Alice\", age: 25, email: \"alice@example.com\" });\nmanager.addUser({ name: \"Bob\", age: 17 });\n\nconsole.log(\\`Total users: \\${manager.getUserCount()}\\`);\nconsole.log(\\`Adults: \\${manager.getAdults().length}\\`);`,\n  },\n  {\n    name: \"JavaScript\",\n    filetype: \"javascript\" as const,\n    code: `// React Component Example\nimport React, { useState, useEffect } from 'react';\n\nfunction TodoApp() {\n  const [todos, setTodos] = useState([]);\n  const [input, setInput] = useState('');\n\n  useEffect(() => {\n    // Load todos from localStorage\n    const saved = localStorage.getItem('todos');\n    if (saved) {\n      setTodos(JSON.parse(saved));\n    }\n  }, []);\n\n  const addTodo = () => {\n    if (input.trim()) {\n      const newTodo = {\n        id: Date.now(),\n        text: input,\n        completed: false\n      };\n      setTodos([...todos, newTodo]);\n      setInput('');\n    }\n  };\n\n  const toggleTodo = (id) => {\n    setTodos(todos.map(todo =>\n      todo.id === id ? { ...todo, completed: !todo.completed } : todo\n    ));\n  };\n\n  return (\n    <div className=\"todo-app\">\n      <h1>My Todo List</h1>\n      <input\n        value={input}\n        onChange={(e) => setInput(e.target.value)}\n        onKeyPress={(e) => e.key === 'Enter' && addTodo()}\n      />\n      <button onClick={addTodo}>Add</button>\n      <ul>\n        {todos.map(todo => (\n          <li key={todo.id} onClick={() => toggleTodo(todo.id)}>\n            {todo.completed ? '✓' : '○'} {todo.text}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}`,\n  },\n  {\n    name: \"Markdown\",\n    filetype: \"markdown\" as const,\n    code: `# OpenTUI Documentation\n\n## Getting Started\n\nOpenTUI is a modern terminal UI framework built on **tree-sitter** and WebGPU.\n\n### Features\n\n- 🚀 Fast rendering with WebGPU\n- 🎨 Syntax highlighting via tree-sitter\n- 📦 Component-based architecture\n- ⌨️ Rich keyboard input handling\n\n### Installation\n\n\\`\\`\\`bash\nbun install opentui\n\\`\\`\\`\n\n### Quick Example\n\n\\`\\`\\`typescript\nimport { createCliRenderer, BoxRenderable } from 'opentui';\n\nconst renderer = await createCliRenderer();\nconst box = new BoxRenderable(renderer, {\n  border: true,\n  title: \"Hello World\"\n});\nrenderer.root.add(box);\n\\`\\`\\`\n\n## API Reference\n\n### CodeRenderable\n\nThe \\`CodeRenderable\\` component provides syntax highlighting:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| content | string | Code to display |\n| filetype | string | Language type |\n| syntaxStyle | SyntaxStyle | Styling rules |\n\n> **Note**: Tree-sitter parsers are loaded lazily for performance.\n\nCJK: 알겠습니다. Task 에이전트에 ktlint + detekt 검사를 위임하겠습니다.\n\n---\n\nFor more info, visit [github.com/opentui](https://github.com)`,\n  },\n  {\n    name: \"Zig\",\n    filetype: \"zig\" as const,\n    code: `const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\n/// A simple text buffer implementation\npub const TextBuffer = struct {\n    allocator: Allocator,\n    lines: std.ArrayList([]u8),\n    dirty: bool,\n\n    pub fn init(allocator: Allocator) !TextBuffer {\n        return TextBuffer{\n            .allocator = allocator,\n            .lines = std.ArrayList([]u8).init(allocator),\n            .dirty = false,\n        };\n    }\n\n    pub fn deinit(self: *TextBuffer) void {\n        for (self.lines.items) |line| {\n            self.allocator.free(line);\n        }\n        self.lines.deinit();\n    }\n\n    /// Insert a line at the specified position\n    pub fn insertLine(self: *TextBuffer, line_num: usize, content: []const u8) !void {\n        const line = try self.allocator.dupe(u8, content);\n        errdefer self.allocator.free(line);\n\n        try self.lines.insert(line_num, line);\n        self.dirty = true;\n    }\n\n    /// Get the content of a line\n    pub fn getLine(self: *const TextBuffer, line_num: usize) ?[]const u8 {\n        if (line_num >= self.lines.items.len) return null;\n        return self.lines.items[line_num];\n    }\n\n    /// Count total characters in buffer\n    pub fn countChars(self: *const TextBuffer) usize {\n        var total: usize = 0;\n        for (self.lines.items) |line| {\n            total += line.len;\n        }\n        return total;\n    }\n};\n\ntest \"TextBuffer basic operations\" {\n    const allocator = std.testing.allocator;\n    var buffer = try TextBuffer.init(allocator);\n    defer buffer.deinit();\n\n    try buffer.insertLine(0, \"Hello, World!\");\n    try buffer.insertLine(1, \"This is Zig.\");\n\n    try std.testing.expectEqual(@as(usize, 2), buffer.lines.items.len);\n    try std.testing.expect(buffer.dirty);\n    \n    const first_line = buffer.getLine(0).?;\n    try std.testing.expectEqualStrings(\"Hello, World!\", first_line);\n}`,\n  },\n]\n\nlet renderer: CliRenderer | null = null\nlet keyboardHandler: ((key: ParsedKey) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\nlet codeScrollBox: ScrollBoxRenderable | null = null\nlet codeDisplay: CodeRenderable | null = null\nlet codeWithLineNumbers: LineNumberRenderable | null = null\nlet timingText: TextRenderable | null = null\nlet syntaxStyle: SyntaxStyle | null = null\nlet helpModal: BoxRenderable | null = null\nlet currentExampleIndex = 0\nlet concealEnabled = true\nlet highlightsEnabled = false\nlet diagnosticsEnabled = false\nlet showingHelp = false\n\nexport async function run(rendererInstance: CliRenderer): Promise<void> {\n  renderer = rendererInstance\n  renderer.start()\n  renderer.setBackgroundColor(\"#0D1117\")\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n    padding: 1,\n  })\n  renderer.root.add(parentContainer)\n\n  const titleBox = new BoxRenderable(renderer, {\n    id: \"title-box\",\n    height: 3,\n    borderStyle: \"double\",\n    borderColor: \"#4ECDC4\",\n    backgroundColor: \"#0D1117\",\n    title: \"Code Demo - Syntax Highlighting + Line Numbers\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentContainer.add(titleBox)\n\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"instructions\",\n    content: \"ESC to return | Press ? for keybindings\",\n    fg: \"#888888\",\n  })\n  titleBox.add(instructionsText)\n\n  // Create help modal (hidden by default)\n  helpModal = new BoxRenderable(renderer, {\n    id: \"help-modal\",\n    position: \"absolute\",\n    left: \"50%\",\n    top: \"50%\",\n    width: 60,\n    height: 16,\n    marginLeft: -30, // Center horizontally\n    marginTop: -8, // Center vertically\n    border: true,\n    borderStyle: \"double\",\n    borderColor: \"#4ECDC4\",\n    backgroundColor: \"#0D1117\",\n    title: \"Keybindings\",\n    titleAlignment: \"center\",\n    padding: 2,\n    zIndex: 100,\n    visible: false,\n  })\n\n  const helpContent = new TextRenderable(renderer, {\n    id: \"help-content\",\n    content: `Navigation:\n  ← → : Switch between code examples\n\nView Controls:\n  L : Toggle line numbers\n  C : Toggle concealment (Markdown links, etc.)\n\nDiff Highlighting:\n  H : Toggle diff highlights (+ green, - red)\n\nDiagnostics:\n  D : Toggle diagnostic signs (❌ ⚠️  💡)\n\nOther:\n  ? : Toggle this help screen\n  ESC : Return to main menu`,\n    fg: \"#E6EDF3\",\n  })\n\n  helpModal.add(helpContent)\n  renderer.root.add(helpModal)\n\n  codeScrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"code-scroll-box\",\n    borderStyle: \"single\",\n    borderColor: \"#6BCF7F\",\n    backgroundColor: \"#0D1117\",\n    title: `${examples[currentExampleIndex].name} (CodeRenderable)`,\n    titleAlignment: \"left\",\n    border: true,\n    scrollY: true,\n    scrollX: false,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n  parentContainer.add(codeScrollBox)\n\n  // Create syntax style similar to GitHub Dark theme\n  syntaxStyle = SyntaxStyle.fromStyles({\n    // JS/TS styles\n    keyword: { fg: parseColor(\"#FF7B72\"), bold: true },\n    \"keyword.import\": { fg: parseColor(\"#FF7B72\"), bold: true },\n    \"keyword.coroutine\": { fg: parseColor(\"#FF9492\") },\n    \"keyword.operator\": { fg: parseColor(\"#FF7B72\") },\n    string: { fg: parseColor(\"#A5D6FF\") },\n    comment: { fg: parseColor(\"#8B949E\"), italic: true },\n    number: { fg: parseColor(\"#79C0FF\") },\n    boolean: { fg: parseColor(\"#79C0FF\") },\n    constant: { fg: parseColor(\"#79C0FF\") },\n    function: { fg: parseColor(\"#D2A8FF\") },\n    \"function.call\": { fg: parseColor(\"#D2A8FF\") },\n    \"function.method.call\": { fg: parseColor(\"#D2A8FF\") },\n    constructor: { fg: parseColor(\"#FFA657\") },\n    type: { fg: parseColor(\"#FFA657\") },\n    operator: { fg: parseColor(\"#FF7B72\") },\n    variable: { fg: parseColor(\"#E6EDF3\") },\n    \"variable.member\": { fg: parseColor(\"#79C0FF\") },\n    property: { fg: parseColor(\"#79C0FF\") },\n    bracket: { fg: parseColor(\"#F0F6FC\") },\n    \"punctuation.bracket\": { fg: parseColor(\"#F0F6FC\") },\n    \"punctuation.delimiter\": { fg: parseColor(\"#C9D1D9\") },\n    punctuation: { fg: parseColor(\"#F0F6FC\") },\n\n    // Markdown specific styles (matching tree-sitter capture names)\n    \"markup.heading\": { fg: parseColor(\"#58A6FF\"), bold: true },\n    \"markup.heading.1\": { fg: parseColor(\"#00FF88\"), bold: true, underline: true },\n    \"markup.heading.2\": { fg: parseColor(\"#00D7FF\"), bold: true },\n    \"markup.heading.3\": { fg: parseColor(\"#FF69B4\") },\n    \"markup.heading.4\": { fg: parseColor(\"#FFA657\"), bold: true },\n    \"markup.heading.5\": { fg: parseColor(\"#FF7B72\"), bold: true },\n    \"markup.heading.6\": { fg: parseColor(\"#8B949E\"), bold: true },\n    \"markup.bold\": { fg: parseColor(\"#F0F6FC\"), bold: true },\n    \"markup.strong\": { fg: parseColor(\"#F0F6FC\"), bold: true },\n    \"markup.italic\": { fg: parseColor(\"#F0F6FC\"), italic: true },\n    \"markup.list\": { fg: parseColor(\"#FF7B72\") },\n    \"markup.quote\": { fg: parseColor(\"#8B949E\"), italic: true },\n    \"markup.raw\": { fg: parseColor(\"#A5D6FF\"), bg: parseColor(\"#161B22\") },\n    \"markup.raw.block\": { fg: parseColor(\"#A5D6FF\"), bg: parseColor(\"#161B22\") },\n    \"markup.raw.inline\": { fg: parseColor(\"#A5D6FF\"), bg: parseColor(\"#161B22\") },\n    \"markup.link\": { fg: parseColor(\"#58A6FF\"), underline: true },\n    \"markup.link.label\": { fg: parseColor(\"#A5D6FF\"), underline: true },\n    \"markup.link.url\": { fg: parseColor(\"#58A6FF\"), underline: true },\n    label: { fg: parseColor(\"#7EE787\") },\n    spell: { fg: parseColor(\"#E6EDF3\") },\n    nospell: { fg: parseColor(\"#E6EDF3\") },\n    conceal: { fg: parseColor(\"#6E7681\") },\n    \"punctuation.special\": { fg: parseColor(\"#8B949E\") },\n\n    default: { fg: parseColor(\"#E6EDF3\") },\n  })\n\n  // Create code display using CodeRenderable wrapped in LineNumberRenderable\n  codeDisplay = new CodeRenderable(renderer, {\n    id: \"code-display\",\n    content: examples[currentExampleIndex].code,\n    filetype: examples[currentExampleIndex].filetype,\n    syntaxStyle,\n    selectable: true,\n    selectionBg: \"#264F78\",\n    selectionFg: \"#FFFFFF\",\n    conceal: concealEnabled,\n    width: \"100%\",\n  })\n\n  codeWithLineNumbers = new LineNumberRenderable(renderer, {\n    id: \"code-with-lines\",\n    target: codeDisplay,\n    minWidth: 3,\n    paddingRight: 1,\n    fg: \"#6b7280\",\n    bg: \"#161b22\",\n    width: \"100%\",\n  })\n\n  codeScrollBox.add(codeWithLineNumbers)\n\n  timingText = new TextRenderable(renderer, {\n    id: \"timing-display\",\n    content: \"Initializing...\",\n    fg: \"#A5D6FF\",\n    wrapMode: \"word\",\n    flexShrink: 0,\n  })\n  parentContainer.add(timingText)\n\n  const updateTimingText = () => {\n    if (timingText) {\n      const lineNums = codeWithLineNumbers?.showLineNumbers ? \"ON\" : \"OFF\"\n      const diff = highlightsEnabled ? \"ON\" : \"OFF\"\n      const diag = diagnosticsEnabled ? \"ON\" : \"OFF\"\n      timingText.content = `${examples[currentExampleIndex].name} (${currentExampleIndex + 1}/${examples.length}) | Conceal: ${concealEnabled ? \"ON\" : \"OFF\"} | Lines: ${lineNums} | Diff: ${diff} | Diag: ${diag}`\n    }\n  }\n\n  updateTimingText()\n\n  keyboardHandler = (key: ParsedKey) => {\n    // Handle help modal toggle\n    if (key.raw === \"?\" && helpModal) {\n      showingHelp = !showingHelp\n      helpModal.visible = showingHelp\n      return\n    }\n\n    // Don't process other keys when help is showing\n    if (showingHelp) return\n\n    if (key.name === \"right\" || key.name === \"left\") {\n      // Navigate between examples\n      if (key.name === \"right\") {\n        currentExampleIndex = (currentExampleIndex + 1) % examples.length\n      } else {\n        currentExampleIndex = (currentExampleIndex - 1 + examples.length) % examples.length\n      }\n\n      const example = examples[currentExampleIndex]\n      if (codeScrollBox) {\n        codeScrollBox.title = `${example.name} (CodeRenderable)`\n      }\n\n      if (codeDisplay) {\n        codeDisplay.content = example.code\n        codeDisplay.filetype = example.filetype\n        updateTimingText()\n      }\n    } else if (key.name === \"c\" && !key.ctrl && !key.meta) {\n      // Toggle conceal\n      concealEnabled = !concealEnabled\n      if (codeDisplay) {\n        codeDisplay.conceal = concealEnabled\n      }\n      updateTimingText()\n    } else if (key.name === \"l\" && !key.ctrl && !key.meta) {\n      // Toggle line numbers\n      if (codeWithLineNumbers) {\n        codeWithLineNumbers.showLineNumbers = !codeWithLineNumbers.showLineNumbers\n      }\n      updateTimingText()\n    } else if (key.name === \"h\" && !key.ctrl && !key.meta) {\n      // Toggle diff highlights\n      if (codeWithLineNumbers && codeDisplay) {\n        highlightsEnabled = !highlightsEnabled\n        if (highlightsEnabled) {\n          // Add diff-style highlights for demonstration\n          const lineCount = codeDisplay.virtualLineCount\n          for (let i = 0; i < lineCount; i += 7) {\n            if (i % 14 === 0) {\n              codeWithLineNumbers.setLineColor(i, \"#1a4d1a\")\n              codeWithLineNumbers.setLineSign(i, { after: \" +\", afterColor: \"#22c55e\" })\n            } else {\n              codeWithLineNumbers.setLineColor(i, \"#4d1a1a\")\n              codeWithLineNumbers.setLineSign(i, { after: \" -\", afterColor: \"#ef4444\" })\n            }\n          }\n        } else {\n          codeWithLineNumbers.clearAllLineColors()\n          // Clear only after signs\n          const currentSigns = codeWithLineNumbers.getLineSigns()\n          for (const [line, sign] of currentSigns) {\n            if (sign.after) {\n              if (sign.before) {\n                codeWithLineNumbers.setLineSign(line, { before: sign.before, beforeColor: sign.beforeColor })\n              } else {\n                codeWithLineNumbers.clearLineSign(line)\n              }\n            }\n          }\n        }\n      }\n      updateTimingText()\n    } else if (key.name === \"d\" && !key.ctrl && !key.meta) {\n      // Toggle diagnostics\n      if (codeWithLineNumbers && codeDisplay) {\n        diagnosticsEnabled = !diagnosticsEnabled\n        if (diagnosticsEnabled) {\n          // Add diagnostic signs for demonstration\n          const lineCount = codeDisplay.virtualLineCount\n          for (let i = 0; i < lineCount; i += 9) {\n            if (i % 27 === 0) {\n              codeWithLineNumbers.setLineSign(i, { before: \"❌\", beforeColor: \"#ef4444\" })\n            } else if (i % 18 === 0) {\n              codeWithLineNumbers.setLineSign(i, { before: \"⚠️\", beforeColor: \"#f59e0b\" })\n            } else {\n              codeWithLineNumbers.setLineSign(i, { before: \"💡\", beforeColor: \"#3b82f6\" })\n            }\n          }\n        } else {\n          // Clear only before signs\n          const currentSigns = codeWithLineNumbers.getLineSigns()\n          for (const [line, sign] of currentSigns) {\n            if (sign.before) {\n              if (sign.after) {\n                codeWithLineNumbers.setLineSign(line, { after: sign.after, afterColor: sign.afterColor })\n              } else {\n                codeWithLineNumbers.clearLineSign(line)\n              }\n            }\n          }\n        }\n      }\n      updateTimingText()\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  parentContainer?.destroy()\n  helpModal?.destroy()\n  parentContainer = null\n  codeScrollBox = null\n  codeDisplay = null\n  codeWithLineNumbers = null\n  timingText = null\n  syntaxStyle = null\n  helpModal = null\n\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/console-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  TextAttributes,\n  TextRenderable,\n  BoxRenderable,\n  type MouseEvent,\n  OptimizedBuffer,\n  type RenderContext,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet titleText: TextRenderable | null = null\nlet instructionsText: TextRenderable | null = null\nlet consoleButtons: ConsoleButton[] = []\nlet statusText: TextRenderable | null = null\nlet buttonCounters = {\n  log: 0,\n  info: 0,\n  warn: 0,\n  error: 0,\n  debug: 0,\n}\n\nclass ConsoleButton extends BoxRenderable {\n  private isHovered = false\n  private isPressed = false\n  private originalBg: RGBA\n  private hoverBg: RGBA\n  private pressBg: RGBA\n  private logType: string\n  private lastClickTime = 0\n\n  constructor(\n    ctx: RenderContext,\n    id: string,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    color: RGBA,\n    label: string,\n    logType: string,\n  ) {\n    const borderColor = RGBA.fromValues(color.r * 1.3, color.g * 1.3, color.b * 1.3, 1.0)\n\n    super(ctx, {\n      id,\n      position: \"absolute\",\n      left: x,\n      top: y,\n      width,\n      height,\n      zIndex: 100,\n      backgroundColor: color,\n      borderColor: borderColor,\n      borderStyle: \"rounded\",\n      title: label,\n      titleAlignment: \"center\",\n      border: true,\n    })\n\n    this.logType = logType\n    this.originalBg = color\n    this.hoverBg = RGBA.fromValues(color.r * 1.2, color.g * 1.2, color.b * 1.2, color.a)\n    this.pressBg = RGBA.fromValues(color.r * 0.8, color.g * 0.8, color.b * 0.8, color.a)\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    if (this.isPressed) {\n      this.backgroundColor = this.pressBg\n    } else if (this.isHovered) {\n      this.backgroundColor = this.hoverBg\n    } else {\n      this.backgroundColor = this.originalBg\n    }\n\n    super.renderSelf(buffer)\n\n    const timeSinceClick = Date.now() - this.lastClickTime\n    if (timeSinceClick < 300) {\n      const alpha = 1 - timeSinceClick / 300\n      const sparkleColor = RGBA.fromValues(1, 1, 1, alpha)\n\n      const centerX = this.x + Math.floor(this.width / 2)\n      const centerY = this.y + Math.floor(this.height / 2)\n\n      buffer.setCell(centerX - 1, centerY, \"✦\", sparkleColor, this.backgroundColor)\n      buffer.setCell(centerX + 1, centerY, \"✦\", sparkleColor, this.backgroundColor)\n    }\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    switch (event.type) {\n      case \"down\":\n        this.isPressed = true\n        this.lastClickTime = Date.now()\n        buttonCounters[this.logType as keyof typeof buttonCounters]++\n\n        this.triggerConsoleLog()\n        event.stopPropagation()\n        break\n\n      case \"up\":\n        this.isPressed = false\n        event.stopPropagation()\n        break\n\n      case \"over\":\n        this.isHovered = true\n        break\n\n      case \"out\":\n        this.isHovered = false\n        this.isPressed = false\n        break\n    }\n  }\n\n  private triggerConsoleLog(): void {\n    const count = buttonCounters[this.logType as keyof typeof buttonCounters]\n    const timestamp = new Date().toLocaleTimeString()\n\n    switch (this.logType) {\n      case \"log\":\n        console.log(`Console Log #${count} triggered at ${timestamp}`, {\n          data: \"This is a regular log message\",\n          count,\n          timestamp: new Date(),\n          metadata: { source: \"console-demo\", type: \"log\" },\n        })\n        break\n\n      case \"info\":\n        console.info(`Info Log #${count} triggered at ${timestamp}`, {\n          message: \"This is an informational message\",\n          details: \"Info messages are used for general information\",\n          level: \"INFO\",\n          count,\n        })\n        break\n\n      case \"warn\":\n        console.warn(`Warning Log #${count} triggered at ${timestamp}`, {\n          warning: \"This is a warning message\",\n          reason: \"Something might need attention\",\n          severity: \"WARNING\",\n          count,\n          stack: new Error().stack?.split(\"\\n\").slice(0, 3),\n        })\n        break\n\n      case \"error\":\n        console.error(`Error Log #${count} triggered at ${timestamp}`, {\n          error: \"This is an error message\",\n          details: \"Something went wrong (simulated)\",\n          errorCode: `ERR_${count}`,\n          count,\n          fakeStack: new Error(\"Simulated error\").stack,\n        })\n        break\n\n      case \"debug\":\n        console.debug(`Debug Log #${count} triggered at ${timestamp}`, {\n          debug: \"This is a debug message\",\n          variables: { x: Math.random(), y: Math.random(), count },\n          state: \"debugging\",\n          performance: { memory: process.memoryUsage() },\n        })\n        break\n    }\n\n    if (statusText) {\n      statusText.content = `Last triggered: ${this.logType.toUpperCase()} #${count} at ${timestamp}`\n    }\n  }\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n\n  renderer.console.keyBindings = [{ name: \"y\", ctrl: true, action: \"copy-selection\" }]\n  renderer.console.onCopySelection = (text) => {\n    // Use OSC 52 escape sequence for clipboard - works over SSH and on all platforms\n    // The terminal emulator handles the clipboard operation locally\n    const success = renderer.copyToClipboardOSC52(text)\n    if (success) {\n      console.info(`Copied to clipboard: \"${text.substring(0, 50)}${text.length > 50 ? \"...\" : \"\"}\"`)\n    } else {\n      console.warn(\"Clipboard copy failed - OSC 52 not supported or stdout is not a TTY\")\n    }\n  }\n\n  renderer.console.show()\n\n  const backgroundColor = RGBA.fromInts(18, 22, 35, 255)\n  renderer.setBackgroundColor(backgroundColor)\n\n  titleText = new TextRenderable(renderer, {\n    id: \"console_demo_title\",\n    content: \"Console Logging Demo\",\n    position: \"absolute\",\n    left: 2,\n    top: 1,\n    fg: RGBA.fromInts(255, 215, 135),\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  renderer.root.add(titleText)\n\n  instructionsText = new TextRenderable(renderer, {\n    id: \"console_demo_instructions\",\n    content:\n      \"Click buttons to trigger different console log levels • Press ` to toggle console • Ctrl+Y to copy selection • Escape: return to menu\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    fg: RGBA.fromInts(176, 196, 222),\n    zIndex: 1000,\n  })\n  renderer.root.add(instructionsText)\n\n  statusText = new TextRenderable(renderer, {\n    id: \"console_demo_status\",\n    content: \"Click any button to start logging...\",\n    position: \"absolute\",\n    left: 2,\n    top: 4,\n    fg: RGBA.fromInts(144, 238, 144),\n    attributes: TextAttributes.ITALIC,\n    zIndex: 1000,\n  })\n  renderer.root.add(statusText)\n\n  const logColor = RGBA.fromInts(160, 160, 170, 255)\n  const infoColor = RGBA.fromInts(100, 180, 200, 255)\n  const warnColor = RGBA.fromInts(220, 180, 100, 255)\n  const errorColor = RGBA.fromInts(200, 120, 120, 255)\n  const debugColor = RGBA.fromInts(140, 140, 150, 255)\n\n  const startY = 7\n  const buttonWidth = 16\n  const buttonHeight = 6\n  const spacing = 18\n\n  consoleButtons = [\n    new ConsoleButton(renderer, \"log-btn\", 2, startY, buttonWidth, buttonHeight, logColor, \"LOG\", \"log\"),\n    new ConsoleButton(renderer, \"info-btn\", 2 + spacing, startY, buttonWidth, buttonHeight, infoColor, \"INFO\", \"info\"),\n    new ConsoleButton(\n      renderer,\n      \"warn-btn\",\n      2 + spacing * 2,\n      startY,\n      buttonWidth,\n      buttonHeight,\n      warnColor,\n      \"WARN\",\n      \"warn\",\n    ),\n    new ConsoleButton(\n      renderer,\n      \"error-btn\",\n      2 + spacing * 3,\n      startY,\n      buttonWidth,\n      buttonHeight,\n      errorColor,\n      \"ERROR\",\n      \"error\",\n    ),\n    new ConsoleButton(\n      renderer,\n      \"debug-btn\",\n      2 + spacing * 4,\n      startY,\n      buttonWidth,\n      buttonHeight,\n      debugColor,\n      \"DEBUG\",\n      \"debug\",\n    ),\n  ]\n\n  for (const button of consoleButtons) {\n    renderer.root.add(button)\n  }\n\n  const decorText1 = new TextRenderable(renderer, {\n    id: \"decor1\",\n    content: \"✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦\",\n    position: \"absolute\",\n    left: 2,\n    top: startY + 12,\n    fg: RGBA.fromInts(100, 120, 150, 120),\n    zIndex: 50,\n  })\n  renderer.root.add(decorText1)\n\n  const decorText2 = new TextRenderable(renderer, {\n    id: \"decor2\",\n    content: \"Console will appear at the bottom. Use Ctrl+P/Ctrl+O to change position, +/- to resize.\",\n    position: \"absolute\",\n    left: 2,\n    top: startY + 14,\n    fg: RGBA.fromInts(120, 140, 160, 200),\n    attributes: TextAttributes.ITALIC,\n    zIndex: 50,\n  })\n  renderer.root.add(decorText2)\n\n  console.log(\"Console Demo initialized! Click the buttons above to test different log levels.\")\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n\n  if (titleText) {\n    renderer.root.remove(\"console_demo_title\")\n    titleText = null\n  }\n\n  if (instructionsText) {\n    renderer.root.remove(\"console_demo_instructions\")\n    instructionsText = null\n  }\n\n  if (statusText) {\n    renderer.root.remove(\"console_demo_status\")\n    statusText = null\n  }\n\n  for (const button of consoleButtons) {\n    renderer.root.remove(button.id)\n  }\n  consoleButtons = []\n\n  renderer.root.remove(\"decor1\")\n  renderer.root.remove(\"decor2\")\n\n  buttonCounters = {\n    log: 0,\n    info: 0,\n    warn: 0,\n    error: 0,\n    debug: 0,\n  }\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/core-plugin-slots-demo.ts",
    "content": "import {\n  BoxRenderable,\n  type CliRenderer,\n  createCliRenderer,\n  createCoreSlotRegistry,\n  registerCorePlugin,\n  SlotRenderable,\n  TextRenderable,\n  type CorePlugin,\n  type CoreSlotMode,\n  type CoreSlotRegistry,\n  type PluginErrorEvent,\n  type KeyEvent,\n} from \"../index\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys\"\n\ntype DemoSlot = \"statusbar\" | \"sidebar\"\ntype DemoContext = { appName: string; version: string }\ntype DemoSlotData = { label?: string; section?: string }\n\nconst DEMO_STATUS_LABEL = \"host-status\"\nconst DEMO_SIDEBAR_SECTION = \"plugins\"\n\ninterface PluginStats {\n  statusbarCreates: number\n  sidebarCreates: number\n}\n\nlet renderer: CliRenderer | null = null\nlet rootContainer: BoxRenderable | null = null\nlet statusbarSlot: SlotRenderable<DemoSlot, DemoContext, DemoSlotData> | null = null\nlet sidebarSlot: SlotRenderable<DemoSlot, DemoContext, DemoSlotData> | null = null\nlet infoPanel: BoxRenderable | null = null\nlet infoText: TextRenderable | null = null\n\nlet slotRegistry: CoreSlotRegistry<DemoSlot, DemoContext, DemoSlotData> | null = null\n\nlet unregisterClockPlugin: (() => void) | null = null\nlet unregisterActivityPlugin: (() => void) | null = null\n\nconst clockStats: PluginStats = {\n  statusbarCreates: 0,\n  sidebarCreates: 0,\n}\n\nconst activityStats: PluginStats = {\n  statusbarCreates: 0,\n  sidebarCreates: 0,\n}\n\nlet clockPluginEnabled = false\nlet activityPluginEnabled = false\nlet orderFlipped = false\nlet statusbarMode: CoreSlotMode = \"append\"\nlet clockStatusbarErrorEnabled = false\nlet clockSidebarErrorEnabled = false\nlet activityStatusbarErrorEnabled = false\nlet showPluginFailurePlaceholder = true\nlet unregisterPluginErrorListener: (() => void) | null = null\nlet pluginErrorHistory: string[] = []\nlet placeholderCreateCount = 0\n\nconst MAX_PLUGIN_ERROR_HISTORY = 6\n\nconst getClockOrder = () => (orderFlipped ? 20 : 0)\nconst getActivityOrder = () => (orderFlipped ? -10 : 10)\n\nfunction nextStatusbarMode(mode: CoreSlotMode): CoreSlotMode {\n  if (mode === \"append\") {\n    return \"replace\"\n  }\n\n  if (mode === \"replace\") {\n    return \"single_winner\"\n  }\n\n  return \"append\"\n}\n\nfunction formatPluginError(event: PluginErrorEvent): string {\n  const slot = event.slot ?? \"<none>\"\n  return `${event.pluginId} [${event.phase}/${event.source}] @ ${slot}: ${event.error.message}`\n}\n\nfunction pushPluginError(event: PluginErrorEvent): void {\n  pluginErrorHistory = [formatPluginError(event), ...pluginErrorHistory].slice(0, MAX_PLUGIN_ERROR_HISTORY)\n}\n\nfunction createPluginFailurePlaceholder(\n  rendererInstance: CliRenderer,\n  failure: PluginErrorEvent,\n  color: string,\n): BoxRenderable {\n  placeholderCreateCount++\n\n  const container = new BoxRenderable(rendererInstance, {\n    id: `plugin-error-${failure.pluginId}-${placeholderCreateCount}`,\n    border: true,\n    borderStyle: \"single\",\n    borderColor: color,\n    paddingLeft: 1,\n    paddingRight: 1,\n    marginLeft: 1,\n    backgroundColor: \"#1f1115\",\n  })\n\n  const title = new TextRenderable(rendererInstance, {\n    id: `plugin-error-title-${failure.pluginId}-${placeholderCreateCount}`,\n    content: `Plugin error: ${failure.pluginId}`,\n    fg: \"#fecaca\",\n  })\n\n  const details = new TextRenderable(rendererInstance, {\n    id: `plugin-error-details-${failure.pluginId}-${placeholderCreateCount}`,\n    content: `${failure.phase}/${failure.source} @ ${failure.slot ?? \"unknown\"}`,\n    fg: \"#fca5a5\",\n  })\n\n  container.add(title)\n  container.add(details)\n  return container\n}\n\nfunction remountEnabledPlugins(): void {\n  if (!slotRegistry || !renderer) {\n    return\n  }\n\n  if (clockPluginEnabled) {\n    unregisterClockPlugin?.()\n    unregisterClockPlugin = registerCorePlugin(slotRegistry, createClockPlugin(renderer))\n  }\n\n  if (activityPluginEnabled) {\n    unregisterActivityPlugin?.()\n    unregisterActivityPlugin = registerCorePlugin(slotRegistry, createActivityPlugin(renderer))\n  }\n\n  statusbarSlot?.refresh()\n  sidebarSlot?.refresh()\n}\n\nfunction resetPluginFailureState(): void {\n  clockStatusbarErrorEnabled = false\n  clockSidebarErrorEnabled = false\n  activityStatusbarErrorEnabled = false\n  pluginErrorHistory = []\n  slotRegistry?.clearPluginErrors()\n\n  remountEnabledPlugins()\n  updateInfoPanel()\n}\n\nfunction updateInfoPanel(): void {\n  if (!infoText) {\n    return\n  }\n\n  infoText.content = [\n    \"Core Plugin Slot Demo\",\n    \"\",\n    `Statusbar mode: ${statusbarMode.toUpperCase()} (press m to cycle)`,\n    `Clock plugin: ${clockPluginEnabled ? \"ON\" : \"OFF\"} (press 1)`,\n    `Activity plugin: ${activityPluginEnabled ? \"ON\" : \"OFF\"} (press 2)`,\n    `Order flipped: ${orderFlipped ? \"YES\" : \"NO\"} (press o)`,\n    `Show error placeholders: ${showPluginFailurePlaceholder ? \"YES\" : \"NO\"} (press p)`,\n    \"\",\n    `Clock statusbar throw: ${clockStatusbarErrorEnabled ? \"ON\" : \"OFF\"} (press e)`,\n    `Clock sidebar throw: ${clockSidebarErrorEnabled ? \"ON\" : \"OFF\"} (press w)`,\n    `Activity statusbar throw: ${activityStatusbarErrorEnabled ? \"ON\" : \"OFF\"} (press d)`,\n    \"Press x to reset all forced errors.\",\n    \"\",\n    `Clock create counts -> statusbar: ${clockStats.statusbarCreates}, sidebar: ${clockStats.sidebarCreates}`,\n    `Activity create counts -> statusbar: ${activityStats.statusbarCreates}`,\n    \"\",\n    \"Press r to force slot refresh.\",\n    `Statusbar slot data.label: ${statusbarSlot?.data.label ?? DEMO_STATUS_LABEL}`,\n    `Sidebar slot data.section: ${sidebarSlot?.data.section ?? DEMO_SIDEBAR_SECTION}`,\n    \"\",\n    \"Statusbar fallback is always shown in APPEND mode.\",\n    \"Sidebar fallback appears only when no sidebar plugin is active.\",\n    \"\",\n    \"Recent plugin errors:\",\n    ...(pluginErrorHistory.length > 0 ? pluginErrorHistory : [\"(none)\"]),\n  ].join(\"\\n\")\n}\n\nfunction createClockPlugin(rendererInstance: CliRenderer): CorePlugin<DemoSlot, DemoContext, DemoSlotData> {\n  let statusbarItem: BoxRenderable | null = null\n  let statusText: TextRenderable | null = null\n  let sidebarPanel: BoxRenderable | null = null\n  let sidebarText: TextRenderable | null = null\n  let timer: ReturnType<typeof setInterval> | null = null\n  const activeSlots = new Set<DemoSlot>()\n  let statusbarLabel = \"status\"\n\n  const startClockTimer = () => {\n    if (timer) {\n      return\n    }\n\n    updateClockText()\n    timer = setInterval(updateClockText, 1000)\n  }\n\n  const stopClockTimer = () => {\n    if (!timer) {\n      return\n    }\n\n    clearInterval(timer)\n    timer = null\n  }\n\n  const syncClockTimer = () => {\n    if (activeSlots.size > 0) {\n      startClockTimer()\n      return\n    }\n\n    stopClockTimer()\n  }\n\n  const disposeStatusbarNode = () => {\n    statusbarItem?.destroyRecursively()\n    statusbarItem = null\n    statusText = null\n  }\n\n  const disposeSidebarNode = () => {\n    sidebarPanel?.destroyRecursively()\n    sidebarPanel = null\n    sidebarText = null\n  }\n\n  const updateClockText = () => {\n    const timestamp = new Date().toLocaleTimeString()\n\n    if (statusText) {\n      if (statusText.isDestroyed) {\n        statusText = null\n      } else {\n        statusText.content = `Clock plugin -> ${statusbarLabel} (${timestamp})`\n      }\n    }\n\n    if (sidebarText) {\n      if (sidebarText.isDestroyed) {\n        sidebarText = null\n      } else {\n        sidebarText.content = `Last tick: ${timestamp}`\n      }\n    }\n  }\n\n  return {\n    id: \"clock-plugin\",\n    order: getClockOrder(),\n    dispose() {\n      stopClockTimer()\n      activeSlots.clear()\n      disposeStatusbarNode()\n      disposeSidebarNode()\n    },\n    slots: {\n      statusbar: {\n        render(_ctx, data) {\n          if (clockStatusbarErrorEnabled) {\n            throw new Error(\"Forced clock statusbar failure\")\n          }\n\n          statusbarLabel = data.label ?? \"status\"\n\n          clockStats.statusbarCreates++\n\n          const item = new BoxRenderable(rendererInstance, {\n            id: `clock-statusbar-${clockStats.statusbarCreates}`,\n            border: true,\n            borderStyle: \"single\",\n            borderColor: \"#2563eb\",\n            paddingLeft: 1,\n            paddingRight: 1,\n            height: 3,\n            marginLeft: 1,\n            backgroundColor: \"#0f172a\",\n          })\n          statusbarItem = item\n\n          statusText = new TextRenderable(rendererInstance, {\n            id: `clock-statusbar-text-${clockStats.statusbarCreates}`,\n            content: `Clock plugin -> ${statusbarLabel}`,\n            fg: \"#93c5fd\",\n          })\n\n          item.add(statusText)\n          updateClockText()\n          updateInfoPanel()\n          return item\n        },\n        onActivate() {\n          activeSlots.add(\"statusbar\")\n          syncClockTimer()\n        },\n        onDeactivate() {\n          activeSlots.delete(\"statusbar\")\n          disposeStatusbarNode()\n          syncClockTimer()\n        },\n        onDispose() {\n          activeSlots.delete(\"statusbar\")\n          disposeStatusbarNode()\n          syncClockTimer()\n        },\n      },\n      sidebar: {\n        render(_ctx, data) {\n          if (clockSidebarErrorEnabled) {\n            throw new Error(\"Forced clock sidebar failure\")\n          }\n\n          clockStats.sidebarCreates++\n\n          const panel = new BoxRenderable(rendererInstance, {\n            id: `clock-sidebar-${clockStats.sidebarCreates}`,\n            border: true,\n            borderStyle: \"single\",\n            borderColor: \"#0ea5e9\",\n            flexDirection: \"column\",\n            height: 6,\n            marginBottom: 1,\n            padding: 1,\n          })\n          sidebarPanel = panel\n\n          panel.add(\n            new TextRenderable(rendererInstance, {\n              id: `clock-sidebar-title-${clockStats.sidebarCreates}`,\n              content: `Clock Sidebar (${data.section ?? \"left\"})`,\n              fg: \"#38bdf8\",\n            }),\n          )\n\n          sidebarText = new TextRenderable(rendererInstance, {\n            id: `clock-sidebar-text-${clockStats.sidebarCreates}`,\n            content: \"Last tick: --:--:--\",\n            fg: \"#e2e8f0\",\n            marginTop: 1,\n          })\n\n          panel.add(sidebarText)\n          updateClockText()\n          updateInfoPanel()\n          return panel\n        },\n        onActivate() {\n          activeSlots.add(\"sidebar\")\n          syncClockTimer()\n        },\n        onDeactivate() {\n          activeSlots.delete(\"sidebar\")\n          disposeSidebarNode()\n          syncClockTimer()\n        },\n        onDispose() {\n          activeSlots.delete(\"sidebar\")\n          disposeSidebarNode()\n          syncClockTimer()\n        },\n      },\n    },\n  }\n}\n\nfunction createActivityPlugin(rendererInstance: CliRenderer): CorePlugin<DemoSlot, DemoContext, DemoSlotData> {\n  let activityItem: BoxRenderable | null = null\n  let activityText: TextRenderable | null = null\n  let timer: ReturnType<typeof setInterval> | null = null\n  let phase = 0\n  let statusbarActive = false\n  let statusbarLabel = \"status\"\n\n  const pulse = [\".\", \"..\", \"...\", \"....\"]\n\n  const startActivityTimer = () => {\n    if (timer) {\n      return\n    }\n\n    updateActivityText()\n    timer = setInterval(updateActivityText, 700)\n  }\n\n  const stopActivityTimer = () => {\n    if (!timer) {\n      return\n    }\n\n    clearInterval(timer)\n    timer = null\n  }\n\n  const syncActivityTimer = () => {\n    if (statusbarActive) {\n      startActivityTimer()\n      return\n    }\n\n    stopActivityTimer()\n  }\n\n  const disposeStatusbarNode = () => {\n    activityItem?.destroyRecursively()\n    activityItem = null\n    activityText = null\n  }\n\n  const updateActivityText = () => {\n    phase = (phase + 1) % pulse.length\n    if (activityText) {\n      if (activityText.isDestroyed) {\n        activityText = null\n      } else {\n        activityText.content = `Activity (${statusbarLabel})${pulse[phase]}`\n      }\n    }\n  }\n\n  return {\n    id: \"activity-plugin\",\n    order: getActivityOrder(),\n    dispose() {\n      statusbarActive = false\n      stopActivityTimer()\n      disposeStatusbarNode()\n    },\n    slots: {\n      statusbar: {\n        render(_ctx, data) {\n          if (activityStatusbarErrorEnabled) {\n            throw new Error(\"Forced activity statusbar failure\")\n          }\n\n          statusbarLabel = data.label ?? \"status\"\n\n          activityStats.statusbarCreates++\n\n          const item = new BoxRenderable(rendererInstance, {\n            id: `activity-statusbar-${activityStats.statusbarCreates}`,\n            border: true,\n            borderStyle: \"single\",\n            borderColor: \"#16a34a\",\n            paddingLeft: 1,\n            paddingRight: 1,\n            height: 3,\n            marginLeft: 1,\n            backgroundColor: \"#052e16\",\n          })\n          activityItem = item\n\n          activityText = new TextRenderable(rendererInstance, {\n            id: `activity-statusbar-text-${activityStats.statusbarCreates}`,\n            content: `Activity (${statusbarLabel})`,\n            fg: \"#86efac\",\n          })\n\n          item.add(activityText)\n          updateActivityText()\n          updateInfoPanel()\n          return item\n        },\n        onActivate() {\n          statusbarActive = true\n          syncActivityTimer()\n        },\n        onDeactivate() {\n          statusbarActive = false\n          disposeStatusbarNode()\n          syncActivityTimer()\n        },\n        onDispose() {\n          statusbarActive = false\n          disposeStatusbarNode()\n          syncActivityTimer()\n        },\n      },\n    },\n  }\n}\n\nfunction setClockPluginEnabled(enabled: boolean): void {\n  if (!slotRegistry || !renderer) {\n    return\n  }\n\n  if (enabled && !clockPluginEnabled) {\n    unregisterClockPlugin = registerCorePlugin(slotRegistry, createClockPlugin(renderer))\n    clockPluginEnabled = true\n  } else if (!enabled && clockPluginEnabled) {\n    unregisterClockPlugin?.()\n    unregisterClockPlugin = null\n    clockPluginEnabled = false\n  }\n\n  updateInfoPanel()\n}\n\nfunction setActivityPluginEnabled(enabled: boolean): void {\n  if (!slotRegistry || !renderer) {\n    return\n  }\n\n  if (enabled && !activityPluginEnabled) {\n    unregisterActivityPlugin = registerCorePlugin(slotRegistry, createActivityPlugin(renderer))\n    activityPluginEnabled = true\n  } else if (!enabled && activityPluginEnabled) {\n    unregisterActivityPlugin?.()\n    unregisterActivityPlugin = null\n    activityPluginEnabled = false\n  }\n\n  updateInfoPanel()\n}\n\nfunction handleKeyPress(key: KeyEvent): void {\n  if (!slotRegistry) {\n    return\n  }\n\n  switch (key.name) {\n    case \"1\":\n      setClockPluginEnabled(!clockPluginEnabled)\n      break\n    case \"2\":\n      setActivityPluginEnabled(!activityPluginEnabled)\n      break\n    case \"m\":\n      statusbarMode = nextStatusbarMode(statusbarMode)\n      if (statusbarSlot) {\n        statusbarSlot.mode = statusbarMode\n      }\n      updateInfoPanel()\n      break\n    case \"o\":\n      orderFlipped = !orderFlipped\n      slotRegistry.updateOrder(\"clock-plugin\", getClockOrder())\n      slotRegistry.updateOrder(\"activity-plugin\", getActivityOrder())\n      updateInfoPanel()\n      break\n    case \"r\":\n      statusbarSlot?.refresh()\n      sidebarSlot?.refresh()\n      updateInfoPanel()\n      break\n    case \"e\":\n      clockStatusbarErrorEnabled = !clockStatusbarErrorEnabled\n      remountEnabledPlugins()\n      updateInfoPanel()\n      break\n    case \"w\":\n      clockSidebarErrorEnabled = !clockSidebarErrorEnabled\n      remountEnabledPlugins()\n      updateInfoPanel()\n      break\n    case \"d\":\n      activityStatusbarErrorEnabled = !activityStatusbarErrorEnabled\n      remountEnabledPlugins()\n      updateInfoPanel()\n      break\n    case \"p\":\n      showPluginFailurePlaceholder = !showPluginFailurePlaceholder\n      remountEnabledPlugins()\n      updateInfoPanel()\n      break\n    case \"x\":\n      resetPluginFailureState()\n      break\n  }\n}\n\nfunction createLayout(rendererInstance: CliRenderer): void {\n  rootContainer = new BoxRenderable(rendererInstance, {\n    id: \"core-plugin-demo-root\",\n    width: \"100%\",\n    height: \"100%\",\n    flexDirection: \"column\",\n    padding: 1,\n    backgroundColor: \"#020617\",\n  })\n\n  const body = new BoxRenderable(rendererInstance, {\n    id: \"core-plugin-demo-body\",\n    width: \"100%\",\n    flexGrow: 1,\n    flexDirection: \"row\",\n  })\n\n  infoPanel = new BoxRenderable(rendererInstance, {\n    id: \"core-plugin-demo-info-panel\",\n    flexGrow: 1,\n    border: true,\n    borderStyle: \"single\",\n    borderColor: \"#334155\",\n    flexDirection: \"column\",\n    padding: 1,\n  })\n\n  infoText = new TextRenderable(rendererInstance, {\n    id: \"core-plugin-demo-info-text\",\n    fg: \"#e2e8f0\",\n    content: \"\",\n  })\n\n  infoPanel.add(infoText)\n\n  rootContainer.add(body)\n  rendererInstance.root.add(rootContainer)\n\n  // The SlotRenderables are created later in run() after the registry exists,\n  // but we need to save the body reference to add them.\n  // We'll add the slots in run() instead.\n  ;(rootContainer as any).__body = body\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  clockStats.statusbarCreates = 0\n  clockStats.sidebarCreates = 0\n  activityStats.statusbarCreates = 0\n  activityStats.sidebarCreates = 0\n\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#000000\")\n\n  createLayout(rendererInstance)\n\n  slotRegistry = createCoreSlotRegistry<DemoSlot, DemoContext, DemoSlotData>(rendererInstance, {\n    appName: \"core-plugin-slots-demo\",\n    version: \"1.0.0\",\n  })\n\n  if (!slotRegistry || !rootContainer) {\n    return\n  }\n\n  const body = (rootContainer as any).__body as BoxRenderable\n\n  unregisterPluginErrorListener = slotRegistry.onPluginError((event) => {\n    pushPluginError(event)\n    updateInfoPanel()\n  })\n\n  statusbarSlot = new SlotRenderable(rendererInstance, {\n    id: \"core-plugin-demo-statusbar-slot\",\n    registry: slotRegistry,\n    name: \"statusbar\",\n    data: { label: DEMO_STATUS_LABEL },\n    mode: statusbarMode,\n    width: \"100%\",\n    height: 5,\n    alignItems: \"center\",\n    flexDirection: \"row\",\n    paddingLeft: 1,\n    marginBottom: 1,\n    fallback: () =>\n      new TextRenderable(rendererInstance, {\n        id: \"statusbar-fallback\",\n        content: \"Fallback statusbar content\",\n        fg: \"#94a3b8\",\n      }),\n    pluginFailurePlaceholder: (failure) => {\n      if (!showPluginFailurePlaceholder) {\n        return undefined\n      }\n\n      return createPluginFailurePlaceholder(rendererInstance, failure, \"#fb7185\")\n    },\n  })\n\n  sidebarSlot = new SlotRenderable(rendererInstance, {\n    id: \"core-plugin-demo-sidebar-slot\",\n    registry: slotRegistry,\n    name: \"sidebar\",\n    data: { section: DEMO_SIDEBAR_SECTION },\n    mode: \"replace\",\n    width: 36,\n    flexDirection: \"column\",\n    padding: 1,\n    marginRight: 1,\n    fallback: () =>\n      new TextRenderable(rendererInstance, {\n        id: \"sidebar-fallback\",\n        content: \"No sidebar plugin active\",\n        fg: \"#94a3b8\",\n      }),\n    pluginFailurePlaceholder: (failure) => {\n      if (!showPluginFailurePlaceholder) {\n        return undefined\n      }\n\n      return createPluginFailurePlaceholder(rendererInstance, failure, \"#f97316\")\n    },\n  })\n\n  // Insert statusbar before the body\n  rootContainer.add(statusbarSlot, 0)\n  body.add(sidebarSlot)\n  body.add(infoPanel!)\n\n  setClockPluginEnabled(true)\n  setActivityPluginEnabled(true)\n\n  renderer.keyInput.on(\"keypress\", handleKeyPress)\n  updateInfoPanel()\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  rendererInstance.keyInput.off(\"keypress\", handleKeyPress)\n  unregisterPluginErrorListener?.()\n  unregisterPluginErrorListener = null\n\n  unregisterClockPlugin?.()\n  unregisterClockPlugin = null\n  unregisterActivityPlugin?.()\n  unregisterActivityPlugin = null\n\n  // SlotRenderables are destroyed as part of the tree via destroyRecursively\n  statusbarSlot = null\n  sidebarSlot = null\n\n  slotRegistry = null\n\n  rootContainer?.destroyRecursively()\n\n  rootContainer = null\n  infoPanel = null\n  infoText = null\n\n  clockPluginEnabled = false\n  activityPluginEnabled = false\n  statusbarMode = \"append\"\n  orderFlipped = false\n  clockStatusbarErrorEnabled = false\n  clockSidebarErrorEnabled = false\n  activityStatusbarErrorEnabled = false\n  showPluginFailurePlaceholder = true\n  pluginErrorHistory = []\n  placeholderCreateCount = 0\n\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 30,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/diff-demo.ts",
    "content": "import {\n  CliRenderer,\n  createCliRenderer,\n  DiffRenderable,\n  BoxRenderable,\n  TextRenderable,\n  type ParsedKey,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { parseColor, type RGBA } from \"../lib/RGBA.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\n\ninterface DiffTheme {\n  name: string\n  backgroundColor: string\n  borderColor: string\n  addedBg: string\n  removedBg: string\n  contextBg: string\n  addedSignColor: string\n  removedSignColor: string\n  lineNumberFg: string\n  lineNumberBg: string\n  addedLineNumberBg: string\n  removedLineNumberBg: string\n  selectionBg: string\n  selectionFg: string\n  syntaxStyle: {\n    keyword: { fg: RGBA; bold?: boolean }\n    \"keyword.import\": { fg: RGBA; bold?: boolean }\n    string: { fg: RGBA }\n    comment: { fg: RGBA; italic?: boolean }\n    number: { fg: RGBA }\n    boolean: { fg: RGBA }\n    constant: { fg: RGBA }\n    function: { fg: RGBA }\n    \"function.call\": { fg: RGBA }\n    constructor: { fg: RGBA }\n    type: { fg: RGBA }\n    operator: { fg: RGBA }\n    variable: { fg: RGBA }\n    property: { fg: RGBA }\n    bracket: { fg: RGBA }\n    punctuation: { fg: RGBA }\n    default: { fg: RGBA }\n  }\n}\n\nconst themes: DiffTheme[] = [\n  {\n    name: \"GitHub Dark\",\n    backgroundColor: \"#0D1117\",\n    borderColor: \"#4ECDC4\",\n    addedBg: \"#1a4d1a\",\n    removedBg: \"#4d1a1a\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#22c55e\",\n    removedSignColor: \"#ef4444\",\n    lineNumberFg: \"#6b7280\",\n    lineNumberBg: \"#161b22\",\n    addedLineNumberBg: \"#0d3a0d\",\n    removedLineNumberBg: \"#3a0d0d\",\n    selectionBg: \"#264F78\",\n    selectionFg: \"#FFFFFF\",\n    syntaxStyle: {\n      keyword: { fg: parseColor(\"#FF7B72\"), bold: true },\n      \"keyword.import\": { fg: parseColor(\"#FF7B72\"), bold: true },\n      string: { fg: parseColor(\"#A5D6FF\") },\n      comment: { fg: parseColor(\"#8B949E\"), italic: true },\n      number: { fg: parseColor(\"#79C0FF\") },\n      boolean: { fg: parseColor(\"#79C0FF\") },\n      constant: { fg: parseColor(\"#79C0FF\") },\n      function: { fg: parseColor(\"#D2A8FF\") },\n      \"function.call\": { fg: parseColor(\"#D2A8FF\") },\n      constructor: { fg: parseColor(\"#FFA657\") },\n      type: { fg: parseColor(\"#FFA657\") },\n      operator: { fg: parseColor(\"#FF7B72\") },\n      variable: { fg: parseColor(\"#E6EDF3\") },\n      property: { fg: parseColor(\"#79C0FF\") },\n      bracket: { fg: parseColor(\"#F0F6FC\") },\n      punctuation: { fg: parseColor(\"#F0F6FC\") },\n      default: { fg: parseColor(\"#E6EDF3\") },\n    },\n  },\n  {\n    name: \"Monokai\",\n    backgroundColor: \"#272822\",\n    borderColor: \"#FD971F\",\n    addedBg: \"#2d4a2b\",\n    removedBg: \"#4a2b2b\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#A6E22E\",\n    removedSignColor: \"#F92672\",\n    lineNumberFg: \"#75715E\",\n    lineNumberBg: \"#1e1f1c\",\n    addedLineNumberBg: \"#1e3a1e\",\n    removedLineNumberBg: \"#3a1e1e\",\n    selectionBg: \"#49483E\",\n    selectionFg: \"#F8F8F2\",\n    syntaxStyle: {\n      keyword: { fg: parseColor(\"#F92672\"), bold: true },\n      \"keyword.import\": { fg: parseColor(\"#F92672\"), bold: true },\n      string: { fg: parseColor(\"#E6DB74\") },\n      comment: { fg: parseColor(\"#75715E\"), italic: true },\n      number: { fg: parseColor(\"#AE81FF\") },\n      boolean: { fg: parseColor(\"#AE81FF\") },\n      constant: { fg: parseColor(\"#AE81FF\") },\n      function: { fg: parseColor(\"#A6E22E\") },\n      \"function.call\": { fg: parseColor(\"#A6E22E\") },\n      constructor: { fg: parseColor(\"#FD971F\") },\n      type: { fg: parseColor(\"#66D9EF\") },\n      operator: { fg: parseColor(\"#F92672\") },\n      variable: { fg: parseColor(\"#F8F8F2\") },\n      property: { fg: parseColor(\"#66D9EF\") },\n      bracket: { fg: parseColor(\"#F8F8F2\") },\n      punctuation: { fg: parseColor(\"#F8F8F2\") },\n      default: { fg: parseColor(\"#F8F8F2\") },\n    },\n  },\n  {\n    name: \"Dracula\",\n    backgroundColor: \"#282A36\",\n    borderColor: \"#BD93F9\",\n    addedBg: \"#2d4737\",\n    removedBg: \"#4d2d37\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#50FA7B\",\n    removedSignColor: \"#FF5555\",\n    lineNumberFg: \"#6272A4\",\n    lineNumberBg: \"#21222C\",\n    addedLineNumberBg: \"#1f3626\",\n    removedLineNumberBg: \"#3a2328\",\n    selectionBg: \"#44475A\",\n    selectionFg: \"#F8F8F2\",\n    syntaxStyle: {\n      keyword: { fg: parseColor(\"#FF79C6\"), bold: true },\n      \"keyword.import\": { fg: parseColor(\"#FF79C6\"), bold: true },\n      string: { fg: parseColor(\"#F1FA8C\") },\n      comment: { fg: parseColor(\"#6272A4\"), italic: true },\n      number: { fg: parseColor(\"#BD93F9\") },\n      boolean: { fg: parseColor(\"#BD93F9\") },\n      constant: { fg: parseColor(\"#BD93F9\") },\n      function: { fg: parseColor(\"#50FA7B\") },\n      \"function.call\": { fg: parseColor(\"#50FA7B\") },\n      constructor: { fg: parseColor(\"#FFB86C\") },\n      type: { fg: parseColor(\"#8BE9FD\") },\n      operator: { fg: parseColor(\"#FF79C6\") },\n      variable: { fg: parseColor(\"#F8F8F2\") },\n      property: { fg: parseColor(\"#8BE9FD\") },\n      bracket: { fg: parseColor(\"#F8F8F2\") },\n      punctuation: { fg: parseColor(\"#F8F8F2\") },\n      default: { fg: parseColor(\"#F8F8F2\") },\n    },\n  },\n  {\n    name: \"Solarized Dark\",\n    backgroundColor: \"#002b36\", // base03 - official\n    borderColor: \"#2aa198\", // cyan - official\n    addedBg: \"#1a4032\",\n    removedBg: \"#4d2a30\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#859900\", // green - official\n    removedSignColor: \"#dc322f\", // red - official\n    lineNumberFg: \"#586e75\", // base01 - official\n    lineNumberBg: \"#073642\", // base02 - official\n    addedLineNumberBg: \"#0d3326\",\n    removedLineNumberBg: \"#3a2026\",\n    selectionBg: \"#073642\",\n    selectionFg: \"#93a1a1\",\n    syntaxStyle: {\n      keyword: { fg: parseColor(\"#859900\"), bold: true }, // green\n      \"keyword.import\": { fg: parseColor(\"#859900\"), bold: true },\n      string: { fg: parseColor(\"#2aa198\") }, // cyan\n      comment: { fg: parseColor(\"#586e75\"), italic: true }, // base01\n      number: { fg: parseColor(\"#d33682\") }, // magenta\n      boolean: { fg: parseColor(\"#d33682\") },\n      constant: { fg: parseColor(\"#b58900\") }, // yellow\n      function: { fg: parseColor(\"#268bd2\") }, // blue\n      \"function.call\": { fg: parseColor(\"#268bd2\") },\n      constructor: { fg: parseColor(\"#cb4b16\") }, // orange\n      type: { fg: parseColor(\"#cb4b16\") },\n      operator: { fg: parseColor(\"#859900\") },\n      variable: { fg: parseColor(\"#839496\") }, // base0 - official foreground\n      property: { fg: parseColor(\"#268bd2\") },\n      bracket: { fg: parseColor(\"#839496\") }, // base0\n      punctuation: { fg: parseColor(\"#839496\") },\n      default: { fg: parseColor(\"#839496\") }, // base0\n    },\n  },\n  {\n    name: \"One Dark\",\n    backgroundColor: \"#282c34\", // official\n    borderColor: \"#61afef\", // blue - official\n    addedBg: \"#2d4a2d\",\n    removedBg: \"#4d2d2d\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#98c379\", // green - official\n    removedSignColor: \"#e06c75\", // red - official\n    lineNumberFg: \"#636d83\", // gutter - official\n    lineNumberBg: \"#21252b\",\n    addedLineNumberBg: \"#1e3a1e\",\n    removedLineNumberBg: \"#3a1e1e\",\n    selectionBg: \"#3E4451\",\n    selectionFg: \"#abb2bf\",\n    syntaxStyle: {\n      keyword: { fg: parseColor(\"#c678dd\"), bold: true }, // purple - official\n      \"keyword.import\": { fg: parseColor(\"#c678dd\"), bold: true },\n      string: { fg: parseColor(\"#98c379\") }, // green - official\n      comment: { fg: parseColor(\"#5c6370\"), italic: true }, // comment - official\n      number: { fg: parseColor(\"#d19a66\") }, // orange - official\n      boolean: { fg: parseColor(\"#d19a66\") },\n      constant: { fg: parseColor(\"#d19a66\") },\n      function: { fg: parseColor(\"#61afef\") }, // blue - official\n      \"function.call\": { fg: parseColor(\"#61afef\") },\n      constructor: { fg: parseColor(\"#e5c07b\") }, // yellow - official\n      type: { fg: parseColor(\"#e5c07b\") },\n      operator: { fg: parseColor(\"#56b6c2\") }, // cyan - official\n      variable: { fg: parseColor(\"#abb2bf\") }, // foreground - official\n      property: { fg: parseColor(\"#e06c75\") }, // red - official\n      bracket: { fg: parseColor(\"#abb2bf\") },\n      punctuation: { fg: parseColor(\"#abb2bf\") },\n      default: { fg: parseColor(\"#abb2bf\") },\n    },\n  },\n]\n\ninterface ContentExample {\n  name: string\n  filetype: \"typescript\" | \"markdown\" | \"json\"\n  diff: string\n}\n\nconst contentExamples: ContentExample[] = [\n  {\n    name: \"TypeScript\",\n    filetype: \"typescript\",\n    diff: `--- a/calculator.ts\n+++ b/calculator.ts\n@@ -1,13 +1,20 @@\n class Calculator {\n   add(a: number, b: number): number {\n     return a + b;\n   }\n \n-  subtract(a: number, b: number): number {\n-    return a - b;\n+  subtract(a: number, b: number, c: number = 0): number {\n+    return a - b - c;\n   }\n \n   multiply(a: number, b: number): number {\n     return a * b;\n   }\n+\n+  divide(a: number, b: number): number {\n+    if (b === 0) {\n+      throw new Error(\"Division by zero\");\n+    }\n+    return a / b;\n+  }\n }`,\n  },\n  {\n    name: \"Real Session: Text Demo\",\n    filetype: \"typescript\",\n    diff: `Index: packages/core/src/examples/index.ts\n===================================================================\n--- packages/core/src/examples/index.ts\tbefore\n+++ packages/core/src/examples/index.ts\tafter\n@@ -56,6 +56,7 @@\n import * as terminalDemo from \"./terminal.js\"\n import * as diffDemo from \"./diff-demo.js\"\n import * as keypressDebugDemo from \"./keypress-debug-demo.js\"\n+import * as textTruncationDemo from \"./text-truncation-demo.js\"\n import { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n \n interface Example {\n@@ -85,6 +86,12 @@\n     destroy: textSelectionExample.destroy,\n   },\n   {\n+    name: \"Text Truncation Demo\",\n+    description: \"Middle truncation with ellipsis - toggle with 'T' key and resize to test responsive behavior\",\n+    run: textTruncationDemo.run,\n+    destroy: textTruncationDemo.destroy,\n+  },\n+  {\n     name: \"ASCII Font Selection Demo\",\n     description: \"Text selection with ASCII fonts - precise character-level selection across different font types\",\n     run: asciiFontSelectionExample.run,`,\n  },\n  {\n    name: \"Markdown\",\n    filetype: \"markdown\",\n    diff: `--- a/README.md\n+++ b/README.md\n@@ -1,12 +1,21 @@\n # Project Name\n \n-A simple project description.\n+A comprehensive project description with detailed features.\n \n ## Features\n \n-- Feature 1\n-- Feature 2\n+- **Feature 1**: Enhanced with new capabilities\n+- **Feature 2**: Now supports multiple formats\n+- **Feature 3**: Added real-time synchronization\n \n ## Installation\n \n-\\`npm install\\`\n+\\`\\`\\`bash\n+npm install\n+# or\n+yarn install\n+\\`\\`\\`\n+\n+## Usage\n+\n+See the [documentation](./docs) for detailed usage instructions.`,\n  },\n  {\n    name: \"Real Session: Truncate Feature\",\n    filetype: \"typescript\",\n    diff: `Index: packages/core/src/renderables/TextBufferRenderable.ts\n===================================================================\n--- packages/core/src/renderables/TextBufferRenderable.ts\tbefore\n+++ packages/core/src/renderables/TextBufferRenderable.ts\tafter\n@@ -19,6 +19,7 @@\n   wrapMode?: \"none\" | \"char\" | \"word\"\n   tabIndicator?: string | number\n   tabIndicatorColor?: string | RGBA\n+  truncate?: boolean\n }\n \n export abstract class TextBufferRenderable extends Renderable implements LineInfoProvider {\n@@ -35,6 +36,7 @@\n   protected _tabIndicatorColor?: RGBA\n   protected _scrollX: number = 0\n   protected _scrollY: number = 0\n+  protected _truncate: boolean = false\n \n   protected textBuffer: TextBuffer\n   protected textBufferView: TextBufferView`,\n  },\n  {\n    name: \"Markdown (Conceal Test)\",\n    filetype: \"markdown\",\n    diff: `--- a/test.md\n+++ b/test.md\n@@ -1,2 +1,2 @@\n-Some text **boldtext**\n-Short\n+Some text **boldtext**\n+More text **formats**`,\n  },\n  {\n    name: \"JSON\",\n    filetype: \"json\",\n    diff: `--- a/config.json\n+++ b/config.json\n@@ -1,9 +1,15 @@\n {\n-  \"name\": \"my-app\",\n-  \"version\": \"1.0.0\",\n+  \"name\": \"my-awesome-app\",\n+  \"version\": \"2.0.0\",\n   \"config\": {\n-    \"port\": 3000,\n-    \"host\": \"localhost\"\n+    \"port\": 8080,\n+    \"host\": \"0.0.0.0\",\n+    \"ssl\": true,\n+    \"timeout\": 30000\n   },\n-  \"debug\": false\n+  \"debug\": true,\n+  \"features\": {\n+    \"analytics\": true,\n+    \"logging\": \"verbose\"\n+  }\n }`,\n  },\n  {\n    name: \"Real Session: CJK Wrap Test\",\n    filetype: \"typescript\",\n    diff: `Index: packages/core/src/renderables/Text.test.ts\n===================================================================\n--- packages/core/src/renderables/Text.test.ts\tbefore\n+++ packages/core/src/renderables/Text.test.ts\tafter\n@@ -1428,6 +1428,37 @@\n       const frame = captureFrame()\n       expect(frame).toMatchSnapshot()\n     })\n+\n+    it(\"should render word wrapped text with CJK and English correctly\", async () => {\n+      resize(60, 10)\n+\n+      const { text } = await createTextRenderable(currentRenderer, {\n+        content: \"🌟 Unicode test: こんにちは世界 Hello World 你好世界\",\n+        wrapMode: \"word\",\n+        width: 35,\n+        left: 0,\n+        top: 0,\n+      })\n+\n+      await renderOnce()\n+\n+      const frame = captureFrame()\n+      const lines = frame.split(\"\\\\n\").filter((l) => l.trim().length > 0)\n+\n+      console.log(\"Frame:\\\\n\" + frame)\n+      console.log(\"Line 0:\", JSON.stringify(lines[0]))\n+      console.log(\"Line 1:\", JSON.stringify(lines[1]))\n+\n+      // Verify no character duplication - each character should appear only once\n+      const line0 = lines[0] || \"\"\n+      const line1 = lines[1] || \"\"\n+\n+      const line0_ends_with_kai = line0.trimEnd().endsWith(\"界\")\n+      const line1_starts_with_kai = line1.trimStart().startsWith(\"界\")\n+\n+      // \"界\" should not appear on both lines (would indicate duplication bug)\n+      expect(line0_ends_with_kai && line1_starts_with_kai).toBe(false)\n+    })\n   })\n \n   describe(\"Text Node Dimension Updates\", () => {`,\n  },\n]\n\nconst malformedDiff = `--- a/calculator.ts\n+++ b/calculator.ts\n@@ -a,b +c,d @@\n class Calculator {\n   add(a: number, b: number): number {\n     return a + b;\n   }\n \n-  subtract(a: number, b: number): number {\n-    return a - b;\n+  subtract(a: number, b: number, c: number = 0): number {\n+    return a - b - c;\n   }\n }`\n\nlet renderer: CliRenderer | null = null\nlet keyboardHandler: ((key: ParsedKey) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\nlet diffRenderable: DiffRenderable | null = null\nlet instructionsText: TextRenderable | null = null\nlet titleBox: BoxRenderable | null = null\nlet syntaxStyle: SyntaxStyle | null = null\nlet helpModal: BoxRenderable | null = null\nlet currentView: \"unified\" | \"split\" = \"unified\"\nlet showLineNumbers = true\nlet currentWrapMode: \"none\" | \"word\" = \"none\"\nlet currentThemeIndex = 0\nlet currentContentIndex = 0\nlet showMalformedDiff = false\nlet showingHelp = false\nlet concealEnabled = true\n\nconst applyTheme = (themeIndex: number) => {\n  const theme = themes[themeIndex]\n\n  if (renderer) {\n    renderer.setBackgroundColor(theme.backgroundColor)\n  }\n\n  if (titleBox) {\n    titleBox.borderColor = theme.borderColor\n    titleBox.backgroundColor = theme.backgroundColor\n    const contentName = contentExamples[currentContentIndex].name\n    titleBox.title = `Diff Demo - ${theme.name} - ${contentName}`\n  }\n\n  if (helpModal) {\n    helpModal.borderColor = theme.borderColor\n    helpModal.backgroundColor = theme.backgroundColor\n  }\n\n  if (syntaxStyle) {\n    syntaxStyle.destroy()\n  }\n  syntaxStyle = SyntaxStyle.fromStyles(theme.syntaxStyle)\n\n  if (diffRenderable) {\n    diffRenderable.syntaxStyle = syntaxStyle\n    diffRenderable.addedBg = theme.addedBg\n    diffRenderable.removedBg = theme.removedBg\n    diffRenderable.contextBg = theme.contextBg\n    diffRenderable.addedSignColor = theme.addedSignColor\n    diffRenderable.removedSignColor = theme.removedSignColor\n    diffRenderable.lineNumberFg = theme.lineNumberFg\n    diffRenderable.lineNumberBg = theme.lineNumberBg\n    diffRenderable.addedLineNumberBg = theme.addedLineNumberBg\n    diffRenderable.removedLineNumberBg = theme.removedLineNumberBg\n    diffRenderable.selectionBg = theme.selectionBg\n    diffRenderable.selectionFg = theme.selectionFg\n  }\n}\n\nexport async function run(rendererInstance: CliRenderer): Promise<void> {\n  renderer = rendererInstance\n  renderer.start()\n\n  const theme = themes[currentThemeIndex]\n  renderer.setBackgroundColor(theme.backgroundColor)\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n    padding: 1,\n  })\n  renderer.root.add(parentContainer)\n\n  titleBox = new BoxRenderable(renderer, {\n    id: \"title-box\",\n    height: 3,\n    borderStyle: \"double\",\n    borderColor: theme.borderColor,\n    backgroundColor: theme.backgroundColor,\n    title: `Diff Demo - ${theme.name} - ${contentExamples[currentContentIndex].name}`,\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentContainer.add(titleBox)\n\n  instructionsText = new TextRenderable(renderer, {\n    id: \"instructions\",\n    content: \"ESC to return | Press ? for keybindings\",\n    fg: \"#888888\",\n  })\n  titleBox.add(instructionsText)\n\n  // Create help modal (hidden by default)\n  helpModal = new BoxRenderable(renderer, {\n    id: \"help-modal\",\n    position: \"absolute\",\n    left: \"10%\",\n    top: \"10%\",\n    width: \"80%\",\n    height: \"80%\",\n    border: true,\n    borderStyle: \"double\",\n    borderColor: theme.borderColor,\n    backgroundColor: theme.backgroundColor,\n    title: \"Keybindings\",\n    titleAlignment: \"center\",\n    padding: 2,\n    zIndex: 100,\n    visible: false,\n  })\n\n  const helpContent = new TextRenderable(renderer, {\n    id: \"help-content\",\n    content: `View Controls:\n  V : Toggle view mode (Unified/Split)\n  L : Toggle line numbers\n  W : Toggle wrap mode (None/Word)\n  O : Toggle conceal (hide/show markup)\n\nTheme & Content:\n  T : Cycle through themes (5 themes)\n  C : Cycle through diff examples (6 examples)\n  M : Toggle malformed diff example\n\nOther:\n  ? : Toggle this help screen\n  ESC : Return to main menu`,\n    fg: \"#E6EDF3\",\n  })\n\n  helpModal.add(helpContent)\n  renderer.root.add(helpModal)\n\n  syntaxStyle = SyntaxStyle.fromStyles(theme.syntaxStyle)\n\n  // Create diff display\n  const currentContent = contentExamples[currentContentIndex]\n  diffRenderable = new DiffRenderable(renderer, {\n    id: \"diff-display\",\n    diff: currentContent.diff,\n    view: currentView,\n    filetype: currentContent.filetype,\n    syntaxStyle,\n    showLineNumbers,\n    wrapMode: currentWrapMode,\n    conceal: concealEnabled,\n    addedBg: theme.addedBg,\n    removedBg: theme.removedBg,\n    contextBg: theme.contextBg,\n    addedSignColor: theme.addedSignColor,\n    removedSignColor: theme.removedSignColor,\n    lineNumberFg: theme.lineNumberFg,\n    lineNumberBg: theme.lineNumberBg,\n    addedLineNumberBg: theme.addedLineNumberBg,\n    removedLineNumberBg: theme.removedLineNumberBg,\n    selectionBg: theme.selectionBg,\n    selectionFg: theme.selectionFg,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  parentContainer.add(diffRenderable)\n\n  keyboardHandler = (key: ParsedKey) => {\n    // Handle help modal toggle\n    if (key.raw === \"?\" && helpModal) {\n      showingHelp = !showingHelp\n      helpModal.visible = showingHelp\n      return\n    }\n\n    // Don't process other keys when help is showing\n    if (showingHelp) return\n\n    if (key.name === \"v\" && !key.ctrl && !key.meta) {\n      // Toggle view mode\n      currentView = currentView === \"unified\" ? \"split\" : \"unified\"\n      if (diffRenderable) {\n        diffRenderable.view = currentView\n      }\n    } else if (key.name === \"l\" && !key.ctrl && !key.meta) {\n      // Toggle line numbers\n      showLineNumbers = !showLineNumbers\n      if (diffRenderable) {\n        diffRenderable.showLineNumbers = showLineNumbers\n      }\n    } else if (key.name === \"w\" && !key.ctrl && !key.meta) {\n      // Toggle wrap mode\n      currentWrapMode = currentWrapMode === \"none\" ? \"word\" : \"none\"\n      if (diffRenderable) {\n        diffRenderable.wrapMode = currentWrapMode\n      }\n    } else if (key.name === \"t\" && !key.ctrl && !key.meta) {\n      // Change theme\n      currentThemeIndex = (currentThemeIndex + 1) % themes.length\n      applyTheme(currentThemeIndex)\n    } else if (key.name === \"m\" && !key.ctrl && !key.meta) {\n      // Toggle malformed diff\n      showMalformedDiff = !showMalformedDiff\n      if (diffRenderable) {\n        diffRenderable.diff = showMalformedDiff ? malformedDiff : contentExamples[currentContentIndex].diff\n      }\n    } else if (key.name === \"c\" && !key.ctrl && !key.meta) {\n      // Cycle through content types\n      currentContentIndex = (currentContentIndex + 1) % contentExamples.length\n      if (diffRenderable) {\n        const currentContent = contentExamples[currentContentIndex]\n        diffRenderable.diff = showMalformedDiff ? malformedDiff : currentContent.diff\n        diffRenderable.filetype = currentContent.filetype\n      }\n      if (titleBox) {\n        const theme = themes[currentThemeIndex]\n        const contentName = contentExamples[currentContentIndex].name\n        titleBox.title = `Diff Demo - ${theme.name} - ${contentName}`\n      }\n    } else if (key.name === \"o\" && !key.ctrl && !key.meta) {\n      // Toggle conceal\n      concealEnabled = !concealEnabled\n      if (diffRenderable) {\n        diffRenderable.conceal = concealEnabled\n      }\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  parentContainer?.destroy()\n  helpModal?.destroy()\n  parentContainer = null\n  diffRenderable = null\n  instructionsText = null\n  titleBox = null\n  syntaxStyle = null\n  helpModal = null\n\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/draggable-three-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  BoxRenderable,\n  TextRenderable,\n  type KeyEvent,\n  type MouseEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport {\n  Scene as ThreeScene,\n  Mesh as ThreeMesh,\n  PerspectiveCamera,\n  Color,\n  DirectionalLight,\n  AmbientLight,\n  BoxGeometry,\n  MeshPhongMaterial,\n  Vector3,\n} from \"three\"\nimport { ThreeRenderable } from \"../3d.js\"\n\nlet nextZIndex = 200\nlet keyListener: ((key: KeyEvent) => void) | null = null\nlet resizeListener: ((width: number, height: number) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\nlet draggableCube: DraggableThreeRenderable | null = null\n\nconst HEADER_HEIGHT = 4\n\nclass DraggableThreeRenderable extends ThreeRenderable {\n  private isDragging = false\n  private dragOffsetX = 0\n  private dragOffsetY = 0\n  private dragBoundsTop: number\n\n  constructor(ctx: CliRenderer, dragBoundsTop: number, options: ConstructorParameters<typeof ThreeRenderable>[1]) {\n    super(ctx, options)\n    this.dragBoundsTop = dragBoundsTop\n  }\n\n  public setDragBoundsTop(top: number): void {\n    this.dragBoundsTop = top\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    switch (event.type) {\n      case \"down\":\n        this.isDragging = true\n        this.dragOffsetX = event.x - this.x\n        this.dragOffsetY = event.y - this.y\n        this.zIndex = nextZIndex++\n        event.stopPropagation()\n        break\n      case \"drag\":\n        if (!this.isDragging) return\n        this.updateDragPosition(event.x, event.y)\n        event.stopPropagation()\n        break\n      case \"drag-end\":\n        if (this.isDragging) {\n          this.isDragging = false\n          event.stopPropagation()\n        }\n        break\n    }\n  }\n\n  private updateDragPosition(pointerX: number, pointerY: number): void {\n    const newX = pointerX - this.dragOffsetX\n    const newY = pointerY - this.dragOffsetY\n    const maxX = this._ctx.width - this.width\n    const maxY = this._ctx.height - this.height\n\n    this.x = Math.max(0, Math.min(newX, maxX))\n    this.y = Math.max(this.dragBoundsTop, Math.min(newY, maxY))\n  }\n}\n\nfunction getRenderSize(width: number, height: number): { width: number; height: number } {\n  return {\n    width: Math.max(24, Math.min(64, Math.floor(width * 0.55))),\n    height: Math.max(12, Math.min(28, Math.floor(height * 0.55))),\n  }\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  renderer.setBackgroundColor(\"#0A0E14\")\n\n  const width = renderer.terminalWidth\n  const height = renderer.terminalHeight\n  const size = getRenderSize(width, height)\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"draggable-three-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  const titleText = new TextRenderable(renderer, {\n    id: \"draggable-three-title\",\n    content: \"Draggable ThreeRenderable - rotating cube (drag with mouse)\",\n    position: \"absolute\",\n    left: 2,\n    top: 1,\n    fg: \"#E2E8F0\",\n    zIndex: 20,\n  })\n  parentContainer.add(titleText)\n\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"draggable-three-instructions\",\n    content: \"Space: toggle rotation | P: screenshot | Esc: return\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    fg: \"#94A3B8\",\n    zIndex: 20,\n  })\n  parentContainer.add(instructionsText)\n\n  const controlsText = new TextRenderable(renderer, {\n    id: \"draggable-three-controls\",\n    content: \"Drag the cube to see transparency and live rendering\",\n    position: \"absolute\",\n    left: 2,\n    top: height - 2,\n    fg: \"#CBD5F5\",\n    zIndex: 20,\n  })\n  parentContainer.add(controlsText)\n\n  const sceneRoot = new ThreeScene()\n\n  const ambientLight = new AmbientLight(new Color(0.35, 0.35, 0.35), 1.0)\n  sceneRoot.add(ambientLight)\n\n  const keyLight = new DirectionalLight(new Color(1.0, 0.95, 0.9), 1.2)\n  keyLight.position.set(2.5, 2.0, 3.0)\n  sceneRoot.add(keyLight)\n\n  const fillLight = new DirectionalLight(new Color(0.5, 0.7, 1.0), 0.6)\n  fillLight.position.set(-2.0, -1.5, 2.5)\n  sceneRoot.add(fillLight)\n\n  const cubeGeometry = new BoxGeometry(1.0, 1.0, 1.0)\n  const cubeMaterial = new MeshPhongMaterial({\n    color: new Color(0.25, 0.8, 1.0),\n    shininess: 80,\n    specular: new Color(0.9, 0.9, 1.0),\n  })\n  const cubeMesh = new ThreeMesh(cubeGeometry, cubeMaterial)\n  cubeMesh.name = \"cube\"\n  sceneRoot.add(cubeMesh)\n\n  const cameraNode = new PerspectiveCamera(45, 1, 0.1, 100)\n  cameraNode.position.set(0, 0, 3)\n  cameraNode.name = \"main_camera\"\n\n  const startX = Math.max(2, Math.floor((width - size.width) / 2))\n  const startY = Math.max(HEADER_HEIGHT, Math.floor((height - size.height) / 2))\n\n  draggableCube = new DraggableThreeRenderable(renderer, HEADER_HEIGHT, {\n    id: \"draggable-three\",\n    width: size.width,\n    height: size.height,\n    position: \"absolute\",\n    left: startX,\n    top: startY,\n    zIndex: 50,\n    scene: sceneRoot,\n    camera: cameraNode,\n    renderer: {\n      focalLength: 8,\n      alpha: true,\n      backgroundColor: RGBA.fromValues(0, 0, 0, 0),\n    },\n  })\n  renderer.root.add(draggableCube)\n\n  const rotationSpeed = new Vector3(0.6, 0.4, 0.2)\n  let rotationEnabled = true\n\n  renderer.setFrameCallback(async (deltaMs) => {\n    const deltaTime = deltaMs / 1000\n    if (!rotationEnabled) return\n    cubeMesh.rotation.x += rotationSpeed.x * deltaTime\n    cubeMesh.rotation.y += rotationSpeed.y * deltaTime\n    cubeMesh.rotation.z += rotationSpeed.z * deltaTime\n  })\n\n  resizeListener = (newWidth: number, newHeight: number) => {\n    controlsText.y = newHeight - 2\n\n    if (!draggableCube) return\n\n    const nextSize = getRenderSize(newWidth, newHeight)\n    draggableCube.width = nextSize.width\n    draggableCube.height = nextSize.height\n    draggableCube.setDragBoundsTop(HEADER_HEIGHT)\n\n    const maxX = newWidth - draggableCube.width\n    const maxY = newHeight - draggableCube.height\n    draggableCube.x = Math.max(0, Math.min(draggableCube.x, maxX))\n    draggableCube.y = Math.max(HEADER_HEIGHT, Math.min(draggableCube.y, maxY))\n  }\n\n  renderer.on(\"resize\", resizeListener)\n\n  keyListener = (key: KeyEvent) => {\n    if (key.name === \"p\" && draggableCube) {\n      draggableCube.renderer.saveToFile(`screenshot-${Date.now()}.png`)\n    }\n\n    if (key.name === \"space\") {\n      rotationEnabled = !rotationEnabled\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", keyListener)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n\n  if (resizeListener) {\n    renderer.off(\"resize\", resizeListener)\n    resizeListener = null\n  }\n\n  if (keyListener) {\n    renderer.keyInput.off(\"keypress\", keyListener)\n    keyListener = null\n  }\n\n  if (draggableCube) {\n    draggableCube.destroy()\n    draggableCube = null\n  }\n\n  if (parentContainer) {\n    renderer.root.remove(\"draggable-three-container\")\n    parentContainer = null\n  }\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/editor-demo.ts",
    "content": "import {\n  CliRenderer,\n  createCliRenderer,\n  TextareaRenderable,\n  BoxRenderable,\n  TextRenderable,\n  LineNumberRenderable,\n  KeyEvent,\n  t,\n  bold,\n  cyan,\n  fg,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nconst initialContent = `Welcome to the TextareaRenderable Demo!\n\nThis is an interactive text editor powered by EditBuffer and EditorView.\n\n\\tThis is a tab\n\\t\\t\\tMultiple tabs\n\nEmojis:\n👩🏽‍💻  👨‍👩‍👧‍👦  🏳️‍🌈  🇺🇸  🇩🇪  🇯🇵  🇮🇳\n\nNAVIGATION:\n  • Arrow keys to move cursor\n  • Ctrl+A/Ctrl+E for line start/end\n  • Home/End for buffer start/end\n  • Ctrl+F/Ctrl+B to move right/left (Emacs-style)\n  • Alt+F/Alt+B for word forward/backward\n  • Alt+Left/Alt+Right for word forward/backward\n  • Ctrl+Left/Ctrl+Right for word forward/backward\n  • Alt+A/Alt+E for visual line start/end\n\nSELECTION:\n  • Shift+Arrow keys to select\n  • Ctrl+Shift+A/E to select to line start/end\n  • Shift+Home/End to select to buffer start/end\n  • Alt+Shift+F/B to select word forward/backward\n  • Alt+Shift+Left/Right to select word forward/backward\n  • Alt+Shift+A/E to select to visual line start/end\n\nEDITING:\n  • Type any text to insert\n  • Backspace/Delete to remove text\n  • Enter to create new lines\n  • Ctrl+Shift+D to delete current line\n  • Ctrl+D to delete character forward\n  • Ctrl+K to delete to line end\n  • Ctrl+U to delete to line start\n  • Alt+D to delete word forward\n  • Alt+Backspace or Ctrl+W to delete word backward\n  • Ctrl+Delete or Alt+Delete to delete word forward\n\nUNDO/REDO:\n  • Ctrl+- to undo or Cmd+Z (Mac)\n  • Ctrl+. to redo or Cmd+Shift+Z (Mac)\n\nVIEW:\n  • Shift+W to toggle wrap mode (word/char/none)\n  • Shift+L to toggle line numbers\n  • Shift+H to toggle diff highlights (colors + +/- signs)\n  • Shift+D to toggle diagnostics (error/warning/info emojis)\n  • Ctrl+] to increase scroll speed\n  • Ctrl+[ to decrease scroll speed\n\nFEATURES:\n  ✓ Grapheme-aware cursor movement\n  ✓ Unicode (emoji 🌟 and CJK 世界, 你好世界, 中文, 한글)\n  ✓ Incremental editing\n  ✓ Text wrapping and viewport management\n  ✓ Undo/redo support\n  ✓ Word-based navigation and deletion\n  ✓ Text selection with shift keys\n\nPress ESC to return to main menu`\n\nlet renderer: CliRenderer | null = null\nlet parentContainer: BoxRenderable | null = null\nlet editor: TextareaRenderable | null = null\nlet editorWithLines: LineNumberRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet highlightsEnabled: boolean = false\nlet diagnosticsEnabled: boolean = false\n\nexport async function run(rendererInstance: CliRenderer): Promise<void> {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#0D1117\")\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n    padding: 1,\n  })\n  renderer.root.add(parentContainer)\n\n  const editorBox = new BoxRenderable(renderer, {\n    id: \"editor-box\",\n    borderStyle: \"single\",\n    borderColor: \"#6BCF7F\",\n    backgroundColor: \"#0D1117\",\n    title: \"Interactive Editor (TextareaRenderable)\",\n    titleAlignment: \"left\",\n    border: true,\n  })\n  parentContainer.add(editorBox)\n\n  // Create interactive editor\n  editor = new TextareaRenderable(renderer, {\n    id: \"editor\",\n    initialValue: initialContent,\n    textColor: \"#F0F6FC\",\n    selectionBg: \"#264F78\",\n    selectionFg: \"#FFFFFF\",\n    wrapMode: \"word\",\n    showCursor: true,\n    cursorColor: \"#4ECDC4\",\n    placeholder: t`${fg(\"#333333\")(\"Enter\")} ${cyan(bold(\"text\"))} ${fg(\"#333333\")(\"here...\")}`,\n    tabIndicator: \"→\",\n    tabIndicatorColor: \"#30363D\",\n  })\n\n  editorWithLines = new LineNumberRenderable(renderer, {\n    id: \"editor-lines\",\n    target: editor,\n    minWidth: 3,\n    paddingRight: 1,\n    fg: \"#6b7280\", // Dimmed gray for line numbers\n    bg: \"#161b22\", // Slightly darker than editor background for distinction\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  editorBox.add(editorWithLines)\n\n  statusText = new TextRenderable(renderer, {\n    id: \"status\",\n    content: \"\",\n    fg: \"#A5D6FF\",\n    height: 1,\n  })\n  parentContainer.add(statusText)\n\n  editor.focus()\n\n  rendererInstance.setFrameCallback(async () => {\n    if (statusText && editor && !editor.isDestroyed) {\n      try {\n        const cursor = editor.logicalCursor\n        const wrap = editor.wrapMode !== \"none\" ? \"ON\" : \"OFF\"\n        const highlights = highlightsEnabled ? \"ON\" : \"OFF\"\n        const diagnostics = diagnosticsEnabled ? \"ON\" : \"OFF\"\n        const scrollSpeed = editor.scrollSpeed\n        statusText.content = `Line ${cursor.row + 1}, Col ${cursor.col + 1} | Wrap: ${wrap} | Diff: ${highlights} | Diag: ${diagnostics} | Scroll: ${scrollSpeed} lines/s`\n      } catch (error) {\n        // Ignore errors during shutdown\n      }\n    }\n  })\n\n  rendererInstance.keyInput.on(\"keypress\", (key: KeyEvent) => {\n    if (key.shift && key.name === \"l\") {\n      key.preventDefault()\n      if (editorWithLines && !editorWithLines.isDestroyed) {\n        editorWithLines.showLineNumbers = !editorWithLines.showLineNumbers\n      }\n    }\n    if (key.shift && key.name === \"w\") {\n      key.preventDefault()\n      if (editor && !editor.isDestroyed) {\n        const currentMode = editor.wrapMode\n        const nextMode = currentMode === \"word\" ? \"char\" : currentMode === \"char\" ? \"none\" : \"word\"\n        editor.wrapMode = nextMode\n      }\n    }\n    if (key.shift && key.name === \"h\") {\n      key.preventDefault()\n      if (editorWithLines && !editorWithLines.isDestroyed) {\n        highlightsEnabled = !highlightsEnabled\n        if (highlightsEnabled) {\n          // Add modern diff-style line colors and +/- signs throughout the document\n          editorWithLines.setLineColor(2, \"#1a4d1a\") // Line 3: Added (fresh green)\n          editorWithLines.setLineSign(2, { after: \" +\", afterColor: \"#22c55e\" })\n\n          editorWithLines.setLineColor(5, \"#4d1a1a\") // Line 6: Removed (vibrant red)\n          editorWithLines.setLineSign(5, { after: \" -\", afterColor: \"#ef4444\" })\n\n          editorWithLines.setLineColor(8, \"#1a4d1a\") // Line 9: Added (fresh green)\n          editorWithLines.setLineSign(8, { after: \" +\", afterColor: \"#22c55e\" })\n\n          editorWithLines.setLineColor(11, \"#4d1a1a\") // Line 12: Removed (vibrant red)\n          editorWithLines.setLineSign(11, { after: \" -\", afterColor: \"#ef4444\" })\n\n          editorWithLines.setLineColor(14, \"#1a4d1a\") // Line 15: Added (fresh green)\n          editorWithLines.setLineSign(14, { after: \" +\", afterColor: \"#22c55e\" })\n\n          editorWithLines.setLineColor(17, \"#4d1a1a\") // Line 18: Removed (vibrant red)\n          editorWithLines.setLineSign(17, { after: \" -\", afterColor: \"#ef4444\" })\n\n          editorWithLines.setLineColor(20, \"#1a4d1a\") // Line 21: Added (fresh green)\n          editorWithLines.setLineSign(20, { after: \" +\", afterColor: \"#22c55e\" })\n\n          editorWithLines.setLineColor(23, \"#4d1a1a\") // Line 24: Removed (vibrant red)\n          editorWithLines.setLineSign(23, { after: \" -\", afterColor: \"#ef4444\" })\n\n          editorWithLines.setLineColor(27, \"#1a4d1a\") // Line 28: Added (fresh green)\n          editorWithLines.setLineSign(27, { after: \" +\", afterColor: \"#22c55e\" })\n\n          editorWithLines.setLineColor(30, \"#4d1a1a\") // Line 31: Removed (vibrant red)\n          editorWithLines.setLineSign(30, { after: \" -\", afterColor: \"#ef4444\" })\n\n          editorWithLines.setLineColor(34, \"#1a4d1a\") // Line 35: Added (fresh green)\n          editorWithLines.setLineSign(34, { after: \" +\", afterColor: \"#22c55e\" })\n\n          editorWithLines.setLineColor(38, \"#4d1a1a\") // Line 39: Removed (vibrant red)\n          editorWithLines.setLineSign(38, { after: \" -\", afterColor: \"#ef4444\" })\n\n          editorWithLines.setLineColor(42, \"#1a4d1a\") // Line 43: Added (fresh green)\n          editorWithLines.setLineSign(42, { after: \" +\", afterColor: \"#22c55e\" })\n\n          editorWithLines.setLineColor(46, \"#4d1a1a\") // Line 47: Removed (vibrant red)\n          editorWithLines.setLineSign(46, { after: \" -\", afterColor: \"#ef4444\" })\n\n          editorWithLines.setLineColor(50, \"#1a4d1a\") // Line 51: Added (fresh green)\n          editorWithLines.setLineSign(50, { after: \" +\", afterColor: \"#22c55e\" })\n\n          editorWithLines.setLineColor(54, \"#4d1a1a\") // Line 55: Removed (vibrant red)\n          editorWithLines.setLineSign(54, { after: \" -\", afterColor: \"#ef4444\" })\n\n          editorWithLines.setLineColor(58, \"#1a4d1a\") // Line 59: Added (fresh green)\n          editorWithLines.setLineSign(58, { after: \" +\", afterColor: \"#22c55e\" })\n        } else {\n          editorWithLines.clearAllLineColors()\n          // Clear only the after signs (keep diagnostics if enabled)\n          const currentSigns = editorWithLines.getLineSigns()\n          for (const [line, sign] of currentSigns) {\n            if (sign.after) {\n              if (sign.before) {\n                // Keep the before sign, remove only after\n                editorWithLines.setLineSign(line, { before: sign.before, beforeColor: sign.beforeColor })\n              } else {\n                // No before sign, remove entirely\n                editorWithLines.clearLineSign(line)\n              }\n            }\n          }\n        }\n      }\n    }\n    if (key.shift && key.name === \"d\") {\n      key.preventDefault()\n      if (editorWithLines && !editorWithLines.isDestroyed) {\n        diagnosticsEnabled = !diagnosticsEnabled\n        if (diagnosticsEnabled) {\n          // Add diagnostic signs (errors, warnings, info) on some lines\n          editorWithLines.setLineSign(0, { before: \"❌\", beforeColor: \"#ef4444\" }) // Line 1: Error\n          editorWithLines.setLineSign(4, { before: \"⚠️\", beforeColor: \"#f59e0b\" }) // Line 5: Warning\n          editorWithLines.setLineSign(10, { before: \"💡\", beforeColor: \"#3b82f6\" }) // Line 11: Info\n          editorWithLines.setLineSign(25, { before: \"❌\", beforeColor: \"#ef4444\" }) // Line 26: Error\n          editorWithLines.setLineSign(40, { before: \"⚠️\", beforeColor: \"#f59e0b\" }) // Line 41: Warning\n          editorWithLines.setLineSign(52, { before: \"💡\", beforeColor: \"#3b82f6\" }) // Line 53: Info\n        } else {\n          // Clear only the before signs (keep diff signs if enabled)\n          const currentSigns = editorWithLines.getLineSigns()\n          for (const [line, sign] of currentSigns) {\n            if (sign.before) {\n              if (sign.after) {\n                // Keep the after sign, remove only before\n                editorWithLines.setLineSign(line, { after: sign.after, afterColor: sign.afterColor })\n              } else {\n                // No after sign, remove entirely\n                editorWithLines.clearLineSign(line)\n              }\n            }\n          }\n        }\n      }\n    }\n    if (key.ctrl && (key.name === \"pageup\" || key.name === \"pagedown\")) {\n      key.preventDefault()\n      if (editor && !editor.isDestroyed) {\n        if (key.name === \"pageup\") {\n          editor.editBuffer.setCursor(0, 0)\n        } else {\n          editor.gotoBufferEnd()\n        }\n      }\n    }\n    if (key.ctrl && key.name === \"]\") {\n      key.preventDefault()\n      if (editor && !editor.isDestroyed) {\n        editor.scrollSpeed = Math.min(100, editor.scrollSpeed + 4)\n      }\n    }\n    if (key.ctrl && key.name === \"[\") {\n      key.preventDefault()\n      if (editor && !editor.isDestroyed) {\n        editor.scrollSpeed = Math.max(4, editor.scrollSpeed - 4)\n      }\n    }\n  })\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  rendererInstance.clearFrameCallbacks()\n  parentContainer?.destroy()\n  parentContainer = null\n  editorWithLines = null\n  editor = null\n  statusText = null\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/extmarks-demo.ts",
    "content": "import {\n  CliRenderer,\n  createCliRenderer,\n  TextareaRenderable,\n  BoxRenderable,\n  TextRenderable,\n  KeyEvent,\n  type ExtmarksController,\n  type ExtmarkDeletedEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\n\nconst initialContent = `Welcome to the Extmarks Demo!\n\nThis demo showcases virtual extmarks - text ranges that the cursor jumps over.\n\nTry moving your cursor through the [VIRTUAL] markers below:\n- Use arrow keys to navigate\n- Notice how the cursor skips over [VIRTUAL] ranges\n- Try backspacing at the end of a [VIRTUAL] marker\n- It will delete the entire marker!\n\nExample text with [LINK:https://example.com] embedded links.\nYou can also have [TAG:important] tags that act like atoms.\n\nRegular text here can be edited normally.\n\nPress Ctrl+L to add a new [MARKER] at cursor position.\nPress ESC to return to main menu.`\n\nlet renderer: CliRenderer | null = null\nlet parentContainer: BoxRenderable | null = null\nlet editor: TextareaRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet helpText: TextRenderable | null = null\nlet extmarksController: ExtmarksController | null = null\nlet syntaxStyle: SyntaxStyle | null = null\nlet virtualStyleId: number = 0\n\nexport async function run(rendererInstance: CliRenderer): Promise<void> {\n  renderer = rendererInstance\n  renderer.start()\n  renderer.setBackgroundColor(\"#0D1117\")\n\n  syntaxStyle = SyntaxStyle.create()\n  virtualStyleId = syntaxStyle.registerStyle(\"virtual\", {\n    fg: RGBA.fromValues(0.3, 0.7, 1.0, 1.0),\n    bg: RGBA.fromValues(0.1, 0.2, 0.3, 1.0),\n  })\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n    padding: 1,\n  })\n  renderer.root.add(parentContainer)\n\n  const editorBox = new BoxRenderable(renderer, {\n    id: \"editor-box\",\n    borderStyle: \"single\",\n    borderColor: \"#6BCF7F\",\n    backgroundColor: \"#0D1117\",\n    title: \"Extmarks Demo - Virtual Text Ranges\",\n    titleAlignment: \"left\",\n    paddingLeft: 1,\n    paddingRight: 1,\n    border: true,\n  })\n  parentContainer.add(editorBox)\n\n  editor = new TextareaRenderable(renderer, {\n    id: \"editor\",\n    initialValue: initialContent,\n    textColor: \"#F0F6FC\",\n    selectionBg: \"#264F78\",\n    selectionFg: \"#FFFFFF\",\n    wrapMode: \"word\",\n    showCursor: true,\n    cursorColor: \"#4ECDC4\",\n    syntaxStyle,\n  })\n  editorBox.add(editor)\n\n  extmarksController = editor.extmarks\n  if (!extmarksController) {\n    throw new Error(\"Failed to create extmarks controller\")\n  }\n\n  findAndMarkVirtualRanges()\n\n  extmarksController.on(\"extmark-deleted\", (event: ExtmarkDeletedEvent) => {\n    if (helpText) {\n      const extmark = event.extmark\n      helpText.content = `Deleted extmark at ${extmark.start}-${extmark.end} via ${event.trigger}`\n    }\n  })\n\n  helpText = new TextRenderable(renderer, {\n    id: \"help\",\n    content: \"Move cursor with arrows. Try backspacing at end of [VIRTUAL] markers!\",\n    fg: \"#FFA657\",\n    height: 1,\n  })\n  parentContainer.add(helpText)\n\n  statusText = new TextRenderable(renderer, {\n    id: \"status\",\n    content: \"\",\n    fg: \"#A5D6FF\",\n    height: 1,\n  })\n  parentContainer.add(statusText)\n\n  editor.focus()\n\n  rendererInstance.setFrameCallback(async () => {\n    if (statusText && editor && !editor.isDestroyed && extmarksController) {\n      try {\n        const cursor = editor.logicalCursor\n        const offset = editor.cursorOffset\n        const extmarksAtCursor = extmarksController.getAtOffset(offset)\n        const virtualCount = extmarksController.getVirtual().length\n\n        let extmarkInfo = \"\"\n        if (extmarksAtCursor.length > 0) {\n          extmarkInfo = ` | Inside extmark(s): ${extmarksAtCursor.length}`\n        }\n\n        statusText.content = `Line ${cursor.row + 1}, Col ${cursor.col + 1}, Offset ${offset} | Virtual extmarks: ${virtualCount}${extmarkInfo}`\n      } catch (error) {\n        // Ignore errors during shutdown\n      }\n    }\n  })\n\n  rendererInstance.keyInput.on(\"keypress\", (key: KeyEvent) => {\n    if (key.ctrl && key.name === \"l\") {\n      key.preventDefault()\n      if (editor && !editor.isDestroyed && extmarksController) {\n        const offset = editor.cursorOffset\n        const markerText = \"[MARKER]\"\n        editor.insertText(markerText)\n\n        extmarksController.create({\n          start: offset,\n          end: offset + markerText.length,\n          virtual: true,\n          styleId: virtualStyleId,\n          data: { type: \"marker\", added: \"manual\" },\n        })\n\n        if (helpText) {\n          helpText.content = `Added virtual marker at offset ${offset}!`\n        }\n      }\n    }\n  })\n}\n\nfunction findAndMarkVirtualRanges(): void {\n  if (!editor || !extmarksController) return\n\n  const text = editor.plainText\n  const pattern = /\\[(VIRTUAL|LINK:[^\\]]+|TAG:[^\\]]+|MARKER)\\]/g\n  let match: RegExpExecArray | null\n\n  while ((match = pattern.exec(text)) !== null) {\n    const start = match.index\n    const end = match.index + match[0].length\n\n    extmarksController.create({\n      start,\n      end,\n      virtual: true,\n      styleId: virtualStyleId,\n      data: { type: \"auto-detected\", content: match[0] },\n    })\n  }\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  rendererInstance.clearFrameCallbacks()\n  extmarksController?.destroy()\n  extmarksController = null\n  syntaxStyle?.destroy()\n  syntaxStyle = null\n  parentContainer?.destroy()\n  parentContainer = null\n  editor = null\n  statusText = null\n  helpText = null\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/focus-restore-demo.ts",
    "content": "#!/usr/bin/env bun\n\n// Interactive demo to test the focus restore fix on Windows Terminal.\n//\n// How to test:\n//   1. Run from the example selector, or: bun src/examples/focus-restore-demo.ts\n//   2. Move the mouse around - you should see the mouse position update live\n//   3. Alt-tab away from the terminal, then alt-tab back\n//   4. Move the mouse again - if the fix works, mouse tracking resumes immediately\n//   5. Try minimizing and restoring the window too\n//   6. Press Escape to return to menu, Ctrl+C to exit\n\nimport {\n  type CliRenderer,\n  createCliRenderer,\n  BoxRenderable,\n  TextRenderable,\n  RGBA,\n  TextAttributes,\n  type MouseEvent,\n} from \"../index\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys\"\n\nlet container: BoxRenderable | null = null\nlet mouseArea: BoxRenderable | null = null\n\nlet mouseX = 0\nlet mouseY = 0\nlet mouseEvents = 0\nlet focusCount = 0\nlet blurCount = 0\nlet restoreCount = 0\nlet lastFocusTime = \"\"\nlet lastBlurTime = \"\"\nlet lastMouseTime = \"\"\nlet focused = true\nlet originalRestore: any = null\nlet focusHandler: (() => void) | null = null\nlet blurHandler: (() => void) | null = null\n\n// Log storage\nconst logEntries: Array<{ text: string; color: RGBA }> = []\nconst maxLogEntries = 20\n\n// Renderable references for updates\nlet focusStatus: TextRenderable | null = null\nlet mouseStatus: TextRenderable | null = null\nlet countersStatus: TextRenderable | null = null\nlet timestampStatus: TextRenderable | null = null\nlet logBox: BoxRenderable | null = null\nconst logRenderables: TextRenderable[] = []\n\nfunction ts(): string {\n  return new Date().toLocaleTimeString(\"en-US\", { hour12: false })\n}\n\nfunction addLogLine(renderer: CliRenderer, text: string, color: RGBA) {\n  if (!logBox) return\n\n  logEntries.push({ text, color })\n  while (logEntries.length > maxLogEntries) {\n    logEntries.shift()\n  }\n\n  // Remove old renderables\n  for (const r of logRenderables) {\n    logBox.remove(r.id)\n    r.destroy()\n  }\n  logRenderables.length = 0\n\n  // Rebuild from entries\n  for (let i = 0; i < logEntries.length; i++) {\n    const entry = logEntries[i]\n    const line = new TextRenderable(renderer, {\n      id: `focus-demo-log-${i}`,\n      content: entry.text,\n      fg: entry.color,\n      height: 1,\n    })\n    logBox.add(line)\n    logRenderables.push(line)\n  }\n}\n\nfunction updateDisplay() {\n  if (focusStatus) {\n    focusStatus.content = focused\n      ? \"Focus: YES  (terminal modes active)\"\n      : \"Focus: NO   (modes may be stripped by terminal)\"\n    focusStatus.fg = focused ? RGBA.fromInts(126, 231, 135) : RGBA.fromInts(255, 100, 100)\n  }\n  if (mouseStatus) {\n    mouseStatus.content = `Mouse: (${mouseX}, ${mouseY}) | Events: ${mouseEvents}`\n  }\n  if (countersStatus) {\n    countersStatus.content = `Focus-in: ${focusCount} | Focus-out: ${blurCount} | Mode restores: ${restoreCount}`\n  }\n  if (timestampStatus) {\n    timestampStatus.content = `Last focus: ${lastFocusTime || \"--\"} | Last blur: ${lastBlurTime || \"--\"} | Last mouse: ${lastMouseTime || \"--\"}`\n  }\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"#0D1117\")\n\n  // Reset state\n  mouseX = 0\n  mouseY = 0\n  mouseEvents = 0\n  focusCount = 0\n  blurCount = 0\n  restoreCount = 0\n  lastFocusTime = \"\"\n  lastBlurTime = \"\"\n  lastMouseTime = \"\"\n  focused = true\n  logEntries.length = 0\n\n  container = new BoxRenderable(renderer, {\n    id: \"focus-demo-main\",\n    flexDirection: \"column\",\n    padding: 1,\n  })\n  renderer.root.add(container)\n\n  // Title\n  const title = new TextRenderable(renderer, {\n    id: \"focus-demo-title\",\n    content: \"Focus Restore Demo - Mouse Tracking + Terminal Mode Restore\",\n    fg: RGBA.fromInts(72, 209, 204),\n    attributes: TextAttributes.BOLD,\n    height: 2,\n  })\n  container.add(title)\n\n  // Instructions\n  const instructions = new TextRenderable(renderer, {\n    id: \"focus-demo-instructions\",\n    content:\n      \"Move mouse to see tracking. Alt-tab away and back. Mouse should resume.\\n\" +\n      \"Minimize and restore. Try clicking after returning. Escape to return to menu.\",\n    fg: RGBA.fromInts(160, 160, 180),\n    height: 3,\n  })\n  container.add(instructions)\n\n  // Status box\n  const statusBox = new BoxRenderable(renderer, {\n    id: \"focus-demo-status-box\",\n    border: true,\n    borderColor: \"#4ECDC4\",\n    borderStyle: \"rounded\",\n    title: \"Terminal State\",\n    titleAlignment: \"center\",\n    padding: 1,\n    flexDirection: \"column\",\n    marginTop: 1,\n  })\n  container.add(statusBox)\n\n  focusStatus = new TextRenderable(renderer, {\n    id: \"focus-demo-focus-status\",\n    content: \"Focus: YES  (terminal modes active)\",\n    fg: RGBA.fromInts(126, 231, 135),\n    height: 1,\n  })\n  statusBox.add(focusStatus)\n\n  mouseStatus = new TextRenderable(renderer, {\n    id: \"focus-demo-mouse-status\",\n    content: \"Mouse: (0, 0) | Events: 0\",\n    fg: RGBA.fromInts(165, 214, 255),\n    height: 1,\n  })\n  statusBox.add(mouseStatus)\n\n  countersStatus = new TextRenderable(renderer, {\n    id: \"focus-demo-counters\",\n    content: \"Focus-in: 0 | Focus-out: 0 | Mode restores: 0\",\n    fg: RGBA.fromInts(210, 168, 255),\n    height: 1,\n  })\n  statusBox.add(countersStatus)\n\n  timestampStatus = new TextRenderable(renderer, {\n    id: \"focus-demo-timestamps\",\n    content: \"Last focus: -- | Last blur: -- | Last mouse: --\",\n    fg: RGBA.fromInts(139, 148, 158),\n    height: 1,\n  })\n  statusBox.add(timestampStatus)\n\n  // Event log box\n  logBox = new BoxRenderable(renderer, {\n    id: \"focus-demo-log-box\",\n    border: true,\n    borderColor: \"#6BCF7F\",\n    borderStyle: \"rounded\",\n    title: \"Event Log (latest 20)\",\n    titleAlignment: \"center\",\n    padding: 1,\n    flexDirection: \"column\",\n    marginTop: 1,\n    flexGrow: 1,\n  })\n  container.add(logBox)\n\n  // Mouse tracking area (covers whole screen, behind everything)\n  mouseArea = new BoxRenderable(renderer, {\n    id: \"focus-demo-mouse-area\",\n    position: \"absolute\",\n    left: 0,\n    top: 0,\n    width: \"100%\",\n    height: \"100%\",\n    zIndex: -1,\n    onMouse(event: MouseEvent) {\n      mouseX = event.x\n      mouseY = event.y\n      mouseEvents++\n      lastMouseTime = ts()\n      updateDisplay()\n    },\n  })\n  renderer.root.add(mouseArea)\n\n  // Spy on restoreTerminalModes to count restore calls\n  originalRestore = (renderer as any).lib.restoreTerminalModes\n  ;(renderer as any).lib.restoreTerminalModes = (...args: any[]) => {\n    restoreCount++\n    return originalRestore.call((renderer as any).lib, ...args)\n  }\n\n  // Focus/blur handlers\n  focusHandler = () => {\n    focused = true\n    focusCount++\n    lastFocusTime = ts()\n    addLogLine(\n      renderer,\n      `[${ts()}] FOCUS IN  - terminal modes restored (restore #${restoreCount})`,\n      RGBA.fromInts(126, 231, 135),\n    )\n    updateDisplay()\n  }\n\n  blurHandler = () => {\n    focused = false\n    blurCount++\n    lastBlurTime = ts()\n    addLogLine(renderer, `[${ts()}] FOCUS OUT - terminal may strip escape codes`, RGBA.fromInts(255, 165, 0))\n    updateDisplay()\n  }\n\n  renderer.on(\"focus\", focusHandler)\n  renderer.on(\"blur\", blurHandler)\n\n  addLogLine(renderer, `[${ts()}] Demo started. Move mouse, then alt-tab away and back.`, RGBA.fromInts(165, 214, 255))\n  updateDisplay()\n\n  renderer.requestRender()\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  // Restore spy\n  if (originalRestore) {\n    ;(renderer as any).lib.restoreTerminalModes = originalRestore\n    originalRestore = null\n  }\n\n  // Remove event listeners\n  if (focusHandler) {\n    renderer.off(\"focus\", focusHandler)\n    focusHandler = null\n  }\n  if (blurHandler) {\n    renderer.off(\"blur\", blurHandler)\n    blurHandler = null\n  }\n\n  // Clean up renderables\n  if (mouseArea) {\n    renderer.root.remove(mouseArea.id)\n    mouseArea.destroy()\n    mouseArea = null\n  }\n  if (container) {\n    renderer.root.remove(container.id)\n    container.destroyRecursively()\n    container = null\n  }\n\n  logRenderables.length = 0\n  logEntries.length = 0\n  focusStatus = null\n  mouseStatus = null\n  countersStatus = null\n  timestampStatus = null\n  logBox = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    enableMouseMovement: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/fonts.ts",
    "content": "import {\n  BoxRenderable,\n  CliRenderer,\n  createCliRenderer,\n  FrameBufferRenderable,\n  RGBA,\n  TextRenderable,\n  type KeyEvent,\n} from \"../index.js\"\nimport { renderFontToFrameBuffer } from \"../lib/ascii.font.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet scrollY = 0\nlet contentHeight = 56\nlet buffer: FrameBufferRenderable | null = null\nlet renderer: CliRenderer | null = null\nlet parentContainer: BoxRenderable | null = null\n\nfunction updateScrollPosition(): void {\n  if (!buffer || !renderer) return\n\n  const maxScroll = Math.max(0, contentHeight - renderer.terminalHeight)\n  scrollY = Math.max(0, Math.min(scrollY, maxScroll))\n  buffer.y = -scrollY\n  renderer.requestRender()\n}\n\nfunction handleKeyPress(key: KeyEvent): void {\n  const scrollAmount = 3\n\n  switch (key.name) {\n    case \"up\":\n    case \"k\":\n      console.log(\"up\")\n      scrollY -= scrollAmount\n      updateScrollPosition()\n      break\n    case \"down\":\n    case \"j\":\n      scrollY += scrollAmount\n      updateScrollPosition()\n      break\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#000028\")\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"fonts-container\",\n    zIndex: 15,\n    visible: true,\n  })\n  renderer.root.add(parentContainer)\n\n  buffer = new FrameBufferRenderable(renderer, {\n    id: \"ascii-demo\",\n    width: renderer.terminalWidth,\n    height: contentHeight,\n    position: \"absolute\",\n    zIndex: 10,\n  })\n  rendererInstance.root.add(buffer)\n  buffer.frameBuffer.clear()\n\n  // Reset scroll position\n  scrollY = 0\n\n  renderer.keyInput.on(\"keypress\", handleKeyPress)\n\n  // Large title with block font (multi-color)\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"FONTS\",\n    x: 5,\n    y: 1,\n    color: [RGBA.fromInts(255, 100, 100, 255), RGBA.fromInts(100, 100, 255, 255)],\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"block\",\n  })\n\n  // Tiny font title\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"TINY FONT DEMO\",\n    x: 5,\n    y: 8,\n    color: RGBA.fromInts(255, 255, 255, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"tiny\",\n  })\n\n  // Sample text in yellow\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"HELLO WORLD\",\n    x: 5,\n    y: 11,\n    color: RGBA.fromInts(255, 255, 0, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"tiny\",\n  })\n\n  // Numbers and symbols in green\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"1234567890\",\n    x: 5,\n    y: 14,\n    color: RGBA.fromInts(0, 255, 0, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"tiny\",\n  })\n\n  // Special characters in magenta\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"!@#$%&*()+-=\",\n    x: 5,\n    y: 17,\n    color: RGBA.fromInts(255, 0, 255, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"tiny\",\n  })\n\n  // Block font demo section\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"BLOCK FONT DEMO\",\n    x: 5,\n    y: 20,\n    color: RGBA.fromInts(255, 255, 255, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"tiny\",\n  })\n\n  // Multi-color block font example\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"HI\",\n    x: 5,\n    y: 23,\n    color: [RGBA.fromInts(255, 255, 0, 255), RGBA.fromInts(0, 255, 255, 255)],\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"block\",\n  })\n\n  // Another multi-color example\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"2025\",\n    x: 25,\n    y: 23,\n    color: [RGBA.fromInts(255, 128, 0, 255), RGBA.fromInts(128, 255, 128, 255)],\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"block\",\n  })\n\n  // Shade font demo section\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"SHADE FONT DEMO\",\n    x: 5,\n    y: 30,\n    color: RGBA.fromInts(255, 255, 255, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"tiny\",\n  })\n\n  // Shade font with multi-color\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"COOL\",\n    x: 5,\n    y: 33,\n    color: [\n      RGBA.fromInts(255, 200, 100, 255), // c1 - warm orange\n      RGBA.fromInts(100, 150, 200, 255), // c2 - cool blue\n    ],\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"shade\",\n  })\n\n  // Slick font demo section\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"SLICK FONT DEMO\",\n    x: 5,\n    y: 42,\n    color: RGBA.fromInts(255, 255, 255, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"tiny\",\n  })\n\n  // Slick font with multi-color\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"STYLE\",\n    x: 5,\n    y: 45,\n    color: [\n      RGBA.fromInts(100, 255, 100, 255), // c1 - bright green\n      RGBA.fromInts(255, 100, 255, 255), // c2 - bright magenta\n    ],\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"slick\",\n  })\n\n  const scrollInstructions = new TextRenderable(renderer, {\n    id: \"scroll-instructions\",\n    content: \"USE J/K OR ARROW KEYS TO SCROLL\",\n    position: \"absolute\",\n    left: renderer.terminalWidth - 32,\n    top: 1,\n    fg: RGBA.fromInts(255, 255, 0, 255),\n    zIndex: 25,\n  })\n  parentContainer.add(scrollInstructions)\n\n  renderFontToFrameBuffer(buffer.frameBuffer, {\n    text: \"ESC TO RETURN\",\n    x: 5,\n    y: 53,\n    color: RGBA.fromInts(128, 128, 128, 255),\n    backgroundColor: RGBA.fromInts(0, 0, 40, 255),\n    font: \"tiny\",\n  })\n\n  updateScrollPosition()\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (renderer) {\n    renderer.keyInput.off(\"keypress\", handleKeyPress)\n  }\n\n  rendererInstance.root.remove(\"ascii-demo\")\n\n  if (parentContainer) {\n    rendererInstance.root.remove(\"fonts-container\")\n    parentContainer = null\n  }\n\n  scrollY = 0\n  buffer = null\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 30,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/fractal-shader-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport { BoxRenderable, CliRenderer, createCliRenderer, RGBA, TextRenderable, type KeyEvent } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { Scene as ThreeScene, Mesh as ThreeMesh, PerspectiveCamera, PlaneGeometry, Vector2 } from \"three\"\nimport { MeshBasicNodeMaterial } from \"three/webgpu\"\nimport {\n  uniform,\n  vec2,\n  vec3,\n  vec4,\n  float,\n  screenCoordinate,\n  sin,\n  cos,\n  atan,\n  length,\n  normalize,\n  ceil,\n  Loop,\n  Fn,\n  int,\n} from \"three/tsl\"\nimport { ThreeCliRenderer } from \"../3d.js\"\n\nlet engine: ThreeCliRenderer | null = null\nlet sceneRoot: ThreeScene | null = null\nlet timeUniform: any = null\nlet resolutionUniform: any = null\nlet cellAspectRatio: any = null\nlet cameraNode: PerspectiveCamera | null = null\nlet time = 0\nlet timeSpeed = 1.0\nlet paused = false\nlet keyHandler: ((key: KeyEvent) => void) | null = null\nlet handleResize: ((width: number, height: number) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const WIDTH = renderer.terminalWidth\n  const HEIGHT = renderer.terminalHeight\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"fractal-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  engine = new ThreeCliRenderer(renderer, {\n    width: WIDTH,\n    height: HEIGHT,\n    focalLength: 8,\n    backgroundColor: RGBA.fromValues(0.0, 0.0, 0.0, 1.0),\n  })\n  await engine.init()\n\n  sceneRoot = new ThreeScene()\n\n  timeUniform = uniform(0.0)\n  resolutionUniform = uniform(new Vector2(WIDTH * 2, HEIGHT * 2))\n  cellAspectRatio = uniform(2.0)\n\n  const fractalMaterial = new MeshBasicNodeMaterial()\n\n  const fractalColor = Fn(() => {\n    const FC = screenCoordinate\n    const r = resolutionUniform\n    const t = timeUniform\n\n    const z = float(0.0).toVar()\n    const d = float(0.0).toVar()\n    const i = float(0.0).toVar()\n    const o = vec4(0.0).toVar()\n\n    Loop({ start: int(1), end: int(90), type: \"int\", condition: \"<\" }, ({ i: loopI }) => {\n      i.assign(float(loopI))\n\n      const correctedFC = vec2(FC.x, FC.y.mul(cellAspectRatio))\n      const FCrgb = vec3(correctedFC.x, correctedFC.y, float(0.0))\n      const rxyx = vec3(r.x, r.y.mul(cellAspectRatio), r.x)\n      const p = z.mul(normalize(FCrgb.mul(2.0).sub(rxyx))).toVar()\n\n      p.assign(vec3(atan(p.y, p.x), p.z.div(3.0).sub(t), length(p.xy).sub(9.0)))\n\n      Loop({ start: int(1), end: int(5), type: \"int\", condition: \"<\" }, ({ i: innerI }) => {\n        const dValue = float(innerI)\n        d.assign(dValue)\n\n        const iVec = i.mul(vec3(0.2, 0.0, 0.0))\n        const pyzx = vec3(p.y, p.z, p.x)\n        const arg = pyzx.mul(dValue).sub(iVec)\n        const distortion = sin(ceil(arg)).div(dValue)\n        p.addAssign(distortion)\n      })\n\n      const cos6p = cos(p.mul(6.0))\n      const cosTerm = cos6p.mul(0.2).sub(0.2)\n      const distanceVec = vec4(cosTerm.x, cosTerm.y, cosTerm.z, p.z)\n      const dNew = length(distanceVec).mul(0.2)\n      d.assign(dNew)\n      z.addAssign(dNew)\n\n      const colorPhase = vec4(0.0, 0.5, 1.0, 0.0)\n      const cosResult = cos(p.x.add(colorPhase))\n      const colorContrib = cosResult.add(1.0).div(d).div(z)\n      o.addAssign(colorContrib)\n    })\n\n    const oSquared = o.mul(o)\n    const processed = oSquared.div(800.0)\n\n    const x = processed\n    const x2 = x.mul(x)\n    const tanhApprox = x.mul(x2.add(27.0)).div(x2.mul(9.0).add(27.0))\n\n    return vec4(tanhApprox.rgb, 1.0)\n  })()\n\n  fractalMaterial.colorNode = fractalColor\n\n  const planeGeometry = new PlaneGeometry(10, 10)\n  const planeMesh = new ThreeMesh(planeGeometry, fractalMaterial)\n  planeMesh.name = \"fractal_plane\"\n  sceneRoot.add(planeMesh)\n\n  cameraNode = new PerspectiveCamera(45, engine.aspectRatio, 0.1, 100.0)\n  cameraNode.position.set(0, 0, 5)\n  cameraNode.name = \"main_camera\"\n\n  engine.setActiveCamera(cameraNode)\n\n  const titleText = new TextRenderable(renderer, {\n    id: \"fractal_title\",\n    content: \"Shader by @XorDev\",\n    fg: \"#FFFFFF\",\n    zIndex: 25,\n  })\n  parentContainer.add(titleText)\n\n  const controlsText = new TextRenderable(renderer, {\n    id: \"fractal_controls\",\n    content: \"Space: Pause/Resume | R: Reset | P: Screenshot | +/-: Speed | Escape: Back to menu\",\n    position: \"absolute\",\n    top: HEIGHT - 2,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(controlsText)\n\n  const statusText = new TextRenderable(renderer, {\n    id: \"fractal_status\",\n    content: \"Speed: 1.0x\",\n    position: \"absolute\",\n    top: 1,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(statusText)\n\n  handleResize = (width: number, height: number) => {\n    if (cameraNode && engine) {\n      cameraNode.aspect = engine.aspectRatio\n      cameraNode.updateProjectionMatrix()\n    }\n\n    if (resolutionUniform) {\n      resolutionUniform.value.set(width * 2, height * 2)\n    }\n\n    controlsText.y = height - 2\n  }\n\n  renderer.on(\"resize\", handleResize)\n\n  time = 0\n  timeSpeed = 1.0\n  paused = false\n\n  keyHandler = (key: KeyEvent) => {\n    if (key.name === \"p\" && engine) {\n      engine.saveToFile(`fractal-${Date.now()}.png`)\n    }\n\n    if (key.name === \"r\" && cameraNode) {\n      timeSpeed = 1.0\n      paused = false\n      cameraNode.position.set(0, 0, 5)\n      cameraNode.rotation.set(0, 0, 0)\n      cameraNode.lookAt(0, 0, 0)\n      statusText.content = `Speed: ${timeSpeed.toFixed(1)}x`\n    }\n\n    if (key.name === \"space\") {\n      paused = !paused\n      statusText.content = `Speed: ${timeSpeed.toFixed(1)}x`\n    }\n\n    if (key.name === \"+\" || key.name === \"=\") {\n      timeSpeed = Math.min(timeSpeed + 0.1, 3.0)\n      statusText.content = `Speed: ${timeSpeed.toFixed(1)}x`\n    }\n\n    if (key.name === \"-\" || key.name === \"_\") {\n      timeSpeed = Math.max(timeSpeed - 0.1, 0.1)\n      statusText.content = `Speed: ${timeSpeed.toFixed(1)}x`\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", keyHandler)\n\n  renderer.setFrameCallback(async (deltaMs) => {\n    const deltaTime = deltaMs / 1000\n\n    if (!paused) {\n      time += deltaTime * timeSpeed\n    }\n\n    if (timeUniform) {\n      timeUniform.value = time\n    }\n\n    statusText.content = `Speed: ${timeSpeed.toFixed(1)}x`\n\n    if (engine && sceneRoot) {\n      await engine.drawScene(sceneRoot, renderer.nextRenderBuffer, deltaTime)\n    }\n  })\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (keyHandler) {\n    renderer.keyInput.off(\"keypress\", keyHandler)\n    keyHandler = null\n  }\n\n  if (handleResize) {\n    renderer.off(\"resize\", handleResize)\n    handleResize = null\n  }\n\n  renderer.clearFrameCallbacks()\n\n  if (parentContainer) {\n    renderer.root.remove(\"fractal-container\")\n    parentContainer = null\n  }\n\n  engine?.destroy()\n  engine = null\n  sceneRoot = null\n  timeUniform = null\n  resolutionUniform = null\n  cellAspectRatio = null\n  cameraNode = null\n  time = 0\n  timeSpeed = 1.0\n  paused = false\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/framebuffer-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  TextAttributes,\n  TextRenderable,\n  FrameBufferRenderable,\n  BoxRenderable,\n  ASCIIFontRenderable,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\n/**\n * This demo showcases framebuffers with multiple\n * overlapping framebuffers and transparency.\n */\n\nlet boxX = 10\nlet boxY = 10\nlet boxDx = 5\nlet boxDy = 3\nlet ballX = 20\nlet ballY = 20\nlet ballDx = 15\nlet ballDy = 10\nlet currentWidth = 10\nlet currentHeight = 5\nlet growingWidth = true\nlet growingHeight = true\nlet lastResizeTime = 0\nconst resizeInterval = 0.1\nlet parentContainer: BoxRenderable | null = null\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  const backgroundColor = RGBA.fromInts(10, 10, 30)\n  renderer.setBackgroundColor(backgroundColor)\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"framebuffer-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  const titleText = new TextRenderable(renderer, {\n    id: \"framebuffer_title\",\n    content: \"FrameBuffer Demo\",\n    position: \"absolute\",\n    left: 2,\n    top: 1,\n    fg: RGBA.fromInts(255, 255, 100),\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  parentContainer.add(titleText)\n\n  const subtitleText = new TextRenderable(renderer, {\n    id: \"framebuffer_subtitle\",\n    content: \"Showcasing framebuffers with transparency and partial drawing\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    fg: RGBA.fromInts(200, 200, 200),\n    zIndex: 1000,\n  })\n  parentContainer.add(subtitleText)\n\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"framebuffer_instructions\",\n    content: \"Press Escape to return to menu\",\n    position: \"absolute\",\n    left: 2,\n    top: 3,\n    fg: RGBA.fromInts(150, 150, 150),\n    zIndex: 1000,\n  })\n  parentContainer.add(instructionsText)\n\n  const patternBufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"pattern\",\n    width: renderer.terminalWidth,\n    height: renderer.terminalHeight,\n    position: \"absolute\",\n    zIndex: 0,\n    respectAlpha: true,\n  })\n  renderer.root.add(patternBufferRenderable)\n  const { frameBuffer: patternBuffer } = patternBufferRenderable\n\n  for (let y = 0; y < patternBuffer.height; y++) {\n    for (let x = 0; x < patternBuffer.width; x++) {\n      if ((x + y) % 5 === 0) {\n        patternBuffer.drawText(\"·\", x, y, RGBA.fromInts(50, 50, 80))\n      }\n    }\n  }\n\n  const nestedBox = new BoxRenderable(renderer, {\n    id: \"nested-box\",\n    width: 20,\n    height: 10,\n    position: \"absolute\",\n    left: 4,\n    top: 4,\n    zIndex: 5,\n    border: true,\n    title: \"Nested example\",\n    backgroundColor: RGBA.fromInts(120, 0, 120, 120),\n  })\n  parentContainer.add(nestedBox)\n\n  const innerBoxWidth = 10\n  const innerBoxHeight = 4\n\n  const nestedInnerBox = new BoxRenderable(renderer, {\n    id: \"nested-inner-box\",\n    width: innerBoxWidth,\n    height: innerBoxHeight,\n    left: 3,\n    top: 3,\n    zIndex: 1,\n    // buffered: true,\n    border: true,\n    title: \"Inner\",\n    backgroundColor: RGBA.fromInts(0, 255, 0, 10),\n  })\n  nestedBox.add(nestedInnerBox)\n\n  const boxObj = new BoxRenderable(renderer, {\n    id: \"moving-box\",\n    width: 20,\n    height: 10,\n    position: \"absolute\",\n    left: 10,\n    top: 10,\n    zIndex: 1,\n    overflow: \"hidden\",\n    // NOTE: This color is rendered, it is just overlayed by the boxFrame fill color\n    backgroundColor: RGBA.fromInts(255, 120, 120, 255),\n  })\n\n  boxObj.add(\n    new ASCIIFontRenderable(renderer, {\n      id: \"moving-box-ascii\",\n      text: \"ASCII\",\n      position: \"relative\",\n      left: 2,\n      top: 5,\n      zIndex: 2,\n    }),\n  )\n\n  const boxFrame = new FrameBufferRenderable(renderer, {\n    id: \"moving-box-buffer\",\n    width: 20,\n    height: 10,\n    position: \"relative\",\n    marginTop: -2,\n    zIndex: 1,\n    respectAlpha: true,\n  })\n\n  boxObj.add(boxFrame)\n\n  renderer.root.add(boxObj)\n  const boxBuffer = boxFrame.frameBuffer\n\n  const boxColor = RGBA.fromInts(80, 30, 100, 128)\n  boxBuffer.fillRect(0, 0, 20, 10, boxColor)\n\n  for (let x = 0; x < 20; x++) {\n    boxBuffer.drawText(\"-\", x, 0, RGBA.fromInts(150, 100, 200))\n    boxBuffer.drawText(\"-\", x, 9, RGBA.fromInts(150, 100, 200))\n  }\n  for (let y = 0; y < 10; y++) {\n    boxBuffer.drawText(\"|\", 0, y, RGBA.fromInts(150, 100, 200))\n    boxBuffer.drawText(\"|\", 19, y, RGBA.fromInts(150, 100, 200))\n  }\n\n  boxBuffer.drawText(\"+\", 0, 0, RGBA.fromInts(200, 150, 255))\n  boxBuffer.drawText(\"+\", 19, 0, RGBA.fromInts(200, 150, 255))\n  boxBuffer.drawText(\"+\", 0, 9, RGBA.fromInts(200, 150, 255))\n  boxBuffer.drawText(\"+\", 19, 9, RGBA.fromInts(200, 150, 255))\n\n  boxBuffer.drawText(\"Moving Box\", 5, 2, RGBA.fromInts(255, 255, 255), RGBA.fromInts(100, 40, 120), TextAttributes.BOLD)\n\n  const overlayBufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"overlay\",\n    width: 40,\n    height: 15,\n    position: \"absolute\",\n    left: 30,\n    top: 15,\n    zIndex: 2,\n    respectAlpha: true,\n  })\n  renderer.root.add(overlayBufferRenderable)\n  const { frameBuffer: overlayBuffer } = overlayBufferRenderable\n\n  for (let y = 0; y < overlayBuffer.height; y++) {\n    for (let x = 0; x < overlayBuffer.width; x++) {\n      if ((x + y) % 3 !== 0) {\n        overlayBuffer.setCell(x, y, \" \", RGBA.fromInts(255, 255, 255), RGBA.fromInts(0, 100, 150, 128))\n      }\n    }\n  }\n\n  overlayBuffer.drawText(\n    \"Transparent Overlay\",\n    10,\n    2,\n    RGBA.fromInts(255, 255, 255),\n    RGBA.fromInts(0, 120, 180, 180),\n    TextAttributes.BOLD,\n  )\n  overlayBuffer.drawText(\n    \"This overlay has transparent\",\n    5,\n    5,\n    RGBA.fromInts(255, 0, 255),\n    RGBA.fromInts(0, 120, 180, 180),\n  )\n  overlayBuffer.drawText(\n    \"cells that let content below\",\n    5,\n    6,\n    RGBA.fromInts(255, 255, 255),\n    RGBA.fromInts(0, 120, 180, 180),\n  )\n  overlayBuffer.drawText(\"show through!\", 5, 7, RGBA.fromInts(255, 255, 255), RGBA.fromInts(0, 120, 180, 180))\n\n  const ballObj = new FrameBufferRenderable(renderer, {\n    id: \"ball\",\n    width: 3,\n    height: 3,\n    position: \"absolute\",\n    left: 20,\n    top: 20,\n    zIndex: 3,\n  })\n  renderer.root.add(ballObj)\n  const ballBuffer = ballObj.frameBuffer\n\n  ballBuffer.drawText(\" \", 0, 0, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n  ballBuffer.drawText(\" \", 1, 0, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n  ballBuffer.drawText(\" \", 2, 0, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n  ballBuffer.drawText(\" \", 0, 1, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n  ballBuffer.drawText(\"O\", 1, 1, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n  ballBuffer.drawText(\" \", 2, 1, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n  ballBuffer.drawText(\" \", 0, 2, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n  ballBuffer.drawText(\" \", 1, 2, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n  ballBuffer.drawText(\" \", 2, 2, RGBA.fromInts(255, 255, 255), RGBA.fromInts(200, 50, 50))\n\n  const resizableObj = new FrameBufferRenderable(renderer, {\n    id: \"resizable-box\",\n    width: 10,\n    height: 5,\n    position: \"absolute\",\n    left: 50,\n    top: 8,\n    zIndex: 3,\n  })\n  renderer.root.add(resizableObj)\n  const resizableBuffer = resizableObj.frameBuffer\n\n  function drawResizableContent() {\n    resizableBuffer.clear(RGBA.fromInts(0, 0, 0, 0))\n\n    for (let x = 0; x < resizableBuffer.width; x++) {\n      resizableBuffer.drawText(\"=\", x, 0, RGBA.fromInts(255, 200, 100))\n      resizableBuffer.drawText(\"=\", x, resizableBuffer.height - 1, RGBA.fromInts(255, 200, 100))\n    }\n\n    for (let y = 0; y < resizableBuffer.height; y++) {\n      resizableBuffer.drawText(\"|\", 0, y, RGBA.fromInts(255, 200, 100))\n      resizableBuffer.drawText(\"|\", resizableBuffer.width - 1, y, RGBA.fromInts(255, 200, 100))\n    }\n\n    resizableBuffer.drawText(\"+\", 0, 0, RGBA.fromInts(255, 230, 150))\n    resizableBuffer.drawText(\"+\", resizableBuffer.width - 1, 0, RGBA.fromInts(255, 230, 150))\n    resizableBuffer.drawText(\"+\", 0, resizableBuffer.height - 1, RGBA.fromInts(255, 230, 150))\n    resizableBuffer.drawText(\"+\", resizableBuffer.width - 1, resizableBuffer.height - 1, RGBA.fromInts(255, 230, 150))\n\n    if (resizableBuffer.width >= 18 && resizableBuffer.height >= 3) {\n      resizableBuffer.drawText(\n        \"Resizable Box\",\n        Math.floor((resizableBuffer.width - 13) / 2),\n        2,\n        RGBA.fromInts(255, 255, 100),\n        undefined,\n        TextAttributes.BOLD,\n      )\n    }\n  }\n\n  drawResizableContent()\n\n  // Create a large source framebuffer for partial drawing demonstration\n  const sourceObj = new FrameBufferRenderable(renderer, {\n    id: \"large-source\",\n    width: 40,\n    height: 20,\n    position: \"absolute\",\n    zIndex: -1,\n    visible: false,\n  })\n  renderer.root.add(sourceObj)\n  const sourceBuffer = sourceObj.frameBuffer\n\n  // Fill source buffer with a pattern we can crop from\n  for (let y = 0; y < sourceBuffer.height; y++) {\n    for (let x = 0; x < sourceBuffer.width; x++) {\n      const char = String.fromCharCode(65 + ((x + y) % 26)) // A-Z pattern\n      const hue = (x * 10 + y * 5) % 360\n      const r = Math.floor(128 + 127 * Math.sin((hue * Math.PI) / 180))\n      const g = Math.floor(128 + 127 * Math.sin(((hue + 120) * Math.PI) / 180))\n      const b = Math.floor(128 + 127 * Math.sin(((hue + 240) * Math.PI) / 180))\n      sourceBuffer.drawText(char, x, y, RGBA.fromInts(r, g, b))\n    }\n  }\n\n  // Create smaller framebuffers to demonstrate partial drawing\n  const cropBuffer1Renderable = new FrameBufferRenderable(renderer, {\n    id: \"crop-demo-1\",\n    width: 12,\n    height: 8,\n    position: \"absolute\",\n    left: 5,\n    top: 35,\n    zIndex: 4,\n  })\n  renderer.root.add(cropBuffer1Renderable)\n  const { frameBuffer: cropBuffer1 } = cropBuffer1Renderable\n\n  const cropBuffer2Renderable = new FrameBufferRenderable(renderer, {\n    id: \"crop-demo-2\",\n    width: 15,\n    height: 6,\n    position: \"absolute\",\n    left: 25,\n    top: 35,\n    zIndex: 4,\n  })\n  renderer.root.add(cropBuffer2Renderable)\n  const { frameBuffer: cropBuffer2 } = cropBuffer2Renderable\n\n  const cropBuffer3Renderable = new FrameBufferRenderable(renderer, {\n    id: \"crop-demo-3\",\n    width: 10,\n    height: 10,\n    position: \"absolute\",\n    left: 45,\n    top: 35,\n    zIndex: 4,\n  })\n  renderer.root.add(cropBuffer3Renderable)\n  const { frameBuffer: cropBuffer3 } = cropBuffer3Renderable\n\n  // Label for the crop demo\n  const cropDemoLabel = new TextRenderable(renderer, {\n    id: \"crop_demo_label\",\n    content: \"Partial FrameBuffer Drawing Demo:\",\n    position: \"absolute\",\n    left: 5,\n    top: 34,\n    fg: RGBA.fromInts(255, 255, 200),\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  parentContainer.add(cropDemoLabel)\n\n  // Emoji demo using encodeUnicode and drawChar\n  const emojiBufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"emoji-demo\",\n    width: 20,\n    height: 5,\n    position: \"absolute\",\n    left: 60,\n    top: 35,\n    zIndex: 4,\n  })\n  renderer.root.add(emojiBufferRenderable)\n  const { frameBuffer: emojiBuffer } = emojiBufferRenderable\n\n  const emojiDemoLabel = new TextRenderable(renderer, {\n    id: \"emoji_demo_label\",\n    content: \"encodeUnicode Demo:\",\n    position: \"absolute\",\n    left: 60,\n    top: 34,\n    fg: RGBA.fromInts(255, 255, 200),\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  parentContainer.add(emojiDemoLabel)\n\n  // Pre-encode the monkey emoji frames\n  const monkeyFrames = [\"🐵 \", \"🙈 \", \"🙉 \", \"🙊 \"]\n  let currentMonkeyFrame = 0\n  let lastMonkeyFrameTime = 0\n  const monkeyFrameInterval = 0.3 // Change frame every 300ms\n\n  boxX = 10\n  boxY = 10\n  boxDx = 5\n  boxDy = 3\n  ballX = 20\n  ballY = 20\n  ballDx = 15\n  ballDy = 10\n  currentWidth = 10\n  currentHeight = 5\n  growingWidth = true\n  growingHeight = true\n  lastResizeTime = 0\n\n  renderer.setFrameCallback(async (deltaTime) => {\n    const clampedDelta = Math.min(deltaTime, 100) / 1000\n\n    boxX += boxDx * clampedDelta\n    boxY += boxDy * clampedDelta\n\n    if (boxX < 0) {\n      boxX = 0\n      boxDx = Math.abs(boxDx)\n    } else if (boxX + boxBuffer.width > renderer.terminalWidth) {\n      boxX = renderer.terminalWidth - boxBuffer.width\n      boxDx = -Math.abs(boxDx)\n    }\n\n    if (boxY < 5) {\n      boxY = 5\n      boxDy = Math.abs(boxDy)\n    } else if (boxY + boxBuffer.height > renderer.terminalHeight) {\n      boxY = renderer.terminalHeight - boxBuffer.height\n      boxDy = -Math.abs(boxDy)\n    }\n\n    boxObj.x = Math.round(boxX)\n    boxObj.y = Math.round(boxY)\n\n    ballX += ballDx * clampedDelta\n    ballY += ballDy * clampedDelta\n\n    if (ballX < 0) {\n      ballX = 0\n      ballDx = Math.abs(ballDx)\n    } else if (ballX + ballBuffer.width > renderer.terminalWidth) {\n      ballX = renderer.terminalWidth - ballBuffer.width\n      ballDx = -Math.abs(ballDx)\n    }\n\n    if (ballY < 5) {\n      ballY = 5\n      ballDy = Math.abs(ballDy)\n    } else if (ballY + ballBuffer.height > renderer.terminalHeight) {\n      ballY = renderer.terminalHeight - ballBuffer.height\n      ballDy = -Math.abs(ballDy)\n    }\n\n    ballObj.x = Math.round(ballX)\n    ballObj.y = Math.round(ballY)\n\n    const time = Date.now() / 1000\n\n    // Update partial drawing demonstration\n    const scrollOffset = Math.floor(time * 3) % 20 // Scroll through source buffer\n\n    // Clear crop demo buffers\n    cropBuffer1.clear(RGBA.fromInts(0, 0, 0, 1))\n    cropBuffer2.clear(RGBA.fromInts(0, 0, 0, 1))\n    cropBuffer3.clear(RGBA.fromInts(0, 0, 0, 1))\n\n    // Demo 1: Top-left crop that scrolls horizontally\n    cropBuffer1.drawFrameBuffer(0, 0, sourceBuffer, scrollOffset, 0, 12, 8)\n    cropBuffer1.drawText(\n      \"TopLeft\",\n      1,\n      7,\n      RGBA.fromInts(255, 255, 255),\n      RGBA.fromInts(0, 0, 0, 180),\n      TextAttributes.BOLD,\n    )\n\n    // Demo 2: Center crop - super simple slow movement\n    const centerX = 10 + Math.floor((Math.sin(time * 0.3) + 1) * 5) // 10 to 20, very slow\n    const centerY = 5 + Math.floor((Math.cos(time * 0.2) + 1) * 3) // 5 to 11, very slow\n    cropBuffer2.drawFrameBuffer(0, 0, sourceBuffer, centerX, centerY, 15, 6)\n    cropBuffer2.drawText(\"Center\", 1, 5, RGBA.fromInts(255, 255, 255), RGBA.fromInts(0, 0, 0, 180), TextAttributes.BOLD)\n\n    // Demo 3: Bottom-right crop - simple back and forth\n    const brX = 20 + Math.floor((Math.sin(time * 0.4) + 1) * 5) // 20 to 30, very slow\n    const brY = 5 + Math.floor((Math.cos(time * 0.4) + 1) * 3) // 5 to 11, same speed for circle\n    cropBuffer3.drawFrameBuffer(0, 0, sourceBuffer, brX, brY, 10, 10)\n    cropBuffer3.drawText(\n      \"BotRight\",\n      1,\n      9,\n      RGBA.fromInts(255, 255, 255),\n      RGBA.fromInts(0, 0, 0, 180),\n      TextAttributes.BOLD,\n    )\n\n    if (time - lastResizeTime > resizeInterval) {\n      lastResizeTime = time\n\n      if (growingWidth) {\n        currentWidth++\n        if (currentWidth >= 30) growingWidth = false\n      } else {\n        currentWidth--\n        if (currentWidth <= 10) growingWidth = true\n      }\n\n      if (growingHeight) {\n        currentHeight++\n        if (currentHeight >= 15) growingHeight = false\n      } else {\n        currentHeight--\n        if (currentHeight <= 5) growingHeight = true\n      }\n\n      resizableBuffer.resize(currentWidth, currentHeight)\n\n      drawResizableContent()\n\n      const centerX = 50\n      const centerY = 8\n\n      resizableObj.x = Math.round(centerX - currentWidth / 2)\n      resizableObj.y = Math.round(centerY - currentHeight / 2)\n    }\n\n    const hue = (time * 20) % 360\n    const r = Math.floor(128 + 127 * Math.sin((hue * Math.PI) / 180))\n    const g = Math.floor(128 + 127 * Math.sin(((hue + 120) * Math.PI) / 180))\n    const b = Math.floor(128 + 127 * Math.sin(((hue + 240) * Math.PI) / 180))\n\n    if (Math.floor(time * 10) % 1 === 0) {\n      overlayBuffer.clear(RGBA.fromInts(0, 0, 0, 0))\n\n      for (let y = 0; y < overlayBuffer.height; y++) {\n        for (let x = 0; x < overlayBuffer.width; x++) {\n          if ((x + y) % 3 !== 0) {\n            overlayBuffer.setCell(x, y, \" \", RGBA.fromInts(255, 255, 255), RGBA.fromInts(r, g, b, 128))\n          }\n        }\n      }\n\n      overlayBuffer.drawText(\n        \"Transparent Overlay\",\n        10,\n        2,\n        RGBA.fromInts(255, 255, 255),\n        RGBA.fromInts(255, 255, 255, 180),\n        TextAttributes.BOLD,\n      )\n      overlayBuffer.drawText(\n        \"This overlay has transparent\",\n        5,\n        5,\n        RGBA.fromInts(255, 255, 255),\n        RGBA.fromInts(255, 255, 255, 180),\n      )\n      overlayBuffer.drawText(\n        \"cells that let content below\",\n        5,\n        6,\n        RGBA.fromInts(255, 255, 255),\n        RGBA.fromInts(255, 255, 255, 180),\n      )\n      overlayBuffer.drawText(\"show through!\", 5, 7, RGBA.fromInts(255, 255, 255), RGBA.fromInts(255, 255, 255, 180))\n    }\n\n    // Update monkey emoji animation\n    if (time - lastMonkeyFrameTime > monkeyFrameInterval) {\n      lastMonkeyFrameTime = time\n      currentMonkeyFrame = (currentMonkeyFrame + 1) % monkeyFrames.length\n\n      // Clear emoji buffer\n      emojiBuffer.clear(RGBA.fromInts(0, 0, 0, 1))\n\n      // Draw border\n      for (let x = 0; x < emojiBuffer.width; x++) {\n        emojiBuffer.drawText(\"=\", x, 0, RGBA.fromInts(255, 200, 100))\n        emojiBuffer.drawText(\"=\", x, emojiBuffer.height - 1, RGBA.fromInts(255, 200, 100))\n      }\n      for (let y = 0; y < emojiBuffer.height; y++) {\n        emojiBuffer.drawText(\"|\", 0, y, RGBA.fromInts(255, 200, 100))\n        emojiBuffer.drawText(\"|\", emojiBuffer.width - 1, y, RGBA.fromInts(255, 200, 100))\n      }\n      emojiBuffer.drawText(\"+\", 0, 0, RGBA.fromInts(255, 230, 150))\n      emojiBuffer.drawText(\"+\", emojiBuffer.width - 1, 0, RGBA.fromInts(255, 230, 150))\n      emojiBuffer.drawText(\"+\", 0, emojiBuffer.height - 1, RGBA.fromInts(255, 230, 150))\n      emojiBuffer.drawText(\"+\", emojiBuffer.width - 1, emojiBuffer.height - 1, RGBA.fromInts(255, 230, 150))\n\n      // Draw only the current frame using encodeUnicode and drawChar\n      const fg = RGBA.fromInts(255, 255, 255)\n      const bg = RGBA.fromInts(0, 0, 0, 0) // Transparent background\n\n      // Encode the current frame\n      const currentFrame = monkeyFrames[currentMonkeyFrame]\n      const encoded = emojiBuffer.encodeUnicode(currentFrame)\n\n      if (encoded) {\n        let x = 7 // Center the emoji\n        for (const encodedChar of encoded.data) {\n          emojiBuffer.drawChar(encodedChar.char, x, 2, fg, bg)\n          x += encodedChar.width\n        }\n        emojiBuffer.freeUnicode(encoded)\n      }\n\n      // Draw frame indicator\n      const frameText = `Frame ${currentMonkeyFrame + 1}/4`\n      emojiBuffer.drawText(frameText, 5, 3, RGBA.fromInts(200, 200, 200))\n    }\n  })\n\n  const debugInstructionsText = new TextRenderable(renderer, {\n    id: \"framebuffer_debug_instructions\",\n    content: \"Press 1-4 to change corner | Escape: Back to menu\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    fg: RGBA.fromInts(200, 200, 200),\n    zIndex: 1000,\n  })\n  parentContainer.add(debugInstructionsText)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n\n  if (parentContainer) {\n    renderer.root.remove(\"framebuffer-container\")\n    parentContainer = null\n  }\n\n  renderer.root.remove(\"pattern\")\n  renderer.root.remove(\"moving-box\")\n  renderer.root.remove(\"overlay\")\n  renderer.root.remove(\"ball\")\n  renderer.root.remove(\"resizable-box\")\n  renderer.root.remove(\"large-source\")\n  renderer.root.remove(\"crop-demo-1\")\n  renderer.root.remove(\"crop-demo-2\")\n  renderer.root.remove(\"crop-demo-3\")\n  renderer.root.remove(\"emoji-demo\")\n\n  boxX = 10\n  boxY = 10\n  boxDx = 5\n  boxDy = 3\n  ballX = 20\n  ballY = 20\n  ballDx = 15\n  ballDy = 10\n  currentWidth = 10\n  currentHeight = 5\n  growingWidth = true\n  growingHeight = true\n  lastResizeTime = 0\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/full-unicode-demo.ts",
    "content": "import {\n  createCliRenderer,\n  RGBA,\n  FrameBufferRenderable,\n  TextRenderable,\n  t,\n  blue,\n  bold,\n  underline,\n  fg,\n  type MouseEvent,\n  type CliRenderer,\n  type RenderContext,\n  BoxRenderable,\n} from \"../index.js\"\n\nconst GRAPHEME_LINES: string[] = [\n  \"👩🏽‍💻  👨‍👩‍👧‍👦  🏳️‍🌈  🇺🇸  🇩🇪  🇯🇵  🇮🇳\",\n  \"a̐éö̲  Z͑͗͛̒͘a̴͈͚̐̓l̷͓̱͉g̶̙̗̓͘o̵͍͈  क्‍ष\",\n  \"مرحبا  こんにちは  สวัสดี  Здравствуйте\",\n  \"𝔘𝔫𝔦𝔠𝔬𝔡𝔢  𝒻𝓊𝓁𝓁 𝓌𝒾𝒹𝓉𝒽：ＡＢＣ  ½ ⅞ ⅓\",\n]\n\nclass DraggableGraphemeBox extends FrameBufferRenderable {\n  private isDragging = false\n  private dragOffsetX = 0\n  private dragOffsetY = 0\n\n  constructor(\n    ctx: RenderContext,\n    id: string,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    bg: RGBA,\n    respectAlpha = true,\n  ) {\n    super(ctx, { id, width, height, position: \"absolute\", left: x, top: y, respectAlpha })\n\n    // Fill the internal framebuffer with graphemes\n    this.frameBuffer.clear(RGBA.fromInts(0, 0, 0, Math.round(bg.a * 255)))\n\n    const fg = RGBA.fromInts(255, 255, 255, 255)\n    let row = 0\n    for (const line of GRAPHEME_LINES) {\n      if (row >= height) break\n      this.frameBuffer.drawText(line, 1, row, fg, bg)\n      row += 1\n    }\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    switch (event.type) {\n      case \"down\":\n        this.isDragging = true\n        this.dragOffsetX = event.x - this.x\n        this.dragOffsetY = event.y - this.y\n        event.stopPropagation()\n        break\n      case \"drag\":\n        if (this.isDragging) {\n          this.x = event.x - this.dragOffsetX\n          this.y = event.y - this.dragOffsetY\n          event.stopPropagation()\n        }\n        break\n      case \"drag-end\":\n        if (this.isDragging) {\n          this.isDragging = false\n          event.stopPropagation()\n        }\n        break\n    }\n  }\n}\n\nclass GraphemeBackground extends FrameBufferRenderable {\n  constructor(ctx: RenderContext, id: string, width: number, height: number) {\n    super(ctx, { id, width, height, position: \"absolute\", left: 0, top: 0, respectAlpha: false })\n\n    // Fill entire background with repeating grapheme lines\n    const fg = RGBA.fromInts(220, 220, 220, 255)\n    const bg = RGBA.fromInts(0, 17, 34, 255)\n    this.frameBuffer.clear(RGBA.fromInts(0, 17, 34, 255))\n    for (let y = 0; y < height; y++) {\n      const line = GRAPHEME_LINES[y % GRAPHEME_LINES.length]\n      this.frameBuffer.drawText(line, 2, y, fg, bg)\n    }\n  }\n}\n\nclass DraggableStyledText extends TextRenderable {\n  private isDragging = false\n  private dragOffsetX = 0\n  private dragOffsetY = 0\n\n  constructor(ctx: RenderContext, id: string, x: number, y: number) {\n    super(ctx, {\n      id,\n      position: \"absolute\",\n      left: x,\n      top: y,\n      zIndex: 2,\n      selectable: false,\n    })\n\n    // Styled text content with graphemes\n    const content = t`${bold(blue(\"Graphemes:\"))} 👩🏽‍💻  👨‍👩‍👧‍👦  🏳️‍🌈  🇺🇸  🇩🇪  🇯🇵  🇮🇳\n${underline(\"Complex:\")} a̐éö̲  Z͑͗͛̒͘a̴͈͚̐̓l̷͓̱͉g̶̙̗̓͘o̵͍͈  क्‍ष`\n\n    this.content = content\n    this.fg = RGBA.fromInts(255, 255, 255, 255)\n    this.bg = RGBA.fromInts(0, 0, 0, 0)\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    switch (event.type) {\n      case \"down\":\n        this.isDragging = true\n        this.dragOffsetX = event.x - this.x\n        this.dragOffsetY = event.y - this.y\n        event.stopPropagation()\n        break\n      case \"drag\":\n        if (this.isDragging) {\n          this.x = event.x - this.dragOffsetX\n          this.y = event.y - this.dragOffsetY\n          event.stopPropagation()\n        }\n        break\n      case \"drag-end\":\n        if (this.isDragging) {\n          this.isDragging = false\n          event.stopPropagation()\n        }\n        break\n    }\n  }\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  renderer.setBackgroundColor(RGBA.fromInts(0, 17, 34, 255))\n\n  const rootGroup = new BoxRenderable(renderer, { id: \"full-unicode-root\", zIndex: 1 })\n  renderer.root.add(rootGroup)\n\n  const bg = new GraphemeBackground(renderer, \"grapheme-bg\", renderer.terminalWidth, renderer.terminalHeight)\n  rootGroup.add(bg)\n\n  const box1 = new DraggableGraphemeBox(renderer, \"grapheme-box-1\", 6, 4, 30, 6, RGBA.fromInts(32, 96, 192, 160), true)\n  const box2 = new DraggableGraphemeBox(\n    renderer,\n    \"grapheme-box-2\",\n    24,\n    10,\n    28,\n    6,\n    RGBA.fromInts(192, 96, 128, 180),\n    true,\n  )\n  const box3 = new DraggableGraphemeBox(renderer, \"grapheme-box-3\", 42, 7, 26, 6, RGBA.fromInts(64, 176, 96, 128), true)\n\n  rootGroup.add(box1)\n  rootGroup.add(box2)\n  rootGroup.add(box3)\n\n  // Draggable styled text using TextRenderable (grapheme-aware via TextBuffer)\n  const styledText = new DraggableStyledText(renderer, \"draggable-styled-text\", 8, 12)\n  rootGroup.add(styledText)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.root.remove(\"full-unicode-root\")\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({ exitOnCtrlC: true })\n  run(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/golden-star-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport { createCliRenderer, CliRenderer, FrameBufferRenderable, BoxRenderable, OptimizedBuffer } from \"../index.js\"\nimport { RGBA } from \"../lib/index.js\"\nimport { ASCIIFontRenderable } from \"../renderables/ASCIIFont.js\"\nimport type { ASCIIFontName } from \"../lib/ascii.font.js\"\nimport {\n  Scene as ThreeScene,\n  Mesh as ThreeMesh,\n  PerspectiveCamera,\n  Color,\n  MeshPhongMaterial,\n  AmbientLight,\n  DirectionalLight as ThreeDirectionalLight,\n  PointLight as ThreePointLight,\n  ExtrudeGeometry,\n  Shape,\n  BoxGeometry,\n  BackSide,\n  InstancedMesh,\n  Matrix4,\n  Vector3,\n  Euler,\n  Quaternion,\n  ConeGeometry,\n} from \"three\"\nimport { ThreeCliRenderer } from \"../3d.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\ninterface StarParticle {\n  instanceIndex: number\n  meshIndex: number\n  localInstanceIndex: number\n  position: Vector3\n  velocity: Vector3\n  rotation: Euler\n  angularVelocity: Vector3\n  lifetime: number\n  maxLifetime: number\n  scale: number\n}\n\nclass StarParticleSystem {\n  private particles: StarParticle[] = []\n  private instancedMeshes: InstancedMesh[] = []\n  private maxParticles: number\n  private freeIndices: number[] = []\n  private emitterPosition: Vector3 = new Vector3(0, 0, 0)\n  private tempMatrix: Matrix4 = new Matrix4()\n  private tempPosition: Vector3 = new Vector3()\n  private tempQuaternion = new Quaternion()\n  private tempScale: Vector3 = new Vector3()\n  private gravity: number = -2.5\n  private normalGravity: number = -2.5\n  private hellGravity: number = -0.5\n  private colors: Color[] = []\n  private cyberColors: Color[] = []\n  private hellColors: Color[] = []\n  private materials: MeshPhongMaterial[] = []\n  private isHellMode: boolean = false\n\n  constructor(scene: ThreeScene, maxParticles: number = 100) {\n    this.maxParticles = maxParticles\n\n    const starShape = this.createMiniStarShape(0.18, 0.07, 5)\n    const extrudeSettings = {\n      depth: 0.05,\n      bevelEnabled: true,\n      bevelThickness: 0.012,\n      bevelSize: 0.012,\n      bevelSegments: 2,\n    }\n    const geometry = new ExtrudeGeometry(starShape, extrudeSettings)\n\n    this.cyberColors = [\n      new Color(0.0, 0.9, 1.0),\n      new Color(0.2, 0.7, 1.0),\n      new Color(0.4, 0.5, 1.0),\n      new Color(0.6, 0.3, 1.0),\n      new Color(0.8, 0.2, 1.0),\n      new Color(1.0, 0.2, 0.9),\n      new Color(1.0, 0.3, 0.7),\n      new Color(1.0, 0.85, 0.2),\n      new Color(1.0, 0.75, 0.3),\n      new Color(0.9, 0.7, 0.25),\n      new Color(0.0, 0.9, 1.0),\n      new Color(0.6, 0.3, 1.0),\n      new Color(1.0, 0.85, 0.2),\n      new Color(0.0, 1.0, 0.85),\n      new Color(0.5, 0.2, 1.0),\n      new Color(1.0, 0.15, 0.85),\n      new Color(0.1, 0.6, 1.0),\n      new Color(0.9, 0.1, 1.0),\n    ]\n\n    this.hellColors = [\n      new Color(1.0, 0.0, 0.0),\n      new Color(1.0, 0.15, 0.0),\n      new Color(1.0, 0.3, 0.0),\n      new Color(1.0, 0.5, 0.0),\n      new Color(1.0, 0.65, 0.0),\n      new Color(1.0, 0.8, 0.0),\n      new Color(1.0, 0.95, 0.0),\n      new Color(1.0, 0.0, 0.0),\n      new Color(1.0, 0.15, 0.0),\n      new Color(1.0, 0.5, 0.0),\n      new Color(0.8, 0.0, 0.0),\n      new Color(0.9, 0.1, 0.0),\n      new Color(1.0, 0.8, 0.0),\n      new Color(0.6, 0.0, 0.0),\n      new Color(0.5, 0.05, 0.0),\n      new Color(1.0, 0.4, 0.0),\n      new Color(1.0, 1.0, 0.0),\n      new Color(0.7, 0.0, 0.1),\n    ]\n\n    this.colors = this.cyberColors\n\n    const particlesPerColor = Math.ceil(maxParticles / this.colors.length)\n    for (let colorIdx = 0; colorIdx < this.colors.length; colorIdx++) {\n      const color = this.colors[colorIdx]\n      const material = new MeshPhongMaterial({\n        color: color,\n        emissive: color,\n        emissiveIntensity: 0.65,\n        shininess: 55,\n      })\n      this.materials.push(material)\n\n      const mesh = new InstancedMesh(geometry, material, particlesPerColor)\n      mesh.instanceMatrix.setUsage(35048)\n      mesh.renderOrder = -1\n      mesh.frustumCulled = false\n\n      const hiddenMatrix = new Matrix4().scale(new Vector3(0, 0, 0))\n      for (let i = 0; i < particlesPerColor; i++) {\n        mesh.setMatrixAt(i, hiddenMatrix)\n      }\n      mesh.instanceMatrix.needsUpdate = true\n\n      this.instancedMeshes.push(mesh)\n      scene.add(mesh)\n    }\n\n    for (let i = 0; i < maxParticles; i++) {\n      this.freeIndices.push(i)\n    }\n  }\n\n  private createMiniStarShape(outerRadius: number, innerRadius: number, points: number = 5): Shape {\n    const shape = new Shape()\n    const angleStep = (Math.PI * 2) / points\n\n    for (let i = 0; i < points * 2; i++) {\n      const angle = (i * angleStep) / 2 - Math.PI / 2\n      const radius = i % 2 === 0 ? outerRadius : innerRadius\n      const x = Math.cos(angle) * radius\n      const y = Math.sin(angle) * radius\n\n      if (i === 0) {\n        shape.moveTo(x, y)\n      } else {\n        shape.lineTo(x, y)\n      }\n    }\n    shape.closePath()\n    return shape\n  }\n\n  setEmitterPosition(x: number, y: number, z: number) {\n    this.emitterPosition.set(x, y, z)\n  }\n\n  emit(count: number = 1) {\n    for (let i = 0; i < count; i++) {\n      if (this.freeIndices.length === 0) {\n        const oldestParticle = this.particles.shift()\n        if (oldestParticle) {\n          this.freeIndices.push(oldestParticle.instanceIndex)\n        } else {\n          break\n        }\n      }\n\n      const instanceIndex = this.freeIndices.pop()!\n\n      const particlesPerColor = Math.ceil(this.maxParticles / this.colors.length)\n      const meshIndex = Math.floor(instanceIndex / particlesPerColor)\n      const localInstanceIndex = instanceIndex % particlesPerColor\n\n      const spreadAngle = (Math.random() - 0.5) * Math.PI * 0.8\n      const upwardBias = Math.random() * 0.7 + 0.4\n\n      const baseSpeed = this.isHellMode ? Math.random() * 1.0 + 0.8 : Math.random() * 2.0 + 1.5\n\n      const velocity = new Vector3(\n        Math.sin(spreadAngle) * baseSpeed * 0.9,\n        upwardBias * baseSpeed,\n        Math.random() * 0.3 - 0.5,\n      )\n\n      const particle: StarParticle = {\n        instanceIndex,\n        meshIndex,\n        localInstanceIndex,\n        position: new Vector3(\n          this.emitterPosition.x + (Math.random() - 0.5) * 0.1,\n          this.emitterPosition.y - 0.1,\n          this.emitterPosition.z - 0.2,\n        ),\n        velocity,\n        rotation: new Euler(Math.random() * Math.PI * 2, Math.random() * Math.PI * 2, Math.random() * Math.PI * 2),\n        angularVelocity: new Vector3(\n          (Math.random() - 0.5) * 10,\n          (Math.random() - 0.5) * 10,\n          (Math.random() - 0.5) * 10,\n        ),\n        lifetime: 0,\n        maxLifetime: 5.0 + Math.random() * 3.0, // 5.0-8.0 seconds (much longer lifetime)\n        scale: 0.8 + Math.random() * 0.4, // Slight size variation\n      }\n\n      this.particles.push(particle)\n    }\n  }\n\n  update(deltaTime: number) {\n    for (let i = this.particles.length - 1; i >= 0; i--) {\n      const particle = this.particles[i]\n      particle.lifetime += deltaTime\n\n      if (particle.lifetime >= particle.maxLifetime) {\n        this.particles.splice(i, 1)\n        this.freeIndices.push(particle.instanceIndex)\n        const hiddenMatrix = new Matrix4().scale(new Vector3(0, 0, 0))\n        const mesh = this.instancedMeshes[particle.meshIndex]\n        if (mesh) {\n          mesh.setMatrixAt(particle.localInstanceIndex, hiddenMatrix)\n          mesh.instanceMatrix.needsUpdate = true\n        }\n        continue\n      }\n\n      particle.velocity.y += this.gravity * deltaTime\n      particle.position.x += particle.velocity.x * deltaTime\n      particle.position.y += particle.velocity.y * deltaTime\n      particle.position.z += particle.velocity.z * deltaTime\n\n      particle.rotation.x += particle.angularVelocity.x * deltaTime\n      particle.rotation.y += particle.angularVelocity.y * deltaTime\n      particle.rotation.z += particle.angularVelocity.z * deltaTime\n\n      const lifeRatio = particle.lifetime / particle.maxLifetime\n      const fadeStart = 0.7\n      const alpha = lifeRatio > fadeStart ? 1.0 - (lifeRatio - fadeStart) / (1.0 - fadeStart) : 1.0\n      const scale = particle.scale * alpha\n\n      this.tempQuaternion.setFromEuler(particle.rotation)\n      this.tempMatrix.compose(particle.position, this.tempQuaternion, this.tempScale.set(scale, scale, scale))\n\n      const mesh = this.instancedMeshes[particle.meshIndex]\n      if (mesh) {\n        mesh.setMatrixAt(particle.localInstanceIndex, this.tempMatrix)\n      }\n    }\n\n    for (const mesh of this.instancedMeshes) {\n      mesh.instanceMatrix.needsUpdate = true\n    }\n  }\n\n  setHellMode(isHellMode: boolean) {\n    this.isHellMode = isHellMode\n    this.colors = isHellMode ? this.hellColors : this.cyberColors\n    this.gravity = isHellMode ? this.hellGravity : this.normalGravity\n\n    for (let i = 0; i < this.materials.length && i < this.colors.length; i++) {\n      const color = this.colors[i]\n      this.materials[i].color.copy(color)\n      this.materials[i].emissive.copy(color)\n    }\n  }\n\n  dispose() {\n    for (const mesh of this.instancedMeshes) {\n      mesh.geometry.dispose()\n      if (Array.isArray(mesh.material)) {\n        mesh.material.forEach((mat) => mat.dispose())\n      } else {\n        mesh.material.dispose()\n      }\n    }\n  }\n}\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const WIDTH = renderer.terminalWidth\n  const HEIGHT = renderer.terminalHeight\n  const CAM_DISTANCE = 3\n\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"golden-star-main\",\n    width: WIDTH,\n    height: HEIGHT,\n    zIndex: 10,\n  })\n  renderer.root.add(framebufferRenderable)\n  const framebuffer = framebufferRenderable.frameBuffer\n\n  const engine = new ThreeCliRenderer(renderer, {\n    width: WIDTH,\n    height: HEIGHT,\n    focalLength: 8,\n    backgroundColor: RGBA.fromInts(0, 0, 0, 0),\n    alpha: true,\n  })\n  await engine.init()\n\n  const sceneRoot = new ThreeScene()\n\n  // Create the room (cube with inverted normals)\n  const roomSize = 10\n  const roomGeometry = new BoxGeometry(roomSize, roomSize, roomSize)\n  const roomMaterial = new MeshPhongMaterial({\n    color: new Color(0.1, 0.05, 0.15), // Deep purple-black walls\n    side: BackSide, // Render inside of the cube\n    shininess: 10,\n  })\n  const roomMesh = new ThreeMesh(roomGeometry, roomMaterial)\n  sceneRoot.add(roomMesh)\n\n  // Create ambient light for base illumination - slightly brighter\n  const ambientLightNode = new AmbientLight(new Color(0.2, 0.15, 0.25), 0.5)\n  sceneRoot.add(ambientLightNode)\n\n  // Key light - main directional light from top-front - more intense\n  const keyLight = new ThreeDirectionalLight(new Color(1.0, 0.95, 0.85), 2.5)\n  keyLight.position.set(3, 4, 5)\n  keyLight.target.position.set(0, 0, 0)\n  sceneRoot.add(keyLight)\n  sceneRoot.add(keyLight.target)\n\n  const lightningDirectionalLight = new ThreeDirectionalLight(new Color(1.0, 0.98, 0.95), 0.0)\n  lightningDirectionalLight.position.set(0, 5, 8)\n  lightningDirectionalLight.target.position.set(0, 0, 0)\n  lightningDirectionalLight.name = \"lightningDirectional\"\n  sceneRoot.add(lightningDirectionalLight)\n  sceneRoot.add(lightningDirectionalLight.target)\n\n  const movingLight1 = new ThreePointLight(new Color(0.0, 0.9, 1.0), 15.0, 25)\n  movingLight1.position.set(0, 0, -3)\n  movingLight1.name = \"movingLight1\"\n  sceneRoot.add(movingLight1)\n\n  const movingLight2 = new ThreePointLight(new Color(1.0, 0.2, 0.9), 15.0, 25)\n  movingLight2.position.set(0, 0, -3)\n  movingLight2.name = \"movingLight2\"\n  sceneRoot.add(movingLight2)\n\n  const movingLight3 = new ThreePointLight(new Color(0.6, 0.3, 1.0), 15.0, 25)\n  movingLight3.position.set(0, 0, -3)\n  movingLight3.name = \"movingLight3\"\n  sceneRoot.add(movingLight3)\n\n  const movingLight4 = new ThreePointLight(new Color(1.0, 0.85, 0.2), 15.0, 25)\n  movingLight4.position.set(0, 0, -3)\n  movingLight4.name = \"movingLight4\"\n  sceneRoot.add(movingLight4)\n\n  const lightningLight1 = new ThreePointLight(new Color(1.0, 0.95, 0.9), 0.0, 50)\n  lightningLight1.position.set(-2, 2, 5)\n  lightningLight1.name = \"lightningLight1\"\n  sceneRoot.add(lightningLight1)\n\n  const lightningLight2 = new ThreePointLight(new Color(1.0, 0.95, 0.9), 0.0, 50)\n  lightningLight2.position.set(2, 1, 5)\n  lightningLight2.name = \"lightningLight2\"\n  sceneRoot.add(lightningLight2)\n\n  const lightningLight3 = new ThreePointLight(new Color(1.0, 0.95, 0.9), 0.0, 50)\n  lightningLight3.position.set(0, 3, 6)\n  lightningLight3.name = \"lightningLight3\"\n  sceneRoot.add(lightningLight3)\n\n  function createStarShape(outerRadius: number, innerRadius: number, points: number = 5): Shape {\n    const shape = new Shape()\n    const angleStep = (Math.PI * 2) / points\n\n    for (let i = 0; i < points * 2; i++) {\n      const angle = (i * angleStep) / 2 - Math.PI / 2\n      const radius = i % 2 === 0 ? outerRadius : innerRadius\n      const x = Math.cos(angle) * radius\n      const y = Math.sin(angle) * radius\n\n      if (i === 0) {\n        shape.moveTo(x, y)\n      } else {\n        shape.lineTo(x, y)\n      }\n    }\n    shape.closePath()\n    return shape\n  }\n\n  const starShape = createStarShape(1.0, 0.4, 5)\n  const extrudeSettings = {\n    depth: 0.3,\n    bevelEnabled: true,\n    bevelThickness: 0.08,\n    bevelSize: 0.08,\n    bevelSegments: 5,\n  }\n\n  const starGeometry = new ExtrudeGeometry(starShape, extrudeSettings)\n\n  const goldenMaterial = new MeshPhongMaterial({\n    color: new Color(1.0, 0.88, 0.2),\n    specular: new Color(1.0, 1.0, 0.85),\n    shininess: 200,\n    emissive: new Color(0.7, 0.55, 0.15),\n    emissiveIntensity: 1.2,\n    flatShading: false,\n  })\n\n  const starMeshNode = new ThreeMesh(starGeometry, goldenMaterial)\n  starMeshNode.name = \"star\"\n  starMeshNode.rotation.z = Math.PI\n  sceneRoot.add(starMeshNode)\n\n  const hornGeometry = new ConeGeometry(0.15, 0.6, 8)\n  const hornMaterial = new MeshPhongMaterial({\n    color: new Color(0.85, 0.85, 0.85),\n    emissive: new Color(0.1, 0.1, 0.1),\n    shininess: 50,\n  })\n\n  const leftHorn = new ThreeMesh(hornGeometry, hornMaterial)\n  leftHorn.name = \"leftHorn\"\n  leftHorn.position.set(-0.32, -0.5, 0.4)\n  leftHorn.rotation.set(Math.PI - 0.6, 0, 0.25)\n  leftHorn.visible = false\n  starMeshNode.add(leftHorn)\n\n  const rightHorn = new ThreeMesh(hornGeometry, hornMaterial)\n  rightHorn.name = \"rightHorn\"\n  rightHorn.position.set(0.32, -0.5, 0.4)\n  rightHorn.rotation.set(Math.PI - 0.6, 0, -0.25)\n  rightHorn.visible = false\n  starMeshNode.add(rightHorn)\n\n  const cameraNode = new PerspectiveCamera(45, engine.aspectRatio, 1.0, 100.0)\n  cameraNode.position.set(0, 0, CAM_DISTANCE)\n  cameraNode.name = \"main_camera\"\n\n  sceneRoot.add(cameraNode)\n  engine.setActiveCamera(cameraNode)\n\n  const particleSystem = new StarParticleSystem(sceneRoot, 150)\n\n  const resizeHandler = (width: number, height: number) => {\n    if (framebuffer) {\n      framebuffer.resize(width, height)\n    }\n    if (cameraNode) {\n      cameraNode.aspect = engine.aspectRatio\n      cameraNode.updateProjectionMatrix()\n    }\n    updateGradientBand()\n  }\n\n  renderer.on(\"resize\", resizeHandler)\n\n  const bandPadding = 2\n  const gradientBand = new FrameBufferRenderable(renderer, {\n    id: \"gradientBand\",\n    position: \"absolute\",\n    left: 0,\n    top: 0,\n    width: renderer.terminalWidth,\n    height: 1,\n    zIndex: 50,\n    respectAlpha: true,\n  })\n\n  gradientBand.visible = true\n\n  renderer.root.add(gradientBand)\n\n  function updateGradientBand() {\n    const opentuiHeight = opentuiContainer.height\n    const fiveKHeight = fiveKContainer.height\n\n    if (opentuiHeight === 0 || fiveKHeight === 0) {\n      return\n    }\n\n    const opentuiAbsY = overlayContainer.y + opentuiContainer.y\n    const fiveKAbsY = overlayContainer.y + fiveKContainer.y\n\n    const bandTop = Math.max(0, opentuiAbsY - bandPadding)\n    const bandBottom = fiveKAbsY + fiveKHeight + bandPadding\n    const bandHeight = Math.max(1, bandBottom - bandTop)\n    const bandWidth = renderer.terminalWidth\n\n    gradientBand.top = bandTop\n    gradientBand.height = bandHeight\n    gradientBand.width = bandWidth\n\n    const bandBuffer = gradientBand.frameBuffer\n    if (!bandBuffer) return\n\n    if (bandBuffer.width !== bandWidth || bandBuffer.height !== bandHeight) {\n      bandBuffer.resize(bandWidth, bandHeight)\n    }\n\n    bandBuffer.clear(RGBA.fromInts(0, 0, 0, 0))\n\n    for (let y = 0; y < bandHeight; y++) {\n      const distFromCenter = Math.abs(y - bandHeight / 2) / (bandHeight / 2)\n      const alpha = (1.0 - distFromCenter * 0.7) * 0.5\n\n      const color = RGBA.fromValues(0.04, 0.02, 0.1, alpha)\n      bandBuffer.fillRect(0, y, bandWidth, 1, color)\n    }\n  }\n\n  let isHellMode = false\n\n  renderer.keyInput.on(\"keypress\", (keyEvent) => {\n    if (keyEvent.name === \"b\") {\n      gradientBand.visible = !gradientBand.visible\n    } else if (keyEvent.name === \"h\") {\n      isHellMode = !isHellMode\n      particleSystem.setHellMode(isHellMode)\n\n      // Update main star material and show/hide horns\n      const leftHornNode = starMeshNode.getObjectByName(\"leftHorn\")\n      const rightHornNode = starMeshNode.getObjectByName(\"rightHorn\")\n\n      if (isHellMode) {\n        goldenMaterial.color.setRGB(0.4, 0.0, 0.0)\n        goldenMaterial.emissive.setRGB(0.2, 0.0, 0.0)\n        goldenMaterial.specular.setRGB(0.5, 0.1, 0.1)\n        roomMaterial.color.setRGB(0.15, 0.02, 0.0)\n        ambientLightNode.color.setRGB(0.3, 0.05, 0.0)\n        keyLight.color.setRGB(1.0, 0.4, 0.1)\n        movingLight1.color.setRGB(1.0, 0.0, 0.0)\n        movingLight2.color.setRGB(1.0, 0.3, 0.0)\n        movingLight3.color.setRGB(1.0, 0.5, 0.0)\n        movingLight4.color.setRGB(1.0, 0.8, 0.0)\n\n        for (const char of opentuiChars) {\n          char.color = [RGBA.fromInts(255, 40, 0, 255), RGBA.fromInts(200, 0, 0, 255)]\n        }\n        for (const char of fiveKChars) {\n          char.color = [RGBA.fromInts(255, 100, 0, 255), RGBA.fromInts(255, 200, 0, 255)]\n        }\n\n        if (leftHornNode) leftHornNode.visible = true\n        if (rightHornNode) rightHornNode.visible = true\n        nextLightningTime = elapsedTime + 0.1\n      } else {\n        goldenMaterial.color.setRGB(1.0, 0.88, 0.2)\n        goldenMaterial.emissive.setRGB(0.7, 0.55, 0.15)\n        goldenMaterial.specular.setRGB(1.0, 1.0, 0.85)\n        roomMaterial.color.setRGB(0.1, 0.05, 0.15)\n        ambientLightNode.color.setRGB(0.2, 0.15, 0.25)\n        keyLight.color.setRGB(1.0, 0.95, 0.85)\n        movingLight1.color.setRGB(0.0, 0.9, 1.0)\n        movingLight2.color.setRGB(1.0, 0.2, 0.9)\n        movingLight3.color.setRGB(0.6, 0.3, 1.0)\n        movingLight4.color.setRGB(1.0, 0.85, 0.2)\n\n        for (const char of opentuiChars) {\n          char.color = [RGBA.fromInts(255, 80, 120, 255), RGBA.fromInts(255, 60, 215, 255)]\n        }\n        for (const char of fiveKChars) {\n          char.color = [RGBA.fromInts(50, 255, 120, 255), RGBA.fromInts(100, 255, 150, 255)]\n        }\n\n        if (leftHornNode) leftHornNode.visible = false\n        if (rightHornNode) rightHornNode.visible = false\n      }\n    }\n  })\n\n  const overlayContainer = new BoxRenderable(renderer, {\n    id: \"overlay\",\n    position: \"absolute\",\n    left: 0,\n    top: 0,\n    width: \"100%\",\n    height: \"100%\",\n    zIndex: 100,\n    justifyContent: \"center\",\n    alignItems: \"center\",\n    paddingTop: 10,\n  })\n  renderer.root.add(overlayContainer)\n\n  const opentuiChars: ASCIIFontRenderable[] = []\n  const opentuiContainer = new BoxRenderable(renderer, {\n    id: \"opentuiContainer\",\n    flexDirection: \"row\",\n    justifyContent: \"center\",\n    alignItems: \"flex-end\",\n    zIndex: 101,\n  })\n  overlayContainer.add(opentuiContainer)\n\n  const opentuiText = \"opentui\"\n  for (let i = 0; i < opentuiText.length; i++) {\n    const char = new ASCIIFontRenderable(renderer, {\n      id: `opentui-char-${i}`,\n      text: opentuiText[i],\n      font: \"block\" as ASCIIFontName,\n      color: [RGBA.fromInts(255, 80, 120, 255), RGBA.fromInts(255, 60, 215, 255)],\n      backgroundColor: RGBA.fromInts(0, 0, 0, 0),\n      zIndex: 101,\n    })\n    opentuiContainer.add(char)\n    opentuiChars.push(char)\n  }\n\n  const fiveKChars: ASCIIFontRenderable[] = []\n  const fiveKContainer = new BoxRenderable(renderer, {\n    id: \"fiveKContainer\",\n    flexDirection: \"row\",\n    justifyContent: \"center\",\n    alignItems: \"flex-end\",\n    top: 2,\n    zIndex: 101,\n  })\n  overlayContainer.add(fiveKContainer)\n\n  const fiveKText = \"5000\"\n  for (let i = 0; i < fiveKText.length; i++) {\n    const char = new ASCIIFontRenderable(renderer, {\n      id: `fivek-char-${i}`,\n      text: fiveKText[i],\n      font: \"huge\" as ASCIIFontName,\n      color: [RGBA.fromInts(50, 255, 120, 255), RGBA.fromInts(100, 255, 150, 255)],\n      backgroundColor: RGBA.fromInts(0, 0, 0, 0),\n      zIndex: 101,\n    })\n    fiveKContainer.add(char)\n    fiveKChars.push(char)\n  }\n\n  opentuiContainer.onSizeChange = updateGradientBand\n  fiveKContainer.onSizeChange = updateGradientBand\n  overlayContainer.onSizeChange = updateGradientBand\n\n  for (const char of opentuiChars) {\n    char.onSizeChange = updateGradientBand\n  }\n  for (const char of fiveKChars) {\n    char.onSizeChange = updateGradientBand\n  }\n\n  let elapsedTime = 0\n  let initialLayoutFrames = 3\n  let jumpPhase = 0\n  let randomOffset = 0\n  let nextRandomTime = 0\n  let targetTiltX = 0\n  let targetTiltZ = 0\n  let currentTiltX = 0\n  let currentTiltZ = 0\n  let targetRotationY = 0\n  let currentRotationY = 0\n  let nextRotationChangeTime = 0\n  let particleEmitAccumulator = 0\n  const particleEmitRate = 0.03\n  let headbangPhase = 0\n\n  let waveStartTime = 0\n  const waveCycleDuration = 2.5\n  const waveWaitTime = 1.0\n  const charJumpDuration = 0.5\n  const charDelay = 0.1\n  const charRandomOffsets: number[] = [...opentuiChars, ...fiveKChars].map(() => Math.random() * 0.1 - 0.05)\n  let nextRandomRefresh = elapsedTime + 3.0\n\n  interface LightningStrike {\n    light: ThreePointLight\n    startTime: number\n    duration: number\n    maxIntensity: number\n    flickerPattern: number[]\n  }\n  let activeLightningStrikes: LightningStrike[] = []\n  let nextLightningTime = 0\n\n  renderer.setFrameCallback(async (deltaMs) => {\n    const deltaTime = deltaMs / 1000\n    elapsedTime += deltaTime\n    particleEmitAccumulator += deltaTime\n\n    if (initialLayoutFrames > 0) {\n      updateGradientBand()\n      initialLayoutFrames--\n    }\n\n    const starObject = sceneRoot.getObjectByName(\"star\") as ThreeMesh | undefined\n\n    if (starObject) {\n      if (isHellMode) {\n        headbangPhase = elapsedTime * 8.0\n\n        const headbangIntensity = 0.6\n        const headbangTilt = Math.sin(headbangPhase) * headbangIntensity\n\n        const sideHeadbang = Math.sin(headbangPhase * 0.5) * 0.3\n\n        const verticalBob = Math.sin(headbangPhase) * 0.08\n        starObject.position.y = verticalBob\n\n        starObject.rotation.x = headbangTilt\n        starObject.rotation.y = sideHeadbang\n        starObject.rotation.z = Math.PI\n\n        const scalePulse = 1.0 + Math.abs(Math.sin(headbangPhase)) * 0.08\n        starObject.scale.set(scalePulse, scalePulse, scalePulse)\n      } else {\n        if (elapsedTime > nextRotationChangeTime) {\n          const maxAngle = Math.PI / 3\n          targetRotationY = (Math.random() - 0.5) * 2 * maxAngle\n          nextRotationChangeTime = elapsedTime + 0.4 + Math.random() * 0.8\n        }\n\n        const rotationLerpSpeed = 2.0 * deltaTime\n        currentRotationY += (targetRotationY - currentRotationY) * rotationLerpSpeed\n        starObject.rotation.y = currentRotationY\n\n        if (elapsedTime > nextRandomTime) {\n          randomOffset = (Math.random() - 0.5) * 0.2\n          targetTiltX = (Math.random() - 0.5) * 0.15\n          targetTiltZ = (Math.random() - 0.5) * 0.15\n          nextRandomTime = elapsedTime + 0.3 + Math.random() * 0.5\n        }\n\n        const tiltLerpSpeed = 5.0 * deltaTime\n        currentTiltX += (targetTiltX - currentTiltX) * tiltLerpSpeed\n        currentTiltZ += (targetTiltZ - currentTiltZ) * tiltLerpSpeed\n\n        jumpPhase = elapsedTime * 6.0\n        const rawJump = Math.sin(jumpPhase)\n        const easedJump = rawJump < 0 ? rawJump : Math.pow(rawJump, 0.6)\n        const jump = easedJump * 0.25 + randomOffset * 0.1\n        starObject.position.y = jump\n\n        const squashAmount = 0.08\n        const landingSquash = Math.max(0, -rawJump) * squashAmount\n        const scaleY = 1.0 - landingSquash\n        const scaleXZ = 1.0 + landingSquash * 0.5\n\n        const pulsate = Math.sin(elapsedTime * 3) * 0.03 + 1.0 + randomOffset * 0.05\n        starObject.scale.set(pulsate * scaleXZ, pulsate * scaleY, pulsate * scaleXZ)\n\n        const jumpInfluence = Math.max(0, rawJump)\n        starObject.rotation.x = currentTiltX * jumpInfluence\n        starObject.rotation.z = Math.PI + currentTiltZ * jumpInfluence\n      }\n\n      particleSystem.setEmitterPosition(starObject.position.x, starObject.position.y, starObject.position.z)\n\n      while (particleEmitAccumulator >= particleEmitRate) {\n        particleSystem.emit(3)\n        particleEmitAccumulator -= particleEmitRate\n      }\n    }\n\n    particleSystem.update(deltaTime)\n\n    const light1 = sceneRoot.getObjectByName(\"movingLight1\") as ThreePointLight | undefined\n    const light2 = sceneRoot.getObjectByName(\"movingLight2\") as ThreePointLight | undefined\n    const light3 = sceneRoot.getObjectByName(\"movingLight3\") as ThreePointLight | undefined\n    const light4 = sceneRoot.getObjectByName(\"movingLight4\") as ThreePointLight | undefined\n\n    const radius = 2.5\n    const speed = 1.5\n\n    if (light1) {\n      light1.position.x = Math.cos(elapsedTime * speed) * radius\n      light1.position.y = Math.sin(elapsedTime * speed) * radius\n      light1.position.z = -3\n    }\n\n    if (light2) {\n      light2.position.x = Math.cos(elapsedTime * speed + Math.PI / 2) * radius\n      light2.position.y = Math.sin(elapsedTime * speed + Math.PI / 2) * radius\n      light2.position.z = -3\n    }\n\n    if (light3) {\n      light3.position.x = Math.cos(elapsedTime * speed + Math.PI) * radius\n      light3.position.y = Math.sin(elapsedTime * speed + Math.PI) * radius\n      light3.position.z = -3\n    }\n\n    if (light4) {\n      light4.position.x = Math.cos(elapsedTime * speed + (3 * Math.PI) / 2) * radius\n      light4.position.y = Math.sin(elapsedTime * speed + (3 * Math.PI) / 2) * radius\n      light4.position.z = -3\n    }\n\n    if (isHellMode) {\n      if (elapsedTime >= nextLightningTime) {\n        const lightningLights = [\n          sceneRoot.getObjectByName(\"lightningLight1\") as ThreePointLight | undefined,\n          sceneRoot.getObjectByName(\"lightningLight2\") as ThreePointLight | undefined,\n          sceneRoot.getObjectByName(\"lightningLight3\") as ThreePointLight | undefined,\n        ].filter((l) => l !== undefined) as ThreePointLight[]\n\n        const numStrikes = Math.random() < 0.4 ? 1 : Math.random() < 0.7 ? 2 : 3\n        const availableLights = [...lightningLights].sort(() => Math.random() - 0.5)\n\n        for (let i = 0; i < Math.min(numStrikes, availableLights.length); i++) {\n          const light = availableLights[i]\n          const flickerPattern = [130, 25, 160, 120, 75, 35, 10, 0]\n          const duration = 0.25\n\n          activeLightningStrikes.push({\n            light,\n            startTime: elapsedTime,\n            duration,\n            maxIntensity: 100 + Math.random() * 50,\n            flickerPattern,\n          })\n        }\n\n        nextLightningTime = elapsedTime + 0.15 + Math.random() * 0.85\n      }\n\n      const lightningDir = sceneRoot.getObjectByName(\"lightningDirectional\") as ThreeDirectionalLight | undefined\n      if (lightningDir && activeLightningStrikes.length > 0) {\n        const maxStrikeIntensity = Math.max(...activeLightningStrikes.map((s) => s.light.intensity))\n        lightningDir.intensity = maxStrikeIntensity * 0.2\n      } else if (lightningDir) {\n        lightningDir.intensity = 0\n      }\n\n      for (let i = activeLightningStrikes.length - 1; i >= 0; i--) {\n        const strike = activeLightningStrikes[i]\n        const strikeAge = elapsedTime - strike.startTime\n\n        if (strikeAge >= strike.duration) {\n          strike.light.intensity = 0\n          activeLightningStrikes.splice(i, 1)\n        } else {\n          const progress = strikeAge / strike.duration\n          const patternIndex = Math.floor(progress * strike.flickerPattern.length)\n          const patternValue = strike.flickerPattern[Math.min(patternIndex, strike.flickerPattern.length - 1)]\n          strike.light.intensity = (patternValue / 120) * strike.maxIntensity\n\n          const microFlicker = 1.0 + (Math.random() - 0.5) * 0.4\n          strike.light.intensity *= microFlicker\n\n          const wobble = 0.3\n          strike.light.position.x += (Math.random() - 0.5) * wobble\n          strike.light.position.y += (Math.random() - 0.5) * wobble\n        }\n      }\n    } else {\n      const lightningLight1Node = sceneRoot.getObjectByName(\"lightningLight1\") as ThreePointLight | undefined\n      const lightningLight2Node = sceneRoot.getObjectByName(\"lightningLight2\") as ThreePointLight | undefined\n      const lightningLight3Node = sceneRoot.getObjectByName(\"lightningLight3\") as ThreePointLight | undefined\n      const lightningDir = sceneRoot.getObjectByName(\"lightningDirectional\") as ThreeDirectionalLight | undefined\n\n      if (lightningLight1Node) lightningLight1Node.intensity = 0\n      if (lightningLight2Node) lightningLight2Node.intensity = 0\n      if (lightningLight3Node) lightningLight3Node.intensity = 0\n      if (lightningDir) lightningDir.intensity = 0\n      activeLightningStrikes = []\n    }\n\n    const timeSinceWaveStart = elapsedTime - waveStartTime\n    const totalWaveDuration = waveCycleDuration + waveWaitTime\n\n    if (timeSinceWaveStart >= totalWaveDuration) {\n      waveStartTime = elapsedTime\n      if (elapsedTime > nextRandomRefresh) {\n        for (let i = 0; i < charRandomOffsets.length; i++) {\n          charRandomOffsets[i] = Math.random() * 0.1 - 0.05\n        }\n        nextRandomRefresh = elapsedTime + 3.0\n      }\n    }\n\n    const allChars = [...opentuiChars, ...fiveKChars]\n    const jumpHeight = 3\n\n    for (let i = 0; i < allChars.length; i++) {\n      const char = allChars[i]\n      const charStartTime = i * charDelay\n      const charEndTime = charStartTime + charJumpDuration\n\n      let jump = 0\n\n      if (timeSinceWaveStart < waveCycleDuration) {\n        if (timeSinceWaveStart >= charStartTime && timeSinceWaveStart <= charEndTime) {\n          const jumpProgress = (timeSinceWaveStart - charStartTime) / charJumpDuration\n          const rawJump = Math.sin(jumpProgress * Math.PI)\n          const easedJump = Math.pow(rawJump, 0.6)\n          jump = easedJump * jumpHeight * (1.0 + charRandomOffsets[i])\n        }\n      }\n\n      char.bottom = Math.round(Math.max(0, jump))\n    }\n\n    engine.drawScene(sceneRoot, framebuffer, deltaTime)\n  })\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n  renderer.root.remove(\"golden-star-main\")\n  renderer.root.remove(\"overlay\")\n  renderer.root.remove(\"gradientBand\")\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/grayscale-buffer-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  OptimizedBuffer,\n  RGBA,\n  FrameBufferRenderable,\n  type KeyEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet framebuffer: OptimizedBuffer | null = null\nlet keyListener: ((key: KeyEvent) => void) | null = null\nlet resizeListener: ((width: number, height: number) => void) | null = null\nlet leftBuffer: Float32Array | null = null\nlet rightBuffer: Float32Array | null = null\n\nlet patternMode = 0\nconst PATTERN_NAMES = [\"Plasma\", \"Ripples\", \"Waves\", \"Starburst\", \"Dots\", \"Checkers\"]\n\nfunction generatePlasma(x: number, y: number, w: number, h: number, t: number): number {\n  const nx = x / w\n  const ny = y / h\n  const v1 = Math.sin(nx * 10 + t)\n  const v2 = Math.sin(ny * 10 + t * 0.7)\n  const v3 = Math.sin((nx + ny) * 8 + t * 1.3)\n  const v4 = Math.sin(Math.sqrt((nx - 0.5) ** 2 + (ny - 0.5) ** 2) * 12 - t * 2)\n  return (v1 + v2 + v3 + v4 + 4) / 8\n}\n\nfunction generateRipples(x: number, y: number, w: number, h: number, t: number): number {\n  const cx = w / 2\n  const cy = h / 2\n  const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2)\n  const wave = Math.sin(dist * 0.5 - t * 3) * 0.5 + 0.5\n  const fade = 1 - Math.min(dist / Math.max(w, h), 1)\n  return wave * fade\n}\n\nfunction generateWaves(x: number, y: number, w: number, h: number, t: number): number {\n  const nx = x / w\n  const ny = y / h\n  const diagonal = (nx + ny) * 6 - t * 2\n  const cross = Math.sin(nx * 8 + t) * Math.sin(ny * 8 + t * 0.8)\n  return (Math.sin(diagonal) * 0.5 + 0.5) * 0.6 + (cross * 0.5 + 0.5) * 0.4\n}\n\nfunction generateStarburst(x: number, y: number, w: number, h: number, t: number): number {\n  const cx = w / 2\n  const cy = h / 2\n  const dx = x - cx\n  const dy = y - cy\n  const angle = Math.atan2(dy, dx) + t * 0.5\n  const numRays = 12\n  const rayAngle = (angle * numRays) / (2 * Math.PI)\n  const rayIntensity = Math.abs(Math.sin(rayAngle * Math.PI))\n  return rayIntensity > 0.7 ? 1.0 : 0.0\n}\n\nfunction generateDots(x: number, y: number, w: number, h: number, t: number): number {\n  const gridSize = Math.min(w, h) / 6\n  const offsetX = t * 3\n  const offsetY = t * 2\n  const gx = ((((x + offsetX) % gridSize) + gridSize) % gridSize) - gridSize / 2\n  const gy = ((((y + offsetY) % gridSize) + gridSize) % gridSize) - gridSize / 2\n  const dist = Math.sqrt(gx * gx + gy * gy)\n  const radius = gridSize * 0.35\n  return dist < radius ? 1.0 : 0.0\n}\n\nfunction generateCheckers(x: number, y: number, w: number, h: number, t: number): number {\n  const cx = w / 2\n  const cy = h / 2\n  const dx = x - cx\n  const dy = y - cy\n  const cos = Math.cos(t * 0.3)\n  const sin = Math.sin(t * 0.3)\n  const rx = dx * cos - dy * sin\n  const ry = dx * sin + dy * cos\n  const size = Math.min(w, h) / 8\n  const checkX = Math.floor(rx / size)\n  const checkY = Math.floor(ry / size)\n  return (checkX + checkY) % 2 === 0 ? 1.0 : 0.0\n}\n\nfunction getIntensity(x: number, y: number, w: number, h: number, t: number): number {\n  switch (patternMode) {\n    case 0:\n      return generatePlasma(x, y, w, h, t)\n    case 1:\n      return generateRipples(x, y, w, h, t)\n    case 2:\n      return generateWaves(x, y, w, h, t)\n    case 3:\n      return generateStarburst(x, y, w, h, t)\n    case 4:\n      return generateDots(x, y, w, h, t)\n    case 5:\n      return generateCheckers(x, y, w, h, t)\n    default:\n      return generatePlasma(x, y, w, h, t)\n  }\n}\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n\n  let WIDTH = renderer.terminalWidth\n  let HEIGHT = renderer.terminalHeight\n  let time = 0\n  let paused = false\n\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"grayscale-demo\",\n    width: WIDTH,\n    height: HEIGHT,\n    zIndex: 0,\n  })\n  renderer.root.add(framebufferRenderable)\n  framebuffer = framebufferRenderable.frameBuffer\n\n  function renderDemo(): void {\n    if (!framebuffer) return\n\n    const fb = framebuffer\n    const totalWidth = fb.width\n    const totalHeight = fb.height\n\n    const headerHeight = 3\n    const panelHeight = totalHeight - headerHeight\n    const panelWidth = Math.floor((totalWidth - 3) / 2)\n\n    if (panelWidth < 10 || panelHeight < 5) return\n\n    const bgColor = RGBA.fromInts(20, 20, 30, 255)\n\n    fb.fillRect(0, 0, totalWidth, totalHeight, bgColor)\n\n    if (!leftBuffer || leftBuffer.length !== panelWidth * panelHeight) {\n      leftBuffer = new Float32Array(panelWidth * panelHeight)\n    }\n    for (let y = 0; y < panelHeight; y++) {\n      for (let x = 0; x < panelWidth; x++) {\n        leftBuffer[y * panelWidth + x] = getIntensity(x, y, panelWidth, panelHeight, time)\n      }\n    }\n    fb.drawGrayscaleBuffer(0, headerHeight, leftBuffer, panelWidth, panelHeight)\n\n    const rightX = panelWidth + 3\n    const ssWidth = panelWidth * 2\n    const ssHeight = panelHeight * 2\n    if (!rightBuffer || rightBuffer.length !== ssWidth * ssHeight) {\n      rightBuffer = new Float32Array(ssWidth * ssHeight)\n    }\n    for (let y = 0; y < ssHeight; y++) {\n      for (let x = 0; x < ssWidth; x++) {\n        rightBuffer[y * ssWidth + x] = getIntensity(x, y, ssWidth, ssHeight, time)\n      }\n    }\n    fb.drawGrayscaleBufferSupersampled(rightX, headerHeight, rightBuffer, ssWidth, ssHeight)\n\n    const dividerX = panelWidth + 1\n    for (let y = headerHeight; y < totalHeight; y++) {\n      fb.setCell(dividerX, y, \"|\", RGBA.fromInts(60, 60, 80, 255), bgColor)\n    }\n\n    const headerBg = RGBA.fromInts(40, 40, 60, 255)\n    const labelColor = RGBA.fromInts(200, 200, 220, 255)\n    const highlightColor = RGBA.fromInts(100, 200, 255, 255)\n\n    fb.fillRect(0, 0, totalWidth, headerHeight, headerBg)\n\n    const leftLabel = \"1:1 Standard\"\n    const leftLabelX = Math.floor(panelWidth / 2 - leftLabel.length / 2)\n    for (let i = 0; i < leftLabel.length; i++) {\n      fb.setCell(leftLabelX + i, 1, leftLabel[i], labelColor, headerBg)\n    }\n\n    const rightLabel = \"2x Supersampled\"\n    const rightLabelX = rightX + Math.floor(panelWidth / 2 - rightLabel.length / 2)\n    for (let i = 0; i < rightLabel.length; i++) {\n      fb.setCell(rightLabelX + i, 1, rightLabel[i], highlightColor, headerBg)\n    }\n\n    const info = `[${PATTERN_NAMES[patternMode]}] SPACE:pause P:pattern`\n    const infoX = Math.floor(totalWidth / 2 - info.length / 2)\n    for (let i = 0; i < info.length; i++) {\n      fb.setCell(infoX + i, 0, info[i], RGBA.fromInts(150, 150, 170, 255), headerBg)\n    }\n  }\n\n  keyListener = (key: KeyEvent) => {\n    switch (key.name) {\n      case \"space\":\n        paused = !paused\n        break\n      case \"p\":\n        patternMode = (patternMode + 1) % 6\n        break\n    }\n  }\n  renderer.keyInput.on(\"keypress\", keyListener)\n\n  resizeListener = (width: number, height: number) => {\n    WIDTH = width\n    HEIGHT = height\n    if (framebuffer) {\n      framebuffer.resize(width, height)\n    }\n  }\n  renderer.on(\"resize\", resizeListener)\n\n  renderer.setFrameCallback(async (deltaTime) => {\n    if (!paused) {\n      time += (deltaTime / 1000) * 0.8\n    }\n    renderDemo()\n  })\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n\n  if (resizeListener) {\n    renderer.off(\"resize\", resizeListener)\n    resizeListener = null\n  }\n\n  if (keyListener) {\n    renderer.keyInput.off(\"keypress\", keyListener)\n    keyListener = null\n  }\n\n  renderer.root.remove(\"grayscale-demo\")\n  framebuffer = null\n  leftBuffer = null\n  rightBuffer = null\n  patternMode = 0\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 30,\n  })\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/hast-syntax-highlighting-demo.ts",
    "content": "import { CliRenderer, createCliRenderer, TextRenderable, BoxRenderable, type KeyEvent } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { parseColor } from \"../lib/RGBA.js\"\nimport { hastToStyledText, type HASTElement } from \"../lib/hast-styled-text.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\n\nconst exampleHAST: HASTElement = (await import(\"./assets/hast-example.json\", { with: { type: \"json\" } })) as HASTElement\n\nlet renderer: CliRenderer | null = null\nlet keyboardHandler: ((key: KeyEvent) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\n\nexport function run(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.start()\n  renderer.setBackgroundColor(\"#0D1117\")\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n    padding: 1,\n  })\n  renderer.root.add(parentContainer)\n\n  const titleBox = new BoxRenderable(renderer, {\n    id: \"title-box\",\n    height: 3,\n    borderStyle: \"double\",\n    borderColor: \"#4ECDC4\",\n    backgroundColor: \"#0D1117\",\n    title: \"HAST to Styled Text Demo\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentContainer.add(titleBox)\n\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"instructions\",\n    content: \"ESC to return | R to re-transform | Demonstrating HAST tree conversion to syntax-highlighted text\",\n    fg: \"#888888\",\n  })\n  titleBox.add(instructionsText)\n\n  const codeBox = new BoxRenderable(renderer, {\n    id: \"code-box\",\n    borderStyle: \"single\",\n    borderColor: \"#6BCF7F\",\n    backgroundColor: \"#0D1117\",\n    title: \"TypeScript Code\",\n    titleAlignment: \"left\",\n    paddingLeft: 1,\n    border: true,\n  })\n  parentContainer.add(codeBox)\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    keyword: { fg: parseColor(\"#FF6B9D\"), bold: true },\n    string: { fg: parseColor(\"#A8E6CF\") },\n    comment: { fg: parseColor(\"#888888\"), italic: true },\n    number: { fg: parseColor(\"#FFD93D\") },\n    function: { fg: parseColor(\"#6BCF7F\") },\n    type: { fg: parseColor(\"#4ECDC4\") },\n    operator: { fg: parseColor(\"#FF8C94\") },\n    variable: { fg: parseColor(\"#C7CEEA\") },\n    bracket: { fg: parseColor(\"#FFFFFF\") },\n    punctuation: { fg: parseColor(\"#DDDDDD\") },\n    default: { fg: parseColor(\"#FFFFFF\") },\n  })\n  const transformStart = performance.now()\n  const styledText = hastToStyledText(exampleHAST, syntaxStyle)\n  const transformEnd = performance.now()\n  const transformTime = (transformEnd - transformStart).toFixed(2)\n\n  const codeDisplay = new TextRenderable(renderer, {\n    id: \"code-display\",\n    content: styledText,\n    bg: \"#0D1117\",\n    selectable: true,\n    selectionBg: \"#264F78\",\n    selectionFg: \"#FFFFFF\",\n  })\n  codeBox.add(codeDisplay)\n\n  const timingText = new TextRenderable(renderer, {\n    id: \"timing-display\",\n    content: `HAST transformation time: ${transformTime}ms (Cache: ${syntaxStyle.getCacheSize()} entries) (Press 'R' to re-transform)`,\n    fg: \"#A8E6CF\",\n  })\n  parentContainer.add(timingText)\n\n  keyboardHandler = (key: KeyEvent) => {\n    if (key.name === \"r\" || key.name === \"R\") {\n      syntaxStyle.clearCache()\n\n      const retransformStart = performance.now()\n      const newStyledText = hastToStyledText(exampleHAST, syntaxStyle)\n      const retransformEnd = performance.now()\n      const newTransformTime = (retransformEnd - retransformStart).toFixed(2)\n\n      codeDisplay.content = newStyledText\n      timingText.content = `HAST transformation time: ${newTransformTime}ms (Cache: ${syntaxStyle.getCacheSize()} entries) (Press 'R' to re-transform)`\n\n      console.log(`Style cache entries: ${syntaxStyle.getCacheSize()}`)\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  parentContainer?.destroy()\n  parentContainer = null\n\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/index.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  ASCIIFontRenderable,\n  BoxRenderable,\n  CliRenderer,\n  createCliRenderer,\n  FrameBufferRenderable,\n  RGBA,\n  SelectRenderable,\n  SelectRenderableEvents,\n  TextareaRenderable,\n  TextRenderable,\n  TimeToFirstDrawRenderable,\n  type KeyEvent,\n  type SelectOption,\n  type ThemeMode,\n} from \"../index.js\"\nimport { measureText } from \"../lib/ascii.font.js\"\nimport * as goldenStarDemo from \"./golden-star-demo.js\"\nimport * as boxExample from \"./fonts.js\"\nimport * as fractalShaderExample from \"./fractal-shader-demo.js\"\nimport * as framebufferExample from \"./framebuffer-demo.js\"\nimport * as lightsPhongExample from \"./lights-phong-demo.js\"\nimport * as physxPlanckExample from \"./physx-planck-2d-demo.js\"\nimport * as physxRapierExample from \"./physx-rapier-2d-demo.js\"\nimport * as opentuiDemo from \"./opentui-demo.js\"\nimport * as nestedZIndexDemo from \"./nested-zindex-demo.js\"\nimport * as relativePositioningDemo from \"./relative-positioning-demo.js\"\nimport * as transparencyDemo from \"./transparency-demo.js\"\nimport * as draggableThreeDemo from \"./draggable-three-demo.js\"\nimport * as scrollExample from \"./scroll-example.js\"\nimport * as stickyScrollExample from \"./sticky-scroll-example.js\"\nimport * as shaderCubeExample from \"./shader-cube-demo.js\"\nimport * as spriteAnimationExample from \"./sprite-animation-demo.js\"\nimport * as spriteParticleExample from \"./sprite-particle-generator-demo.js\"\nimport * as staticSpriteExample from \"./static-sprite-demo.js\"\nimport * as textureLoadingExample from \"./texture-loading-demo.js\"\nimport * as timelineExample from \"./timeline-example.js\"\nimport * as tabSelectExample from \"./tab-select-demo.js\"\nimport * as selectExample from \"./select-demo.js\"\nimport * as inputExample from \"./input-demo.js\"\nimport * as layoutExample from \"./simple-layout-example.js\"\nimport * as inputSelectLayoutExample from \"./input-select-layout-demo.js\"\nimport * as styledTextExample from \"./styled-text-demo.js\"\nimport * as textTableExample from \"./text-table-demo.js\"\nimport * as mouseInteractionExample from \"./mouse-interaction-demo.js\"\nimport * as textSelectionExample from \"./text-selection-demo.js\"\nimport * as asciiFontSelectionExample from \"./ascii-font-selection-demo.js\"\nimport * as splitModeExample from \"./split-mode-demo.js\"\nimport * as consoleExample from \"./console-demo.js\"\nimport * as vnodeCompositionDemo from \"./vnode-composition-demo.js\"\nimport * as hastSyntaxHighlightingExample from \"./hast-syntax-highlighting-demo.js\"\nimport * as codeDemo from \"./code-demo.js\"\nimport * as liveStateExample from \"./live-state-demo.js\"\nimport * as fullUnicodeExample from \"./full-unicode-demo.js\"\nimport * as textNodeDemo from \"./text-node-demo.js\"\nimport * as textWrapExample from \"./text-wrap.js\"\nimport * as editorDemo from \"./editor-demo.js\"\nimport * as sliderDemo from \"./slider-demo.js\"\nimport * as terminalDemo from \"./terminal.js\"\nimport * as diffDemo from \"./diff-demo.js\"\nimport * as keypressDebugDemo from \"./keypress-debug-demo.js\"\nimport * as extmarksDemo from \"./extmarks-demo.js\"\nimport * as markdownDemo from \"./markdown-demo.js\"\nimport * as linkDemo from \"./link-demo.js\"\nimport * as opacityExample from \"./opacity-example.js\"\nimport * as scrollboxOverlayHitTest from \"./scrollbox-overlay-hit-test.js\"\nimport * as scrollboxMouseTest from \"./scrollbox-mouse-test.js\"\nimport * as textTruncationDemo from \"./text-truncation-demo.js\"\nimport * as grayscaleBufferDemo from \"./grayscale-buffer-demo.js\"\nimport * as focusRestoreDemo from \"./focus-restore-demo.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport * as corePluginSlotsDemo from \"./core-plugin-slots-demo.js\"\n\ninterface Example {\n  name: string\n  description: string\n  run?: (renderer: CliRenderer) => void\n  destroy?: (renderer: CliRenderer) => void\n}\n\ninterface ExampleTheme {\n  titleColor: RGBA\n  borderColor: string\n  focusedBorderColor: string\n  inputTextColor: string\n  inputFocusedTextColor: string\n  inputPlaceholderColor: string\n  inputCursorColor: string\n  selectSelectedBackgroundColor: string\n  selectTextColor: string\n  selectSelectedTextColor: string\n  selectDescriptionColor: string\n  selectSelectedDescriptionColor: string\n  instructionsColor: string\n  notImplementedColor: string\n}\n\nconst DEFAULT_THEME_MODE: ThemeMode = \"dark\"\n\nconst MENU_THEMES: Record<ThemeMode, ExampleTheme> = {\n  dark: {\n    titleColor: RGBA.fromInts(240, 248, 255, 255),\n    borderColor: \"#475569\",\n    focusedBorderColor: \"#60A5FA\",\n    inputTextColor: \"#E2E8F0\",\n    inputFocusedTextColor: \"#F8FAFC\",\n    inputPlaceholderColor: \"#94A3B8\",\n    inputCursorColor: \"#60A5FA\",\n    selectSelectedBackgroundColor: \"#1E3A5F\",\n    selectTextColor: \"#E2E8F0\",\n    selectSelectedTextColor: \"#38BDF8\",\n    selectDescriptionColor: \"#64748B\",\n    selectSelectedDescriptionColor: \"#94A3B8\",\n    instructionsColor: \"#94A3B8\",\n    notImplementedColor: \"#FACC15\",\n  },\n  light: {\n    titleColor: RGBA.fromInts(15, 23, 42, 255),\n    borderColor: \"#CBD5E1\",\n    focusedBorderColor: \"#2563EB\",\n    inputTextColor: \"#0F172A\",\n    inputFocusedTextColor: \"#0B1221\",\n    inputPlaceholderColor: \"#64748B\",\n    inputCursorColor: \"#2563EB\",\n    selectSelectedBackgroundColor: \"#DBEAFE\",\n    selectTextColor: \"#0F172A\",\n    selectSelectedTextColor: \"#1D4ED8\",\n    selectDescriptionColor: \"#475569\",\n    selectSelectedDescriptionColor: \"#1E40AF\",\n    instructionsColor: \"#475569\",\n    notImplementedColor: \"#B45309\",\n  },\n}\n\nconst examples: Example[] = [\n  {\n    name: \"Golden Star Demo\",\n    description: \"3D golden star with particle effects and animated text celebrating 5000 stars\",\n    run: goldenStarDemo.run,\n    destroy: goldenStarDemo.destroy,\n  },\n  {\n    name: \"Mouse Interaction Demo\",\n    description: \"Interactive mouse trails and clickable cells demonstration\",\n    run: mouseInteractionExample.run,\n    destroy: mouseInteractionExample.destroy,\n  },\n  {\n    name: \"Text Selection Demo\",\n    description: \"Text selection across multiple renderables with mouse drag\",\n    run: textSelectionExample.run,\n    destroy: textSelectionExample.destroy,\n  },\n  {\n    name: \"Text Truncation Demo\",\n    description: \"Middle truncation with ellipsis - toggle with 'T' key and resize to test responsive behavior\",\n    run: textTruncationDemo.run,\n    destroy: textTruncationDemo.destroy,\n  },\n  {\n    name: \"ASCII Font Selection Demo\",\n    description: \"Text selection with ASCII fonts - precise character-level selection across different font types\",\n    run: asciiFontSelectionExample.run,\n    destroy: asciiFontSelectionExample.destroy,\n  },\n  {\n    name: \"Text Wrap Demo\",\n    description: \"Text wrapping example\",\n    run: textWrapExample.run,\n    destroy: textWrapExample.destroy,\n  },\n  {\n    name: \"Console Demo\",\n    description: \"Interactive console logging with clickable buttons for different log levels\",\n    run: consoleExample.run,\n    destroy: consoleExample.destroy,\n  },\n  {\n    name: \"Styled Text Demo\",\n    description: \"Template literals with styled text, colors, and formatting\",\n    run: styledTextExample.run,\n    destroy: styledTextExample.destroy,\n  },\n  {\n    name: \"TextTable Demo\",\n    description: \"TextTable renderable with styled chunks, Unicode content, and wrap/border toggles\",\n    run: textTableExample.run,\n    destroy: textTableExample.destroy,\n  },\n  {\n    name: \"Link Demo\",\n    description: \"Hyperlink support with OSC 8 - clickable links and link inheritance in styled text\",\n    run: linkDemo.run,\n    destroy: linkDemo.destroy,\n  },\n  {\n    name: \"Extmarks Demo\",\n    description: \"Virtual extmarks - text ranges that cursor jumps over, like inline tags and links\",\n    run: extmarksDemo.run,\n    destroy: extmarksDemo.destroy,\n  },\n  {\n    name: \"Opacity Demo\",\n    description: \"Box opacity and transparency effects with animated opacity transitions\",\n    run: opacityExample.run,\n    destroy: opacityExample.destroy,\n  },\n  {\n    name: \"TextNode Demo\",\n    description: \"TextNode API for building complex styled text structures\",\n    run: textNodeDemo.run,\n    destroy: textNodeDemo.destroy,\n  },\n  {\n    name: \"HAST Syntax Highlighting Demo\",\n    description: \"Convert HAST trees to syntax-highlighted text with efficient chunk generation\",\n    run: hastSyntaxHighlightingExample.run,\n    destroy: hastSyntaxHighlightingExample.destroy,\n  },\n  {\n    name: \"Code Demo\",\n    description:\n      \"Code viewer with line numbers, diff highlights, and diagnostics using CodeRenderable + LineNumberRenderable\",\n    run: codeDemo.run,\n    destroy: codeDemo.destroy,\n  },\n  {\n    name: \"Diff Demo\",\n    description: \"Unified and split diff views with syntax highlighting and multiple themes\",\n    run: diffDemo.run,\n    destroy: diffDemo.destroy,\n  },\n  {\n    name: \"Markdown Demo\",\n    description: \"Markdown rendering with table alignment, syntax highlighting, and theme switching\",\n    run: markdownDemo.run,\n    destroy: markdownDemo.destroy,\n  },\n  {\n    name: \"Live State Management Demo\",\n    description: \"Test automatic renderer lifecycle management with live renderables\",\n    run: liveStateExample.run,\n    destroy: liveStateExample.destroy,\n  },\n  {\n    name: \"Core Plugin Slots Demo\",\n    description: \"Framework-free plugin slots with cached renderables and deterministic ordering\",\n    run: corePluginSlotsDemo.run,\n    destroy: corePluginSlotsDemo.destroy,\n  },\n  {\n    name: \"Layout System Demo\",\n    description: \"Flex layout system with multiple configurations\",\n    run: layoutExample.run,\n    destroy: layoutExample.destroy,\n  },\n  {\n    name: \"Input & Select Layout Demo\",\n    description: \"Interactive layout with input and select elements\",\n    run: inputSelectLayoutExample.run,\n    destroy: inputSelectLayoutExample.destroy,\n  },\n  {\n    name: \"ASCII Font Demo\",\n    description: \"ASCII font rendering with various colors and text\",\n    run: boxExample.run,\n    destroy: boxExample.destroy,\n  },\n  {\n    name: \"OpenTUI Demo\",\n    description: \"Multi-tab demo with various features\",\n    run: opentuiDemo.run,\n    destroy: opentuiDemo.destroy,\n  },\n  {\n    name: \"Nested Z-Index Demo\",\n    description: \"Demonstrates z-index behavior with nested render objects\",\n    run: nestedZIndexDemo.run,\n    destroy: nestedZIndexDemo.destroy,\n  },\n  {\n    name: \"Relative Positioning Demo\",\n    description: \"Shows how child positions are relative to their parent containers\",\n    run: relativePositioningDemo.run,\n    destroy: relativePositioningDemo.destroy,\n  },\n  {\n    name: \"Transparency Demo\",\n    description: \"Alpha blending and transparency effects demonstration\",\n    run: transparencyDemo.run,\n    destroy: transparencyDemo.destroy,\n  },\n  {\n    name: \"Draggable ThreeRenderable\",\n    description: \"Draggable WebGPU cube with live animation\",\n    run: draggableThreeDemo.run,\n    destroy: draggableThreeDemo.destroy,\n  },\n  {\n    name: \"Static Sprite\",\n    description: \"Static sprite rendering demo\",\n    run: staticSpriteExample.run,\n    destroy: staticSpriteExample.destroy,\n  },\n  {\n    name: \"Sprite Animation\",\n    description: \"Animated sprite sequences\",\n    run: spriteAnimationExample.run,\n    destroy: spriteAnimationExample.destroy,\n  },\n  {\n    name: \"Sprite Particles\",\n    description: \"Particle system with sprites\",\n    run: spriteParticleExample.run,\n    destroy: spriteParticleExample.destroy,\n  },\n  {\n    name: \"Framebuffer Demo\",\n    description: \"Framebuffer rendering techniques\",\n    run: framebufferExample.run,\n    destroy: framebufferExample.destroy,\n  },\n  {\n    name: \"Texture Loading\",\n    description: \"Loading and displaying textures\",\n    run: textureLoadingExample.run,\n    destroy: textureLoadingExample.destroy,\n  },\n  {\n    name: \"ScrollBox Demo\",\n    description: \"Scrollable container with customization\",\n    run: scrollExample.run,\n    destroy: scrollExample.destroy,\n  },\n  {\n    name: \"Sticky Scroll Demo\",\n    description: \"ScrollBox with sticky scroll behavior - maintains position at borders when content changes\",\n    run: stickyScrollExample.run,\n    destroy: stickyScrollExample.destroy,\n  },\n  {\n    name: \"Scrollbox Mouse Test\",\n    description: \"Test scrollbox mouse hit detection with hover and click events\",\n    run: scrollboxMouseTest.run,\n    destroy: scrollboxMouseTest.destroy,\n  },\n  {\n    name: \"Scrollbox Overlay Hit Test\",\n    description: \"Test scrollbox hit detection with overlays and dialogs\",\n    run: scrollboxOverlayHitTest.run,\n    destroy: scrollboxOverlayHitTest.destroy,\n  },\n  {\n    name: \"Shader Cube\",\n    description: \"3D cube with custom shaders\",\n    run: shaderCubeExample.run,\n    destroy: shaderCubeExample.destroy,\n  },\n  {\n    name: \"Fractal Shader\",\n    description: \"Fractal rendering with shaders\",\n    run: fractalShaderExample.run,\n    destroy: fractalShaderExample.destroy,\n  },\n  {\n    name: \"Phong Lighting\",\n    description: \"Phong lighting model demo\",\n    run: lightsPhongExample.run,\n    destroy: lightsPhongExample.destroy,\n  },\n  {\n    name: \"Physics Planck\",\n    description: \"2D physics with Planck.js\",\n    run: physxPlanckExample.run,\n    destroy: physxPlanckExample.destroy,\n  },\n  {\n    name: \"Physics Rapier\",\n    description: \"2D physics with Rapier\",\n    run: physxRapierExample.run,\n    destroy: physxRapierExample.destroy,\n  },\n  {\n    name: \"Timeline Example\",\n    description: \"Animation timeline system\",\n    run: timelineExample.run,\n    destroy: timelineExample.destroy,\n  },\n  {\n    name: \"Tab Select\",\n    description: \"Tab selection demo\",\n    run: tabSelectExample.run,\n    destroy: tabSelectExample.destroy,\n  },\n  {\n    name: \"Select Demo\",\n    description: \"Interactive SelectElement demo with customizable options\",\n    run: selectExample.run,\n    destroy: selectExample.destroy,\n  },\n  {\n    name: \"Input Demo\",\n    description: \"Interactive InputElement demo with validation and multiple fields\",\n    run: inputExample.run,\n    destroy: inputExample.destroy,\n  },\n  {\n    name: \"Terminal Palette Demo\",\n    description: \"Terminal color palette detection and visualization - fetch and display all 256 terminal colors\",\n    run: terminalDemo.run,\n    destroy: terminalDemo.destroy,\n  },\n  {\n    name: \"Editor Demo\",\n    description: \"Interactive text editor with TextareaRenderable - supports full editing capabilities\",\n    run: editorDemo.run,\n    destroy: editorDemo.destroy,\n  },\n  {\n    name: \"Extmarks Demo\",\n    description: \"Virtual extmarks - text ranges that the cursor jumps over, with deletion handling\",\n    run: extmarksDemo.run,\n    destroy: extmarksDemo.destroy,\n  },\n  {\n    name: \"Slider Demo\",\n    description: \"Interactive slider components with various orientations and configurations\",\n    run: sliderDemo.run,\n    destroy: sliderDemo.destroy,\n  },\n  {\n    name: \"VNode Composition Demo\",\n    description: \"Declarative Box(Box(Box(children))) composition\",\n    run: vnodeCompositionDemo.run,\n    destroy: vnodeCompositionDemo.destroy,\n  },\n  {\n    name: \"Full Unicode Demo\",\n    description: \"Draggable boxes and background filled with complex graphemes\",\n    run: fullUnicodeExample.run,\n    destroy: fullUnicodeExample.destroy,\n  },\n  {\n    name: \"Split Mode Demo (Experimental)\",\n    description: \"Renderer confined to bottom area with normal terminal output above\",\n    run: splitModeExample.run,\n    destroy: splitModeExample.destroy,\n  },\n  {\n    name: \"Keypress Debug Tool\",\n    description: \"Debug tool to inspect keypress events, raw input, and terminal capabilities\",\n    run: keypressDebugDemo.run,\n    destroy: keypressDebugDemo.destroy,\n  },\n  {\n    name: \"Grayscale Buffer\",\n    description: \"Grayscale buffer rendering with 1x vs 2x supersampled intensity\",\n    run: grayscaleBufferDemo.run,\n    destroy: grayscaleBufferDemo.destroy,\n  },\n  {\n    name: \"Focus Restore Demo\",\n    description: \"Test focus restore - alt-tab away and back to verify mouse tracking resumes\",\n    run: focusRestoreDemo.run,\n    destroy: focusRestoreDemo.destroy,\n  },\n]\n\nclass ExampleSelector {\n  private renderer: CliRenderer\n  private currentExample: Example | null = null\n  private inMenu = true\n  private themeMode: ThemeMode = DEFAULT_THEME_MODE\n\n  private menuContainer: BoxRenderable | null = null\n  private title: FrameBufferRenderable | null = null\n  private filterBox: BoxRenderable | null = null\n  private filterInput: TextareaRenderable | null = null\n  private instructions: TextRenderable | null = null\n  private timeToFirstDrawText: TimeToFirstDrawRenderable | null = null\n  private selectElement: SelectRenderable | null = null\n  private selectBox: BoxRenderable | null = null\n  private notImplementedText: TextRenderable | null = null\n  private allExamples: Example[] = examples\n\n  constructor(renderer: CliRenderer) {\n    this.renderer = renderer\n    this.themeMode = this.renderer.themeMode ?? DEFAULT_THEME_MODE\n    this.createLayout()\n    this.setupKeyboardHandling()\n\n    this.renderer.on(\"theme_mode\", (mode: ThemeMode) => {\n      this.applyTheme(mode)\n      console.log(`Theme mode changed to ${mode}, applied new theme to menu`)\n    })\n\n    this.applyTheme(this.renderer.themeMode)\n\n    this.renderer.on(\"resize\", (width: number, height: number) => {\n      this.handleResize(width, height)\n    })\n  }\n\n  private createLayout(): void {\n    const width = this.renderer.terminalWidth\n    const theme = MENU_THEMES[this.themeMode]\n\n    // Menu container with column layout\n    this.menuContainer = new BoxRenderable(renderer, {\n      id: \"example-menu-container\",\n      flexDirection: \"column\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n    this.renderer.root.add(this.menuContainer)\n\n    // Title\n    const titleText = \"OPENTUI EXAMPLES\"\n    const titleFont = \"tiny\"\n    const { width: titleWidth } = measureText({ text: titleText, font: titleFont })\n    const centerX = Math.floor(width / 2) - Math.floor(titleWidth / 2)\n\n    this.title = new ASCIIFontRenderable(renderer, {\n      id: \"example-index-title\",\n      left: centerX,\n      margin: 1,\n      text: titleText,\n      font: titleFont,\n      color: theme.titleColor,\n      backgroundColor: \"transparent\",\n    })\n    this.menuContainer.add(this.title)\n\n    // Filter box with border (grows with content)\n    this.filterBox = new BoxRenderable(renderer, {\n      id: \"example-index-filter-box\",\n      marginLeft: 1,\n      marginRight: 1,\n      flexShrink: 0,\n      backgroundColor: \"transparent\",\n      border: true,\n      borderStyle: \"single\",\n      borderColor: theme.borderColor,\n    })\n    this.menuContainer.add(this.filterBox)\n\n    // Filter input inside the box (transparent bg so box bg shows through)\n    this.filterInput = new TextareaRenderable(renderer, {\n      id: \"example-index-filter-input\",\n      width: \"100%\",\n      height: 1,\n      placeholder: \"Filter examples by title...\",\n      placeholderColor: theme.inputPlaceholderColor,\n      backgroundColor: \"transparent\",\n      focusedBackgroundColor: \"transparent\",\n      textColor: theme.inputTextColor,\n      focusedTextColor: theme.inputFocusedTextColor,\n      wrapMode: \"none\",\n      showCursor: true,\n      cursorColor: theme.inputCursorColor,\n      onContentChange: () => {\n        this.filterExamples()\n      },\n    })\n    this.filterBox.add(this.filterInput)\n    this.filterInput.focus()\n\n    // Select box (grows to fill remaining space)\n    this.selectBox = new BoxRenderable(renderer, {\n      id: \"example-selector-box\",\n      marginLeft: 1,\n      marginRight: 1,\n      marginBottom: 1,\n      flexGrow: 1,\n      borderStyle: \"single\",\n      borderColor: theme.borderColor,\n      focusedBorderColor: theme.focusedBorderColor,\n      title: \"Examples\",\n      titleAlignment: \"center\",\n      backgroundColor: \"transparent\",\n      shouldFill: true,\n      border: true,\n    })\n    this.menuContainer.add(this.selectBox)\n\n    // Select element\n    const selectOptions: SelectOption[] = examples.map((example) => ({\n      name: example.name,\n      description: example.description,\n      value: example,\n    }))\n\n    this.selectElement = new SelectRenderable(renderer, {\n      id: \"example-selector\",\n      height: \"100%\",\n      options: selectOptions,\n      backgroundColor: \"transparent\",\n      focusedBackgroundColor: \"transparent\",\n      selectedBackgroundColor: theme.selectSelectedBackgroundColor,\n      textColor: theme.selectTextColor,\n      selectedTextColor: theme.selectSelectedTextColor,\n      descriptionColor: theme.selectDescriptionColor,\n      selectedDescriptionColor: theme.selectSelectedDescriptionColor,\n      showScrollIndicator: true,\n      wrapSelection: true,\n      showDescription: true,\n      fastScrollStep: 5,\n    })\n    this.selectBox.add(this.selectElement)\n\n    this.selectElement.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {\n      this.runSelected(option.value as Example)\n    })\n\n    this.timeToFirstDrawText = new TimeToFirstDrawRenderable(renderer, {\n      id: \"example-index-time-to-first-draw\",\n      fg: theme.instructionsColor,\n    })\n    this.menuContainer.add(this.timeToFirstDrawText)\n\n    // Instructions at the bottom\n    this.instructions = new TextRenderable(renderer, {\n      id: \"example-index-instructions\",\n      height: 1,\n      flexShrink: 0,\n      alignSelf: \"center\",\n      content: \"Type to filter | ↑↓/j/k navigate | Enter run | Esc clear/return | ctrl+c quit\",\n      fg: theme.instructionsColor,\n    })\n    this.menuContainer.add(this.instructions)\n  }\n\n  private applyTheme(mode: ThemeMode | null): void {\n    this.themeMode = mode ?? DEFAULT_THEME_MODE\n    const theme = MENU_THEMES[this.themeMode]\n\n    if (this.title) {\n      this.title.color = theme.titleColor\n    }\n\n    if (this.filterBox) {\n      this.filterBox.borderColor = theme.borderColor\n    }\n\n    if (this.filterInput) {\n      this.filterInput.textColor = theme.inputTextColor\n      this.filterInput.focusedTextColor = theme.inputFocusedTextColor\n      this.filterInput.placeholderColor = theme.inputPlaceholderColor\n      this.filterInput.cursorColor = theme.inputCursorColor\n    }\n\n    if (this.selectBox) {\n      this.selectBox.borderColor = theme.borderColor\n      this.selectBox.focusedBorderColor = theme.focusedBorderColor\n    }\n\n    if (this.selectElement) {\n      this.selectElement.selectedBackgroundColor = theme.selectSelectedBackgroundColor\n      this.selectElement.textColor = theme.selectTextColor\n      this.selectElement.selectedTextColor = theme.selectSelectedTextColor\n      this.selectElement.descriptionColor = theme.selectDescriptionColor\n      this.selectElement.selectedDescriptionColor = theme.selectSelectedDescriptionColor\n    }\n\n    if (this.instructions) {\n      this.instructions.fg = theme.instructionsColor\n    }\n\n    if (this.timeToFirstDrawText) {\n      this.timeToFirstDrawText.color = theme.instructionsColor\n    }\n\n    if (this.notImplementedText) {\n      this.notImplementedText.fg = theme.notImplementedColor\n    }\n\n    this.renderer.requestRender()\n  }\n\n  private filterExamples(): void {\n    if (!this.filterInput || !this.selectElement) return\n\n    const filterText = this.filterInput.editBuffer.getText().toLowerCase().trim()\n\n    if (filterText === \"\") {\n      // Show all examples\n      const selectOptions: SelectOption[] = this.allExamples.map((example) => ({\n        name: example.name,\n        description: example.description,\n        value: example,\n      }))\n      this.selectElement.options = selectOptions\n    } else {\n      // Filter by title only\n      const filtered = this.allExamples.filter((example) => example.name.toLowerCase().includes(filterText))\n      const selectOptions: SelectOption[] = filtered.map((example) => ({\n        name: example.name,\n        description: example.description,\n        value: example,\n      }))\n      this.selectElement.options = selectOptions\n    }\n  }\n\n  private handleResize(width: number, height: number): void {\n    if (this.title) {\n      const titleWidth = this.title.frameBuffer.width\n      const centerX = Math.floor(width / 2) - Math.floor(titleWidth / 2)\n      this.title.x = centerX\n    }\n\n    this.renderer.requestRender()\n  }\n\n  private setupKeyboardHandling(): void {\n    this.renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n      if (key.name === \"c\" && key.ctrl) {\n        this.cleanup()\n        return\n      }\n\n      if (!this.inMenu) {\n        switch (key.name) {\n          case \"escape\":\n            this.returnToMenu()\n            break\n        }\n        return\n      }\n\n      // Forward navigation keys to select even when filter is focused\n      if (this.filterInput?.focused && this.selectElement) {\n        // Navigation keys: arrow up/down, j/k, shift variants\n        if (key.name === \"up\" || key.name === \"k\") {\n          key.preventDefault()\n          if (key.shift) {\n            this.selectElement.moveUp(5)\n          } else {\n            this.selectElement.moveUp(1)\n          }\n          return\n        }\n        if (key.name === \"down\" || key.name === \"j\") {\n          key.preventDefault()\n          if (key.shift) {\n            this.selectElement.moveDown(5)\n          } else {\n            this.selectElement.moveDown(1)\n          }\n          return\n        }\n        // Enter to select\n        if (key.name === \"return\" || key.name === \"linefeed\") {\n          key.preventDefault()\n          this.selectElement.selectCurrent()\n          return\n        }\n      }\n\n      // Handle Escape: clear filter if has content\n      if (key.name === \"escape\") {\n        if (this.filterInput) {\n          const filterText = this.filterInput.editBuffer.getText()\n          if (filterText.length > 0) {\n            key.preventDefault()\n            this.filterInput.editBuffer.setText(\"\")\n            this.filterExamples()\n            return\n          }\n        }\n      }\n\n      if (key.name === \"c\" && key.ctrl) {\n        this.cleanup()\n        return\n      }\n      switch (key.name) {\n        case \"c\":\n          console.log(\"Capabilities:\", this.renderer.capabilities)\n          break\n        case \"z\":\n          if (key.ctrl) {\n            console.log(\"Suspending renderer... (will auto-resume in 5 seconds)\")\n            this.renderer.suspend()\n            setTimeout(() => {\n              console.log(\"Resuming renderer...\")\n              this.renderer.resume()\n            }, 5000)\n          }\n          break\n      }\n    })\n    setupCommonDemoKeys(this.renderer)\n  }\n\n  private runSelected(selected: Example): void {\n    this.inMenu = false\n    this.hideMenuElements()\n\n    if (selected.run) {\n      this.currentExample = selected\n      selected.run(this.renderer)\n    } else {\n      if (!this.notImplementedText) {\n        const theme = MENU_THEMES[this.themeMode]\n        this.notImplementedText = new TextRenderable(renderer, {\n          id: \"not-implemented\",\n          position: \"absolute\",\n          left: 10,\n          top: 10,\n          content: `${selected.name} not yet implemented. Press Escape to return.`,\n          fg: theme.notImplementedColor,\n          zIndex: 10,\n        })\n        this.renderer.root.add(this.notImplementedText)\n      }\n      this.renderer.requestRender()\n    }\n  }\n\n  private hideMenuElements(): void {\n    if (this.menuContainer) {\n      this.menuContainer.visible = false\n    }\n    if (this.title) {\n      this.title.visible = false\n    }\n    if (this.filterBox) {\n      this.filterBox.visible = false\n    }\n    if (this.selectBox) {\n      this.selectBox.visible = false\n    }\n    if (this.instructions) {\n      this.instructions.visible = false\n    }\n    if (this.timeToFirstDrawText) {\n      this.timeToFirstDrawText.visible = false\n    }\n    if (this.filterInput) {\n      this.filterInput.blur()\n    }\n    if (this.selectElement) {\n      this.selectElement.blur()\n    }\n  }\n\n  private showMenuElements(): void {\n    if (this.menuContainer) {\n      this.menuContainer.visible = true\n    }\n    if (this.title) {\n      this.title.visible = true\n    }\n    if (this.filterBox) {\n      this.filterBox.visible = true\n    }\n    if (this.selectBox) {\n      this.selectBox.visible = true\n    }\n    if (this.instructions) {\n      this.instructions.visible = true\n    }\n    if (this.timeToFirstDrawText) {\n      this.timeToFirstDrawText.visible = true\n    }\n    if (this.filterInput) {\n      // Clear filter when returning to menu\n      this.filterInput.editBuffer.setText(\"\")\n      this.filterInput.focus()\n    }\n    // Reset filter to show all examples\n    this.filterExamples()\n  }\n\n  private returnToMenu(): void {\n    if (this.currentExample) {\n      this.currentExample.destroy?.(this.renderer)\n      this.currentExample = null\n    }\n\n    if (this.notImplementedText) {\n      this.renderer.root.remove(this.notImplementedText.id)\n      this.notImplementedText = null\n    }\n\n    this.inMenu = true\n    this.restart()\n  }\n\n  private restart(): void {\n    this.renderer.pause()\n    this.renderer.auto()\n    this.showMenuElements()\n    this.renderer.setBackgroundColor(\"transparent\")\n    this.renderer.requestRender()\n  }\n\n  private cleanup(): void {\n    if (this.currentExample) {\n      this.currentExample.destroy?.(this.renderer)\n    }\n    if (this.filterInput) {\n      this.filterInput.blur()\n    }\n    if (this.selectElement) {\n      this.selectElement.blur()\n    }\n    if (this.menuContainer) {\n      this.menuContainer.destroy()\n    }\n    this.renderer.destroy()\n  }\n}\n\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: false,\n  targetFps: 60,\n  // useAlternateScreen: false,\n})\n\nrenderer.setBackgroundColor(\"transparent\")\nnew ExampleSelector(renderer)\n"
  },
  {
    "path": "packages/core/src/examples/input-demo.ts",
    "content": "import {\n  createCliRenderer,\n  InputRenderable,\n  InputRenderableEvents,\n  RenderableEvents,\n  type CliRenderer,\n  t,\n  bold,\n  fg,\n  BoxRenderable,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\nlet nameInput: InputRenderable | null = null\nlet emailInput: InputRenderable | null = null\nlet passwordInput: InputRenderable | null = null\nlet commentInput: InputRenderable | null = null\nlet renderer: CliRenderer | null = null\nlet keyboardHandler: ((key: any) => void) | null = null\nlet keyLegendDisplay: TextRenderable | null = null\nlet statusDisplay: TextRenderable | null = null\nlet lastActionText: string = \"Welcome to InputRenderable demo! Use Tab to navigate between fields.\"\nlet lastActionColor: string = \"#FFCC00\"\nlet activeInputIndex: number = 0\n\nconst inputElements: InputRenderable[] = []\n\nfunction getActiveInput(): InputRenderable | null {\n  return inputElements[activeInputIndex] || null\n}\n\nfunction updateDisplays() {\n  if (inputElements.length === 0) return\n\n  const activeInput = getActiveInput()\n  const activeInputName = getInputName(activeInput)\n\n  const keyLegendText = t`${bold(fg(\"#FFFFFF\")(\"Key Controls:\"))}\nTab/Shift+Tab: Navigate between inputs\nLeft/Right: Move cursor within input\nHome/End: Move to start/end of input\nBackspace/Delete: Remove characters\nEnter: Submit current input\nCtrl+F: Toggle focus on active input\nCtrl+C: Clear active input\nCtrl+R: Reset all inputs to defaults\nType: Enter text in focused field`\n\n  if (keyLegendDisplay) {\n    keyLegendDisplay.content = keyLegendText\n  }\n\n  const nameValue = nameInput?.value || \"\"\n  const emailValue = emailInput?.value || \"\"\n  const passwordValue = passwordInput?.value || \"\"\n  const commentValue = commentInput?.value || \"\"\n\n  const nameStatus = nameInput?.focused ? \"FOCUSED\" : \"BLURRED\"\n  const nameColor = nameInput?.focused ? \"#00FF00\" : \"#FF0000\"\n\n  const emailStatus = emailInput?.focused ? \"FOCUSED\" : \"BLURRED\"\n  const emailColor = emailInput?.focused ? \"#00FF00\" : \"#FF0000\"\n\n  const passwordStatus = passwordInput?.focused ? \"FOCUSED\" : \"BLURRED\"\n  const passwordColor = passwordInput?.focused ? \"#00FF00\" : \"#FF0000\"\n\n  const commentStatus = commentInput?.focused ? \"FOCUSED\" : \"BLURRED\"\n  const commentColor = commentInput?.focused ? \"#00FF00\" : \"#FF0000\"\n\n  const statusText = t`${bold(fg(\"#FFFFFF\")(\"Input Values:\"))}\nName: \"${nameValue}\" (${fg(nameColor)(nameStatus)})\nEmail: \"${emailValue}\" (${fg(emailColor)(emailStatus)})\nPassword: \"${passwordValue.replace(/./g, \"*\")}\" (${fg(passwordColor)(passwordStatus)})\nComment: \"${commentValue}\" (${fg(commentColor)(commentStatus)})\n\n${bold(fg(\"#FFAA00\")(`Active Input: ${activeInputName}`))}\n\n${bold(fg(\"#CCCCCC\")(\"Validation:\"))}\nName: ${validateName(nameValue) ? fg(\"#00FF00\")(\"✓ Valid\") : fg(\"#FF0000\")(\"✗ Invalid (min 2 chars)\")}\nEmail: ${validateEmail(emailValue) ? fg(\"#00FF00\")(\"✓ Valid\") : fg(\"#FF0000\")(\"✗ Invalid format\")}\nPassword: ${validatePassword(passwordValue) ? fg(\"#00FF00\")(\"✓ Valid\") : fg(\"#FF0000\")(\"✗ Invalid (min 6 chars)\")}\n\n${fg(lastActionColor)(lastActionText)}`\n\n  if (statusDisplay) {\n    statusDisplay.content = statusText\n  }\n}\n\nfunction getInputName(input: InputRenderable | null): string {\n  if (input === nameInput) return \"Name\"\n  if (input === emailInput) return \"Email\"\n  if (input === passwordInput) return \"Password\"\n  if (input === commentInput) return \"Comment\"\n  return \"Unknown\"\n}\n\nfunction validateName(value: string): boolean {\n  return value.length >= 2\n}\n\nfunction validateEmail(value: string): boolean {\n  const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n  return emailRegex.test(value)\n}\n\nfunction validatePassword(value: string): boolean {\n  return value.length >= 6\n}\n\nfunction navigateToInput(index: number): void {\n  const currentActive = getActiveInput()\n  currentActive?.blur()\n\n  activeInputIndex = Math.max(0, Math.min(index, inputElements.length - 1))\n  const newActive = getActiveInput()\n  newActive?.focus()\n\n  lastActionText = `Switched to ${getInputName(newActive)} input`\n  lastActionColor = \"#FFCC00\"\n  updateDisplays()\n}\n\nfunction resetInputs(): void {\n  nameInput!.value = \"\"\n  emailInput!.value = \"\"\n  passwordInput!.value = \"\"\n  commentInput!.value = \"\"\n\n  lastActionText = \"All inputs reset to empty values\"\n  lastActionColor = \"#FF00FF\"\n  updateDisplays()\n\n  setTimeout(() => {\n    lastActionColor = \"#FFCC00\"\n    updateDisplays()\n  }, 1000)\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#001122\")\n\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  // Create input elements\n  nameInput = new InputRenderable(renderer, {\n    id: \"name-input\",\n    position: \"absolute\",\n    left: 5,\n    top: 2,\n    width: 40,\n    height: 3,\n    zIndex: 100,\n    backgroundColor: \"#001122\",\n    textColor: \"#FFFFFF\",\n    placeholder: \"Enter your name...\",\n    placeholderColor: \"#666666\",\n    cursorColor: \"#FFFF00\",\n    value: \"\",\n    maxLength: 50,\n  })\n\n  emailInput = new InputRenderable(renderer, {\n    id: \"email-input\",\n    position: \"absolute\",\n    left: 5,\n    top: 6,\n    width: 40,\n    height: 3,\n    zIndex: 100,\n    backgroundColor: \"#001122\",\n    textColor: \"#FFFFFF\",\n    placeholder: \"Enter your email...\",\n    placeholderColor: \"#666666\",\n    cursorColor: \"#FFFF00\",\n    value: \"\",\n    maxLength: 100,\n  })\n\n  passwordInput = new InputRenderable(renderer, {\n    id: \"password-input\",\n    position: \"absolute\",\n    left: 5,\n    top: 10,\n    width: 40,\n    height: 3,\n    zIndex: 100,\n    backgroundColor: \"#001122\",\n    textColor: \"#FFFFFF\",\n    placeholder: \"Enter password...\",\n    placeholderColor: \"#666666\",\n    cursorColor: \"#FFFF00\",\n    value: \"\",\n    maxLength: 50,\n  })\n\n  commentInput = new InputRenderable(renderer, {\n    id: \"comment-input\",\n    position: \"absolute\",\n    left: 5,\n    top: 14,\n    width: 60,\n    height: 3,\n    zIndex: 100,\n    backgroundColor: \"#001122\",\n    textColor: \"#FFFFFF\",\n    placeholder: \"Enter a comment...\",\n    placeholderColor: \"#666666\",\n    cursorColor: \"#FFFF00\",\n    value: \"\",\n    maxLength: 200,\n  })\n\n  inputElements.push(nameInput, emailInput, passwordInput, commentInput)\n\n  renderer.root.add(nameInput)\n  renderer.root.add(emailInput)\n  renderer.root.add(passwordInput)\n  renderer.root.add(commentInput)\n\n  keyLegendDisplay = new TextRenderable(renderer, {\n    id: \"key-legend\",\n    content: t``,\n    width: 50,\n    height: 12,\n    position: \"absolute\",\n    left: 50,\n    top: 2,\n    zIndex: 50,\n    fg: \"#AAAAAA\",\n  })\n  parentContainer.add(keyLegendDisplay)\n\n  statusDisplay = new TextRenderable(renderer, {\n    id: \"status-display\",\n    content: t``,\n    width: 80,\n    height: 18,\n    position: \"absolute\",\n    left: 5,\n    top: 19,\n    zIndex: 50,\n  })\n  parentContainer.add(statusDisplay)\n\n  // Set up event handlers for all inputs\n  inputElements.forEach((input, index) => {\n    input.on(InputRenderableEvents.INPUT, (value: string) => {\n      lastActionText = `${getInputName(input)} input: \"${value}\"`\n      lastActionColor = \"#00FFFF\"\n      updateDisplays()\n    })\n\n    input.on(InputRenderableEvents.CHANGE, (value: string) => {\n      lastActionText = `*** ${getInputName(input)} CHANGED: \"${value}\" ***`\n      lastActionColor = \"#FF00FF\"\n      updateDisplays()\n      setTimeout(() => {\n        lastActionColor = \"#FFCC00\"\n        updateDisplays()\n      }, 1000)\n    })\n\n    input.on(InputRenderableEvents.ENTER, (value: string) => {\n      const inputName = getInputName(input)\n      const isValid =\n        inputName === \"Name\"\n          ? validateName(value)\n          : inputName === \"Email\"\n            ? validateEmail(value)\n            : inputName === \"Password\"\n              ? validatePassword(value)\n              : true\n\n      lastActionText = `*** ${inputName} SUBMITTED: \"${value}\" ${isValid ? \"(Valid)\" : \"(Invalid)\"} ***`\n      lastActionColor = isValid ? \"#00FF00\" : \"#FF0000\"\n      updateDisplays()\n      setTimeout(() => {\n        lastActionColor = \"#FFCC00\"\n        updateDisplays()\n      }, 1500)\n    })\n\n    input.on(RenderableEvents.FOCUSED, () => {\n      updateDisplays()\n    })\n\n    input.on(RenderableEvents.BLURRED, () => {\n      updateDisplays()\n    })\n  })\n\n  updateDisplays()\n\n  keyboardHandler = (key) => {\n    const anyInputFocused = inputElements.some((input) => input.focused)\n\n    if (key.name === \"tab\") {\n      if (key.shift) {\n        // Navigate backward\n        navigateToInput(activeInputIndex - 1)\n      } else {\n        // Navigate forward\n        navigateToInput(activeInputIndex + 1)\n      }\n    } else if (key.ctrl && key.name === \"f\") {\n      // Only respond to Ctrl+F for focus toggle\n      const activeInput = getActiveInput()\n      if (activeInput?.focused) {\n        activeInput.blur()\n        lastActionText = `Focus removed from ${getInputName(activeInput)} input`\n      } else {\n        activeInput?.focus()\n        lastActionText = `${getInputName(activeInput)} input focused`\n      }\n      lastActionColor = \"#FFCC00\"\n      updateDisplays()\n    } else if (key.ctrl && key.name === \"c\") {\n      // Only respond to Ctrl+C for clear\n      const activeInput = getActiveInput()\n      if (activeInput) {\n        activeInput.value = \"\"\n        lastActionText = `${getInputName(activeInput)} input cleared`\n        lastActionColor = \"#FFAA00\"\n        updateDisplays()\n      }\n    } else if (key.ctrl && key.name === \"r\") {\n      // Only respond to Ctrl+R for reset\n      resetInputs()\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n  nameInput.focus()\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  inputElements.forEach((input) => {\n    if (input) {\n      rendererInstance.root.remove(input.id)\n      input.destroy()\n    }\n  })\n  inputElements.length = 0\n\n  rendererInstance.root.remove(\"parent-container\")\n\n  nameInput = null\n  emailInput = null\n  passwordInput = null\n  commentInput = null\n  keyLegendDisplay = null\n  statusDisplay = null\n  renderer = null\n  activeInputIndex = 0\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/input-select-layout-demo.ts",
    "content": "import { CliRenderer, BoxRenderable, TextRenderable, createCliRenderer, type KeyEvent } from \"../index.js\"\nimport { InputRenderable, InputRenderableEvents } from \"../renderables/Input.js\"\nimport { SelectRenderable, SelectRenderableEvents, type SelectOption } from \"../renderables/Select.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet renderer: CliRenderer | null = null\nlet header: TextRenderable | null = null\nlet headerBox: BoxRenderable | null = null\nlet selectContainer: BoxRenderable | null = null\nlet selectContainerBox: BoxRenderable | null = null\nlet leftSelect: SelectRenderable | null = null\nlet leftSelectBox: BoxRenderable | null = null\nlet rightSelect: SelectRenderable | null = null\nlet rightSelectBox: BoxRenderable | null = null\nlet inputContainer: BoxRenderable | null = null\nlet inputContainerBox: BoxRenderable | null = null\nlet inputLabel: TextRenderable | null = null\nlet textInput: InputRenderable | null = null\nlet textInputBox: BoxRenderable | null = null\nlet footer: TextRenderable | null = null\nlet footerBox: BoxRenderable | null = null\nlet currentFocusIndex = 0\n\nconst focusableElements: Array<InputRenderable | SelectRenderable> = []\nconst focusableBoxes: Array<BoxRenderable | null> = []\n\nconst colorOptions: SelectOption[] = [\n  { name: \"Red\", description: \"A warm primary color\", value: \"#ff0000\" },\n  { name: \"Blue\", description: \"A cool primary color\", value: \"#0066ff\" },\n  { name: \"Green\", description: \"A natural color\", value: \"#00aa00\" },\n  { name: \"Purple\", description: \"A regal color\", value: \"#8a2be2\" },\n  { name: \"Orange\", description: \"A vibrant color\", value: \"#ff8c00\" },\n  { name: \"Teal\", description: \"A calming color\", value: \"#008080\" },\n]\n\nconst sizeOptions: SelectOption[] = [\n  { name: \"Small\", description: \"Compact size (8px)\", value: 8 },\n  { name: \"Medium\", description: \"Standard size (12px)\", value: 12 },\n  { name: \"Large\", description: \"Big size (16px)\", value: 16 },\n  { name: \"Extra Large\", description: \"Huge size (20px)\", value: 20 },\n]\n\nfunction createLayoutElements(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#001122\")\n\n  headerBox = new BoxRenderable(renderer, {\n    id: \"header-box\",\n    zIndex: 0,\n    width: \"auto\",\n    height: 3,\n    backgroundColor: \"#3b82f6\",\n    borderStyle: \"single\",\n    borderColor: \"#2563eb\",\n    flexGrow: 0,\n    flexShrink: 0,\n    border: true,\n  })\n\n  header = new TextRenderable(renderer, {\n    id: \"header\",\n    content: \"INPUT & SELECT LAYOUT DEMO\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n    zIndex: 1,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  headerBox.add(header)\n\n  selectContainerBox = new BoxRenderable(renderer, {\n    id: \"select-container-box\",\n    zIndex: 0,\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    flexShrink: 1,\n    minHeight: 10,\n    backgroundColor: \"#1e293b\",\n    borderStyle: \"single\",\n    borderColor: \"#475569\",\n    border: true,\n  })\n\n  selectContainer = new BoxRenderable(renderer, {\n    id: \"select-container\",\n    zIndex: 1,\n    width: \"auto\",\n    height: \"auto\",\n    flexDirection: \"row\",\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  selectContainerBox.add(selectContainer)\n\n  leftSelectBox = new BoxRenderable(renderer, {\n    id: \"color-select-box\",\n    zIndex: 0,\n    width: \"auto\",\n    height: \"auto\",\n    minHeight: 8,\n    borderStyle: \"single\",\n    borderColor: \"#475569\",\n    focusedBorderColor: \"#3b82f6\",\n    title: \"Color Selection\",\n    titleAlignment: \"center\",\n    flexGrow: 1,\n    flexShrink: 1,\n    backgroundColor: \"transparent\",\n    border: true,\n  })\n\n  leftSelect = new SelectRenderable(renderer, {\n    id: \"color-select\",\n    zIndex: 1,\n    width: \"auto\",\n    height: \"auto\",\n    minHeight: 6,\n    options: colorOptions,\n    backgroundColor: \"#1e293b\",\n    focusedBackgroundColor: \"#2d3748\",\n    textColor: \"#e2e8f0\",\n    focusedTextColor: \"#f7fafc\",\n    selectedBackgroundColor: \"#3b82f6\",\n    selectedTextColor: \"#ffffff\",\n    descriptionColor: \"#94a3b8\",\n    selectedDescriptionColor: \"#cbd5e1\",\n    showScrollIndicator: true,\n    wrapSelection: true,\n    showDescription: true,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  leftSelectBox.add(leftSelect)\n\n  rightSelectBox = new BoxRenderable(renderer, {\n    id: \"size-select-box\",\n    zIndex: 0,\n    width: \"auto\",\n    height: \"auto\",\n    minHeight: 8,\n    borderStyle: \"single\",\n    borderColor: \"#475569\",\n    focusedBorderColor: \"#059669\",\n    title: \"Size Selection\",\n    titleAlignment: \"center\",\n    flexGrow: 1,\n    flexShrink: 1,\n    backgroundColor: \"transparent\",\n    border: true,\n  })\n\n  rightSelect = new SelectRenderable(renderer, {\n    id: \"size-select\",\n    zIndex: 1,\n    width: \"auto\",\n    height: \"auto\",\n    minHeight: 6,\n    options: sizeOptions,\n    backgroundColor: \"#1e293b\",\n    focusedBackgroundColor: \"#2d3748\",\n    textColor: \"#e2e8f0\",\n    focusedTextColor: \"#f7fafc\",\n    selectedBackgroundColor: \"#059669\",\n    selectedTextColor: \"#ffffff\",\n    descriptionColor: \"#94a3b8\",\n    selectedDescriptionColor: \"#cbd5e1\",\n    showScrollIndicator: true,\n    wrapSelection: true,\n    showDescription: true,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  rightSelectBox.add(rightSelect)\n\n  inputContainerBox = new BoxRenderable(renderer, {\n    id: \"input-container-box\",\n    zIndex: 0,\n    width: \"auto\",\n    height: 7,\n    flexGrow: 0,\n    flexShrink: 0,\n    backgroundColor: \"#0f172a\",\n    borderStyle: \"single\",\n    borderColor: \"#334155\",\n    border: true,\n  })\n\n  inputContainer = new BoxRenderable(renderer, {\n    id: \"input-container\",\n    zIndex: 1,\n    width: \"auto\",\n    height: \"auto\",\n    flexDirection: \"column\",\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  inputContainerBox.add(inputContainer)\n\n  inputLabel = new TextRenderable(renderer, {\n    id: \"input-label\",\n    content: \"Enter your text:\",\n    fg: \"#f1f5f9\",\n    bg: \"#0f172a\",\n    zIndex: 0,\n    flexGrow: 0,\n    flexShrink: 0,\n  })\n\n  textInputBox = new BoxRenderable(renderer, {\n    id: \"text-input-box\",\n    zIndex: 0,\n    width: \"auto\",\n    height: 3,\n    borderStyle: \"single\",\n    borderColor: \"#475569\",\n    focusedBorderColor: \"#eab308\",\n    flexGrow: 0,\n    flexShrink: 0,\n    marginTop: 1,\n    backgroundColor: \"transparent\",\n    border: true,\n  })\n\n  textInput = new InputRenderable(renderer, {\n    id: \"text-input\",\n    zIndex: 1,\n    width: \"auto\",\n    height: 1,\n    placeholder: \"Type something here...\",\n    backgroundColor: \"#1e293b\",\n    focusedBackgroundColor: \"#334155\",\n    textColor: \"#f1f5f9\",\n    focusedTextColor: \"#ffffff\",\n    placeholderColor: \"#64748b\",\n    cursorColor: \"#f1f5f9\",\n    maxLength: 100,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  textInputBox.add(textInput)\n\n  footerBox = new BoxRenderable(renderer, {\n    id: \"footer-box\",\n    zIndex: 0,\n    width: \"auto\",\n    height: 3,\n    backgroundColor: \"#1e40af\",\n    borderStyle: \"single\",\n    borderColor: \"#1d4ed8\",\n    flexGrow: 0,\n    flexShrink: 0,\n    border: true,\n  })\n\n  footer = new TextRenderable(renderer, {\n    id: \"footer\",\n    content: \"TAB: focus next | SHIFT+TAB: focus prev | ARROWS/JK: navigate | ESC: quit\",\n    fg: \"#dbeafe\",\n    bg: \"transparent\",\n    zIndex: 1,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  footerBox.add(footer)\n\n  selectContainer.add(leftSelectBox)\n  selectContainer.add(rightSelectBox)\n  inputContainer.add(inputLabel)\n  inputContainer.add(textInputBox)\n\n  renderer.root.add(headerBox)\n  renderer.root.add(selectContainerBox)\n  renderer.root.add(inputContainerBox)\n  renderer.root.add(footerBox)\n\n  focusableElements.push(leftSelect, rightSelect, textInput)\n  focusableBoxes.push(leftSelectBox, rightSelectBox, textInputBox)\n  setupEventHandlers()\n  updateFocus()\n\n  renderer.on(\"resize\", handleResize)\n}\n\nfunction setupEventHandlers(): void {\n  if (!leftSelect || !rightSelect || !textInput) return\n\n  leftSelect.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {\n    updateDisplay()\n  })\n\n  leftSelect.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {\n    updateDisplay()\n  })\n\n  rightSelect.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {\n    updateDisplay()\n  })\n\n  rightSelect.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {\n    updateDisplay()\n  })\n\n  textInput.on(InputRenderableEvents.INPUT, (value: string) => {\n    updateDisplay()\n  })\n\n  textInput.on(InputRenderableEvents.CHANGE, (value: string) => {\n    updateDisplay()\n  })\n}\n\nfunction updateDisplay(): void {\n  if (!leftSelect || !rightSelect || !textInput || !inputLabel) return\n\n  const selectedColor = leftSelect.getSelectedOption()\n  const selectedSize = rightSelect.getSelectedOption()\n  const inputValue = textInput.value\n\n  let displayText = \"Enter your text:\"\n  if (inputValue) {\n    displayText += ` \"${inputValue}\"`\n  }\n  if (selectedColor) {\n    displayText += ` in ${selectedColor.name}`\n  }\n  if (selectedSize) {\n    displayText += ` (${selectedSize.name})`\n  }\n\n  inputLabel.content = displayText\n}\n\nfunction handleResize(width: number, height: number): void {\n  // Root layout is automatically resized by the renderer\n}\n\nfunction updateFocus(): void {\n  focusableElements.forEach((element) => element.blur())\n  focusableBoxes.forEach((box) => {\n    if (box) box.blur()\n  })\n\n  if (focusableElements[currentFocusIndex]) {\n    focusableElements[currentFocusIndex].focus()\n  }\n  if (focusableBoxes[currentFocusIndex]) {\n    focusableBoxes[currentFocusIndex]!.focus()\n  }\n}\n\nfunction handleKeyPress(key: KeyEvent): void {\n  if (key.name === \"tab\") {\n    if (key.shift) {\n      currentFocusIndex = (currentFocusIndex - 1 + focusableElements.length) % focusableElements.length\n    } else {\n      currentFocusIndex = (currentFocusIndex + 1) % focusableElements.length\n    }\n    updateFocus()\n    return\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  createLayoutElements(rendererInstance)\n  rendererInstance.keyInput.on(\"keypress\", handleKeyPress)\n  updateDisplay()\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  rendererInstance.keyInput.off(\"keypress\", handleKeyPress)\n\n  if (renderer) {\n    renderer.off(\"resize\", handleResize)\n  }\n\n  // Properly destroy all elements that need cleanup\n  if (leftSelect) leftSelect.destroy()\n  if (rightSelect) rightSelect.destroy()\n  if (textInput) textInput.destroy()\n\n  // Clean up elements directly from root\n  if (headerBox) rendererInstance.root.remove(headerBox.id)\n  if (selectContainerBox) rendererInstance.root.remove(selectContainerBox.id)\n  if (inputContainerBox) rendererInstance.root.remove(inputContainerBox.id)\n  if (footerBox) rendererInstance.root.remove(footerBox.id)\n\n  // Clean up all elements\n  header = null\n  headerBox = null\n  selectContainer = null\n  selectContainerBox = null\n  leftSelect = null\n  leftSelectBox = null\n  rightSelect = null\n  rightSelectBox = null\n  inputContainer = null\n  inputContainerBox = null\n  inputLabel = null\n  textInput = null\n  textInputBox = null\n  footer = null\n  footerBox = null\n  renderer = null\n  currentFocusIndex = 0\n  focusableElements.length = 0\n  focusableBoxes.length = 0\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 30,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/install.sh",
    "content": "#!/bin/sh\nset -e\n\n# OpenTUI Examples Installation Script\n# Downloads and runs the latest opentui-examples binary\n\nREPO=\"anomalyco/opentui\"\nGITHUB_API=\"https://api.github.com/repos/$REPO\"\n\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# Default to stable releases\nUSE_PRERELEASE=false\n\n# Parse arguments\nwhile [ $# -gt 0 ]; do\n  case $1 in\n    --pre|--prerelease)\n      USE_PRERELEASE=true\n      shift\n      ;;\n    -h|--help)\n      echo \"Usage: $0 [OPTIONS]\"\n      echo \"\"\n      echo \"Options:\"\n      echo \"  --pre, --prerelease    Download the latest pre-release version\"\n      echo \"  -h, --help            Show this help message\"\n      exit 0\n      ;;\n    *)\n      printf \"${RED}Error: Unknown option: $1${NC}\\n\"\n      echo \"Use --help for usage information\"\n      exit 1\n      ;;\n  esac\ndone\n\nprintf \"${GREEN}OpenTUI Examples Installer${NC}\\n\"\necho \"Installing opentui-examples binary...\"\necho \"\"\n\n# Detect platform\nOS=$(uname -s | tr '[:upper:]' '[:lower:]')\nARCH=$(uname -m)\n\ncase \"$ARCH\" in\n    x86_64|amd64) ARCH=\"x64\" ;;\n    arm64|aarch64) ARCH=\"arm64\" ;;\n    *) printf \"${RED}Error: Unsupported architecture: $ARCH${NC}\\n\"; exit 1 ;;\nesac\n\ncase \"$OS\" in\n    darwin) OS=\"darwin\" ;;\n    linux) OS=\"linux\" ;;\n    mingw*|cygwin*|msys*) OS=\"windows\" ;;\n    *) printf \"${RED}Error: Unsupported OS: $OS${NC}\\n\"; exit 1 ;;\nesac\n\nPLATFORM=\"${OS}-${ARCH}\"\necho \"Detected platform: $PLATFORM\"\n\n# Find the latest release\necho \"Fetching latest release information...\"\n\nif [ \"$USE_PRERELEASE\" = \"true\" ]; then\n  printf \"${YELLOW}Looking for latest pre-release...${NC}\\n\"\n  # Get all releases and find the first one (which could be a pre-release)\n  RELEASE_DATA=$(curl -s \"$GITHUB_API/releases\" | grep -m 1 '\"tag_name\"' | cut -d '\"' -f 4)\n  if [ -z \"$RELEASE_DATA\" ]; then\n    printf \"${RED}Error: Failed to fetch release information${NC}\\n\"\n    exit 1\n  fi\n  VERSION=\"$RELEASE_DATA\"\nelse\n  # Get the latest stable release\n  RELEASE_DATA=$(curl -s \"$GITHUB_API/releases/latest\")\n  VERSION=$(echo \"$RELEASE_DATA\" | grep '\"tag_name\"' | cut -d '\"' -f 4)\n  if [ -z \"$VERSION\" ]; then\n    printf \"${RED}Error: Failed to fetch latest release information${NC}\\n\"\n    exit 1\n  fi\nfi\n\n# Remove 'v' prefix if present\nVERSION_NO_V=\"${VERSION#v}\"\n\nprintf \"${BLUE}Version: $VERSION${NC}\\n\"\n\n# Construct download URL\nASSET_NAME=\"opentui-examples-v${VERSION_NO_V}-${PLATFORM}.zip\"\nDOWNLOAD_URL=\"https://github.com/$REPO/releases/download/${VERSION}/${ASSET_NAME}\"\n\necho \"Download URL: $DOWNLOAD_URL\"\necho \"\"\n\n# Create temporary directory\nTEMP_DIR=$(mktemp -d)\ntrap \"rm -rf $TEMP_DIR\" EXIT\n\n# Download the zip file\necho \"Downloading $ASSET_NAME...\"\nif ! curl -L -f -o \"$TEMP_DIR/examples.zip\" \"$DOWNLOAD_URL\"; then\n  printf \"${RED}Error: Failed to download examples binary${NC}\\n\"\n  echo \"URL attempted: $DOWNLOAD_URL\"\n  exit 1\nfi\n\n# Unzip to current directory\necho \"Extracting to current directory...\"\nif ! unzip -q -o \"$TEMP_DIR/examples.zip\" -d .; then\n  printf \"${RED}Error: Failed to extract archive${NC}\\n\"\n  exit 1\nfi\n\n# Make executable (if not on Windows)\nif [ \"$OS\" != \"windows\" ]; then\n  if [ -f \"./opentui-examples\" ]; then\n    chmod +x ./opentui-examples\n    EXEC_NAME=\"./opentui-examples\"\n  elif [ -f \"./opentui-examples.exe\" ]; then\n    EXEC_NAME=\"./opentui-examples.exe\"\n  else\n    printf \"${RED}Error: Executable not found after extraction${NC}\\n\"\n    ls -la\n    exit 1\n  fi\nelse\n  EXEC_NAME=\"./opentui-examples.exe\"\nfi\n\nprintf \"${GREEN}✓ OpenTUI Examples installed successfully!${NC}\\n\"\necho \"\"\nprintf \"${BLUE}To run the examples, execute:${NC}\\n\"\nif [ \"$OS\" = \"windows\" ]; then\n  printf \"  ${GREEN}.\\\\\\\\opentui-examples.exe${NC}\\n\"\nelse\n  printf \"  ${GREEN}./opentui-examples${NC}\\n\"\nfi\necho \"\"\n"
  },
  {
    "path": "packages/core/src/examples/keypress-debug-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  type CliRenderer,\n  createCliRenderer,\n  BoxRenderable,\n  TextRenderable,\n  type KeyEvent,\n  type PasteEvent,\n  decodePasteBytes,\n} from \"../index.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { TextNodeRenderable } from \"../renderables/TextNode.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { env, registerEnvVar } from \"../lib/env.js\"\n\nregisterEnvVar({\n  name: \"OTUI_KEYPRESS_DEBUG_SHOW_JSON\",\n  description: \"Show full JSON alongside formatted output in keypress debug tool\",\n  type: \"boolean\",\n  default: false,\n})\n\nlet scrollBox: ScrollBoxRenderable | null = null\nlet eventCount = 0\nlet helpModal: BoxRenderable | null = null\nlet helpContent: TextRenderable | null = null\nlet scrollHint: TextRenderable | null = null\nlet showingHelp = false\nlet showJson = false\nlet inputHandler: ((sequence: string) => boolean) | null = null\nlet keypressHandler: ((event: KeyEvent) => void) | null = null\nlet keyreleaseHandler: ((event: KeyEvent) => void) | null = null\nlet pasteHandler: ((event: PasteEvent) => void) | null = null\n\n// Storage for all captured data\nlet allRawInputs: Array<{ timestamp: string; sequence: string }> = []\nlet allKeyEvents: Array<{ timestamp: string; type: string; event: any }> = []\n\nfunction saveToFile(capabilities: CliRenderer[\"capabilities\"]) {\n  const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\")\n  const filename = `keypress-debug-${timestamp}.json`\n\n  const data = {\n    exportedAt: new Date().toISOString(),\n    rawInputs: allRawInputs,\n    keyEvents: allKeyEvents,\n    summary: {\n      totalRawInputs: allRawInputs.length,\n      totalKeyEvents: allKeyEvents.length,\n    },\n    capabilities,\n  }\n\n  try {\n    Bun.write(filename, JSON.stringify(data, null, 2))\n    console.log(`Saved debug data to ${filename}`)\n  } catch (error) {\n    console.error(`Failed to save file: ${error}`)\n  }\n}\n\nfunction formatEventAsText(renderer: CliRenderer, eventType: string, event: any): TextRenderable {\n  const eventText = new TextRenderable(renderer, {\n    id: `event-text-${eventCount}`,\n  })\n\n  // Event type header with icon\n  let icon = \"⌨️ \"\n  let typeColor = \"#A5D6FF\"\n  if (eventType === \"keypress\") {\n    icon = \"↓ \"\n    typeColor = \"#7EE787\"\n  } else if (eventType === \"keyrelease\") {\n    icon = \"↑ \"\n    typeColor = \"#FFA657\"\n  } else if (eventType === \"paste\") {\n    icon = \"📋 \"\n    typeColor = \"#D2A8FF\"\n  } else if (eventType === \"capabilities\") {\n    icon = \"ℹ️  \"\n    typeColor = \"#79C0FF\"\n  }\n\n  const typeNode = TextNodeRenderable.fromString(`${icon}${eventType.toUpperCase()}`, {\n    fg: typeColor,\n    attributes: 1, // bold\n  })\n  eventText.textNode.add(typeNode)\n\n  // Key name (if available)\n  if (event.name) {\n    const keyNode = TextNodeRenderable.fromString(` ${event.name}`, {\n      fg: \"#FFA657\",\n      attributes: 1,\n    })\n    eventText.textNode.add(keyNode)\n  }\n\n  // Modifiers\n  const modifiers: string[] = []\n  if (event.ctrl) modifiers.push(\"Ctrl\")\n  if (event.meta) modifiers.push(\"Meta\")\n  if (event.shift) modifiers.push(\"Shift\")\n  if (event.option) modifiers.push(\"Option\")\n  if (event.super) modifiers.push(\"Super\")\n  if (event.hyper) modifiers.push(\"Hyper\")\n\n  if (modifiers.length > 0) {\n    const modNode = TextNodeRenderable.fromString(` [${modifiers.join(\"+\")}]`, {\n      fg: \"#D2A8FF\",\n    })\n    eventText.textNode.add(modNode)\n  }\n\n  // Sequence/Raw\n  if (event.raw || event.sequence) {\n    const raw = event.raw || event.sequence\n    const displayRaw = JSON.stringify(raw)\n    const rawNode = TextNodeRenderable.fromString(` ${displayRaw}`, {\n      fg: \"#79C0FF\",\n    })\n    eventText.textNode.add(rawNode)\n  }\n\n  // Source\n  if (event.source) {\n    const sourceNode = TextNodeRenderable.fromString(` (${event.source})`, {\n      fg: \"#8B949E\",\n    })\n    eventText.textNode.add(sourceNode)\n  }\n\n  // Paste text\n  if (eventType === \"paste\") {\n    const pasteText = decodePasteBytes(event.bytes)\n    const textPreview = pasteText.length > 50 ? pasteText.substring(0, 47) + \"...\" : pasteText\n    const pasteNode = TextNodeRenderable.fromString(`\\n  \"${textPreview}\"`, {\n      fg: \"#A5D6FF\",\n    })\n    eventText.textNode.add(pasteNode)\n  }\n\n  // Capabilities info - show full details\n  if (eventType === \"capabilities\") {\n    const capsText = JSON.stringify(event, null, 2)\n    const capsNode = TextNodeRenderable.fromString(`\\n${capsText}`, {\n      fg: \"#8B949E\",\n    })\n    eventText.textNode.add(capsNode)\n  }\n\n  // Timestamp\n  const time = new Date().toLocaleTimeString()\n  const timeNode = TextNodeRenderable.fromString(`\\n  ${time}`, {\n    fg: \"#6E7681\",\n  })\n  eventText.textNode.add(timeNode)\n\n  // Show full JSON if enabled\n  if (showJson && eventType !== \"capabilities\") {\n    const jsonText = JSON.stringify({ type: eventType, timestamp: new Date().toISOString(), ...event }, null, 2)\n    const jsonNode = TextNodeRenderable.fromString(`\\n\\n${jsonText}`, {\n      fg: \"#8B949E\",\n    })\n    eventText.textNode.add(jsonNode)\n  }\n\n  return eventText\n}\n\nfunction addEvent(renderer: CliRenderer, eventType: string, event: object) {\n  if (!scrollBox) return\n\n  eventCount++\n\n  const eventBox = new BoxRenderable(renderer, {\n    id: `event-${eventCount}`,\n    width: \"auto\",\n    marginBottom: 1,\n    padding: 1,\n    backgroundColor: \"#1f2937\",\n    borderColor: \"#374151\",\n    borderStyle: \"single\",\n    border: true,\n  })\n\n  const eventDisplay = formatEventAsText(renderer, eventType, event)\n  eventBox.add(eventDisplay)\n  scrollBox.add(eventBox)\n\n  const children = scrollBox.getChildren()\n  if (children.length > 50) {\n    const oldest = children[0]\n    if (oldest) {\n      scrollBox.remove(oldest.id)\n      oldest.destroyRecursively()\n    }\n  }\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"#0D1117\")\n\n  // Initialize showJson from env var\n  showJson = env.OTUI_KEYPRESS_DEBUG_SHOW_JSON\n\n  // Get any debug inputs captured before this tool started (e.g., during setupTerminal)\n  const cachedDebugInputs = renderer.getDebugInputs()\n  if (cachedDebugInputs.length > 0) {\n    allRawInputs.push(...cachedDebugInputs)\n    console.log(`Loaded ${cachedDebugInputs.length} pre-captured debug inputs (including terminal setup)`)\n  }\n\n  const mainContainer = new BoxRenderable(renderer, {\n    id: \"main-container\",\n    flexGrow: 1,\n    flexDirection: \"column\",\n  })\n\n  renderer.root.add(mainContainer)\n\n  scrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"event-scroll-box\",\n    stickyScroll: true,\n    stickyStart: \"bottom\",\n    border: true,\n    borderColor: \"#6BCF7F\",\n    title: \"Keypress Debug Tool (Press ? for keys)\",\n    titleAlignment: \"center\",\n    contentOptions: {\n      paddingLeft: 1,\n      paddingRight: 1,\n      paddingTop: 1,\n    },\n  })\n\n  mainContainer.add(scrollBox)\n\n  // Create help modal (hidden by default)\n  helpModal = new BoxRenderable(renderer, {\n    id: \"help-modal\",\n    position: \"absolute\",\n    left: \"5%\",\n    top: \"5%\",\n    width: \"90%\",\n    height: \"90%\",\n    border: true,\n    borderStyle: \"double\",\n    borderColor: \"#4ECDC4\",\n    backgroundColor: \"#0D1117\",\n    title: \"Keybindings\",\n    titleAlignment: \"center\",\n    flexDirection: \"column\",\n    zIndex: 100,\n    visible: false,\n  })\n\n  helpContent = new TextRenderable(renderer, {\n    id: \"help-content\",\n    content: `Actions:\n  Shift+C : Refresh terminal capabilities\n  Shift+J : Toggle JSON view (show full JSON)\n  Shift+S : Save all captured data to JSON file\n  ?       : Toggle this help screen\n  ESC     : Return to main menu\n\nEvents Captured:\n  • All keypress events\n  • All keyrelease events\n  • Paste events\n  • Raw input sequences (including unhandled)\n\nEnv Vars:\n  OTUI_KEYPRESS_DEBUG_SHOW_JSON=true\n    Enable JSON view at startup\n\nThe debug tool displays all keyboard and\ninput events in real-time. Use Shift+S to\nsave all captured data to a timestamped\nJSON file in the current directory.`,\n    fg: \"#E6EDF3\",\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  helpModal.add(helpContent)\n\n  // Scroll hint (shown only when there's content to scroll)\n  scrollHint = new TextRenderable(renderer, {\n    id: \"scroll-hint\",\n    content: \"↑↓ to scroll\",\n    fg: \"#6E7681\",\n    flexShrink: 0,\n    height: 1,\n    visible: false,\n  })\n  helpModal.add(scrollHint)\n\n  renderer.root.add(helpModal)\n\n  addEvent(renderer, \"capabilities\", renderer.capabilities)\n\n  inputHandler = (sequence: string) => {\n    // Store all raw input\n    allRawInputs.push({\n      timestamp: new Date().toISOString(),\n      sequence,\n    })\n\n    addEvent(renderer, \"raw-input\", { sequence })\n    return false\n  }\n  // Prepend to capture everything, even what other handlers process\n  renderer.prependInputHandler(inputHandler)\n\n  keypressHandler = (event: KeyEvent) => {\n    // Store all keypress events\n    allKeyEvents.push({\n      timestamp: new Date().toISOString(),\n      type: \"keypress\",\n      event: { ...event },\n    })\n\n    // Handle help modal toggle\n    if (event.raw === \"?\" && helpModal) {\n      showingHelp = !showingHelp\n      helpModal.visible = showingHelp\n\n      // Update scroll hint visibility when modal opens\n      if (showingHelp && helpContent && scrollHint) {\n        const canScroll = helpContent.maxScrollY > 0\n        scrollHint.visible = canScroll\n      }\n      return\n    }\n\n    // Handle scrolling when help modal is open\n    if (showingHelp && helpContent) {\n      if (event.name === \"up\") {\n        helpContent.scrollY = Math.max(0, helpContent.scrollY - 1)\n        return\n      } else if (event.name === \"down\") {\n        helpContent.scrollY = Math.min(helpContent.maxScrollY, helpContent.scrollY + 1)\n        return\n      }\n    }\n\n    // Handle JSON view toggle\n    if (event.name === \"j\" && event.shift) {\n      showJson = !showJson\n      return\n    }\n\n    // Handle save to file\n    if (event.name === \"s\" && event.shift) {\n      saveToFile(renderer.capabilities)\n      return\n    }\n\n    // Don't log modal toggle key\n    if (showingHelp && event.raw === \"?\") {\n      return\n    }\n\n    addEvent(renderer, \"keypress\", event)\n\n    if (event.name === \"c\" && event.shift) {\n      addEvent(renderer, \"capabilities\", renderer.capabilities)\n    }\n  }\n  renderer.keyInput.on(\"keypress\", keypressHandler)\n\n  keyreleaseHandler = (event: KeyEvent) => {\n    // Store all keyrelease events\n    allKeyEvents.push({\n      timestamp: new Date().toISOString(),\n      type: \"keyrelease\",\n      event: { ...event },\n    })\n\n    addEvent(renderer, \"keyrelease\", event)\n  }\n  renderer.keyInput.on(\"keyrelease\", keyreleaseHandler)\n\n  pasteHandler = (event: PasteEvent) => {\n    // Store all paste events\n    allKeyEvents.push({\n      timestamp: new Date().toISOString(),\n      type: \"paste\",\n      event: { ...event },\n    })\n\n    addEvent(renderer, \"paste\", event)\n  }\n  renderer.keyInput.on(\"paste\", pasteHandler)\n\n  renderer.requestRender()\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n\n  // Remove event listeners\n  if (keypressHandler) {\n    renderer.keyInput.off(\"keypress\", keypressHandler)\n    keypressHandler = null\n  }\n\n  if (keyreleaseHandler) {\n    renderer.keyInput.off(\"keyrelease\", keyreleaseHandler)\n    keyreleaseHandler = null\n  }\n\n  if (pasteHandler) {\n    renderer.keyInput.off(\"paste\", pasteHandler)\n    pasteHandler = null\n  }\n\n  if (inputHandler) {\n    renderer.removeInputHandler(inputHandler)\n    inputHandler = null\n  }\n\n  if (scrollBox) {\n    renderer.root.remove(\"main-container\")\n    scrollBox = null\n  }\n\n  helpModal?.destroy()\n  helpModal = null\n  helpContent = null\n  scrollHint = null\n\n  eventCount = 0\n  showingHelp = false\n  showJson = false\n\n  // Clear captured data\n  allRawInputs = []\n  allKeyEvents = []\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n    useKittyKeyboard: { events: true },\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/lib/HexList.ts",
    "content": "import type { RenderableOptions } from \"../../Renderable.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\nimport { FrameBufferRenderable, type FrameBufferOptions } from \"../../renderables/FrameBuffer.js\"\nimport type { RenderContext } from \"../../types.js\"\nimport { TextAttributes } from \"../../index.js\"\n\nexport interface HexListOptions extends Omit<RenderableOptions<HexListRenderable>, \"width\" | \"height\"> {\n  colors: string[]\n  columns?: number\n  blockWidth?: number\n  blockHeight?: number\n  maxHeight?: number\n}\n\nexport class HexListRenderable extends FrameBufferRenderable {\n  private _colors: string[]\n  private _columns: number\n  private _blockWidth: number\n  private _blockHeight: number\n  private _maxHeight: number\n  private _itemWidth: number\n\n  constructor(ctx: RenderContext, options: HexListOptions) {\n    const columns = options.columns ?? 4\n    const blockWidth = options.blockWidth ?? 4\n    const blockHeight = options.blockHeight ?? 2\n    const itemWidth = 18 // Space for color box + spacing + index + hex\n    const maxHeight = options.maxHeight ?? Math.ceil(256 / columns) * (blockHeight + 1)\n\n    const colors = options.colors ?? []\n    const numRows = Math.ceil(colors.length / columns)\n    const requiredHeight = numRows * (blockHeight + 1)\n    const height = Math.min(requiredHeight, maxHeight)\n    const width = columns * itemWidth\n\n    super(ctx, {\n      ...options,\n      width,\n      height: Math.max(height, 1),\n    } as FrameBufferOptions)\n\n    this._colors = colors\n    this._columns = columns\n    this._blockWidth = blockWidth\n    this._blockHeight = blockHeight\n    this._maxHeight = maxHeight\n    this._itemWidth = itemWidth\n\n    this.renderHexList()\n  }\n\n  get colors(): string[] {\n    return this._colors\n  }\n\n  set colors(value: string[]) {\n    this._colors = value\n    this.updateDimensions()\n    this.renderHexList()\n    this.requestRender()\n  }\n\n  private updateDimensions(): void {\n    const numRows = Math.ceil(this._colors.length / this._columns)\n    const requiredHeight = numRows * (this._blockHeight + 1)\n    const newHeight = Math.min(requiredHeight, this._maxHeight)\n\n    if (this.height !== newHeight) {\n      this.height = Math.max(newHeight, 1)\n    }\n  }\n\n  protected onResize(width: number, height: number): void {\n    super.onResize(width, height)\n    this.renderHexList()\n  }\n\n  private renderHexList(): void {\n    if (this.isDestroyed) return\n\n    const buffer = this.frameBuffer\n    buffer.clear(RGBA.fromInts(30, 41, 59, 255)) // Slate-800 background\n\n    const actualSize = Math.min(this._colors.length, 256)\n\n    for (let i = 0; i < actualSize; i++) {\n      const color = this._colors[i]\n      if (!color) continue\n\n      const row = Math.floor(i / this._columns)\n      const col = i % this._columns\n\n      const x = col * this._itemWidth\n      const y = row * (this._blockHeight + 1) // Add spacing between rows\n\n      // Parse hex color\n      const hex = color.replace(\"#\", \"\")\n      const r = parseInt(hex.substring(0, 2), 16)\n      const g = parseInt(hex.substring(2, 4), 16)\n      const b = parseInt(hex.substring(4, 6), 16)\n      const rgba = RGBA.fromInts(r, g, b)\n\n      // Draw colored box\n      for (let dy = 0; dy < this._blockHeight; dy++) {\n        for (let dx = 0; dx < this._blockWidth; dx++) {\n          buffer.setCell(x + dx, y + dy, \" \", RGBA.fromInts(255, 255, 255), rgba)\n        }\n      }\n\n      // Draw index and hex value next to the box\n      const text = `${i.toString().padStart(3, \" \")}: ${color.toUpperCase()}`\n      const textColor = RGBA.fromInts(148, 163, 184)\n      const bgColor = RGBA.fromInts(30, 41, 59, 255)\n      const textStartX = x + this._blockWidth + 1\n      const spacing = 2\n\n      for (let ci = 0; ci < text.length && textStartX + ci < x + this._itemWidth - spacing; ci++) {\n        buffer.drawText(text[ci], textStartX + ci, y, textColor, bgColor, TextAttributes.NONE)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/examples/lib/PaletteGrid.ts",
    "content": "import type { RenderableOptions } from \"../../Renderable.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\nimport { FrameBufferRenderable, type FrameBufferOptions } from \"../../renderables/FrameBuffer.js\"\nimport type { RenderContext } from \"../../types.js\"\nimport { TextAttributes } from \"../../index.js\"\n\nexport interface PaletteGridOptions extends Omit<RenderableOptions<PaletteGridRenderable>, \"width\" | \"height\"> {\n  colors: string[]\n  blockWidth?: number\n  blockHeight?: number\n  colorsPerRow?: number\n  maxHeight?: number\n}\n\nexport class PaletteGridRenderable extends FrameBufferRenderable {\n  private _colors: string[]\n  private _blockWidth: number\n  private _blockHeight: number\n  private _colorsPerRow: number\n  private _maxHeight: number\n\n  constructor(ctx: RenderContext, options: PaletteGridOptions) {\n    const blockWidth = options.blockWidth ?? 4\n    const blockHeight = options.blockHeight ?? 2\n    const colorsPerRow = options.colorsPerRow ?? 16\n    const maxHeight = options.maxHeight ?? 32\n\n    const colors = options.colors ?? []\n    const numRows = Math.ceil(colors.length / colorsPerRow)\n    const requiredHeight = numRows * blockHeight\n    const height = Math.min(requiredHeight, maxHeight)\n    const width = colorsPerRow * blockWidth\n\n    super(ctx, {\n      ...options,\n      width,\n      height: Math.max(height, 1),\n    } as FrameBufferOptions)\n\n    this._colors = colors\n    this._blockWidth = blockWidth\n    this._blockHeight = blockHeight\n    this._colorsPerRow = colorsPerRow\n    this._maxHeight = maxHeight\n\n    this.renderPalette()\n  }\n\n  get colors(): string[] {\n    return this._colors\n  }\n\n  set colors(value: string[]) {\n    this._colors = value\n    this.updateDimensions()\n    this.renderPalette()\n    this.requestRender()\n  }\n\n  private updateDimensions(): void {\n    const numRows = Math.ceil(this._colors.length / this._colorsPerRow)\n    const requiredHeight = numRows * this._blockHeight\n    const newHeight = Math.min(requiredHeight, this._maxHeight)\n\n    if (this.height !== newHeight) {\n      this.height = Math.max(newHeight, 1)\n    }\n  }\n\n  protected onResize(width: number, height: number): void {\n    super.onResize(width, height)\n    this.renderPalette()\n  }\n\n  private renderPalette(): void {\n    if (this.isDestroyed) return\n\n    const buffer = this.frameBuffer\n    buffer.clear(RGBA.fromInts(30, 41, 59, 255)) // Slate-800 background\n\n    const size = this._colors.length\n\n    for (let i = 0; i < size; i++) {\n      const color = this._colors[i]\n      if (!color) continue\n\n      const row = Math.floor(i / this._colorsPerRow)\n      const col = i % this._colorsPerRow\n\n      const x = col * this._blockWidth\n      const y = row * this._blockHeight\n\n      // Parse hex color\n      const hex = color.replace(\"#\", \"\")\n      const r = parseInt(hex.substring(0, 2), 16)\n      const g = parseInt(hex.substring(2, 4), 16)\n      const b = parseInt(hex.substring(4, 6), 16)\n      const rgba = RGBA.fromInts(r, g, b)\n\n      // Draw the color block using spaces with background color\n      for (let dy = 0; dy < this._blockHeight; dy++) {\n        for (let dx = 0; dx < this._blockWidth; dx++) {\n          buffer.setCell(x + dx, y + dy, \" \", RGBA.fromInts(255, 255, 255), rgba)\n        }\n      }\n\n      // Add color index number in the center of the block (if block is large enough)\n      if (this._blockWidth >= 3 && this._blockHeight >= 1) {\n        const indexStr = i.toString()\n        const textX = x + Math.floor((this._blockWidth - indexStr.length) / 2)\n        const textY = y + Math.floor(this._blockHeight / 2)\n\n        // Choose text color based on background brightness\n        const brightness = (r * 299 + g * 587 + b * 114) / 1000\n        const textColor = brightness > 128 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255)\n\n        if (indexStr.length <= this._blockWidth) {\n          for (let ci = 0; ci < indexStr.length; ci++) {\n            buffer.drawText(indexStr[ci], textX + ci, textY, textColor, rgba, TextAttributes.NONE)\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/examples/lib/standalone-keys.ts",
    "content": "import { resolveRenderLib, type CliRenderer, type KeyEvent } from \"../../index.js\"\n\nexport function setupCommonDemoKeys(renderer: CliRenderer) {\n  renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"`\" || key.name === '\"') {\n      renderer.console.toggle()\n    } else if (key.name === \".\") {\n      renderer.toggleDebugOverlay()\n    } else if (key.name === \"g\" && key.ctrl) {\n      console.log(\"dumping hit grid\")\n      renderer.dumpHitGrid()\n    } else if (key.name === \"l\" && key.shift) {\n      renderer.start()\n    } else if (key.name === \"s\" && key.shift) {\n      renderer.stop()\n    } else if (key.name === \"a\" && key.shift) {\n      renderer.auto()\n    } else if (key.name === \"a\" && key.ctrl) {\n      const lib = resolveRenderLib()\n      const rawBytes = lib.getArenaAllocatedBytes()\n      const formattedBytes = `${(rawBytes / 1024 / 1024).toFixed(2)} MB`\n      console.log(\"arena allocated bytes:\", formattedBytes)\n    }\n  })\n}\n"
  },
  {
    "path": "packages/core/src/examples/lib/tab-controller.ts",
    "content": "import { Renderable, type RenderableOptions, RenderableEvents } from \"../../Renderable.js\"\nimport { OptimizedBuffer } from \"../../buffer.js\"\nimport { BoxRenderable } from \"../../renderables/index.js\"\nimport { TabSelectRenderable, TabSelectRenderableEvents } from \"../../renderables/TabSelect.js\"\nimport type { CliRenderer, TabSelectOption } from \"../../index.js\"\nimport { parseColor, type ColorInput } from \"../../lib/RGBA.js\"\n\nexport interface TabObject {\n  title: string\n  init(tabGroup: Renderable): void\n  update?(deltaMs: number, tabGroup: Renderable): void\n  show?(): void\n  hide?(): void\n}\n\ninterface Tab {\n  title: string\n  tabObject: TabObject\n  group: Renderable\n  initialized: boolean\n}\n\nexport interface TabControllerOptions extends RenderableOptions<TabControllerRenderable> {\n  backgroundColor?: ColorInput\n  textColor?: ColorInput\n  tabBarHeight?: number\n  tabBarBackgroundColor?: ColorInput\n  selectedBackgroundColor?: ColorInput\n  selectedTextColor?: ColorInput\n  selectedDescriptionColor?: ColorInput\n  showDescription?: boolean\n  showUnderline?: boolean\n  showScrollArrows?: boolean\n}\n\nexport enum TabControllerEvents {\n  TAB_CHANGED = \"tabChanged\",\n}\n\nexport class TabControllerRenderable extends Renderable {\n  public tabs: Tab[] = []\n  private currentTabIndex = 0\n  private tabSelectElement: TabSelectRenderable\n  private tabBarHeight: number\n  private frameCallback: ((deltaMs: number) => Promise<void>) | null = null\n\n  constructor(\n    id: string,\n    private renderer: CliRenderer,\n    options: TabControllerOptions,\n  ) {\n    super(renderer, { ...options, id, buffered: options.backgroundColor ? true : false })\n\n    this.tabBarHeight = options.tabBarHeight || 4\n\n    this.tabSelectElement = new TabSelectRenderable(renderer, {\n      id: `${id}-tabs`,\n      width: \"100%\",\n      height: this.tabBarHeight,\n      options: [],\n      zIndex: this.zIndex + 100,\n      selectedBackgroundColor: options.selectedBackgroundColor || \"#333333\",\n      selectedTextColor: options.selectedTextColor || \"#FFFF00\",\n      textColor: parseColor(options.textColor || \"#FFFFFF\"),\n      selectedDescriptionColor: options.selectedDescriptionColor || \"#FFFFFF\",\n      backgroundColor: options.tabBarBackgroundColor || options.backgroundColor || \"transparent\",\n      showDescription: options.showDescription ?? true,\n      showUnderline: options.showUnderline ?? true,\n      showScrollArrows: options.showScrollArrows ?? true,\n    })\n\n    this.tabSelectElement.on(TabSelectRenderableEvents.SELECTION_CHANGED, (index: number) => {\n      this.switchToTab(index)\n    })\n\n    this.add(this.tabSelectElement)\n\n    this.frameCallback = async (deltaMs) => {\n      this.update(deltaMs)\n    }\n    this.renderer.setFrameCallback(this.frameCallback)\n  }\n\n  public addTab(tabObject: TabObject): Tab {\n    const tabGroup = new BoxRenderable(this.ctx, {\n      id: `${this.id}-tab-${this.tabs.length}`,\n      left: 0,\n      top: this.tabBarHeight,\n      zIndex: this.zIndex + 50,\n      visible: false,\n      width: \"100%\",\n      height: 1,\n    })\n\n    this.add(tabGroup)\n\n    const tab: Tab = {\n      title: tabObject.title,\n      tabObject,\n      group: tabGroup,\n      initialized: false,\n    }\n    this.tabs.push(tab)\n\n    this.updateTabSelectOptions()\n    return tab\n  }\n\n  private updateTabSelectOptions(): void {\n    const options: TabSelectOption[] = this.tabs.map((tab, index) => ({\n      name: tab.title,\n      description: `Tab ${index + 1}/${this.tabs.length} - Use Left/Right arrows to navigate | Press Ctrl+C to exit | D: toggle debug`,\n      value: index,\n    }))\n\n    this.tabSelectElement.setOptions(options)\n\n    if (this.tabs.length === 1) {\n      const firstTab = this.getCurrentTab()\n      firstTab.group.visible = true\n      this.initializeTab(firstTab)\n\n      if (firstTab.tabObject.show) {\n        firstTab.tabObject.show()\n      }\n    }\n  }\n\n  private initializeTab(tab: Tab): void {\n    if (!tab.initialized) {\n      tab.tabObject.init(tab.group)\n      tab.initialized = true\n    }\n  }\n\n  public getCurrentTab(): Tab {\n    return this.tabs[this.currentTabIndex]\n  }\n\n  public getCurrentTabGroup(): Renderable {\n    return this.getCurrentTab().group\n  }\n\n  public switchToTab(index: number): void {\n    if (index < 0 || index >= this.tabs.length) return\n    if (index === this.currentTabIndex) return\n\n    const currentTab = this.getCurrentTab()\n    currentTab.group.visible = false\n    if (currentTab.tabObject.hide) {\n      currentTab.tabObject.hide()\n    }\n\n    this.currentTabIndex = index\n    this.tabSelectElement.setSelectedIndex(index)\n\n    const newTab = this.getCurrentTab()\n    newTab.group.visible = true\n\n    this.initializeTab(newTab)\n\n    if (newTab.tabObject.show) {\n      newTab.tabObject.show()\n    }\n\n    this.emit(TabControllerEvents.TAB_CHANGED, index, newTab)\n  }\n\n  public nextTab(): void {\n    this.switchToTab((this.currentTabIndex + 1) % this.tabs.length)\n  }\n\n  public previousTab(): void {\n    this.switchToTab((this.currentTabIndex - 1 + this.tabs.length) % this.tabs.length)\n  }\n\n  public update(deltaMs: number): void {\n    const currentTab = this.getCurrentTab()\n    if (currentTab && currentTab.tabObject.update) {\n      currentTab.tabObject.update(deltaMs, currentTab.group)\n    }\n  }\n\n  public getCurrentTabIndex(): number {\n    return this.currentTabIndex\n  }\n\n  public getTabSelectElement(): TabSelectRenderable {\n    return this.tabSelectElement\n  }\n\n  public focus(): void {\n    this.tabSelectElement.focus()\n    this.emit(RenderableEvents.FOCUSED)\n  }\n\n  public blur(): void {\n    this.tabSelectElement.blur()\n    this.emit(RenderableEvents.BLURRED)\n  }\n\n  public get focused(): boolean {\n    return this.tabSelectElement.focused\n  }\n\n  public onResize(width: number, height: number): void {\n    if (this.width === width && this.height === height) return\n\n    this.width = width\n    this.height = height\n\n    this.tabSelectElement.width = width\n    this.tabSelectElement.height = this.tabBarHeight\n\n    for (const tab of this.tabs) {\n      tab.group.y = this.tabBarHeight\n      tab.group.width = width\n      tab.group.height = height - this.tabBarHeight\n    }\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    // TabController doesn't render content directly, it manages tab selection and tab content\n    // The tab select element and tab content groups handle their own rendering\n  }\n\n  protected destroySelf(): void {\n    this.blur()\n\n    if (this.frameCallback) {\n      this.renderer.removeFrameCallback(this.frameCallback)\n      this.frameCallback = null\n    }\n\n    for (const tab of this.tabs) {\n      tab.group.destroy()\n    }\n\n    this.tabSelectElement.destroy()\n\n    this.removeAllListeners()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/examples/lights-phong-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  BoxRenderable,\n  TextRenderable,\n  FrameBufferRenderable,\n  type KeyEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { TextureUtils } from \"../3d/TextureUtils.js\"\nimport {\n  Scene as ThreeScene,\n  Mesh as ThreeMesh,\n  PerspectiveCamera,\n  PointLight as ThreePointLight,\n  SphereGeometry,\n  RepeatWrapping,\n  DataTexture,\n} from \"three\"\nimport { MeshPhongNodeMaterial } from \"three/webgpu\"\nimport { lights } from \"three/tsl\"\nimport { color, fog, rangeFogFactor, checker, uv, mix, texture, normalMap } from \"three/tsl\"\nimport { TeapotGeometry } from \"three/addons/geometries/TeapotGeometry.js\"\n\n// @ts-ignore\nimport normalTexPath from \"./assets/Water_2_M_Normal.jpg\" with { type: \"image/jpeg\" }\n// @ts-ignore\nimport alphaTexPath from \"./assets/roughness_map.jpg\" with { type: \"image/jpeg\" }\nimport { ThreeCliRenderer } from \"../3d.js\"\n\ninterface PhongDemoState {\n  camera: PerspectiveCamera\n  sceneRoot: ThreeScene\n  engine: ThreeCliRenderer\n  light1: ThreePointLight\n  light2: ThreePointLight\n  light3: ThreePointLight\n  light4: ThreePointLight\n  normalMapTexture: DataTexture | null\n  alphaTexture: DataTexture | null\n  parentContainer: BoxRenderable\n  titleText: TextRenderable\n  statusText: TextRenderable\n  controlsText: TextRenderable\n  cleanup: () => void\n}\n\nlet demoState: PhongDemoState | null = null\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  if (demoState) {\n    destroy(renderer)\n  }\n\n  const WIDTH = renderer.terminalWidth\n  const HEIGHT = renderer.terminalHeight\n\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"phong-container\",\n    zIndex: 15,\n  })\n  renderer.root.add(parentContainer)\n\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"phong-main\",\n    width: WIDTH,\n    height: HEIGHT,\n    zIndex: 10,\n  })\n  renderer.root.add(framebufferRenderable)\n  const { frameBuffer: framebuffer } = framebufferRenderable\n\n  const engine = new ThreeCliRenderer(renderer, {\n    width: WIDTH,\n    height: HEIGHT,\n    focalLength: 8,\n    backgroundColor: RGBA.fromValues(0.0, 0.0, 0.0, 1.0),\n  })\n  await engine.init()\n\n  const camera = new PerspectiveCamera(50, engine.aspectRatio, 0.01, 100)\n  camera.position.z = 7\n\n  const sceneRoot = new ThreeScene()\n  sceneRoot.fogNode = fog(color(0xff00ff), rangeFogFactor(12, 30))\n\n  const sphereGeometry = new SphereGeometry(0.1, 16, 8)\n\n  const normalMapTexture = await TextureUtils.fromFile(normalTexPath)\n  const alphaTexture = await TextureUtils.fromFile(alphaTexPath)\n\n  if (normalMapTexture) {\n    normalMapTexture.wrapS = RepeatWrapping\n    normalMapTexture.wrapT = RepeatWrapping\n  }\n  if (alphaTexture) {\n    alphaTexture.wrapS = RepeatWrapping\n    alphaTexture.wrapT = RepeatWrapping\n  }\n\n  const addLight = (hexColor: number, power = 1700, distance = 100) => {\n    const material = new MeshPhongNodeMaterial()\n    material.colorNode = color(hexColor)\n    material.lights = false\n\n    const mesh = new ThreeMesh(sphereGeometry, material)\n\n    const light = new ThreePointLight(hexColor, 1, distance)\n    light.power = power\n    light.decay = 2\n    light.add(mesh)\n\n    sceneRoot.add(light)\n    return light\n  }\n\n  const light1 = addLight(0x0040ff)\n  const light2 = addLight(0xffffff)\n  const light3 = addLight(0x80ff80)\n  const light4 = addLight(0xffaa00)\n\n  const blueLightsNode = lights([light1])\n  const whiteLightsNode = lights([light2])\n  const allLightsNode = lights([light1, light2, light3, light4])\n\n  const geometryTeapot = new TeapotGeometry(0.8, 18)\n\n  const leftMaterial = new MeshPhongNodeMaterial({ color: 0x555555 })\n  leftMaterial.lightsNode = blueLightsNode\n  if (alphaTexture) {\n    leftMaterial.specularNode = texture(alphaTexture)\n  }\n  const leftObject = new ThreeMesh(geometryTeapot, leftMaterial)\n  leftObject.position.x = -3\n  sceneRoot.add(leftObject)\n\n  const centerMaterial = new MeshPhongNodeMaterial({ color: 0x555555 })\n  if (normalMapTexture) {\n    centerMaterial.normalNode = normalMap(texture(normalMapTexture))\n  }\n  centerMaterial.shininess = 80\n  centerMaterial.lightsNode = allLightsNode\n  const centerObject = new ThreeMesh(geometryTeapot, centerMaterial)\n  sceneRoot.add(centerObject)\n\n  const rightMaterial = new MeshPhongNodeMaterial({ color: 0x555555 })\n  rightMaterial.lightsNode = whiteLightsNode\n  rightMaterial.specularNode = mix(color(0x0000ff), color(0xff0000), checker(uv().mul(5)))\n  rightMaterial.shininess = 90\n  const rightObject = new ThreeMesh(geometryTeapot, rightMaterial)\n  rightObject.position.x = 3\n  sceneRoot.add(rightObject)\n\n  leftObject.rotation.y = centerObject.rotation.y = rightObject.rotation.y = Math.PI * -0.5\n  leftObject.position.y = centerObject.position.y = rightObject.position.y = -1\n\n  engine.setActiveCamera(camera)\n  sceneRoot.add(camera)\n\n  const titleText = new TextRenderable(renderer, {\n    id: \"phong-title\",\n    content: \"WebGPU Phong Lights Demo\",\n    position: \"absolute\",\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(titleText)\n\n  const statusText = new TextRenderable(renderer, {\n    id: \"phong-status\",\n    content: \"Ready.\",\n    position: \"absolute\",\n    top: 1,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(statusText)\n\n  const controlsText = new TextRenderable(renderer, {\n    id: \"phong-controls\",\n    content: \"WASD: Move | QE: Rotate | ZX: Zoom | R: Reset | U: Super Sample\",\n    position: \"absolute\",\n    top: HEIGHT - 2,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(controlsText)\n\n  const resizeHandler = (width: number, height: number) => {\n    framebuffer.resize(width, height)\n    engine.setSize(width, height)\n    camera.aspect = engine.aspectRatio\n    camera.updateProjectionMatrix()\n    controlsText.y = height - 2\n  }\n\n  const inputHandler = (key: KeyEvent) => {\n    if (key.name === \"w\") camera.translateY(0.5)\n    if (key.name === \"s\") camera.translateY(-0.5)\n    if (key.name === \"a\") camera.translateX(-0.5)\n    if (key.name === \"d\") camera.translateX(0.5)\n    if (key.name === \"q\") camera.rotateY(0.1)\n    if (key.name === \"e\") camera.rotateY(-0.1)\n    if (key.name === \"z\") camera.translateZ(1)\n    if (key.name === \"x\") camera.translateZ(-1)\n    if (key.name === \"r\") {\n      camera.position.set(0, 0, 7)\n      camera.rotation.set(0, 0, 0)\n      camera.quaternion.set(0, 0, 0, 1)\n      camera.up.set(0, 1, 0)\n      camera.lookAt(0, 0, 0)\n    }\n    if (key.name === \"u\") {\n      engine.toggleSuperSampling()\n    }\n  }\n\n  const animate = async (deltaTime: number) => {\n    const time = performance.now() / 1000\n    const lightTime = time * 0.5\n\n    light1.position.x = Math.sin(lightTime * 0.7) * 3\n    light1.position.y = Math.cos(lightTime * 0.5) * 4\n    light1.position.z = Math.cos(lightTime * 0.3) * 3\n\n    light2.position.x = Math.cos(lightTime * 0.3) * 3\n    light2.position.y = Math.sin(lightTime * 0.5) * 4\n    light2.position.z = Math.sin(lightTime * 0.7) * 3\n\n    light3.position.x = Math.sin(lightTime * 0.7) * 3\n    light3.position.y = Math.cos(lightTime * 0.3) * 4\n    light3.position.z = Math.sin(lightTime * 0.5) * 3\n\n    light4.position.x = Math.sin(lightTime * 0.3) * 3\n    light4.position.y = Math.cos(lightTime * 0.7) * 4\n    light4.position.z = Math.sin(lightTime * 0.5) * 3\n\n    engine.drawScene(sceneRoot, framebuffer, deltaTime)\n  }\n\n  renderer.on(\"resize\", resizeHandler)\n  renderer.keyInput.on(\"keypress\", inputHandler)\n  renderer.setFrameCallback(animate)\n\n  const cleanup = () => {\n    renderer.off(\"resize\", resizeHandler)\n    renderer.keyInput.off(\"keypress\", inputHandler)\n    renderer.removeFrameCallback(animate)\n    engine.destroy()\n  }\n\n  demoState = {\n    camera,\n    sceneRoot,\n    engine,\n    light1,\n    light2,\n    light3,\n    light4,\n    normalMapTexture,\n    alphaTexture,\n    parentContainer,\n    titleText,\n    statusText,\n    controlsText,\n    cleanup,\n  }\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (demoState) {\n    demoState.cleanup()\n    renderer.root.remove(\"phong-main\")\n    renderer.root.remove(\"phong-container\")\n    demoState = null\n  }\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/link-demo.ts",
    "content": "import {\n  CliRenderer,\n  createCliRenderer,\n  t,\n  fg,\n  underline,\n  link,\n  bold,\n  italic,\n  BoxRenderable,\n  RGBA,\n  TextRenderable,\n  type MouseEvent,\n  type RenderContext,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet nextZIndex = 100\nlet draggableBoxes: DraggableBox[] = []\nlet dragModeEnabled = false\n\nclass DraggableBox extends BoxRenderable {\n  private isDragging = false\n  private dragOffsetX = 0\n  private dragOffsetY = 0\n\n  constructor(\n    ctx: RenderContext,\n    id: string,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    backgroundColor: RGBA,\n  ) {\n    super(ctx, {\n      id,\n      width,\n      height,\n      zIndex: nextZIndex++,\n      backgroundColor,\n      position: \"absolute\",\n      left: x,\n      top: y,\n      borderStyle: \"rounded\",\n      borderColor: RGBA.fromHex(\"#ffffff\"),\n      padding: 1,\n      flexDirection: \"column\",\n    })\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    if (!dragModeEnabled) return\n\n    switch (event.type) {\n      case \"down\":\n        this.isDragging = true\n        this.dragOffsetX = event.x - this.x\n        this.dragOffsetY = event.y - this.y\n        this.zIndex = nextZIndex++\n        event.stopPropagation()\n        break\n\n      case \"drag-end\":\n        if (this.isDragging) {\n          this.isDragging = false\n          event.stopPropagation()\n        }\n        break\n\n      case \"drag\":\n        if (this.isDragging) {\n          const newX = event.x - this.dragOffsetX\n          const newY = event.y - this.dragOffsetY\n\n          this.x = Math.max(0, Math.min(newX, this._ctx.width - this.width))\n          this.y = Math.max(0, Math.min(newY, this._ctx.height - this.height))\n\n          event.stopPropagation()\n        }\n        break\n    }\n  }\n}\n\nfunction getHeaderContent(): ReturnType<typeof t> {\n  const dragStatus = dragModeEnabled ? fg(\"#34d399\")(\"ON\") : fg(\"#f87171\")(\"OFF\")\n  return t`${bold(fg(\"#38bdf8\")(\"OpenTUI Interactive Link Demo\"))}\n${fg(\"#94a3b8\")(\"Click the links to open them.\")} ${fg(\"#64748b\")(\"Press\")} ${bold(fg(\"#fbbf24\")(\"d\"))} ${fg(\"#64748b\")(\"to toggle drag mode:\")} ${dragStatus}\n${italic(fg(\"#64748b\")(\"(Terminal must support OSC 8 hyperlinks)\"))}`\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  renderer.setBackgroundColor(\"#0f172a\") // Deep slate blue background\n\n  const container = new BoxRenderable(renderer, {\n    id: \"main-container\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n  renderer.root.add(container)\n\n  // Header\n  const header = new TextRenderable(renderer, {\n    id: \"header\",\n    content: getHeaderContent(),\n    position: \"absolute\",\n    left: 2,\n    top: 1,\n    zIndex: 10,\n    width: 80,\n    height: 4,\n  })\n  container.add(header)\n\n  // Toggle drag mode with 'd' key\n  renderer.keyInput.on(\"keypress\", (event) => {\n    if (event.name === \"d\") {\n      dragModeEnabled = !dragModeEnabled\n      header.content = getHeaderContent()\n    }\n  })\n\n  // Card 1: Project Info\n  createCard(\n    renderer,\n    container,\n    \"project-card\",\n    5,\n    6,\n    40,\n    8,\n    RGBA.fromHex(\"#1e293be6\"), // Dark slate\n    t`${bold(fg(\"#f472b6\")(\"♥ Project Info\"))}\n\n${fg(\"#e2e8f0\")(\"Source:\")} ${link(\"https://github.com/anomalyco/opentui\")(underline(fg(\"#38bdf8\")(\"GitHub Repository\")))}\n${fg(\"#e2e8f0\")(\"Web:\")}    ${link(\"https://opentui.com\")(underline(fg(\"#34d399\")(\"Official Website\")))}\n${fg(\"#e2e8f0\")(\"License:\")} ${link(\"https://github.com/anomalyco/opentui/blob/main/LICENSE\")(underline(fg(\"#fbbf24\")(\"MIT\")))}`,\n  )\n\n  // Card 2: Documentation\n  createCard(\n    renderer,\n    container,\n    \"docs-card\",\n    50,\n    8,\n    35,\n    9,\n    RGBA.fromHex(\"#334155e6\"),\n    t`${bold(fg(\"#a78bfa\")(\"📚 Documentation\"))}\n\n${fg(\"#cbd5e1\")(\"Get started with:\")}\n• ${link(\"https://github.com/anomalyco/opentui#readme\")(bold(fg(\"#fff\")(\"Quick Start\")))}\n• ${link(\"https://github.com/anomalyco/opentui/tree/main/packages/core/src/examples\")(fg(\"#fff\")(\"Examples\"))}\n• ${link(\"https://github.com/anomalyco/opentui/issues\")(fg(\"#fff\")(\"Known Issues\"))}`,\n  )\n\n  // Card 3: Socials\n  createCard(\n    renderer,\n    container,\n    \"social-card\",\n    20,\n    16,\n    30,\n    7,\n    RGBA.fromHex(\"#0f766ecc\"), // Teal\n    t`${bold(fg(\"#2dd4bf\")(\"👋 Connect\"))}\n\n${link(\"https://x.com/anomalyco\")(fg(\"#60a5fa\")(\"Twitter / X\"))}\n${link(\"https://discord.gg/Fc8UPAeV\")(fg(\"#818cf8\")(\"Discord Community\"))}`,\n  )\n}\n\nfunction createCard(\n  renderer: CliRenderer,\n  container: BoxRenderable,\n  id: string,\n  x: number,\n  y: number,\n  width: number,\n  height: number,\n  bg: RGBA,\n  content: any,\n) {\n  const card = new DraggableBox(renderer, id, x, y, width, height, bg)\n\n  const text = new TextRenderable(renderer, {\n    id: `${id}-text`,\n    content: content,\n    width: width - 2, // Account for padding\n    height: height - 2,\n  })\n\n  card.add(text)\n  container.add(card)\n  draggableBoxes.push(card)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  for (const box of draggableBoxes) {\n    renderer.root.remove(box.id)\n  }\n  draggableBoxes = []\n  dragModeEnabled = false\n  renderer.root.remove(\"main-container\")\n  renderer.setCursorPosition(0, 0, false)\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/live-state-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  TextAttributes,\n  TextRenderable,\n  BoxRenderable,\n  type MouseEvent,\n  t,\n  bold,\n  red,\n  green,\n  blue,\n  fg,\n  parseColor,\n  Box,\n} from \"../index.js\"\nimport type { BoxOptions } from \"../renderables/Box.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet mainGroup: BoxRenderable | null = null\nlet titleText: TextRenderable | null = null\nlet instructionsText: TextRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet rendererStateText: TextRenderable | null = null\nlet renderableStateText: TextRenderable | null = null\nlet liveButtons: ReturnType<typeof LiveButton>[] = []\nlet demoRenderable: BoxRenderable | null = null\nlet currentRenderer: CliRenderer | null = null\nlet frameCounter = 0\nlet animationCounter = 0\nlet frameCallback: ((deltaTime: number) => Promise<void>) | null = null\n\nfunction LiveButton(options: BoxOptions & { label: string }) {\n  const base = parseColor(options.backgroundColor ?? \"transparent\")\n  const hoverBg = RGBA.fromValues(\n    Math.min(1.0, base.r * 1.4),\n    Math.min(1.0, base.g * 1.4),\n    Math.min(1.0, base.b * 1.4),\n    base.a,\n  )\n  const pressBg = RGBA.fromValues(base.r * 0.6, base.g * 0.6, base.b * 0.6, base.a)\n\n  return Box({\n    ...options,\n    renderAfter(buffer, deltaTime) {\n      const textColor = RGBA.fromValues(1, 1, 1, 1)\n      const centerY = this.y + Math.floor(this.height / 2)\n      const startX = this.x + Math.floor((this.width - options.label.length) / 2)\n\n      buffer.drawText(options.label, startX, centerY, textColor)\n    },\n    onMouse(event: MouseEvent) {\n      switch (event.type) {\n        case \"down\":\n          this.backgroundColor = pressBg\n          event.stopPropagation()\n          break\n\n        case \"up\":\n          this.backgroundColor = base\n          event.stopPropagation()\n          break\n\n        case \"over\":\n          this.backgroundColor = hoverBg\n          break\n\n        case \"out\":\n          this.backgroundColor = base\n          break\n      }\n    },\n  })\n}\n\nfunction updateStatusText(message: string): void {\n  if (statusText) {\n    const timestamp = new Date().toLocaleTimeString()\n    statusText.content = `[${timestamp}] ${message}`\n  }\n}\n\nfunction updateRendererState(renderer: CliRenderer): void {\n  if (rendererStateText) {\n    const running = renderer.isRunning\n    const liveCount = renderer.liveRequestCount\n    const controlState = renderer.currentControlState\n\n    const liveIndicators = [\"▘\", \"▝\", \"▗\", \"▖\"]\n    const liveIndicator = liveCount > 0 ? liveIndicators[animationCounter % liveIndicators.length] : \" \"\n    const styledContent = t`${bold(\"Renderer State:\")} ${running ? green(bold(\"RUNNING\")) : red(bold(\"STOPPED\"))} | ${bold(\"Live Requests:\")} ${liveCount > 0 ? green(bold(liveCount.toString())) : fg(\"#666\")(liveCount.toString())} ${liveCount > 0 ? fg(\"#00FFFF\")(liveIndicator) : \"\"} | ${bold(\"Control State:\")} ${controlState === \"live\" ? green(bold(controlState.toUpperCase())) : blue(bold(controlState.toUpperCase()))} | ${bold(\"Frame:\")} ${fg(\"#888\")(frameCounter.toString())}`\n\n    rendererStateText.content = styledContent\n  }\n}\n\nfunction updateRenderableState(): void {\n  if (renderableStateText) {\n    const exists = demoRenderable !== null\n    const live = demoRenderable?.live || false\n    const visible = demoRenderable?.visible ?? false\n\n    const styledContent = t`${bold(\"Demo Renderable:\")} ${exists ? green(bold(\"ADDED\")) : fg(\"#666\")(bold(\"NOT ADDED\"))} | ${bold(\"Live:\")} ${live ? green(bold(\"TRUE\")) : red(bold(\"FALSE\"))} | ${bold(\"Visible:\")} ${visible ? blue(bold(\"TRUE\")) : fg(\"#666\")(bold(\"FALSE\"))}`\n\n    renderableStateText.content = styledContent\n  }\n}\n\nfunction addDemoRenderable(renderer: CliRenderer): void {\n  if (demoRenderable) {\n    updateStatusText(\"Demo renderable already exists!\")\n    return\n  }\n\n  demoRenderable = new BoxRenderable(renderer, {\n    id: \"demo-renderable\",\n    position: \"absolute\",\n    left: 60,\n    top: 15,\n    width: 30,\n    height: 8,\n    backgroundColor: RGBA.fromInts(100, 200, 150, 255),\n    borderColor: RGBA.fromInts(150, 255, 200, 255),\n    borderStyle: \"double\",\n    title: \"Demo Renderable\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n\n  renderer.root.getRenderable(\"live-demo-main-group\")?.add(demoRenderable)\n  updateStatusText(\"Added demo renderable\")\n}\n\nfunction removeDemoRenderable(renderer: CliRenderer): void {\n  if (!demoRenderable) {\n    updateStatusText(\"No demo renderable to remove!\")\n    return\n  }\n\n  renderer.root.getRenderable(\"live-demo-main-group\")?.remove(demoRenderable.id)\n  demoRenderable = null\n  updateStatusText(\"Removed demo renderable\")\n}\n\nexport function run(renderer: CliRenderer): void {\n  currentRenderer = renderer\n\n  mainGroup = new BoxRenderable(renderer, {\n    id: \"live-demo-main-group\",\n    zIndex: 10,\n  })\n  renderer.root.add(mainGroup)\n\n  const backgroundColor = RGBA.fromInts(25, 30, 45, 255)\n  renderer.setBackgroundColor(backgroundColor)\n\n  titleText = new TextRenderable(renderer, {\n    id: \"live_demo_title\",\n    content: \"Live State Management Demo\",\n    position: \"absolute\",\n    left: 2,\n    top: 1,\n    fg: RGBA.fromInts(255, 215, 135),\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  mainGroup.add(titleText)\n\n  instructionsText = new TextRenderable(renderer, {\n    id: \"live_demo_instructions\",\n    content: \"Test the live state management system • Escape: return to menu\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    fg: RGBA.fromInts(176, 196, 222),\n    zIndex: 1000,\n  })\n  mainGroup.add(instructionsText)\n\n  statusText = new TextRenderable(renderer, {\n    id: \"live_demo_status\",\n    content: \"Ready - Click buttons to test live state management\",\n    position: \"absolute\",\n    left: 2,\n    top: 4,\n    fg: RGBA.fromInts(144, 238, 144),\n    attributes: TextAttributes.ITALIC,\n    zIndex: 1000,\n  })\n  mainGroup.add(statusText)\n\n  rendererStateText = new TextRenderable(renderer, {\n    id: \"renderer_state\",\n    content: \"\",\n    position: \"absolute\",\n    left: 2,\n    top: 6,\n    fg: RGBA.fromInts(255, 255, 100),\n    zIndex: 1000,\n  })\n  mainGroup.add(rendererStateText)\n\n  renderableStateText = new TextRenderable(renderer, {\n    id: \"renderable_state\",\n    content: \"\",\n    position: \"absolute\",\n    left: 2,\n    top: 7,\n    fg: RGBA.fromInts(255, 255, 100),\n    zIndex: 1000,\n  })\n  mainGroup.add(renderableStateText)\n\n  // Button colors\n  const rendererColor = RGBA.fromInts(100, 140, 180, 255) // Blue - darker for better contrast\n  const renderableColor = RGBA.fromInts(180, 100, 140, 255) // Pink - darker for better contrast\n  const liveColor = RGBA.fromInts(140, 180, 100, 255) // Green - darker for better contrast\n  const visibilityColor = RGBA.fromInts(180, 140, 100, 255) // Orange - for visibility controls\n\n  const startY = 10\n  const buttonWidth = 20\n  const buttonHeight = 3\n  const spacing = 22\n\n  // Renderer control buttons\n  liveButtons = [\n    LiveButton({\n      id: \"request-live-btn\",\n      position: \"absolute\",\n      left: 2,\n      top: startY,\n      width: buttonWidth,\n      height: buttonHeight,\n      backgroundColor: rendererColor,\n      label: \"REQUEST LIVE\",\n      onMouseDown: () => {\n        if (!currentRenderer) return\n        currentRenderer.requestLive()\n        updateStatusText(\"Manually requested live\")\n        updateRendererState(currentRenderer)\n        updateRenderableState()\n      },\n    }),\n    LiveButton({\n      id: \"drop-live-btn\",\n      position: \"absolute\",\n      left: 2 + spacing,\n      top: startY,\n      width: buttonWidth,\n      height: buttonHeight,\n      backgroundColor: rendererColor,\n      label: \"DROP LIVE\",\n      onMouseDown: () => {\n        if (!currentRenderer) return\n        currentRenderer.dropLive()\n        updateStatusText(\"Manually dropped live\")\n        updateRendererState(currentRenderer)\n        updateRenderableState()\n      },\n    }),\n\n    // Renderable management buttons\n    LiveButton({\n      id: \"add-renderable-btn\",\n      position: \"absolute\",\n      left: 2,\n      top: startY + 5,\n      width: buttonWidth,\n      height: buttonHeight,\n      backgroundColor: renderableColor,\n      label: \"ADD RENDERABLE\",\n      onMouseDown: () => {\n        if (!currentRenderer) return\n        addDemoRenderable(currentRenderer)\n        updateRendererState(currentRenderer)\n        updateRenderableState()\n      },\n    }),\n    LiveButton({\n      id: \"remove-renderable-btn\",\n      position: \"absolute\",\n      left: 2 + spacing,\n      top: startY + 5,\n      width: buttonWidth,\n      height: buttonHeight,\n      backgroundColor: renderableColor,\n      label: \"REMOVE RENDERABLE\",\n      onMouseDown: () => {\n        if (!currentRenderer) return\n        removeDemoRenderable(currentRenderer)\n        updateRendererState(currentRenderer)\n        updateRenderableState()\n      },\n    }),\n\n    // Live state buttons\n    LiveButton({\n      id: \"set-live-true-btn\",\n      position: \"absolute\",\n      left: 2,\n      top: startY + 10,\n      width: buttonWidth,\n      height: buttonHeight,\n      backgroundColor: liveColor,\n      label: \"LIVE = TRUE\",\n      onMouseDown: () => {\n        if (demoRenderable) {\n          demoRenderable.live = true\n          updateStatusText(\"Set demo renderable live = true\")\n        } else {\n          updateStatusText(\"No demo renderable to set live!\")\n        }\n        if (currentRenderer) {\n          updateRendererState(currentRenderer)\n        }\n        updateRenderableState()\n      },\n    }),\n    LiveButton({\n      id: \"set-live-false-btn\",\n      position: \"absolute\",\n      left: 2 + spacing,\n      top: startY + 10,\n      width: buttonWidth,\n      height: buttonHeight,\n      backgroundColor: liveColor,\n      label: \"LIVE = FALSE\",\n      onMouseDown: () => {\n        if (demoRenderable) {\n          demoRenderable.live = false\n          updateStatusText(\"Set demo renderable live = false\")\n        } else {\n          updateStatusText(\"No demo renderable to set live!\")\n        }\n        if (currentRenderer) {\n          updateRendererState(currentRenderer)\n        }\n        updateRenderableState()\n      },\n    }),\n\n    // Visibility state buttons\n    LiveButton({\n      id: \"set-visible-true-btn\",\n      position: \"absolute\",\n      left: 2,\n      top: startY + 15,\n      width: buttonWidth,\n      height: buttonHeight,\n      backgroundColor: visibilityColor,\n      label: \"VISIBLE = TRUE\",\n      onMouseDown: () => {\n        if (demoRenderable) {\n          demoRenderable.visible = true\n          updateStatusText(\"Set demo renderable visible = true\")\n        } else {\n          updateStatusText(\"No demo renderable to set visible!\")\n        }\n        if (currentRenderer) {\n          updateRendererState(currentRenderer)\n        }\n        updateRenderableState()\n      },\n    }),\n    LiveButton({\n      id: \"set-visible-false-btn\",\n      position: \"absolute\",\n      left: 2 + spacing,\n      top: startY + 15,\n      width: buttonWidth,\n      height: buttonHeight,\n      backgroundColor: visibilityColor,\n      label: \"VISIBLE = FALSE\",\n      onMouseDown: () => {\n        if (demoRenderable) {\n          demoRenderable.visible = false\n          updateStatusText(\"Set demo renderable visible = false\")\n        } else {\n          updateStatusText(\"No demo renderable to set visible!\")\n        }\n        if (currentRenderer) {\n          updateRendererState(currentRenderer)\n        }\n        updateRenderableState()\n      },\n    }),\n  ]\n\n  for (const button of liveButtons) {\n    mainGroup.add(button)\n  }\n\n  // Add section labels\n  const rendererLabel = new TextRenderable(renderer, {\n    id: \"renderer_label\",\n    content: \"Renderer Control:\",\n    position: \"absolute\",\n    left: 2,\n    top: startY - 1,\n    fg: rendererColor,\n    attributes: TextAttributes.BOLD,\n    zIndex: 500,\n  })\n  mainGroup.add(rendererLabel)\n\n  const renderableLabel = new TextRenderable(renderer, {\n    id: \"renderable_label\",\n    content: \"Renderable Management:\",\n    position: \"absolute\",\n    left: 2,\n    top: startY + 4,\n    fg: renderableColor,\n    attributes: TextAttributes.BOLD,\n    zIndex: 500,\n  })\n  mainGroup.add(renderableLabel)\n\n  const liveLabel = new TextRenderable(renderer, {\n    id: \"live_label\",\n    content: \"Live State Control:\",\n    position: \"absolute\",\n    left: 2,\n    top: startY + 9,\n    fg: liveColor,\n    attributes: TextAttributes.BOLD,\n    zIndex: 500,\n  })\n  mainGroup.add(liveLabel)\n\n  const visibilityLabel = new TextRenderable(renderer, {\n    id: \"visibility_label\",\n    content: \"Visibility Control:\",\n    position: \"absolute\",\n    left: 2,\n    top: startY + 14,\n    fg: visibilityColor,\n    attributes: TextAttributes.BOLD,\n    zIndex: 500,\n  })\n  mainGroup.add(visibilityLabel)\n\n  frameCallback = async (deltaTime) => {\n    frameCounter++\n    if (frameCounter % 10 === 0) {\n      animationCounter++\n      updateRendererState(renderer)\n      updateRenderableState()\n    }\n  }\n  renderer.setFrameCallback(frameCallback)\n\n  updateRendererState(renderer)\n  updateRenderableState()\n\n  console.log(\"Live State Demo initialized! Test the automatic live state management system.\")\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (frameCallback) {\n    renderer.removeFrameCallback(frameCallback)\n    frameCallback = null\n  }\n\n  currentRenderer = null\n  frameCounter = 0\n  animationCounter = 0\n\n  renderer.root.getRenderable(\"live-demo-main-group\")?.destroyRecursively()\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/markdown-demo.ts",
    "content": "import {\n  CliRenderer,\n  CliRenderEvents,\n  createCliRenderer,\n  BoxRenderable,\n  TextRenderable,\n  type ParsedKey,\n  ScrollBoxRenderable,\n} from \"../index.js\"\nimport { parseColor } from \"../lib/RGBA.js\"\nimport { getTreeSitterClient } from \"../lib/tree-sitter/index.js\"\nimport { MarkdownRenderable } from \"../renderables/Markdown.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\n// Rich markdown example showcasing various features\nconst markdownContent = `# OpenTUI Markdown Demo\n\nWelcome to the **MarkdownRenderable** showcase! This demonstrates automatic table alignment and syntax highlighting.\n\n## Features\n\n- Automatic **table column alignment** based on content width\n- Proper handling of \\`inline code\\`, **bold**, and *italic* in tables\n- Multiple syntax themes to choose from\n- Conceal mode hides formatting markers\n\n## Comparison Table\n\n| Feature | Status | Priority | Notes |\n|---|---|---|---|\n| Table alignment | **Done** | High | Uses \\`marked\\` parser |\n| Conceal mode | *Working* | Medium | Hides \\`**\\`, \\`\\`\\`, etc. |\n| Theme switching | **Done** | Low | Multiple themes available |\n| Unicode support | 日本語 | High | CJK characters |\n\n## Code Examples\n\nHere's how to use it:\n\n\\`\\`\\`typescript\nimport { MarkdownRenderable } from \"@opentui/core\"\n\nconst md = new MarkdownRenderable(renderer, {\n  content: \"# Hello World\",\n  syntaxStyle: mySyntaxStyle,\n  fg: \"#24292F\",\n  bg: \"#FFFFFF\",\n  conceal: true, // Hide formatting markers\n})\n\\`\\`\\`\n\nAnd a JSON configuration example:\n\n\\`\\`\\`json\n{\n  \"name\": \"opentui-markdown-demo\",\n  \"theme\": \"github\",\n  \"features\": [\"table-alignment\", \"syntax-highlighting\", \"conceal-mode\"],\n  \"streaming\": {\n    \"enabled\": true,\n    \"speed\": \"slowest\"\n  }\n}\n\\`\\`\\`\n\nHere's a TSX component example:\n\n\\`\\`\\`tsx\nimport React from \"react\"\nimport { useState } from \"react\"\n\ninterface Props {\n  title: string\n  count: number\n}\n\nexport const Counter: React.FC<Props> = ({ title, count: initialCount }) => {\n  const [count, setCount] = useState(initialCount)\n\n  return (\n    <div className=\"counter\">\n      <h1>{title}</h1>\n      <p>Count: {count}</p>\n      <button onClick={() => setCount(c => c + 1)}>\n        Increment\n      </button>\n    </div>\n  )\n}\n\\`\\`\\`\n\n## Light Theme Fallback Checks\n\nPress \\`T\\` until **GitHub Light**. These fences intentionally skip syntax\nhighlighting and should still inherit the theme text color.\n\nUnlabeled fenced block:\n\n\\`\\`\\`\nthis fence has no language tag\nit should stay readable in GitHub Light\n\\`\\`\\`\n\nUnsupported parser fallback:\n\n\\`\\`\\`toml\ntitle = \"GitHub Light\"\nstatus = \"fallback text should stay readable\"\n\\`\\`\\`\n\n### API Reference\n\n| Method | Parameters | Returns | Description |\n|---|---|---|---|\n| \\`constructor\\` | \\`ctx, options\\` | \\`MarkdownRenderable\\` | Create new instance |\n| \\`clearCache\\` | none | \\`void\\` | Force re-render content |\n\n## Inline Formatting Examples\n\n| Style | Syntax | Rendered |\n|---|---|---|\n| Bold | \\`**text**\\` | **bold text** |\n| Italic | \\`*text*\\` | *italic text* |\n| Code | \\`code\\` | \\`inline code\\` |\n| Link | \\`[text](url)\\` | [OpenTUI](https://github.com) |\n\n## Mixed Content\n\n> **Note**: This blockquote contains **bold** and \\`code\\` formatting.\n> It should render correctly with proper styling.\n\n### Emoji Support\n\n| Emoji | Name | Category |\n|---|---|---|\n| 🚀 | Rocket | Transport |\n| 🎨 | Palette | Art |\n| ⚡ | Lightning | Nature |\n| 🔥 | Fire | Nature |\n\n---\n\n## Alignment Examples\n\n| Left | Center | Right |\n|:---|:---:|---:|\n| L1 | C1 | R1 |\n| Left aligned | Centered text | Right aligned |\n| Short | Medium length | Longer content here |\n\n## Performance\n\nThe table alignment uses:\n1. AST-based parsing with \\`marked\\`\n2. Caching for repeated content\n3. Smart width calculation accounting for concealed chars\n\n---\n\n*Press \\`?\\` for keybindings*\n`\n\n// Theme definitions\nconst themes = {\n  githubLight: {\n    name: \"GitHub Light\",\n    bg: \"#FFFFFF\",\n    styles: {\n      keyword: { fg: parseColor(\"#CF222E\"), bold: true },\n      string: { fg: parseColor(\"#0A3069\") },\n      comment: { fg: parseColor(\"#6E7781\"), italic: true },\n      number: { fg: parseColor(\"#0550AE\") },\n      function: { fg: parseColor(\"#8250DF\") },\n      type: { fg: parseColor(\"#953800\") },\n      operator: { fg: parseColor(\"#CF222E\") },\n      variable: { fg: parseColor(\"#24292F\") },\n      property: { fg: parseColor(\"#0550AE\") },\n      \"punctuation.bracket\": { fg: parseColor(\"#24292F\") },\n      \"punctuation.delimiter\": { fg: parseColor(\"#57606A\") },\n      \"markup.heading\": { fg: parseColor(\"#0550AE\"), bold: true },\n      \"markup.heading.1\": { fg: parseColor(\"#1A7F37\"), bold: true, underline: true },\n      \"markup.heading.2\": { fg: parseColor(\"#0550AE\"), bold: true },\n      \"markup.heading.3\": { fg: parseColor(\"#8250DF\") },\n      \"markup.bold\": { fg: parseColor(\"#24292F\"), bold: true },\n      \"markup.strong\": { fg: parseColor(\"#24292F\"), bold: true },\n      \"markup.italic\": { fg: parseColor(\"#24292F\"), italic: true },\n      \"markup.list\": { fg: parseColor(\"#CF222E\") },\n      \"markup.quote\": { fg: parseColor(\"#6E7781\"), italic: true },\n      \"markup.raw\": { fg: parseColor(\"#24292F\"), bg: parseColor(\"#F6F8FA\") },\n      \"markup.raw.block\": { fg: parseColor(\"#24292F\"), bg: parseColor(\"#F6F8FA\") },\n      \"markup.raw.inline\": { fg: parseColor(\"#24292F\"), bg: parseColor(\"#F6F8FA\") },\n      \"markup.link\": { fg: parseColor(\"#0969DA\"), underline: true },\n      \"markup.link.label\": { fg: parseColor(\"#0A3069\"), underline: true },\n      \"markup.link.url\": { fg: parseColor(\"#0969DA\"), underline: true },\n      label: { fg: parseColor(\"#1A7F37\") },\n      conceal: { fg: parseColor(\"#6E7781\") },\n      \"punctuation.special\": { fg: parseColor(\"#57606A\") },\n      default: { fg: parseColor(\"#24292F\") },\n    },\n  },\n  github: {\n    name: \"GitHub Dark\",\n    bg: \"#0D1117\",\n    styles: {\n      keyword: { fg: parseColor(\"#FF7B72\"), bold: true },\n      string: { fg: parseColor(\"#A5D6FF\") },\n      comment: { fg: parseColor(\"#8B949E\"), italic: true },\n      number: { fg: parseColor(\"#79C0FF\") },\n      function: { fg: parseColor(\"#D2A8FF\") },\n      type: { fg: parseColor(\"#FFA657\") },\n      operator: { fg: parseColor(\"#FF7B72\") },\n      variable: { fg: parseColor(\"#E6EDF3\") },\n      property: { fg: parseColor(\"#79C0FF\") },\n      \"punctuation.bracket\": { fg: parseColor(\"#F0F6FC\") },\n      \"punctuation.delimiter\": { fg: parseColor(\"#C9D1D9\") },\n      \"markup.heading\": { fg: parseColor(\"#58A6FF\"), bold: true },\n      \"markup.heading.1\": { fg: parseColor(\"#00FF88\"), bold: true, underline: true },\n      \"markup.heading.2\": { fg: parseColor(\"#00D7FF\"), bold: true },\n      \"markup.heading.3\": { fg: parseColor(\"#FF69B4\") },\n      \"markup.bold\": { fg: parseColor(\"#F0F6FC\"), bold: true },\n      \"markup.strong\": { fg: parseColor(\"#F0F6FC\"), bold: true },\n      \"markup.italic\": { fg: parseColor(\"#F0F6FC\"), italic: true },\n      \"markup.list\": { fg: parseColor(\"#FF7B72\") },\n      \"markup.quote\": { fg: parseColor(\"#8B949E\"), italic: true },\n      \"markup.raw\": { fg: parseColor(\"#A5D6FF\"), bg: parseColor(\"#161B22\") },\n      \"markup.raw.block\": { fg: parseColor(\"#A5D6FF\"), bg: parseColor(\"#161B22\") },\n      \"markup.raw.inline\": { fg: parseColor(\"#A5D6FF\"), bg: parseColor(\"#161B22\") },\n      \"markup.link\": { fg: parseColor(\"#58A6FF\"), underline: true },\n      \"markup.link.label\": { fg: parseColor(\"#A5D6FF\"), underline: true },\n      \"markup.link.url\": { fg: parseColor(\"#58A6FF\"), underline: true },\n      label: { fg: parseColor(\"#7EE787\") },\n      conceal: { fg: parseColor(\"#6E7681\") },\n      \"punctuation.special\": { fg: parseColor(\"#8B949E\") },\n      default: { fg: parseColor(\"#E6EDF3\") },\n    },\n  },\n  monokai: {\n    name: \"Monokai\",\n    bg: \"#272822\",\n    styles: {\n      keyword: { fg: parseColor(\"#F92672\"), bold: true },\n      string: { fg: parseColor(\"#E6DB74\") },\n      comment: { fg: parseColor(\"#75715E\"), italic: true },\n      number: { fg: parseColor(\"#AE81FF\") },\n      function: { fg: parseColor(\"#A6E22E\") },\n      type: { fg: parseColor(\"#66D9EF\"), italic: true },\n      operator: { fg: parseColor(\"#F92672\") },\n      variable: { fg: parseColor(\"#F8F8F2\") },\n      property: { fg: parseColor(\"#A6E22E\") },\n      \"punctuation.bracket\": { fg: parseColor(\"#F8F8F2\") },\n      \"punctuation.delimiter\": { fg: parseColor(\"#F8F8F2\") },\n      \"markup.heading\": { fg: parseColor(\"#A6E22E\"), bold: true },\n      \"markup.heading.1\": { fg: parseColor(\"#F92672\"), bold: true, underline: true },\n      \"markup.heading.2\": { fg: parseColor(\"#66D9EF\"), bold: true },\n      \"markup.heading.3\": { fg: parseColor(\"#E6DB74\") },\n      \"markup.bold\": { fg: parseColor(\"#F8F8F2\"), bold: true },\n      \"markup.strong\": { fg: parseColor(\"#F8F8F2\"), bold: true },\n      \"markup.italic\": { fg: parseColor(\"#F8F8F2\"), italic: true },\n      \"markup.list\": { fg: parseColor(\"#F92672\") },\n      \"markup.quote\": { fg: parseColor(\"#75715E\"), italic: true },\n      \"markup.raw\": { fg: parseColor(\"#E6DB74\"), bg: parseColor(\"#3E3D32\") },\n      \"markup.raw.block\": { fg: parseColor(\"#E6DB74\"), bg: parseColor(\"#3E3D32\") },\n      \"markup.raw.inline\": { fg: parseColor(\"#E6DB74\"), bg: parseColor(\"#3E3D32\") },\n      \"markup.link\": { fg: parseColor(\"#66D9EF\"), underline: true },\n      \"markup.link.label\": { fg: parseColor(\"#E6DB74\"), underline: true },\n      \"markup.link.url\": { fg: parseColor(\"#66D9EF\"), underline: true },\n      label: { fg: parseColor(\"#A6E22E\") },\n      conceal: { fg: parseColor(\"#75715E\") },\n      \"punctuation.special\": { fg: parseColor(\"#75715E\") },\n      default: { fg: parseColor(\"#F8F8F2\") },\n    },\n  },\n  nord: {\n    name: \"Nord\",\n    bg: \"#2E3440\",\n    styles: {\n      keyword: { fg: parseColor(\"#81A1C1\"), bold: true },\n      string: { fg: parseColor(\"#A3BE8C\") },\n      comment: { fg: parseColor(\"#616E88\"), italic: true },\n      number: { fg: parseColor(\"#B48EAD\") },\n      function: { fg: parseColor(\"#88C0D0\") },\n      type: { fg: parseColor(\"#8FBCBB\") },\n      operator: { fg: parseColor(\"#81A1C1\") },\n      variable: { fg: parseColor(\"#D8DEE9\") },\n      property: { fg: parseColor(\"#88C0D0\") },\n      \"punctuation.bracket\": { fg: parseColor(\"#ECEFF4\") },\n      \"punctuation.delimiter\": { fg: parseColor(\"#D8DEE9\") },\n      \"markup.heading\": { fg: parseColor(\"#88C0D0\"), bold: true },\n      \"markup.heading.1\": { fg: parseColor(\"#8FBCBB\"), bold: true, underline: true },\n      \"markup.heading.2\": { fg: parseColor(\"#81A1C1\"), bold: true },\n      \"markup.heading.3\": { fg: parseColor(\"#B48EAD\") },\n      \"markup.bold\": { fg: parseColor(\"#ECEFF4\"), bold: true },\n      \"markup.strong\": { fg: parseColor(\"#ECEFF4\"), bold: true },\n      \"markup.italic\": { fg: parseColor(\"#ECEFF4\"), italic: true },\n      \"markup.list\": { fg: parseColor(\"#81A1C1\") },\n      \"markup.quote\": { fg: parseColor(\"#616E88\"), italic: true },\n      \"markup.raw\": { fg: parseColor(\"#A3BE8C\"), bg: parseColor(\"#3B4252\") },\n      \"markup.raw.block\": { fg: parseColor(\"#A3BE8C\"), bg: parseColor(\"#3B4252\") },\n      \"markup.raw.inline\": { fg: parseColor(\"#A3BE8C\"), bg: parseColor(\"#3B4252\") },\n      \"markup.link\": { fg: parseColor(\"#88C0D0\"), underline: true },\n      \"markup.link.label\": { fg: parseColor(\"#A3BE8C\"), underline: true },\n      \"markup.link.url\": { fg: parseColor(\"#88C0D0\"), underline: true },\n      label: { fg: parseColor(\"#A3BE8C\") },\n      conceal: { fg: parseColor(\"#4C566A\") },\n      \"punctuation.special\": { fg: parseColor(\"#616E88\") },\n      default: { fg: parseColor(\"#D8DEE9\") },\n    },\n  },\n}\n\ntype ThemeKey = keyof typeof themes\nconst themeKeys = [\"github\", \"githubLight\", \"monokai\", \"nord\"] as const satisfies readonly ThemeKey[]\n\nlet renderer: CliRenderer | null = null\nlet keyboardHandler: ((key: ParsedKey) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\nlet markdownScrollBox: ScrollBoxRenderable | null = null\nlet markdownDisplay: MarkdownRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet syntaxStyle: SyntaxStyle | null = null\nlet helpModal: BoxRenderable | null = null\nlet currentThemeIndex = 0\nlet concealEnabled = true\nlet showingHelp = false\nlet streamingMode = false\nlet streamingTimer: Timer | null = null\nlet streamPosition = 0\nlet endlessMode = false\nlet rendererDestroyHandler: (() => void) | null = null\n\n// Streaming speed presets: [minDelay, maxDelay] in milliseconds\nconst streamSpeeds = [\n  { name: \"Slowest\", min: 200, max: 500 }, // 0: Default\n  { name: \"Slower\", min: 150, max: 350 }, // 1\n  { name: \"Slow\", min: 100, max: 250 }, // 2\n  { name: \"Medium\", min: 70, max: 150 }, // 3\n  { name: \"Fast\", min: 40, max: 100 }, // 4\n  { name: \"Faster\", min: 20, max: 60 }, // 5\n  { name: \"Fastest\", min: 10, max: 50 }, // 6\n]\nlet currentSpeedIndex = 0\n\nconst JSON_PARSER_WASM_URL =\n  \"https://github.com/tree-sitter/tree-sitter-json/releases/download/v0.24.8/tree-sitter-json.wasm\"\nconst JSON_HIGHLIGHTS_QUERY_URL =\n  \"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/json/highlights.scm\"\n\nlet jsonParserRegistered = false\n\nfunction registerJsonParserForDemo(): void {\n  if (jsonParserRegistered) return\n\n  getTreeSitterClient().addFiletypeParser({\n    filetype: \"json\",\n    wasm: JSON_PARSER_WASM_URL,\n    queries: {\n      highlights: [JSON_HIGHLIGHTS_QUERY_URL],\n    },\n  })\n\n  jsonParserRegistered = true\n}\n\nfunction getCurrentTheme() {\n  return themes[themeKeys[currentThemeIndex]]\n}\n\nfunction getThemeTextColor(theme: (typeof themes)[ThemeKey]) {\n  return theme.styles.default.fg\n}\n\nfunction getThemeMutedTextColor(theme: (typeof themes)[ThemeKey]) {\n  return theme.styles.conceal.fg ?? theme.styles.default.fg\n}\n\nfunction getCurrentSpeed() {\n  return streamSpeeds[currentSpeedIndex]\n}\n\nfunction stopStreaming() {\n  if (streamingTimer) {\n    clearTimeout(streamingTimer)\n    streamingTimer = null\n  }\n  streamingMode = false\n  streamPosition = 0\n}\n\nfunction startStreaming() {\n  stopStreaming()\n  streamingMode = true\n  streamPosition = 0\n\n  if (!markdownDisplay || !markdownScrollBox) return\n\n  // Reset to empty and enable streaming mode\n  markdownDisplay.streaming = true\n  markdownDisplay.content = \"\"\n\n  // Enable sticky scroll to bottom for streaming\n  markdownScrollBox.stickyScroll = true\n\n  markdownScrollBox.stickyStart = \"bottom\"\n\n  // Update status\n  if (statusText) {\n    const theme = getCurrentTheme()\n    const speed = getCurrentSpeed()\n    const mode = endlessMode ? \"ENDLESS\" : \"NORMAL\"\n    statusText.content = `Theme: ${theme.name} | Conceal: ${concealEnabled ? \"ON\" : \"OFF\"} | Streaming: IN PROGRESS (${speed.name}, ${mode}) | Press X to stop`\n  }\n\n  function streamNextChunk() {\n    if (!streamingMode || !markdownDisplay || markdownDisplay.isDestroyed) return\n\n    // Random chunk size between 1 and 50 characters\n    const chunkSize = Math.floor(Math.random() * 50) + 1\n\n    // Calculate which iteration we're on and position within that iteration\n    const positionInCurrentIteration = streamPosition % markdownContent.length\n    const nextPositionInIteration = Math.min(positionInCurrentIteration + chunkSize, markdownContent.length)\n\n    // Build content by repeating the markdown as many times as needed\n    const fullIterations = Math.floor(streamPosition / markdownContent.length)\n    const currentIterationContent = markdownContent.slice(0, nextPositionInIteration)\n\n    // Construct full content: (full iterations of content) + (partial current iteration)\n    let fullContent = markdownContent.repeat(fullIterations) + currentIterationContent\n\n    markdownDisplay.content = fullContent\n    streamPosition += chunkSize\n\n    // In endless mode, never stop. In normal mode, stop after first iteration\n    const shouldContinue = endlessMode || streamPosition < markdownContent.length\n\n    if (shouldContinue) {\n      // Random delay based on current speed setting\n      const speed = getCurrentSpeed()\n      const delayRange = speed.max - speed.min\n      const delay = Math.floor(Math.random() * delayRange) + speed.min\n      streamingTimer = setTimeout(streamNextChunk, delay)\n    } else {\n      // Normal mode - streaming complete\n      streamingMode = false\n      if (statusText) {\n        const theme = getCurrentTheme()\n        const speed = getCurrentSpeed()\n        statusText.content = `Theme: ${theme.name} | Conceal: ${concealEnabled ? \"ON\" : \"OFF\"} | Streaming: COMPLETE (${speed.name}) | Press S to restart`\n      }\n    }\n  }\n\n  streamNextChunk()\n}\n\nexport async function run(rendererInstance: CliRenderer): Promise<void> {\n  renderer = rendererInstance\n\n  rendererDestroyHandler = () => {\n    stopStreaming()\n    markdownDisplay = null\n    markdownScrollBox = null\n    statusText = null\n    parentContainer = null\n    helpModal = null\n  }\n  rendererInstance.on(CliRenderEvents.DESTROY, rendererDestroyHandler)\n\n  renderer.start()\n  registerJsonParserForDemo()\n\n  const theme = getCurrentTheme()\n  renderer.setBackgroundColor(theme.bg)\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n    padding: 1,\n  })\n  renderer.root.add(parentContainer)\n\n  const titleBox = new BoxRenderable(renderer, {\n    id: \"title-box\",\n    height: 3,\n    borderStyle: \"double\",\n    borderColor: \"#4ECDC4\",\n    backgroundColor: theme.bg,\n    title: \"Markdown Demo - Table Alignment + Syntax Highlighting\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentContainer.add(titleBox)\n\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"instructions\",\n    content: \"ESC to return | Press ? for keybindings\",\n    fg: \"#888888\",\n  })\n  titleBox.add(instructionsText)\n\n  // Create help modal (hidden by default)\n  helpModal = new BoxRenderable(renderer, {\n    id: \"help-modal\",\n    position: \"absolute\",\n    left: \"50%\",\n    top: \"50%\",\n    width: 60,\n    height: 20,\n    marginLeft: -30,\n    marginTop: -10,\n    border: true,\n    borderStyle: \"double\",\n    borderColor: \"#4ECDC4\",\n    backgroundColor: theme.bg,\n    title: \"Keybindings\",\n    titleAlignment: \"center\",\n    padding: 2,\n    zIndex: 100,\n    visible: false,\n  })\n\n  const helpContent = new TextRenderable(renderer, {\n    id: \"help-content\",\n    content: `Theme:\n  T : Cycle through themes\n\nView Controls:\n  C : Toggle concealment (hide **, \\`, etc.)\n\nStreaming:\n  S : Start/restart streaming simulation\n  E : Toggle endless mode (repeats content forever)\n  X : Stop streaming (when in endless mode)\n  [ : Decrease speed (slower)\n  ] : Increase speed (faster)\n\nOther:\n  ? : Toggle this help screen\n  ESC : Return to main menu`,\n    fg: \"#E6EDF3\",\n  })\n\n  helpModal.add(helpContent)\n  renderer.root.add(helpModal)\n\n  markdownScrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"markdown-scroll-box\",\n    borderStyle: \"single\",\n\n    borderColor: \"#6BCF7F\",\n    backgroundColor: theme.bg,\n    title: `MarkdownRenderable - ${theme.name}`,\n    titleAlignment: \"left\",\n    border: true,\n    scrollY: true,\n    scrollX: false,\n    flexGrow: 1,\n    flexShrink: 1,\n    padding: 2,\n  })\n  markdownScrollBox.focus()\n  parentContainer.add(markdownScrollBox)\n\n  // Create syntax style from current theme\n  syntaxStyle = SyntaxStyle.fromStyles(theme.styles)\n\n  // Create markdown display using MarkdownRenderable\n  markdownDisplay = new MarkdownRenderable(renderer, {\n    id: \"markdown-display\",\n    content: markdownContent,\n    syntaxStyle,\n    fg: getThemeTextColor(theme),\n    bg: theme.bg,\n    conceal: concealEnabled,\n    width: \"100%\",\n  })\n\n  markdownScrollBox.add(markdownDisplay)\n\n  statusText = new TextRenderable(renderer, {\n    id: \"status-display\",\n    content: \"\",\n    fg: \"#A5D6FF\",\n    wrapMode: \"word\",\n    flexShrink: 0,\n  })\n  parentContainer.add(statusText)\n\n  const applyTheme = (theme: (typeof themes)[ThemeKey]) => {\n    rendererInstance.setBackgroundColor(theme.bg)\n    syntaxStyle = SyntaxStyle.fromStyles(theme.styles)\n\n    titleBox.backgroundColor = theme.bg\n    instructionsText.fg = getThemeMutedTextColor(theme)\n    helpContent.fg = getThemeTextColor(theme)\n\n    if (markdownDisplay) {\n      markdownDisplay.syntaxStyle = syntaxStyle\n      markdownDisplay.fg = getThemeTextColor(theme)\n      markdownDisplay.bg = theme.bg\n    }\n\n    if (markdownScrollBox) {\n      markdownScrollBox.title = `MarkdownRenderable - ${theme.name}`\n      markdownScrollBox.backgroundColor = theme.bg\n    }\n\n    if (helpModal) {\n      helpModal.backgroundColor = theme.bg\n    }\n\n    if (statusText) {\n      statusText.fg = getThemeTextColor(theme)\n    }\n  }\n\n  const updateStatusText = () => {\n    if (statusText) {\n      const theme = getCurrentTheme()\n      const speed = getCurrentSpeed()\n      const streamStatus = streamingMode ? \"STREAMING\" : \"NORMAL\"\n      const endlessStatus = endlessMode ? \" [ENDLESS]\" : \"\"\n      statusText.content = `Theme: ${theme.name} | Conceal: ${concealEnabled ? \"ON\" : \"OFF\"} | Mode: ${streamStatus}${endlessStatus} | Speed: ${speed.name} | Press T/C/S/E/[/]`\n    }\n  }\n\n  applyTheme(theme)\n  updateStatusText()\n\n  keyboardHandler = (key: ParsedKey) => {\n    // Handle help modal toggle\n    if (key.raw === \"?\" && helpModal) {\n      showingHelp = !showingHelp\n      helpModal.visible = showingHelp\n      return\n    }\n\n    // Don't process other keys when help is showing\n    if (showingHelp) return\n\n    if (key.name === \"s\" && !key.ctrl && !key.meta) {\n      // Start/restart streaming simulation\n      startStreaming()\n    } else if (key.name === \"e\" && !key.ctrl && !key.meta) {\n      // Toggle endless mode\n      endlessMode = !endlessMode\n      updateStatusText()\n    } else if (key.name === \"x\" && !key.ctrl && !key.meta) {\n      // Stop streaming (for endless mode)\n      stopStreaming()\n      if (markdownDisplay) {\n        markdownDisplay.streaming = false\n      }\n      updateStatusText()\n    } else if (key.raw === \"[\" && !key.ctrl && !key.meta) {\n      // Decrease streaming speed (slower)\n      if (currentSpeedIndex > 0) {\n        currentSpeedIndex--\n        updateStatusText()\n      }\n    } else if (key.raw === \"]\" && !key.ctrl && !key.meta) {\n      // Increase streaming speed (faster)\n      if (currentSpeedIndex < streamSpeeds.length - 1) {\n        currentSpeedIndex++\n        updateStatusText()\n      }\n    } else if (key.name === \"t\" && !key.ctrl && !key.meta) {\n      // Cycle through themes\n      currentThemeIndex = (currentThemeIndex + 1) % themeKeys.length\n      applyTheme(getCurrentTheme())\n\n      updateStatusText()\n    } else if (key.name === \"c\" && !key.ctrl && !key.meta) {\n      // Stop streaming when toggling conceal\n      stopStreaming()\n\n      concealEnabled = !concealEnabled\n      if (markdownDisplay) {\n        markdownDisplay.conceal = concealEnabled\n        markdownDisplay.streaming = false\n        markdownDisplay.content = markdownContent\n      }\n      updateStatusText()\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  stopStreaming()\n\n  if (rendererDestroyHandler) {\n    rendererInstance.off(CliRenderEvents.DESTROY, rendererDestroyHandler)\n    rendererDestroyHandler = null\n  }\n\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  parentContainer?.destroy()\n  helpModal?.destroy()\n  parentContainer = null\n  markdownScrollBox = null\n  markdownDisplay = null\n  statusText = null\n  syntaxStyle = null\n  helpModal = null\n\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/mouse-interaction-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  TextAttributes,\n  FrameBufferRenderable,\n  TextRenderable,\n  t,\n  type MouseEvent,\n  OptimizedBuffer,\n  BoxRenderable,\n  createTimeline,\n  engine,\n  Box,\n  type ProxiedVNode,\n  type BoxOptions,\n  Text,\n  type VChild,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\ninterface TrailCell {\n  x: number\n  y: number\n  timestamp: number\n  isDrag?: boolean\n}\n\nlet demoContainer: MouseInteractionFrameBuffer | null = null\nlet titleText: TextRenderable | null = null\nlet instructionsText: TextRenderable | null = null\nlet draggableBoxes: ProxiedVNode<typeof BoxRenderable>[] = []\nlet nextZIndex = 101\n\nfunction DraggableBox(\n  props: BoxOptions & {\n    x: number\n    y: number\n    width: number\n    height: number\n    color: RGBA\n    label: string\n  },\n  children?: VChild,\n) {\n  const bgColor = RGBA.fromValues(props.color.r, props.color.g, props.color.b, 0.8)\n  const borderColor = RGBA.fromValues(props.color.r * 1.2, props.color.g * 1.2, props.color.b * 1.2, 1.0)\n\n  let isDragging = false\n  let gotText = \"\"\n  let scrollText = \"\"\n  let scrollTimestamp = 0\n  let dragOffsetX = 0\n  let dragOffsetY = 0\n  let bounceScale = { value: 1 }\n  let baseWidth: number = props.width\n  let baseHeight: number = props.height\n  let originalBg: RGBA = bgColor\n  let dragBg: RGBA = RGBA.fromValues(props.color.r, props.color.g, props.color.b, 0.3)\n  let originalBorderColor: RGBA = borderColor\n  let dragBorderColor: RGBA = RGBA.fromValues(props.color.r * 1.2, props.color.g * 1.2, props.color.b * 1.2, 0.5)\n\n  return Box(\n    {\n      ...props,\n      position: \"absolute\",\n      left: props.x,\n      top: props.y,\n      width: props.width,\n      height: props.height,\n      backgroundColor: bgColor,\n      borderColor: borderColor,\n      borderStyle: \"rounded\",\n      title: props.label,\n      titleAlignment: \"center\",\n      border: true,\n      zIndex: 100,\n      renderAfter(buffer, deltaTime) {\n        const currentTime = Date.now()\n        if (scrollText && currentTime - scrollTimestamp > 2000) {\n          scrollText = \"\"\n        }\n\n        const baseCenterX = this.x + Math.floor(this.width / 2)\n        const baseCenterY = this.y + Math.floor(this.height / 2)\n\n        let textLines = 0\n        if (isDragging) textLines++\n        if (scrollText) textLines++\n        if (gotText) textLines += 2\n\n        let currentY = textLines > 1 ? baseCenterY - Math.floor(textLines / 2) : baseCenterY\n\n        if (isDragging) {\n          const centerX = baseCenterX - 2\n          buffer.drawText(\"drag\", centerX, currentY, RGBA.fromInts(64, 224, 208))\n          currentY++\n        }\n\n        if (scrollText) {\n          const age = currentTime - scrollTimestamp\n          const fadeRatio = Math.max(0, 1 - age / 2000)\n          const alpha = Math.round(255 * fadeRatio)\n\n          const centerX = baseCenterX - Math.floor(scrollText.length / 2)\n          buffer.drawText(scrollText, centerX, currentY, RGBA.fromInts(255, 255, 0, alpha))\n          currentY++\n        }\n\n        if (gotText) {\n          const gotX = baseCenterX - 2\n          const gotTextX = baseCenterX - Math.floor(gotText.length / 2)\n          buffer.drawText(\"got\", gotX, currentY, RGBA.fromInts(255, 182, 193))\n          currentY++\n          buffer.drawText(gotText, gotTextX, currentY, RGBA.fromInts(147, 226, 255))\n        }\n      },\n      onMouse(event: MouseEvent): void {\n        switch (event.type) {\n          case \"down\":\n            gotText = \"\"\n            isDragging = true\n            dragOffsetX = event.x - this.x\n            dragOffsetY = event.y - this.y\n            this.zIndex = nextZIndex++\n            this.backgroundColor = dragBg\n            this.borderColor = dragBorderColor\n            event.stopPropagation()\n            break\n\n          case \"drag-end\":\n            if (isDragging) {\n              isDragging = false\n              this.zIndex = 100\n              this.backgroundColor = originalBg\n              this.borderColor = originalBorderColor\n              event.stopPropagation()\n            }\n            break\n\n          case \"drag\":\n            if (isDragging) {\n              const newX = event.x - dragOffsetX\n              const newY = event.y - dragOffsetY\n\n              const boundedX = Math.max(0, Math.min(newX, this._ctx.width - this.width))\n              const boundedY = Math.max(4, Math.min(newY, this._ctx.height - this.height))\n\n              this.x = boundedX\n              this.y = boundedY\n\n              event.stopPropagation()\n            }\n            break\n\n          case \"over\":\n            gotText = \"over \" + (event.source?.id || \"\")\n            break\n\n          case \"out\":\n            gotText = \"out\"\n            break\n\n          case \"drop\":\n            gotText = event.source?.id || \"\"\n            const timeline = createTimeline()\n\n            timeline.add(bounceScale, {\n              value: 1.5,\n              duration: 200,\n              ease: \"outExpo\",\n              onUpdate: (values) => {\n                const scale = values.targets[0].value\n                this.width = Math.round(baseWidth * scale)\n                this.height = Math.round(baseHeight * scale)\n              },\n            })\n\n            timeline.add(\n              bounceScale,\n              {\n                value: 1.0,\n                duration: 400,\n                ease: \"outExpo\",\n                onUpdate: (values) => {\n                  const scale = values.targets[0].value\n                  this.width = Math.round(baseWidth * scale)\n                  this.height = Math.round(baseHeight * scale)\n                },\n              },\n              200,\n            )\n            break\n\n          case \"scroll\":\n            if (event.scroll) {\n              scrollText = `scroll ${event.scroll.direction}`\n              scrollTimestamp = Date.now()\n              event.stopPropagation()\n            }\n            break\n        }\n      },\n    },\n    children,\n  )\n}\n\nclass MouseInteractionFrameBuffer extends FrameBufferRenderable {\n  private readonly trailCells = new Map<string, TrailCell>()\n  private readonly activatedCells = new Set<string>()\n  private readonly TRAIL_FADE_DURATION = 3000\n\n  private readonly TRAIL_COLOR = RGBA.fromInts(64, 224, 208, 255)\n  private readonly DRAG_COLOR = RGBA.fromInts(255, 165, 0, 255)\n  private readonly ACTIVATED_COLOR = RGBA.fromInts(255, 20, 147, 255)\n  private readonly BACKGROUND_COLOR = RGBA.fromInts(15, 15, 35, 255)\n  private readonly CURSOR_COLOR = RGBA.fromInts(255, 255, 255, 255)\n\n  constructor(id: string, renderer: CliRenderer) {\n    super(renderer, {\n      id,\n      width: renderer.terminalWidth,\n      height: renderer.terminalHeight,\n      zIndex: 0,\n    })\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    const currentTime = Date.now()\n\n    this.frameBuffer.clear(this.BACKGROUND_COLOR)\n\n    for (const [key, cell] of this.trailCells.entries()) {\n      if (currentTime - cell.timestamp > this.TRAIL_FADE_DURATION) {\n        this.trailCells.delete(key)\n      }\n    }\n\n    for (const [, cell] of this.trailCells.entries()) {\n      const age = currentTime - cell.timestamp\n      const fadeRatio = 1 - age / this.TRAIL_FADE_DURATION\n\n      if (fadeRatio > 0) {\n        const baseColor = cell.isDrag ? this.DRAG_COLOR : this.TRAIL_COLOR\n        const smoothAlpha = fadeRatio\n\n        const fadedColor = RGBA.fromValues(baseColor.r, baseColor.g, baseColor.b, smoothAlpha)\n\n        this.frameBuffer.setCellWithAlphaBlending(cell.x, cell.y, \"█\", fadedColor, this.BACKGROUND_COLOR)\n      }\n    }\n\n    for (const cellKey of this.activatedCells) {\n      const [x, y] = cellKey.split(\",\").map(Number)\n\n      this.frameBuffer.drawText(\"█\", x, y, this.ACTIVATED_COLOR, this.BACKGROUND_COLOR)\n    }\n\n    const recentTrails = Array.from(this.trailCells.values())\n      .filter((cell) => currentTime - cell.timestamp < 100)\n      .sort((a, b) => b.timestamp - a.timestamp)\n\n    if (recentTrails.length > 0) {\n      const latest = recentTrails[0]\n      this.frameBuffer.setCellWithAlphaBlending(latest.x, latest.y, \"+\", this.CURSOR_COLOR, this.BACKGROUND_COLOR)\n    }\n\n    super.renderSelf(buffer)\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    if (event.propagationStopped) return\n\n    const cellKey = `${event.x},${event.y}`\n\n    switch (event.type) {\n      case \"move\":\n        this.trailCells.set(cellKey, {\n          x: event.x,\n          y: event.y,\n          timestamp: Date.now(),\n          isDrag: false,\n        })\n        this.requestRender()\n        break\n\n      case \"drag\":\n        this.trailCells.set(cellKey, {\n          x: event.x,\n          y: event.y,\n          timestamp: Date.now(),\n          isDrag: true,\n        })\n        this.requestRender()\n        break\n\n      case \"down\":\n        if (this.activatedCells.has(cellKey)) {\n          this.activatedCells.delete(cellKey)\n        } else {\n          this.activatedCells.add(cellKey)\n        }\n        this.requestRender()\n        break\n    }\n  }\n\n  public clearState(): void {\n    this.trailCells.clear()\n    this.activatedCells.clear()\n  }\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  const backgroundColor = RGBA.fromInts(15, 15, 35, 255)\n  renderer.setBackgroundColor(backgroundColor)\n\n  engine.attach(renderer)\n\n  const mainGroup = new BoxRenderable(renderer, {\n    id: \"mouse-demo-main-group\",\n    zIndex: 10,\n  })\n  renderer.root.add(mainGroup)\n\n  titleText = new TextRenderable(renderer, {\n    id: \"mouse_demo_title\",\n    content: \"Mouse Interaction Demo with Draggable Objects\",\n    width: \"100%\",\n    position: \"absolute\",\n    left: 2,\n    top: 1,\n    fg: RGBA.fromInts(72, 209, 204),\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  mainGroup.add(titleText)\n\n  instructionsText = new TextRenderable(renderer, {\n    id: \"mouse_demo_instructions\",\n    content: t`Drag boxes around • Move mouse: turquoise trails\nHold + move: orange drag trails • Click cells: toggle pink\nScroll on boxes: shows direction • Escape: menu`,\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    width: renderer.width - 4,\n    height: 3,\n    fg: RGBA.fromInts(176, 196, 222),\n    zIndex: 1000,\n  })\n  mainGroup.add(instructionsText)\n\n  demoContainer = new MouseInteractionFrameBuffer(\"mouse-demo-buffer\", renderer)\n  mainGroup.add(demoContainer)\n\n  draggableBoxes = [\n    DraggableBox({\n      id: \"drag-box-1\",\n      x: 10,\n      y: 8,\n      width: 20,\n      height: 10,\n      color: RGBA.fromInts(200, 100, 150),\n      label: \"Box 1\",\n    }),\n    DraggableBox({\n      id: \"drag-box-2\",\n      x: 30,\n      y: 12,\n      width: 18,\n      height: 10,\n      color: RGBA.fromInts(100, 200, 150),\n      label: \"Box 2\",\n    }),\n    DraggableBox({\n      id: \"drag-box-3\",\n      x: 50,\n      y: 15,\n      width: 20,\n      height: 11,\n      color: RGBA.fromInts(150, 150, 200),\n      label: \"Box 3\",\n    }),\n    DraggableBox(\n      {\n        id: \"drag-box-4\",\n        x: 15,\n        y: 20,\n        width: 18,\n        height: 11,\n        color: RGBA.fromInts(200, 200, 100),\n        label: \"O hidden\",\n        overflow: \"hidden\",\n      },\n      Text({\n        id: \"overflow-hidden-box\",\n        content: \"This should be cut off to the right\",\n        width: 25,\n        height: 25,\n        onMouse: (event: MouseEvent) => {\n          console.log(\"mouse\", event.type)\n        },\n      }),\n    ),\n  ]\n\n  for (const box of draggableBoxes) {\n    mainGroup.add(box)\n  }\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n  renderer.root.getRenderable(\"mouse-demo-main-group\")?.destroyRecursively()\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/nested-zindex-demo.ts",
    "content": "import { TextAttributes, createCliRenderer, TextRenderable, BoxRenderable, type KeyEvent } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport type { CliRenderer } from \"../index.js\"\n\nlet globalKeyboardHandler: ((key: KeyEvent) => void) | null = null\nlet zIndexPhase = 0\nlet animationSpeed = 2000\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  renderer.setBackgroundColor(\"#001122\")\n\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  const title = new TextRenderable(renderer, {\n    id: \"main-title\",\n    content: \"Nested Render Objects & Z-Index Demo\",\n    position: \"absolute\",\n    left: 10,\n    top: 2,\n    fg: \"#FFFF00\",\n    attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n    zIndex: 1000,\n  })\n  parentContainer.add(title)\n\n  // Parent group with high z-index\n  const parentGroupA = new BoxRenderable(renderer, {\n    id: \"parent-group-a\",\n    position: \"absolute\",\n    zIndex: 100,\n    visible: true,\n  })\n  parentContainer.add(parentGroupA)\n\n  // Parent group with medium z-index\n  const parentGroupB = new BoxRenderable(renderer, {\n    id: \"parent-group-b\",\n    position: \"absolute\",\n    zIndex: 50,\n    visible: true,\n  })\n  parentContainer.add(parentGroupB)\n\n  // Parent group with low z-index\n  const parentGroupC = new BoxRenderable(renderer, {\n    id: \"parent-group-c\",\n    position: \"absolute\",\n    zIndex: 20,\n    visible: true,\n  })\n  parentContainer.add(parentGroupC)\n\n  // Group A - High Z-Index Parent (z=100)\n  const boxA1 = new BoxRenderable(renderer, {\n    id: \"box-a1\",\n    position: \"absolute\",\n    left: 15,\n    top: 8,\n    width: 25,\n    height: 6,\n    backgroundColor: \"#220044\",\n    zIndex: 10,\n    borderStyle: \"single\",\n    borderColor: \"#FF44FF\",\n    title: \"Parent A (z=100)\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentGroupA.add(boxA1)\n\n  const textA1 = new TextRenderable(renderer, {\n    id: \"text-a1\",\n    content: \"Child A1 (z=10)\",\n    position: \"absolute\",\n    left: 17,\n    top: 10,\n    fg: \"#FF44FF\",\n    attributes: TextAttributes.BOLD,\n    zIndex: 10,\n  })\n  parentGroupA.add(textA1)\n\n  const boxA2 = new BoxRenderable(renderer, {\n    id: \"box-a2\",\n    position: \"absolute\",\n    left: 20,\n    top: 11,\n    width: 15,\n    height: 4,\n    backgroundColor: \"#440044\",\n    zIndex: 5,\n    borderStyle: \"single\",\n    borderColor: \"#FF88FF\",\n    border: true,\n  })\n  parentGroupA.add(boxA2)\n\n  const textA2 = new TextRenderable(renderer, {\n    id: \"text-a2\",\n    content: \"Child A2 (z=5)\",\n    position: \"absolute\",\n    left: 22,\n    top: 12,\n    fg: \"#FF88FF\",\n    zIndex: 5,\n  })\n  parentGroupA.add(textA2)\n\n  // Group B - Medium Z-Index Parent (z=50)\n  const boxB1 = new BoxRenderable(renderer, {\n    id: \"box-b1\",\n    position: \"absolute\",\n    left: 30,\n    top: 12,\n    width: 25,\n    height: 6,\n    backgroundColor: \"#004422\",\n    zIndex: 20,\n    borderStyle: \"double\",\n    borderColor: \"#44FF44\",\n    title: \"Parent B (z=50)\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentGroupB.add(boxB1)\n\n  const textB1 = new TextRenderable(renderer, {\n    id: \"text-b1\",\n    content: \"Child B1 (z=20)\",\n    position: \"absolute\",\n    left: 32,\n    top: 14,\n    fg: \"#44FF44\",\n    attributes: TextAttributes.BOLD,\n    zIndex: 20,\n  })\n  parentGroupB.add(textB1)\n\n  const boxB2 = new BoxRenderable(renderer, {\n    id: \"box-b2\",\n    position: \"absolute\",\n    left: 35,\n    top: 15,\n    width: 15,\n    height: 4,\n    backgroundColor: \"#004400\",\n    zIndex: 15,\n    borderStyle: \"single\",\n    borderColor: \"#88FF88\",\n    border: true,\n  })\n  parentGroupB.add(boxB2)\n\n  const textB2 = new TextRenderable(renderer, {\n    id: \"text-b2\",\n    content: \"Child B2 (z=15)\",\n    position: \"absolute\",\n    left: 37,\n    top: 16,\n    fg: \"#88FF88\",\n    zIndex: 15,\n  })\n  parentGroupB.add(textB2)\n\n  // Group C - Low Z-Index Parent (z=20)\n  const boxC1 = new BoxRenderable(renderer, {\n    id: \"box-c1\",\n    position: \"absolute\",\n    left: 45,\n    top: 16,\n    width: 25,\n    height: 6,\n    backgroundColor: \"#442200\",\n    zIndex: 30,\n    borderStyle: \"rounded\",\n    borderColor: \"#FFFF44\",\n    title: \"Parent C (z=20)\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentGroupC.add(boxC1)\n\n  const textC1 = new TextRenderable(renderer, {\n    id: \"text-c1\",\n    content: \"Child C1 (z=30)\",\n    position: \"absolute\",\n    left: 47,\n    top: 18,\n    fg: \"#FFFF44\",\n    attributes: TextAttributes.BOLD,\n    zIndex: 30,\n  })\n  parentGroupC.add(textC1)\n\n  const boxC2 = new BoxRenderable(renderer, {\n    id: \"box-c2\",\n    position: \"absolute\",\n    left: 50,\n    top: 19,\n    width: 15,\n    height: 4,\n    backgroundColor: \"#444400\",\n    zIndex: 25,\n    borderStyle: \"single\",\n    borderColor: \"#FFFF88\",\n    border: true,\n  })\n  parentGroupC.add(boxC2)\n\n  const textC2 = new TextRenderable(renderer, {\n    id: \"text-c2\",\n    content: \"Child C2 (z=25)\",\n    position: \"absolute\",\n    left: 52,\n    top: 20,\n    fg: \"#FFFF88\",\n    zIndex: 25,\n  })\n  parentGroupC.add(textC2)\n\n  const explanation1 = new TextRenderable(renderer, {\n    id: \"explanation1\",\n    content: \"Key Concept: Parent z-index determines group layering, child z-index determines order within group\",\n    position: \"absolute\",\n    left: 10,\n    top: 25,\n    fg: \"#AAAAAA\",\n    zIndex: 1000,\n  })\n  parentContainer.add(explanation1)\n\n  const explanation2 = new TextRenderable(renderer, {\n    id: \"explanation2\",\n    content: \"Even if Child C1 has z=30, it renders behind Parent A & B because Parent C has z=20\",\n    position: \"absolute\",\n    left: 10,\n    top: 26,\n    fg: \"#AAAAAA\",\n    zIndex: 1000,\n  })\n  parentContainer.add(explanation2)\n\n  const phaseIndicator = new TextRenderable(renderer, {\n    id: \"phase-indicator\",\n    content: \"Animation Phase: 1/4\",\n    position: \"absolute\",\n    left: 10,\n    top: 28,\n    fg: \"#FFFFFF\",\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  parentContainer.add(phaseIndicator)\n\n  const zIndexDisplay = new TextRenderable(renderer, {\n    id: \"zindex-display\",\n    content: \"Current Z-Indices - A:100, B:50, C:20\",\n    position: \"absolute\",\n    left: 10,\n    top: 29,\n    fg: \"#FFFFFF\",\n    zIndex: 1000,\n  })\n  parentContainer.add(zIndexDisplay)\n\n  renderer.setFrameCallback(async (deltaMs) => {\n    const time = Date.now()\n    const newPhase = Math.floor((time % (animationSpeed * 4)) / animationSpeed)\n\n    if (newPhase !== zIndexPhase) {\n      zIndexPhase = newPhase\n\n      // Reset to original z-indices\n      parentGroupA.zIndex = 100\n      parentGroupB.zIndex = 50\n      parentGroupC.zIndex = 20\n\n      // Update box titles and colors based on phase\n      switch (zIndexPhase) {\n        case 0: // Original state\n          parentGroupA.zIndex = 100\n          parentGroupB.zIndex = 50\n          parentGroupC.zIndex = 20\n          boxA1.title = \"Parent A (z=100)\"\n          boxB1.title = \"Parent B (z=50)\"\n          boxC1.title = \"Parent C (z=20)\"\n          break\n        case 1: // C becomes highest\n          parentGroupA.zIndex = 50\n          parentGroupB.zIndex = 20\n          parentGroupC.zIndex = 100\n          boxA1.title = \"Parent A (z=50)\"\n          boxB1.title = \"Parent B (z=20)\"\n          boxC1.title = \"Parent C (z=100)\"\n          break\n        case 2: // B becomes highest\n          parentGroupA.zIndex = 20\n          parentGroupB.zIndex = 100\n          parentGroupC.zIndex = 50\n          boxA1.title = \"Parent A (z=20)\"\n          boxB1.title = \"Parent B (z=100)\"\n          boxC1.title = \"Parent C (z=50)\"\n          break\n        case 3: // All equal - shows child z-index importance\n          parentGroupA.zIndex = 60\n          parentGroupB.zIndex = 60\n          parentGroupC.zIndex = 60\n          boxA1.title = \"Parent A (z=60)\"\n          boxB1.title = \"Parent B (z=60)\"\n          boxC1.title = \"Parent C (z=60)\"\n          break\n      }\n\n      const phases = [\"Original Hierarchy\", \"C Group on Top\", \"B Group on Top\", \"Equal Parents (Child z-index matters)\"]\n      phaseIndicator.content = `Animation Phase: ${zIndexPhase + 1}/4 - ${phases[zIndexPhase]}`\n\n      zIndexDisplay.content = `Current Z-Indices - A:${parentGroupA.zIndex}, B:${parentGroupB.zIndex}, C:${parentGroupC.zIndex}`\n    }\n  })\n\n  globalKeyboardHandler = (key: KeyEvent) => {\n    if (key.name === \"+\" || key.name === \"=\") {\n      animationSpeed = Math.max(500, animationSpeed - 200)\n    } else if (key.name === \"-\" || key.name === \"_\") {\n      animationSpeed = Math.min(5000, animationSpeed + 200)\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", globalKeyboardHandler)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (globalKeyboardHandler) {\n    renderer.keyInput.off(\"keypress\", globalKeyboardHandler)\n    globalKeyboardHandler = null\n  }\n\n  renderer.root.remove(\"main-title\")\n  renderer.root.remove(\"parent-container\")\n\n  renderer.clearFrameCallbacks()\n  renderer.setCursorPosition(0, 0, false)\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/opacity-example.ts",
    "content": "import { CliRenderer, BoxRenderable, TextRenderable, createCliRenderer, type KeyEvent } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet renderer: CliRenderer | null = null\nlet header: BoxRenderable | null = null\nlet container: BoxRenderable | null = null\nlet infoText: TextRenderable | null = null\nlet boxes: BoxRenderable[] = []\nlet opacityValues = [1.0, 0.8, 0.5, 0.3]\nlet animationInterval: Timer | null = null\n\nfunction createOpacityDemo(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#1a1a2e\")\n\n  // Info header\n  header = new BoxRenderable(renderer, {\n    id: \"opacity-demo-header\",\n    width: \"auto\",\n    height: 3,\n    backgroundColor: \"#16213e\",\n    border: true,\n    borderStyle: \"single\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  })\n\n  infoText = new TextRenderable(renderer, {\n    id: \"info\",\n    content: \"OPACITY DEMO | 1-4: Toggle opacity | A: Animate | Ctrl+C: Exit\",\n    fg: \"#e94560\",\n    bg: \"transparent\",\n  })\n  header.add(infoText)\n\n  // Main container\n  container = new BoxRenderable(renderer, {\n    id: \"opacity-demo-container\",\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    padding: 2,\n  })\n\n  // Create 4 overlapping boxes with different opacities\n  const colors = [\"#e94560\", \"#0f3460\", \"#533483\", \"#16a085\"]\n  const labels = [\"Box 1\", \"Box 2\", \"Box 3\", \"Box 4\"]\n\n  for (let i = 0; i < 4; i++) {\n    const box = new BoxRenderable(renderer, {\n      id: `box-${i}`,\n      width: 20,\n      height: 8,\n      backgroundColor: colors[i],\n      border: true,\n      borderStyle: \"double\",\n      borderColor: \"#ffffff\",\n      position: \"absolute\",\n      left: 10 + i * 8,\n      top: 5 + i * 2,\n      opacity: opacityValues[i],\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      flexDirection: \"column\",\n    })\n\n    const label = new TextRenderable(renderer, {\n      id: `label-${i}`,\n      content: labels[i],\n      fg: \"#ffffff\",\n      bg: \"transparent\",\n    })\n\n    const opacityLabel = new TextRenderable(renderer, {\n      id: `opacity-${i}`,\n      content: `Opacity: ${opacityValues[i].toFixed(1)}`,\n      fg: \"#ffffff\",\n      bg: \"transparent\",\n    })\n\n    box.add(label)\n    box.add(opacityLabel)\n    boxes.push(box)\n    container.add(box)\n  }\n\n  // Nested opacity demo\n  const nestedContainer = new BoxRenderable(renderer, {\n    id: \"nested-container\",\n    width: 35,\n    height: 10,\n    backgroundColor: \"#e94560\",\n    border: true,\n    borderStyle: \"single\",\n    position: \"absolute\",\n    right: 5,\n    top: 5,\n    opacity: 0.7,\n    padding: 1,\n    flexDirection: \"column\",\n  })\n\n  const nestedLabel = new TextRenderable(renderer, {\n    id: \"nested-label\",\n    content: \"Parent: 0.7 opacity\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n  })\n\n  const nestedChild = new BoxRenderable(renderer, {\n    id: \"nested-child\",\n    width: \"auto\",\n    height: 5,\n    backgroundColor: \"#0f3460\",\n    border: true,\n    opacity: 0.5, // Effective: 0.7 * 0.5 = 0.35\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    flexDirection: \"column\",\n  })\n\n  const childLabel = new TextRenderable(renderer, {\n    id: \"child-label\",\n    content: \"Child: 0.5 opacity\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n  })\n\n  const effectiveLabel = new TextRenderable(renderer, {\n    id: \"effective-label\",\n    content: \"Effective: 0.35\",\n    fg: \"#ffcc00\",\n    bg: \"transparent\",\n  })\n\n  nestedChild.add(childLabel)\n  nestedChild.add(effectiveLabel)\n  nestedContainer.add(nestedLabel)\n  nestedContainer.add(nestedChild)\n  container.add(nestedContainer)\n\n  renderer.root.add(header)\n  renderer.root.add(container)\n}\n\nfunction updateOpacityLabels(): void {\n  for (let i = 0; i < boxes.length; i++) {\n    const opacityLabel = boxes[i].getRenderable(`opacity-${i}`) as TextRenderable | undefined\n    if (opacityLabel) {\n      opacityLabel.content = `Opacity: ${boxes[i].opacity.toFixed(1)}`\n    }\n  }\n}\n\nfunction handleKeyPress(key: KeyEvent): void {\n  switch (key.name) {\n    case \"1\":\n      boxes[0].opacity = boxes[0].opacity === 1.0 ? 0.3 : 1.0\n      updateOpacityLabels()\n      break\n    case \"2\":\n      boxes[1].opacity = boxes[1].opacity === 1.0 ? 0.3 : 1.0\n      updateOpacityLabels()\n      break\n    case \"3\":\n      boxes[2].opacity = boxes[2].opacity === 1.0 ? 0.3 : 1.0\n      updateOpacityLabels()\n      break\n    case \"4\":\n      boxes[3].opacity = boxes[3].opacity === 1.0 ? 0.3 : 1.0\n      updateOpacityLabels()\n      break\n    case \"a\":\n      toggleAnimation()\n      break\n  }\n}\n\nfunction toggleAnimation(): void {\n  if (animationInterval) {\n    clearInterval(animationInterval)\n    animationInterval = null\n    if (infoText) {\n      infoText.content = \"OPACITY DEMO | 1-4: Toggle opacity | A: Animate | Ctrl+C: Exit\"\n    }\n  } else {\n    let phase = 0\n    animationInterval = setInterval(() => {\n      phase += 0.05\n      for (let i = 0; i < boxes.length; i++) {\n        boxes[i].opacity = 0.3 + 0.7 * Math.abs(Math.sin(phase + i * 0.5))\n      }\n      updateOpacityLabels()\n    }, 50)\n    if (infoText) {\n      infoText.content = \"OPACITY DEMO | Animating... | A: Stop | Ctrl+C: Exit\"\n    }\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  createOpacityDemo(rendererInstance)\n  rendererInstance.keyInput.on(\"keypress\", handleKeyPress)\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (animationInterval) {\n    clearInterval(animationInterval)\n    animationInterval = null\n  }\n  rendererInstance.keyInput.off(\"keypress\", handleKeyPress)\n  if (header) {\n    rendererInstance.root.remove(\"opacity-demo-header\")\n    header = null\n  }\n  if (container) {\n    rendererInstance.root.remove(\"opacity-demo-container\")\n    container = null\n  }\n  boxes = []\n  infoText = null\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 30,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/opentui-demo.ts",
    "content": "import {\n  TextAttributes,\n  rgbToHex,\n  hsvToRgb,\n  createCliRenderer,\n  TextRenderable,\n  BoxRenderable,\n  parseColor,\n  getBorderFromSides,\n  type KeyEvent,\n} from \"../index.js\"\nimport type { BorderCharacters, BorderSidesConfig, CliRenderer } from \"../index.js\"\nimport { TabControllerRenderable } from \"./lib/tab-controller.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet globalTabController: TabControllerRenderable | null = null\nlet globalKeyboardHandler: ((key: KeyEvent) => void) | null = null\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  renderer.setBackgroundColor(\"#000028\")\n\n  const tabController = new TabControllerRenderable(\"main-tab-controller\", renderer, {\n    position: \"absolute\",\n    left: 0,\n    top: 0,\n    width: renderer.terminalWidth,\n    height: renderer.terminalHeight,\n    zIndex: 0,\n  })\n  globalTabController = tabController\n  renderer.root.add(tabController)\n\n  // Tab: Text & Attributes\n  const wheelRadius = 7\n  const wheelCenterX = 70\n  const wheelCenterY = 15\n  let activeWheelPixels = new Set<string>()\n\n  tabController.addTab({\n    title: \"Text & Attributes\",\n    init: (tabGroup) => {\n      const textTitle = new TextRenderable(renderer, {\n        id: \"text-title\",\n        content: \"Text Styling & Color Gradients\",\n        position: \"absolute\",\n        left: 10,\n        top: 5,\n        fg: \"#FFFF00\",\n        attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(textTitle)\n\n      // Text attributes\n      const attrBold = new TextRenderable(renderer, {\n        id: \"attr-bold\",\n        content: \"Bold Text\",\n        position: \"absolute\",\n        left: 10,\n        top: 8,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(attrBold)\n\n      const attrItalic = new TextRenderable(renderer, {\n        id: \"attr-italic\",\n        content: \"Italic Text\",\n        position: \"absolute\",\n        left: 10,\n        top: 9,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.ITALIC,\n        zIndex: 10,\n      })\n      tabGroup.add(attrItalic)\n\n      const attrUnderline = new TextRenderable(renderer, {\n        id: \"attr-underline\",\n        content: \"Underlined Text\",\n        position: \"absolute\",\n        left: 10,\n        top: 10,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(attrUnderline)\n\n      const attrDim = new TextRenderable(renderer, {\n        id: \"attr-dim\",\n        content: \"Dim Text\",\n        position: \"absolute\",\n        left: 10,\n        top: 11,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.DIM,\n        zIndex: 10,\n      })\n      tabGroup.add(attrDim)\n\n      const attrCombined = new TextRenderable(renderer, {\n        id: \"attr-combined\",\n        content: \"Bold + Italic + Underline\",\n        position: \"absolute\",\n        left: 10,\n        top: 12,\n        fg: \"#FF6464\",\n        attributes: TextAttributes.BOLD | TextAttributes.ITALIC | TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(attrCombined)\n\n      // Color gradient\n      const gradientTitle = new TextRenderable(renderer, {\n        id: \"gradient-title\",\n        content: \"Rainbow Gradient:\",\n        position: \"absolute\",\n        left: 10,\n        top: 15,\n        fg: \"#CCCCCC\",\n        zIndex: 10,\n      })\n      tabGroup.add(gradientTitle)\n\n      for (let i = 0; i < 40; i++) {\n        const hue = (i / 40) * 360\n        const color = hsvToRgb(hue, 1, 1)\n        const hexColor = rgbToHex(color)\n\n        const gradientPixel = new TextRenderable(renderer, {\n          id: `gradient-${i}`,\n          content: \"█\",\n          position: \"absolute\",\n          left: 10 + i,\n          top: 17,\n          fg: hexColor,\n          zIndex: 10,\n        })\n        tabGroup.add(gradientPixel)\n      }\n    },\n    update: (deltaMs: number, tabGroup: BoxRenderable) => {\n      // Animate the rotating color wheel\n      const time = Date.now() / 1000\n      const rotationSpeed = 45 // degrees per second\n      const rotationAngle = (time * rotationSpeed) % 360\n      const rotationRadians = rotationAngle * (Math.PI / 180)\n\n      // Track new wheel pixels for this frame\n      const newWheelPixels = new Set<string>()\n\n      for (let y = wheelCenterY - wheelRadius; y <= wheelCenterY + wheelRadius; y++) {\n        for (let x = wheelCenterX - wheelRadius * 2; x <= wheelCenterX + wheelRadius * 2; x++) {\n          const dx = (x - wheelCenterX) / 2 // Adjust for terminal character aspect ratio\n          const dy = y - wheelCenterY\n          const distance = Math.sqrt(dx * dx + dy * dy)\n\n          if (distance <= wheelRadius) {\n            const angle = Math.atan2(dy, dx)\n            const rotatedAngle = angle + rotationRadians\n            const hue = ((rotatedAngle / Math.PI) * 180 + 180) % 360\n            const saturation = distance / wheelRadius\n            const color = hsvToRgb(hue, saturation, 1)\n\n            const pixelId = `wheel-${x}-${y}`\n            newWheelPixels.add(pixelId)\n\n            const existingPixel = tabGroup.getRenderable(pixelId) as TextRenderable\n            if (existingPixel) {\n              existingPixel.setPosition({ left: x, top: y })\n              existingPixel.fg = color\n            } else {\n              const wheelPixel = new TextRenderable(renderer, {\n                id: pixelId,\n                content: \"█\",\n                position: \"absolute\",\n                left: x,\n                top: y,\n                fg: color,\n                zIndex: 10,\n              })\n              tabGroup.add(wheelPixel)\n              activeWheelPixels.add(pixelId)\n            }\n          }\n        }\n      }\n\n      // Remove any wheel pixels that are no longer part of the wheel\n      for (const pixelId of activeWheelPixels) {\n        if (!newWheelPixels.has(pixelId)) {\n          tabGroup.remove(pixelId)\n          activeWheelPixels.delete(pixelId)\n        }\n      }\n\n      activeWheelPixels = newWheelPixels\n    },\n    show: () => {\n      activeWheelPixels.clear()\n    },\n    hide: () => {\n      for (const pixelId of activeWheelPixels) {\n        renderer.root.remove(pixelId)\n      }\n      activeWheelPixels.clear()\n    },\n  })\n\n  // Tab: Basics\n  tabController.addTab({\n    title: \"Basics\",\n    init: (tabGroup) => {\n      const title = new TextRenderable(renderer, {\n        id: \"opentui-title\",\n        content: \"Basic CLI Renderer Demo\",\n        position: \"absolute\",\n        left: 10,\n        top: 5,\n        fg: \"#FFFF00\",\n        attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(title)\n\n      const box1 = new BoxRenderable(renderer, {\n        id: \"box1\",\n        position: \"absolute\",\n        left: 10,\n        top: 8,\n        width: 20,\n        height: 8,\n        backgroundColor: \"#333366\",\n        zIndex: 0,\n        borderStyle: \"single\",\n        borderColor: \"#FFFFFF\",\n        border: true,\n      })\n      tabGroup.add(box1)\n\n      const box1Title = new TextRenderable(renderer, {\n        id: \"box1-title\",\n        content: \"Simple Box\",\n        position: \"absolute\",\n        left: 12,\n        top: 10,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(box1Title)\n\n      const box2 = new BoxRenderable(renderer, {\n        id: \"box2\",\n        position: \"absolute\",\n        left: 35,\n        top: 10,\n        width: 25,\n        height: 6,\n        backgroundColor: \"#663333\",\n        zIndex: 1,\n        borderStyle: \"double\",\n        borderColor: \"#FFFF00\",\n        border: true,\n      })\n      tabGroup.add(box2)\n\n      const box2Title = new TextRenderable(renderer, {\n        id: \"box2-title\",\n        content: \"Double Border Box\",\n        position: \"absolute\",\n        left: 37,\n        top: 12,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(box2Title)\n\n      const description = new TextRenderable(renderer, {\n        id: \"description\",\n        content: \"This tab demonstrates basic box and text rendering with different border styles.\",\n        position: \"absolute\",\n        left: 10,\n        top: 18,\n        fg: \"#CCCCCC\",\n        zIndex: 10,\n      })\n      tabGroup.add(description)\n\n      const cursorInfo = new TextRenderable(renderer, {\n        id: \"cursor-info\",\n        content: \"Cursor: (0,0) - Style: block\",\n        position: \"absolute\",\n        left: 10,\n        top: 20,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(cursorInfo)\n    },\n    update: (deltaMs: number, tabGroup: BoxRenderable) => {\n      // Update cursor position (make it move in a small circle)\n      const cursorTime = Date.now() / 1000\n      const cursorX = 15 + Math.floor(3 * Math.cos(cursorTime))\n      const cursorY = 13 + Math.floor(2 * Math.sin(cursorTime))\n\n      // Change cursor style every few seconds\n      const cursorStyleIndex = Math.floor(cursorTime / 2) % 6\n      let cursorStyle: \"block\" | \"line\" | \"underline\" = \"block\"\n      let cursorBlinking = false\n\n      switch (cursorStyleIndex) {\n        case 0:\n          cursorStyle = \"block\"\n          cursorBlinking = false\n          break\n        case 1:\n          cursorStyle = \"block\"\n          cursorBlinking = true\n          break\n        case 2:\n          cursorStyle = \"line\"\n          cursorBlinking = false\n          break\n        case 3:\n          cursorStyle = \"line\"\n          cursorBlinking = true\n          break\n        case 4:\n          cursorStyle = \"underline\"\n          cursorBlinking = false\n          break\n        case 5:\n          cursorStyle = \"underline\"\n          cursorBlinking = true\n          break\n      }\n\n      renderer.setCursorStyle({ style: cursorStyle, blinking: cursorBlinking })\n      renderer.setCursorPosition(cursorX, cursorY)\n\n      // Display cursor position and style info\n      const cursorInfo = tabGroup.getRenderable(\"cursor-info\") as TextRenderable\n      if (cursorInfo) {\n        cursorInfo.content = `Cursor: (${cursorX},${cursorY}) - Style: ${cursorStyle}${cursorBlinking ? \" (blinking)\" : \"\"}`\n      }\n    },\n    show: () => {\n      renderer.setCursorPosition(15, 13, true)\n    },\n    hide: () => {\n      renderer.setCursorPosition(0, 0, false)\n    },\n  })\n\n  // Tab: Borders\n  let partialBorderPhase = 0\n  tabController.addTab({\n    title: \"Borders\",\n    init: (tabGroup) => {\n      const borderTitle = new TextRenderable(renderer, {\n        id: \"border-title\",\n        content: \"Border Styles & Partial Borders\",\n        position: \"absolute\",\n        left: 10,\n        top: 5,\n        fg: \"#FFFF00\",\n        attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(borderTitle)\n\n      // Different border styles\n      const singleBox = new BoxRenderable(renderer, {\n        id: \"single-box\",\n        position: \"absolute\",\n        left: 10,\n        top: 8,\n        width: 15,\n        height: 5,\n        backgroundColor: \"#222244\",\n        zIndex: 0,\n        borderStyle: \"single\",\n        borderColor: \"#FFFFFF\",\n        border: true,\n      })\n      tabGroup.add(singleBox)\n      const singleLabel = new TextRenderable(renderer, {\n        id: \"single-label\",\n        content: \"Single\",\n        position: \"absolute\",\n        left: 12,\n        top: 10,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(singleLabel)\n\n      const doubleBox = new BoxRenderable(renderer, {\n        id: \"double-box\",\n        position: \"absolute\",\n        left: 30,\n        top: 8,\n        width: 15,\n        height: 5,\n        backgroundColor: \"#442222\",\n        zIndex: 0,\n        borderStyle: \"double\",\n        borderColor: \"#FFFFFF\",\n        border: true,\n      })\n      tabGroup.add(doubleBox)\n      const doubleLabel = new TextRenderable(renderer, {\n        id: \"double-label\",\n        content: \"Double\",\n        position: \"absolute\",\n        left: 32,\n        top: 10,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(doubleLabel)\n\n      const roundedBox = new BoxRenderable(renderer, {\n        id: \"rounded-box\",\n        position: \"absolute\",\n        left: 50,\n        top: 8,\n        width: 15,\n        height: 5,\n        backgroundColor: \"#224422\",\n        zIndex: 0,\n        borderStyle: \"rounded\",\n        borderColor: \"#FFFFFF\",\n        border: true,\n      })\n      tabGroup.add(roundedBox)\n      const roundedLabel = new TextRenderable(renderer, {\n        id: \"rounded-label\",\n        content: \"Rounded\",\n        position: \"absolute\",\n        left: 52,\n        top: 10,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(roundedLabel)\n\n      // Partial borders\n      const partialTitle = new TextRenderable(renderer, {\n        id: \"partial-title\",\n        content: \"Partial Borders:\",\n        position: \"absolute\",\n        left: 10,\n        top: 15,\n        fg: \"#CCCCCC\",\n        attributes: TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(partialTitle)\n\n      const partialLeft = new BoxRenderable(renderer, {\n        id: \"partial-left\",\n        position: \"absolute\",\n        left: 10,\n        top: 17,\n        width: 12,\n        height: 4,\n        backgroundColor: \"#222244\",\n        zIndex: 0,\n        borderStyle: \"single\",\n        borderColor: \"#FFFFFF\",\n        border: [\"left\"],\n      })\n      tabGroup.add(partialLeft)\n      const partialLeftLabel = new TextRenderable(renderer, {\n        id: \"partial-left-label\",\n        content: \"Left Only\",\n        position: \"absolute\",\n        left: 12,\n        top: 18,\n        fg: \"#FFFFFF\",\n        zIndex: 10,\n      })\n      tabGroup.add(partialLeftLabel)\n\n      const partialAnimated = new BoxRenderable(renderer, {\n        id: \"partial-animated\",\n        position: \"absolute\",\n        left: 30,\n        top: 17,\n        width: 20,\n        height: 4,\n        backgroundColor: \"#334455\",\n        zIndex: 0,\n        borderStyle: \"single\",\n        borderColor: \"#FFFFFF\",\n        border: true,\n      })\n      tabGroup.add(partialAnimated)\n      const partialAnimatedLabel = new TextRenderable(renderer, {\n        id: \"partial-animated-label\",\n        content: \"Animated Borders\",\n        position: \"absolute\",\n        left: 32,\n        top: 18,\n        fg: \"#FFFFFF\",\n        zIndex: 10,\n      })\n      tabGroup.add(partialAnimatedLabel)\n\n      const partialPhase = new TextRenderable(renderer, {\n        id: \"partial-phase\",\n        content: \"Phase: 1/8\",\n        position: \"absolute\",\n        left: 30,\n        top: 22,\n        fg: \"#AAAAAA\",\n        zIndex: 10,\n      })\n      tabGroup.add(partialPhase)\n\n      const customBorderTitle = new TextRenderable(renderer, {\n        id: \"custom-border-title\",\n        content: \"Custom Border Characters:\",\n        position: \"absolute\",\n        left: 10,\n        top: 25,\n        fg: \"#CCCCCC\",\n        attributes: TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(customBorderTitle)\n\n      const asciiBorders: BorderCharacters = {\n        topLeft: \"+\",\n        topRight: \"+\",\n        bottomLeft: \"+\",\n        bottomRight: \"+\",\n        horizontal: \"-\",\n        vertical: \"|\",\n        topT: \"+\",\n        bottomT: \"+\",\n        leftT: \"+\",\n        rightT: \"+\",\n        cross: \"+\",\n      }\n\n      const blockBorders: BorderCharacters = {\n        topLeft: \"█\",\n        topRight: \"█\",\n        bottomLeft: \"█\",\n        bottomRight: \"█\",\n        horizontal: \"█\",\n        vertical: \"█\",\n        topT: \"█\",\n        bottomT: \"█\",\n        leftT: \"█\",\n        rightT: \"█\",\n        cross: \"█\",\n      }\n\n      const starBorders: BorderCharacters = {\n        topLeft: \"*\",\n        topRight: \"*\",\n        bottomLeft: \"*\",\n        bottomRight: \"*\",\n        horizontal: \"*\",\n        vertical: \"*\",\n        topT: \"*\",\n        bottomT: \"*\",\n        leftT: \"*\",\n        rightT: \"*\",\n        cross: \"*\",\n      }\n\n      const asciiBox = new BoxRenderable(renderer, {\n        id: \"ascii-box\",\n        position: \"absolute\",\n        left: 10,\n        top: 27,\n        width: 15,\n        height: 5,\n        backgroundColor: \"#222244\",\n        zIndex: 0,\n        borderStyle: \"single\",\n        borderColor: \"#FFFFFF\",\n        customBorderChars: asciiBorders,\n        border: true,\n      })\n      tabGroup.add(asciiBox)\n      const asciiLabel = new TextRenderable(renderer, {\n        id: \"ascii-label\",\n        content: \"ASCII Border\",\n        position: \"absolute\",\n        left: 12,\n        top: 29,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(asciiLabel)\n\n      const blockBox = new BoxRenderable(renderer, {\n        id: \"block-box\",\n        position: \"absolute\",\n        left: 30,\n        top: 27,\n        width: 15,\n        height: 5,\n        backgroundColor: \"#442222\",\n        zIndex: 0,\n        borderStyle: \"single\",\n        borderColor: \"#FFFFFF\",\n        border: true,\n      })\n      blockBox.customBorderChars = blockBorders\n      tabGroup.add(blockBox)\n      const blockLabel = new TextRenderable(renderer, {\n        id: \"block-label\",\n        content: \"Block Border\",\n        position: \"absolute\",\n        left: 32,\n        top: 29,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(blockLabel)\n\n      const starBox = new BoxRenderable(renderer, {\n        id: \"star-box\",\n        position: \"absolute\",\n        left: 50,\n        top: 27,\n        width: 15,\n        height: 5,\n        backgroundColor: \"#224422\",\n        zIndex: 0,\n        borderStyle: \"single\",\n        borderColor: \"#FFFFFF\",\n        customBorderChars: starBorders,\n        border: true,\n      })\n      tabGroup.add(starBox)\n      const starLabel = new TextRenderable(renderer, {\n        id: \"star-label\",\n        content: \"Star Border\",\n        position: \"absolute\",\n        left: 52,\n        top: 29,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(starLabel)\n    },\n    update: (deltaMs: number, tabGroup: BoxRenderable) => {\n      // Animate partial borders\n      const time = Date.now() / 1000\n      const phase = Math.floor(time % 8)\n\n      if (phase !== partialBorderPhase) {\n        partialBorderPhase = phase\n\n        const borderSides: BorderSidesConfig = {\n          top: [0, 3, 5, 7].includes(phase),\n          right: [1, 3, 6, 7].includes(phase),\n          bottom: [2, 3, 5, 7].includes(phase),\n          left: [4, 5, 6, 7].includes(phase),\n        }\n\n        const partialAnimatedBox = tabGroup.getRenderable(\"partial-animated\") as BoxRenderable\n        if (partialAnimatedBox) {\n          partialAnimatedBox.border = getBorderFromSides(borderSides)\n          partialAnimatedBox.borderStyle = \"single\"\n        }\n\n        const partialPhaseText = tabGroup.getRenderable(\"partial-phase\") as TextRenderable\n        if (partialPhaseText) {\n          partialPhaseText.content = `Phase: ${phase + 1}/8`\n        }\n      }\n    },\n  })\n\n  // Tab: Animation\n  let animPosition = 5\n  let animDirection = 1\n  let animSpeed = 15\n  tabController.addTab({\n    title: \"Animation\",\n    init: (tabGroup) => {\n      const animTitle = new TextRenderable(renderer, {\n        id: \"anim-title\",\n        content: \"Animation Demonstrations\",\n        position: \"absolute\",\n        left: 10,\n        top: 5,\n        fg: \"#FFFF00\",\n        attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(animTitle)\n\n      const movingText = new TextRenderable(renderer, {\n        id: \"moving-text\",\n        content: \"Moving Text\",\n        position: \"absolute\",\n        left: animPosition,\n        top: 8,\n        fg: \"#00FF00\",\n        attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(movingText)\n\n      const animatedBox = new BoxRenderable(renderer, {\n        id: \"animated-box\",\n        position: \"absolute\",\n        left: animPosition,\n        top: 10,\n        width: 10,\n        height: 3,\n        backgroundColor: \"#550055\",\n        zIndex: 0,\n        borderStyle: \"rounded\",\n        borderColor: \"#FF00FF\",\n        border: true,\n      })\n      tabGroup.add(animatedBox)\n\n      const colorBox = new BoxRenderable(renderer, {\n        id: \"color-box\",\n        position: \"absolute\",\n        left: 50,\n        top: 12,\n        width: 18,\n        height: 5,\n        backgroundColor: \"#550055\",\n        zIndex: 0,\n        borderStyle: \"double\",\n        borderColor: \"#FFFFFF\",\n        border: true,\n      })\n      tabGroup.add(colorBox)\n\n      const colorBoxTitle = new TextRenderable(renderer, {\n        id: \"color-box-title\",\n        content: \"Animated Color\",\n        position: \"absolute\",\n        left: 52,\n        top: 14,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(colorBoxTitle)\n    },\n    update: (deltaMs: number, tabGroup: BoxRenderable) => {\n      // Animate moving elements\n      const deltaTime = Math.min(deltaMs / 1000, 0.1)\n      animPosition += animSpeed * animDirection * deltaTime\n\n      if (animPosition > 40) {\n        animPosition = 40\n        animDirection = -1\n      } else if (animPosition < 5) {\n        animPosition = 5\n        animDirection = 1\n      }\n\n      const x = Math.round(animPosition)\n\n      const movingText = tabGroup.getRenderable(\"moving-text\") as TextRenderable\n      if (movingText) {\n        movingText.setPosition({ left: x, top: 8 })\n      }\n\n      const animatedBox = tabGroup.getRenderable(\"animated-box\") as BoxRenderable\n      if (animatedBox) {\n        animatedBox.setPosition({ left: x, top: 10 })\n      }\n\n      // Animate color-changing box\n      const time = Date.now() / 1000\n      const hue = (time * 30) % 360\n      const color = hsvToRgb(hue, 1, 0.7)\n      const hexColor = rgbToHex(color)\n\n      const colorBox = tabGroup.getRenderable(\"color-box\") as BoxRenderable\n      if (colorBox) {\n        colorBox.backgroundColor = parseColor(hexColor)\n      }\n    },\n  })\n\n  // Tab: Titles\n  tabController.addTab({\n    title: \"Titles\",\n    init: (tabGroup) => {\n      const layoutTitle = new TextRenderable(renderer, {\n        id: \"layout-title\",\n        content: \"Box Titles\",\n        position: \"absolute\",\n        left: 10,\n        top: 5,\n        fg: \"#FFFF00\",\n        attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(layoutTitle)\n\n      // Boxes with titles and different alignments\n      const titledLeft = new BoxRenderable(renderer, {\n        id: \"titled-left\",\n        position: \"absolute\",\n        left: 10,\n        top: 8,\n        width: 20,\n        height: 5,\n        backgroundColor: \"#222244\",\n        zIndex: 0,\n        borderStyle: \"single\",\n        borderColor: \"#FFFFFF\",\n        title: \"Left Aligned\",\n        titleAlignment: \"left\",\n        border: true,\n      })\n      tabGroup.add(titledLeft)\n\n      const titledCenter = new BoxRenderable(renderer, {\n        id: \"titled-center\",\n        position: \"absolute\",\n        left: 35,\n        top: 8,\n        width: 20,\n        height: 5,\n        backgroundColor: \"#442222\",\n        zIndex: 0,\n        borderStyle: \"double\",\n        borderColor: \"#FFFFFF\",\n        title: \"Centered Title\",\n        titleAlignment: \"center\",\n        border: true,\n      })\n      tabGroup.add(titledCenter)\n\n      const titledRight = new BoxRenderable(renderer, {\n        id: \"titled-right\",\n        position: \"absolute\",\n        left: 60,\n        top: 8,\n        width: 20,\n        height: 5,\n        backgroundColor: \"#224422\",\n        zIndex: 0,\n        borderStyle: \"rounded\",\n        borderColor: \"#FFFFFF\",\n        title: \"Right Aligned\",\n        titleAlignment: \"right\",\n        border: true,\n      })\n      tabGroup.add(titledRight)\n    },\n  })\n\n  // Tab: Interactive\n  const interactiveBorderSides = {\n    top: true,\n    right: true,\n    bottom: true,\n    left: true,\n  }\n\n  tabController.addTab({\n    title: \"Interactive\",\n    init: (tabGroup) => {\n      const interactiveTitle = new TextRenderable(renderer, {\n        id: \"interactive-title\",\n        content: \"Interactive Controls\",\n        position: \"absolute\",\n        left: 10,\n        top: 5,\n        fg: \"#FFFF00\",\n        attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(interactiveTitle)\n\n      const interactiveBorder = new BoxRenderable(renderer, {\n        id: \"interactive-border\",\n        position: \"absolute\",\n        left: 15,\n        top: 8,\n        width: 40,\n        height: 8,\n        backgroundColor: \"#333344\",\n        zIndex: 0,\n        borderStyle: \"double\",\n        borderColor: \"#FFFFFF\",\n        border: true,\n      })\n      tabGroup.add(interactiveBorder)\n\n      const interactiveLabel = new TextRenderable(renderer, {\n        id: \"interactive-label\",\n        content: \"Press keys to toggle borders\",\n        position: \"absolute\",\n        left: 22,\n        top: 12,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.BOLD,\n        zIndex: 10,\n      })\n      tabGroup.add(interactiveLabel)\n\n      const interactiveInstructions = new TextRenderable(renderer, {\n        id: \"interactive-instructions\",\n        content: \"Keyboard Controls:\",\n        position: \"absolute\",\n        left: 10,\n        top: 18,\n        fg: \"#FFFFFF\",\n        attributes: TextAttributes.UNDERLINE,\n        zIndex: 10,\n      })\n      tabGroup.add(interactiveInstructions)\n\n      const keyT = new TextRenderable(renderer, {\n        id: \"key-t\",\n        content: \"T - Toggle top border\",\n        position: \"absolute\",\n        left: 10,\n        top: 19,\n        fg: \"#CCCCCC\",\n        zIndex: 10,\n      })\n      tabGroup.add(keyT)\n\n      const keyR = new TextRenderable(renderer, {\n        id: \"key-r\",\n        content: \"R - Toggle right border\",\n        position: \"absolute\",\n        left: 10,\n        top: 20,\n        fg: \"#CCCCCC\",\n        zIndex: 10,\n      })\n      tabGroup.add(keyR)\n\n      const keyB = new TextRenderable(renderer, {\n        id: \"key-b\",\n        content: \"B - Toggle bottom border\",\n        position: \"absolute\",\n        left: 10,\n        top: 21,\n        fg: \"#CCCCCC\",\n        zIndex: 10,\n      })\n      tabGroup.add(keyB)\n\n      const keyL = new TextRenderable(renderer, {\n        id: \"key-l\",\n        content: \"L - Toggle left border\",\n        position: \"absolute\",\n        left: 10,\n        top: 22,\n        fg: \"#CCCCCC\",\n        zIndex: 10,\n      })\n      tabGroup.add(keyL)\n\n      const borderState = new TextRenderable(renderer, {\n        id: \"border-state\",\n        content: \"Active borders: All\",\n        position: \"absolute\",\n        left: 10,\n        top: 24,\n        fg: \"#AAAAAA\",\n        zIndex: 10,\n      })\n      tabGroup.add(borderState)\n    },\n    update: (deltaMs: number, tabGroup: BoxRenderable) => {\n      // Update interactive border state\n      const interactiveBorder = tabGroup.getRenderable(\"interactive-border\") as BoxRenderable\n      if (interactiveBorder) {\n        interactiveBorder.border = getBorderFromSides(interactiveBorderSides)\n      }\n\n      let borderDesc = \"\"\n      if (interactiveBorderSides.top) borderDesc += \"Top \"\n      if (interactiveBorderSides.right) borderDesc += \"Right \"\n      if (interactiveBorderSides.bottom) borderDesc += \"Bottom \"\n      if (interactiveBorderSides.left) borderDesc += \"Left \"\n      if (!borderDesc) borderDesc = \"None\"\n\n      const borderState = tabGroup.getRenderable(\"border-state\") as TextRenderable\n      if (borderState) {\n        borderState.content = `Active borders: ${borderDesc}`\n      }\n    },\n  })\n\n  tabController.focus()\n\n  globalKeyboardHandler = (key: KeyEvent) => {\n    // Interactive border controls (only active in Interactive tab)\n    if (tabController.getCurrentTab().title === \"Interactive\") {\n      if (key.name === \"t\" || key.name === \"T\") {\n        interactiveBorderSides.top = !interactiveBorderSides.top\n      } else if (key.name === \"r\" || key.name === \"R\") {\n        interactiveBorderSides.right = !interactiveBorderSides.right\n      } else if (key.name === \"b\" || key.name === \"B\") {\n        interactiveBorderSides.bottom = !interactiveBorderSides.bottom\n      } else if (key.name === \"l\" || key.name === \"L\") {\n        interactiveBorderSides.left = !interactiveBorderSides.left\n      }\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", globalKeyboardHandler)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n\n  if (globalKeyboardHandler) {\n    renderer.keyInput.off(\"keypress\", globalKeyboardHandler)\n    globalKeyboardHandler = null\n  }\n\n  if (globalTabController) {\n    renderer.root.remove(globalTabController.id)\n    globalTabController = null\n  }\n\n  renderer.setCursorPosition(0, 0, false)\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/physx-planck-2d-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  TextRenderable,\n  FrameBufferRenderable,\n  BoxRenderable,\n  createCliRenderer,\n  type KeyEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport * as THREE from \"three\"\nimport {\n  SpriteAnimator,\n  TiledSprite,\n  type SpriteDefinition,\n  type AnimationDefinition,\n} from \"../3d/animation/SpriteAnimator.js\"\nimport { SpriteResourceManager, type ResourceConfig } from \"../3d/SpriteResourceManager.js\"\nimport { PhysicsExplosionManager, type PhysicsExplosionHandle } from \"../3d/animation/PhysicsExplodingSpriteEffect.js\"\nimport { PlanckPhysicsWorld } from \"../3d/physics/PlanckPhysicsAdapter.js\"\nimport * as planck from \"planck\"\nimport { ThreeCliRenderer } from \"../3d.js\"\n\n// @ts-ignore\nimport cratePath from \"./assets/crate.png\" with { type: \"image/png\" }\n\ninterface PhysicsBox {\n  rigidBody: planck.Body\n  sprite: TiledSprite\n  width: number\n  height: number\n  id: string\n}\n\ninterface PhysicsWorld {\n  world: planck.World\n  ground: planck.Body\n  boxes: PhysicsBox[]\n}\n\ninterface DemoState {\n  engine: ThreeCliRenderer\n  scene: THREE.Scene\n  camera: THREE.OrthographicCamera\n  resourceManager: SpriteResourceManager\n  spriteAnimator: SpriteAnimator\n  physicsExplosionManager: PhysicsExplosionManager\n  physicsWorld: PhysicsWorld\n  activeExplosionHandles: PhysicsExplosionHandle[]\n  isInitialized: boolean\n  boxIdCounter: number\n  lastSpawnTime: number\n  boxSpawnCount: number\n  maxInstancesReached: boolean\n  crateResource: any\n  crateDef: SpriteDefinition\n  parentContainer: BoxRenderable\n  instructionsText: TextRenderable\n  controlsText: TextRenderable\n  statsText: TextRenderable\n  frameCallback: (deltaTime: number) => Promise<void>\n  keyHandler: (key: KeyEvent) => void\n  statsInterval: NodeJS.Timeout\n  resizeHandler: (width: number, height: number) => void\n}\n\nlet demoState: DemoState | null = null\n\nconst spawnInterval = 800\nconst orthoViewHeight = 20.0\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const initialTermWidth = renderer.terminalWidth\n  const initialTermHeight = renderer.terminalHeight\n\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"planck-container\",\n    zIndex: 15,\n    visible: true,\n  })\n  renderer.root.add(parentContainer)\n\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"planck-main\",\n    width: initialTermWidth,\n    height: initialTermHeight,\n    zIndex: 10,\n  })\n  renderer.root.add(framebufferRenderable)\n  const { frameBuffer: framebuffer } = framebufferRenderable\n\n  const engine = new ThreeCliRenderer(renderer, {\n    width: initialTermWidth,\n    height: initialTermHeight,\n    focalLength: 1,\n  })\n\n  await engine.init()\n\n  const scene = new THREE.Scene()\n\n  const orthoViewWidth = orthoViewHeight * engine.aspectRatio\n  const camera = new THREE.OrthographicCamera(\n    orthoViewWidth / -2,\n    orthoViewWidth / 2,\n    orthoViewHeight / 2,\n    orthoViewHeight / -2,\n    0.1,\n    1000,\n  )\n  camera.position.set(0, 0, 5)\n  camera.lookAt(0, 0, 0)\n  scene.add(camera)\n\n  engine.setActiveCamera(camera)\n\n  const resourceManager = new SpriteResourceManager(scene)\n  const spriteAnimator = new SpriteAnimator(scene)\n\n  const crateResourceConfig: ResourceConfig = {\n    imagePath: cratePath,\n    sheetNumFrames: 1,\n  }\n\n  const crateResource = await resourceManager.createResource(crateResourceConfig)\n\n  const crateIdleAnimation: AnimationDefinition = {\n    resource: crateResource,\n    frameDuration: 1000,\n  }\n\n  const crateDef: SpriteDefinition = {\n    initialAnimation: \"idle\",\n    animations: {\n      idle: crateIdleAnimation,\n    },\n    scale: 1.0,\n  }\n\n  // Initialize physics\n  const gravity = planck.Vec2(0.0, -9.81)\n  const world = planck.World(gravity)\n\n  const groundShape = planck.Box(15.0, 0.2)\n  const ground = world.createBody({\n    position: planck.Vec2(0.0, -8.0),\n  })\n  ground.createFixture({\n    shape: groundShape,\n  })\n\n  const physicsWorld: PhysicsWorld = {\n    world,\n    ground,\n    boxes: [],\n  }\n\n  const physicsExplosionManager = new PhysicsExplosionManager(scene, PlanckPhysicsWorld.createFromPlanckWorld(world))\n\n  // Setup lighting\n  const ambientLight = new THREE.AmbientLight(0xffffff, 1.2)\n  scene.add(ambientLight)\n\n  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5)\n  directionalLight.position.set(5, 10, 5)\n  directionalLight.castShadow = false\n  scene.add(directionalLight)\n\n  const groundGeometry = new THREE.BoxGeometry(30, 0.4, 0.2)\n  const groundMaterial = new THREE.MeshPhongMaterial({\n    color: 0x666666,\n    transparent: true,\n    opacity: 0.8,\n  })\n  const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial)\n  groundMesh.position.set(0, -8, -0.5)\n  scene.add(groundMesh)\n\n  // Create UI elements\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"planck-instructions\",\n    content: \"Planck.js 2D Demo - Falling Crates (Instanced Sprites)\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(instructionsText)\n\n  const controlsText = new TextRenderable(renderer, {\n    id: \"planck-controls\",\n    content: \"Press: [Space] spawn crate, [E] explode crate, [R] reset, [T] toggle debug, [C] clear crates\",\n    position: \"absolute\",\n    left: 1,\n    top: 2,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(controlsText)\n\n  const statsText = new TextRenderable(renderer, {\n    id: \"planck-stats\",\n    content: \"\",\n    position: \"absolute\",\n    left: 1,\n    top: 3,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(statsText)\n\n  const state: DemoState = {\n    engine,\n    scene,\n    camera,\n    resourceManager,\n    spriteAnimator,\n    physicsExplosionManager,\n    physicsWorld,\n    activeExplosionHandles: [],\n    isInitialized: true,\n    boxIdCounter: 0,\n    lastSpawnTime: 0,\n    boxSpawnCount: 0,\n    maxInstancesReached: false,\n    crateResource,\n    crateDef,\n    parentContainer,\n    instructionsText,\n    controlsText,\n    statsText,\n    frameCallback: async () => {},\n    keyHandler: () => {},\n    statsInterval: setInterval(() => {}, 100),\n    resizeHandler: () => {},\n  }\n\n  async function createBox(\n    x: number,\n    y: number,\n    width: number = 1.0,\n    height: number = 1.0,\n  ): Promise<PhysicsBox | null> {\n    if (!state.isInitialized) return null\n\n    const bodyDef: planck.BodyDef = {\n      type: \"dynamic\",\n      position: planck.Vec2(x, y),\n      angle: Math.random() * 0.5 - 0.25,\n    }\n\n    const rigidBody = state.physicsWorld.world.createBody(bodyDef)\n\n    const shape = planck.Box(width * 0.6, height * 0.6)\n    rigidBody.createFixture({\n      shape: shape,\n      density: 1.0,\n      friction: 0.7,\n      restitution: 0.3,\n    })\n\n    const id = `box_${state.boxIdCounter++}`\n\n    try {\n      const sprite = await state.spriteAnimator.createSprite({\n        ...state.crateDef,\n        id: id,\n      })\n\n      const spriteScale = Math.min(width, height) * 1.2\n      sprite.setScale(new THREE.Vector3(spriteScale, spriteScale, spriteScale))\n      sprite.setPosition(new THREE.Vector3(x, y, 0))\n\n      const box: PhysicsBox = {\n        rigidBody,\n        sprite,\n        width,\n        height,\n        id,\n      }\n\n      state.physicsWorld.boxes.push(box)\n      return box\n    } catch (error) {\n      state.physicsWorld.world.destroyBody(rigidBody)\n      console.warn(`Failed to create crate sprite: ${error instanceof Error ? error.message : String(error)}`)\n      return null\n    }\n  }\n\n  async function explodeRandomCrate(): Promise<void> {\n    if (!state.isInitialized || state.physicsWorld.boxes.length === 0) return\n\n    const randomIndex = Math.floor(Math.random() * state.physicsWorld.boxes.length)\n    const boxToExplode = state.physicsWorld.boxes[randomIndex]\n\n    state.physicsWorld.world.destroyBody(boxToExplode.rigidBody)\n    state.physicsWorld.boxes.splice(randomIndex, 1)\n\n    const explosionHandle = await state.physicsExplosionManager.createExplosionForSprite(boxToExplode.sprite, {\n      numRows: 4,\n      numCols: 4,\n      explosionForce: 2.0,\n      forceVariation: 0.4,\n      torqueStrength: 2.0,\n      durationMs: 5000,\n      fadeOut: false,\n      linearDamping: 1.2,\n      angularDamping: 0.8,\n      restitution: 0.3,\n      friction: 0.9,\n      density: 1.2,\n    })\n\n    if (explosionHandle) {\n      state.activeExplosionHandles.push(explosionHandle)\n      console.log(\"💥 Crate exploded!\")\n    }\n  }\n\n  function updatePhysics(deltaTime: number): void {\n    if (!state.isInitialized) return\n\n    state.physicsWorld.world.step(deltaTime / 1000, 8, 3)\n\n    for (const box of state.physicsWorld.boxes) {\n      const position = box.rigidBody.getPosition()\n      const rotation = box.rigidBody.getAngle()\n\n      box.sprite.setPosition(new THREE.Vector3(position.x, position.y, 0))\n      box.sprite.setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), rotation))\n    }\n\n    state.physicsWorld.boxes = state.physicsWorld.boxes.filter((box) => {\n      const pos = box.rigidBody.getPosition()\n      if (pos.y < -15) {\n        box.sprite.destroy()\n        state.physicsWorld.world.destroyBody(box.rigidBody)\n        return false\n      }\n      return true\n    })\n  }\n\n  state.frameCallback = async (deltaTime: number) => {\n    const currentTime = Date.now()\n\n    if (\n      state.isInitialized &&\n      currentTime - state.lastSpawnTime > spawnInterval &&\n      state.boxSpawnCount < 100 &&\n      !state.maxInstancesReached\n    ) {\n      const x = (Math.random() - 0.5) * 16\n      const y = 8 + Math.random() * 2\n      const size = 0.8 + Math.random() * 1.2\n\n      const newBox = await createBox(x, y, size, size)\n      if (newBox) {\n        state.lastSpawnTime = currentTime\n        state.boxSpawnCount++\n      } else {\n        state.maxInstancesReached = true\n      }\n    }\n\n    updatePhysics(deltaTime)\n    state.spriteAnimator.update(deltaTime)\n    if (state.physicsExplosionManager) {\n      state.physicsExplosionManager.update(deltaTime)\n    }\n    await state.engine.drawScene(state.scene, framebuffer, deltaTime)\n  }\n\n  state.keyHandler = (key: KeyEvent) => {\n    const keyStr = key.name\n\n    if (keyStr === \"space\" && state.isInitialized) {\n      ;(async () => {\n        const x = (Math.random() - 0.5) * 16\n        const y = 8 + Math.random() * 2\n        const size = 0.8 + Math.random() * 1.2\n\n        const newBox = await createBox(x, y, size, size)\n        if (newBox) {\n          console.log(\"Crate spawned manually!\")\n        } else {\n          state.maxInstancesReached = true\n          console.log(\"Cannot spawn crate - maximum instances reached!\")\n        }\n      })()\n    }\n\n    if (keyStr === \"e\" && state.isInitialized) {\n      explodeRandomCrate()\n    }\n\n    if (keyStr === \"r\" && state.isInitialized) {\n      for (const box of state.physicsWorld.boxes) {\n        box.sprite.destroy()\n        state.physicsWorld.world.destroyBody(box.rigidBody)\n      }\n      state.physicsWorld.boxes = []\n      state.boxSpawnCount = 0\n\n      state.physicsExplosionManager.disposeAll()\n      state.activeExplosionHandles.length = 0\n\n      console.log(\"Physics world reset!\")\n    }\n\n    if (keyStr === \"c\" && state.isInitialized) {\n      for (const box of state.physicsWorld.boxes) {\n        box.sprite.destroy()\n        state.physicsWorld.world.destroyBody(box.rigidBody)\n      }\n      state.physicsWorld.boxes = []\n      state.boxSpawnCount = 0\n\n      state.physicsExplosionManager.disposeAll()\n      state.activeExplosionHandles.length = 0\n\n      console.log(\"All crates cleared!\")\n    }\n\n    if (keyStr === \"b\" && state.isInitialized) {\n      console.log(\"Spawning burst of crates!\")\n      ;(async () => {\n        for (let i = 0; i < 10; i++) {\n          const x = (Math.random() - 0.5) * 12\n          const y = 8 + Math.random() * 4\n          const size = 0.6 + Math.random() * 1.0\n\n          const newBox = await createBox(x, y, size, size)\n          if (!newBox) {\n            state.maxInstancesReached = true\n            console.log(`Burst stopped at ${i + 1} crates - maximum instances reached!`)\n            break\n          }\n          await new Promise((resolve) => setTimeout(resolve, 50))\n        }\n      })()\n    }\n  }\n\n  state.resizeHandler = (newWidth: number, newHeight: number) => {\n    framebuffer.resize(newWidth, newHeight)\n\n    const newOrthoViewWidth = orthoViewHeight * state.engine.aspectRatio\n    state.camera.left = newOrthoViewWidth / -2\n    state.camera.right = newOrthoViewWidth / 2\n    state.camera.top = orthoViewHeight / 2\n    state.camera.bottom = orthoViewHeight / -2\n    state.camera.updateProjectionMatrix()\n  }\n\n  state.statsInterval = setInterval(() => {\n    if (state.isInitialized) {\n      const explosionCount = state.activeExplosionHandles.filter((h) => !h.hasBeenRestored).length\n      state.statsText.content = `Crates: ${state.physicsWorld.boxes.length} | Explosions: ${explosionCount} | Press [B] for burst spawn`\n    }\n  }, 100)\n\n  // Register handlers\n  renderer.setFrameCallback(state.frameCallback)\n  renderer.keyInput.on(\"keypress\", state.keyHandler)\n  renderer.on(\"resize\", state.resizeHandler)\n\n  demoState = state\n  console.log(\"Planck physics demo initialized!\")\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (!demoState) return\n\n  renderer.removeFrameCallback(demoState.frameCallback)\n  renderer.keyInput.off(\"keypress\", demoState.keyHandler)\n  renderer.root.removeListener(\"resize\", demoState.resizeHandler)\n\n  clearInterval(demoState.statsInterval)\n\n  for (const box of demoState.physicsWorld.boxes) {\n    box.sprite.destroy()\n    demoState.physicsWorld.world.destroyBody(box.rigidBody)\n  }\n\n  demoState.physicsExplosionManager.disposeAll()\n  demoState.engine.destroy()\n\n  renderer.root.remove(\"planck-main\")\n  renderer.root.remove(\"planck-container\")\n\n  demoState = null\n  console.log(\"Planck physics demo cleaned up!\")\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/physx-rapier-2d-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  TextRenderable,\n  FrameBufferRenderable,\n  BoxRenderable,\n  createCliRenderer,\n  type KeyEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport * as THREE from \"three\"\nimport {\n  SpriteAnimator,\n  TiledSprite,\n  type SpriteDefinition,\n  type AnimationDefinition,\n} from \"../3d/animation/SpriteAnimator.js\"\nimport { SpriteResourceManager, type ResourceConfig } from \"../3d/SpriteResourceManager.js\"\nimport { PhysicsExplosionManager, type PhysicsExplosionHandle } from \"../3d/animation/PhysicsExplodingSpriteEffect.js\"\nimport { RapierPhysicsWorld } from \"../3d/physics/RapierPhysicsAdapter.js\"\nimport RAPIER from \"@dimforge/rapier2d-simd-compat\"\nimport { MeshLambertNodeMaterial } from \"three/webgpu\"\nimport { ThreeCliRenderer } from \"../3d.js\"\n\n// @ts-ignore\nimport cratePath from \"./assets/concrete.png\" with { type: \"image/png\" }\n\nconst SUBDIVISION = 4\nconst DENSITY = 2.2\nconst EXPLOSION_FORCE = 2.0\nconst EXPLOSION_FORCE_VARIATION = 0.2\nconst TORQUE_STRENGTH = 2.0\n\ninterface PhysicsBox {\n  rigidBody: RAPIER.RigidBody\n  sprite: TiledSprite\n  width: number\n  height: number\n  id: string\n}\n\ninterface PhysicsWorld {\n  world: RAPIER.World\n  ground: RAPIER.Collider\n  boxes: PhysicsBox[]\n}\n\ninterface DemoState {\n  engine: ThreeCliRenderer\n  scene: THREE.Scene\n  camera: THREE.OrthographicCamera\n  resourceManager: SpriteResourceManager\n  spriteAnimator: SpriteAnimator\n  physicsExplosionManager: PhysicsExplosionManager\n  physicsWorld: PhysicsWorld\n  activeExplosionHandles: PhysicsExplosionHandle[]\n  isInitialized: boolean\n  boxIdCounter: number\n  lastSpawnTime: number\n  boxSpawnCount: number\n  maxInstancesReached: boolean\n  crateResource: any\n  crateDef: SpriteDefinition\n  parentContainer: BoxRenderable\n  instructionsText: TextRenderable\n  controlsText: TextRenderable\n  statsText: TextRenderable\n  frameCallback: (deltaTime: number) => Promise<void>\n  keyHandler: (key: KeyEvent) => void\n  statsInterval: NodeJS.Timeout\n  resizeHandler: (width: number, height: number) => void\n}\n\nlet demoState: DemoState | null = null\n\nconst spawnInterval = 800\nconst orthoViewHeight = 20.0\n\nconst materialFactory = () =>\n  new MeshLambertNodeMaterial({\n    transparent: true,\n    alphaTest: 0.01,\n    depthWrite: false,\n  })\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const initialTermWidth = renderer.terminalWidth\n  const initialTermHeight = renderer.terminalHeight\n\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"rapier-container\",\n    zIndex: 15,\n  })\n  renderer.root.add(parentContainer)\n\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"rapier-main\",\n    width: initialTermWidth,\n    height: initialTermHeight,\n    zIndex: 10,\n  })\n  renderer.root.add(framebufferRenderable)\n  const { frameBuffer: framebuffer } = framebufferRenderable\n\n  const engine = new ThreeCliRenderer(renderer, {\n    width: initialTermWidth,\n    height: initialTermHeight,\n    focalLength: 1,\n  })\n\n  await engine.init()\n\n  const scene = new THREE.Scene()\n\n  const orthoViewWidth = orthoViewHeight * engine.aspectRatio\n  const camera = new THREE.OrthographicCamera(\n    orthoViewWidth / -2,\n    orthoViewWidth / 2,\n    orthoViewHeight / 2,\n    orthoViewHeight / -2,\n    0.1,\n    1000,\n  )\n  camera.position.set(0, 0, 5)\n  camera.lookAt(0, 0, 0)\n  scene.add(camera)\n\n  engine.setActiveCamera(camera)\n\n  const resourceManager = new SpriteResourceManager(scene)\n  const spriteAnimator = new SpriteAnimator(scene)\n\n  const crateResourceConfig: ResourceConfig = {\n    imagePath: cratePath,\n    sheetNumFrames: 1,\n  }\n\n  const crateResource = await resourceManager.createResource(crateResourceConfig)\n  const crateIdleAnimation: AnimationDefinition = {\n    resource: crateResource,\n    frameDuration: 1000,\n  }\n\n  const crateDef: SpriteDefinition = {\n    initialAnimation: \"idle\",\n    animations: {\n      idle: crateIdleAnimation,\n    },\n    scale: 1.0,\n  }\n\n  // Initialize physics\n  await RAPIER.init()\n\n  const gravity = { x: 0.0, y: -9.81 }\n  const world = new RAPIER.World(gravity)\n\n  const groundColliderDesc = RAPIER.ColliderDesc.cuboid(15.0, 0.2)\n  const ground = world.createCollider(groundColliderDesc)\n  ground.setTranslation({ x: 0.0, y: -8.0 })\n\n  const physicsWorld: PhysicsWorld = {\n    world,\n    ground,\n    boxes: [],\n  }\n\n  const physicsExplosionManager = new PhysicsExplosionManager(scene, RapierPhysicsWorld.createFromRapierWorld(world))\n  physicsExplosionManager.fillPool(crateResource, 512, { numRows: SUBDIVISION, numCols: SUBDIVISION, materialFactory })\n\n  // Setup lighting\n  const ambientLight = new THREE.AmbientLight(0x6666ff, 3.2)\n  scene.add(ambientLight)\n\n  const spotlight1 = new THREE.SpotLight(0xff9999, 6.5)\n  spotlight1.position.set(-5, 0, 6)\n  spotlight1.target.position.set(-2, -2.5, 0)\n  spotlight1.penumbra = 0.3\n  spotlight1.angle = Math.PI / 1.7\n  spotlight1.distance = 25.0\n  spotlight1.power = 500\n  scene.add(spotlight1.target)\n  scene.add(spotlight1)\n\n  const spotlight2 = spotlight1.clone()\n  spotlight2.color.set(0x99ff99)\n  spotlight2.position.set(5, 0, 6)\n  spotlight2.target.position.set(2, -2.5, 0)\n  scene.add(spotlight2.target)\n  scene.add(spotlight2)\n\n  const groundGeometry = new THREE.BoxGeometry(30, 0.4, 0.2)\n  const groundMaterial = new THREE.MeshPhongMaterial({\n    color: 0x666666,\n    transparent: true,\n    opacity: 0.8,\n  })\n  const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial)\n  groundMesh.position.set(0, -8, -0.5)\n  scene.add(groundMesh)\n\n  // Create UI elements\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"rapier-instructions\",\n    content: \"Rapier.js 2D Demo - Falling Crates (Instanced Sprites)\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(instructionsText)\n\n  const controlsText = new TextRenderable(renderer, {\n    id: \"rapier-controls\",\n    content: \"Press: [Space] spawn crate, [E] explode crate, [R] reset, [T] toggle debug, [C] clear crates\",\n    position: \"absolute\",\n    left: 1,\n    top: 2,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(controlsText)\n\n  const statsText = new TextRenderable(renderer, {\n    id: \"rapier-stats\",\n    content: \"\",\n    position: \"absolute\",\n    left: 1,\n    top: 3,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(statsText)\n\n  const state: DemoState = {\n    engine,\n    scene,\n    camera,\n    resourceManager,\n    spriteAnimator,\n    physicsExplosionManager,\n    physicsWorld,\n    activeExplosionHandles: [],\n    isInitialized: true,\n    boxIdCounter: 0,\n    lastSpawnTime: 0,\n    boxSpawnCount: 0,\n    maxInstancesReached: false,\n    crateResource,\n    crateDef,\n    parentContainer,\n    instructionsText,\n    controlsText,\n    statsText,\n    frameCallback: async () => {},\n    keyHandler: () => {},\n    statsInterval: setInterval(() => {}, 100),\n    resizeHandler: () => {},\n  }\n\n  async function createBox(\n    x: number,\n    y: number,\n    width: number = 1.0,\n    height: number = 1.0,\n  ): Promise<PhysicsBox | null> {\n    if (!state.isInitialized) return null\n\n    const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic()\n      .setTranslation(x, y)\n      .setRotation(Math.random() * 0.5 - 0.25)\n\n    const rigidBody = state.physicsWorld.world.createRigidBody(rigidBodyDesc)\n\n    const colliderDesc = RAPIER.ColliderDesc.cuboid(width * 0.6, height * 0.6)\n    state.physicsWorld.world.createCollider(colliderDesc, rigidBody)\n\n    const id = `box_${state.boxIdCounter++}`\n\n    try {\n      const sprite = await state.spriteAnimator.createSprite(\n        {\n          ...state.crateDef,\n          id: id,\n        },\n        materialFactory,\n      )\n\n      const spriteScale = Math.min(width, height) * 1.2\n      sprite.setScale(new THREE.Vector3(spriteScale, spriteScale, spriteScale))\n      sprite.setPosition(new THREE.Vector3(x, y, 0))\n\n      const box: PhysicsBox = {\n        rigidBody,\n        sprite,\n        width,\n        height,\n        id,\n      }\n\n      state.physicsWorld.boxes.push(box)\n      return box\n    } catch (error) {\n      state.physicsWorld.world.removeRigidBody(rigidBody)\n      console.warn(`Failed to create crate sprite: ${error instanceof Error ? error.message : String(error)}`)\n      return null\n    }\n  }\n\n  async function explodeRandomCrate(): Promise<void> {\n    if (!state.isInitialized || state.physicsWorld.boxes.length === 0) return\n\n    const randomIndex = Math.floor(Math.random() * state.physicsWorld.boxes.length)\n    const boxToExplode = state.physicsWorld.boxes[randomIndex]\n\n    state.physicsWorld.world.removeRigidBody(boxToExplode.rigidBody)\n    state.physicsWorld.boxes.splice(randomIndex, 1)\n\n    const explosionHandle = await state.physicsExplosionManager.createExplosionForSprite(boxToExplode.sprite, {\n      numRows: SUBDIVISION,\n      numCols: SUBDIVISION,\n      explosionForce: EXPLOSION_FORCE,\n      forceVariation: EXPLOSION_FORCE_VARIATION,\n      torqueStrength: TORQUE_STRENGTH,\n      durationMs: 10000,\n      fadeOut: false,\n      linearDamping: 1.2,\n      angularDamping: 0.8,\n      restitution: 0.3,\n      friction: 0.9,\n      density: DENSITY,\n      materialFactory,\n    })\n\n    if (explosionHandle) {\n      state.activeExplosionHandles.push(explosionHandle)\n      console.log(\"💥 Crate exploded!\")\n    }\n  }\n\n  function updatePhysics(deltaTime: number): void {\n    if (!state.isInitialized) return\n\n    state.physicsWorld.world.step()\n\n    for (const box of state.physicsWorld.boxes) {\n      const position = box.rigidBody.translation()\n      const rotation = box.rigidBody.rotation()\n\n      box.sprite.setPosition(new THREE.Vector3(position.x, position.y, 0))\n      box.sprite.setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), rotation))\n    }\n\n    state.physicsWorld.boxes = state.physicsWorld.boxes.filter((box) => {\n      const pos = box.rigidBody.translation()\n      if (pos.y < -15) {\n        box.sprite.destroy()\n        state.physicsWorld.world.removeRigidBody(box.rigidBody)\n        return false\n      }\n      return true\n    })\n  }\n\n  state.frameCallback = async (deltaTime: number) => {\n    const currentTime = Date.now()\n\n    if (\n      state.isInitialized &&\n      currentTime - state.lastSpawnTime > spawnInterval &&\n      state.boxSpawnCount < 100 &&\n      !state.maxInstancesReached\n    ) {\n      const x = (Math.random() - 0.5) * 16\n      const y = 8 + Math.random() * 2\n      const size = 0.8 + Math.random() * 1.2\n\n      const newBox = await createBox(x, y, size, size)\n      if (newBox) {\n        state.lastSpawnTime = currentTime\n        state.boxSpawnCount++\n      } else {\n        state.maxInstancesReached = true\n      }\n    }\n\n    updatePhysics(deltaTime)\n    state.spriteAnimator.update(deltaTime)\n    if (state.physicsExplosionManager) {\n      state.physicsExplosionManager.update(deltaTime)\n    }\n    await state.engine.drawScene(state.scene, framebuffer, deltaTime)\n  }\n\n  state.keyHandler = (key: KeyEvent) => {\n    const keyStr = key.name\n\n    if (keyStr === \"space\" && state.isInitialized) {\n      ;(async () => {\n        const x = (Math.random() - 0.5) * 16\n        const y = 8 + Math.random() * 2\n        const size = 0.8 + Math.random() * 1.2\n\n        const newBox = await createBox(x, y, size, size)\n        if (newBox) {\n          console.log(\"Crate spawned manually!\")\n        } else {\n          state.maxInstancesReached = true\n          console.log(\"Cannot spawn crate - maximum instances reached!\")\n        }\n      })()\n    }\n\n    if (keyStr === \"e\" && state.isInitialized) {\n      explodeRandomCrate()\n    }\n\n    if (keyStr === \"r\" && state.isInitialized) {\n      for (const box of state.physicsWorld.boxes) {\n        box.sprite.destroy()\n        state.physicsWorld.world.removeRigidBody(box.rigidBody)\n      }\n      state.physicsWorld.boxes = []\n      state.boxSpawnCount = 0\n\n      state.physicsExplosionManager.disposeAll()\n      state.activeExplosionHandles.length = 0\n\n      console.log(\"Physics world reset!\")\n    }\n\n    if (keyStr === \"c\" && state.isInitialized) {\n      for (const box of state.physicsWorld.boxes) {\n        box.sprite.destroy()\n        state.physicsWorld.world.removeRigidBody(box.rigidBody)\n      }\n      state.physicsWorld.boxes = []\n      state.boxSpawnCount = 0\n\n      state.physicsExplosionManager.disposeAll()\n      state.activeExplosionHandles.length = 0\n\n      console.log(\"All crates cleared!\")\n    }\n\n    if (keyStr === \"b\" && state.isInitialized) {\n      console.log(\"Spawning burst of crates!\")\n      ;(async () => {\n        for (let i = 0; i < 10; i++) {\n          const x = (Math.random() - 0.5) * 12\n          const y = 8 + Math.random() * 4\n          const size = 0.6 + Math.random() * 1.0\n\n          const newBox = await createBox(x, y, size, size)\n          if (!newBox) {\n            state.maxInstancesReached = true\n            console.log(`Burst stopped at ${i + 1} crates - maximum instances reached!`)\n            break\n          }\n          await new Promise((resolve) => setTimeout(resolve, 50))\n        }\n      })()\n    }\n  }\n\n  state.resizeHandler = (newWidth: number, newHeight: number) => {\n    framebuffer.resize(newWidth, newHeight)\n\n    const newOrthoViewWidth = orthoViewHeight * state.engine.aspectRatio\n    state.camera.left = newOrthoViewWidth / -2\n    state.camera.right = newOrthoViewWidth / 2\n    state.camera.top = orthoViewHeight / 2\n    state.camera.bottom = orthoViewHeight / -2\n    state.camera.updateProjectionMatrix()\n  }\n\n  state.statsInterval = setInterval(() => {\n    if (state.isInitialized) {\n      const explosionCount = state.activeExplosionHandles.filter((h) => !h.hasBeenRestored).length\n      state.statsText.content = `Crates: ${state.physicsWorld.boxes.length} | Explosions: ${explosionCount} | Press [B] for burst spawn`\n    }\n  }, 100)\n\n  renderer.setFrameCallback(state.frameCallback)\n  renderer.keyInput.on(\"keypress\", state.keyHandler)\n  renderer.on(\"resize\", state.resizeHandler)\n\n  demoState = state\n  console.log(\"Rapier physics demo initialized!\")\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (!demoState) return\n\n  renderer.removeFrameCallback(demoState.frameCallback)\n  renderer.keyInput.off(\"keypress\", demoState.keyHandler)\n  renderer.root.removeListener(\"resize\", demoState.resizeHandler)\n\n  clearInterval(demoState.statsInterval)\n\n  for (const box of demoState.physicsWorld.boxes) {\n    box.sprite.destroy()\n    demoState.physicsWorld.world.removeRigidBody(box.rigidBody)\n  }\n\n  demoState.physicsExplosionManager.disposeAll()\n  demoState.engine.destroy()\n\n  renderer.root.remove(\"rapier-main\")\n  renderer.root.remove(\"rapier-container\")\n\n  demoState = null\n  console.log(\"Rapier physics demo cleaned up!\")\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/relative-positioning-demo.ts",
    "content": "import { TextAttributes, createCliRenderer, TextRenderable, BoxRenderable, type KeyEvent } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport type { CliRenderer } from \"../index.js\"\n\nlet globalKeyboardHandler: ((key: KeyEvent) => void) | null = null\nlet animationSpeed = 4000\nlet animationTime = 0\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  renderer.setBackgroundColor(\"#001122\")\n\n  const rootContainer = new BoxRenderable(renderer, {\n    id: \"root-container\",\n    position: \"relative\",\n    left: 0,\n    top: 0,\n    zIndex: 10,\n  })\n  renderer.root.add(rootContainer)\n\n  const title = new TextRenderable(renderer, {\n    id: \"main-title\",\n    content: \"Relative Positioning Demo - Child positions are relative to parent\",\n    position: \"absolute\",\n    left: 5,\n    top: 1,\n    fg: \"#FFFF00\",\n    attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n    zIndex: 1000,\n  })\n  rootContainer.add(title)\n\n  const parentContainerA = new BoxRenderable(renderer, {\n    id: \"parent-container-a\",\n    position: \"absolute\",\n    left: 10,\n    top: 5,\n    zIndex: 50,\n  })\n  rootContainer.add(parentContainerA)\n\n  const parentBoxA = new BoxRenderable(renderer, {\n    id: \"parent-box-a\",\n    left: 0,\n    top: 0,\n    width: 40,\n    height: 12,\n    backgroundColor: \"#220044\",\n    zIndex: 1,\n    borderStyle: \"double\",\n    borderColor: \"#FF44FF\",\n    title: \"Parent A (moves in circle)\",\n    titleAlignment: \"center\",\n    flexDirection: \"row\",\n    alignItems: \"stretch\",\n    justifyContent: \"space-between\",\n    border: true,\n  })\n  parentContainerA.add(parentBoxA)\n\n  const childA1 = new BoxRenderable(renderer, {\n    id: \"child-a1\",\n    width: \"auto\",\n    height: \"auto\",\n    backgroundColor: \"#440066\",\n    zIndex: 2,\n    borderStyle: \"single\",\n    borderColor: \"#FF88FF\",\n    title: \"Child 1\",\n    titleAlignment: \"center\",\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 8,\n    border: true,\n  })\n  parentBoxA.add(childA1)\n\n  const childA2 = new BoxRenderable(renderer, {\n    id: \"child-a2\",\n    width: \"auto\",\n    height: \"auto\",\n    backgroundColor: \"#660044\",\n    zIndex: 2,\n    borderStyle: \"single\",\n    borderColor: \"#FF88FF\",\n    title: \"Child 2\",\n    titleAlignment: \"center\",\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 8,\n    border: true,\n  })\n  parentBoxA.add(childA2)\n\n  const childA3 = new BoxRenderable(renderer, {\n    id: \"child-a3\",\n    width: \"auto\",\n    height: \"auto\",\n    backgroundColor: \"#440044\",\n    zIndex: 2,\n    borderStyle: \"single\",\n    borderColor: \"#FF88FF\",\n    title: \"Child 3\",\n    titleAlignment: \"center\",\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 8,\n    border: true,\n  })\n  parentBoxA.add(childA3)\n\n  const parentContainerB = new BoxRenderable(renderer, {\n    id: \"parent-container-b\",\n    position: \"absolute\",\n    left: 50,\n    top: 8,\n    zIndex: 50,\n  })\n  rootContainer.add(parentContainerB)\n\n  const parentBoxB = new BoxRenderable(renderer, {\n    id: \"parent-box-b\",\n    left: 0,\n    top: 0,\n    width: 40,\n    height: 10,\n    backgroundColor: \"#004422\",\n    zIndex: 1,\n    borderStyle: \"rounded\",\n    borderColor: \"#44FF44\",\n    title: \"Parent B (moves vertically)\",\n    titleAlignment: \"center\",\n    padding: 1,\n    flexDirection: \"column\",\n    justifyContent: \"space-between\",\n    border: true,\n  })\n  parentContainerB.add(parentBoxB)\n\n  const parentLabelB = new TextRenderable(renderer, {\n    id: \"parent-label-b\",\n    content: \"Parent B Position: (50, 8)\",\n    fg: \"#44FF44\",\n    attributes: TextAttributes.BOLD,\n    zIndex: 2,\n  })\n  parentBoxB.add(parentLabelB)\n\n  const childB1 = new TextRenderable(renderer, {\n    id: \"child-b1\",\n    content: \"Child at (1,3) - relative to parent\",\n    fg: \"#88FF88\",\n    zIndex: 2,\n  })\n  parentBoxB.add(childB1)\n\n  const childB2 = new TextRenderable(renderer, {\n    id: \"child-b2\",\n    content: \"Child at (1,5) - relative to parent\",\n    fg: \"#88FF88\",\n    zIndex: 2,\n  })\n  parentBoxB.add(childB2)\n\n  const staticContainer = new BoxRenderable(renderer, {\n    id: \"static-container\",\n    position: \"absolute\",\n    left: 5,\n    top: 20,\n    zIndex: 50,\n  })\n  rootContainer.add(staticContainer)\n\n  const staticBox = new BoxRenderable(renderer, {\n    id: \"static-box\",\n    left: 0,\n    top: 0,\n    width: 40,\n    height: 8,\n    backgroundColor: \"#442200\",\n    zIndex: 1,\n    borderStyle: \"single\",\n    borderColor: \"#FFFF44\",\n    title: \"Static Parent (doesn't move)\",\n    titleAlignment: \"center\",\n    padding: 1,\n    flexDirection: \"column\",\n    border: true,\n    overflow: \"hidden\",\n  })\n  staticContainer.add(staticBox)\n\n  const staticChild1 = new TextRenderable(renderer, {\n    id: \"static-child1\",\n    content: \"Static child at (2,2) - never moves\",\n    fg: \"#FFFF88\",\n    zIndex: 2,\n  })\n  staticBox.add(staticChild1)\n\n  const staticChild2 = new TextRenderable(renderer, {\n    id: \"static-child2\",\n    content: \"Static child at (2,4) - never moves\",\n    fg: \"#FFFF88\",\n    zIndex: 2,\n  })\n  staticBox.add(staticChild2)\n\n  const explanation1 = new TextRenderable(renderer, {\n    id: \"explanation1\",\n    content: \"Key Concept: Parent A uses flex layout - children are arranged in a row\",\n    position: \"absolute\",\n    left: 5,\n    top: 30,\n    fg: \"#AAAAAA\",\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  rootContainer.add(explanation1)\n\n  const explanation2 = new TextRenderable(renderer, {\n    id: \"explanation2\",\n    content: \"When parent moves, children move with it while maintaining flex layout\",\n    position: \"absolute\",\n    left: 5,\n    top: 31,\n    fg: \"#AAAAAA\",\n    zIndex: 1000,\n  })\n  rootContainer.add(explanation2)\n\n  const explanation3 = new TextRenderable(renderer, {\n    id: \"explanation3\",\n    content: \"Flex children automatically fit parent width and grow/shrink as needed\",\n    position: \"absolute\",\n    left: 5,\n    top: 32,\n    fg: \"#AAAAAA\",\n    zIndex: 1000,\n  })\n  rootContainer.add(explanation3)\n\n  const controls = new TextRenderable(renderer, {\n    id: \"controls\",\n    content: \"Controls: +/- to change animation speed\",\n    position: \"absolute\",\n    left: 5,\n    top: 34,\n    fg: \"#FFFFFF\",\n    attributes: TextAttributes.BOLD,\n    zIndex: 1000,\n  })\n  rootContainer.add(controls)\n\n  const speedDisplay = new TextRenderable(renderer, {\n    id: \"speed-display\",\n    content: `Animation Speed: ${animationSpeed}ms (min: 500, max: 8000)`,\n    position: \"absolute\",\n    left: 5,\n    top: 35,\n    fg: \"#CCCCCC\",\n    zIndex: 1000,\n  })\n  rootContainer.add(speedDisplay)\n\n  renderer.setFrameCallback(async (deltaMs) => {\n    animationTime += deltaMs\n\n    const circleRadius = 15\n    const circleSpeed = (animationTime / animationSpeed) * Math.PI * 2\n    const parentAX = 20 + Math.cos(circleSpeed) * circleRadius\n    const parentAY = 8 + (Math.sin(circleSpeed) * circleRadius) / 2\n\n    parentContainerA.setPosition({\n      left: Math.round(parentAX),\n      top: Math.round(parentAY),\n    })\n\n    const verticalSpeed = (animationTime / (animationSpeed * 1.5)) * Math.PI * 2\n    const parentBY = 8 + Math.sin(verticalSpeed) * 8\n\n    parentContainerB.setPosition({\n      left: 50,\n      top: Math.round(parentBY),\n    })\n    parentLabelB.content = `Parent B Position: (50, ${Math.round(parentBY)})`\n  })\n\n  globalKeyboardHandler = (key: KeyEvent) => {\n    if (key.name === \"+\" || key.name === \"=\") {\n      animationSpeed = Math.max(500, animationSpeed - 300)\n      speedDisplay.content = `Animation Speed: ${animationSpeed}ms (min: 500, max: 8000)`\n    } else if (key.name === \"-\" || key.name === \"_\") {\n      animationSpeed = Math.min(8000, animationSpeed + 300)\n      speedDisplay.content = `Animation Speed: ${animationSpeed}ms (min: 500, max: 8000)`\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", globalKeyboardHandler)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (globalKeyboardHandler) {\n    renderer.keyInput.off(\"keypress\", globalKeyboardHandler)\n    globalKeyboardHandler = null\n  }\n\n  renderer.root.remove(\"root-container\")\n\n  renderer.clearFrameCallbacks()\n  renderer.setCursorPosition(0, 0, false)\n  animationTime = 0\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/scroll-example.ts",
    "content": "import {\n  ASCIIFontRenderable,\n  BoxRenderable,\n  type CliRenderer,\n  createCliRenderer,\n  TextRenderable,\n  RGBA,\n  t,\n  fg,\n  bold,\n  underline,\n  italic,\n} from \"../index.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet scrollBox: ScrollBoxRenderable | null = null\nlet renderer: CliRenderer | null = null\nlet mainContainer: BoxRenderable | null = null\nlet instructionsBox: BoxRenderable | null = null\nlet nextIndex = 1000\n\nfunction addBox(i: number) {\n  if (!renderer || !scrollBox) return\n\n  const box = new BoxRenderable(renderer, {\n    id: `box-${i + 1}`,\n    width: \"auto\",\n    padding: 1,\n    marginBottom: 1,\n    backgroundColor: i % 2 === 0 ? \"#292e42\" : \"#2f3449\",\n  })\n\n  const content = makeMultilineContent(i)\n  const text = new TextRenderable(renderer, {\n    content,\n  })\n\n  box.add(text)\n  scrollBox.add(box)\n}\n\nfunction addAsciiRenderable(i: number) {\n  if (!renderer || !scrollBox) return\n\n  const fonts = [\"tiny\", \"block\", \"shade\", \"slick\"] as const\n  const font = fonts[i % fonts.length]\n  const colors = [\n    [RGBA.fromInts(166, 227, 161, 255), RGBA.fromInts(122, 162, 247, 255)],\n    [RGBA.fromInts(247, 118, 142, 255), RGBA.fromInts(245, 194, 231, 255)],\n    [RGBA.fromInts(125, 196, 228, 255), RGBA.fromInts(199, 146, 234, 255)],\n    [RGBA.fromInts(244, 191, 117, 255), RGBA.fromInts(249, 226, 175, 255)],\n  ][i % 4]\n\n  const longText =\n    `ASCII FONT RENDERABLE #${i + 1} - ${font.toUpperCase()} STYLE - This is an extremely long piece of text that will definitely exceed the width of the scrollbox and trigger horizontal scrolling functionality. `.repeat(\n      15,\n    ) +\n    `Additional content includes: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. `.repeat(\n      12,\n    ) +\n    `The quick brown fox jumps over the lazy dog while the sly red panda silently observes from the treetops, contemplating the mysteries of the universe and wondering about the meaning of life. Meanwhile, technology continues to advance at an unprecedented rate, bringing both amazing opportunities and challenging ethical dilemmas to humanity's doorstep. From artificial intelligence to quantum computing, the future holds limitless possibilities that our ancestors could only dream of in their wildest imaginations.`.repeat(\n      8,\n    )\n\n  const asciiRenderable = new ASCIIFontRenderable(renderer, {\n    id: `ascii-${i + 1}`,\n    text: longText,\n    font: font,\n    color: colors,\n    backgroundColor: RGBA.fromInts(26, 27, 38, 255),\n    selectionBg: \"#f7768e\",\n    selectionFg: \"#c0caf5\",\n    zIndex: 10,\n  })\n\n  scrollBox.add(asciiRenderable)\n}\n\nfunction makeMultilineContent(i: number) {\n  const palette = [fg(\"#7aa2f7\"), fg(\"#9ece6a\"), fg(\"#f7768e\"), fg(\"#7dcfff\"), fg(\"#bb9af7\"), fg(\"#e0af68\")]\n  const colorize = palette[i % palette.length]\n  const id = (i + 1).toString().padStart(4, \"0\")\n  const tag = i % 3 === 0 ? underline(\"INFO\") : i % 3 === 1 ? bold(\"WARN\") : bold(fg(\"#f7768e\")(\"ERROR\"))\n\n  const barUnits = 10 + (i % 30)\n  const bar = \"█\".repeat(Math.floor(barUnits * 0.6)).padEnd(barUnits, \"░\")\n  const details = \"data \".repeat((i % 4) + 2)\n\n  return t`${fg(\"#565f89\")(`[${id}]`)} ${bold(colorize(`Box ${i + 1}`))} ${fg(\"#565f89\")(\"|\")} ${tag}\n${fg(\"#9aa5ce\")(\"Multiline content with mixed styles for stress testing.\")}\n${colorize(\"• Title:\")} ${bold(italic(`Lorem ipsum ${i}`))}\n${fg(\"#9ece6a\")(\"• Detail A:\")} ${fg(\"#c0caf5\")(details.trim())}\n${fg(\"#bb9af7\")(\"• Detail B:\")} ${fg(\"#a9b1d6\")(\"The quick brown fox jumps over the lazy dog.\")}\n${fg(\"#7dcfff\")(\"• Progress:\")} ${fg(\"#73daca\")(bar)} ${fg(\"#565f89\")(barUnits)}\n${fg(\"#565f89\")(\"— end of box —\")}`\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#1a1b26\")\n\n  mainContainer = new BoxRenderable(renderer, {\n    id: \"main-container\",\n    flexGrow: 1,\n    maxHeight: \"100%\",\n    maxWidth: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#1a1b26\",\n  })\n\n  scrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"scroll-box\",\n    rootOptions: {\n      backgroundColor: \"#24283b\",\n      border: true,\n    },\n    wrapperOptions: {\n      backgroundColor: \"#1f2335\",\n    },\n    viewportOptions: {\n      backgroundColor: \"#1a1b26\",\n    },\n    contentOptions: {\n      backgroundColor: \"#16161e\",\n    },\n    scrollbarOptions: {\n      //   showArrows: true,\n      trackOptions: {\n        foregroundColor: \"#7aa2f7\",\n        backgroundColor: \"#414868\",\n      },\n    },\n  })\n\n  instructionsBox = new BoxRenderable(renderer, {\n    id: \"instructions\",\n    width: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#2a2b3a\",\n    paddingLeft: 1,\n    flexShrink: 0,\n  })\n\n  const instructionsText1 = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Controls:\"))} ${fg(\"#c0caf5\")(\"↑/↓/PgUp/PgDn/Home/End\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#9ece6a\")(\"A\"))} ${fg(\"#c0caf5\")(\"Toggle arrows\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#bb9af7\")(\"Tab\"))} ${fg(\"#c0caf5\")(\"Focus scrollbox\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#f7768e\")(\"N\"))} ${fg(\"#c0caf5\")(\"Add child\")}`,\n  })\n\n  const instructionsText2 = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Scrollbars:\"))} ${bold(fg(\"#e0af68\")(\"V\"))} ${fg(\"#c0caf5\")(\"Toggle vertical\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#f7768e\")(\"H\"))} ${fg(\"#c0caf5\")(\"Toggle horizontal\")}`,\n  })\n\n  instructionsBox.add(instructionsText1)\n  instructionsBox.add(instructionsText2)\n\n  mainContainer.add(scrollBox)\n  mainContainer.add(instructionsBox)\n\n  renderer.root.add(mainContainer)\n\n  scrollBox.focus()\n\n  // Generate 1000 boxes, each with multiline styled text\n  // Add an ASCII renderable at the top (index 0) for immediate visibility\n  addAsciiRenderable(0)\n\n  for (let index = 1; index < nextIndex; index++) {\n    if ((index + 1) % 100 === 0) {\n      addAsciiRenderable(index)\n    } else {\n      addBox(index)\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", (key) => {\n    if (key.name === \"a\" && scrollBox) {\n      const currentState = scrollBox.verticalScrollBar?.showArrows ?? false\n      scrollBox.verticalScrollBar!.showArrows = !currentState\n      scrollBox.horizontalScrollBar!.showArrows = !currentState\n      console.log(`Arrows ${!currentState ? \"enabled\" : \"disabled\"}`)\n    } else if (key.name === \"v\" && scrollBox) {\n      const currentState = scrollBox.verticalScrollBar.visible\n      scrollBox.verticalScrollBar.visible = !currentState\n      console.log(`Vertical scrollbar ${!currentState ? \"shown\" : \"hidden\"}`)\n    } else if (key.name === \"h\" && scrollBox) {\n      const currentState = scrollBox.horizontalScrollBar.visible\n      scrollBox.horizontalScrollBar.visible = !currentState\n      console.log(`Horizontal scrollbar ${!currentState ? \"shown\" : \"hidden\"}`)\n    } else if (key.name === \"n\" && scrollBox) {\n      addBox(nextIndex)\n      nextIndex++\n    }\n  })\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (mainContainer) {\n    rendererInstance.root.remove(mainContainer.id)\n    mainContainer.destroyRecursively()\n    mainContainer = null\n  }\n  scrollBox = null\n  instructionsBox = null\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/scrollbox-mouse-test.ts",
    "content": "#!/usr/bin/env bun\nimport { BoxRenderable, type CliRenderer, createCliRenderer, TextRenderable, RGBA, t, fg, bold } from \"../index.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet scrollBox: ScrollBoxRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet hoveredItem: string | null = null\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"#1a1b26\")\n\n  const mainContainer = new BoxRenderable(renderer, {\n    id: \"main-container\",\n    flexGrow: 1,\n    maxHeight: \"100%\",\n    maxWidth: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#1a1b26\",\n  })\n\n  const header = new BoxRenderable(renderer, {\n    id: \"header\",\n    width: \"100%\",\n    height: 3,\n    backgroundColor: \"#24283b\",\n    paddingLeft: 1,\n    flexShrink: 0,\n  })\n\n  const title = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"ScrollBox Mouse Hit Test\"))} - Scroll and hover items to test hit detection`,\n  })\n  header.add(title)\n\n  statusText = new TextRenderable(renderer, {\n    content: t`${fg(\"#565f89\")(\"Hovered:\")} ${fg(\"#c0caf5\")(\"none\")}`,\n  })\n  header.add(statusText)\n\n  scrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"scroll-box\",\n    rootOptions: {\n      backgroundColor: \"#24283b\",\n      border: true,\n    },\n    contentOptions: {\n      backgroundColor: \"#16161e\",\n    },\n  })\n\n  for (let i = 0; i < 50; i++) {\n    const item = new BoxRenderable(renderer, {\n      id: `item-${i}`,\n      width: \"100%\",\n      height: 2,\n      backgroundColor: i % 2 === 0 ? \"#292e42\" : \"#2f3449\",\n      paddingLeft: 1,\n      onMouseOver: () => {\n        hoveredItem = `item-${i}`\n        updateStatus()\n      },\n      onMouseOut: () => {\n        if (hoveredItem === `item-${i}`) {\n          hoveredItem = null\n          updateStatus()\n        }\n      },\n      onClick: () => {\n        console.log(`Clicked item-${i}`)\n      },\n    })\n\n    const text = new TextRenderable(renderer, {\n      content: t`${fg(\"#7aa2f7\")(`[${i.toString().padStart(2, \"0\")}]`)} ${fg(\"#c0caf5\")(`Item ${i} - Hover over me to test hit detection`)}`,\n    })\n    item.add(text)\n    scrollBox.add(item)\n  }\n\n  mainContainer.add(header)\n  mainContainer.add(scrollBox)\n  renderer.root.add(mainContainer)\n\n  scrollBox.focus()\n\n  function updateStatus() {\n    if (statusText) {\n      const hovered = hoveredItem || \"none\"\n      statusText.content = t`${fg(\"#565f89\")(\"Hovered:\")} ${fg(\"#9ece6a\")(hovered)}`\n    }\n  }\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.root.getChildren().forEach((child) => {\n    renderer.root.remove(child.id)\n    child.destroyRecursively()\n  })\n  scrollBox = null\n  statusText = null\n  hoveredItem = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/scrollbox-overlay-hit-test.ts",
    "content": "#!/usr/bin/env bun\nimport {\n  BoxRenderable,\n  type CliRenderer,\n  createCliRenderer,\n  TextRenderable,\n  t,\n  fg,\n  bold,\n  type KeyEvent,\n} from \"../index.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet overlay: BoxRenderable | null = null\nlet dialog: BoxRenderable | null = null\nlet scrollBox: ScrollBoxRenderable | null = null\nlet baseStatusText: TextRenderable | null = null\nlet dialogStatusText: TextRenderable | null = null\nlet keyHandler: ((key: KeyEvent) => void) | null = null\nlet dialogOpen = false\nlet lastClick = \"none\"\n\nconst updateStatus = () => {\n  const content = t`${fg(\"#9aa5ce\")(\"Last click:\")} ${fg(\"#9ece6a\")(lastClick)}`\n  if (baseStatusText) {\n    baseStatusText.content = content\n  }\n  if (dialogStatusText) {\n    dialogStatusText.content = content\n  }\n}\n\nconst setDialogVisible = (visible: boolean) => {\n  dialogOpen = visible\n  if (overlay) {\n    overlay.visible = visible\n  }\n}\n\nconst setLastClick = (value: string) => {\n  lastClick = value\n  updateStatus()\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"#1a1b26\")\n\n  const app = new BoxRenderable(renderer, {\n    id: \"app\",\n    flexDirection: \"column\",\n    width: \"100%\",\n    height: \"100%\",\n    backgroundColor: \"#1a1b26\",\n    paddingLeft: 1,\n    paddingTop: 1,\n    gap: 1,\n  })\n  renderer.root.add(app)\n\n  const title = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Scrollbox Overlay Hit Test\"))}`,\n  })\n  app.add(title)\n\n  const instructions = new TextRenderable(renderer, {\n    content: t`${fg(\"#c0caf5\")(\"Press 'd' to toggle dialog, 'esc' to close, 'q' to quit\")}`,\n  })\n  app.add(instructions)\n\n  baseStatusText = new TextRenderable(renderer, {\n    content: t`${fg(\"#9aa5ce\")(\"Last click:\")} ${fg(\"#9ece6a\")(lastClick)}`,\n  })\n  app.add(baseStatusText)\n\n  overlay = new BoxRenderable(renderer, {\n    id: \"overlay\",\n    position: \"absolute\",\n    top: 0,\n    left: 0,\n    width: \"100%\",\n    height: \"100%\",\n    backgroundColor: \"#ff000033\",\n    zIndex: 100,\n    visible: false,\n    onMouseDown: () => {\n      setLastClick(\"overlay (red)\")\n      setDialogVisible(false)\n    },\n  })\n  renderer.root.add(overlay)\n\n  dialog = new BoxRenderable(renderer, {\n    id: \"dialog\",\n    position: \"absolute\",\n    top: \"25%\",\n    left: \"25%\",\n    width: \"50%\",\n    height: \"50%\",\n    flexDirection: \"column\",\n    gap: 1,\n    padding: 1,\n    backgroundColor: \"#0f172a\",\n    border: true,\n    borderColor: \"#7aa2f7\",\n    onMouseDown: (event) => {\n      setLastClick(\"dialog (blue)\")\n      event.stopPropagation()\n    },\n  })\n  overlay.add(dialog)\n\n  const dialogTitle = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Dialog\"))} ${fg(\"#565f89\")(\"- scroll, then click outside the list\")}`,\n  })\n  dialog.add(dialogTitle)\n\n  const dialogHint = new TextRenderable(renderer, {\n    content: t`${fg(\"#c0caf5\")(\"Click the red overlay above/below the dialog to close it\")}`,\n  })\n  dialog.add(dialogHint)\n\n  dialogStatusText = new TextRenderable(renderer, {\n    content: t`${fg(\"#9aa5ce\")(\"Last click:\")} ${fg(\"#9ece6a\")(lastClick)}`,\n  })\n  dialog.add(dialogStatusText)\n\n  scrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"scrollbox\",\n    flexGrow: 1,\n    scrollY: true,\n    onMouseDown: (event) => {\n      setLastClick(\"scrollbox (yellow)\")\n      event.stopPropagation()\n    },\n    rootOptions: {\n      backgroundColor: \"#eab308\",\n      border: true,\n      borderColor: \"#0f172a\",\n    },\n    contentOptions: {\n      backgroundColor: \"#111827\",\n    },\n  })\n  dialog.add(scrollBox)\n\n  for (let i = 0; i < 50; i++) {\n    const item = new BoxRenderable(renderer, {\n      id: `line-${i}`,\n      width: \"100%\",\n      height: 1,\n      paddingLeft: 1,\n      backgroundColor: i % 2 === 0 ? \"#1f2937\" : \"#111827\",\n    })\n    const text = new TextRenderable(renderer, {\n      content: t`${fg(\"#cbd5f5\")(`Line ${i + 1}: This is some content`)}`,\n    })\n    item.add(text)\n    scrollBox.add(item)\n  }\n\n  keyHandler = (key: KeyEvent) => {\n    if (key.name === \"q\") {\n      renderer.destroy()\n      process.exit(0)\n    }\n    if (key.name === \"d\") {\n      setDialogVisible(!dialogOpen)\n    }\n    if (key.name === \"escape\") {\n      setDialogVisible(false)\n    }\n  }\n  renderer.keyInput.on(\"keypress\", keyHandler)\n\n  updateStatus()\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (keyHandler) {\n    renderer.keyInput.off(\"keypress\", keyHandler)\n  }\n\n  renderer.root.getChildren().forEach((child) => {\n    renderer.root.remove(child.id)\n    child.destroyRecursively()\n  })\n\n  overlay = null\n  dialog = null\n  scrollBox = null\n  baseStatusText = null\n  dialogStatusText = null\n  keyHandler = null\n  dialogOpen = false\n  lastClick = \"none\"\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/select-demo.ts",
    "content": "import {\n  createCliRenderer,\n  SelectRenderable,\n  SelectRenderableEvents,\n  RenderableEvents,\n  type SelectOption,\n  type CliRenderer,\n  t,\n  bold,\n  fg,\n  BoxRenderable,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\nlet selectElement: SelectRenderable | null = null\nlet renderer: CliRenderer | null = null\nlet keyboardHandler: ((key: any) => void) | null = null\nlet keyLegendDisplay: TextRenderable | null = null\nlet statusDisplay: TextRenderable | null = null\nlet lastActionText: string = \"Welcome to SelectRenderable demo! Use the controls to test features.\"\nlet lastActionColor: string = \"#FFCC00\"\n\nconst selectOptions: SelectOption[] = [\n  { name: \"Home\", description: \"Navigate to the home page\", value: \"home\" },\n  { name: \"Profile\", description: \"View and edit your user profile\", value: \"profile\" },\n  { name: \"Settings\", description: \"Configure application preferences\", value: \"settings\" },\n  { name: \"Dashboard\", description: \"View analytics and key metrics\", value: \"dashboard\" },\n  { name: \"Projects\", description: \"Manage your active projects\", value: \"projects\" },\n  { name: \"Reports\", description: \"Generate and view detailed reports\", value: \"reports\" },\n  { name: \"Users\", description: \"Manage user accounts and permissions\", value: \"users\" },\n  { name: \"Analytics\", description: \"Deep dive into usage analytics\", value: \"analytics\" },\n  { name: \"Tools\", description: \"Access various utility tools\", value: \"tools\" },\n  { name: \"API Documentation\", description: \"Browse API endpoints and examples\", value: \"api\" },\n  { name: \"Help Center\", description: \"Find answers to common questions\", value: \"help\" },\n  { name: \"Support\", description: \"Contact our support team\", value: \"support\" },\n  { name: \"Billing\", description: \"Manage your subscription and billing\", value: \"billing\" },\n  { name: \"Integrations\", description: \"Connect with third-party services\", value: \"integrations\" },\n  { name: \"Security\", description: \"Configure security settings\", value: \"security\" },\n  { name: \"Notifications\", description: \"Manage your notification preferences\", value: \"notifications\" },\n  { name: \"Backup\", description: \"Backup and restore your data\", value: \"backup\" },\n  { name: \"Import/Export\", description: \"Import or export your data\", value: \"import-export\" },\n  { name: \"Advanced Settings\", description: \"Configure advanced options\", value: \"advanced\" },\n  { name: \"About\", description: \"Learn more about this application\", value: \"about\" },\n]\n\nfunction updateDisplays() {\n  if (!selectElement) return\n\n  const scrollIndicator = selectElement.showScrollIndicator ? \"on\" : \"off\"\n  const description = selectElement.showDescription ? \"on\" : \"off\"\n  const wrap = selectElement.wrapSelection ? \"on\" : \"off\"\n\n  const keyLegendText = t`${bold(fg(\"#FFFFFF\")(\"Key Controls:\"))}\n↑/↓ or j/k: Navigate items\nShift+↑/↓ or Shift+j/k: Fast scroll\nEnter: Select item\nF: Toggle focus\nD: Toggle descriptions\nS: Toggle scroll indicator\nW: Toggle wrap selection`\n\n  if (keyLegendDisplay) {\n    keyLegendDisplay.content = keyLegendText\n  }\n\n  const currentSelection = selectElement.getSelectedOption()\n  const selectionText = currentSelection\n    ? `Selection: ${currentSelection.name} (${currentSelection.value}) - Index: ${selectElement.getSelectedIndex()}`\n    : \"No selection\"\n\n  const focusText = selectElement.focused ? \"Select element is FOCUSED\" : \"Select element is BLURRED\"\n  const focusColor = selectElement.focused ? \"#00FF00\" : \"#FF0000\"\n\n  const statusText = t`${fg(\"#00FF00\")(selectionText)}\n\n${fg(focusColor)(focusText)}\n\n${fg(\"#CCCCCC\")(`Scroll indicator: ${scrollIndicator} | Description: ${description} | Wrap: ${wrap}`)}\n\n${fg(lastActionColor)(lastActionText)}`\n\n  if (statusDisplay) {\n    statusDisplay.content = statusText\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#001122\")\n\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  selectElement = new SelectRenderable(renderer, {\n    id: \"demo-select\",\n    position: \"absolute\",\n    left: 5,\n    top: 2,\n    width: 50,\n    height: 20,\n    options: selectOptions,\n    zIndex: 100,\n    backgroundColor: \"#1e293b\",\n    focusedBackgroundColor: \"#2d3748\",\n    textColor: \"#e2e8f0\",\n    focusedTextColor: \"#f7fafc\",\n    selectedBackgroundColor: \"#3b82f6\",\n    selectedTextColor: \"#ffffff\",\n    descriptionColor: \"#94a3b8\",\n    selectedDescriptionColor: \"#cbd5e1\",\n    showDescription: true,\n    showScrollIndicator: true,\n    wrapSelection: false,\n    fastScrollStep: 5,\n  })\n\n  renderer.root.add(selectElement)\n\n  keyLegendDisplay = new TextRenderable(renderer, {\n    id: \"key-legend\",\n    content: t``,\n    width: 40,\n    height: 9,\n    position: \"absolute\",\n    left: 60,\n    top: 3,\n    zIndex: 50,\n    fg: \"#AAAAAA\",\n  })\n  parentContainer.add(keyLegendDisplay)\n\n  statusDisplay = new TextRenderable(renderer, {\n    id: \"status-display\",\n    content: t``,\n    width: 80,\n    height: 8,\n    position: \"absolute\",\n    left: 5,\n    top: 24,\n    zIndex: 50,\n  })\n  parentContainer.add(statusDisplay)\n\n  selectElement.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {\n    lastActionText = `Navigation: Moved to \"${option.name}\"`\n    lastActionColor = \"#FFCC00\"\n    updateDisplays()\n  })\n\n  selectElement.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {\n    lastActionText = `*** ACTIVATED: ${option.name} (${option.value}) ***`\n    lastActionColor = \"#FF00FF\"\n    updateDisplays()\n    setTimeout(() => {\n      lastActionColor = \"#FFCC00\"\n      updateDisplays()\n    }, 1000)\n  })\n\n  selectElement.on(RenderableEvents.FOCUSED, () => {\n    updateDisplays()\n  })\n\n  selectElement.on(RenderableEvents.BLURRED, () => {\n    updateDisplays()\n  })\n\n  updateDisplays()\n\n  keyboardHandler = (key) => {\n    if (key.name === \"f\") {\n      if (selectElement?.focused) {\n        selectElement.blur()\n        lastActionText = \"Focus removed from select element\"\n      } else {\n        selectElement?.focus()\n        lastActionText = \"Select element focused\"\n      }\n      lastActionColor = \"#FFCC00\"\n      updateDisplays()\n    } else if (key.name === \"d\") {\n      const newState = !selectElement?.showDescription\n      selectElement!.showDescription = newState\n      lastActionText = `Descriptions ${newState ? \"enabled\" : \"disabled\"}`\n      lastActionColor = \"#FFCC00\"\n      updateDisplays()\n    } else if (key.name === \"s\") {\n      const newState = !selectElement?.showScrollIndicator\n      selectElement!.showScrollIndicator = newState\n      lastActionText = `Scroll indicator ${newState ? \"enabled\" : \"disabled\"}`\n      lastActionColor = \"#FFCC00\"\n      updateDisplays()\n    } else if (key.name === \"w\") {\n      const newState = !selectElement?.wrapSelection\n      selectElement!.wrapSelection = newState\n      lastActionText = `Wrap selection ${newState ? \"enabled\" : \"disabled\"}`\n      lastActionColor = \"#FFCC00\"\n      updateDisplays()\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n  selectElement.focus()\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  if (selectElement) {\n    rendererInstance.root.remove(selectElement.id)\n    selectElement.destroy()\n    selectElement = null\n  }\n\n  rendererInstance.root.remove(\"parent-container\")\n\n  keyLegendDisplay = null\n  statusDisplay = null\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/shader-cube-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  createCliRenderer,\n  CliRenderer,\n  TextRenderable,\n  BoxRenderable,\n  FrameBufferRenderable,\n  type KeyEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { RGBA } from \"../lib/index.js\"\nimport { TextureUtils } from \"../3d/TextureUtils.js\"\nimport {\n  Scene as ThreeScene,\n  Mesh as ThreeMesh,\n  PerspectiveCamera,\n  Color,\n  DirectionalLight as ThreeDirectionalLight,\n  PointLight as ThreePointLight,\n  MeshPhongMaterial,\n  BoxGeometry,\n  AmbientLight,\n} from \"three\"\nimport * as Filters from \"../post/filters.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport { ThreeCliRenderer } from \"../3d.js\"\nimport {\n  DistortionEffect,\n  VignetteEffect,\n  CloudsEffect,\n  FlamesEffect,\n  RainbowTextEffect,\n  CRTRollingBarEffect,\n} from \"../post/effects.js\"\nimport * as Matrices from \"../post/matrices.js\"\n\n// State management for the demo\ninterface ShaderCubeDemoState {\n  engine: ThreeCliRenderer\n  sceneRoot: ThreeScene\n  cameraNode: PerspectiveCamera\n  mainLightNode: ThreeDirectionalLight\n  pointLightNode: ThreePointLight\n  ambientLightNode: AmbientLight\n  lightVisualizerMesh: ThreeMesh\n  cubeMeshNode: ThreeMesh\n  materials: MeshPhongMaterial[]\n  distortionEffectInstance: DistortionEffect\n  vignetteEffectInstance: VignetteEffect\n  cloudsEffectInstance: CloudsEffect\n  flamesEffectInstance: FlamesEffect\n  rainbowTextEffectInstance: RainbowTextEffect\n  crtRollingBarEffectInstance: CRTRollingBarEffect\n  pipboyVignetteEffectInstance: VignetteEffect\n  pipboyBarEffectInstance: CRTRollingBarEffect\n  brightnessValue: number\n  gainValue: number\n  colorMatrixEffectInstance: ColorMatrixEffect\n  filterFunctions: { name: string; func: ((buffer: OptimizedBuffer, deltaTime: number) => void) | null }[]\n  currentFilterIndex: number\n  time: number\n  lightColorMode: number\n  rotationEnabled: boolean\n  showLightVisualizers: boolean\n  customLightsEnabled: boolean\n  currentMaterial: number\n  manualMaterialSelection: boolean\n  specularMapEnabled: boolean\n  normalMapEnabled: boolean\n  emissiveMapEnabled: boolean\n  parentContainer: BoxRenderable\n  backgroundBox: BoxRenderable\n  lightVizText: TextRenderable\n  lightColorText: TextRenderable\n  customLightsText: TextRenderable\n  materialToggleText: TextRenderable\n  textureEffectsText: TextRenderable\n  filterStatusText: TextRenderable\n  param1StatusText: TextRenderable\n  param2StatusText: TextRenderable\n  controlsText: TextRenderable\n  keyHandler: (key: KeyEvent) => void\n  resizeHandler: (width: number, height: number) => void\n  frameCallbackId: boolean\n}\n\nlet demoState: ShaderCubeDemoState | null = null\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const WIDTH = renderer.terminalWidth\n  const HEIGHT = renderer.terminalHeight\n  const CAM_DISTANCE = 3.5\n  const CAMERA_PAN_STEP = 0.2\n  const CAMERA_ZOOM_STEP = 0.35\n  const rotationSpeed = [0.2, 0.4, 0.1]\n\n  const lightColors = [\n    { color: [255, 220, 180], name: \"Warm\" },\n    { color: [180, 220, 255], name: \"Cool\" },\n    { color: [255, 100, 100], name: \"Red\" },\n    { color: [100, 255, 100], name: \"Green\" },\n    { color: [100, 100, 255], name: \"Blue\" },\n    { color: [255, 255, 100], name: \"Yellow\" },\n  ]\n\n  // Create parent container for all UI elements\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"shader-cube-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  // Initialize effect instances\n  const distortionEffectInstance = new DistortionEffect()\n  const vignetteEffectInstance = new VignetteEffect()\n  const cloudsEffectInstance = new CloudsEffect(0.27, 0.001, 0.75, 1.0)\n  const flamesEffectInstance = new FlamesEffect(0.04, 0.02, 0.9)\n  const rainbowTextEffectInstance = new RainbowTextEffect(0.006, 1.0, 1.0, 10.0)\n  const crtRollingBarEffectInstance = new CRTRollingBarEffect(0.8, 0.1, 0.4, 0.2)\n\n  // Pipboy-specific instances (decoupled from other effects)\n  const pipboyVignetteEffectInstance = new VignetteEffect(0.75)\n  const pipboyBarEffectInstance = new CRTRollingBarEffect(2.5, 0.08, 0.75, 0.15)\n\n  // Simple value-based brightness and gain (no class instances)\n  let brightnessValue = 0.0\n  let gainValue = 1.0\n\n  // Helper function to create right-half cell masks for selective saturation\n  function createRightHalfCellMask(width: number, height: number): Float32Array {\n    const rightHalfWidth = Math.floor(width / 2)\n    const rightHalfPixels = rightHalfWidth * height\n    const cellMask = new Float32Array(rightHalfPixels * 3)\n    let i = 0\n    for (let y = 0; y < height; y++) {\n      for (let x = Math.floor(width / 2); x < width; x++) {\n        cellMask[i++] = x\n        cellMask[i++] = y\n        cellMask[i++] = 1\n      }\n    }\n    return cellMask\n  }\n\n  // Full screen saturation mode toggle (null cellMask = uniform)\n  let saturationFullScreen = false\n\n  // Saturation state variables\n  let saturationValue = 1.0\n  let saturationCellMask: Float32Array | null = createRightHalfCellMask(WIDTH, HEIGHT)\n\n  // Registry of all available color matrices with their display names\n  const colorMatrixRegistry: { name: string; matrix: Float32Array }[] = [\n    { name: \"Sepia\", matrix: Matrices.SEPIA_MATRIX },\n    { name: \"Protanopia Sim\", matrix: Matrices.PROTANOPIA_SIM_MATRIX },\n    { name: \"Deuteranopia Sim\", matrix: Matrices.DEUTERANOPIA_SIM_MATRIX },\n    { name: \"Tritanopia Sim\", matrix: Matrices.TRITANOPIA_SIM_MATRIX },\n    { name: \"Achromatopsia\", matrix: Matrices.ACHROMATOPSIA_MATRIX },\n    { name: \"Protanopia Comp\", matrix: Matrices.PROTANOPIA_COMP_MATRIX },\n    { name: \"Deuteranopia Comp\", matrix: Matrices.DEUTERANOPIA_COMP_MATRIX },\n    { name: \"Tritanopia Comp\", matrix: Matrices.TRITANOPIA_COMP_MATRIX },\n    // Creative effects\n    { name: \"Technicolor\", matrix: Matrices.TECHNICOLOR_MATRIX },\n    { name: \"Solarization\", matrix: Matrices.SOLARIZATION_MATRIX },\n    { name: \"Synthwave\", matrix: Matrices.SYNTHWAVE_MATRIX },\n    { name: \"Greenscale\", matrix: Matrices.GREENSCALE_MATRIX },\n    { name: \"Grayscale\", matrix: Matrices.GRAYSCALE_MATRIX },\n    { name: \"Invert\", matrix: Matrices.INVERT_MATRIX },\n  ]\n\n  // ColorMatrix effect that can cycle through all matrices\n  class ColorMatrixEffect {\n    private currentIndex = 0\n\n    public get currentMatrixName(): string {\n      return colorMatrixRegistry[this.currentIndex].name\n    }\n\n    public apply(buffer: OptimizedBuffer): void {\n      const { matrix } = colorMatrixRegistry[this.currentIndex]\n      buffer.colorMatrixUniform(matrix, 1.0)\n    }\n\n    public nextMatrix(): void {\n      this.currentIndex = (this.currentIndex + 1) % colorMatrixRegistry.length\n    }\n\n    public previousMatrix(): void {\n      this.currentIndex = (this.currentIndex - 1 + colorMatrixRegistry.length) % colorMatrixRegistry.length\n    }\n  }\n\n  const colorMatrixEffectInstance = new ColorMatrixEffect()\n\n  const filterFunctions: { name: string; func: ((buffer: OptimizedBuffer, deltaTime: number) => void) | null }[] = [\n    { name: \"None\", func: null },\n    { name: \"Scanlines\", func: (buf, _dt) => Filters.applyScanlines(buf, 0.85) },\n    { name: \"Vignette\", func: vignetteEffectInstance.apply.bind(vignetteEffectInstance) },\n    { name: \"Color Matrix\", func: colorMatrixEffectInstance.apply.bind(colorMatrixEffectInstance) },\n    { name: \"Noise\", func: (buf, _dt) => Filters.applyNoise(buf, 0.05) },\n    { name: \"Chromatic Aberration\", func: (buf, _dt) => Filters.applyChromaticAberration(buf, 2) },\n    { name: \"ASCII Art\", func: (buf, _dt) => Filters.applyAsciiArt(buf) },\n    { name: \"Distortion\", func: distortionEffectInstance.apply.bind(distortionEffectInstance) },\n    { name: \"Clouds\", func: cloudsEffectInstance.apply.bind(cloudsEffectInstance) },\n    { name: \"Flames\", func: flamesEffectInstance.apply.bind(flamesEffectInstance) },\n    { name: \"Rainbow Text\", func: rainbowTextEffectInstance.apply.bind(rainbowTextEffectInstance) },\n    { name: \"CRT Rolling Bar\", func: crtRollingBarEffectInstance.apply.bind(crtRollingBarEffectInstance) },\n    {\n      name: \"Pipboy\",\n      func: (buf, dt) => {\n        pipboyVignetteEffectInstance.apply(buf)\n        buf.colorMatrixUniform(Matrices.GREENSCALE_MATRIX, 1.0)\n        pipboyBarEffectInstance.apply(buf, dt)\n      },\n    },\n    { name: \"Brightness\", func: (buf, _dt) => Filters.applyBrightness(buf, brightnessValue) },\n    { name: \"Gain\", func: (buf, _dt) => Filters.applyGain(buf, gainValue) },\n    {\n      name: \"Saturation\",\n      func: (buf, _dt) => Filters.applySaturation(buf, saturationCellMask ?? undefined, saturationValue),\n    },\n  ]\n\n  // Box in the background to show alpha channel works\n  const backgroundBox = new BoxRenderable(renderer, {\n    id: \"shader-cube-box\",\n    position: \"absolute\",\n    left: 5,\n    top: 5,\n    width: WIDTH - 10,\n    height: HEIGHT - 10,\n    backgroundColor: \"#131336\",\n    zIndex: 0,\n    borderStyle: \"single\",\n    borderColor: \"#FFFFFF\",\n    title: \"Shader Cube Demo\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentContainer.add(backgroundBox)\n\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"shader-cube-main\",\n    width: WIDTH,\n    height: HEIGHT,\n    zIndex: 10,\n    respectAlpha: true,\n  })\n  renderer.root.add(framebufferRenderable)\n  const { frameBuffer: framebuffer } = framebufferRenderable\n\n  const engine = new ThreeCliRenderer(renderer, {\n    width: WIDTH,\n    height: HEIGHT,\n    focalLength: 8,\n    backgroundColor: RGBA.fromInts(0, 0, 0, 0),\n    alpha: true,\n  })\n  await engine.init()\n\n  const sceneRoot = new ThreeScene()\n\n  const mainLightNode = new ThreeDirectionalLight(new Color(1, 1, 1), 0.8)\n  mainLightNode.position.set(-10, -5, 1)\n  mainLightNode.target.position.set(0, 0, 0)\n  mainLightNode.name = \"main_light\"\n\n  sceneRoot.add(mainLightNode)\n  sceneRoot.add(mainLightNode.target)\n\n  const pointLightNode = new ThreePointLight(new Color(1, 220 / 255, 180 / 255), 2.0, 4)\n  pointLightNode.position.set(1.5, 0, 0)\n  pointLightNode.name = \"point_light\"\n  sceneRoot.add(pointLightNode)\n\n  const ambientLightNode = new AmbientLight(new Color(0.25, 0.25, 0.25), 1)\n  ambientLightNode.name = \"ambient_light\"\n  sceneRoot.add(ambientLightNode)\n\n  const lightVisualizerGeometry = new BoxGeometry(0.2, 0.2, 0.2)\n  const lightVisualizerMaterial = new MeshPhongMaterial({\n    color: 0x000000,\n    emissive: new Color(1.0, 0.8, 0.4),\n    emissiveIntensity: 1.0,\n    shininess: 0,\n  })\n  const lightVisualizerMesh = new ThreeMesh(lightVisualizerGeometry, lightVisualizerMaterial)\n  lightVisualizerMesh.name = \"light_viz\"\n  lightVisualizerMesh.position.copy(pointLightNode.position)\n  sceneRoot.add(lightVisualizerMesh)\n\n  // Create textures\n  const redTexture = TextureUtils.createCheckerboard(\n    256,\n    new Color(255 / 255, 40 / 255, 40 / 255),\n    new Color(180 / 255, 10 / 255, 10 / 255),\n  )\n  const greenTexture = TextureUtils.createCheckerboard(\n    256,\n    new Color(40 / 255, 255 / 255, 40 / 255),\n    new Color(10 / 255, 180 / 255, 10 / 255),\n  )\n  const blueTexture = TextureUtils.createCheckerboard(\n    256,\n    new Color(40 / 255, 40 / 255, 255 / 255),\n    new Color(10 / 255, 10 / 255, 180 / 255),\n  )\n  const yellowTexture = TextureUtils.createCheckerboard(\n    256,\n    new Color(255 / 255, 255 / 255, 40 / 255),\n    new Color(180 / 255, 180 / 255, 10 / 255),\n  )\n  const cyanTexture = TextureUtils.createCheckerboard(\n    256,\n    new Color(40 / 255, 255 / 255, 255 / 255),\n    new Color(10 / 255, 180 / 255, 180 / 255),\n  )\n  const magentaTexture = TextureUtils.createCheckerboard(\n    256,\n    new Color(255 / 255, 40 / 255, 255 / 255),\n    new Color(180 / 255, 10 / 255, 180 / 255),\n  )\n  const specularMapTexture = TextureUtils.createGradient(\n    256,\n    new Color(1, 1, 1),\n    new Color(0.2, 0.2, 0.2),\n    \"horizontal\",\n  )\n  const emissiveMapTexture = TextureUtils.createGradient(256, new Color(1, 0.6, 0), new Color(0, 0, 0), \"radial\")\n  const normalMapTexture = TextureUtils.createNoise(\n    256,\n    2,\n    3,\n    new Color(127 / 255, 127 / 255, 255 / 255),\n    new Color(127 / 255, 127 / 255, 127 / 255),\n  )\n\n  const materials: MeshPhongMaterial[] = [\n    new MeshPhongMaterial({ map: redTexture, shininess: 30, specular: new Color(0.8, 0.8, 0.8) }),\n    new MeshPhongMaterial({ map: greenTexture, shininess: 30, specular: new Color(0.8, 0.8, 0.8) }),\n    new MeshPhongMaterial({ map: blueTexture, shininess: 30, specular: new Color(0.8, 0.8, 0.8) }),\n    new MeshPhongMaterial({ map: yellowTexture, shininess: 30, specular: new Color(0.8, 0.8, 0.8) }),\n    new MeshPhongMaterial({ map: cyanTexture, shininess: 30, specular: new Color(0.8, 0.8, 0.8) }),\n    new MeshPhongMaterial({ map: magentaTexture, shininess: 30, specular: new Color(0.8, 0.8, 0.8) }),\n    new MeshPhongMaterial({\n      color: new Color(1, 1, 1),\n      specular: new Color(1, 1, 1),\n      shininess: 80,\n    }),\n  ]\n\n  const cubeGeometry = new BoxGeometry(1.0, 1.0, 1.0)\n  const cubeMeshNode = new ThreeMesh(cubeGeometry, materials[0])\n  cubeMeshNode.name = \"cube\"\n\n  sceneRoot.add(cubeMeshNode)\n\n  const cameraNode = new PerspectiveCamera(45, engine.aspectRatio, 1.0, 100.0)\n  cameraNode.position.set(0, 0, CAM_DISTANCE)\n  cameraNode.name = \"main_camera\"\n\n  sceneRoot.add(cameraNode)\n  engine.setActiveCamera(cameraNode)\n\n  // Initialize state variables\n  let currentFilterIndex = 0\n  let time = 0\n  let lightColorMode = 0\n  let rotationEnabled = true\n  let showLightVisualizers = true\n  let customLightsEnabled = true\n  let currentMaterial = 0\n  let manualMaterialSelection = false\n  let specularMapEnabled = false\n  let normalMapEnabled = false\n  let emissiveMapEnabled = false\n\n  // Create UI elements\n  let uiLine = 0\n  const lightVizText = new TextRenderable(renderer, {\n    id: \"shader-light-viz\",\n    content: \"Light Visualization: ON (V to toggle)\",\n    position: \"absolute\",\n    left: 0,\n    top: uiLine++,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(lightVizText)\n\n  const lightColorText = new TextRenderable(renderer, {\n    id: \"shader-light-color\",\n    content: \"Point Light: Warm (C to change)\",\n    position: \"absolute\",\n    left: 0,\n    top: uiLine++,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(lightColorText)\n\n  const customLightsText = new TextRenderable(renderer, {\n    id: \"shader-custom-lights\",\n    content: \"Custom Lights: ON (L to toggle)\",\n    position: \"absolute\",\n    left: 0,\n    top: uiLine++,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(customLightsText)\n\n  const materialToggleText = new TextRenderable(renderer, {\n    id: \"shader-material-toggle\",\n    content: \"Material: Auto-cycling (M to toggle, N to change)\",\n    position: \"absolute\",\n    left: 0,\n    top: uiLine++,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(materialToggleText)\n\n  const textureEffectsText = new TextRenderable(renderer, {\n    id: \"shader-texture-effects\",\n    content: \"Texture Effects: P-Specular [OFF] | B-Normal [OFF] | I-Emissive [OFF]\",\n    position: \"absolute\",\n    left: 0,\n    top: uiLine++,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(textureEffectsText)\n\n  const filterStatusText = new TextRenderable(renderer, {\n    id: \"shader-filter-status\",\n    content: `Filter: ${filterFunctions[currentFilterIndex].name} (J/K to cycle)`,\n    position: \"absolute\",\n    left: 0,\n    top: uiLine++,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(filterStatusText)\n\n  const param1StatusText = new TextRenderable(renderer, {\n    id: \"shader-param1-status\",\n    content: ``,\n    position: \"absolute\",\n    left: 0,\n    top: uiLine++,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  param1StatusText.visible = false\n  parentContainer.add(param1StatusText)\n\n  const param2StatusText = new TextRenderable(renderer, {\n    id: \"shader-param2-status\",\n    content: ``,\n    position: \"absolute\",\n    left: 0,\n    top: uiLine++,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  param2StatusText.visible = false\n  parentContainer.add(param2StatusText)\n\n  const controlsText = new TextRenderable(renderer, {\n    id: \"shader-controls\",\n    content:\n      \"WASD: Move | QE: Rotate | ZX: Zoom | V: Light Viz | C: Light Color | L: Lights | M/N: Material | P/B/I: Maps | R: Reset | Space: Rotation | J/K Filter | [/]{/} Params | T: Saturation Mode\",\n    position: \"absolute\",\n    left: 0,\n    top: HEIGHT - 2,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(controlsText)\n\n  function updateParameterUI() {\n    const selectedFilter = filterFunctions[currentFilterIndex]\n    let param1Text = \"\"\n    let param1Visible = false\n\n    switch (selectedFilter.name) {\n      case \"Distortion\":\n        param1Text = `Distortion Chance: ${distortionEffectInstance.glitchChancePerSecond.toFixed(2)} ([/])`\n        param1Visible = true\n        break\n      case \"Vignette\":\n        param1Text = `Vignette Strength: ${vignetteEffectInstance.strength.toFixed(2)} ([/])`\n        param1Visible = true\n        break\n      case \"Brightness\":\n        param1Text = `Brightness Factor: ${brightnessValue.toFixed(2)} ([/])`\n        param1Visible = true\n        break\n      case \"Gain\":\n        param1Text = `Gain Factor: ${gainValue.toFixed(2)} ([/])`\n        param1Visible = true\n        break\n      case \"Saturation\":\n        param1Text = `Saturation: ${saturationValue.toFixed(2)} (T: ${saturationFullScreen ? \"Full\" : \"Half\"}) ([/])`\n        param1Visible = true\n        break\n      case \"Color Matrix\":\n        param1Text = `Matrix: ${colorMatrixEffectInstance.currentMatrixName} ([/] to cycle)`\n        param1Visible = true\n        break\n      case \"Clouds\":\n        param1Text = `Clouds: scale=${cloudsEffectInstance.scale.toFixed(3)} ([/] to adjust)`\n        param2StatusText.content = `speed=${cloudsEffectInstance.speed.toFixed(3)} ({/} to adjust)`\n        param1Visible = true\n        param2StatusText.visible = true\n        break\n      case \"Flames\":\n        param1Text = `Flames: scale=${flamesEffectInstance.scale.toFixed(3)} ([/] to adjust)`\n        param2StatusText.content = `speed=${flamesEffectInstance.speed.toFixed(3)} ({/} to adjust)`\n        param1Visible = true\n        param2StatusText.visible = true\n        break\n      case \"Rainbow Text\":\n        param1Text = `Rainbow: speed=${rainbowTextEffectInstance.speed.toFixed(3)} ([/] to adjust)`\n        param2StatusText.content = `repeats=${rainbowTextEffectInstance.repeats.toFixed(1)} ({/} to adjust)`\n        param1Visible = true\n        param2StatusText.visible = true\n        break\n      case \"CRT Rolling Bar\":\n        param1Text = `CRT Bar: speed=${crtRollingBarEffectInstance.speed.toFixed(2)} ([/] to adjust)`\n        param2StatusText.content = `intensity=${crtRollingBarEffectInstance.intensity.toFixed(2)} ({/} to adjust)`\n        param1Visible = true\n        param2StatusText.visible = true\n        break\n      case \"Pipboy\":\n        param1Text = `Pipboy: bar speed=${pipboyBarEffectInstance.speed.toFixed(2)} ([/] to adjust)`\n        param2StatusText.content = `vignette=${pipboyVignetteEffectInstance.strength.toFixed(2)} ({/} to adjust)`\n        param1Visible = true\n        param2StatusText.visible = true\n        break\n    }\n\n    param1StatusText.content = param1Text\n    param1StatusText.visible = param1Visible\n    if (\n      selectedFilter.name !== \"Clouds\" &&\n      selectedFilter.name !== \"Flames\" &&\n      selectedFilter.name !== \"Rainbow Text\" &&\n      selectedFilter.name !== \"CRT Rolling Bar\" &&\n      selectedFilter.name !== \"Pipboy\"\n    ) {\n      param2StatusText.content = \"\"\n      param2StatusText.visible = false\n    }\n  }\n\n  function updateTextureEffectsUI() {\n    textureEffectsText.content = `Texture Effects: P-Specular [${specularMapEnabled ? \"ON\" : \"OFF\"}] | B-Normal [${normalMapEnabled ? \"ON\" : \"OFF\"}] | I-Emissive [${emissiveMapEnabled ? \"ON\" : \"OFF\"}]`\n  }\n\n  const keyHandler = (key: KeyEvent) => {\n    const cubeObject = sceneRoot.getObjectByName(\"cube\") as ThreeMesh | undefined\n\n    if (key.name === \"w\") cameraNode.translateY(CAMERA_PAN_STEP)\n    else if (key.name === \"s\") cameraNode.translateY(-CAMERA_PAN_STEP)\n    else if (key.name === \"a\") cameraNode.translateX(-CAMERA_PAN_STEP)\n    else if (key.name === \"d\") cameraNode.translateX(CAMERA_PAN_STEP)\n    if (key.name === \"q\") cameraNode.rotateY(0.1)\n    else if (key.name === \"e\") cameraNode.rotateY(-0.1)\n    if (key.name === \"z\") cameraNode.translateZ(CAMERA_ZOOM_STEP)\n    else if (key.name === \"x\") cameraNode.translateZ(-CAMERA_ZOOM_STEP)\n    if (key.name === \"r\") {\n      cameraNode.position.set(0, 0, CAM_DISTANCE)\n      cameraNode.rotation.set(0, 0, 0)\n      cameraNode.lookAt(0, 0, 0)\n    }\n    if (key.name === \"space\") rotationEnabled = !rotationEnabled\n\n    // Toggle light visualization\n    if (key.name === \"v\") {\n      showLightVisualizers = !showLightVisualizers\n      const vizObject = sceneRoot.getObjectByName(\"light_viz\")\n      if (vizObject) {\n        vizObject.visible = showLightVisualizers\n      }\n      lightVizText.content = `Light Visualization: ${showLightVisualizers ? \"ON\" : \"OFF\"} (V to toggle)`\n    }\n\n    // Add light color cycling\n    if (key.name === \"c\") {\n      lightColorMode = (lightColorMode + 1) % lightColors.length\n      const colorInfo = lightColors[lightColorMode]\n\n      if (pointLightNode) {\n        pointLightNode.color.setRGB(colorInfo.color[0] / 255, colorInfo.color[1] / 255, colorInfo.color[2] / 255)\n\n        const vizObject = sceneRoot.getObjectByName(\"light_viz\") as ThreeMesh | undefined\n        if (vizObject && vizObject.material instanceof MeshPhongMaterial) {\n          vizObject.material.emissive.setRGB(\n            colorInfo.color[0] / 255,\n            colorInfo.color[1] / 255,\n            colorInfo.color[2] / 255,\n          )\n        }\n      }\n      lightColorText.content = `Point Light: ${colorInfo.name} (C to change)`\n    }\n\n    // Toggle custom lights\n    if (key.name === \"l\") {\n      customLightsEnabled = !customLightsEnabled\n      if (mainLightNode) mainLightNode.visible = customLightsEnabled\n      if (pointLightNode) pointLightNode.visible = customLightsEnabled\n      customLightsText.content = `Custom Lights: ${customLightsEnabled ? \"ON\" : \"OFF\"} (L to toggle)`\n    }\n\n    // Material toggling\n    if (key.name === \"m\") {\n      manualMaterialSelection = !manualMaterialSelection\n      materialToggleText.content = `Material: ${manualMaterialSelection ? \"Manual\" : \"Auto-cycling\"} (M to toggle, N to change)`\n    }\n    if (key.name === \"n\") {\n      currentMaterial = (currentMaterial + 1) % materials.length\n      materialToggleText.content = `Material: ${manualMaterialSelection ? \"Manual\" : \"Auto-cycling\"} (#${currentMaterial}${currentMaterial === 6 ? \" - White\" : \"\"}) (M/N)`\n      if (cubeObject) {\n        const newMaterialInstance = materials[currentMaterial]\n        cubeObject.material = newMaterialInstance\n      }\n    }\n\n    // Toggle super sampling\n    if (key.name === \"u\") {\n      engine.toggleSuperSampling()\n    }\n\n    // Cycle through region modes for current filter (if applicable)\n    // NOTE: Region cycling removed - effects now apply to entire buffer\n    // Previously handled by key 'h'\n\n    // Toggle debug mode for console caller info\n    if (key.name === \"o\") {\n      renderer.console.toggle()\n    }\n\n    // Toggle texture effects\n    let effectsChanged = false\n    if (key.name === \"p\") {\n      specularMapEnabled = !specularMapEnabled\n      effectsChanged = true\n    } else if (key.name === \"b\") {\n      normalMapEnabled = !normalMapEnabled\n      effectsChanged = true\n    } else if (key.name === \"i\") {\n      emissiveMapEnabled = !emissiveMapEnabled\n      effectsChanged = true\n    }\n\n    if (effectsChanged) {\n      if (cubeObject) {\n        const material = cubeObject.material as MeshPhongMaterial\n        material.specularMap = specularMapEnabled ? specularMapTexture : null\n        material.normalMap = normalMapEnabled ? normalMapTexture : null\n        material.emissiveMap = emissiveMapEnabled ? emissiveMapTexture : null\n        material.emissive = new Color(0, 0, 0)\n        material.emissiveIntensity = emissiveMapEnabled ? 0.7 : 0.0\n        material.needsUpdate = true\n      }\n      updateTextureEffectsUI()\n    }\n\n    let filterChanged = false\n    if (key.name === \"j\") {\n      currentFilterIndex = (currentFilterIndex - 1 + filterFunctions.length) % filterFunctions.length\n      filterChanged = true\n    } else if (key.name === \"k\") {\n      currentFilterIndex = (currentFilterIndex + 1) % filterFunctions.length\n      filterChanged = true\n    }\n\n    if (filterChanged) {\n      const selectedFilter = filterFunctions[currentFilterIndex]\n      renderer.clearPostProcessFns()\n      if (selectedFilter.func) {\n        renderer.addPostProcessFn(selectedFilter.func)\n      }\n      filterStatusText.content = `Filter: ${selectedFilter.name} (J/K to cycle)`\n      updateParameterUI()\n    }\n\n    let paramChanged = false\n\n    if (key.name === \"t\" && filterFunctions[currentFilterIndex].name === \"Saturation\") {\n      saturationFullScreen = !saturationFullScreen\n      if (saturationFullScreen) {\n        // null cellMask = uniform saturation (uses colorMatrixUniform, much faster)\n        saturationCellMask = null\n      } else {\n        // cellMask = selective saturation on right half\n        saturationCellMask = createRightHalfCellMask(renderer.terminalWidth, renderer.terminalHeight)\n      }\n      paramChanged = true\n    }\n\n    // Parameter Adjustment Keys ([ / ] and { / })\n    const currentFilterName = filterFunctions[currentFilterIndex].name\n    const height = renderer.terminalHeight\n\n    if (key.name === \"[\") {\n      switch (currentFilterName) {\n        case \"Distortion\":\n          distortionEffectInstance.glitchChancePerSecond = Math.max(\n            0,\n            distortionEffectInstance.glitchChancePerSecond - 0.1,\n          )\n          paramChanged = true\n          break\n        case \"Vignette\":\n          vignetteEffectInstance.strength = Math.max(0, vignetteEffectInstance.strength - 0.05)\n          paramChanged = true\n          break\n        case \"Brightness\":\n          brightnessValue = Math.max(-1.0, brightnessValue - 0.05)\n          paramChanged = true\n          break\n        case \"Gain\":\n          gainValue = Math.max(0, gainValue - 0.05)\n          paramChanged = true\n          break\n        case \"Saturation\":\n          saturationValue = Math.max(0, saturationValue - 0.05)\n          paramChanged = true\n          break\n        case \"Color Matrix\":\n          colorMatrixEffectInstance.previousMatrix()\n          paramChanged = true\n          break\n        case \"Clouds\":\n          cloudsEffectInstance.scale = Math.max(0.05, cloudsEffectInstance.scale - 0.01)\n          paramChanged = true\n          break\n        case \"Flames\":\n          flamesEffectInstance.scale = Math.max(0.01, flamesEffectInstance.scale - 0.002)\n          paramChanged = true\n          break\n        case \"Rainbow Text\":\n          rainbowTextEffectInstance.speed = Math.max(0, rainbowTextEffectInstance.speed - 0.001)\n          paramChanged = true\n          break\n        case \"CRT Rolling Bar\":\n          crtRollingBarEffectInstance.speed = Math.max(0.1, crtRollingBarEffectInstance.speed - 0.1)\n          paramChanged = true\n          break\n        case \"Pipboy\":\n          pipboyBarEffectInstance.speed = Math.max(0.1, pipboyBarEffectInstance.speed - 0.1)\n          paramChanged = true\n          break\n      }\n    } else if (key.name === \"]\") {\n      switch (currentFilterName) {\n        case \"Distortion\":\n          distortionEffectInstance.glitchChancePerSecond = Math.min(\n            25,\n            distortionEffectInstance.glitchChancePerSecond + 0.1,\n          )\n          paramChanged = true\n          break\n        case \"Vignette\":\n          vignetteEffectInstance.strength = Math.min(5, vignetteEffectInstance.strength + 0.05)\n          paramChanged = true\n          break\n        case \"Brightness\":\n          brightnessValue = Math.min(1.0, brightnessValue + 0.05)\n          paramChanged = true\n          break\n        case \"Gain\":\n          gainValue = Math.min(50, gainValue + 0.05)\n          paramChanged = true\n          break\n        case \"Saturation\":\n          saturationValue = Math.min(10, saturationValue + 0.05)\n          paramChanged = true\n          break\n        case \"Color Matrix\":\n          colorMatrixEffectInstance.nextMatrix()\n          paramChanged = true\n          break\n        case \"Clouds\":\n          cloudsEffectInstance.scale = Math.min(1.0, cloudsEffectInstance.scale + 0.01)\n          paramChanged = true\n          break\n        case \"Flames\":\n          flamesEffectInstance.scale = Math.min(0.1, flamesEffectInstance.scale + 0.002)\n          paramChanged = true\n          break\n        case \"Rainbow Text\":\n          rainbowTextEffectInstance.speed = Math.min(0.5, rainbowTextEffectInstance.speed + 0.001)\n          paramChanged = true\n          break\n        case \"CRT Rolling Bar\":\n          crtRollingBarEffectInstance.speed = Math.min(5.0, crtRollingBarEffectInstance.speed + 0.1)\n          paramChanged = true\n          break\n        case \"Pipboy\":\n          pipboyBarEffectInstance.speed = Math.min(10.0, pipboyBarEffectInstance.speed + 0.1)\n          paramChanged = true\n          break\n      }\n    }\n\n    // Parameter 2 Adjustment ({/})\n    if (key.name === \"{\") {\n      switch (currentFilterName) {\n        case \"Distortion\":\n          distortionEffectInstance.maxGlitchLines = Math.max(0, distortionEffectInstance.maxGlitchLines - 1)\n          paramChanged = true\n          break\n        case \"Clouds\":\n          cloudsEffectInstance.speed = Math.max(0.0, cloudsEffectInstance.speed - 0.001)\n          paramChanged = true\n          break\n        case \"Flames\":\n          flamesEffectInstance.speed = Math.max(0.005, flamesEffectInstance.speed - 0.001)\n          paramChanged = true\n          break\n        case \"Rainbow Text\":\n          rainbowTextEffectInstance.repeats = Math.max(1.0, rainbowTextEffectInstance.repeats - 0.5)\n          paramChanged = true\n          break\n        case \"CRT Rolling Bar\":\n          crtRollingBarEffectInstance.intensity = Math.max(0.0, crtRollingBarEffectInstance.intensity - 0.05)\n          paramChanged = true\n          break\n        case \"Pipboy\":\n          pipboyVignetteEffectInstance.strength = Math.max(0.0, pipboyVignetteEffectInstance.strength - 0.05)\n          paramChanged = true\n          break\n      }\n    } else if (key.name === \"}\") {\n      switch (currentFilterName) {\n        case \"Distortion\":\n          distortionEffectInstance.maxGlitchLines = Math.min(height - 1, distortionEffectInstance.maxGlitchLines + 1)\n          paramChanged = true\n          break\n        case \"Clouds\":\n          cloudsEffectInstance.speed = Math.min(0.02, cloudsEffectInstance.speed + 0.001)\n          paramChanged = true\n          break\n        case \"Flames\":\n          flamesEffectInstance.speed = Math.min(0.1, flamesEffectInstance.speed + 0.001)\n          paramChanged = true\n          break\n        case \"Rainbow Text\":\n          rainbowTextEffectInstance.repeats = Math.min(20.0, rainbowTextEffectInstance.repeats + 0.5)\n          paramChanged = true\n          break\n        case \"CRT Rolling Bar\":\n          crtRollingBarEffectInstance.intensity = Math.min(1.0, crtRollingBarEffectInstance.intensity + 0.05)\n          paramChanged = true\n          break\n        case \"Pipboy\":\n          pipboyVignetteEffectInstance.strength = Math.min(3.0, pipboyVignetteEffectInstance.strength + 0.05)\n          paramChanged = true\n          break\n      }\n    }\n\n    if (paramChanged) {\n      updateParameterUI()\n    }\n  }\n\n  const resizeHandler = (width: number, height: number) => {\n    framebuffer.resize(width, height)\n\n    if (cameraNode) {\n      cameraNode.aspect = engine.aspectRatio\n      cameraNode.updateProjectionMatrix()\n    }\n\n    backgroundBox.width = width - 10\n    backgroundBox.height = height - 10\n    controlsText.y = height - 2\n  }\n\n  renderer.keyInput.on(\"keypress\", keyHandler)\n  renderer.on(\"resize\", resizeHandler)\n\n  renderer.setFrameCallback(async (deltaMs) => {\n    const deltaTime = deltaMs / 1000\n    time += deltaTime\n    const cubeObject = sceneRoot.getObjectByName(\"cube\") as ThreeMesh | undefined\n\n    if (rotationEnabled && cubeObject) {\n      cubeObject.rotation.x += rotationSpeed[0] * deltaTime\n      cubeObject.rotation.y += rotationSpeed[1] * deltaTime\n      cubeObject.rotation.z += rotationSpeed[2] * deltaTime\n    }\n\n    if (pointLightNode) {\n      const radius = 3\n      const speed = 0.9\n      pointLightNode.position.set(Math.sin(time * speed) * radius, 1.5, Math.cos(time * speed) * radius)\n\n      const vizObject = sceneRoot.getObjectByName(\"light_viz\")\n      if (vizObject) {\n        vizObject.position.copy(pointLightNode.position)\n      }\n    }\n\n    if (cubeObject) {\n      let materialIndex = currentMaterial\n      if (!manualMaterialSelection) {\n        materialIndex = Math.floor(time * 0.5) % (materials.length - 1)\n      }\n\n      if (materialIndex < materials.length && cubeObject.material !== materials[materialIndex]) {\n        const newMaterialInstance = materials[materialIndex]\n        cubeObject.material = newMaterialInstance\n\n        const material = cubeObject.material as MeshPhongMaterial\n        material.specularMap = specularMapEnabled ? specularMapTexture : null\n        material.normalMap = normalMapEnabled ? normalMapTexture : null\n        material.emissiveMap = emissiveMapEnabled ? emissiveMapTexture : null\n        material.emissive = new Color(0, 0, 0)\n        material.emissiveIntensity = emissiveMapEnabled ? 0.7 : 0.0\n        material.needsUpdate = true\n      }\n    }\n\n    framebuffer.clear(RGBA.fromValues(0, 0, 0, 0))\n    await engine.drawScene(sceneRoot, framebuffer, deltaTime)\n  })\n\n  // Store state for cleanup\n  demoState = {\n    engine,\n    sceneRoot,\n    cameraNode,\n    mainLightNode,\n    pointLightNode,\n    ambientLightNode,\n    lightVisualizerMesh,\n    cubeMeshNode,\n    materials,\n    distortionEffectInstance,\n    vignetteEffectInstance,\n    cloudsEffectInstance,\n    flamesEffectInstance,\n    rainbowTextEffectInstance,\n    crtRollingBarEffectInstance,\n    pipboyVignetteEffectInstance,\n    pipboyBarEffectInstance,\n    brightnessValue,\n    gainValue,\n    colorMatrixEffectInstance,\n    filterFunctions,\n    currentFilterIndex,\n    time,\n    lightColorMode,\n    rotationEnabled,\n    showLightVisualizers,\n    customLightsEnabled,\n    currentMaterial,\n    manualMaterialSelection,\n    specularMapEnabled,\n    normalMapEnabled,\n    emissiveMapEnabled,\n    parentContainer,\n    backgroundBox,\n    lightVizText,\n    lightColorText,\n    customLightsText,\n    materialToggleText,\n    textureEffectsText,\n    filterStatusText,\n    param1StatusText,\n    param2StatusText,\n    controlsText,\n    keyHandler,\n    resizeHandler,\n    frameCallbackId: true,\n  }\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (!demoState) return\n\n  renderer.keyInput.off(\"keypress\", demoState.keyHandler)\n  renderer.root.removeListener(\"resize\", demoState.resizeHandler)\n\n  if (demoState.frameCallbackId) {\n    renderer.clearFrameCallbacks()\n  }\n\n  demoState.engine.destroy()\n  renderer.clearPostProcessFns()\n\n  renderer.root.remove(\"shader-cube-main\")\n  renderer.root.remove(\"shader-cube-container\")\n\n  demoState = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/simple-layout-example.ts",
    "content": "import { CliRenderer, BoxRenderable, TextRenderable, createCliRenderer, type KeyEvent } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\ninterface LayoutDemo {\n  name: string\n  description: string\n  setup: () => void\n}\n\nlet renderer: CliRenderer | null = null\nlet header: BoxRenderable | null = null\nlet headerText: TextRenderable | null = null\nlet contentArea: BoxRenderable | null = null\nlet sidebar: BoxRenderable | null = null\nlet sidebarText: TextRenderable | null = null\nlet mainContent: BoxRenderable | null = null\nlet mainContentText: TextRenderable | null = null\nlet rightSidebar: BoxRenderable | null = null\nlet rightSidebarText: TextRenderable | null = null\nlet footer: BoxRenderable | null = null\nlet footerText: TextRenderable | null = null\nlet moveableElement: BoxRenderable | null = null\nlet moveableText: TextRenderable | null = null\nlet absolutePositionedBox: BoxRenderable | null = null\nlet absolutePositionedText: TextRenderable | null = null\nlet currentDemoIndex = 0\nlet autoAdvanceTimeout: Timer | null = null\nlet autoplayEnabled = true\nlet moveableElementVisible = true\nlet moveableElementX = 0\nlet moveableElementY = 0\n\nconst layoutDemos: LayoutDemo[] = [\n  {\n    name: \"Horizontal Layout\",\n    description: \"Sidebar on left, main content on right\",\n    setup: () => setupHorizontalLayout(),\n  },\n  {\n    name: \"Vertical Layout\",\n    description: \"Sidebar on top, main content below\",\n    setup: () => setupVerticalLayout(),\n  },\n  {\n    name: \"Centered Layout\",\n    description: \"Content centered with margins\",\n    setup: () => setupCenteredLayout(),\n  },\n  {\n    name: \"Three Column\",\n    description: \"Left sidebar, center content, right sidebar\",\n    setup: () => setupThreeColumnLayout(),\n  },\n]\n\nfunction resetElementLayout(element: BoxRenderable): void {\n  element.flexBasis = \"auto\"\n  element.flexGrow = 0\n  element.flexShrink = 0\n  element.width = \"auto\"\n  element.height = \"auto\"\n\n  element.minWidth = undefined\n  element.maxWidth = undefined\n  element.minHeight = undefined\n  element.maxHeight = undefined\n}\n\nfunction setupHorizontalLayout(): void {\n  if (!contentArea || !sidebar || !mainContent || !rightSidebar) return\n\n  sidebar.visible = true\n  mainContent.visible = true\n  rightSidebar.visible = false\n\n  resetElementLayout(sidebar)\n  resetElementLayout(mainContent)\n\n  contentArea.flexDirection = \"row\"\n  contentArea.alignItems = \"stretch\"\n\n  const sidebarWidth = Math.max(15, Math.floor(renderer!.terminalWidth * 0.2))\n  sidebar.flexBasis = sidebarWidth\n  sidebar.flexGrow = 0\n  sidebar.flexShrink = 0\n  sidebar.width = sidebarWidth\n  sidebar.minWidth = 15\n  sidebar.height = \"auto\"\n  if (sidebarText) sidebarText.content = \"LEFT SIDEBAR\"\n  sidebar.backgroundColor = \"#64748b\"\n\n  mainContent.flexBasis = \"auto\"\n  mainContent.flexGrow = 1\n  mainContent.flexShrink = 1\n  mainContent.width = \"auto\"\n  mainContent.minWidth = 20\n  mainContent.height = \"auto\"\n  if (mainContentText) mainContentText.content = \"MAIN CONTENT\"\n  mainContent.backgroundColor = \"#eab308\"\n}\n\nfunction setupVerticalLayout(): void {\n  if (!contentArea || !sidebar || !mainContent || !rightSidebar) return\n\n  sidebar.visible = true\n  mainContent.visible = true\n  rightSidebar.visible = false\n\n  resetElementLayout(sidebar)\n  resetElementLayout(mainContent)\n\n  contentArea.flexDirection = \"column\"\n  contentArea.alignItems = \"stretch\"\n\n  const contentHeight = renderer!.terminalHeight - 6\n  const topBarHeight = Math.max(3, Math.floor(contentHeight * 0.2))\n  sidebar.flexBasis = topBarHeight\n  sidebar.flexGrow = 0\n  sidebar.flexShrink = 0\n  sidebar.height = topBarHeight\n  sidebar.minHeight = 3\n  sidebar.width = \"auto\"\n  if (sidebarText) sidebarText.content = \"TOP BAR\"\n  sidebar.backgroundColor = \"#059669\"\n\n  mainContent.flexBasis = \"auto\"\n  mainContent.flexGrow = 1\n  mainContent.flexShrink = 1\n  mainContent.height = \"auto\"\n  mainContent.minHeight = 5\n  mainContent.width = \"auto\"\n  if (mainContentText) mainContentText.content = \"MAIN CONTENT\"\n  mainContent.backgroundColor = \"#eab308\"\n}\n\nfunction setupCenteredLayout(): void {\n  if (!contentArea || !sidebar || !mainContent || !rightSidebar) return\n\n  sidebar.visible = false\n  mainContent.visible = true\n  rightSidebar.visible = false\n\n  resetElementLayout(mainContent)\n\n  contentArea.flexDirection = \"row\"\n  contentArea.alignItems = \"stretch\"\n  contentArea.justifyContent = \"center\"\n\n  const centerWidth = Math.max(30, Math.floor(renderer!.terminalWidth * 0.6))\n  mainContent.flexBasis = centerWidth\n  mainContent.flexGrow = 0\n  mainContent.flexShrink = 0\n  mainContent.width = centerWidth\n  mainContent.minWidth = 30\n  mainContent.maxWidth = Math.floor(renderer!.terminalWidth * 0.8)\n  mainContent.height = \"auto\"\n  if (mainContentText) mainContentText.content = \"CENTERED CONTENT\"\n  mainContent.backgroundColor = \"#7c3aed\"\n}\n\nfunction setupThreeColumnLayout(): void {\n  if (!contentArea || !sidebar || !mainContent || !rightSidebar) return\n\n  sidebar.visible = true\n  mainContent.visible = true\n  rightSidebar.visible = true\n\n  resetElementLayout(sidebar)\n  resetElementLayout(mainContent)\n  resetElementLayout(rightSidebar)\n\n  contentArea.flexDirection = \"row\"\n  contentArea.alignItems = \"stretch\"\n\n  const terminalWidth = renderer!.terminalWidth\n  const sidebarWidth = Math.max(12, Math.floor(terminalWidth * 0.15))\n\n  sidebar.flexBasis = sidebarWidth\n  sidebar.flexGrow = 0\n  sidebar.flexShrink = 0\n  sidebar.width = sidebarWidth\n  sidebar.minWidth = 12\n  sidebar.height = \"auto\"\n  if (sidebarText) sidebarText.content = \"LEFT\"\n  sidebar.backgroundColor = \"#dc2626\"\n\n  mainContent.flexBasis = \"auto\"\n  mainContent.flexGrow = 1\n  mainContent.flexShrink = 1\n  mainContent.width = \"auto\"\n  mainContent.minWidth = 20\n  mainContent.height = \"auto\"\n  if (mainContentText) mainContentText.content = \"CENTER\"\n  mainContent.backgroundColor = \"#059669\"\n\n  rightSidebar.flexBasis = sidebarWidth\n  rightSidebar.flexGrow = 0\n  rightSidebar.flexShrink = 0\n  rightSidebar.width = sidebarWidth\n  rightSidebar.minWidth = 12\n  rightSidebar.height = \"auto\"\n  if (rightSidebarText) rightSidebarText.content = \"RIGHT\"\n  rightSidebar.backgroundColor = \"#7c3aed\"\n}\n\nfunction createLayoutElements(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#001122\")\n\n  header = new BoxRenderable(renderer, {\n    id: \"header\",\n    zIndex: 0,\n    width: \"auto\",\n    height: 3,\n    backgroundColor: \"#3b82f6\",\n    borderStyle: \"single\",\n    alignItems: \"center\",\n    border: true,\n  })\n\n  headerText = new TextRenderable(renderer, {\n    id: \"header-text\",\n    content: \"LAYOUT DEMO\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n    zIndex: 1,\n  })\n\n  header.add(headerText)\n\n  contentArea = new BoxRenderable(renderer, {\n    id: \"content-area\",\n    zIndex: 0,\n    width: \"auto\",\n    height: \"auto\",\n    flexDirection: \"row\",\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  sidebar = new BoxRenderable(renderer, {\n    id: \"sidebar\",\n    zIndex: 0,\n    width: \"auto\",\n    height: \"auto\",\n    backgroundColor: \"#64748b\",\n    borderStyle: \"single\",\n    flexGrow: 0,\n    flexShrink: 0,\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    border: true,\n  })\n\n  sidebarText = new TextRenderable(renderer, {\n    id: \"sidebar-text\",\n    content: \"SIDEBAR\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n    zIndex: 1,\n  })\n\n  sidebar.add(sidebarText)\n\n  mainContent = new BoxRenderable(renderer, {\n    id: \"main-content\",\n    zIndex: 0,\n    width: \"auto\",\n    height: \"auto\",\n    backgroundColor: \"#919599\",\n    borderStyle: \"single\",\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    border: true,\n  })\n\n  mainContentText = new TextRenderable(renderer, {\n    id: \"main-content-text\",\n    content: \"MAIN CONTENT\",\n    fg: \"#1e293b\",\n    bg: \"transparent\",\n    zIndex: 1,\n  })\n\n  mainContent.add(mainContentText)\n\n  rightSidebar = new BoxRenderable(renderer, {\n    id: \"right-sidebar\",\n    zIndex: 0,\n    width: \"auto\",\n    height: \"auto\",\n    backgroundColor: \"#7c3aed\",\n    borderStyle: \"single\",\n    flexGrow: 0,\n    flexShrink: 0,\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    border: true,\n  })\n\n  rightSidebarText = new TextRenderable(renderer, {\n    id: \"right-sidebar-text\",\n    content: \"RIGHT\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n    zIndex: 1,\n  })\n\n  rightSidebar.add(rightSidebarText)\n\n  footer = new BoxRenderable(renderer, {\n    id: \"footer\",\n    zIndex: 0,\n    width: \"auto\",\n    height: 3,\n    backgroundColor: \"#1e40af\",\n    borderStyle: \"single\",\n    flexGrow: 0,\n    flexShrink: 0,\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    border: true,\n  })\n\n  footerText = new TextRenderable(renderer, {\n    id: \"footer-text\",\n    content: \"\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n    zIndex: 1,\n  })\n\n  footer.add(footerText)\n\n  moveableElement = new BoxRenderable(renderer, {\n    id: \"moveable\",\n    zIndex: 100,\n    width: 8,\n    height: 3,\n    backgroundColor: \"#ff6b6b\",\n    borderStyle: \"single\",\n    borderColor: \"#ff4757\",\n    position: \"absolute\",\n    left: 0,\n    top: 0,\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    border: true,\n  })\n\n  moveableText = new TextRenderable(renderer, {\n    id: \"moveable-text\",\n    content: \"MOVE\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n    zIndex: 101,\n  })\n\n  moveableElement.add(moveableText)\n\n  absolutePositionedBox = new BoxRenderable(renderer, {\n    id: \"absolute-positioned-box\",\n    zIndex: 150,\n    width: 20,\n    height: 3,\n    backgroundColor: \"#22c55e\",\n    borderStyle: \"single\",\n    borderColor: \"#16a34a\",\n    position: \"absolute\",\n    bottom: 1,\n    right: 1,\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    border: true,\n  })\n\n  absolutePositionedText = new TextRenderable(renderer, {\n    id: \"absolute-positioned-text\",\n    content: \"BOTTOM RIGHT\",\n    fg: \"#ffffff\",\n    bg: \"transparent\",\n    zIndex: 151,\n  })\n\n  absolutePositionedBox.add(absolutePositionedText)\n\n  // Add all elements to contentArea in the correct order: left, center, right\n  contentArea.add(sidebar)\n  contentArea.add(mainContent)\n  contentArea.add(rightSidebar)\n\n  // Set initial visibility (rightSidebar is hidden for the first demo)\n  rightSidebar.visible = false\n\n  renderer.root.add(header)\n  renderer.root.add(contentArea)\n  renderer.root.add(footer)\n  renderer.root.add(moveableElement)\n  renderer.root.add(absolutePositionedBox)\n\n  centerMoveableElement()\n  updateFooterText()\n  renderer.on(\"resize\", handleResize)\n}\n\nfunction handleResize(width: number, height: number): void {\n  // Root layout is automatically resized by the renderer\n  centerMoveableElement()\n}\n\nfunction handleKeyPress(key: KeyEvent): void {\n  switch (key.name) {\n    case \"space\": // Space - next layout\n      nextDemo()\n      break\n    case \"r\": // R - restart cycle\n      currentDemoIndex = 0\n      applyCurrentDemo()\n      break\n    case \"p\": // P - toggle autoplay\n      toggleAutoplay()\n      break\n    case \"v\": // V - toggle moveable element visibility\n      toggleMoveableElement()\n      break\n    case \"w\": // W - move up\n      moveMoveableElement(0, -1)\n      break\n    case \"a\": // A - move left\n      moveMoveableElement(-1, 0)\n      break\n    case \"s\": // S - move down\n      moveMoveableElement(0, 1)\n      break\n    case \"d\": // D - move right\n      moveMoveableElement(1, 0)\n      break\n  }\n}\n\nfunction nextDemo(): void {\n  currentDemoIndex = (currentDemoIndex + 1) % layoutDemos.length\n  applyCurrentDemo()\n}\n\nfunction toggleAutoplay(): void {\n  autoplayEnabled = !autoplayEnabled\n\n  if (autoplayEnabled) {\n    if (autoAdvanceTimeout) {\n      clearTimeout(autoAdvanceTimeout)\n    }\n    autoAdvanceTimeout = setTimeout(() => {\n      nextDemo()\n    }, 4000)\n  } else {\n    if (autoAdvanceTimeout) {\n      clearTimeout(autoAdvanceTimeout)\n      autoAdvanceTimeout = null\n    }\n  }\n\n  updateFooterText()\n}\n\nfunction toggleMoveableElement(): void {\n  if (!moveableElement) return\n\n  moveableElementVisible = !moveableElementVisible\n  moveableElement.visible = moveableElementVisible\n  updateFooterText()\n}\n\nfunction moveMoveableElement(deltaX: number, deltaY: number): void {\n  if (!moveableElement || !renderer) return\n\n  moveableElementX += deltaX\n  moveableElementY += deltaY\n\n  moveableElementX = Math.max(0, Math.min(renderer.terminalWidth - 8, moveableElementX))\n  moveableElementY = Math.max(0, Math.min(renderer.terminalHeight - 3, moveableElementY))\n\n  moveableElement.setPosition({\n    left: moveableElementX,\n    top: moveableElementY,\n  })\n}\n\nfunction centerMoveableElement(): void {\n  if (!renderer || !moveableElement) return\n\n  moveableElementX = Math.floor((renderer.terminalWidth - 8) / 2)\n  moveableElementY = Math.floor((renderer.terminalHeight - 3) / 2)\n\n  moveableElement.setPosition({\n    left: moveableElementX,\n    top: moveableElementY,\n  })\n}\n\nfunction updateFooterText(): void {\n  if (!footerText) return\n\n  const autoplayStatus = autoplayEnabled ? \"ON\" : \"OFF\"\n  const moveableStatus = moveableElementVisible ? \"ON\" : \"OFF\"\n  footerText.content = `SPACE: next | R: restart | P: autoplay (${autoplayStatus}) | V: overlay (${moveableStatus}) | WASD: move`\n}\n\nfunction applyCurrentDemo(): void {\n  const demo = layoutDemos[currentDemoIndex]\n  if (!headerText) return\n\n  const autoplayStatus = autoplayEnabled ? \"AUTO\" : \"MANUAL\"\n  headerText.content = `${demo.name} (${currentDemoIndex + 1}/${layoutDemos.length}) - ${autoplayStatus}`\n  demo.setup()\n\n  if (autoAdvanceTimeout) {\n    clearTimeout(autoAdvanceTimeout)\n  }\n\n  if (autoplayEnabled) {\n    autoAdvanceTimeout = setTimeout(() => {\n      nextDemo()\n    }, 4000)\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  createLayoutElements(rendererInstance)\n  rendererInstance.keyInput.on(\"keypress\", handleKeyPress)\n  currentDemoIndex = 0\n  applyCurrentDemo()\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (autoAdvanceTimeout) {\n    clearTimeout(autoAdvanceTimeout)\n    autoAdvanceTimeout = null\n  }\n\n  rendererInstance.keyInput.off(\"keypress\", handleKeyPress)\n\n  if (renderer) {\n    renderer.off(\"resize\", handleResize)\n  }\n\n  if (header) rendererInstance.root.remove(header.id)\n  if (contentArea) rendererInstance.root.remove(contentArea.id)\n  if (footer) rendererInstance.root.remove(footer.id)\n  if (moveableElement) rendererInstance.root.remove(moveableElement.id)\n  if (absolutePositionedBox) rendererInstance.root.remove(absolutePositionedBox.id)\n\n  header = null\n  headerText = null\n  contentArea = null\n  sidebar = null\n  sidebarText = null\n  mainContent = null\n  mainContentText = null\n  rightSidebar = null\n  rightSidebarText = null\n  footer = null\n  footerText = null\n  moveableElement = null\n  moveableText = null\n  absolutePositionedBox = null\n  absolutePositionedText = null\n  renderer = null\n  currentDemoIndex = 0\n  moveableElementVisible = true\n  moveableElementX = 0\n  moveableElementY = 0\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 30,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  // renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/slider-demo.ts",
    "content": "import { type CliRenderer, createCliRenderer, t, fg, bold, BoxRenderable, TextRenderable } from \"../index.js\"\nimport { SliderRenderable } from \"../renderables/Slider.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet horizontalSlider1: SliderRenderable | null = null\nlet horizontalSlider2: SliderRenderable | null = null\nlet horizontalSlider3: SliderRenderable | null = null\nlet verticalSlider1: SliderRenderable | null = null\nlet verticalSlider2: SliderRenderable | null = null\nlet verticalSlider3: SliderRenderable | null = null\nlet animatedVerticalSlider: SliderRenderable | null = null\nlet renderer: CliRenderer | null = null\nlet mainContainer: BoxRenderable | null = null\nlet instructionsBox: BoxRenderable | null = null\nlet keyboardHandler: ((key: any) => void) | null = null\nlet frameCallback: ((deltaTime: number) => Promise<void>) | null = null\nlet animationTime = 0\n\nlet lastActionText: string = \"Welcome to SliderRenderable demo! Use mouse to interact with sliders.\"\nlet lastActionColor: string = \"#FFCC00\"\n\n// Value display elements\nlet h1ValueText: TextRenderable | null = null\nlet h2ValueText: TextRenderable | null = null\nlet h3ValueText: TextRenderable | null = null\nlet v1ValueText: TextRenderable | null = null\nlet v2ValueText: TextRenderable | null = null\nlet v3ValueText: TextRenderable | null = null\nlet vAValueText: TextRenderable | null = null\n\nfunction updateDisplays() {\n  // Update individual slider value displays\n  if (h1ValueText && horizontalSlider1) {\n    h1ValueText.content = t`${bold(fg(\"#e0af68\")(\"Value:\"))} ${horizontalSlider1.value.toFixed(1)}`\n  }\n  if (h2ValueText && horizontalSlider2) {\n    h2ValueText.content = t`${bold(fg(\"#bb9af7\")(\"Value:\"))} ${horizontalSlider2.value.toFixed(1)}`\n  }\n  if (h3ValueText && horizontalSlider3) {\n    h3ValueText.content = t`${bold(fg(\"#FF6B6B\")(\"Value:\"))} ${horizontalSlider3.value.toFixed(2)}`\n  }\n  if (v1ValueText && verticalSlider1) {\n    v1ValueText.content = t`${bold(fg(\"#f7768e\")(verticalSlider1.value.toFixed(1)))}`\n  }\n  if (v2ValueText && verticalSlider2) {\n    v2ValueText.content = t`${bold(fg(\"#ff9e64\")(verticalSlider2.value.toFixed(1)))}`\n  }\n  if (v3ValueText && verticalSlider3) {\n    v3ValueText.content = t`${bold(fg(\"#73daca\")(verticalSlider3.value.toFixed(1)))}`\n  }\n  if (vAValueText && animatedVerticalSlider) {\n    vAValueText.content = t`${bold(fg(\"#FF6B6B\")(animatedVerticalSlider.value.toFixed(2)))}`\n  }\n}\n\nfunction resetSliders() {\n  if (horizontalSlider1) horizontalSlider1.value = 25\n  if (horizontalSlider2) horizontalSlider2.value = 100\n  if (horizontalSlider3) horizontalSlider3.value = 25\n  if (verticalSlider1) verticalSlider1.value = 0\n  if (verticalSlider2) verticalSlider2.value = 0\n  if (verticalSlider3) verticalSlider3.value = 50\n  if (animatedVerticalSlider) animatedVerticalSlider.value = 50\n\n  lastActionText = \"*** All sliders reset to default values ***\"\n  lastActionColor = \"#FF00FF\"\n  updateDisplays()\n\n  setTimeout(() => {\n    lastActionColor = \"#FFCC00\"\n    updateDisplays()\n  }, 1000)\n}\n\nfunction focusSlider(index: number) {\n  // Remove focus from all sliders first\n  horizontalSlider1?.blur()\n  horizontalSlider2?.blur()\n  horizontalSlider3?.blur()\n  verticalSlider1?.blur()\n  verticalSlider2?.blur()\n  verticalSlider3?.blur()\n  animatedVerticalSlider?.blur()\n\n  let slider: SliderRenderable | null = null\n  let sliderName = \"\"\n\n  switch (index) {\n    case 1:\n      slider = horizontalSlider1\n      sliderName = \"H1 (1h×100w)\"\n      break\n    case 2:\n      slider = horizontalSlider2\n      sliderName = \"H2 (5h×100w)\"\n      break\n    case 3:\n      slider = horizontalSlider3\n      sliderName = \"H3 (1h×80w, animated)\"\n      break\n    case 4:\n      slider = verticalSlider1\n      sliderName = \"V1 (15h×1w)\"\n      break\n    case 5:\n      slider = verticalSlider2\n      sliderName = \"V2 (15h×3w)\"\n      break\n    case 6:\n      slider = verticalSlider3\n      sliderName = \"V3 (15h×5w)\"\n      break\n    case 7:\n      slider = animatedVerticalSlider\n      sliderName = \"VA (10h×2w, animated)\"\n      break\n  }\n\n  if (slider) {\n    slider.focus()\n    lastActionText = `Focused: ${sliderName}`\n    lastActionColor = \"#00FF00\"\n    updateDisplays()\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#1a1b26\")\n  renderer.start()\n\n  mainContainer = new BoxRenderable(renderer, {\n    id: \"slider-demo-main-container\",\n    flexGrow: 1,\n    maxHeight: \"100%\",\n    maxWidth: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#1a1b26\",\n  })\n  renderer.root.add(mainContainer)\n\n  // Create sliders container\n  const slidersContainer = new BoxRenderable(renderer, {\n    id: \"sliders-container\",\n    width: \"100%\",\n    flexGrow: 1,\n    flexDirection: \"column\",\n    backgroundColor: \"#1a1b26\",\n    padding: 2,\n  })\n\n  // Horizontal Slider 1 - 1-height (very thin) - now H1\n  const h1Container = new BoxRenderable(renderer, {\n    id: \"h1-container\",\n    width: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#24283b\",\n    marginBottom: 1,\n    padding: 1,\n  })\n\n  const h1Label = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#e0af68\")(\"H1\"))} ${fg(\"#565f89\")(\"- 1h×100w (0-50)\")}`,\n  })\n\n  h1ValueText = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#e0af68\")(\"Value:\"))} 25.0`,\n  })\n\n  horizontalSlider1 = new SliderRenderable(renderer, {\n    id: \"horizontal-slider-1\",\n    orientation: \"horizontal\",\n    width: \"100%\",\n    height: 1,\n    value: 25,\n    min: 0,\n    max: 50,\n    viewPortSize: 1,\n    backgroundColor: \"#414868\",\n    foregroundColor: \"#e0af68\",\n    onChange: (value: number) => {\n      lastActionText = `H1: ${value.toFixed(1)}`\n      lastActionColor = \"#FFA500\"\n      updateDisplays()\n    },\n  })\n\n  h1Container.add(h1Label)\n  h1Container.add(h1ValueText)\n  h1Container.add(horizontalSlider1)\n\n  // Horizontal Slider 2 - 5-height (thick) - now H2\n  const h2Container = new BoxRenderable(renderer, {\n    id: \"h2-container\",\n    width: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#24283b\",\n    marginBottom: 1,\n    padding: 1,\n  })\n\n  const h2Label = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#bb9af7\")(\"H2\"))} ${fg(\"#565f89\")(\"- 5h×100w (0-200)\")}`,\n  })\n\n  h2ValueText = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#bb9af7\")(\"Value:\"))} 100.0`,\n  })\n\n  horizontalSlider2 = new SliderRenderable(renderer, {\n    id: \"horizontal-slider-2\",\n    orientation: \"horizontal\",\n    width: \"100%\",\n    height: 5,\n    value: 100,\n    min: 0,\n    max: 200,\n    viewPortSize: 50,\n    backgroundColor: \"#414868\",\n    foregroundColor: \"#bb9af7\",\n    onChange: (value: number) => {\n      lastActionText = `H2: ${value.toFixed(1)}`\n      lastActionColor = \"#BB9AF7\"\n      updateDisplays()\n    },\n  })\n\n  h2Container.add(h2Label)\n  h2Container.add(h2ValueText)\n  h2Container.add(horizontalSlider2)\n\n  // Horizontal Slider 3 - Animated (sub-cell rendering) - now H3\n  const h3Container = new BoxRenderable(renderer, {\n    id: \"h3-container\",\n    width: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#24283b\",\n    marginBottom: 1,\n    padding: 1,\n  })\n\n  const h3Label = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#FF6B6B\")(\"H3\"))} ${fg(\"#565f89\")(\"- 1h×80w (animated, sub-cell rendering)\")}`,\n  })\n\n  h3ValueText = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#FF6B6B\")(\"Value:\"))} 25.00`,\n  })\n\n  horizontalSlider3 = new SliderRenderable(renderer, {\n    id: \"horizontal-slider-3\",\n    orientation: \"horizontal\",\n    height: 1,\n    value: 25,\n    min: 0,\n    max: 50,\n    viewPortSize: 0.1, // Fine step size for smooth animation\n    backgroundColor: \"#414868\",\n    foregroundColor: \"#FF6B6B\",\n    onChange: (value: number) => {\n      // Update the animated horizontal slider value display\n      updateDisplays()\n    },\n  })\n\n  h3Container.add(h3Label)\n  h3Container.add(h3ValueText)\n  h3Container.add(horizontalSlider3)\n\n  // Vertical sliders container\n  const verticalContainer = new BoxRenderable(renderer, {\n    id: \"vertical-container\",\n    width: \"100%\",\n    height: 17,\n    flexDirection: \"row\",\n    backgroundColor: \"#1a1b26\",\n    marginBottom: 1,\n    padding: 1,\n  })\n\n  // Vertical Slider 1 - 1-width (very narrow)\n  const v1Container = new BoxRenderable(renderer, {\n    id: \"v1-container\",\n    width: 8,\n    height: \"100%\",\n    flexDirection: \"column\",\n    alignItems: \"flex-end\",\n    backgroundColor: \"#24283b\",\n    marginRight: 1,\n    padding: 1,\n  })\n\n  const v1SliderWrapper = new BoxRenderable(renderer, {\n    id: \"v1-slider-wrapper\",\n    flexDirection: \"row\",\n    height: \"100%\",\n    flexGrow: 1,\n  })\n\n  const v1Label = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#f7768e\")(\"V1\"))}\n${fg(\"#565f89\")(\"1w\")}`,\n    width: 3,\n  })\n\n  verticalSlider1 = new SliderRenderable(renderer, {\n    id: \"vertical-slider-1\",\n    orientation: \"vertical\",\n    width: 1,\n    height: \"100%\",\n    value: 0,\n    min: -10,\n    max: 10,\n    viewPortSize: 1,\n    backgroundColor: \"#414868\",\n    foregroundColor: \"#f7768e\",\n    onChange: (value: number) => {\n      lastActionText = `V1: ${value.toFixed(1)}`\n      lastActionColor = \"#FF00FF\"\n      updateDisplays()\n    },\n  })\n\n  v1ValueText = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#f7768e\")(\"0.0\"))}`,\n  })\n\n  v1SliderWrapper.add(v1Label)\n  v1SliderWrapper.add(verticalSlider1)\n  v1Container.add(v1SliderWrapper)\n  v1Container.add(v1ValueText)\n\n  // Vertical Slider 2 - 3-width (medium)\n  const v2Container = new BoxRenderable(renderer, {\n    id: \"v2-container\",\n    width: 10,\n    height: \"100%\",\n    flexDirection: \"column\",\n    alignItems: \"flex-end\",\n    backgroundColor: \"#24283b\",\n    marginRight: 1,\n    padding: 1,\n  })\n\n  const v2SliderWrapper = new BoxRenderable(renderer, {\n    id: \"v2-slider-wrapper\",\n    flexDirection: \"row\",\n    height: \"100%\",\n    flexGrow: 1,\n  })\n\n  const v2Label = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#ff9e64\")(\"V2\"))}\n${fg(\"#565f89\")(\"3w\")}`,\n    width: 3,\n  })\n\n  verticalSlider2 = new SliderRenderable(renderer, {\n    id: \"vertical-slider-2\",\n    orientation: \"vertical\",\n    width: 3,\n    height: \"100%\",\n    value: 0,\n    min: -50,\n    max: 50,\n    viewPortSize: 5,\n    backgroundColor: \"#414868\",\n    foregroundColor: \"#ff9e64\",\n    onChange: (value: number) => {\n      lastActionText = `V2: ${value.toFixed(1)}`\n      lastActionColor = \"#FF9E64\"\n      updateDisplays()\n    },\n  })\n\n  v2ValueText = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#ff9e64\")(\"0.0\"))}`,\n  })\n\n  v2SliderWrapper.add(v2Label)\n  v2SliderWrapper.add(verticalSlider2)\n  v2Container.add(v2SliderWrapper)\n  v2Container.add(v2ValueText)\n\n  // Vertical Slider 3 - 5-width (wide)\n  const v3Container = new BoxRenderable(renderer, {\n    id: \"v3-container\",\n    width: 12,\n    height: \"100%\",\n    flexDirection: \"column\",\n    alignItems: \"flex-end\",\n    backgroundColor: \"#24283b\",\n    marginRight: 1,\n    padding: 1,\n  })\n\n  const v3SliderWrapper = new BoxRenderable(renderer, {\n    id: \"v3-slider-wrapper\",\n    flexDirection: \"row\",\n    height: \"100%\",\n    flexGrow: 1,\n  })\n\n  const v3Label = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#73daca\")(\"V3\"))}\n${fg(\"#565f89\")(\"5w\")}`,\n    width: 3,\n  })\n\n  verticalSlider3 = new SliderRenderable(renderer, {\n    id: \"vertical-slider-3\",\n    orientation: \"vertical\",\n    width: 5,\n    height: \"100%\",\n    value: 50,\n    min: 0,\n    max: 100,\n    viewPortSize: 10,\n    backgroundColor: \"#414868\",\n    foregroundColor: \"#73daca\",\n    onChange: (value: number) => {\n      lastActionText = `V3: ${value.toFixed(1)}`\n      lastActionColor = \"#73DACA\"\n      updateDisplays()\n    },\n  })\n\n  v3ValueText = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#73daca\")(\"50.0\"))}`,\n  })\n\n  v3SliderWrapper.add(v3Label)\n  v3SliderWrapper.add(verticalSlider3)\n  v3Container.add(v3SliderWrapper)\n  v3Container.add(v3ValueText)\n\n  // Animated Vertical Slider - demonstrates sub-cell rendering\n  const animatedVContainer = new BoxRenderable(renderer, {\n    id: \"animated-v-container\",\n    width: 10,\n    height: \"100%\",\n    flexDirection: \"column\",\n    alignItems: \"flex-end\",\n    backgroundColor: \"#24283b\",\n    marginRight: 1,\n    padding: 1,\n  })\n\n  const animatedVSliderWrapper = new BoxRenderable(renderer, {\n    id: \"animated-v-slider-wrapper\",\n    flexDirection: \"row\",\n    height: \"100%\",\n    flexGrow: 1,\n  })\n\n  const animatedVLabel = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#FF6B6B\")(\"VA\"))}\n${fg(\"#565f89\")(\"2w\")}`,\n    width: 3,\n  })\n\n  animatedVerticalSlider = new SliderRenderable(renderer, {\n    id: \"animated-vertical-slider\",\n    orientation: \"vertical\",\n    width: 2,\n    height: 10,\n    value: 50,\n    min: 0,\n    max: 100,\n    viewPortSize: 0.2, // Fine step size for smooth animation\n    backgroundColor: \"#414868\",\n    foregroundColor: \"#FF6B6B\",\n    onChange: (value: number) => {\n      // Update the animated vertical slider value display\n      updateDisplays()\n    },\n  })\n\n  vAValueText = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#FF6B6B\")(\"50.00\"))}`,\n  })\n\n  animatedVSliderWrapper.add(animatedVLabel)\n  animatedVSliderWrapper.add(animatedVerticalSlider)\n  animatedVContainer.add(animatedVSliderWrapper)\n  animatedVContainer.add(vAValueText)\n\n  verticalContainer.add(v1Container)\n  verticalContainer.add(v2Container)\n  verticalContainer.add(v3Container)\n  verticalContainer.add(animatedVContainer)\n\n  // Add some spacing\n  const spacer = new BoxRenderable(renderer, {\n    id: \"spacer\",\n    width: \"100%\",\n    flexGrow: 1,\n  })\n\n  slidersContainer.add(h1Container)\n  slidersContainer.add(h2Container)\n  slidersContainer.add(h3Container)\n  slidersContainer.add(verticalContainer)\n  slidersContainer.add(spacer)\n\n  // Instructions box\n  instructionsBox = new BoxRenderable(renderer, {\n    id: \"instructions\",\n    width: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#2a2b3a\",\n    paddingLeft: 1,\n  })\n\n  const instructionsText1 = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Slider Demo\"))} ${fg(\"#565f89\")(\"-\")} ${bold(fg(\"#FFFF00\")(\"Mouse\"))} ${fg(\"#c0caf5\")(\"Click & drag on sliders\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#FFAA00\")(\"R\"))} ${fg(\"#c0caf5\")(\"Reset all\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#00FF00\")(\"1-7\"))} ${fg(\"#c0caf5\")(\"Focus sliders\")}`,\n  })\n\n  const instructionsText2 = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Features:\"))} ${fg(\"#c0caf5\")(\"Different ranges, step sizes, orientations & dimensions (1-5 height/width)\")}`,\n  })\n\n  instructionsBox.add(instructionsText1)\n  instructionsBox.add(instructionsText2)\n\n  mainContainer.add(slidersContainer)\n  mainContainer.add(instructionsBox)\n\n  updateDisplays()\n\n  keyboardHandler = (key) => {\n    if (key.name === \"r\") {\n      resetSliders()\n    } else if (key.name === \"1\") {\n      focusSlider(1)\n    } else if (key.name === \"2\") {\n      focusSlider(2)\n    } else if (key.name === \"3\") {\n      focusSlider(3)\n    } else if (key.name === \"4\") {\n      focusSlider(4)\n    } else if (key.name === \"5\") {\n      focusSlider(5)\n    } else if (key.name === \"6\") {\n      focusSlider(6)\n    } else if (key.name === \"7\") {\n      focusSlider(7)\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n\n  // Set up animation frame callback for animated sliders\n  frameCallback = async (deltaTime: number) => {\n    animationTime += deltaTime\n\n    // Animate horizontal slider - smooth sine wave motion covering full range\n    if (horizontalSlider3) {\n      const hValue = 25 + Math.sin(animationTime * 0.002) * 25\n      horizontalSlider3.value = Math.max(0, Math.min(50, hValue))\n    }\n\n    // Animate vertical slider - smooth cosine wave motion covering full range\n    if (animatedVerticalSlider) {\n      const vValue = 50 + Math.cos(animationTime * 0.0015) * 50\n      animatedVerticalSlider.value = Math.max(0, Math.min(100, vValue))\n    }\n  }\n  renderer.setFrameCallback(frameCallback)\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  if (frameCallback) {\n    rendererInstance.removeFrameCallback(frameCallback)\n    frameCallback = null\n  }\n\n  rendererInstance.root.getRenderable(\"slider-demo-main-container\")?.destroyRecursively()\n\n  mainContainer = null\n  horizontalSlider1 = null\n  horizontalSlider2 = null\n  horizontalSlider3 = null\n  verticalSlider1 = null\n  verticalSlider2 = null\n  verticalSlider3 = null\n  animatedVerticalSlider = null\n  h1ValueText = null\n  h2ValueText = null\n  h3ValueText = null\n  v1ValueText = null\n  v2ValueText = null\n  v3ValueText = null\n  vAValueText = null\n  instructionsBox = null\n  // Note: slider wrappers are automatically cleaned up by destroyRecursively\n  renderer = null\n  frameCallback = null\n  animationTime = 0\n\n  lastActionText = \"Welcome to SliderRenderable demo! Use mouse to interact with sliders.\"\n  lastActionColor = \"#FFCC00\"\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/split-mode-demo.ts",
    "content": "import { createCliRenderer, TextRenderable, t, type CliRenderer, BoxRenderable, bold, fg } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { createTimeline, type JSAnimation, Timeline } from \"../animation/Timeline.js\"\n\nlet text: TextRenderable | null = null\nlet instructionsText: TextRenderable | null = null\nlet keyHandler: ((key: any) => void) | null = null\nlet outputTimer: Timer | null = null\nlet animationSystem: SplitModeAnimations | null = null\nlet testOutputInterval = 100\n\nclass SplitModeAnimations {\n  private timeline: Timeline\n  private renderer: CliRenderer\n  private container: BoxRenderable\n\n  private systemLoadingBars: BoxRenderable[] = []\n  private movingOrbs: BoxRenderable[] = []\n  private statusCounters: TextRenderable[] = []\n  private pulsingElements: BoxRenderable[] = []\n\n  private systemProgress = { cpu: 0, memory: 0, network: 0, disk: 0 }\n  private counters = { packets: 0, connections: 0, processes: 0, uptime: 0 }\n  private orbPositions = [\n    { x: 2, y: 2 },\n    { x: 15, y: 3 },\n    { x: 30, y: 2 },\n  ]\n  private pulseValues = [1.0, 1.0, 1.0]\n\n  constructor(renderer: CliRenderer) {\n    this.renderer = renderer\n    this.timeline = createTimeline({\n      duration: 8000,\n      loop: true,\n    })\n\n    this.container = new BoxRenderable(renderer, {\n      id: \"animation-container\",\n      zIndex: 5,\n    })\n    this.renderer.root.add(this.container)\n\n    this.setupUI()\n    this.setupAnimations()\n    this.timeline.play()\n  }\n\n  private setupUI(): void {\n    const statusPanel = new BoxRenderable(this.renderer, {\n      id: \"status-panel\",\n      position: \"absolute\",\n      left: 2,\n      top: 5,\n      width: this.renderer.width - 6,\n      height: 8,\n      backgroundColor: \"#1a1a2e\",\n      zIndex: 1,\n      borderStyle: \"double\",\n      borderColor: \"#4a4a6a\",\n      title: \"◆ SYSTEM MONITOR ◆\",\n      titleAlignment: \"center\",\n      border: true,\n    })\n    this.container.add(statusPanel)\n\n    this.systemLoadingBars = []\n    const systems = [\n      { name: \"CPU\", color: \"#6a5acd\", y: 6 },\n      { name: \"MEM\", color: \"#4682b4\", y: 7 },\n      { name: \"NET\", color: \"#20b2aa\", y: 8 },\n      { name: \"DSK\", color: \"#daa520\", y: 9 },\n    ]\n\n    systems.forEach((system, index) => {\n      const label = new TextRenderable(this.renderer, {\n        id: `${system.name.toLowerCase()}-label`,\n        content: `${system.name}:`,\n        position: \"absolute\",\n        left: 4,\n        top: system.y,\n        fg: system.color,\n        zIndex: 2,\n      })\n      this.container.add(label)\n\n      const bgBar = new BoxRenderable(this.renderer, {\n        id: `${system.name.toLowerCase()}-bg`,\n        position: \"absolute\",\n        left: 9,\n        top: system.y,\n        width: this.renderer.width - 16,\n        height: 1,\n        backgroundColor: \"#333333\",\n        zIndex: 1,\n      })\n      this.container.add(bgBar)\n\n      const progressBar = new BoxRenderable(this.renderer, {\n        id: `${system.name.toLowerCase()}-progress`,\n        position: \"absolute\",\n        left: 9,\n        top: system.y,\n        width: 1,\n        height: 1,\n        backgroundColor: system.color,\n        zIndex: 2,\n      })\n      this.container.add(progressBar)\n      this.systemLoadingBars.push(progressBar)\n    })\n\n    const statsPanel = new BoxRenderable(this.renderer, {\n      id: \"stats-panel\",\n      position: \"absolute\",\n      left: 2,\n      top: 14,\n      width: this.renderer.width - 6,\n      height: 4,\n      backgroundColor: \"#2d1b2e\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#8a4a8a\",\n      title: \"◇ REAL-TIME STATS ◇\",\n      titleAlignment: \"center\",\n      border: true,\n    })\n    this.container.add(statsPanel)\n\n    this.statusCounters = []\n    const counterLabels = [\"PACKETS\", \"CONNECTIONS\", \"PROCESSES\", \"UPTIME\"]\n    counterLabels.forEach((label, index) => {\n      const counter = new TextRenderable(this.renderer, {\n        id: `counter-${index}`,\n        content: `${label}: 0`,\n        position: \"absolute\",\n        left: 4 + index * 15,\n        top: 15,\n        fg: \"#9a9acd\",\n        zIndex: 2,\n      })\n      this.container.add(counter)\n      this.statusCounters.push(counter)\n    })\n\n    this.movingOrbs = []\n    const orbColors = [\"#ff6b9d\", \"#4ecdc4\", \"#ffe66d\"]\n    orbColors.forEach((color, index) => {\n      const orb = new BoxRenderable(this.renderer, {\n        id: `orb-${index}`,\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 3,\n        height: 1,\n        backgroundColor: color,\n        zIndex: 3,\n      })\n      this.container.add(orb)\n      this.movingOrbs.push(orb)\n    })\n\n    this.pulsingElements = []\n    const pulseColors = [\"#ff8a80\", \"#80cbc4\", \"#fff176\"]\n    pulseColors.forEach((color, index) => {\n      const pulse = new BoxRenderable(this.renderer, {\n        id: `pulse-${index}`,\n        position: \"absolute\",\n        left: this.renderer.width - 8 + index * 2,\n        top: 1,\n        width: 1,\n        height: 1,\n        backgroundColor: color,\n        zIndex: 3,\n      })\n      this.container.add(pulse)\n      this.pulsingElements.push(pulse)\n    })\n  }\n\n  private setupAnimations(): void {\n    this.timeline.add(\n      this.systemProgress,\n      {\n        cpu: 85,\n        memory: 70,\n        network: 95,\n        disk: 60,\n        duration: 3000,\n        ease: \"inOutQuad\",\n        onUpdate: (values: JSAnimation) => {\n          const progress = values.targets[0]\n          const maxWidth = this.renderer.width - 16\n\n          this.systemLoadingBars[0].width = Math.max(1, Math.floor((progress.cpu / 100) * maxWidth))\n          this.systemLoadingBars[1].width = Math.max(1, Math.floor((progress.memory / 100) * maxWidth))\n          this.systemLoadingBars[2].width = Math.max(1, Math.floor((progress.network / 100) * maxWidth))\n          this.systemLoadingBars[3].width = Math.max(1, Math.floor((progress.disk / 100) * maxWidth))\n        },\n      },\n      0,\n    )\n\n    this.timeline.add(\n      this.systemProgress,\n      {\n        cpu: 20,\n        memory: 30,\n        network: 15,\n        disk: 25,\n        duration: 2000,\n        ease: \"inOutSine\",\n        onUpdate: (values: JSAnimation) => {\n          const progress = values.targets[0]\n          const maxWidth = this.renderer.width - 16\n\n          this.systemLoadingBars[0].width = Math.max(1, Math.floor((progress.cpu / 100) * maxWidth))\n          this.systemLoadingBars[1].width = Math.max(1, Math.floor((progress.memory / 100) * maxWidth))\n          this.systemLoadingBars[2].width = Math.max(1, Math.floor((progress.network / 100) * maxWidth))\n          this.systemLoadingBars[3].width = Math.max(1, Math.floor((progress.disk / 100) * maxWidth))\n        },\n      },\n      4000,\n    )\n\n    this.timeline.add(\n      this.counters,\n      {\n        packets: 12847,\n        connections: 234,\n        processes: 187,\n        uptime: 86400,\n        duration: 8000,\n        ease: \"linear\",\n        onUpdate: (values: JSAnimation) => {\n          const counters = values.targets[0]\n          this.statusCounters[0].content = `PACKETS: ${Math.floor(counters.packets)}`\n          this.statusCounters[1].content = `CONN: ${Math.floor(counters.connections)}`\n          this.statusCounters[2].content = `PROC: ${Math.floor(counters.processes)}`\n          this.statusCounters[3].content = `UP: ${Math.floor(counters.uptime)}s`\n        },\n      },\n      0,\n    )\n\n    this.orbPositions.forEach((orbPos, index) => {\n      this.timeline.add(\n        orbPos,\n        {\n          x: this.renderer.width - 10,\n          duration: 2000 + index * 400,\n          ease: \"inOutSine\",\n          onUpdate: (values: JSAnimation) => {\n            const pos = values.targets[0]\n            this.movingOrbs[index].x = Math.floor(pos.x)\n          },\n        },\n        index * 800,\n      )\n\n      this.timeline.add(\n        orbPos,\n        {\n          x: 2,\n          duration: 2000 + index * 400,\n          ease: \"inOutSine\",\n          onUpdate: (values: JSAnimation) => {\n            const pos = values.targets[0]\n            this.movingOrbs[index].x = Math.floor(pos.x)\n          },\n        },\n        4000 + index * 800,\n      )\n    })\n\n    this.pulseValues.forEach((pulseVal, index) => {\n      const pulseData = { intensity: 1.0 }\n      this.timeline.add(\n        pulseData,\n        {\n          intensity: 3.0,\n          duration: 1000,\n          ease: \"inOutQuad\",\n          loop: 8,\n          alternate: true,\n          onUpdate: (values: JSAnimation) => {\n            const intensity = values.targets[0].intensity\n            const height = Math.max(1, Math.floor(intensity))\n            this.pulsingElements[index].height = Math.min(3, height)\n          },\n        },\n        index * 300,\n      )\n    })\n  }\n\n  public update(deltaTime: number): void {\n    this.timeline.update(deltaTime)\n  }\n\n  public destroy(): void {\n    this.timeline.pause()\n    this.renderer.root.remove(\"animation-container\")\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  rendererInstance.setBackgroundColor(\"#001122\")\n  rendererInstance.experimental_splitHeight = 20\n\n  animationSystem = new SplitModeAnimations(rendererInstance)\n\n  text = new TextRenderable(rendererInstance, {\n    id: \"demo-text\",\n    position: \"absolute\",\n    left: 2,\n    top: 0,\n    width: rendererInstance.width - 4,\n    height: 2,\n    zIndex: 10,\n    content: t`${bold(fg(\"#00ffff\")(\"◆ SPLIT MODE DEMO - ANIMATED DASHBOARD ◆\"))}`,\n  })\n\n  instructionsText = new TextRenderable(rendererInstance, {\n    id: \"split-mode-instructions\",\n    position: \"absolute\",\n    left: 2,\n    top: 19,\n    width: rendererInstance.width - 4,\n    height: 2,\n    zIndex: 10,\n    content: t`${bold(fg(\"#cccccc\")(\"[+/-] Split height | [0] Toggle fullscreen | [M/L] Output speed | [U] Toggle mouse\"))}`,\n  })\n\n  rendererInstance.root.add(text)\n  rendererInstance.root.add(instructionsText)\n\n  rendererInstance.setFrameCallback(async (deltaTime: number) => {\n    if (animationSystem) {\n      animationSystem.update(deltaTime)\n    }\n  })\n\n  console.log(\"=== Split Mode Demo ===\")\n  console.log(`Terminal size: ${rendererInstance.terminalWidth}x${rendererInstance.terminalHeight}`)\n  console.log(`Renderer split height: ${rendererInstance.experimental_splitHeight}`)\n  console.log(`Renderer offset: ${rendererInstance.terminalHeight - rendererInstance.experimental_splitHeight}`)\n  console.log(\"Console output should appear here and scroll naturally\")\n  console.log(\"The renderer should stay fixed at the bottom as a footer\")\n  console.log(`Test output running at ${testOutputInterval}ms intervals (use M/L to adjust speed)`)\n  console.log(`Mouse functionality: ${rendererInstance.useMouse ? \"enabled\" : \"disabled\"} (use U to toggle)`)\n\n  let messageCount = 0\n\n  const startTestOutput = () => {\n    if (outputTimer) {\n      clearInterval(outputTimer)\n    }\n    outputTimer = setInterval(() => {\n      messageCount++\n      console.log(`Test output ${messageCount}: This should appear above the renderer and scroll naturally`)\n    }, testOutputInterval)\n  }\n\n  startTestOutput()\n\n  keyHandler = (key) => {\n    if (key.name === \"+\") {\n      const currentHeight = rendererInstance.experimental_splitHeight || 0\n      const newHeight = Math.min(currentHeight + 1, rendererInstance.terminalHeight - 5)\n      rendererInstance.experimental_splitHeight = newHeight\n      console.log(`Split height increased to ${newHeight}`)\n    } else if (key.name === \"-\") {\n      const currentHeight = rendererInstance.experimental_splitHeight || 0\n      const newHeight = Math.max(currentHeight - 1, 5)\n      rendererInstance.experimental_splitHeight = newHeight\n      console.log(`Split height decreased to ${newHeight}`)\n    } else if (key.name === \"0\") {\n      if (rendererInstance.experimental_splitHeight > 0) {\n        rendererInstance.experimental_splitHeight = 0\n        console.log(\"Switched to fullscreen mode\")\n      } else {\n        rendererInstance.experimental_splitHeight = 20\n        console.log(\"Switched to split mode (height 10)\")\n      }\n    } else if (key.name === \"m\") {\n      testOutputInterval = Math.max(5, testOutputInterval - 5)\n      startTestOutput()\n      console.log(`Test output speed increased (interval: ${testOutputInterval}ms)`)\n    } else if (key.name === \"l\") {\n      testOutputInterval = Math.min(1000, testOutputInterval + 5)\n      startTestOutput()\n      console.log(`Test output speed decreased (interval: ${testOutputInterval}ms)`)\n    } else if (key.name === \"u\") {\n      rendererInstance.useMouse = !rendererInstance.useMouse\n      console.log(`Mouse functionality ${rendererInstance.useMouse ? \"enabled\" : \"disabled\"}`)\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyHandler)\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (keyHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyHandler)\n    keyHandler = null\n  }\n\n  if (outputTimer) {\n    clearInterval(outputTimer)\n    outputTimer = null\n  }\n\n  if (animationSystem) {\n    animationSystem.destroy()\n    animationSystem = null\n  }\n\n  if (text) {\n    rendererInstance.root.remove(text.id)\n    text = null\n  }\n\n  if (instructionsText) {\n    rendererInstance.root.remove(instructionsText.id)\n    instructionsText = null\n  }\n\n  rendererInstance.clearFrameCallbacks()\n  rendererInstance.experimental_splitHeight = 0\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    targetFps: 30,\n    exitOnCtrlC: true,\n    useMouse: true,\n    useAlternateScreen: false,\n    useConsole: false,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/sprite-animation-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  TextRenderable,\n  FrameBufferRenderable,\n  BoxRenderable,\n  type KeyEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport * as THREE from \"three\"\nimport {\n  SpriteAnimator,\n  TiledSprite,\n  type SpriteDefinition,\n  type AnimationDefinition,\n} from \"../3d/animation/SpriteAnimator.js\"\nimport { SpriteResourceManager, type ResourceConfig } from \"../3d/SpriteResourceManager.js\"\nimport {\n  ExplosionManager,\n  type ExplosionHandle,\n  type ExplosionEffectParameters,\n} from \"../3d/animation/ExplodingSpriteEffect.js\"\n\n// @ts-ignore\nimport mainCharIdlePath from \"./assets/main_char_idle.png\" with { type: \"image/png\" }\nimport { randFloat } from \"three/src/math/MathUtils.js\"\nimport { MeshLambertNodeMaterial } from \"three/webgpu\"\nimport { ThreeCliRenderer } from \"../3d.js\"\n\ninterface SpriteAnimationDemoState {\n  engine: ThreeCliRenderer\n  scene: THREE.Scene\n  pCamera: THREE.PerspectiveCamera\n  oCamera: THREE.OrthographicCamera\n  spriteResourceManager: SpriteResourceManager\n  spriteAnimator: SpriteAnimator\n  explosionManager: ExplosionManager\n  mainChar: TiledSprite | null\n  mainCharExplosionHandle: ExplosionHandle | null\n  addedSprites: TiledSprite[]\n  activeExplosionHandles: ExplosionHandle[]\n  isPerspectiveActive: boolean\n  parentContainer: BoxRenderable\n  instructionsText: TextRenderable\n  cameraModeText: TextRenderable\n  keyHandler: ((key: KeyEvent) => void) | null\n}\n\nlet demoState: SpriteAnimationDemoState | null = null\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const initialTermWidth = renderer.terminalWidth\n  const initialTermHeight = renderer.terminalHeight\n\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"sprite-animation-container\",\n    zIndex: 15,\n  })\n  renderer.root.add(parentContainer)\n\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"main\",\n    width: initialTermWidth,\n    height: initialTermHeight,\n    zIndex: 10,\n  })\n  renderer.root.add(framebufferRenderable)\n  const { frameBuffer: framebuffer } = framebufferRenderable\n\n  const engine = new ThreeCliRenderer(renderer, {\n    width: initialTermWidth,\n    height: initialTermHeight,\n    focalLength: 1,\n    backgroundColor: RGBA.fromValues(0.1, 0.1, 0.2, 1.0),\n  })\n  await engine.init()\n\n  const scene = new THREE.Scene()\n\n  const pCamera = new THREE.PerspectiveCamera(75, engine.aspectRatio, 0.1, 1000)\n  pCamera.position.set(0, 0, 3)\n  pCamera.lookAt(0, 0, 0)\n  scene.add(pCamera)\n\n  const orthoViewHeight = 4.0\n  const orthoViewWidth = orthoViewHeight * engine.aspectRatio\n  const oCamera = new THREE.OrthographicCamera(\n    orthoViewWidth / -2,\n    orthoViewWidth / 2,\n    orthoViewHeight / 2,\n    orthoViewHeight / -2,\n    0.1,\n    1000,\n  )\n  oCamera.position.set(0, 0, 3)\n  oCamera.lookAt(0, 0, 0)\n  scene.add(oCamera)\n\n  const ambientLight = new THREE.AmbientLight(0xffffff, 1.2)\n  scene.add(ambientLight)\n\n  const spotlight1 = new THREE.SpotLight(0xff9999, 6.5)\n  spotlight1.position.set(-5, 0, 6)\n  spotlight1.target.position.set(-2, -2.5, 0)\n  spotlight1.penumbra = 0.3\n  spotlight1.angle = Math.PI / 1.7\n  spotlight1.distance = 25.0\n  spotlight1.power = 1000\n  scene.add(spotlight1.target)\n  scene.add(spotlight1)\n\n  const spotlight2 = spotlight1.clone()\n  spotlight2.color.set(0x6666ff)\n  spotlight2.position.set(5, 0, 6)\n  spotlight2.target.position.set(2, -2.5, 0)\n  scene.add(spotlight2.target)\n  scene.add(spotlight2)\n\n  let isPerspectiveActive = true\n  engine.setActiveCamera(pCamera)\n\n  const cameraModeText = new TextRenderable(renderer, {\n    id: \"cameraModeText\",\n    content: `Camera: Perspective (Press 'c' to switch)`,\n    position: \"absolute\",\n    left: 1,\n    top: 3,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(cameraModeText)\n\n  const spriteResourceManager = new SpriteResourceManager(scene)\n  const spriteAnimator = new SpriteAnimator(scene)\n  const explosionManager = new ExplosionManager(scene)\n\n  renderer.on(\"resize\", (newWidth, newHeight) => {\n    framebuffer.resize(newWidth, newHeight)\n\n    pCamera.aspect = engine.aspectRatio\n    pCamera.updateProjectionMatrix()\n\n    const newOrthoViewWidth = orthoViewHeight * engine.aspectRatio\n    oCamera.left = newOrthoViewWidth / -2\n    oCamera.right = newOrthoViewWidth / 2\n    oCamera.top = orthoViewHeight / 2\n    oCamera.bottom = orthoViewHeight / -2\n    oCamera.updateProjectionMatrix()\n  })\n\n  const NUM_SPRITES = 8\n\n  const mainCharResourceConfig: ResourceConfig = {\n    imagePath: mainCharIdlePath,\n    sheetNumFrames: NUM_SPRITES,\n  }\n\n  const mainCharResource = await spriteResourceManager.createResource(mainCharResourceConfig)\n\n  explosionManager.fillPool(mainCharResource, 5, { numRows: 50, numCols: 50 })\n  explosionManager.fillPool(mainCharResource, 128, { numRows: 4, numCols: 4 })\n\n  const mainCharIdleAnimation: AnimationDefinition = {\n    resource: mainCharResource,\n    frameDuration: 150,\n  }\n\n  const mainCharDef: SpriteDefinition = {\n    initialAnimation: \"idle\",\n    animations: {\n      idle: mainCharIdleAnimation,\n    },\n    scale: 8.0,\n  }\n\n  let mainChar: TiledSprite | null = null\n  let mainCharExplosionHandle: ExplosionHandle | null = null\n  let addedSprites: TiledSprite[] = []\n  const activeExplosionHandles: ExplosionHandle[] = []\n\n  const materialFactory = () =>\n    new MeshLambertNodeMaterial({\n      transparent: true,\n      alphaTest: 0.01,\n      side: THREE.DoubleSide,\n      depthWrite: true,\n    })\n\n  mainChar = await spriteAnimator.createSprite(mainCharDef, materialFactory)\n  if (mainChar) {\n    mainChar.setPosition(new THREE.Vector3(0, 0, 0.1))\n  }\n\n  if (mainChar) {\n    // Small and fast\n    const smallChar = await spriteAnimator.createSprite(\n      {\n        ...mainCharDef,\n        id: \"small_char\",\n        scale: 4.0,\n      },\n      materialFactory,\n    )\n    if (smallChar) {\n      smallChar.setPosition(new THREE.Vector3(-1.5, 0, 0))\n      smallChar.setFrameDuration(80)\n      smallChar.goToFrame(3)\n    }\n\n    // Large and slow\n    const largeChar = await spriteAnimator.createSprite(\n      {\n        ...mainCharDef,\n        id: \"large_char\",\n        scale: 6.0,\n      },\n      materialFactory,\n    )\n    if (largeChar) {\n      largeChar.setPosition(new THREE.Vector3(1.5, 0, 0))\n      largeChar.setFrameDuration(300)\n      largeChar.goToFrame(6)\n    }\n  }\n\n  function explodeRandomSprite(): void {\n    if (addedSprites.length === 0) {\n      console.log(\"No added sprites available to explode.\")\n      return\n    }\n\n    for (let i = 0; i < 4; i++) {\n      const randomIndex = Math.floor(Math.random() * addedSprites.length)\n      const spriteToExplode = addedSprites[randomIndex]\n\n      addedSprites.splice(randomIndex, 1)\n\n      const handle = explosionManager.createExplosionForSprite(spriteToExplode, {\n        numRows: 4,\n        numCols: 4,\n        durationMs: 3000,\n        strength: 2,\n        fadeOut: false,\n        materialFactory,\n      })\n\n      if (handle) {\n        activeExplosionHandles.push(handle)\n        console.log(\"💥 Random sprite exploded!\")\n      } else {\n        console.log(\"Failed to explode sprite.\")\n      }\n    }\n  }\n\n  const keyHandler = (key: KeyEvent) => {\n    if (key.name === \"u\") {\n      engine.toggleSuperSampling()\n    }\n\n    if (key.name === \"c\") {\n      isPerspectiveActive = !isPerspectiveActive\n      if (isPerspectiveActive) {\n        engine.setActiveCamera(pCamera)\n        cameraModeText.content = \"Camera: Perspective (Press 'c' to switch)\"\n        console.log(\"Switched to Perspective Camera\")\n      } else {\n        engine.setActiveCamera(oCamera)\n        cameraModeText.content = \"Camera: Orthographic (Press 'c' to switch)\"\n        console.log(\"Switched to Orthographic Camera\")\n      }\n    }\n\n    if (key.name === \"e\") {\n      if (mainChar && mainChar.visible) {\n        if (mainCharExplosionHandle && !mainCharExplosionHandle.hasBeenRestored) {\n          console.log(\"Main character already exploded and awaiting restoration. Restore first or reset demo.\")\n          return\n        }\n        console.log(\"Triggering explosion for main character via ExplosionManager!\")\n        const explosionParams: Partial<ExplosionEffectParameters> = {\n          numRows: 50,\n          numCols: 50,\n          durationMs: 4000,\n          strength: 2,\n          gravity: 9.8,\n          fadeOut: false,\n          materialFactory,\n        }\n        mainCharExplosionHandle = explosionManager.createExplosionForSprite(mainChar, explosionParams)\n\n        if (mainCharExplosionHandle) {\n          console.log(\"Explosion effect created by manager. Main character destroyed.\")\n          mainChar = null\n        } else {\n          console.log(\"Failed to create explosion for main character via manager.\")\n        }\n      } else if (mainCharExplosionHandle && !mainCharExplosionHandle.hasBeenRestored) {\n        console.log(\"Main character already exploded. Press R to restore.\")\n      } else {\n        console.log(\"Main character not available to explode or already restored and re-exploded.\")\n      }\n    }\n\n    if (key.name === \"r\") {\n      if (mainCharExplosionHandle && !mainCharExplosionHandle.hasBeenRestored) {\n        console.log(\"Attempting to restore main character...\")\n        ;(async () => {\n          const restoredSprite = await mainCharExplosionHandle!.restoreSprite(spriteAnimator)\n          if (restoredSprite) {\n            mainChar = restoredSprite\n            console.log(\"Main character restored successfully.\")\n          } else {\n            console.log(\"Failed to restore main character. Handle might be invalid or sprite creation failed.\")\n          }\n        })()\n      } else if (mainCharExplosionHandle && mainCharExplosionHandle.hasBeenRestored) {\n        console.log(\"Main character has already been restored from this explosion event.\")\n      } else {\n        console.log(\"No active explosion to restore for the main character.\")\n      }\n    }\n\n    if (key.name === \"p\") {\n      if (addedSprites.length > 0) {\n        console.log(\"Clearing existing sprites...\")\n        addedSprites.forEach((sprite) => sprite.destroy())\n        addedSprites = []\n        explosionManager.disposeAll()\n        return\n      }\n      console.log(\"Starting stress test: Adding 1000 sprites...\")\n      const stressTestStartTime = performance.now()\n      ;(async () => {\n        for (let i = 0; i < 1000; i++) {\n          const id = `stress_${i}`\n          const stressCharDef: SpriteDefinition = {\n            ...mainCharDef,\n            animations: {\n              idle: {\n                ...mainCharDef.animations[\"idle\"],\n                frameDuration: 100 + Math.random() * 100,\n              },\n            },\n          }\n\n          const instance = await spriteAnimator.createSprite(stressCharDef)\n          const xPos = (Math.random() - 0.5) * pCamera.position.z * engine.aspectRatio * 1.5\n          const yPos = (Math.random() - 0.5) * pCamera.position.z * 1.5\n          const zPos = Math.random() * -2\n          instance.setPosition(new THREE.Vector3(xPos, yPos, zPos))\n\n          const randomScaleMultiplier = randFloat(1.0, 8.0)\n          instance.setScale(new THREE.Vector3(randomScaleMultiplier, randomScaleMultiplier, randomScaleMultiplier))\n\n          const randomStartFrame = Math.floor(Math.random() * NUM_SPRITES)\n          instance.goToFrame(randomStartFrame)\n\n          addedSprites.push(instance)\n        }\n        const stressTestEndTime = performance.now()\n        console.log(\n          `Stress test finished: Added ${addedSprites.length} sprites in ${(stressTestEndTime - stressTestStartTime).toFixed(2)} ms`,\n        )\n      })()\n    }\n\n    if (key.name === \"x\") {\n      explodeRandomSprite()\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", keyHandler)\n\n  renderer.setFrameCallback(async (deltaTime: number) => {\n    spriteAnimator.update(deltaTime)\n    explosionManager.update(deltaTime)\n    await engine.drawScene(scene, framebuffer, deltaTime)\n  })\n\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"instructions\",\n    content:\n      \"Controls: c=camera, e=explode, r=restore, p=stress test, x=explode random, t=debug, u=supersample, `=console, ESC=back\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    fg: \"#AAAAAA\",\n    zIndex: 20,\n  })\n  parentContainer.add(instructionsText)\n\n  demoState = {\n    engine,\n    scene,\n    pCamera,\n    oCamera,\n    spriteResourceManager,\n    spriteAnimator,\n    explosionManager,\n    mainChar,\n    mainCharExplosionHandle,\n    addedSprites,\n    activeExplosionHandles,\n    isPerspectiveActive,\n    parentContainer,\n    instructionsText,\n    cameraModeText,\n    keyHandler,\n  }\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (demoState) {\n    if (demoState.keyHandler) {\n      renderer.keyInput.off(\"keypress\", demoState.keyHandler)\n    }\n\n    demoState.addedSprites.forEach((sprite) => sprite.destroy())\n    demoState.explosionManager.disposeAll()\n    demoState.engine.destroy()\n\n    renderer.root.remove(\"main\")\n    renderer.root.remove(\"sprite-animation-container\")\n    renderer.clearFrameCallbacks()\n\n    demoState = null\n  }\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/sprite-particle-generator-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  OptimizedBuffer,\n  RGBA,\n  BoxRenderable,\n  TextRenderable,\n  FrameBufferRenderable,\n  type KeyEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport * as THREE from \"three\"\nimport {\n  SpriteAnimator,\n  type TiledSprite,\n  type SpriteDefinition,\n  type AnimationDefinition,\n} from \"../3d/animation/SpriteAnimator.js\"\nimport { SpriteResourceManager, type ResourceConfig } from \"../3d/SpriteResourceManager.js\"\nimport { SpriteParticleGenerator, type ParticleEffectParameters } from \"../3d/animation/SpriteParticleGenerator.js\"\nimport { ThreeCliRenderer } from \"../3d.js\"\n\n// @ts-ignore\nimport heartPath from \"./assets/heart.png\" with { type: \"image/png\" }\n// @ts-ignore\nimport simpleSquarePath from \"./assets/forrest_background.png\" with { type: \"image/png\" }\n// @ts-ignore\nimport mainCharRunPath from \"./assets/main_char_run_loop.png\" with { type: \"image/png\" }\n\nlet engine: ThreeCliRenderer | null = null\nlet scene: THREE.Scene | null = null\nlet framebuffer: OptimizedBuffer | null = null\nlet framebufferRenderableRef: FrameBufferRenderable | null = null\nlet spriteAnimator: SpriteAnimator | null = null\nlet resourceManager: SpriteResourceManager | null = null\nlet generators: Record<string, SpriteParticleGenerator> = {}\nlet currentGenerator: SpriteParticleGenerator | null = null\nlet currentGeneratorKey = \"3d-static\"\nlet backgroundSprite: TiledSprite | null = null\nlet configs: Record<string, { name: string; params: ParticleEffectParameters }> = {}\nlet inputListener: ((key: KeyEvent) => void) | null = null\nlet resizeListener: ((width: number, height: number) => void) | null = null\nlet frameCallback: ((deltaTime: number) => Promise<void>) | null = null\nlet parentContainer: BoxRenderable | null = null\nlet instructionsText: TextRenderable | null = null\nlet particleCountText: TextRenderable | null = null\nlet configInfoText: TextRenderable | null = null\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const initialTermWidth = renderer.terminalWidth\n  const initialTermHeight = renderer.terminalHeight\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"particle-container\",\n    zIndex: 15,\n  })\n  renderer.root.add(parentContainer)\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"particle-main\",\n    width: initialTermWidth,\n    height: initialTermHeight,\n    zIndex: 10,\n  })\n  renderer.root.add(framebufferRenderable)\n  framebufferRenderableRef = framebufferRenderable\n  framebuffer = framebufferRenderable.frameBuffer\n\n  engine = new ThreeCliRenderer(renderer, {\n    width: initialTermWidth,\n    height: initialTermHeight,\n    focalLength: 1,\n    backgroundColor: RGBA.fromValues(0.1, 0.1, 0.2, 1.0),\n  })\n  await engine.init()\n\n  scene = new THREE.Scene()\n\n  const pCamera = new THREE.PerspectiveCamera(75, engine.aspectRatio, 0.1, 1000)\n  pCamera.position.set(0, 0, 3)\n  pCamera.lookAt(0, 0, 0)\n  scene.add(pCamera)\n  engine.setActiveCamera(pCamera)\n\n  instructionsText = new TextRenderable(renderer, {\n    id: \"particle-instructions\",\n    content:\n      \"'g'(burst), 'a'(auto), 's'(stop), 'x'(clear), '1'(3D Static), '2'(2D Static), '3'(3D Animated), '4'(Custom), '5'(2D Animated)\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(instructionsText)\n\n  particleCountText = new TextRenderable(renderer, {\n    id: \"particle-count\",\n    content: \"Particles: 0\",\n    position: \"absolute\",\n    left: 1,\n    top: 2,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(particleCountText)\n\n  configInfoText = new TextRenderable(renderer, {\n    id: \"particle-config-info\",\n    content: \"Mode: 3D Static | Auto-spawning\",\n    position: \"absolute\",\n    left: 1,\n    top: 3,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(configInfoText)\n\n  resourceManager = new SpriteResourceManager(scene)\n  spriteAnimator = new SpriteAnimator(scene)\n\n  const staticParticleResourceConfig: ResourceConfig = {\n    imagePath: heartPath,\n    sheetNumFrames: 1,\n  }\n\n  const animatedParticleResourceConfig: ResourceConfig = {\n    imagePath: mainCharRunPath,\n    sheetNumFrames: 10,\n  }\n\n  const staticParticleResource = await resourceManager.createResource(staticParticleResourceConfig)\n  const animatedParticleResource = await resourceManager.createResource(animatedParticleResourceConfig)\n\n  const backgroundResourceConfig: ResourceConfig = {\n    imagePath: simpleSquarePath,\n    sheetNumFrames: 1,\n  }\n  const backgroundResource = await resourceManager.createResource(backgroundResourceConfig)\n  const backgroundAnimDef: AnimationDefinition = {\n    resource: backgroundResource,\n    animNumFrames: 1,\n    animFrameOffset: 0,\n    frameDuration: 1000,\n    loop: false,\n  }\n  const backgroundSpriteDef: SpriteDefinition = {\n    initialAnimation: \"idle\",\n    animations: {\n      idle: backgroundAnimDef,\n    },\n    scale: 5.0,\n    renderOrder: 0,\n  }\n\n  backgroundSprite = await spriteAnimator.createSprite(backgroundSpriteDef)\n  if (backgroundSprite) {\n    backgroundSprite.setPosition(new THREE.Vector3(0, 0, 0))\n  }\n\n  const AUTO_SPAWN_RATE = 30\n\n  configs = {\n    \"3d-static\": {\n      name: \"3D Static\",\n      params: {\n        resource: staticParticleResource,\n        scale: 0.7,\n        renderOrder: 1,\n        maxParticles: 3000,\n        lifetimeMsMin: 2000,\n        lifetimeMsMax: 5000,\n        origins: [\n          new THREE.Vector3(-1, 0, 0),\n          new THREE.Vector3(1, 0, 0),\n          new THREE.Vector3(0, 1, 0),\n          new THREE.Vector3(0, -1, 0),\n        ],\n        spawnRadius: new THREE.Vector3(0.1, 0.1, 0),\n        initialVelocityMin: new THREE.Vector3(-0.5, 2, 0),\n        initialVelocityMax: new THREE.Vector3(0.5, 3.5, 0),\n        angularVelocityMin: new THREE.Vector3(-Math.PI, -Math.PI, -Math.PI),\n        angularVelocityMax: new THREE.Vector3(Math.PI, Math.PI, Math.PI),\n        gravity: new THREE.Vector3(0, -2.0, 0),\n        randomGravityFactorMinMax: new THREE.Vector2(0.8, 1.2),\n        scaleOverLifeMinMax: new THREE.Vector2(1.0, 0.1),\n        fadeOut: true,\n      } as ParticleEffectParameters,\n    },\n    \"2d-static\": {\n      name: \"2D Static\",\n      params: {\n        resource: staticParticleResource,\n        scale: 0.7,\n        renderOrder: 1,\n        maxParticles: 3000,\n        lifetimeMsMin: 2000,\n        lifetimeMsMax: 5000,\n        origins: [new THREE.Vector3(0, 0, 0)],\n        spawnRadius: new THREE.Vector3(0.1, 0.1, 0),\n        initialVelocityMin: new THREE.Vector3(-0.5, 2, 0),\n        initialVelocityMax: new THREE.Vector3(0.5, 3.5, 0),\n        angularVelocityMin: new THREE.Vector3(0, 0, -Math.PI),\n        angularVelocityMax: new THREE.Vector3(0, 0, Math.PI),\n        gravity: new THREE.Vector3(0, -2.0, 0),\n        randomGravityFactorMinMax: new THREE.Vector2(0.8, 1.2),\n        scaleOverLifeMinMax: new THREE.Vector2(1.0, 0.1),\n        fadeOut: true,\n      } as ParticleEffectParameters,\n    },\n    \"3d-animated\": {\n      name: \"3D Animated\",\n      params: {\n        resource: animatedParticleResource,\n        frameDuration: 80,\n        scale: 1.5,\n        renderOrder: 1,\n        maxParticles: 3000,\n        lifetimeMsMin: 2000,\n        lifetimeMsMax: 5000,\n        origins: [\n          new THREE.Vector3(-1, 0, 0),\n          new THREE.Vector3(1, 0, 0),\n          new THREE.Vector3(0, 1, 0),\n          new THREE.Vector3(0, -1, 0),\n        ],\n        spawnRadius: new THREE.Vector3(0.1, 0.1, 0),\n        initialVelocityMin: new THREE.Vector3(-0.5, 2, 0),\n        initialVelocityMax: new THREE.Vector3(0.5, 3.5, 0),\n        angularVelocityMin: new THREE.Vector3(-Math.PI, -Math.PI, -Math.PI),\n        angularVelocityMax: new THREE.Vector3(Math.PI, Math.PI, Math.PI),\n        gravity: new THREE.Vector3(0, -2.0, 0),\n        randomGravityFactorMinMax: new THREE.Vector2(0.8, 1.2),\n        scaleOverLifeMinMax: new THREE.Vector2(1.0, 0.1),\n        fadeOut: true,\n      } as ParticleEffectParameters,\n    },\n    custom: {\n      name: \"Custom Gravity\",\n      params: {\n        resource: staticParticleResource,\n        scale: 0.7,\n        renderOrder: 1,\n        maxParticles: 3000,\n        lifetimeMsMin: 2000,\n        lifetimeMsMax: 5000,\n        origins: [\n          new THREE.Vector3(-1, 0, 0),\n          new THREE.Vector3(1, 0, 0),\n          new THREE.Vector3(0, 1, 0),\n          new THREE.Vector3(0, -1, 0),\n        ],\n        spawnRadius: new THREE.Vector3(0.1, 0.1, 0),\n        initialVelocityMin: new THREE.Vector3(-0.5, 2, 0),\n        initialVelocityMax: new THREE.Vector3(0.5, 3.5, 0),\n        angularVelocityMin: new THREE.Vector3(-Math.PI, -Math.PI, -Math.PI),\n        angularVelocityMax: new THREE.Vector3(Math.PI, Math.PI, Math.PI),\n        gravity: new THREE.Vector3(0, THREE.MathUtils.randFloat(-9.8, 9.8), THREE.MathUtils.randFloat(-2.0, 2.0)),\n        randomGravityFactorMinMax: new THREE.Vector2(0.8, 1.2),\n        scaleOverLifeMinMax: new THREE.Vector2(1.0, 0.1),\n        fadeOut: true,\n      } as ParticleEffectParameters,\n    },\n    \"2d-animated\": {\n      name: \"2D Animated\",\n      params: {\n        resource: animatedParticleResource,\n        frameDuration: 80,\n        scale: 1.5,\n        renderOrder: 1,\n        maxParticles: 3000,\n        lifetimeMsMin: 2000,\n        lifetimeMsMax: 5000,\n        origins: [new THREE.Vector3(0, 0, 0.1)],\n        spawnRadius: new THREE.Vector3(0.1, 0.1, 0),\n        initialVelocityMin: new THREE.Vector3(-0.5, 2, 0),\n        initialVelocityMax: new THREE.Vector3(0.5, 3.5, 0),\n        angularVelocityMin: new THREE.Vector3(0, 0, -Math.PI),\n        angularVelocityMax: new THREE.Vector3(0, 0, Math.PI),\n        gravity: new THREE.Vector3(0, -2.0, 0),\n        randomGravityFactorMinMax: new THREE.Vector2(0.8, 1.2),\n        scaleOverLifeMinMax: new THREE.Vector2(1.0, 0.1),\n        fadeOut: true,\n      } as ParticleEffectParameters,\n    },\n  }\n\n  generators = {}\n  for (const [key, config] of Object.entries(configs)) {\n    generators[key] = new SpriteParticleGenerator(scene, config.params)\n  }\n\n  currentGeneratorKey = \"3d-static\"\n  currentGenerator = generators[currentGeneratorKey]\n  currentGenerator.setAutoSpawn(AUTO_SPAWN_RATE)\n\n  resizeListener = (newWidth: number, newHeight: number) => {\n    if (framebuffer) {\n      framebuffer.resize(newWidth, newHeight)\n    }\n    if (engine) {\n      const camera = engine.getActiveCamera()\n      if (camera && \"aspect\" in camera) {\n        ;(camera as THREE.PerspectiveCamera).aspect = newWidth / newHeight\n        ;(camera as THREE.PerspectiveCamera).updateProjectionMatrix()\n      }\n    }\n  }\n  renderer.on(\"resize\", resizeListener)\n\n  frameCallback = async (deltaTime: number) => {\n    if (spriteAnimator) {\n      spriteAnimator.update(deltaTime)\n    }\n\n    for (const generator of Object.values(generators)) {\n      await generator.update(deltaTime)\n    }\n\n    if (currentGenerator && particleCountText) {\n      particleCountText.content = `Particles: ${currentGenerator.getActiveParticleCount()}`\n    }\n\n    if (engine && scene && framebuffer) {\n      await engine.drawScene(scene, framebuffer, deltaTime)\n    }\n  }\n  renderer.setFrameCallback(frameCallback)\n\n  function switchToGenerator(key: string) {\n    if (!generators[key] || !currentGenerator) return\n\n    const wasAutoSpawning = currentGenerator.hasAutoSpawn()\n\n    currentGenerator.stopAutoSpawn()\n    currentGeneratorKey = key\n    currentGenerator = generators[key]\n\n    if (wasAutoSpawning) {\n      currentGenerator.setAutoSpawn(AUTO_SPAWN_RATE)\n    }\n\n    const configName = configs[key as keyof typeof configs].name\n    const isAutoSpawning = currentGenerator.hasAutoSpawn()\n    const status = isAutoSpawning ? \"Auto-spawning\" : \"Idle\"\n\n    if (configInfoText) {\n      configInfoText.content = `Mode: ${configName} | ${status}`\n    }\n\n    console.log(`Switched to ${configName} generator${wasAutoSpawning ? \" (auto-spawn continued)\" : \"\"}`)\n  }\n\n  inputListener = (key: KeyEvent) => {\n    if (key.name === \"g\" && currentGenerator) {\n      console.log(\"Generating 100 particles (burst)...\")\n      currentGenerator.spawnParticles(100).then(() => {\n        console.log(\"Particle burst spawn call completed.\")\n      })\n    }\n\n    if (key.name === \"a\" && currentGenerator) {\n      console.log(\"Starting auto-spawn (30 particles/sec)...\")\n      currentGenerator.setAutoSpawn(AUTO_SPAWN_RATE)\n      const configName = configs[currentGeneratorKey as keyof typeof configs].name\n      if (configInfoText) {\n        configInfoText.content = `Mode: ${configName} | Auto-spawning`\n      }\n    }\n\n    if (key.name === \"s\" && currentGenerator) {\n      console.log(\"Stopping auto-spawn...\")\n      currentGenerator.stopAutoSpawn()\n      const configName = configs[currentGeneratorKey as keyof typeof configs].name\n      if (configInfoText) {\n        configInfoText.content = `Mode: ${configName} | Idle`\n      }\n    }\n\n    if (key.name === \"x\" && currentGenerator) {\n      console.log(\"Clearing all particles...\")\n      currentGenerator.dispose()\n    }\n\n    if (key.name === \"1\") {\n      switchToGenerator(\"3d-static\")\n    }\n\n    if (key.name === \"2\") {\n      switchToGenerator(\"2d-static\")\n    }\n\n    if (key.name === \"3\") {\n      switchToGenerator(\"3d-animated\")\n    }\n\n    if (key.name === \"4\") {\n      configs.custom.params.gravity = new THREE.Vector3(\n        0,\n        THREE.MathUtils.randFloat(-9.8, 9.8),\n        THREE.MathUtils.randFloat(-2.0, 2.0),\n      )\n      console.log(\n        `Custom gravity: Y=${configs.custom.params.gravity.y.toFixed(1)}, Z=${configs.custom.params.gravity.z.toFixed(1)}`,\n      )\n      switchToGenerator(\"custom\")\n    }\n\n    if (key.name === \"5\") {\n      switchToGenerator(\"2d-animated\")\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", inputListener)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (inputListener) {\n    renderer.keyInput.off(\"keypress\", inputListener)\n    inputListener = null\n  }\n\n  if (resizeListener) {\n    renderer.off(\"resize\", resizeListener)\n    resizeListener = null\n  }\n\n  renderer.clearFrameCallbacks()\n  frameCallback = null\n\n  for (const generator of Object.values(generators)) {\n    generator.dispose()\n  }\n  generators = {}\n\n  if (backgroundSprite) {\n    backgroundSprite = null\n  }\n\n  if (spriteAnimator) {\n    spriteAnimator = null\n  }\n\n  if (resourceManager) {\n    resourceManager = null\n  }\n\n  if (framebufferRenderableRef) {\n    renderer.root.remove(framebufferRenderableRef.id)\n    framebufferRenderableRef = null\n  }\n  framebuffer = null\n\n  if (parentContainer) {\n    renderer.root.remove(\"particle-container\")\n    parentContainer = null\n  }\n\n  instructionsText = null\n  particleCountText = null\n  configInfoText = null\n\n  if (engine) {\n    engine.destroy()\n    engine = null\n  }\n\n  if (scene) {\n    scene = null\n  }\n\n  currentGenerator = null\n  currentGeneratorKey = \"3d-static\"\n  configs = {}\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/static-sprite-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  TextRenderable,\n  FrameBufferRenderable,\n  BoxRenderable,\n  type KeyEvent,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport * as THREE from \"three\"\nimport { ThreeCliRenderer } from \"../3d.js\"\nimport { SpriteUtils } from \"../3d/SpriteUtils.js\"\n\n// @ts-ignore - Bun specific import attribute for assets\nimport staticImagePath from \"./assets/main_char_idle.png\" with { type: \"image/png\" }\n\nlet engine: ThreeCliRenderer | null = null\nlet scene: THREE.Scene | null = null\nlet camera: THREE.OrthographicCamera | null = null\nlet sprite: any = null\nlet frameIndex = 0\nlet accumulatedTime = 0\nlet frameCallback: ((deltaTime: number) => Promise<void>) | null = null\nlet keyHandler: ((key: KeyEvent) => void) | null = null\nlet resizeHandler: ((newWidth: number, newHeight: number) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const TERM_WIDTH = renderer.terminalWidth\n  const TERM_HEIGHT = renderer.terminalHeight\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"static-sprite-container\",\n    zIndex: 15,\n  })\n  renderer.root.add(parentContainer)\n\n  const framebufferRenderable = new FrameBufferRenderable(renderer, {\n    id: \"main\",\n    width: TERM_WIDTH,\n    height: TERM_HEIGHT,\n    zIndex: 10,\n  })\n  renderer.root.add(framebufferRenderable)\n  const { frameBuffer: framebuffer } = framebufferRenderable\n\n  engine = new ThreeCliRenderer(renderer, {\n    width: TERM_WIDTH,\n    height: TERM_HEIGHT,\n    focalLength: 1,\n    backgroundColor: RGBA.fromValues(0.2, 0.1, 0.3, 1.0),\n  })\n  await engine.init()\n\n  scene = new THREE.Scene()\n\n  const aspectRatio = engine.aspectRatio\n  const frustumSize = 1\n  camera = new THREE.OrthographicCamera(\n    (frustumSize * aspectRatio) / -2, // left\n    (frustumSize * aspectRatio) / 2, // right\n    frustumSize / 2, // top\n    frustumSize / -2, // bottom\n    0.1, // near\n    1000, // far\n  )\n\n  camera.position.z = 5\n  scene.add(camera)\n  engine.setActiveCamera(camera)\n\n  const titleText = new TextRenderable(renderer, {\n    id: \"demo-title\",\n    content: \"Static THREE.Sprite Demo\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(titleText)\n\n  const statusText = new TextRenderable(renderer, {\n    id: \"status\",\n    content: \"Loading sprite texture...\",\n    position: \"absolute\",\n    left: 1,\n    top: 2,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(statusText)\n\n  const totalFrames = 8\n  sprite = await SpriteUtils.sheetFromFile(staticImagePath, totalFrames)\n\n  const desiredHeight = 2.0\n  sprite.scale.set(desiredHeight, desiredHeight, desiredHeight)\n\n  scene.add(sprite)\n  statusText.content = \"Sprite loaded. Press t/u/`/k.\"\n\n  resizeHandler = (newWidth: number, newHeight: number) => {\n    framebuffer.resize(newWidth, newHeight)\n\n    const newAspectRatio = engine!.aspectRatio\n    camera!.left = (frustumSize * newAspectRatio) / -2\n    camera!.right = (frustumSize * newAspectRatio) / 2\n    camera!.top = frustumSize / 2\n    camera!.bottom = frustumSize / -2\n    camera!.updateProjectionMatrix()\n  }\n\n  renderer.on(\"resize\", resizeHandler)\n\n  frameCallback = async (deltaTime: number) => {\n    accumulatedTime += deltaTime\n    if (accumulatedTime > 64) {\n      frameIndex = (frameIndex + 1) % totalFrames\n      sprite.setIndex(frameIndex)\n      accumulatedTime = 0\n    }\n\n    await engine!.drawScene(scene!, framebuffer, deltaTime)\n  }\n\n  renderer.setFrameCallback(frameCallback)\n\n  keyHandler = (key: KeyEvent) => {\n    if (key.name === \"u\") {\n      engine!.toggleSuperSampling()\n    }\n\n    if (key.name === \"k\") {\n      renderer.console.toggleDebugMode()\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", keyHandler)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (keyHandler) {\n    renderer.keyInput.off(\"keypress\", keyHandler)\n    keyHandler = null\n  }\n\n  if (frameCallback) {\n    renderer.clearFrameCallbacks()\n    frameCallback = null\n  }\n\n  if (resizeHandler) {\n    renderer.off(\"resize\", resizeHandler)\n    resizeHandler = null\n  }\n\n  renderer.root.remove(\"main\")\n\n  if (parentContainer) {\n    renderer.root.remove(\"static-sprite-container\")\n    parentContainer = null\n  }\n\n  if (sprite && scene) {\n    scene.remove(sprite)\n    sprite = null\n  }\n\n  if (engine) {\n    engine.destroy()\n    engine = null\n  }\n\n  scene = null\n  camera = null\n  frameIndex = 0\n  accumulatedTime = 0\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/sticky-scroll-example.ts",
    "content": "import { BoxRenderable, type CliRenderer, createCliRenderer, TextRenderable, t, fg, bold } from \"../index.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet scrollBox: ScrollBoxRenderable | null = null\nlet renderer: CliRenderer | null = null\nlet mainContainer: BoxRenderable | null = null\nlet instructionsBox: BoxRenderable | null = null\nlet itemCount = 0\nlet animationInterval: ReturnType<typeof setInterval> | null = null\n\n// Track items with their creation time for animation\ninterface AnimatedItem {\n  box: BoxRenderable\n  text: TextRenderable\n  createdAt: number\n  originalContent: string\n  normalBgColor: string\n  isAtTop: boolean\n}\n\nconst animatedItems = new Map<string, AnimatedItem>()\n\n// Clear all items from the scroll box\nfunction clearAllItems() {\n  if (!scrollBox) return\n\n  // Stop any running animations\n  if (animationInterval) {\n    clearInterval(animationInterval)\n    animationInterval = null\n  }\n\n  // Clear animated items tracking\n  animatedItems.clear()\n\n  // Remove all children from scroll box\n  const children = scrollBox.getChildren()\n  for (const child of children) {\n    scrollBox.remove(child.id)\n    child.destroyRecursively()\n  }\n\n  // Reset item count\n  itemCount = 0\n}\n\n// Color interpolation helper\nfunction interpolateColor(color1: string, color2: string, factor: number): string {\n  const c1 = parseInt(color1.slice(1), 16)\n  const c2 = parseInt(color2.slice(1), 16)\n\n  const r1 = (c1 >> 16) & 0xff\n  const g1 = (c1 >> 8) & 0xff\n  const b1 = c1 & 0xff\n\n  const r2 = (c2 >> 16) & 0xff\n  const g2 = (c2 >> 8) & 0xff\n  const b2 = c2 & 0xff\n\n  const r = Math.round(r1 + (r2 - r1) * factor)\n  const g = Math.round(g1 + (g2 - g1) * factor)\n  const b = Math.round(b1 + (b2 - b1) * factor)\n\n  return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, \"0\")}`\n}\n\n// Animation update function\nfunction updateAnimations() {\n  const now = Date.now()\n\n  for (const [id, item] of animatedItems) {\n    const age = now - item.createdAt\n    const duration = 800 // 800ms animation\n\n    if (age >= duration) {\n      // Animation complete, set to final colors\n      item.box.backgroundColor = item.normalBgColor\n\n      // Set final content with normal colors\n      const itemNumber = id.split(\"-\")[1]\n      const timeString = new Date(item.createdAt).toLocaleTimeString()\n      const finalContent = t`${bold(fg(\"#7aa2f7\")(`Item #${itemNumber}`))}\n${fg(\"#9aa5ce\")(\"This is a dynamically added item with enhanced content.\")}\n${fg(\"#c0caf5\")(\"Contains additional information and styling.\")}\n${fg(\"#565f89\")(\"Added at:\")} ${timeString}\n${fg(\"#565f89\")(\"Position:\")} ${item.isAtTop ? \"TOP\" : \"BOTTOM\"}\n${fg(\"#565f89\")(\"Status:\")} ${fg(\"#9ece6a\")(\"ACTIVE\")}`\n\n      item.text.content = finalContent\n      animatedItems.delete(id)\n      continue\n    }\n\n    const progress = age / duration\n    const easeProgress = 1 - Math.pow(1 - progress, 3) // Ease out cubic\n\n    // Interpolate background color (bright to normal)\n    const brightBg = \"#4c4f69\"\n    item.box.backgroundColor = interpolateColor(brightBg, item.normalBgColor, easeProgress)\n\n    // Update text with flashy purple fading to normal blue\n    const itemNumber = id.split(\"-\")[1]\n    const currentTitleColor = interpolateColor(\"#bb9af7\", \"#7aa2f7\", easeProgress)\n    const timeString = new Date(item.createdAt).toLocaleTimeString()\n\n    // Reconstruct content with interpolated color\n    const updatedContent = t`${bold(fg(currentTitleColor)(`Item #${itemNumber}`))}\n${fg(\"#9aa5ce\")(\"This is a dynamically added item with enhanced content.\")}\n${fg(\"#c0caf5\")(\"Contains additional information and styling.\")}\n${fg(\"#565f89\")(\"Added at:\")} ${timeString}\n${fg(\"#565f89\")(\"Position:\")} ${item.isAtTop ? \"TOP\" : \"BOTTOM\"}\n${fg(\"#565f89\")(\"Status:\")} ${fg(\"#9ece6a\")(\"ACTIVE\")}`\n\n    item.text.content = updatedContent\n  }\n\n  // Stop animation loop if no items are animating\n  if (animatedItems.size === 0 && animationInterval) {\n    clearInterval(animationInterval)\n    animationInterval = null\n  }\n}\n\nfunction addItem(atTop: boolean = false) {\n  if (!renderer || !scrollBox) return\n\n  itemCount++\n\n  const boxId = `item-${itemCount}`\n  const normalBgColor = itemCount % 2 === 0 ? \"#24283b\" : \"#1f2335\"\n\n  // Start with flashy colors\n  const box = new BoxRenderable(renderer, {\n    id: boxId,\n    width: \"auto\",\n    padding: 1,\n    marginBottom: 1,\n    backgroundColor: \"#4c4f69\", // Bright initial background\n  })\n\n  const timeString = new Date().toLocaleTimeString()\n\n  // Store original content as string (without template literal processing)\n  const originalContent = `Item #${itemCount}\\nThis is a dynamically added item with enhanced content.\\nContains additional information and styling.\\nAdded at: ${timeString}\\nPosition: ${atTop ? \"TOP\" : \"BOTTOM\"}\\nStatus: ACTIVE`\n\n  // Start with flashy purple title using template literal\n  const flashyContent = t`${bold(fg(\"#bb9af7\")(`Item #${itemCount}`))}\n${fg(\"#9aa5ce\")(\"This is a dynamically added item with enhanced content.\")}\n${fg(\"#c0caf5\")(\"Contains additional information and styling.\")}\n${fg(\"#565f89\")(\"Added at:\")} ${timeString}\n${fg(\"#565f89\")(\"Position:\")} ${atTop ? \"TOP\" : \"BOTTOM\"}\n${fg(\"#565f89\")(\"Status:\")} ${fg(\"#9ece6a\")(\"ACTIVE\")}`\n\n  const text = new TextRenderable(renderer, {\n    content: flashyContent,\n  })\n\n  box.add(text)\n\n  if (atTop) {\n    scrollBox.add(box, 0) // Add at the beginning\n  } else {\n    scrollBox.add(box) // Add at the end\n  }\n\n  // Track for animation\n  animatedItems.set(boxId, {\n    box,\n    text,\n    createdAt: Date.now(),\n    originalContent,\n    normalBgColor,\n    isAtTop: atTop,\n  })\n\n  // Start animation loop if not running\n  if (!animationInterval) {\n    animationInterval = setInterval(updateAnimations, 16) // ~60fps\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#0a0a14\")\n\n  mainContainer = new BoxRenderable(renderer, {\n    id: \"main-container\",\n    flexGrow: 1,\n    maxHeight: \"100%\",\n    maxWidth: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#0f0f23\",\n  })\n\n  scrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"sticky-scroll-box\",\n    stickyScroll: true,\n    stickyStart: \"bottom\",\n    rootOptions: {\n      backgroundColor: \"#1e1e2e\",\n      border: true,\n    },\n    wrapperOptions: {\n      backgroundColor: \"#181825\",\n    },\n    viewportOptions: {\n      backgroundColor: \"#11111b\",\n    },\n    contentOptions: {\n      backgroundColor: \"#0f0f0f\",\n    },\n    scrollbarOptions: {\n      // width: 2,\n      // showArrows: true,\n      trackOptions: {\n        foregroundColor: \"#7aa2f7\",\n        backgroundColor: \"#313244\",\n      },\n    },\n  })\n\n  instructionsBox = new BoxRenderable(renderer, {\n    id: \"instructions\",\n    width: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#1e1e2e\",\n    paddingLeft: 1,\n    flexShrink: 0,\n  })\n\n  const instructionsText1 = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Sticky Scroll Demo\"))} ${fg(\"#565f89\")(\"-\")} ${bold(fg(\"#9ece6a\")(\"S\"))} ${fg(\"#c0caf5\")(\"Toggle sticky scroll\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#bb9af7\")(\"T\"))} ${fg(\"#c0caf5\")(\"Add item at top\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#f7768e\")(\"B\"))} ${fg(\"#c0caf5\")(\"Add item at bottom\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#e0af68\")(\"E\"))} ${fg(\"#c0caf5\")(\"Clear all items\")}`,\n  })\n\n  const instructionsText2 = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Behavior:\"))} ${fg(\"#c0caf5\")(\"Scroll to top/bottom, then add items to see sticky behavior\")}`,\n  })\n\n  const instructionsText3 = new TextRenderable(renderer, {\n    content: t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#c0caf5\")(\"Sticky Scroll:\")} ${scrollBox.stickyScroll ? fg(\"#9ece6a\")(\"ENABLED\") : fg(\"#f7768e\")(\"DISABLED\")}`,\n  })\n\n  instructionsBox.add(instructionsText1)\n  instructionsBox.add(instructionsText2)\n  instructionsBox.add(instructionsText3)\n\n  mainContainer.add(scrollBox)\n  mainContainer.add(instructionsBox)\n\n  renderer.root.add(mainContainer)\n\n  scrollBox.focus()\n\n  // Add some initial items\n  for (let i = 0; i < 10; i++) {\n    addItem(false)\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", (key) => {\n    if (key.name === \"s\" && scrollBox) {\n      // Toggle sticky scroll\n      const currentSticky = scrollBox.stickyScroll\n      scrollBox.stickyScroll = !currentSticky\n      console.log(`Sticky scroll ${!currentSticky ? \"enabled\" : \"disabled\"}`)\n\n      // Update status display\n      if (instructionsBox && instructionsBox.getChildren().length >= 3) {\n        const statusText = instructionsBox.getChildren()[2] as TextRenderable\n        statusText.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#c0caf5\")(\"Sticky Scroll:\")} ${(scrollBox as any).stickyScroll ? fg(\"#9ece6a\")(\"ENABLED\") : fg(\"#f7768e\")(\"DISABLED\")}`\n      }\n    } else if (key.name === \"t\" && scrollBox) {\n      addItem(true) // Add at top\n      console.log(\"Added item at top\")\n    } else if (key.name === \"b\" && scrollBox) {\n      addItem(false) // Add at bottom\n      console.log(\"Added item at bottom\")\n    } else if (key.name === \"e\" && scrollBox) {\n      clearAllItems() // Clear all items\n    }\n  })\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (mainContainer) {\n    rendererInstance.root.remove(mainContainer.id)\n    mainContainer.destroyRecursively()\n    mainContainer = null\n  }\n\n  // Clean up animation\n  if (animationInterval) {\n    clearInterval(animationInterval)\n    animationInterval = null\n  }\n  animatedItems.clear()\n\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/styled-text-demo.ts",
    "content": "import {\n  CliRenderer,\n  createCliRenderer,\n  t,\n  blue,\n  bold,\n  underline,\n  red,\n  green,\n  bgYellow,\n  fg,\n  link,\n  BoxRenderable,\n  type KeyEvent,\n} from \"../index.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet parentContainer: BoxRenderable | null = null\nlet counter = 0\nlet frameCallback: ((deltaTime: number) => Promise<void>) | null = null\nlet updateFrequency = 1 // Updates per frame (1 = every frame, 2 = every 2 frames, etc.)\nlet complexTemplateCounter = 0\nlet startTime = Date.now()\nlet keyboardHandler: ((key: KeyEvent) => void) | null = null\nlet dashboardBox: BoxRenderable | null = null\nlet complexDisplay: TextRenderable | null = null\n\nexport function run(rendererInstance: CliRenderer): void {\n  const renderer = rendererInstance\n  renderer.start()\n  renderer.setBackgroundColor(\"#001122\")\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"styled-text-container\",\n    zIndex: 15,\n  })\n  renderer.root.add(parentContainer)\n\n  counter = 0\n\n  // Example 1\n  const houseText = t`  \nThere's a ${underline(blue(\"house\"))},\nWith a ${bold(blue(\"window\"))},\nAnd a ${blue(\"corvette\")}\nAnd everything is blue`\n\n  const houseDisplay = new TextRenderable(renderer, {\n    id: \"house-text\",\n    content: houseText,\n    width: 30,\n    height: 6,\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    zIndex: 1,\n  })\n  parentContainer.add(houseDisplay)\n\n  // Example 2\n  const statusText = t`${bold(red(\"ERROR:\"))} Connection failed\n${bold(green(\"SUCCESS:\"))} Data loaded\n${bold(fg(\"#FFA500\")(\"WARNING:\"))} Low memory\n${bgYellow(fg(\"black\")(\" NOTICE \"))} System update available`\n\n  const statusDisplay = new TextRenderable(renderer, {\n    id: \"status-text\",\n    content: statusText,\n    width: 50,\n    height: 6,\n    position: \"absolute\",\n    left: 2,\n    top: 8,\n    zIndex: 1,\n  })\n  parentContainer.add(statusDisplay)\n\n  // Example 3 - Original dynamic text (updates every second)\n  dashboardBox = new BoxRenderable(renderer, {\n    id: \"dashboard-box\",\n    width: 72,\n    height: 21,\n    position: \"absolute\",\n    left: 2,\n    top: 27,\n    zIndex: 1,\n    backgroundColor: \"#001122\",\n    borderColor: \"#00FFFF\",\n    borderStyle: \"single\",\n    title: \"COMPLEX REAL-TIME DASHBOARD\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  parentContainer.add(dashboardBox)\n\n  const initialText = t`${bold(\"System Stats:\")} ${fg(\"#888\")(\"[Initializing...]\")}\n${blue(\"Uptime:\")} ${fg(\"#00FF00\")(\"0.00\")}s ${fg(\"#666\")(\"(0m 0s)\")}\n${red(\"CPU Load:\")} ${green(\"0.0%\")}\n${fg(\"#FF6B6B\")(\"Memory:\")} ${fg(\"#FFA500\")(\"0.0%\")}\n${fg(\"#9B59B6\")(\"Network:\")} ${fg(\"#FFA500\")(\"0 KB/s\")}\n${fg(\"#E74C3C\")(\"Temp:\")} ${blue(\"0.0°C\")}\n${fg(\"#F39C12\")(\"Battery:\")} ${green(\"100%\")}\n${underline(\"Connection:\")} ${green(bold(\"ONLINE\"))}\n${underline(\"Health:\")} ${green(bold(\"GOOD\"))}\n${underline(\"Alert:\")} ${green(bold(\"NORMAL\"))}\n${fg(\"#3498DB\")(\"Random ID:\")} ${fg(\"#E67E22\")(\"0000\")}\n${fg(\"#1ABC9C\")(\"Wave:\")} ${green(\"+0.00\")}\n${fg(\"#9B59B6\")(\"Progress:\")} ${fg(\"#00FF00\")(\"░\".repeat(20))}\n${fg(\"#34495E\")(\"Frame:\")} ${fg(\"#ECF0F1\")(\"0\")} ${fg(\"#7F8C8D\")(\"(Total: 0)\")}\n${fg(\"#2ECC71\")(\"Status:\")} ${bold(fg(\"#E74C3C\")(\"●\"))} ${green(\"ALL SYSTEMS GO\")}\n\n${bold(fg(\"#F1C40F\")(\"Controls:\"))} ${fg(\"#BDC3C7\")(\"↑/↓ = Speed, ESC = Exit\")}`\n\n  complexDisplay = new TextRenderable(renderer, {\n    id: \"complex-template\",\n    content: initialText,\n    left: 1,\n    top: 1,\n    zIndex: 1,\n  })\n  dashboardBox.add(complexDisplay)\n\n  frameCallback = async (deltaTime) => {\n    counter++\n    complexTemplateCounter++\n\n    if (counter % 60 === 0) {\n      // Update every second\n      const dynamicText = t`${bold(\"Frame:\")} ${counter}\n${blue(\"Time:\")} ${(counter / 60).toFixed(1)}s\n${underline(\"Dynamic:\")} ${bold(fg(\"#FF6B6B\")(Math.sin(counter * 0.1) > 0 ? \"UP\" : \"DOWN\"))}`\n\n      const dynamicDisplay = parentContainer?.getRenderable(\"dynamic-text\") as TextRenderable\n      if (dynamicDisplay) {\n        dynamicDisplay.content = dynamicText\n      } else {\n        const newDynamicDisplay = new TextRenderable(renderer, {\n          id: \"dynamic-text\",\n          content: dynamicText,\n          width: 40,\n          height: 4,\n          position: \"absolute\",\n          left: 2,\n          top: 15,\n          zIndex: 1,\n        })\n        parentContainer?.add(newDynamicDisplay)\n      }\n    }\n\n    if (complexTemplateCounter % updateFrequency === 0 && complexDisplay) {\n      const currentTime = Date.now()\n      const elapsedMs = currentTime - startTime\n      const elapsedSeconds = elapsedMs / 1000\n\n      const cpuLoad = Math.sin(elapsedSeconds * 0.5) * 50 + 50\n      const memoryUsage = Math.cos(elapsedSeconds * 0.3) * 30 + 70\n      const networkSpeed = Math.abs(Math.sin(elapsedSeconds * 2)) * 1000\n      const temperature = Math.sin(elapsedSeconds * 0.1) * 20 + 60\n      const batteryLevel = Math.max(0, 100 - elapsedSeconds * 0.5)\n      const randomValue = Math.floor(Math.random() * 9999)\n      const waveValue = Math.sin(elapsedSeconds * 3) * 10\n      const progressBar = \"█\".repeat(Math.floor(((elapsedSeconds % 10) / 10) * 20))\n\n      const connectionStatus = Math.sin(elapsedSeconds) > 0 ? \"ONLINE\" : \"OFFLINE\"\n      const systemHealth = cpuLoad < 80 ? \"GOOD\" : \"HIGH\"\n      const alertLevel = temperature > 75 ? \"CRITICAL\" : \"NORMAL\"\n\n      const complexText = t`${bold(\"System Stats:\")} ${fg(\"#888\")(`[Update: ${updateFrequency === 1 ? \"Every Frame\" : `Every ${updateFrequency} frames`}]`)}\n${blue(\"Uptime:\")} ${fg(\"#00FF00\")(elapsedSeconds.toFixed(2))}s ${fg(\"#666\")(`(${Math.floor(elapsedSeconds / 60)}m ${Math.floor(elapsedSeconds % 60)}s)`)}\n${red(\"CPU Load:\")} ${cpuLoad > 80 ? red(bold(`${cpuLoad.toFixed(1)}%`)) : green(`${cpuLoad.toFixed(1)}%`)} ${fg(\"#444\")(\"█\".repeat(Math.floor(cpuLoad / 5)))}\n${fg(\"#FF6B6B\")(\"Memory:\")} ${memoryUsage > 85 ? red(bold(`${memoryUsage.toFixed(1)}%`)) : fg(\"#FFA500\")(`${memoryUsage.toFixed(1)}%`)}\n${fg(\"#9B59B6\")(\"Network:\")} ${networkSpeed > 500 ? green(bold(`${networkSpeed.toFixed(0)} KB/s`)) : fg(\"#FFA500\")(`${networkSpeed.toFixed(0)} KB/s`)}\n${fg(\"#E74C3C\")(\"Temp:\")} ${temperature > 75 ? red(bold(`${temperature.toFixed(1)}°C`)) : blue(`${temperature.toFixed(1)}°C`)}\n${fg(\"#F39C12\")(\"Battery:\")} ${batteryLevel < 20 ? red(bold(`${batteryLevel.toFixed(0)}%`)) : green(`${batteryLevel.toFixed(0)}%`)}\n${underline(\"Connection:\")} ${connectionStatus === \"ONLINE\" ? green(bold(connectionStatus)) : red(bold(connectionStatus))}\n${underline(\"Health:\")} ${systemHealth === \"GOOD\" ? green(bold(systemHealth)) : red(bold(systemHealth))}\n${underline(\"Alert:\")} ${alertLevel === \"NORMAL\" ? green(bold(alertLevel)) : bgYellow(red(bold(alertLevel)))}\n${fg(\"#3498DB\")(\"Random ID:\")} ${fg(\"#E67E22\")(randomValue.toString().padStart(4, \"0\"))}\n${fg(\"#1ABC9C\")(\"Wave:\")} ${waveValue >= 0 ? green(`+${waveValue.toFixed(2)}`) : red(waveValue.toFixed(2))}\n${fg(\"#9B59B6\")(\"Progress:\")} ${fg(\"#00FF00\")(progressBar.padEnd(20, \"░\"))}\n${fg(\"#34495E\")(\"Frame:\")} ${fg(\"#ECF0F1\")(complexTemplateCounter)} ${fg(\"#7F8C8D\")(`(Total: ${counter})`)}\n${fg(\"#2ECC71\")(\"Status:\")} ${bold(fg(\"#E74C3C\")(\"●\"))} ${alertLevel === \"CRITICAL\" ? red(\"SYSTEM ALERT\") : green(\"ALL SYSTEMS GO\")}\n\n${bold(fg(\"#F1C40F\")(\"Controls:\"))} ${fg(\"#BDC3C7\")(\"↑/↓ = Speed, ESC = Exit\")}`\n\n      complexDisplay.content = complexText\n    }\n  }\n\n  renderer.setFrameCallback(frameCallback)\n\n  // Add keyboard controls for update frequency\n  keyboardHandler = (key: KeyEvent) => {\n    if (key.name === \"up\" || key.name === \"arrowup\") {\n      updateFrequency = Math.max(1, updateFrequency - 1)\n    } else if (key.name === \"down\" || key.name === \"arrowdown\") {\n      updateFrequency = Math.min(60, updateFrequency + 1)\n    }\n  }\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n\n  const instructionsText = t`${bold(\"Styled Text Demo\")}\n${fg(\"#888\")(\"ESC to return, ↑/↓ to control speed\")}\n\n${underline(\"Features demonstrated:\")}\n• Template literals with ${blue(\"colors\")}\n• ${bold(\"Bold\")}, ${underline(\"underlined\")}, and other styles\n• Background colors like ${bgYellow(fg(\"black\")(\"this\"))}\n• Custom hex colors like ${fg(\"#FF6B6B\")(\"this red\")}\n• Dynamic updates with ${green(\"controllable frequency\")}\n• Complex templates with ${red(\"many variables\")}\n• Hyperlinks: ${underline(blue(link(\"https://opentui.com\")(\"opentui\")))}`\n\n  const instructionsDisplay = new TextRenderable(renderer, {\n    id: \"instructions\",\n    content: instructionsText,\n    width: 60,\n    height: 12,\n    position: \"absolute\",\n    left: 40,\n    top: 2,\n    zIndex: 1,\n    fg: \"#CCCCCC\",\n  })\n  parentContainer.add(instructionsDisplay)\n\n  // Examples showing number and boolean support\n  const typesText = t`${bold(\"Type Examples:\")}\nNumber: ${green(42)}\nBoolean: ${red(true)}\nFloat: ${blue((3.14159).toFixed(2))}\nCalculated: ${fg(\"#00FFFF\")(Math.floor(Math.random() * 100))}`\n\n  const typesDisplay = new TextRenderable(renderer, {\n    id: \"types-text\",\n    content: typesText,\n    width: 30,\n    height: 6,\n    position: \"absolute\",\n    left: 2,\n    top: 20,\n    zIndex: 1,\n  })\n  parentContainer.add(typesDisplay)\n\n  renderer.requestRender()\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (frameCallback) {\n    rendererInstance.removeFrameCallback(frameCallback)\n    frameCallback = null\n  }\n\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  if (parentContainer) {\n    rendererInstance.root.remove(\"styled-text-container\")\n    parentContainer = null\n  }\n\n  counter = 0\n  updateFrequency = 1\n  complexTemplateCounter = 0\n  startTime = Date.now()\n  dashboardBox = null\n  complexDisplay = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/tab-select-demo.ts",
    "content": "import {\n  createCliRenderer,\n  TabSelectRenderable,\n  TabSelectRenderableEvents,\n  RenderableEvents,\n  BoxRenderable,\n  type TabSelectOption,\n  type CliRenderer,\n  t,\n  bold,\n  fg,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\nlet tabSelect: TabSelectRenderable | null = null\nlet renderer: CliRenderer | null = null\nlet keyboardHandler: ((key: any) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\nlet keyLegendDisplay: TextRenderable | null = null\nlet statusDisplay: TextRenderable | null = null\nlet lastSelectedItem: TabSelectOption | null = null\n\nconst tabOptions: TabSelectOption[] = [\n  { name: \"Home\", description: \"Welcome to the home page\", value: \"home\" },\n  { name: \"Profile\", description: \"Manage your user profile\", value: \"profile\" },\n  { name: \"Settings\", description: \"Configure application settings\", value: \"settings\" },\n  { name: \"About\", description: \"Learn more about this application\", value: \"about\" },\n  { name: \"Help\", description: \"Get help and support\", value: \"help\" },\n  { name: \"Projects\", description: \"View and manage your projects\", value: \"projects\" },\n  { name: \"Dashboard\", description: \"View analytics and statistics\", value: \"dashboard\" },\n  { name: \"Reports\", description: \"Generate and view reports\", value: \"reports\" },\n  { name: \"Users\", description: \"Manage user accounts\", value: \"users\" },\n  { name: \"Admin\", description: \"Administrative functions\", value: \"admin\" },\n  { name: \"Tools\", description: \"Various utility tools\", value: \"tools\" },\n  { name: \"API\", description: \"API documentation and testing\", value: \"api\" },\n]\n\nfunction updateDisplays() {\n  if (!tabSelect || !parentContainer) return\n\n  const underlineStatus = tabSelect.showUnderline ? \"on\" : \"off\"\n  const description = tabSelect.showDescription ? \"on\" : \"off\"\n  const scrollArrows = tabSelect.showScrollArrows ? \"on\" : \"off\"\n  const wrap = tabSelect.wrapSelection ? \"on\" : \"off\"\n\n  const keyLegendText = t`${bold(fg(\"#FFFFFF\")(\"Key Controls:\"))}\n←/→ or [/]: Navigate tabs\nEnter: Select tab\nF: Toggle focus\nU: Toggle underline\nP: Toggle description\nS: Toggle scroll arrows\nW: Toggle wrap selection`\n\n  if (keyLegendDisplay) {\n    keyLegendDisplay.content = keyLegendText\n  }\n\n  const currentHighlighted = tabSelect.getSelectedOption()\n  const highlightedText = currentHighlighted\n    ? `Highlighted: ${currentHighlighted.name} (${currentHighlighted.value}) - Index: ${tabSelect.getSelectedIndex()}`\n    : \"No highlighted item\"\n\n  const selectedText = lastSelectedItem\n    ? `Last Selected: ${lastSelectedItem.name} (${lastSelectedItem.value})`\n    : \"No item selected yet (press Enter to select)\"\n\n  const focusText = tabSelect.focused ? \"Tab selector is FOCUSED\" : \"Tab selector is BLURRED\"\n  const focusColor = tabSelect.focused ? \"#00FF00\" : \"#FF0000\"\n\n  const statusText = t`${fg(\"#00FF00\")(highlightedText)}\n${fg(\"#FFFF00\")(selectedText)}\n\n${fg(focusColor)(focusText)}\n\n${fg(\"#CCCCCC\")(`Underline: ${underlineStatus} | Description: ${description} | Scroll arrows: ${scrollArrows} | Wrap: ${wrap}`)}`\n\n  if (statusDisplay) {\n    statusDisplay.content = statusText\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#001122\")\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"tab-select-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  tabSelect = new TabSelectRenderable(renderer, {\n    id: \"main-tabs\",\n    position: \"absolute\",\n    left: 5,\n    top: 2,\n    width: 70,\n    options: tabOptions,\n    zIndex: 100,\n    tabWidth: 12,\n    backgroundColor: \"#1e293b\",\n    focusedBackgroundColor: \"#2d3748\",\n    textColor: \"#e2e8f0\",\n    focusedTextColor: \"#f7fafc\",\n    selectedBackgroundColor: \"#3b82f6\",\n    selectedTextColor: \"#ffffff\",\n    selectedDescriptionColor: \"#cbd5e1\",\n    showDescription: true,\n    showUnderline: true,\n    showScrollArrows: true,\n    wrapSelection: false,\n  })\n\n  renderer.root.add(tabSelect)\n\n  keyLegendDisplay = new TextRenderable(renderer, {\n    id: \"key-legend\",\n    content: t``,\n    width: 40,\n    height: 10,\n    position: \"absolute\",\n    left: 5,\n    top: 8,\n    zIndex: 50,\n    fg: \"#AAAAAA\",\n  })\n  parentContainer.add(keyLegendDisplay)\n\n  // Create status display\n  statusDisplay = new TextRenderable(renderer, {\n    id: \"status-display\",\n    content: t``,\n    width: 80,\n    height: 6,\n    position: \"absolute\",\n    left: 5,\n    top: 19,\n    zIndex: 50,\n  })\n  parentContainer.add(statusDisplay)\n\n  tabSelect.on(TabSelectRenderableEvents.SELECTION_CHANGED, (index: number, option: TabSelectOption) => {\n    updateDisplays()\n  })\n\n  tabSelect.on(TabSelectRenderableEvents.ITEM_SELECTED, (index: number, option: TabSelectOption) => {\n    lastSelectedItem = option\n    updateDisplays()\n  })\n\n  tabSelect.on(RenderableEvents.FOCUSED, () => {\n    updateDisplays()\n  })\n\n  tabSelect.on(RenderableEvents.BLURRED, () => {\n    updateDisplays()\n  })\n\n  updateDisplays()\n\n  keyboardHandler = (key) => {\n    if (key.name === \"f\") {\n      if (tabSelect?.focused) {\n        tabSelect.blur()\n      } else {\n        tabSelect?.focus()\n      }\n    } else if (key.name === \"u\") {\n      tabSelect!.showUnderline = !tabSelect!.showUnderline\n      updateDisplays()\n    } else if (key.name === \"p\") {\n      tabSelect!.showDescription = !tabSelect!.showDescription\n      updateDisplays()\n    } else if (key.name === \"s\") {\n      tabSelect!.showScrollArrows = !tabSelect!.showScrollArrows\n      updateDisplays()\n    } else if (key.name === \"w\") {\n      tabSelect!.wrapSelection = !tabSelect!.wrapSelection\n      updateDisplays()\n    }\n  }\n\n  rendererInstance.keyInput.on(\"keypress\", keyboardHandler)\n  tabSelect.focus()\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  if (keyboardHandler) {\n    rendererInstance.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  if (tabSelect) {\n    rendererInstance.root.remove(tabSelect.id)\n    tabSelect.destroy()\n    tabSelect = null\n  }\n\n  if (parentContainer) {\n    rendererInstance.root.remove(\"tab-select-container\")\n    parentContainer = null\n  }\n\n  keyLegendDisplay = null\n  statusDisplay = null\n  lastSelectedItem = null\n  renderer = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/terminal-title.ts",
    "content": "import { TextRenderable, createCliRenderer } from \"../index.js\"\n\nasync function testTerminalTitle() {\n  const renderer = await createCliRenderer({ exitOnCtrlC: true })\n  renderer.console.show()\n\n  const text = new TextRenderable(renderer, {\n    content: \"Press Ctrl+C to exit\",\n    margin: 2,\n  })\n\n  renderer.root.add(text)\n  console.log(\"Setting title to: 'OpenTUI Test'\")\n  renderer.setTerminalTitle(\"OpenTUI Test\")\n\n  await new Promise((resolve) => setTimeout(resolve, 2000))\n\n  console.log(\"Setting title to: 'Terminal Title Demo'\")\n  renderer.setTerminalTitle(\"Terminal Title Demo\")\n\n  await new Promise((resolve) => setTimeout(resolve, 2000))\n\n  console.log(\"Setting title to: '🎉 Success! 🎉'\")\n  renderer.setTerminalTitle(\"🎉 Success! 🎉\")\n\n  await new Promise((resolve) => setTimeout(resolve, 2000))\n}\n\ntestTerminalTitle().catch(console.error)\n"
  },
  {
    "path": "packages/core/src/examples/terminal.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  RGBA,\n  TextAttributes,\n  TextRenderable,\n  FrameBufferRenderable,\n  BoxRenderable,\n  InputRenderable,\n  InputRenderableEvents,\n} from \"../index.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport type { TerminalColors } from \"../lib/terminal-palette.js\"\nimport { PaletteGridRenderable } from \"./lib/PaletteGrid.js\"\nimport { HexListRenderable } from \"./lib/HexList.js\"\n\n/**\n * This demo showcases terminal palette detection.\n * Enter a palette size (1-256) in the input field and press Enter to fetch colors.\n */\n\nlet scrollBox: ScrollBoxRenderable | null = null\nlet contentContainer: BoxRenderable | null = null\nlet paletteGrid: PaletteGridRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet hexList: HexListRenderable | null = null\nlet specialColorsBuffer: FrameBufferRenderable | null = null\nlet terminalColors: TerminalColors | null = null\nlet keyboardHandler: ((key: any) => void) | null = null\nlet paletteSizeInput: InputRenderable | null = null\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  const backgroundColor = RGBA.fromInts(15, 23, 42) // Slate-900 inspired\n  renderer.setBackgroundColor(backgroundColor)\n\n  const mainContainer = new BoxRenderable(renderer, {\n    id: \"main-container\",\n    flexGrow: 1,\n    flexDirection: \"column\",\n  })\n  renderer.root.add(mainContainer)\n\n  scrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"terminal-scroll-box\",\n    stickyScroll: false,\n    border: true,\n    borderColor: \"#8B5CF6\",\n    title: \"Terminal Palette Demo (Ctrl+C to exit)\",\n    titleAlignment: \"center\",\n    contentOptions: {\n      paddingLeft: 2,\n      paddingRight: 2,\n      paddingTop: 1,\n    },\n  })\n  mainContainer.add(scrollBox)\n\n  contentContainer = new BoxRenderable(renderer, {\n    id: \"terminal-palette-container\",\n    width: \"auto\",\n    flexDirection: \"column\",\n  })\n  scrollBox.add(contentContainer)\n\n  const subtitleText = new TextRenderable(renderer, {\n    id: \"terminal_subtitle\",\n    content: \"Enter palette size (1-256) and press Enter to fetch | Press 'c' to clear cache\",\n    fg: RGBA.fromInts(148, 163, 184), // Slate-400 - softer contrast\n  })\n  contentContainer.add(subtitleText)\n\n  // Add input field for palette size\n  const inputContainer = new BoxRenderable(renderer, {\n    id: \"input-container\",\n    flexDirection: \"row\",\n    marginTop: 1,\n  })\n  contentContainer.add(inputContainer)\n\n  const inputLabel = new TextRenderable(renderer, {\n    id: \"input-label\",\n    content: \"Palette Size: \",\n    fg: RGBA.fromInts(148, 163, 184),\n  })\n  inputContainer.add(inputLabel)\n\n  paletteSizeInput = new InputRenderable(renderer, {\n    id: \"palette-size-input\",\n    width: 10,\n    height: 1,\n    backgroundColor: RGBA.fromInts(30, 41, 59),\n    textColor: RGBA.fromInts(255, 255, 255),\n    placeholder: \"16\",\n    placeholderColor: RGBA.fromInts(100, 116, 139),\n    cursorColor: RGBA.fromInts(139, 92, 246), // Purple cursor\n    value: \"16\",\n    maxLength: 3,\n  })\n  inputContainer.add(paletteSizeInput)\n\n  statusText = new TextRenderable(renderer, {\n    id: \"terminal_status\",\n    content: \"Status: Ready to fetch palette\",\n    marginTop: 1,\n    fg: RGBA.fromInts(56, 189, 248), // Sky blue - modern accent\n  })\n  contentContainer.add(statusText)\n\n  const instructionsText = new TextRenderable(renderer, {\n    id: \"terminal_instructions\",\n    content: \"Press Escape to return to menu\",\n    marginTop: 1,\n    fg: RGBA.fromInts(100, 116, 139), // Slate-500 - muted but readable\n  })\n  contentContainer.add(instructionsText)\n\n  // Create palette grid - will be populated when palette is fetched\n  paletteGrid = new PaletteGridRenderable(renderer, {\n    id: \"palette-grid\",\n    colors: [],\n    marginTop: 2,\n  })\n  contentContainer.add(paletteGrid)\n\n  // Set up input submit handler\n  paletteSizeInput.on(InputRenderableEvents.ENTER, async (value: string) => {\n    const size = parseInt(value, 10)\n    if (isNaN(size) || size < 1 || size > 256) {\n      if (statusText) {\n        statusText.content = \"Status: Invalid palette size. Please enter a number between 1 and 256.\"\n        statusText.fg = RGBA.fromInts(239, 68, 68) // Red error\n      }\n      return\n    }\n    await fetchAndDisplayPalette(renderer, size)\n  })\n\n  // Set up keyboard handler\n  keyboardHandler = async (key) => {\n    if (key.name === \"c\") {\n      clearPaletteCache(renderer)\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", keyboardHandler)\n\n  // Focus the input field on start\n  paletteSizeInput.focus()\n}\n\nasync function fetchAndDisplayPalette(renderer: CliRenderer, size: number): Promise<void> {\n  if (!statusText || !paletteGrid) return\n\n  try {\n    const wasAlreadyCached = renderer.paletteDetectionStatus === \"cached\"\n    statusText.content = `Status: ${wasAlreadyCached ? \"Using cached palette\" : \"Fetching palette...\"}`\n    statusText.fg = RGBA.fromInts(250, 204, 21) // Amber - warm loading state\n\n    const startTime = performance.now()\n    terminalColors = await renderer.getPalette({ size })\n    const elapsed = Math.round(performance.now() - startTime)\n\n    statusText.content = `Status: Palette (${size} colors) fetched in ${elapsed}ms (${wasAlreadyCached ? \"from cache\" : \"from terminal\"})`\n    statusText.fg = RGBA.fromInts(34, 197, 94) // Emerald - fresh success state\n\n    drawPalette(renderer, terminalColors, size)\n  } catch (error) {\n    if (statusText) {\n      statusText.content = `Status: Error - ${error instanceof Error ? error.message : String(error)}`\n      statusText.fg = RGBA.fromInts(239, 68, 68) // Red-500 - modern error state\n    }\n  }\n}\n\nfunction clearPaletteCache(renderer: CliRenderer): void {\n  if (!statusText) return\n\n  renderer.clearPaletteCache()\n  statusText.content = \"Status: Cache cleared. Enter a size and press Enter to fetch palette again.\"\n  statusText.fg = RGBA.fromInts(148, 163, 184) // Slate-400 - neutral info state\n}\n\nfunction drawPalette(renderer: CliRenderer, terminalColors: TerminalColors, size: number): void {\n  const colors = terminalColors.palette.slice(0, size)\n\n  // Update the palette grid with new colors\n  if (paletteGrid) {\n    paletteGrid.colors = colors\n  }\n\n  // Create special colors list with colored boxes\n  const specialColors = [\n    { label: \"Default FG\", value: terminalColors.defaultForeground },\n    { label: \"Default BG\", value: terminalColors.defaultBackground },\n    { label: \"Cursor\", value: terminalColors.cursorColor },\n    { label: \"Mouse FG\", value: terminalColors.mouseForeground },\n    { label: \"Mouse BG\", value: terminalColors.mouseBackground },\n    { label: \"Tek FG\", value: terminalColors.tekForeground },\n    { label: \"Tek BG\", value: terminalColors.tekBackground },\n    { label: \"Highlight BG\", value: terminalColors.highlightBackground },\n    { label: \"Highlight FG\", value: terminalColors.highlightForeground },\n  ]\n\n  // Create a framebuffer for special colors with colored boxes\n  const specialBufferWidth = 30\n  const specialBufferHeight = specialColors.length * 2\n\n  if (!specialColorsBuffer) {\n    specialColorsBuffer = new FrameBufferRenderable(renderer, {\n      id: \"special-colors-buffer\",\n      width: specialBufferWidth,\n      height: specialBufferHeight,\n      marginTop: 2,\n    })\n    contentContainer!.add(specialColorsBuffer)\n  }\n\n  const specialBuffer = specialColorsBuffer.frameBuffer\n  specialBuffer.clear(RGBA.fromInts(30, 41, 59, 255)) // Slate-800 background\n\n  specialColors.forEach(({ label, value }, index) => {\n    const y = index * 2\n    const boxWidth = 4\n\n    if (value) {\n      // Parse hex color\n      const hex = value.replace(\"#\", \"\")\n      const r = parseInt(hex.substring(0, 2), 16)\n      const g = parseInt(hex.substring(2, 4), 16)\n      const b = parseInt(hex.substring(4, 6), 16)\n      const rgba = RGBA.fromInts(r, g, b)\n\n      // Draw colored box (4x2 block)\n      for (let dy = 0; dy < 2; dy++) {\n        for (let dx = 0; dx < boxWidth; dx++) {\n          specialBuffer.setCell(dx, y + dy, \" \", RGBA.fromInts(255, 255, 255), rgba)\n        }\n      }\n\n      // Draw label and hex value\n      const text = `${label}: ${value.toUpperCase()}`\n      const textColor = RGBA.fromInts(148, 163, 184)\n      const bgColor = RGBA.fromInts(30, 41, 59, 255)\n      for (let i = 0; i < text.length; i++) {\n        specialBuffer.drawText(text[i], boxWidth + 1 + i, y, textColor, bgColor, TextAttributes.NONE)\n      }\n    } else {\n      // Draw N/A\n      const text = `${label}: N/A`\n      const textColor = RGBA.fromInts(100, 116, 139)\n      const bgColor = RGBA.fromInts(30, 41, 59, 255)\n      for (let i = 0; i < text.length; i++) {\n        specialBuffer.drawText(text[i], boxWidth + 1 + i, y, textColor, bgColor, TextAttributes.NONE)\n      }\n    }\n  })\n\n  // Update the hex list with new colors\n  if (!hexList) {\n    hexList = new HexListRenderable(renderer, {\n      id: \"hex-list\",\n      colors: colors,\n      marginTop: 2,\n    })\n    contentContainer!.add(hexList)\n  } else {\n    hexList.colors = colors\n  }\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (keyboardHandler) {\n    renderer.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  if (paletteSizeInput) {\n    paletteSizeInput.destroy()\n    paletteSizeInput = null\n  }\n\n  if (scrollBox) {\n    renderer.root.remove(\"main-container\")\n    scrollBox = null\n  }\n\n  contentContainer = null\n  paletteGrid = null\n  hexList = null\n  specialColorsBuffer = null\n  statusText = null\n  terminalColors = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/text-node-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  TextRenderable,\n  BoxRenderable,\n  t,\n  bold,\n  underline,\n  green,\n  yellow,\n  cyan,\n} from \"../index.js\"\nimport { TextNodeRenderable } from \"../renderables/TextNode.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet mainContainer: BoxRenderable | null = null\nlet demoText: TextRenderable | null = null\nlet instructionsText: TextRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet updateInterval: Timer | null = null\n\nfunction clearUpdateInterval(): void {\n  if (updateInterval) {\n    clearInterval(updateInterval)\n    updateInterval = null\n  }\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"#0d1117\")\n\n  mainContainer = new BoxRenderable(renderer, {\n    id: \"mainContainer\",\n    width: 88,\n    height: 32,\n    backgroundColor: \"#161b22\",\n    zIndex: 1,\n    borderColor: \"#50565d\",\n    title: \"TextNode Demo\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  renderer.root.add(mainContainer)\n\n  // Create the main demo text area\n  demoText = new TextRenderable(renderer, {\n    id: \"demoText\",\n    width: 60,\n    height: 20,\n    zIndex: 2,\n    fg: \"#f0f6fc\",\n  })\n  mainContainer.add(demoText)\n\n  // Create instructions\n  instructionsText = new TextRenderable(renderer, {\n    id: \"instructions\",\n    content: t`${bold(cyan(\"TextNode Demo\"))}\n${yellow(\"•\")} Press ${green(\"1-4\")} to see different examples\n${yellow(\"•\")} Press ${green(\"SPACE\")} to toggle dynamic updates\n${yellow(\"•\")} Press ${green(\"R\")} to reset demo\n${yellow(\"•\")} Press ${green(\"ESC\")} to exit\n\n${underline(\"Current:\")} Example 1 - Basic TextNode Creation`,\n    fg: \"#c9d1d9\",\n  })\n  mainContainer.add(instructionsText)\n\n  // Create status area\n  statusText = new TextRenderable(renderer, {\n    id: \"status\",\n    content: \"Ready - Press 1-4 for examples\",\n    width: 84,\n    height: 3,\n    fg: \"#58a6ff\",\n  })\n  mainContainer.add(statusText)\n\n  // Initialize with first example\n  showExample1()\n\n  // Set up keyboard controls\n  renderer.keyInput.on(\"keypress\", (event) => {\n    const key = event.sequence\n    if (key === \"1\") {\n      showExample1()\n    } else if (key === \"2\") {\n      showExample2()\n    } else if (key === \"3\") {\n      showExample3()\n    } else if (key === \"4\") {\n      showExample4()\n    } else if (key === \" \") {\n      toggleDynamicUpdates()\n    } else if (key === \"r\" || key === \"R\") {\n      resetDemo()\n    }\n  })\n}\n\nfunction showExample1(): void {\n  if (!demoText) return\n\n  // Clear any running intervals\n  clearUpdateInterval()\n\n  // Clear existing TextNodes\n  demoText.clear()\n\n  // Example 1: Basic TextNode Creation\n  const titleNode = TextNodeRenderable.fromString(\"Basic TextNode Demo\", {\n    fg: \"#58a6ff\",\n    attributes: 1, // bold\n  })\n\n  const subtitleNode = TextNodeRenderable.fromString(\"\\n\\nCreating individual TextNodes with different styles:\", {\n    fg: \"#8b949e\",\n  })\n\n  const redNode = TextNodeRenderable.fromString(\"\\n\\nRed Text\", {\n    fg: \"#ff7b72\",\n  })\n\n  const blueNode = TextNodeRenderable.fromString(\" | Blue Text\", {\n    fg: \"#79c0ff\",\n  })\n\n  const greenNode = TextNodeRenderable.fromString(\" | Green Text\", {\n    fg: \"#56d364\",\n  })\n\n  const yellowNode = TextNodeRenderable.fromString(\" | Yellow Background\", {\n    fg: \"#000000\",\n    bg: \"#d29922\",\n  })\n\n  // Create a container node that holds all the styled nodes\n  const containerNode = TextNodeRenderable.fromNodes([\n    titleNode,\n    subtitleNode,\n    redNode,\n    blueNode,\n    greenNode,\n    yellowNode,\n  ])\n\n  // Add to TextRenderable\n  demoText.add(containerNode)\n\n  updateInstructions(\n    \"Example 1 - Basic TextNode Creation\",\n    \"Creating individual TextNodes with different colors and styles\",\n  )\n}\n\nfunction showExample2(): void {\n  if (!demoText) return\n\n  // Clear any running intervals\n  clearUpdateInterval()\n\n  // Clear existing TextNodes\n  demoText.clear()\n\n  // Example 2: Nested TextNode Composition\n  const titleNode = TextNodeRenderable.fromString(\"Nested Composition Demo\", {\n    fg: \"#58a6ff\",\n    attributes: 1, // bold\n  })\n\n  const introNode = TextNodeRenderable.fromString(\"\\n\\nBuilding complex text by nesting TextNodes:\", {\n    fg: \"#8b949e\",\n  })\n\n  // Create nested structure\n  const codeBlock = TextNodeRenderable.fromString(\n    \"\\n\\nfunction calculateTotal(items) {\\n  return items.reduce((sum, item) => {\\n    return sum + item.price;\\n  }, 0);\\n}\",\n    {\n      fg: \"#f0f6fc\",\n      bg: \"#0d1117\",\n    },\n  )\n\n  const commentNode = TextNodeRenderable.fromString(\"\\n\\n// This is a nested comment\", {\n    fg: \"#8b949e\",\n  })\n\n  const highlightNode = TextNodeRenderable.fromString(\" with \", {\n    fg: \"#79c0ff\",\n    attributes: 1, // bold\n  })\n\n  const highlightNode2 = TextNodeRenderable.fromString(\"highlighting\", {\n    fg: \"#ff7b72\",\n    attributes: 4, // underline\n  })\n\n  // Create a sentence that combines multiple styled parts\n  const sentenceNode = TextNodeRenderable.fromNodes([\n    TextNodeRenderable.fromString(\"\\n\\nThis demonstrates \", { fg: \"#c9d1d9\" }),\n    highlightNode,\n    TextNodeRenderable.fromString(\"and \", { fg: \"#c9d1d9\" }),\n    highlightNode2,\n    TextNodeRenderable.fromString(\" within the same text flow.\", { fg: \"#c9d1d9\" }),\n  ])\n\n  // Create the main container\n  const containerNode = TextNodeRenderable.fromNodes([titleNode, introNode, codeBlock, commentNode, sentenceNode])\n\n  demoText.add(containerNode)\n\n  updateInstructions(\n    \"Example 2 - Nested TextNode Composition\",\n    \"Building complex text structures by composing TextNodes together\",\n  )\n}\n\nfunction showExample3(): void {\n  if (!demoText) return\n\n  // Clear any existing intervals before setting up new ones\n  clearUpdateInterval()\n\n  // Clear existing TextNodes\n  demoText.clear()\n\n  // Example 3: Dynamic TextNode Updates\n  const titleNode = TextNodeRenderable.fromString(\"Dynamic Updates Demo\", {\n    fg: \"#58a6ff\",\n    attributes: 1, // bold\n  })\n\n  const introNode = TextNodeRenderable.fromString(\"\\n\\nTextNodes can be updated dynamically:\", {\n    fg: \"#8b949e\",\n  })\n\n  const counterNode = TextNodeRenderable.fromString(`\\n\\nCounter: 0`, {\n    fg: \"#56d364\",\n    attributes: 1, // bold\n  })\n\n  const statusNode = TextNodeRenderable.fromString(\"\\n\\nStatus: Idle\", {\n    fg: \"#79c0ff\",\n  })\n\n  const progressNode = TextNodeRenderable.fromString(\"\\n\\nProgress: [          ]\", {\n    fg: \"#d29922\",\n  })\n\n  // Store references to nodes that will be updated\n  const containerNode = TextNodeRenderable.fromNodes([titleNode, introNode, counterNode, statusNode, progressNode])\n\n  demoText.add(containerNode)\n\n  // Set up dynamic updates for this example\n  let example3Counter = 0\n  const maxCount = 20\n\n  updateInterval = setInterval(() => {\n    if (!demoText || !containerNode) return\n\n    example3Counter++\n    if (example3Counter > maxCount) {\n      example3Counter = 0\n    }\n\n    // Update counter node\n    counterNode.children = [`\\n\\nCounter: ${example3Counter}`]\n\n    // Update status based on counter\n    const status = example3Counter < 5 ? \"Starting\" : example3Counter < 15 ? \"Running\" : \"Finishing\"\n    statusNode.children = [`\\n\\nStatus: ${status}`]\n\n    // Update progress bar\n    const progress = Math.floor((example3Counter / maxCount) * 10)\n    const progressBar = \"█\".repeat(progress).padEnd(10, \"░\")\n    progressNode.children = [`\\n\\nProgress: [${progressBar}]`]\n\n    // TextRenderable will automatically update from TextNode changes\n  }, 100)\n\n  updateInstructions(\n    \"Example 3 - Dynamic TextNode Updates\",\n    \"TextNodes can be modified and the changes reflected in real-time\",\n  )\n}\n\nfunction showExample4(): void {\n  if (!demoText) return\n\n  // Clear any running intervals\n  clearUpdateInterval()\n\n  // Clear existing TextNodes\n  demoText.clear()\n\n  // Example 4: Complex Document Structure\n  const titleNode = TextNodeRenderable.fromString(\"Complex Document Demo\", {\n    fg: \"#58a6ff\",\n    attributes: 1, // bold\n  })\n\n  const introNode = TextNodeRenderable.fromString(\"\\n\\nBuilding a complete document with TextNodes:\", {\n    fg: \"#8b949e\",\n  })\n\n  // Document sections\n  const headerNode = TextNodeRenderable.fromString(\"\\n\\n📋 Project Status Report\", {\n    fg: \"#ffffff\",\n    attributes: 1, // bold\n  })\n\n  const section1Node = TextNodeRenderable.fromNodes([\n    TextNodeRenderable.fromString(\"\\n\\n🚀 \", { fg: \"#56d364\" }),\n    TextNodeRenderable.fromString(\"Progress\", { fg: \"#58a6ff\", attributes: 1 }),\n    TextNodeRenderable.fromString(\": 85% complete\", { fg: \"#c9d1d9\" }),\n  ])\n\n  const section2Node = TextNodeRenderable.fromNodes([\n    TextNodeRenderable.fromString(\"\\n\\n⚠️  \", { fg: \"#d29922\" }),\n    TextNodeRenderable.fromString(\"Issues\", { fg: \"#ff7b72\", attributes: 1 }),\n    TextNodeRenderable.fromString(\": 2 minor issues found\", { fg: \"#c9d1d9\" }),\n  ])\n\n  const section3Node = TextNodeRenderable.fromNodes([\n    TextNodeRenderable.fromString(\"\\n\\n✅ \", { fg: \"#56d364\" }),\n    TextNodeRenderable.fromString(\"Next Steps\", { fg: \"#58a6ff\", attributes: 1 }),\n    TextNodeRenderable.fromString(\": Code review and testing\", { fg: \"#c9d1d9\" }),\n  ])\n\n  const footerNode = TextNodeRenderable.fromString(\"\\n\\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\", {\n    fg: \"#30363d\",\n  })\n\n  const signatureNode = TextNodeRenderable.fromString(\"\\nGenerated by OpenTUI TextNode Demo\", {\n    fg: \"#8b949e\",\n    attributes: 2, // italic\n  })\n\n  // Combine all sections into the final document\n  const documentNode = TextNodeRenderable.fromNodes([\n    titleNode,\n    introNode,\n    headerNode,\n    section1Node,\n    section2Node,\n    section3Node,\n    footerNode,\n    signatureNode,\n  ])\n\n  demoText.add(documentNode)\n\n  updateInstructions(\n    \"Example 4 - Complex Document Structure\",\n    \"Creating complete documents by composing multiple styled TextNode sections\",\n  )\n}\n\nfunction toggleDynamicUpdates(): void {\n  if (updateInterval) {\n    clearUpdateInterval()\n    updateStatus(\"Dynamic updates stopped\")\n  } else {\n    // Restart Example 3 if we're not already on it\n    showExample3()\n    updateStatus(\"Dynamic updates started\")\n  }\n}\n\nfunction resetDemo(): void {\n  clearUpdateInterval()\n  showExample1()\n  updateStatus(\"Demo reset\")\n}\n\nfunction updateInstructions(title: string, description: string): void {\n  if (!instructionsText) return\n\n  instructionsText.content = t`${bold(cyan(\"TextNode Demo\"))}\n${yellow(\"•\")} Press ${green(\"1-4\")} to see different examples\n${yellow(\"•\")} Press ${green(\"SPACE\")} to toggle dynamic updates\n${yellow(\"•\")} Press ${green(\"R\")} to reset demo\n${yellow(\"•\")} Press ${green(\"ESC\")} to exit\n\n${underline(\"Current:\")} ${title}\n${description}`\n}\n\nfunction updateStatus(message: string): void {\n  if (!statusText) return\n  statusText.content = message\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  clearUpdateInterval()\n\n  mainContainer?.destroyRecursively()\n  mainContainer = null\n  demoText = null\n  instructionsText = null\n  statusText = null\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    targetFps: 30,\n    enableMouseMovement: true,\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  // renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/text-selection-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  TextRenderable,\n  BoxRenderable,\n  t,\n  green,\n  bold,\n  italic,\n  yellow,\n  cyan,\n  magenta,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet mainContainer: BoxRenderable | null = null\nlet floatingBox: BoxRenderable | null = null\nlet leftGroup: BoxRenderable | null = null\nlet rightGroup: BoxRenderable | null = null\nlet statusBox: BoxRenderable | null = null\nlet statusText: TextRenderable | null = null\nlet selectionStartText: TextRenderable | null = null\nlet selectionMiddleText: TextRenderable | null = null\nlet selectionEndText: TextRenderable | null = null\nlet debugText: TextRenderable | null = null\nlet allTextRenderables: (TextRenderable | TextRenderable)[] = []\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"#0d1117\")\n\n  mainContainer = new BoxRenderable(renderer, {\n    id: \"mainContainer\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    width: 88,\n    height: 22,\n    backgroundColor: \"#161b22\",\n    zIndex: 1,\n    borderColor: \"#50565d\",\n    title: \"Text Selection Demo\",\n    titleAlignment: \"center\",\n    border: true,\n  })\n  renderer.root.add(mainContainer)\n\n  leftGroup = new BoxRenderable(renderer, {\n    id: \"leftGroup\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    zIndex: 10,\n  })\n  mainContainer.add(leftGroup)\n\n  const box1 = new BoxRenderable(renderer, {\n    id: \"box1\",\n    width: 45,\n    height: 7,\n    backgroundColor: \"#1e2936\",\n    zIndex: 20,\n    borderColor: \"#58a6ff\",\n    title: \"Document Section 1\",\n    flexDirection: \"column\",\n    padding: 1,\n    border: true,\n  })\n  leftGroup.add(box1)\n\n  const text1 = new TextRenderable(renderer, {\n    id: \"text1\",\n    content: \"This is a paragraph in the first box.\",\n    zIndex: 21,\n    fg: \"#f0f6fc\",\n  })\n  box1.add(text1)\n  allTextRenderables.push(text1)\n\n  const text2 = new TextRenderable(renderer, {\n    id: \"text2\",\n    content: \"It contains multiple lines of text\",\n    zIndex: 21,\n    fg: \"#f0f6fc\",\n  })\n  box1.add(text2)\n  allTextRenderables.push(text2)\n\n  const text3 = new TextRenderable(renderer, {\n    id: \"text3\",\n    content: \"that can be selected independently.\",\n    zIndex: 21,\n    fg: \"#f0f6fc\",\n  })\n  box1.add(text3)\n  allTextRenderables.push(text3)\n\n  const text4 = new TextRenderable(renderer, {\n    id: \"text4\",\n    content: \"世界, 你好世界, 中文, 한글\",\n    zIndex: 21,\n    fg: \"#f0f6fc\",\n  })\n  box1.add(text4)\n  allTextRenderables.push(text4)\n\n  const nestedBox = new BoxRenderable(renderer, {\n    id: \"nestedBox\",\n    left: 2,\n    top: 1,\n    width: 31,\n    height: 4,\n    backgroundColor: \"#2d1b69\",\n    zIndex: 25,\n    borderColor: \"#a371f7\",\n    borderStyle: \"double\",\n    border: true,\n  })\n  leftGroup.add(nestedBox)\n\n  const nestedText = new TextRenderable(renderer, {\n    id: \"nestedText\",\n    content: t`${yellow(\"Important:\")} ${bold(cyan(\"Nested content\"))} ${italic(green(\"with styles\"))}`,\n    width: 27,\n    height: 1,\n    zIndex: 26,\n    selectionBg: \"#4a5568\",\n    selectionFg: \"#ffffff\",\n  })\n  nestedBox.add(nestedText)\n  allTextRenderables.push(nestedText)\n\n  rightGroup = new BoxRenderable(renderer, {\n    id: \"rightGroup\",\n    position: \"absolute\",\n    left: 48,\n    top: 2,\n    zIndex: 10,\n  })\n  mainContainer.add(rightGroup)\n\n  const box2 = new BoxRenderable(renderer, {\n    id: \"box2\",\n    left: 2,\n    top: 0,\n    width: 35,\n    height: 12,\n    backgroundColor: \"#1c2128\",\n    zIndex: 20,\n    borderColor: \"#f85149\",\n    title: \"Code Example\",\n    borderStyle: \"rounded\",\n    flexDirection: \"column\",\n    padding: 1,\n    border: true,\n  })\n  rightGroup.add(box2)\n\n  const codeText1 = new TextRenderable(renderer, {\n    id: \"codeText1\",\n    content: t`${magenta(\"function\")} ${cyan(\"handleSelection\")}() {`,\n    zIndex: 21,\n    selectionBg: \"#4a5568\",\n  })\n  box2.add(codeText1)\n  allTextRenderables.push(codeText1)\n\n  const codeText2 = new TextRenderable(renderer, {\n    id: \"codeText2\",\n    content: t`  ${magenta(\"const\")} selected = ${cyan(\"getSelectedText\")}()`,\n    zIndex: 21,\n    selectionBg: \"#4a5568\",\n  })\n  box2.add(codeText2)\n  allTextRenderables.push(codeText2)\n\n  const codeText3 = new TextRenderable(renderer, {\n    id: \"codeText3\",\n    content: t`  ${yellow(\"console\")}.${green(\"log\")}(selected)`,\n    zIndex: 21,\n    selectionBg: \"#4a5568\",\n  })\n  box2.add(codeText3)\n  allTextRenderables.push(codeText3)\n\n  const codeText4 = new TextRenderable(renderer, {\n    id: \"codeText4\",\n    content: \"}\",\n    zIndex: 21,\n    fg: \"#e6edf3\",\n  })\n  box2.add(codeText4)\n  allTextRenderables.push(codeText4)\n\n  floatingBox = new BoxRenderable(renderer, {\n    id: \"floatingBox\",\n    position: \"absolute\",\n    left: 90,\n    top: 11,\n    width: 31,\n    height: 6,\n    backgroundColor: \"#1b2f23\",\n    zIndex: 30,\n    borderColor: \"#2ea043\",\n    title: \"README\",\n    borderStyle: \"single\",\n    border: true,\n  })\n  renderer.root.add(floatingBox)\n\n  const multilineText = new TextRenderable(renderer, {\n    id: \"multilineText\",\n    content: t`${bold(cyan(\"Selection Demo\"))}\n${green(\"✓\")} Cross-renderable selection\n${green(\"✓\")} Nested groups and boxes\n${green(\"✓\")} Styled text support`,\n    zIndex: 31,\n    selectionBg: \"#4a5568\",\n    selectionFg: \"#ffffff\",\n  })\n  floatingBox.add(multilineText)\n  allTextRenderables.push(multilineText)\n\n  const instructions = new TextRenderable(renderer, {\n    id: \"instructions\",\n    content: \"Click and drag to select text across any elements. Press 'C' to clear selection.\",\n    left: 2,\n    top: 17,\n    zIndex: 2,\n    fg: \"#f0f6fc\",\n  })\n  mainContainer.add(instructions)\n  allTextRenderables.push(instructions)\n\n  statusBox = new BoxRenderable(renderer, {\n    id: \"statusBox\",\n    position: \"absolute\",\n    left: 1,\n    top: 24,\n    width: 88,\n    height: 9,\n    backgroundColor: \"#0d1117\",\n    zIndex: 1,\n    borderColor: \"#50565d\",\n    title: \"Selection Status\",\n    titleAlignment: \"left\",\n    padding: 1,\n    border: true,\n  })\n  renderer.root.add(statusBox)\n\n  statusText = new TextRenderable(renderer, {\n    id: \"statusText\",\n    content: \"No selection - try selecting across different nested elements\",\n    zIndex: 2,\n    fg: \"#f0f6fc\",\n  })\n  statusBox.add(statusText)\n\n  selectionStartText = new TextRenderable(renderer, {\n    id: \"selectionStartText\",\n    content: \"\",\n    zIndex: 2,\n    fg: \"#7dd3fc\",\n  })\n  statusBox.add(selectionStartText)\n\n  selectionMiddleText = new TextRenderable(renderer, {\n    id: \"selectionMiddleText\",\n    content: \"\",\n    zIndex: 2,\n    fg: \"#94a3b8\",\n  })\n  statusBox.add(selectionMiddleText)\n\n  selectionEndText = new TextRenderable(renderer, {\n    id: \"selectionEndText\",\n    content: \"\",\n    zIndex: 2,\n    fg: \"#7dd3fc\",\n  })\n  statusBox.add(selectionEndText)\n\n  debugText = new TextRenderable(renderer, {\n    id: \"debugText\",\n    content: \"\",\n    zIndex: 2,\n    fg: \"#e6edf3\",\n  })\n  statusBox.add(debugText)\n\n  // Listen for selection events\n  renderer.on(\"selection\", (selection) => {\n    if (selection && statusText && debugText && selectionStartText && selectionMiddleText && selectionEndText) {\n      const selectedText = selection.getSelectedText()\n\n      // Count how many renderables have selection\n      const selectedCount = allTextRenderables.filter((r) => r.hasSelection()).length\n      const container = renderer.getSelectionContainer()\n      const containerInfo = container ? `Container: ${container.id}` : \"Container: none\"\n      debugText.content = `Selected renderables: ${selectedCount}/${allTextRenderables.length} | ${containerInfo}`\n\n      if (selectedText) {\n        const lines = selectedText.split(\"\\n\")\n        const totalLength = selectedText.length\n\n        if (lines.length > 1) {\n          statusText.content = `Selected ${lines.length} lines (${totalLength} chars):`\n          selectionStartText.content = lines[0]\n          selectionMiddleText.content = \"...\"\n          selectionEndText.content = lines[lines.length - 1]\n        } else if (selectedText.length > 60) {\n          statusText.content = `Selected ${totalLength} chars:`\n          selectionStartText.content = selectedText.substring(0, 30)\n          selectionMiddleText.content = \"...\"\n          selectionEndText.content = selectedText.substring(selectedText.length - 30)\n        } else {\n          statusText.content = `Selected ${totalLength} chars:`\n          selectionStartText.content = `\"${selectedText}\"`\n          selectionMiddleText.content = \"\"\n          selectionEndText.content = \"\"\n        }\n      } else {\n        statusText.content = \"Empty selection\"\n        selectionStartText.content = \"\"\n        selectionMiddleText.content = \"\"\n        selectionEndText.content = \"\"\n      }\n    }\n  })\n\n  renderer.keyInput.on(\"keypress\", (event) => {\n    const key = event.sequence\n    if (key === \"c\" || key === \"C\") {\n      renderer.clearSelection()\n      if (statusText && debugText && selectionStartText && selectionMiddleText && selectionEndText) {\n        statusText.content = \"Selection cleared\"\n        selectionStartText.content = \"\"\n        selectionMiddleText.content = \"\"\n        selectionEndText.content = \"\"\n        debugText.content = \"\"\n      }\n    }\n  })\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  allTextRenderables = []\n\n  mainContainer?.destroyRecursively()\n  statusBox?.destroyRecursively()\n  floatingBox?.destroyRecursively()\n\n  mainContainer = null\n  leftGroup = null\n  rightGroup = null\n  statusBox = null\n  statusText = null\n  selectionStartText = null\n  selectionMiddleText = null\n  selectionEndText = null\n  debugText = null\n\n  renderer.clearSelection()\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    targetFps: 30,\n    enableMouseMovement: true,\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/text-table-demo.ts",
    "content": "import {\n  BoxRenderable,\n  CliRenderer,\n  ScrollBoxRenderable,\n  TextTableRenderable,\n  TextRenderable,\n  bold,\n  createCliRenderer,\n  fg,\n  t,\n  type BorderStyle,\n  type KeyEvent,\n} from \"../index\"\nimport type { Selection } from \"../lib/selection\"\nimport type { TextTableColumnFitter, TextTableColumnWidthMode, TextTableContent } from \"../renderables/TextTable\"\nimport type { TextChunk } from \"../text-buffer\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys\"\n\nlet container: BoxRenderable | null = null\nlet primaryTable: TextTableRenderable | null = null\nlet unicodeTable: TextTableRenderable | null = null\nlet controlsText: TextRenderable | null = null\nlet tableAreaScrollBox: ScrollBoxRenderable | null = null\nlet selectionStatusText: TextRenderable | null = null\nlet selectionMetaText: TextRenderable | null = null\nlet selectionScrollBox: ScrollBoxRenderable | null = null\nlet keyboardHandler: ((key: KeyEvent) => void) | null = null\nlet selectionHandler: ((selection: Selection) => void) | null = null\n\nlet contentIndex = 0\nlet wrapIndex = 1\nlet borderIndex = 0\nlet columnWidthModeIndex = 0\nlet columnFitterIndex = 0\nlet cellPaddingIndex = 0\nlet borderEnabled = true\nlet outerBorderEnabled = true\nlet showBordersEnabled = true\n\nconst PALETTE = {\n  bg: \"#000000\",\n  panel: \"#0d0d0d\",\n  tablePrimaryBg: \"transparent\",\n  tableUnicodeBg: \"transparent\",\n  text: \"#f0f0f0\",\n  muted: \"#666666\",\n  soft: \"#bbbbbb\",\n  rose: \"#e8c97a\",\n  ember: \"#b8a0ff\",\n  flame: \"#ffffff\",\n  eye: \"#00d4aa\",\n  border: \"#2a2a2a\",\n} as const\n\nconst WRAP_MODES: Array<\"none\" | \"word\" | \"char\"> = [\"none\", \"word\", \"char\"]\nconst BORDER_STYLES: BorderStyle[] = [\"single\", \"rounded\", \"double\", \"heavy\"]\nconst COLUMN_WIDTH_MODES: TextTableColumnWidthMode[] = [\"content\", \"full\"]\nconst COLUMN_FITTERS: TextTableColumnFitter[] = [\"proportional\", \"balanced\"]\nconst CELL_PADDING_VALUES: number[] = [0, 1, 2]\n\nfunction cell(text: string): TextChunk[] {\n  return [\n    {\n      __isChunk: true,\n      text,\n    },\n  ]\n}\n\nconst primaryContentSets: TextTableContent[] = [\n  [\n    [[bold(\"Service\")], [bold(\"Status\")], [bold(\"Notes\")]],\n    [cell(\"api\"), [fg(PALETTE.eye)(\"OK\")], [fg(PALETTE.muted)(\"latency\"), ...cell(\" 28ms\")]],\n    [cell(\"worker\"), [fg(PALETTE.ember)(\"DEGRADED\")], cell(\"queue depth: 124\")],\n    [cell(\"billing\"), [fg(PALETTE.flame)(\"ERROR\")], cell(\"retrying payment provider\")],\n  ],\n  [\n    [[bold(\"Region\")], [bold(\"Requests\")], [bold(\"Trend\")]],\n    [cell(\"us-east-1\"), cell(\"1.2M\"), [fg(PALETTE.eye)(\"+12.4%\")]],\n    [cell(\"eu-west-1\"), cell(\"890K\"), [fg(PALETTE.soft)(\"+5.1%\")]],\n    [cell(\"ap-south-1\"), cell(\"540K\"), [fg(PALETTE.flame)(\"-2.0%\")]],\n  ],\n  [\n    [[bold(\"Task\")], [bold(\"Owner\")], [bold(\"ETA\")]],\n    [\n      cell(\n        \"Wrap regression in operational status dashboard with dynamic row heights and constrained layout validation\",\n      ),\n      cell(\"core platform and runtime reliability squad\"),\n      [\n        fg(PALETTE.eye)(\n          \"done after validating none, word, and char wrap modes across narrow, medium, wide, and ultra-wide terminal widths\",\n        ),\n      ],\n    ],\n    [\n      cell(\n        \"Unicode layout stabilization for mixed Latin, punctuation, symbols, and long identifiers in adjacent columns\",\n      ),\n      cell(\"render pipeline maintainers with fallback shaping support\"),\n      cell(\n        \"in review with follow-up checks for border style transitions, cell padding variants, and selection range consistency\",\n      ),\n    ],\n    [\n      cell(\"Snapshot pass for table rendering in content mode and full mode with heavy and double border combinations\"),\n      cell(\"qa automation and visual diff triage group\"),\n      cell(\n        \"today pending final baseline updates for oversized fixtures that intentionally stress wrapping behavior on high-resolution terminals\",\n      ),\n    ],\n    [\n      cell(\n        \"Document edge cases where long tokens without spaces force char wrapping and reveal per-cell clipping regressions\",\n      ),\n      cell(\"developer experience and docs tooling\"),\n      cell(\n        \"planned for this sprint once final reproducible examples are captured and linked to regression tracking tickets\",\n      ),\n    ],\n    [\n      cell(\n        \"Performance sweep of wrapping algorithm under large datasets to confirm stable frame times during rapid key toggling\",\n      ),\n      cell(\"runtime performance task force\"),\n      cell(\"scheduled after review, with benchmark runs on laptop and desktop terminals at 200-plus column widths\"),\n    ],\n  ],\n]\n\nconst unicodeContentSets: TextTableContent[] = [\n  [\n    [[bold(\"Locale\")], [bold(\"Sample\")]],\n    [cell(\"ja-JP\"), cell(\"東京の夜景と絵文字 🌃✨\")],\n    [cell(\"zh-CN\"), cell(\"你好世界，布局检查中 🚀\")],\n    [cell(\"ko-KR\"), cell(\"한글과 이모지 조합 테스트 😄\")],\n  ],\n  [\n    [[bold(\"Expression\")], [bold(\"Meaning\")]],\n    [cell(\"山川异域\"), cell(\"Different lands, shared sky 🌏\")],\n    [cell(\"꽃길만 걷자\"), cell(\"Walk only flower paths 🌸\")],\n    [cell(\"加油\"), cell(\"Keep pushing forward 💪\")],\n  ],\n  [\n    [[bold(\"Column\")], [bold(\"Wrapped Text\")]],\n    [\n      cell(\"mixed-languages\"),\n      cell(\n        \"CJK and emoji wrapping stress case: こんにちは世界 and 안녕하세요 세계 and 你好，世界 followed by long English prose that keeps flowing to test whether each cell wraps naturally even when the terminal is extremely wide and the row still needs multiple visual lines for readability 🌍🚀\",\n      ),\n    ],\n    [\n      cell(\"emoji-and-symbols\"),\n      cell(\n        \"Faces 😀😃😄😁😆 plus symbols 🧪📦🛰️🔧📊 mixed with version tags like release-candidate-build-2026-02-very-long-token-without-breaks to ensure char wrapping remains stable and no glyph alignment issues appear at column boundaries\",\n      ),\n    ],\n    [\n      cell(\"long-cjk-phrase\"),\n      cell(\n        \"長文の日本語テキストと中文段落和한국어문장을連続して配置し、その後に additional English context describing renderer behavior, border intersection handling, and selection extraction so that this single cell remains a reliable wrapping torture test.\",\n      ),\n    ],\n    [\n      cell(\"mixed-punctuation\"),\n      cell(\n        \"Wrap behavior with punctuation-heavy content: [alpha]{beta}(gamma)<delta>|epsilon| then repeated fragments, commas, semicolons, and slashes to verify token boundaries do not break border drawing logic or spacing consistency in neighboring columns.\",\n      ),\n    ],\n  ],\n]\n\nfunction currentWrapMode(): \"none\" | \"word\" | \"char\" {\n  return WRAP_MODES[wrapIndex] ?? \"word\"\n}\n\nfunction currentBorderStyle(): BorderStyle {\n  return BORDER_STYLES[borderIndex] ?? \"single\"\n}\n\nfunction currentColumnWidthMode(): TextTableColumnWidthMode {\n  return COLUMN_WIDTH_MODES[columnWidthModeIndex] ?? \"content\"\n}\n\nfunction currentColumnFitter(): TextTableColumnFitter {\n  return COLUMN_FITTERS[columnFitterIndex] ?? \"proportional\"\n}\n\nfunction currentCellPadding(): number {\n  return CELL_PADDING_VALUES[cellPaddingIndex] ?? 0\n}\n\nfunction updateControlsText(): void {\n  if (!controlsText) return\n\n  controlsText.content = t`${bold(\"TextTable Demo\")}  ${fg(PALETTE.muted)(\"1/2/3 dataset • W wrap • B style • M width • F fitter • P padding • N inner • O outer • H draw • drag to select • C clear\")}\nCurrent: dataset ${fg(PALETTE.soft)(String(contentIndex + 1))} | wrap ${fg(PALETTE.rose)(currentWrapMode())} | style ${fg(PALETTE.ember)(currentBorderStyle())} | width ${fg(PALETTE.eye)(currentColumnWidthMode())} | fitter ${fg(PALETTE.rose)(currentColumnFitter())} | padding ${fg(PALETTE.soft)(String(currentCellPadding()))} | inner ${fg(PALETTE.rose)(borderEnabled ? \"on\" : \"off\")} | outer ${fg(PALETTE.ember)(outerBorderEnabled ? \"on\" : \"off\")} | draw ${fg(PALETTE.eye)(showBordersEnabled ? \"on\" : \"off\")}`\n}\n\nfunction clearSelectionStatus(message: string): void {\n  if (!selectionMetaText || !selectionStatusText) return\n  selectionMetaText.content = message\n  selectionStatusText.content = \"\"\n  if (selectionScrollBox) {\n    selectionScrollBox.scrollTop = 0\n  }\n}\n\nfunction applyTableState(): void {\n  if (!primaryTable || !unicodeTable) return\n\n  primaryTable.content = primaryContentSets[contentIndex] ?? primaryContentSets[0]\n  unicodeTable.content = unicodeContentSets[contentIndex] ?? unicodeContentSets[0]\n\n  primaryTable.wrapMode = currentWrapMode()\n  unicodeTable.wrapMode = currentWrapMode()\n\n  primaryTable.borderStyle = currentBorderStyle()\n  unicodeTable.borderStyle = currentBorderStyle()\n\n  primaryTable.columnWidthMode = currentColumnWidthMode()\n  unicodeTable.columnWidthMode = currentColumnWidthMode()\n\n  primaryTable.columnFitter = currentColumnFitter()\n  unicodeTable.columnFitter = currentColumnFitter()\n\n  primaryTable.cellPadding = currentCellPadding()\n  unicodeTable.cellPadding = currentCellPadding()\n\n  primaryTable.border = borderEnabled\n  unicodeTable.border = borderEnabled\n\n  primaryTable.outerBorder = outerBorderEnabled\n  unicodeTable.outerBorder = outerBorderEnabled\n\n  primaryTable.showBorders = showBordersEnabled\n  unicodeTable.showBorders = showBordersEnabled\n\n  updateControlsText()\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"transparent\")\n\n  container = new BoxRenderable(renderer, {\n    id: \"text-table-demo-container\",\n    width: \"100%\",\n    height: \"100%\",\n    flexDirection: \"column\",\n    padding: 1,\n    gap: 1,\n    backgroundColor: \"transparent\",\n  })\n  renderer.root.add(container)\n\n  controlsText = new TextRenderable(renderer, {\n    id: \"text-table-demo-controls\",\n    content: \"\",\n    fg: PALETTE.text,\n    wrapMode: \"word\",\n    selectable: false,\n  })\n\n  tableAreaScrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"text-table-demo-table-area-scroll\",\n    width: \"100%\",\n    flexGrow: 1,\n    flexShrink: 1,\n    scrollY: true,\n    scrollX: false,\n    border: false,\n    backgroundColor: \"transparent\",\n    contentOptions: {\n      flexDirection: \"column\",\n      gap: 1,\n    },\n  })\n\n  const primaryLabel = new TextRenderable(renderer, {\n    id: \"text-table-demo-primary-label\",\n    content: t`${bold(\"Operational Table\")}`,\n    fg: PALETTE.ember,\n    selectable: false,\n  })\n\n  primaryTable = new TextTableRenderable(renderer, {\n    id: \"text-table-demo-primary\",\n    width: \"100%\",\n    wrapMode: currentWrapMode(),\n    columnFitter: currentColumnFitter(),\n    borderStyle: currentBorderStyle(),\n    borderColor: PALETTE.ember,\n    fg: PALETTE.text,\n    bg: PALETTE.tablePrimaryBg,\n    content: primaryContentSets[contentIndex] ?? primaryContentSets[0],\n  })\n\n  const unicodeLabel = new TextRenderable(renderer, {\n    id: \"text-table-demo-unicode-label\",\n    content: t`${bold(\"Unicode/CJK/Emoji Table\")}`,\n    fg: PALETTE.rose,\n    selectable: false,\n  })\n\n  unicodeTable = new TextTableRenderable(renderer, {\n    id: \"text-table-demo-unicode\",\n    width: \"100%\",\n    wrapMode: currentWrapMode(),\n    columnFitter: currentColumnFitter(),\n    borderStyle: currentBorderStyle(),\n    borderColor: PALETTE.rose,\n    fg: PALETTE.text,\n    bg: PALETTE.tableUnicodeBg,\n    content: unicodeContentSets[contentIndex] ?? unicodeContentSets[0],\n  })\n\n  const selectionBox = new BoxRenderable(renderer, {\n    id: \"text-table-demo-selection-box\",\n    width: \"100%\",\n    height: 10,\n    flexGrow: 0,\n    flexShrink: 0,\n    border: true,\n    borderStyle: \"double\",\n    borderColor: PALETTE.border,\n    title: \"Selected Text\",\n    titleAlignment: \"left\",\n    padding: 1,\n    backgroundColor: PALETTE.panel,\n  })\n\n  selectionMetaText = new TextRenderable(renderer, {\n    id: \"text-table-demo-selection-meta\",\n    content: \"No selection yet\",\n    fg: PALETTE.eye,\n    selectable: false,\n  })\n\n  selectionScrollBox = new ScrollBoxRenderable(renderer, {\n    id: \"text-table-demo-selection-scroll\",\n    width: \"100%\",\n    flexGrow: 1,\n    flexShrink: 1,\n    scrollY: true,\n    scrollX: false,\n    border: false,\n    backgroundColor: \"transparent\",\n  })\n\n  tableAreaScrollBox.verticalScrollbarOptions = { visible: false }\n  selectionScrollBox.verticalScrollbarOptions = { visible: false }\n\n  selectionStatusText = new TextRenderable(renderer, {\n    id: \"text-table-demo-selection-text\",\n    content: \"\",\n    fg: PALETTE.text,\n    wrapMode: \"word\",\n    width: \"100%\",\n    selectable: false,\n  })\n\n  selectionBox.add(selectionMetaText)\n  selectionBox.add(selectionScrollBox)\n  selectionScrollBox.add(selectionStatusText)\n\n  tableAreaScrollBox.add(controlsText)\n  tableAreaScrollBox.add(primaryLabel)\n  tableAreaScrollBox.add(primaryTable)\n  tableAreaScrollBox.add(unicodeLabel)\n  tableAreaScrollBox.add(unicodeTable)\n\n  container.add(tableAreaScrollBox)\n  container.add(selectionBox)\n\n  selectionHandler = (selection: Selection) => {\n    if (!selectionMetaText || !selectionStatusText) return\n\n    const selectedText = selection.getSelectedText()\n    if (!selectedText) {\n      clearSelectionStatus(\"Empty selection\")\n      return\n    }\n\n    const lines = selectedText.split(\"\\n\").length\n    const chars = selectedText.length\n    selectionMetaText.content = `Selected ${lines} line${lines === 1 ? \"\" : \"s\"} (${chars} chars)`\n    selectionStatusText.content = selectedText\n    if (selectionScrollBox) {\n      selectionScrollBox.scrollTop = 0\n    }\n  }\n\n  renderer.on(\"selection\", selectionHandler)\n\n  keyboardHandler = (key: KeyEvent) => {\n    if (key.ctrl || key.meta) return\n\n    if (key.name === \"1\" || key.name === \"2\" || key.name === \"3\") {\n      contentIndex = Number(key.name) - 1\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"w\") {\n      wrapIndex = (wrapIndex + 1) % WRAP_MODES.length\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"b\") {\n      borderIndex = (borderIndex + 1) % BORDER_STYLES.length\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"m\") {\n      columnWidthModeIndex = (columnWidthModeIndex + 1) % COLUMN_WIDTH_MODES.length\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"f\") {\n      columnFitterIndex = (columnFitterIndex + 1) % COLUMN_FITTERS.length\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"p\") {\n      cellPaddingIndex = (cellPaddingIndex + 1) % CELL_PADDING_VALUES.length\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"n\") {\n      borderEnabled = !borderEnabled\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"o\") {\n      outerBorderEnabled = !outerBorderEnabled\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"h\") {\n      showBordersEnabled = !showBordersEnabled\n      applyTableState()\n      return\n    }\n\n    if (key.name === \"c\") {\n      renderer.clearSelection()\n      clearSelectionStatus(\"Selection cleared\")\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", keyboardHandler)\n  applyTableState()\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (keyboardHandler) {\n    renderer.keyInput.off(\"keypress\", keyboardHandler)\n    keyboardHandler = null\n  }\n\n  if (selectionHandler) {\n    renderer.off(\"selection\", selectionHandler)\n    selectionHandler = null\n  }\n\n  container?.destroyRecursively()\n  container = null\n  primaryTable = null\n  unicodeTable = null\n  controlsText = null\n  tableAreaScrollBox = null\n  selectionStatusText = null\n  selectionMetaText = null\n  selectionScrollBox = null\n\n  contentIndex = 0\n  wrapIndex = 1\n  borderIndex = 0\n  columnWidthModeIndex = 0\n  columnFitterIndex = 0\n  cellPaddingIndex = 0\n  borderEnabled = true\n  outerBorderEnabled = true\n  showBordersEnabled = true\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n    enableMouseMovement: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/text-truncation-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport {\n  CliRenderer,\n  createCliRenderer,\n  TextRenderable,\n  BoxRenderable,\n  t,\n  green,\n  bold,\n  cyan,\n  yellow,\n  magenta,\n} from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet renderer: CliRenderer | null = null\nlet mainContainer: BoxRenderable | null = null\nlet header: BoxRenderable | null = null\nlet headerText: TextRenderable | null = null\nlet leftColumn: BoxRenderable | null = null\nlet rightColumn: BoxRenderable | null = null\nlet footer: BoxRenderable | null = null\nlet footerText: TextRenderable | null = null\nlet selectionBox: BoxRenderable | null = null\nlet selectionStatusText: TextRenderable | null = null\nlet selectionStartText: TextRenderable | null = null\nlet selectionMiddleText: TextRenderable | null = null\nlet selectionEndText: TextRenderable | null = null\n\n// Text elements to demonstrate truncation\nlet singleLineText1: TextRenderable | null = null\nlet singleLineText2: TextRenderable | null = null\nlet singleLineText3: TextRenderable | null = null\nlet multilineText1: TextRenderable | null = null\nlet multilineText2: TextRenderable | null = null\nlet styledText: TextRenderable | null = null\n\nlet truncateEnabled = false\nlet wrapMode: \"none\" | \"char\" | \"word\" = \"none\"\n\nconst allTextElements: TextRenderable[] = []\n\nfunction createLayout(rendererInstance: CliRenderer): void {\n  renderer = rendererInstance\n  renderer.setBackgroundColor(\"#0d1117\")\n\n  // Main container\n  mainContainer = new BoxRenderable(renderer, {\n    id: \"mainContainer\",\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    flexDirection: \"column\",\n    backgroundColor: \"#0d1117\",\n  })\n  renderer.root.add(mainContainer)\n\n  // Header\n  header = new BoxRenderable(renderer, {\n    id: \"header\",\n    width: \"auto\",\n    height: 3,\n    backgroundColor: \"#161b22\",\n    borderStyle: \"single\",\n    borderColor: \"#30363d\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    border: true,\n  })\n  mainContainer.add(header)\n\n  headerText = new TextRenderable(renderer, {\n    id: \"headerText\",\n    content: \"Text Truncation Demo - Press 'T' to toggle truncation\",\n    fg: \"#58a6ff\",\n  })\n  header.add(headerText)\n\n  // Content area with two columns\n  const contentArea = new BoxRenderable(renderer, {\n    id: \"contentArea\",\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    flexDirection: \"row\",\n    gap: 1,\n    padding: 1,\n  })\n  mainContainer.add(contentArea)\n\n  // Left column\n  leftColumn = new BoxRenderable(renderer, {\n    id: \"leftColumn\",\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    flexDirection: \"column\",\n    gap: 1,\n  })\n  contentArea.add(leftColumn)\n\n  // Single line text boxes\n  const singleLineBox1 = new BoxRenderable(renderer, {\n    id: \"singleLineBox1\",\n    width: \"auto\",\n    height: \"auto\",\n    minHeight: 5,\n    backgroundColor: \"#161b22\",\n    borderStyle: \"rounded\",\n    borderColor: \"#58a6ff\",\n    title: \"Single Line Text 1\",\n    padding: 1,\n    border: true,\n  })\n  leftColumn.add(singleLineBox1)\n\n  singleLineText1 = new TextRenderable(renderer, {\n    id: \"singleLineText1\",\n    content:\n      \"This is a very long single line of text that will definitely exceed the width of most terminal windows and should be truncated when truncation is enabled\",\n    fg: \"#c9d1d9\",\n    wrapMode: wrapMode,\n  })\n  singleLineBox1.add(singleLineText1)\n  allTextElements.push(singleLineText1)\n\n  const singleLineBox2 = new BoxRenderable(renderer, {\n    id: \"singleLineBox2\",\n    width: \"auto\",\n    height: \"auto\",\n    minHeight: 5,\n    backgroundColor: \"#161b22\",\n    borderStyle: \"rounded\",\n    borderColor: \"#3fb950\",\n    title: \"Single Line Text 2\",\n    padding: 1,\n    border: true,\n  })\n  leftColumn.add(singleLineBox2)\n\n  singleLineText2 = new TextRenderable(renderer, {\n    id: \"singleLineText2\",\n    content: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz\",\n    fg: \"#3fb950\",\n    wrapMode: wrapMode,\n  })\n  singleLineBox2.add(singleLineText2)\n  allTextElements.push(singleLineText2)\n\n  const singleLineBox3 = new BoxRenderable(renderer, {\n    id: \"singleLineBox3\",\n    width: \"auto\",\n    height: \"auto\",\n    minHeight: 7,\n    backgroundColor: \"#161b22\",\n    borderStyle: \"rounded\",\n    borderColor: \"#d29922\",\n    title: \"Single Line Text 3 (Unicode)\",\n    padding: 1,\n    border: true,\n  })\n  leftColumn.add(singleLineBox3)\n\n  singleLineText3 = new TextRenderable(renderer, {\n    id: \"singleLineText3\",\n    content: \"🌟 Unicode test: こんにちは世界 Hello World 你好世界 안녕하세요 🚀 More emoji: 🎨🎭🎪🎬🎮🎯\",\n    fg: \"#d29922\",\n    wrapMode: wrapMode,\n  })\n  singleLineBox3.add(singleLineText3)\n  allTextElements.push(singleLineText3)\n\n  // Right column\n  rightColumn = new BoxRenderable(renderer, {\n    id: \"rightColumn\",\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    flexDirection: \"column\",\n    gap: 1,\n  })\n  contentArea.add(rightColumn)\n\n  // Multiline text boxes\n  const multilineBox1 = new BoxRenderable(renderer, {\n    id: \"multilineBox1\",\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    backgroundColor: \"#161b22\",\n    borderStyle: \"rounded\",\n    borderColor: \"#f778ba\",\n    title: \"Multiline Text (Word Wrap)\",\n    padding: 1,\n    border: true,\n  })\n  rightColumn.add(multilineBox1)\n\n  multilineText1 = new TextRenderable(renderer, {\n    id: \"multilineText1\",\n    content: `This is a multiline text block that demonstrates how truncation works with word wrapping enabled. Each line that exceeds the viewport width will be truncated independently. Try resizing the terminal to see how it behaves!`,\n    fg: \"#f778ba\",\n    wrapMode: wrapMode,\n  })\n  multilineBox1.add(multilineText1)\n  allTextElements.push(multilineText1)\n\n  const multilineBox2 = new BoxRenderable(renderer, {\n    id: \"multilineBox2\",\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    backgroundColor: \"#161b22\",\n    borderStyle: \"rounded\",\n    borderColor: \"#bc8cff\",\n    title: \"Multiline Text\",\n    padding: 1,\n    border: true,\n  })\n  rightColumn.add(multilineBox2)\n\n  multilineText2 = new TextRenderable(renderer, {\n    id: \"multilineText2\",\n    content: `Line 1: This is a long line without wrapping\nLine 2: Another very long line that will be truncated when enabled\nLine 3: Short line\nLine 4: Yet another extremely long line with lots of text to demonstrate middle truncation behavior`,\n    fg: \"#bc8cff\",\n    wrapMode: wrapMode,\n  })\n  multilineBox2.add(multilineText2)\n  allTextElements.push(multilineText2)\n\n  const styledBox = new BoxRenderable(renderer, {\n    id: \"styledBox\",\n    width: \"auto\",\n    height: \"auto\",\n    flexGrow: 1,\n    backgroundColor: \"#161b22\",\n    borderStyle: \"rounded\",\n    borderColor: \"#ff7b72\",\n    title: \"Styled Text with Truncation\",\n    padding: 1,\n    border: true,\n  })\n  rightColumn.add(styledBox)\n\n  styledText = new TextRenderable(renderer, {\n    id: \"styledText\",\n    content: t`${bold(cyan(\"Bold Cyan:\"))} ${yellow(\"Yellow text\")} ${magenta(\"and magenta\")} ${green(\"with green parts\")} and more styled text that goes on and on`,\n    fg: \"#c9d1d9\",\n    wrapMode: wrapMode,\n  })\n  styledBox.add(styledText)\n  allTextElements.push(styledText)\n\n  // Footer\n  footer = new BoxRenderable(renderer, {\n    id: \"footer\",\n    width: \"auto\",\n    height: 3,\n    backgroundColor: \"#161b22\",\n    borderStyle: \"single\",\n    borderColor: \"#30363d\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    border: true,\n  })\n  mainContainer.add(footer)\n\n  footerText = new TextRenderable(renderer, {\n    id: \"footerText\",\n    content: \"\",\n    fg: \"#8b949e\",\n  })\n  footer.add(footerText)\n\n  selectionBox = new BoxRenderable(renderer, {\n    id: \"selectionBox\",\n    width: \"auto\",\n    height: 7,\n    backgroundColor: \"#0d1117\",\n    borderStyle: \"single\",\n    borderColor: \"#30363d\",\n    title: \"Selection\",\n    titleAlignment: \"left\",\n    flexDirection: \"column\",\n    gap: 1,\n    padding: 1,\n    border: true,\n  })\n  mainContainer.add(selectionBox)\n\n  selectionStatusText = new TextRenderable(renderer, {\n    id: \"selectionStatusText\",\n    content: \"Select text to see details here\",\n    fg: \"#8b949e\",\n  })\n  selectionBox.add(selectionStatusText)\n\n  selectionStartText = new TextRenderable(renderer, {\n    id: \"selectionStartText\",\n    content: \"\",\n    fg: \"#7dd3fc\",\n  })\n  selectionBox.add(selectionStartText)\n\n  selectionMiddleText = new TextRenderable(renderer, {\n    id: \"selectionMiddleText\",\n    content: \"\",\n    fg: \"#94a3b8\",\n  })\n  selectionBox.add(selectionMiddleText)\n\n  selectionEndText = new TextRenderable(renderer, {\n    id: \"selectionEndText\",\n    content: \"\",\n    fg: \"#7dd3fc\",\n  })\n  selectionBox.add(selectionEndText)\n\n  renderer.on(\"selection\", (selection) => {\n    if (!selectionStatusText || !selectionStartText || !selectionMiddleText || !selectionEndText) return\n\n    const selectedText = selection?.getSelectedText()\n    if (selectedText) {\n      const lines = selectedText.split(\"\\n\")\n      const totalLength = selectedText.length\n\n      if (lines.length > 1) {\n        selectionStatusText.content = `Selected ${lines.length} lines (${totalLength} chars):`\n        selectionStartText.content = lines[0]\n        selectionMiddleText.content = \"...\"\n        selectionEndText.content = lines[lines.length - 1]\n      } else if (selectedText.length > 60) {\n        selectionStatusText.content = `Selected ${totalLength} chars:`\n        selectionStartText.content = selectedText.substring(0, 30)\n        selectionMiddleText.content = \"...\"\n        selectionEndText.content = selectedText.substring(selectedText.length - 30)\n      } else {\n        selectionStatusText.content = `Selected ${totalLength} chars:`\n        selectionStartText.content = `\"${selectedText}\"`\n        selectionMiddleText.content = \"\"\n        selectionEndText.content = \"\"\n      }\n    } else {\n      selectionStatusText.content = \"Empty selection\"\n      selectionStartText.content = \"\"\n      selectionMiddleText.content = \"\"\n      selectionEndText.content = \"\"\n    }\n  })\n\n  updateFooterText()\n}\n\nfunction updateFooterText(): void {\n  if (!footerText) return\n\n  const truncateStatus = truncateEnabled ? \"ENABLED\" : \"DISABLED\"\n  const truncateColor = truncateEnabled ? green : yellow\n  const wrapColor = wrapMode === \"none\" ? yellow : cyan\n  footerText.content = t`Truncate: ${truncateColor(bold(truncateStatus))} | Wrap: ${wrapColor(bold(wrapMode.toUpperCase()))} | ${cyan(\"T\")}: toggle truncate | ${cyan(\"W\")}: cycle wrap | ${cyan(\"R\")}: resize | ${cyan(\"C\")}: clear selection | ${cyan(\"Ctrl+C\")}: exit`\n}\n\nfunction toggleTruncation(): void {\n  truncateEnabled = !truncateEnabled\n\n  for (const text of allTextElements) {\n    text.truncate = truncateEnabled\n  }\n\n  updateFooterText()\n}\n\nfunction cycleWrapMode(): void {\n  if (wrapMode === \"none\") {\n    wrapMode = \"char\"\n  } else if (wrapMode === \"char\") {\n    wrapMode = \"word\"\n  } else {\n    wrapMode = \"none\"\n  }\n\n  for (const text of allTextElements) {\n    text.wrapMode = wrapMode\n  }\n\n  updateFooterText()\n}\n\nfunction toggleColumnSizes(): void {\n  if (!leftColumn || !rightColumn) return\n\n  // Swap flex-grow values to change relative sizes\n  const leftGrow = leftColumn.flexGrow\n  const rightGrow = rightColumn.flexGrow\n\n  if (leftGrow === 1 && rightGrow === 1) {\n    // Make left column larger\n    leftColumn.flexGrow = 2\n    rightColumn.flexGrow = 1\n  } else if (leftGrow === 2 && rightGrow === 1) {\n    // Make right column larger\n    leftColumn.flexGrow = 1\n    rightColumn.flexGrow = 2\n  } else {\n    // Reset to equal\n    leftColumn.flexGrow = 1\n    rightColumn.flexGrow = 1\n  }\n}\n\nfunction handleKeyPress(event: any): void {\n  const key = event.sequence.toLowerCase()\n\n  switch (key) {\n    case \"t\":\n      toggleTruncation()\n      break\n    case \"w\":\n      cycleWrapMode()\n      break\n    case \"r\":\n      toggleColumnSizes()\n      break\n    case \"c\":\n      renderer?.clearSelection()\n      if (selectionStatusText && selectionStartText && selectionMiddleText && selectionEndText) {\n        selectionStatusText.content = \"Selection cleared\"\n        selectionStartText.content = \"\"\n        selectionMiddleText.content = \"\"\n        selectionEndText.content = \"\"\n      }\n      break\n  }\n}\n\nexport function run(rendererInstance: CliRenderer): void {\n  createLayout(rendererInstance)\n  rendererInstance.keyInput.on(\"keypress\", handleKeyPress)\n}\n\nexport function destroy(rendererInstance: CliRenderer): void {\n  rendererInstance.keyInput.off(\"keypress\", handleKeyPress)\n\n  mainContainer?.destroyRecursively()\n\n  renderer = null\n  mainContainer = null\n  header = null\n  headerText = null\n  leftColumn = null\n  rightColumn = null\n  footer = null\n  footerText = null\n  selectionBox = null\n  selectionStatusText = null\n  selectionStartText = null\n  selectionMiddleText = null\n  selectionEndText = null\n  singleLineText1 = null\n  singleLineText2 = null\n  singleLineText3 = null\n  multilineText1 = null\n  multilineText2 = null\n  styledText = null\n  allTextElements.length = 0\n  rendererInstance.clearSelection()\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    targetFps: 30,\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/text-wrap.ts",
    "content": "#!/usr/bin/env bun\n/**\n * Text wrapping example\n * Demonstrates automatic text wrapping when the wrap option is enabled\n */\nimport {\n  CliRenderer,\n  createCliRenderer,\n  TextRenderable,\n  BoxRenderable,\n  type MouseEvent,\n  t,\n  fg,\n  bold,\n} from \"../index.js\"\nimport { TextNodeRenderable } from \"../renderables/TextNode.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { InputRenderable, InputRenderableEvents } from \"../renderables/Input.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { readFile, stat } from \"node:fs/promises\"\n\nlet mainContainer: BoxRenderable | null = null\nlet contentBox: BoxRenderable | null = null\nlet textBox: ScrollBoxRenderable | null = null\nlet textRenderable: TextRenderable | null = null\nlet instructionsBox: BoxRenderable | null = null\nlet instructionsText1: TextRenderable | null = null\nlet instructionsText2: TextRenderable | null = null\nlet filePathInput: InputRenderable | null = null\nlet fileInputContainer: BoxRenderable | null = null\nlet isInputVisible: boolean = false\n\n// Resize state\nlet isResizing = false\nlet resizeDirection: \"nw\" | \"ne\" | \"sw\" | \"se\" | \"n\" | \"s\" | \"w\" | \"e\" | null = null\nlet resizeStartX = 0\nlet resizeStartY = 0\nlet resizeStartLeft = 0\nlet resizeStartTop = 0\nlet resizeStartWidth = 0\nlet resizeStartHeight = 0\n\n// Helper function to detect resize direction based on mouse position\nfunction getResizeDirection(\n  mouseX: number,\n  mouseY: number,\n  boxLeft: number,\n  boxTop: number,\n  boxWidth: number,\n  boxHeight: number,\n): \"nw\" | \"ne\" | \"sw\" | \"se\" | \"n\" | \"s\" | \"w\" | \"e\" | null {\n  // Check if mouse is exactly on the border (1 pixel wide)\n  // Border coordinates: left edge, right edge, top edge, bottom edge\n  const onLeftBorder = mouseX === boxLeft\n  const onRightBorder = mouseX === boxLeft + boxWidth - 1\n  const onTopBorder = mouseY === boxTop\n  const onBottomBorder = mouseY === boxTop + boxHeight - 1\n\n  // Check if mouse is within the box bounds (including border)\n  const withinHorizontalBounds = mouseX >= boxLeft && mouseX <= boxLeft + boxWidth - 1\n  const withinVerticalBounds = mouseY >= boxTop && mouseY <= boxTop + boxHeight - 1\n\n  // Only detect resize if mouse is on a border AND within bounds\n  const left = onLeftBorder && withinVerticalBounds\n  const right = onRightBorder && withinVerticalBounds\n  const top = onTopBorder && withinHorizontalBounds\n  const bottom = onBottomBorder && withinHorizontalBounds\n\n  if (top && left) return \"nw\"\n  if (top && right) return \"ne\"\n  if (bottom && left) return \"sw\"\n  if (bottom && right) return \"se\"\n  if (top) return \"n\"\n  if (bottom) return \"s\"\n  if (left) return \"w\"\n  if (right) return \"e\"\n\n  return null\n}\n\n// Helper functions for file input\nfunction showFileInput(): void {\n  if (fileInputContainer && filePathInput) {\n    fileInputContainer.visible = true\n    filePathInput.value = \"\"\n    filePathInput.focus()\n    isInputVisible = true\n  }\n}\n\nfunction hideFileInput(): void {\n  if (fileInputContainer && filePathInput) {\n    fileInputContainer.visible = false\n    filePathInput.blur()\n    isInputVisible = false\n  }\n}\n\n// Mouse event handler for resizing\nfunction handleTextBoxMouse(event: MouseEvent): void {\n  if (!textBox) return\n\n  switch (event.type) {\n    case \"move\":\n    case \"over\": {\n      if (!isResizing) {\n        // Use the computed screen position of the textBox\n        const boxLeft = textBox.x\n        const boxTop = textBox.y\n        const direction = getResizeDirection(event.x, event.y, boxLeft, boxTop, textBox.width, textBox.height)\n        resizeDirection = direction\n\n        // Update cursor style based on resize direction\n        if (direction) {\n          const cursorMap = {\n            nw: \"nw-resize\",\n            ne: \"ne-resize\",\n            sw: \"sw-resize\",\n            se: \"se-resize\",\n            n: \"n-resize\",\n            s: \"s-resize\",\n            w: \"w-resize\",\n            e: \"e-resize\",\n          } as const\n          // Note: OpenTUI may not support custom cursor styles yet, but we can still track the direction\n        }\n      }\n      break\n    }\n\n    case \"down\": {\n      if (resizeDirection) {\n        isResizing = true\n        resizeStartX = event.x\n        resizeStartY = event.y\n        resizeStartWidth = textBox.width\n        resizeStartHeight = textBox.height\n        // Store the original position - convert from absolute screen coords to relative coords within contentBox\n        // contentBox has padding: 1, so subtract padding to get relative coordinates\n        const contentPadding = contentBox ? 1 : 0\n        resizeStartLeft = textBox.x - contentPadding\n        resizeStartTop = textBox.y - contentPadding\n        event.stopPropagation()\n      }\n      break\n    }\n\n    case \"drag\": {\n      // Don't handle drag here - let the global handler manage it\n      // Don't stop propagation so global handler can receive events\n      break\n    }\n\n    case \"up\":\n    case \"drag-end\": {\n      // Don't handle resize end here - let the global handler manage it\n      // Don't stop propagation so global handler can receive events\n      break\n    }\n\n    case \"out\": {\n      if (!isResizing) {\n        resizeDirection = null\n      }\n      // During resize, keep the original resizeDirection - don't clear it\n      break\n    }\n  }\n}\n\n// Global mouse handler for resize operations\nfunction handleGlobalMouse(event: MouseEvent): void {\n  switch (event.type) {\n    case \"move\":\n    case \"drag\": {\n      // Only handle if we're in a resize operation\n      if (isResizing && resizeDirection && textBox) {\n        const deltaX = event.x - resizeStartX\n        const deltaY = event.y - resizeStartY\n\n        let newWidth = resizeStartWidth\n        let newHeight = resizeStartHeight\n        let newLeft = resizeStartLeft\n        let newTop = resizeStartTop\n\n        // Handle different resize directions\n        switch (resizeDirection) {\n          case \"nw\":\n            newWidth = Math.max(10, resizeStartWidth - deltaX)\n            newHeight = Math.max(5, resizeStartHeight - deltaY)\n            newLeft = resizeStartLeft + (resizeStartWidth - newWidth)\n            newTop = resizeStartTop + (resizeStartHeight - newHeight)\n            break\n          case \"ne\":\n            newWidth = Math.max(10, resizeStartWidth + deltaX)\n            newHeight = Math.max(5, resizeStartHeight - deltaY)\n            newTop = resizeStartTop + (resizeStartHeight - newHeight)\n            break\n          case \"sw\":\n            newWidth = Math.max(10, resizeStartWidth - deltaX)\n            newHeight = Math.max(5, resizeStartHeight + deltaY)\n            newLeft = resizeStartLeft + (resizeStartWidth - newWidth)\n            break\n          case \"se\":\n            newWidth = Math.max(10, resizeStartWidth + deltaX)\n            newHeight = Math.max(5, resizeStartHeight + deltaY)\n            break\n          case \"n\":\n            newHeight = Math.max(5, resizeStartHeight - deltaY)\n            newTop = resizeStartTop + (resizeStartHeight - newHeight)\n            break\n          case \"s\":\n            newHeight = Math.max(5, resizeStartHeight + deltaY)\n            break\n          case \"w\":\n            newWidth = Math.max(10, resizeStartWidth - deltaX)\n            newLeft = resizeStartLeft + (resizeStartWidth - newWidth)\n            break\n          case \"e\":\n            newWidth = Math.max(10, resizeStartWidth + deltaX)\n            break\n        }\n\n        // Constrain to content box bounds (accounting for padding: 1)\n        if (contentBox) {\n          const contentPadding = 1\n          const maxWidth = contentBox.width - 2 * contentPadding\n          const maxHeight = contentBox.height - 2 * contentPadding\n          const minLeft = contentPadding\n          const minTop = contentPadding\n          const maxLeft = contentBox.width - newWidth - contentPadding\n          const maxTop = contentBox.height - newHeight - contentPadding\n\n          newWidth = Math.min(newWidth, maxWidth)\n          newHeight = Math.min(newHeight, maxHeight)\n          newLeft = Math.max(minLeft, Math.min(newLeft, maxLeft))\n          newTop = Math.max(minTop, Math.min(newTop, maxTop))\n        }\n\n        // Apply the new dimensions and position\n        textBox.width = newWidth\n        textBox.height = newHeight\n        textBox.left = newLeft\n        textBox.top = newTop\n      }\n      break\n    }\n\n    case \"up\": {\n      // End resize operation on any mouse up\n      if (isResizing) {\n        isResizing = false\n        resizeDirection = null\n      }\n      break\n    }\n  }\n}\n\n// Create styled demo text using TextNodes\nfunction createDemoText(): TextNodeRenderable {\n  const titleNode = TextNodeRenderable.fromString(\"🎨 OpenTUI Text Wrapping Demo\", {\n    fg: \"#7aa2f7\",\n    attributes: 1, // bold\n  })\n\n  const introNode = TextNodeRenderable.fromString(\"\\n\\nWelcome to the \", {\n    fg: \"#c0caf5\",\n  })\n\n  const highlightNode = TextNodeRenderable.fromString(\"text wrapping demonstration\", {\n    fg: \"#9ece6a\",\n    attributes: 1, // bold\n  })\n\n  const introContNode = TextNodeRenderable.fromString(\n    \". This example showcases how OpenTUI handles automatic text wrapping with styled content using TextNodes.\",\n    {\n      fg: \"#c0caf5\",\n    },\n  )\n\n  const featuresTitle = TextNodeRenderable.fromString(\"\\n\\n✨ Key Features:\", {\n    fg: \"#bb9af7\",\n    attributes: 1,\n  })\n\n  const feature1Node = TextNodeRenderable.fromNodes([\n    TextNodeRenderable.fromString(\"\\n• \", { fg: \"#9ece6a\" }),\n    TextNodeRenderable.fromString(\"Word-based wrapping\", { fg: \"#c0caf5\", attributes: 1 }),\n    TextNodeRenderable.fromString(\" - Preserves word boundaries when breaking lines 📖\", { fg: \"#565f89\" }),\n  ])\n\n  const feature2Node = TextNodeRenderable.fromNodes([\n    TextNodeRenderable.fromString(\"\\n• \", { fg: \"#9ece6a\" }),\n    TextNodeRenderable.fromString(\"Character-based wrapping\", { fg: \"#c0caf5\", attributes: 1 }),\n    TextNodeRenderable.fromString(\" - Breaks at any character for precise control ✂️\", { fg: \"#565f89\" }),\n  ])\n\n  const feature3Node = TextNodeRenderable.fromNodes([\n    TextNodeRenderable.fromString(\"\\n• \", { fg: \"#9ece6a\" }),\n    TextNodeRenderable.fromString(\"Dynamic resizing\", { fg: \"#c0caf5\", attributes: 1 }),\n    TextNodeRenderable.fromString(\" - Text reflows automatically as container dimensions change 🔄\", { fg: \"#565f89\" }),\n  ])\n\n  const feature4Node = TextNodeRenderable.fromNodes([\n    TextNodeRenderable.fromString(\"\\n• \", { fg: \"#9ece6a\" }),\n    TextNodeRenderable.fromString(\"Rich styling\", { fg: \"#c0caf5\", attributes: 1 }),\n    TextNodeRenderable.fromString(\" - Individual text segments can have different colors and attributes 🎨\", {\n      fg: \"#565f89\",\n    }),\n  ])\n\n  const demoTitle = TextNodeRenderable.fromString(\"\\n\\n🔧 How It Works:\", {\n    fg: \"#bb9af7\",\n    attributes: 1,\n  })\n\n  const demoText = TextNodeRenderable.fromString(\n    \"\\n\\nTextNodes are created with specific styling and then composed together to form rich, formatted text content. Each node can contain different foreground colors, background colors, and text attributes like \",\n    {\n      fg: \"#c0caf5\",\n    },\n  )\n\n  const boldExample = TextNodeRenderable.fromString(\"bold\", {\n    fg: \"#f7768e\",\n    attributes: 1,\n  })\n\n  const demoCont = TextNodeRenderable.fromString(\", \", {\n    fg: \"#c0caf5\",\n  })\n\n  const italicExample = TextNodeRenderable.fromString(\"italic\", {\n    fg: \"#f7768e\",\n    attributes: 2,\n  })\n\n  const demoCont2 = TextNodeRenderable.fromString(\", and \", {\n    fg: \"#c0caf5\",\n  })\n\n  const underlineExample = TextNodeRenderable.fromString(\"underline\", {\n    fg: \"#f7768e\",\n    attributes: 4,\n  })\n\n  const demoCont3 = TextNodeRenderable.fromString(\n    \". When the container is resized, the text automatically reflows to fit the new dimensions while maintaining the specified wrapping mode.\",\n    {\n      fg: \"#c0caf5\",\n    },\n  )\n\n  const codeTitle = TextNodeRenderable.fromString(\"\\n\\n💻 Example Code: 🖥️\", {\n    fg: \"#bb9af7\",\n    attributes: 1,\n  })\n\n  const codeBlock = TextNodeRenderable.fromString(\n    `\\n\\nconst styledText = TextNodeRenderable.fromNodes([\n  TextNodeRenderable.fromString(\"Hello \", { fg: \"#9ece6a\" }),\n  TextNodeRenderable.fromString(\"World\", { fg: \"#7aa2f7\", attributes: 1 }),\n  TextNodeRenderable.fromString(\"!\", { fg: \"#f7768e\" })\n]);\n\ntextRenderable.add(styledText);`,\n    {\n      fg: \"#c0caf5\",\n      bg: \"#1a1a2e\",\n    },\n  )\n\n  const interactionTitle = TextNodeRenderable.fromString(\"\\n\\n🎮 Try It Out:\", {\n    fg: \"#bb9af7\",\n    attributes: 1,\n  })\n\n  const interactionText = TextNodeRenderable.fromString(\n    \"\\n\\nDrag the borders or corners of this text box to resize it and watch how the text wrapping adapts in real-time. Press \",\n    {\n      fg: \"#c0caf5\",\n    },\n  )\n\n  const keyW = TextNodeRenderable.fromString(\"W\", {\n    fg: \"#9ece6a\",\n    attributes: 1,\n  })\n\n  const interactionCont = TextNodeRenderable.fromString(\" to toggle wrapping on/off, \", {\n    fg: \"#c0caf5\",\n  })\n\n  const keyM = TextNodeRenderable.fromString(\"M\", {\n    fg: \"#bb9af7\",\n    attributes: 1,\n  })\n\n  const interactionCont2 = TextNodeRenderable.fromString(\" to switch between word and character wrapping modes, and \", {\n    fg: \"#c0caf5\",\n  })\n\n  const keyD = TextNodeRenderable.fromString(\"D\", {\n    fg: \"#f7768e\",\n    attributes: 1,\n  })\n\n  const interactionCont3 = TextNodeRenderable.fromString(\n    \" to download and display the Babylon.js library source code. The text will reflow instantly to demonstrate the different wrapping behaviors.\",\n    {\n      fg: \"#c0caf5\",\n    },\n  )\n\n  const conclusionNode = TextNodeRenderable.fromString(\n    \"\\n\\n🚀 This demonstrates the power of OpenTUI's flexible text rendering system, combining rich styling with dynamic layout capabilities! ✨🎨📝\",\n    {\n      fg: \"#9ece6a\",\n      attributes: 1,\n    },\n  )\n\n  return TextNodeRenderable.fromNodes([\n    titleNode,\n    introNode,\n    highlightNode,\n    introContNode,\n    featuresTitle,\n    feature1Node,\n    feature2Node,\n    feature3Node,\n    feature4Node,\n    demoTitle,\n    demoText,\n    boldExample,\n    demoCont,\n    italicExample,\n    demoCont2,\n    underlineExample,\n    demoCont3,\n    codeTitle,\n    codeBlock,\n    interactionTitle,\n    interactionText,\n    keyW,\n    interactionCont,\n    keyM,\n    interactionCont2,\n    keyD,\n    interactionCont3,\n    conclusionNode,\n  ])\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.setBackgroundColor(\"#0a0a14\")\n\n  // Add global mouse handler for resize operations\n  renderer.root.onMouse = handleGlobalMouse\n\n  // Create main container (no border, just layout)\n  mainContainer = new BoxRenderable(renderer, {\n    id: \"mainContainer\",\n    flexGrow: 1,\n    maxHeight: \"100%\",\n    maxWidth: \"100%\",\n    backgroundColor: \"#0f0f23\",\n    flexDirection: \"column\",\n  })\n  renderer.root.add(mainContainer)\n\n  // Create content box for main demonstration area\n  contentBox = new BoxRenderable(renderer, {\n    id: \"content-box\",\n    flexGrow: 1,\n    backgroundColor: \"#1e1e2e\",\n    border: true,\n    borderColor: \"#565f89\",\n    padding: 1,\n  })\n\n  textBox = new ScrollBoxRenderable(renderer, {\n    id: \"text-box\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    width: 80,\n    height: 15,\n    borderStyle: \"rounded\",\n    borderColor: \"#9ece6a\",\n    backgroundColor: \"#11111b\",\n    onMouse: handleTextBoxMouse,\n  })\n  contentBox.add(textBox)\n\n  textRenderable = new TextRenderable(renderer, {\n    id: \"text-renderable\",\n    fg: \"#c0caf5\",\n    wrapMode: \"word\", // Enable text wrapping with word mode\n  })\n  textRenderable.add(createDemoText())\n  textBox.add(textRenderable)\n\n  // Create instructions box with border\n  instructionsBox = new BoxRenderable(renderer, {\n    id: \"instructions-box\",\n    width: \"100%\",\n    flexDirection: \"column\",\n    backgroundColor: \"#1e1e2e\",\n    border: true,\n    borderColor: \"#565f89\",\n    padding: 1,\n  })\n\n  // Instructions with styled text\n  instructionsText1 = new TextRenderable(renderer, {\n    id: \"instructions-1\",\n    content: t`${bold(fg(\"#7aa2f7\")(\"Text Wrap Demo\"))} ${fg(\"#565f89\")(\"-\")} ${bold(fg(\"#9ece6a\")(\"W\"))} ${fg(\"#c0caf5\")(\"Cycle wrap mode\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#bb9af7\")(\"M\"))} ${fg(\"#c0caf5\")(\"Toggle char/word\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#f7768e\")(\"D\"))} ${fg(\"#c0caf5\")(\"Download Babylon.js\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#e0af68\")(\"L\"))} ${fg(\"#c0caf5\")(\"Load file\")} ${fg(\"#565f89\")(\"|\")} ${bold(fg(\"#ff9e64\")(\"Drag\"))} ${fg(\"#c0caf5\")(\"borders/corners to resize\")}`,\n  })\n\n  instructionsText2 = new TextRenderable(renderer, {\n    id: \"instructions-2\",\n    content: t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#c0caf5\")(\"Wrap mode:\")} ${fg(\"#bb9af7\")(\"word\")}`,\n  })\n\n  instructionsBox.add(instructionsText1)\n  instructionsBox.add(instructionsText2)\n\n  // Create file path input container (hidden by default, centered with border)\n  fileInputContainer = new BoxRenderable(renderer, {\n    id: \"file-input-container\",\n    position: \"absolute\",\n    left: \"50%\",\n    top: \"50%\",\n    width: 60,\n    height: 3,\n    marginLeft: -30,\n    marginTop: -2,\n    zIndex: 200,\n    border: true,\n    borderStyle: \"rounded\",\n    borderColor: \"#7aa2f7\",\n    backgroundColor: \"#1e1e2e\",\n    visible: false,\n  })\n  mainContainer.add(fileInputContainer)\n\n  // Create file path input\n  filePathInput = new InputRenderable(renderer, {\n    id: \"file-path-input\",\n    width: \"100%\",\n    height: \"100%\",\n    backgroundColor: \"#1e1e2e\",\n    textColor: \"#c0caf5\",\n    placeholder: \"Enter file path (relative to cwd or absolute)...\",\n    placeholderColor: \"#565f89\",\n    cursorColor: \"#7aa2f7\",\n    value: \"\",\n    maxLength: 500,\n    onKeyDown: (key) => {\n      // If backspace is pressed and input is empty, close the prompt\n      if (key.name === \"backspace\" && filePathInput && filePathInput.value === \"\" && isInputVisible) {\n        hideFileInput()\n      }\n    },\n  })\n  fileInputContainer.add(filePathInput)\n\n  // Handle file path input submission\n  filePathInput.on(InputRenderableEvents.ENTER, async (value: string) => {\n    if (!value.trim()) {\n      hideFileInput()\n      return\n    }\n\n    // Close prompt immediately before loading\n    hideFileInput()\n\n    try {\n      const filePath = value.trim()\n\n      // Update status to show loading\n      if (instructionsText2) {\n        instructionsText2.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#f7768e\")(\"Loading file...\")}`\n      }\n\n      // Get file size for display\n      const fileStats = await stat(filePath)\n      const fileSizeBytes = fileStats.size\n      const fileSizeMB = (fileSizeBytes / (1024 * 1024)).toFixed(2)\n\n      // Replace the current content and load file directly into buffer\n      if (textRenderable) {\n        textRenderable.clear()\n\n        // Add header text node\n        const headerNode = TextNodeRenderable.fromString(`// Loaded from: ${filePath}\\n// Size: ${fileSizeMB} MB\\n\\n`, {\n          fg: \"#9ece6a\",\n        })\n        textRenderable.add(headerNode)\n\n        // Trigger lifecycle to commit header\n        textRenderable.onLifecyclePass()\n\n        // Load file directly into the text buffer\n        const textBuffer = (textRenderable as any).textBuffer\n        textBuffer.loadFile(filePath)\n\n        // Get the text buffer size after loading (in bytes)\n        const textBufferBytes = textBuffer.byteSize\n        const textBufferMB = (textBufferBytes / (1024 * 1024)).toFixed(2)\n\n        // Update status\n        if (instructionsText2) {\n          instructionsText2.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#c0caf5\")(\"File: \")} ${fg(\"#9ece6a\")(fileSizeMB)}${fg(\"#c0caf5\")(\" MB, Buffer: \")} ${fg(\"#9ece6a\")(textBufferMB)}${fg(\"#c0caf5\")(\" MB, Mode: \")} ${fg(\"#bb9af7\")(textRenderable.wrapMode)}${fg(\"#c0caf5\")(\")\")}`\n        }\n      }\n    } catch (error) {\n      // Show error in text renderable\n      const errorMessage = error instanceof Error ? error.message : \"Unknown error\"\n      const errorTextNode = TextNodeRenderable.fromString(`ERROR: ${errorMessage}\\n\\nPress L to try again.`, {\n        fg: \"#f7768e\",\n      })\n\n      if (textRenderable) {\n        textRenderable.clear()\n        textRenderable.add(errorTextNode)\n      }\n\n      if (instructionsText2) {\n        instructionsText2.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#f7768e\")(\"Error loading file\")}`\n      }\n    }\n  })\n\n  // Add content and instructions to main container\n  mainContainer.add(contentBox)\n  mainContainer.add(instructionsBox)\n\n  // Handle keyboard input\n  renderer.keyInput.on(\"keypress\", async (event) => {\n    const key = event.sequence\n\n    // If input is visible, don't process other keys (let input handle them)\n    if (isInputVisible) {\n      return\n    }\n\n    if (key === \"l\" || key === \"L\") {\n      // Show file input prompt\n      showFileInput()\n    } else if (key === \"w\" || key === \"W\") {\n      // Cycle through wrap modes: word -> char -> none -> word\n      if (textRenderable && instructionsText2) {\n        if (textRenderable.wrapMode === \"word\") {\n          textRenderable.wrapMode = \"char\"\n        } else if (textRenderable.wrapMode === \"char\") {\n          textRenderable.wrapMode = \"none\"\n        } else {\n          textRenderable.wrapMode = \"word\"\n        }\n        instructionsText2.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#c0caf5\")(\"Wrap mode:\")} ${fg(\"#bb9af7\")(textRenderable.wrapMode)}`\n      }\n    } else if (key === \"m\" || key === \"M\") {\n      // Cycle through word/char modes (skip none)\n      if (textRenderable && instructionsText2) {\n        if (textRenderable.wrapMode === \"none\") {\n          textRenderable.wrapMode = \"word\"\n        } else {\n          textRenderable.wrapMode = textRenderable.wrapMode === \"char\" ? \"word\" : \"char\"\n        }\n        instructionsText2.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#c0caf5\")(\"Wrap mode:\")} ${fg(\"#bb9af7\")(textRenderable.wrapMode)}`\n      }\n    } else if (key === \"d\" || key === \"D\") {\n      // Download Babylon.js and display it\n      if (textRenderable && instructionsText2) {\n        try {\n          // Update status to show downloading\n          instructionsText2.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#f7768e\")(\"Downloading Babylon.js...\")}`\n\n          // Download the file\n          const response = await fetch(\"https://cdnjs.cloudflare.com/ajax/libs/babylonjs/8.20.0/babylon.js\")\n          if (!response.ok) {\n            throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n          }\n          const content = await response.text()\n\n          // Get file size in bytes from the downloaded content\n          const fileSizeBytes = new Blob([content]).size\n          const fileSizeMB = (fileSizeBytes / (1024 * 1024)).toFixed(2)\n\n          // Store in OS tmp directory\n          const tempDir = process.env.TMPDIR || process.env.TEMP || \"/tmp\"\n          const fileName = `babylon-${Date.now()}.js`\n          const filePath = `${tempDir}/${fileName}`\n\n          await Bun.write(filePath, content)\n\n          // Load it back from disk\n          const loadedContent = await readFile(filePath, \"utf8\")\n\n          // Create a new TextNodeRenderable with the downloaded content\n          const babylonTextNode = TextNodeRenderable.fromString(\n            `// Downloaded Babylon.js (${loadedContent.length.toLocaleString()} chars, ${fileSizeMB} MB)\\n// Stored at: ${filePath}\\n\\n${loadedContent}`,\n            {\n              fg: \"#c0caf5\",\n            },\n          )\n\n          // Replace the current content\n          textRenderable.clear()\n          textRenderable.add(babylonTextNode)\n\n          // Trigger the lifecycle pass to commit text to buffer\n          textRenderable.onLifecyclePass()\n\n          // Get the text buffer size after loading (in bytes)\n          const textBufferBytes = (textRenderable as any).textBuffer.byteSize\n          const textBufferMB = (textBufferBytes / (1024 * 1024)).toFixed(2)\n\n          // Update status\n          instructionsText2.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#c0caf5\")(\"Downloaded: \")} ${fg(\"#9ece6a\")(fileSizeMB)}${fg(\"#c0caf5\")(\" MB, Buffer: \")} ${fg(\"#9ece6a\")(textBufferMB)}${fg(\"#c0caf5\")(\" MB, Mode: \")} ${fg(\"#bb9af7\")(textRenderable.wrapMode)}${fg(\"#c0caf5\")(\")\")}`\n        } catch (error) {\n          // Show error in status\n          instructionsText2.content = t`${bold(fg(\"#7aa2f7\")(\"Status:\"))} ${fg(\"#f7768e\")(\"Download failed:\")} ${fg(\"#c0caf5\")(error instanceof Error ? error.message : \"Unknown error\")}`\n        }\n      }\n    }\n  })\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  mainContainer?.destroyRecursively()\n  mainContainer = null\n  contentBox = null\n  textBox = null\n  textRenderable = null\n  instructionsBox = null\n  instructionsText1 = null\n  instructionsText2 = null\n  filePathInput = null\n  fileInputContainer = null\n  isInputVisible = false\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    targetFps: 30,\n    enableMouseMovement: true,\n    exitOnCtrlC: true,\n  })\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  // renderer.start() is called by setupCommonDemoKeys\n}\n"
  },
  {
    "path": "packages/core/src/examples/texture-loading-demo.ts",
    "content": "#!/usr/bin/env bun\n\nimport { CliRenderer, createCliRenderer, RGBA, BoxRenderable, TextRenderable, type KeyEvent } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { TextureUtils } from \"../3d/TextureUtils.js\"\nimport {\n  Scene as ThreeScene,\n  Mesh as ThreeMesh,\n  PerspectiveCamera,\n  Color,\n  PointLight as ThreePointLight,\n  BoxGeometry,\n  MeshBasicMaterial,\n  Vector3,\n} from \"three\"\nimport { MeshPhongNodeMaterial } from \"three/webgpu\"\nimport { lights } from \"three/tsl\"\nimport { ThreeRenderable, SuperSampleAlgorithm } from \"../3d.js\"\n\n// @ts-ignore\nimport cratePath from \"./assets/crate.png\" with { type: \"image/png\" }\n// @ts-ignore\nimport crateEmissivePath from \"./assets/crate_emissive.png\" with { type: \"image/png\" }\n\nlet threeRenderable: ThreeRenderable | null = null\nlet keyListener: ((key: KeyEvent) => void) | null = null\nlet resizeListener: ((width: number, height: number) => void) | null = null\nlet parentContainer: BoxRenderable | null = null\n\nexport async function run(renderer: CliRenderer): Promise<void> {\n  renderer.start()\n  const WIDTH = renderer.terminalWidth\n  const HEIGHT = renderer.terminalHeight\n\n  parentContainer = new BoxRenderable(renderer, {\n    id: \"texture-loading-container\",\n    zIndex: 15,\n  })\n  renderer.root.add(parentContainer)\n\n  const sceneRoot = new ThreeScene()\n\n  const mainLightNode = new ThreePointLight(new Color(1.0, 1.0, 1.0), 1.0, 60)\n  mainLightNode.power = 500\n  mainLightNode.position.set(2, 1, 2)\n  mainLightNode.name = \"main_light\"\n  sceneRoot.add(mainLightNode)\n\n  const lightNode = new ThreePointLight(new Color(1.0, 1.0, 1.0), 1.0, 60)\n  lightNode.power = 500\n  lightNode.position.set(-2, 1, 2)\n  lightNode.name = \"light\"\n  sceneRoot.add(lightNode)\n\n  const allLightsNode = lights([mainLightNode, lightNode])\n\n  const cubeGeometry = new BoxGeometry(1.0, 1.0, 1.0)\n  const cubeMeshNode = new ThreeMesh(cubeGeometry)\n  cubeMeshNode.name = \"cube\"\n\n  cubeMeshNode.position.set(0, 0, 0)\n  cubeMeshNode.rotation.set(0, 0, 0)\n  cubeMeshNode.scale.set(1.0, 1.0, 1.0)\n\n  sceneRoot.add(cubeMeshNode)\n\n  const cameraNode = new PerspectiveCamera(45, 1, 1.0, 100.0)\n  cameraNode.position.set(0, 0, 2)\n  cameraNode.name = \"main_camera\"\n\n  const rotationSpeed = new Vector3(0.4, 0.8, 0.2)\n  let rotationEnabled = true\n\n  renderer.setFrameCallback(async (deltaMs) => {\n    const deltaTime = deltaMs / 1000\n\n    if (rotationEnabled && cubeMeshNode) {\n      cubeMeshNode.rotation.x += rotationSpeed.x * deltaTime\n      cubeMeshNode.rotation.y += rotationSpeed.y * deltaTime\n      cubeMeshNode.rotation.z += rotationSpeed.z * deltaTime\n    }\n  })\n\n  threeRenderable = new ThreeRenderable(renderer, {\n    id: \"main\",\n    width: WIDTH,\n    height: HEIGHT,\n    zIndex: 10,\n    scene: sceneRoot,\n    camera: cameraNode,\n    renderer: {\n      focalLength: 8,\n      backgroundColor: RGBA.fromValues(0.0, 0.0, 0.0, 1.0),\n    },\n  })\n  renderer.root.add(threeRenderable)\n\n  const titleText = new TextRenderable(renderer, {\n    id: \"demo-title\",\n    content: \"Texture Loading Demo\",\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(titleText)\n\n  const statusText = new TextRenderable(renderer, {\n    id: \"status\",\n    content: \"Loading texture...\",\n    position: \"absolute\",\n    left: 0,\n    top: 1,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(statusText)\n\n  const controlsText = new TextRenderable(renderer, {\n    id: \"controls\",\n    content: \"WASD: Move | QE: Rotate | ZX: Zoom | R: Reset | Space: Toggle rotation | Escape: Return\",\n    position: \"absolute\",\n    left: 0,\n    top: HEIGHT - 2,\n    fg: \"#FFFFFF\",\n    zIndex: 20,\n  })\n  parentContainer.add(controlsText)\n\n  resizeListener = (width: number, height: number) => {\n    if (threeRenderable) {\n      threeRenderable.width = width\n      threeRenderable.height = height\n    }\n\n    controlsText.y = height - 2\n  }\n\n  renderer.on(\"resize\", resizeListener)\n\n  keyListener = (key: KeyEvent) => {\n    const engine = threeRenderable?.renderer\n\n    if (key.name === \"p\" && engine) {\n      engine.saveToFile(`screenshot-${Date.now()}.png`)\n    }\n\n    // Handle camera movement\n    if (key.name === \"w\") {\n      cameraNode.translateY(0.5)\n    } else if (key.name === \"s\") {\n      cameraNode.translateY(-0.5)\n    } else if (key.name === \"a\") {\n      cameraNode.translateX(-0.5)\n    } else if (key.name === \"d\") {\n      cameraNode.translateX(0.5)\n    }\n\n    // Handle camera rotation\n    if (key.name === \"q\") {\n      cameraNode.rotateY(0.1)\n    } else if (key.name === \"e\") {\n      cameraNode.rotateY(-0.1)\n    }\n\n    // Handle zoom by changing camera position\n    if (key.name === \"z\") {\n      cameraNode.translateZ(0.1)\n    } else if (key.name === \"x\") {\n      cameraNode.translateZ(-0.1)\n    }\n\n    // Reset camera position and rotation\n    if (key.name === \"r\") {\n      cameraNode.position.set(0, 0, 2)\n      cameraNode.rotation.set(0, 0, 0)\n      cameraNode.quaternion.set(0, 0, 0, 1)\n      cameraNode.up.set(0, 1, 0)\n      cameraNode.lookAt(0, 0, 0)\n    }\n\n    // Toggle super sampling\n    if (key.name === \"u\" && engine) {\n      engine.toggleSuperSampling()\n    }\n\n    if (key.name === \"i\" && engine) {\n      const currentAlgorithm = engine.getSuperSampleAlgorithm()\n      const newAlgorithm =\n        currentAlgorithm === SuperSampleAlgorithm.STANDARD\n          ? SuperSampleAlgorithm.PRE_SQUEEZED\n          : SuperSampleAlgorithm.STANDARD\n      engine.setSuperSampleAlgorithm(newAlgorithm)\n    }\n\n    // Toggle cube rotation\n    if (key.name === \"space\") {\n      rotationEnabled = !rotationEnabled\n    }\n  }\n\n  renderer.keyInput.on(\"keypress\", keyListener)\n\n  const imagePath = cratePath\n  const textureMap = await TextureUtils.fromFile(imagePath)\n  const textureEmissive = await TextureUtils.fromFile(crateEmissivePath)\n\n  let material\n  if (textureMap) {\n    material = new MeshPhongNodeMaterial({\n      map: textureMap,\n      emissiveMap: textureEmissive ? textureEmissive : undefined,\n      emissive: new Color(0.0, 0.0, 0.0),\n      emissiveIntensity: 0.2,\n    })\n    material.lightsNode = allLightsNode\n    statusText.content = \"Using PhongNodeMaterial with texture.\"\n  } else {\n    material = new MeshBasicMaterial({ color: 0x00ff00 })\n    statusText.content = \"Texture failed. Using green BasicMaterial.\"\n  }\n\n  cubeMeshNode.material = material\n\n  statusText.content = \"Using PhongNodeMaterial setup\"\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  renderer.clearFrameCallbacks()\n\n  if (resizeListener) {\n    renderer.off(\"resize\", resizeListener)\n    resizeListener = null\n  }\n\n  if (keyListener) {\n    renderer.keyInput.off(\"keypress\", keyListener)\n    keyListener = null\n  }\n\n  if (threeRenderable) {\n    threeRenderable.destroy()\n    threeRenderable = null\n  }\n\n  if (parentContainer) {\n    renderer.root.remove(\"texture-loading-container\")\n    parentContainer = null\n  }\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n    memorySnapshotInterval: 2000,\n  })\n\n  await run(renderer)\n  setupCommonDemoKeys(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/timeline-example.ts",
    "content": "import { createTimeline, type JSAnimation, Timeline } from \"../animation/Timeline.js\"\nimport { CliRenderer, createCliRenderer, TextRenderable, BoxRenderable, type KeyEvent } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nclass TimelineExample {\n  private _mainTimeline: Timeline\n  private _subTimeline1: Timeline\n  private _subTimeline2: Timeline\n  private renderer: CliRenderer\n  private boxObject: BoxRenderable\n  private alternatingObject: BoxRenderable\n  private parentContainer: BoxRenderable\n\n  private statusLine1: TextRenderable\n  private statusLine2: TextRenderable\n  private statusLine3: TextRenderable\n  private statusLine4: TextRenderable\n  private statusLine5: TextRenderable\n  private statusLine6: TextRenderable\n  private statusLine7: TextRenderable\n  private statusLine8: TextRenderable\n  private statusLine9: TextRenderable\n\n  constructor(renderer: CliRenderer) {\n    this.renderer = renderer\n\n    this._mainTimeline = createTimeline({\n      duration: 10000,\n      loop: true,\n    })\n\n    this._subTimeline1 = createTimeline({\n      duration: 8000,\n      autoplay: false,\n    })\n\n    this._subTimeline2 = createTimeline({\n      duration: 6000,\n      autoplay: false,\n    })\n\n    this.setupAnimations()\n\n    this._mainTimeline.sync(this._subTimeline1, 0)\n    this._mainTimeline.sync(this._subTimeline2, 3000)\n\n    this.parentContainer = new BoxRenderable(renderer, {\n      id: \"timeline-container\",\n      zIndex: 10,\n    })\n    this.renderer.root.add(this.parentContainer)\n\n    this.boxObject = new BoxRenderable(renderer, {\n      id: \"box-object\",\n      position: \"absolute\",\n      left: 10,\n      top: 8,\n      width: 8,\n      height: 4,\n      backgroundColor: \"#FF6B6B\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#FFFFFF\",\n      title: \"Box\",\n      titleAlignment: \"center\",\n    })\n    this.parentContainer.add(this.boxObject)\n\n    const colorObject = new BoxRenderable(renderer, {\n      id: \"color-object\",\n      position: \"absolute\",\n      left: 25,\n      top: 8,\n      width: 12,\n      height: 4,\n      backgroundColor: \"#FF0000\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#FFFFFF\",\n      title: \"Color\",\n      titleAlignment: \"center\",\n    })\n    this.parentContainer.add(colorObject)\n\n    const physicsObject = new BoxRenderable(renderer, {\n      id: \"physics-object\",\n      position: \"absolute\",\n      left: 45,\n      top: 8,\n      width: 12,\n      height: 4,\n      backgroundColor: \"#4ECDC4\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#FFFFFF\",\n      title: \"Physics\",\n      titleAlignment: \"center\",\n    })\n    this.parentContainer.add(physicsObject)\n\n    this.alternatingObject = new BoxRenderable(renderer, {\n      id: \"alternating-object\",\n      position: \"absolute\",\n      left: 1,\n      top: 1,\n      width: 8,\n      height: 4,\n      backgroundColor: \"#9B59B6\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#FFFFFF\",\n      title: \"Alternate\",\n      titleAlignment: \"center\",\n    })\n    this.parentContainer.add(this.alternatingObject)\n\n    const mainTimelineBox = new BoxRenderable(renderer, {\n      id: \"main-timeline\",\n      position: \"absolute\",\n      left: 2,\n      top: 15,\n      width: 60,\n      height: 3,\n      backgroundColor: \"#333366\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#FFFFFF\",\n      title: \"Main Timeline (20s)\",\n      titleAlignment: \"left\",\n    })\n    this.parentContainer.add(mainTimelineBox)\n\n    const subTimeline1Box = new BoxRenderable(renderer, {\n      id: \"sub-timeline-1\",\n      position: \"absolute\",\n      left: 2,\n      top: 19,\n      width: 30,\n      height: 3,\n      backgroundColor: \"#333366\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#FFFFFF\",\n      title: \"Sub Timeline 1 (8s)\",\n      titleAlignment: \"left\",\n    })\n    this.parentContainer.add(subTimeline1Box)\n\n    const subTimeline2Box = new BoxRenderable(renderer, {\n      id: \"sub-timeline-2\",\n      position: \"absolute\",\n      left: 35,\n      top: 19,\n      width: 27,\n      height: 3,\n      backgroundColor: \"#333366\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#FFFFFF\",\n      title: \"Sub Timeline 2 (6s)\",\n      titleAlignment: \"left\",\n    })\n    this.parentContainer.add(subTimeline2Box)\n\n    const statusBox = new BoxRenderable(renderer, {\n      id: \"status\",\n      position: \"absolute\",\n      left: 2,\n      top: 24,\n      width: 60,\n      height: 14,\n      backgroundColor: \"#1a1a2e\",\n      zIndex: 1,\n      borderStyle: \"single\",\n      borderColor: \"#FFFFFF\",\n      title: \"Animation Values\",\n      titleAlignment: \"center\",\n    })\n    this.parentContainer.add(statusBox)\n\n    this.statusLine1 = new TextRenderable(renderer, {\n      id: \"status-line1\",\n      content: \"Timeline: Initializing...\",\n      position: \"absolute\",\n      left: 4,\n      top: 25,\n      fg: \"#FFFFFF\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine1)\n\n    this.statusLine2 = new TextRenderable(renderer, {\n      id: \"status-line2\",\n      content: \"Box Position: x=0.0, y=0.0\",\n      position: \"absolute\",\n      left: 4,\n      top: 26,\n      fg: \"#FFFF00\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine2)\n\n    this.statusLine3 = new TextRenderable(renderer, {\n      id: \"status-line3\",\n      content: \"Box Scale/Rot: scale=1.0, rot=0.0\",\n      position: \"absolute\",\n      left: 4,\n      top: 27,\n      fg: \"#FFE66D\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine3)\n\n    this.statusLine4 = new TextRenderable(renderer, {\n      id: \"status-line4\",\n      content: \"Color: rgb(255, 0, 0)\",\n      position: \"absolute\",\n      left: 4,\n      top: 28,\n      fg: \"#FF6B6B\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine4)\n\n    this.statusLine5 = new TextRenderable(renderer, {\n      id: \"status-line5\",\n      content: \"Color Opacity: 1.0\",\n      position: \"absolute\",\n      left: 4,\n      top: 29,\n      fg: \"#FF9999\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine5)\n\n    this.statusLine6 = new TextRenderable(renderer, {\n      id: \"status-line6\",\n      content: \"Physics: v=0.0, a=0.0, m=1.0\",\n      position: \"absolute\",\n      left: 4,\n      top: 30,\n      fg: \"#4ECDC4\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine6)\n\n    this.statusLine7 = new TextRenderable(renderer, {\n      id: \"status-line7\",\n      content: \"Progress: Main=0% Sub1=0% Sub2=0%\",\n      position: \"absolute\",\n      left: 4,\n      top: 31,\n      fg: \"#CCCCCC\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine7)\n\n    this.statusLine8 = new TextRenderable(renderer, {\n      id: \"status-line8\",\n      content: \"Example Value: 0.000 (0.0 → 0.5)\",\n      position: \"absolute\",\n      left: 4,\n      top: 32,\n      fg: \"#FFE66D\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine8)\n\n    this.statusLine9 = new TextRenderable(renderer, {\n      id: \"status-line9\",\n      content: \"Alternating: x=65 (left/right loop=5)\",\n      position: \"absolute\",\n      left: 4,\n      top: 33,\n      fg: \"#9B59B6\",\n      zIndex: 2,\n    })\n    this.parentContainer.add(this.statusLine9)\n  }\n\n  public update(deltaTime: number): void {\n    this._mainTimeline.update(deltaTime)\n\n    this.updateVisuals()\n  }\n\n  private updateVisuals(): void {\n    // Update timeline progress bars\n    const mainProgress = (this._mainTimeline.currentTime / this._mainTimeline.duration) * 58\n    const sub1Progress = (this._subTimeline1.currentTime / this._subTimeline1.duration) * 28\n    const sub2Progress = (this._subTimeline2.currentTime / this._subTimeline2.duration) * 25\n\n    // Create progress indicators\n    const mainProgressBox = this.parentContainer.getRenderable(\"main-progress\") as BoxRenderable\n    if (mainProgressBox) {\n      mainProgressBox.width = Math.max(1, Math.floor(mainProgress))\n    } else {\n      const newMainProgressBox = new BoxRenderable(this.renderer, {\n        id: \"main-progress\",\n        position: \"absolute\",\n        left: 3,\n        top: 16,\n        width: Math.max(1, Math.floor(mainProgress)),\n        height: 1,\n        backgroundColor: \"#FFE66D\",\n        zIndex: 2,\n      })\n      this.parentContainer.add(newMainProgressBox)\n    }\n\n    const sub1ProgressBox = this.parentContainer.getRenderable(\"sub1-progress\") as BoxRenderable\n    if (sub1ProgressBox) {\n      sub1ProgressBox.width = Math.max(1, Math.floor(sub1Progress))\n    } else {\n      const newSub1ProgressBox = new BoxRenderable(this.renderer, {\n        id: \"sub1-progress\",\n        position: \"absolute\",\n        left: 3,\n        top: 20,\n        width: Math.max(1, Math.floor(sub1Progress)),\n        height: 1,\n        backgroundColor: \"#FF6B6B\",\n        zIndex: 2,\n      })\n      this.parentContainer.add(newSub1ProgressBox)\n    }\n\n    const sub2ProgressBox = this.parentContainer.getRenderable(\"sub2-progress\") as BoxRenderable\n    if (sub2ProgressBox) {\n      sub2ProgressBox.width = Math.max(1, Math.floor(sub2Progress))\n    } else {\n      const newSub2ProgressBox = new BoxRenderable(this.renderer, {\n        id: \"sub2-progress\",\n        position: \"absolute\",\n        left: 36,\n        top: 20,\n        width: Math.max(1, Math.floor(sub2Progress)),\n        height: 1,\n        backgroundColor: \"#4ECDC4\",\n        zIndex: 2,\n      })\n      this.parentContainer.add(newSub2ProgressBox)\n    }\n\n    const mainPercent = Math.floor((this._mainTimeline.currentTime / this._mainTimeline.duration) * 100)\n    const sub1Percent = Math.floor((this._subTimeline1.currentTime / this._subTimeline1.duration) * 100)\n    const sub2Percent = Math.floor((this._subTimeline2.currentTime / this._subTimeline2.duration) * 100)\n\n    this.statusLine7.content = `Progress: Main=${mainPercent}% Sub1=${sub1Percent}% Sub2=${sub2Percent}%`\n  }\n\n  private setupAnimations(): void {\n    const boxObject = {\n      x: 0,\n      y: 0,\n      scale: 1.0,\n      rotation: 0,\n    }\n\n    const colorObject = {\n      red: 255,\n      green: 0,\n      blue: 0,\n      opacity: 1.0,\n    }\n\n    const physicsObject = {\n      velocity: 0,\n      acceleration: 0,\n      mass: 1.0,\n    }\n\n    const exampleValue = {\n      value: 0.0,\n    }\n\n    const alternatingObject = {\n      x: 1,\n    }\n\n    // Sub-timeline 1: Box animations\n    this._subTimeline1.add(\n      boxObject,\n      {\n        x: 100,\n        y: 50,\n        duration: 2000,\n        ease: \"inOutQuad\",\n        onUpdate: (values: JSAnimation) => {\n          const x = values.targets[0].x\n          const y = values.targets[0].y\n\n          this.boxObject.x = Math.max(1, Math.min(70, 10 + Math.round(x / 3)))\n          this.boxObject.y = Math.max(1, Math.min(30, 8 + Math.round(y / 5)))\n\n          this.statusLine2.content = `Box Position: x=${x.toFixed(1)}, y=${y.toFixed(1)}`\n        },\n      },\n      0,\n    )\n\n    this._subTimeline1.add(\n      boxObject,\n      {\n        scale: 2.0,\n        rotation: Math.PI,\n        duration: 1500,\n        ease: \"inOutQuad\",\n        onUpdate: (values: JSAnimation) => {\n          const scale = values.targets[0].scale\n          const rotation = values.targets[0].rotation\n          const size = Math.max(4, Math.round(4 * scale))\n          this.boxObject.width = size\n          this.boxObject.height = Math.max(2, Math.round(size / 2))\n\n          this.statusLine3.content = `Box Scale/Rot: scale=${scale.toFixed(2)}, rot=${rotation.toFixed(2)}`\n        },\n      },\n      1000,\n    )\n\n    this._subTimeline1.add(\n      boxObject,\n      {\n        x: -50,\n        y: -25,\n        scale: 0.5,\n        rotation: 0,\n        duration: 3000,\n        ease: \"inOutSine\",\n        onUpdate: (values: JSAnimation) => {\n          const x = values.targets[0].x\n          const y = values.targets[0].y\n          const scale = values.targets[0].scale\n          const rotation = values.targets[0].rotation\n\n          this.boxObject.x = Math.max(1, Math.min(70, 10 + Math.round(x / 3)))\n          this.boxObject.y = Math.max(1, Math.min(30, 8 + Math.round(y / 5)))\n\n          const size = Math.max(2, Math.round(4 * scale))\n          this.boxObject.width = size\n          this.boxObject.height = Math.max(1, Math.round(size / 2))\n\n          this.statusLine2.content = `Box Position (Reset): x=${x.toFixed(1)}, y=${y.toFixed(1)}`\n          this.statusLine3.content = `Box Scale/Rot (Reset): scale=${scale.toFixed(2)}, rot=${rotation.toFixed(2)}`\n        },\n      },\n      4000,\n    )\n\n    this._subTimeline2.add(\n      colorObject,\n      {\n        red: 0,\n        green: 255,\n        blue: 128,\n        duration: 2000,\n        ease: \"linear\",\n        onUpdate: (values: JSAnimation) => {\n          const r = Math.round(values.targets[0].red)\n          const g = Math.round(values.targets[0].green)\n          const b = Math.round(values.targets[0].blue)\n\n          const hexColor = `#${r.toString(16).padStart(2, \"0\")}${g.toString(16).padStart(2, \"0\")}${b.toString(16).padStart(2, \"0\")}`\n          const colorObject = this.parentContainer.getRenderable(\"color-object\") as BoxRenderable\n          if (colorObject) {\n            colorObject.backgroundColor = hexColor\n          }\n\n          this.statusLine4.content = `Color: rgb(${r}, ${g}, ${b})`\n        },\n      },\n      0,\n    )\n\n    this._subTimeline2.add(\n      colorObject,\n      {\n        opacity: 0.2,\n        duration: 1000,\n        ease: \"inExpo\",\n        onUpdate: (values: JSAnimation) => {\n          const opacity = values.targets[0].opacity\n          this.statusLine5.content = `Color Opacity: ${opacity.toFixed(2)}`\n        },\n      },\n      1500,\n    )\n\n    this._subTimeline2.add(\n      colorObject,\n      {\n        red: 255,\n        green: 255,\n        blue: 0,\n        opacity: 1.0,\n        duration: 2500,\n        ease: \"outExpo\",\n        onUpdate: (values: JSAnimation) => {\n          const r = Math.round(values.targets[0].red)\n          const g = Math.round(values.targets[0].green)\n          const b = Math.round(values.targets[0].blue)\n          const opacity = values.targets[0].opacity\n\n          const hexColor = `#${r.toString(16).padStart(2, \"0\")}${g.toString(16).padStart(2, \"0\")}${b.toString(16).padStart(2, \"0\")}`\n          const colorObject = this.parentContainer.getRenderable(\"color-object\") as BoxRenderable\n          if (colorObject) {\n            colorObject.backgroundColor = hexColor\n          }\n\n          this.statusLine4.content = `Final Color: rgb(${r}, ${g}, ${b}), opacity=${opacity.toFixed(2)}`\n        },\n      },\n      3500,\n    )\n\n    this._mainTimeline.call(() => {\n      this.statusLine1.content = \"=== STARTING ANIMATION CYCLE ===\"\n    }, 0)\n\n    this._mainTimeline.add(\n      exampleValue,\n      {\n        value: 0.5,\n        duration: 10000,\n        ease: \"inOutSine\",\n        onUpdate: (values: JSAnimation) => {\n          const val = values.targets[0].value\n          this.statusLine8.content = `Example Value: ${val.toFixed(3)} (0.0 → 0.5)`\n        },\n      },\n      0,\n    )\n\n    this._mainTimeline.add(\n      alternatingObject,\n      {\n        x: 50,\n        duration: 800,\n        ease: \"inOutQuad\",\n        loop: 5,\n        alternate: true,\n        loopDelay: 200,\n        onUpdate: (values: JSAnimation) => {\n          const x = values.targets[0].x\n          this.alternatingObject.x = Math.round(x)\n          this.alternatingObject.y = 1\n          this.statusLine9.content = `Alternating: x=${x.toFixed(1)} (left/right loop=5)`\n        },\n      },\n      1000,\n    )\n\n    this._mainTimeline.add(\n      physicsObject,\n      {\n        velocity: 50,\n        acceleration: 9.8,\n        mass: 2.5,\n        duration: 4000,\n        ease: \"inOutSine\",\n        onUpdate: (values: JSAnimation) => {\n          const velocity = values.targets[0].velocity\n          const acceleration = values.targets[0].acceleration\n          const mass = values.targets[0].mass\n          const velocityHeight = Math.max(1, Math.round(velocity / 6))\n          const physicsObject = this.parentContainer.getRenderable(\"physics-object\") as BoxRenderable\n          if (physicsObject) {\n            physicsObject.height = Math.min(6, velocityHeight)\n          }\n\n          this.statusLine6.content = `Physics: v=${velocity.toFixed(1)}, a=${acceleration.toFixed(1)}, m=${mass.toFixed(1)}`\n        },\n      },\n      1000,\n    )\n\n    this._mainTimeline.add(\n      physicsObject,\n      {\n        velocity: -20,\n        acceleration: -5,\n        mass: 0.8,\n        duration: 3000,\n        ease: \"inOutSine\",\n        onUpdate: (values: JSAnimation) => {\n          const velocity = values.targets[0].velocity\n          const acceleration = values.targets[0].acceleration\n          const mass = values.targets[0].mass\n\n          const velocityHeight = Math.max(1, Math.abs(Math.round(velocity / 4)))\n          const physicsObject = this.parentContainer.getRenderable(\"physics-object\") as BoxRenderable\n          if (physicsObject) {\n            physicsObject.height = Math.min(6, velocityHeight)\n          }\n\n          this.statusLine6.content = `Physics Reverse: v=${velocity.toFixed(1)}, a=${acceleration.toFixed(1)}, m=${mass.toFixed(1)}`\n        },\n      },\n      8000,\n    )\n\n    this._mainTimeline.call(() => {\n      this.statusLine1.content = \"=== CYCLE COMPLETE ===\"\n    }, 9000)\n  }\n\n  public start(): void {\n    this.statusLine1.content = \"Starting nested timeline example...\"\n    this._mainTimeline.play()\n  }\n\n  public pause(): void {\n    this._mainTimeline.pause()\n  }\n\n  public stop(): void {\n    this._mainTimeline.pause()\n  }\n\n  public destroy(): void {\n    this.renderer.root.remove(\"timeline-container\")\n  }\n}\n\nlet currentExample: TimelineExample | null = null\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n  renderer.setBackgroundColor(\"#000028\")\n\n  currentExample = new TimelineExample(renderer)\n  currentExample.start()\n\n  renderer.setFrameCallback(async (deltaTime: number) => {\n    if (currentExample) {\n      currentExample.update(deltaTime)\n    }\n  })\n\n  renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"p\") {\n      currentExample?.pause()\n    }\n\n    if (key.name === \"r\") {\n      currentExample?.start()\n    }\n  })\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  if (currentExample) {\n    currentExample.stop()\n    currentExample.destroy()\n    currentExample = null\n  }\n\n  renderer.clearFrameCallbacks()\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n    targetFps: 60,\n  })\n\n  setupCommonDemoKeys(renderer)\n  run(renderer)\n}\n"
  },
  {
    "path": "packages/core/src/examples/transparency-demo.ts",
    "content": "import {\n  TextAttributes,\n  createCliRenderer,\n  RGBA,\n  TextRenderable,\n  BoxRenderable,\n  OptimizedBuffer,\n  type KeyEvent,\n  type MouseEvent,\n  t,\n  bold,\n  underline,\n  fg,\n} from \"../index.js\"\nimport type { CliRenderer, RenderContext, ThemeMode } from \"../index.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\n\nlet nextZIndex = 101\nlet draggableBoxes: DraggableTransparentBox[] = []\nlet keyListener: ((key: KeyEvent) => void) | null = null\nlet themeModeListener: ((mode: ThemeMode) => void) | null = null\nlet demoRunVersion = 0\n\nconst DEFAULT_THEME_MODE: ThemeMode = \"dark\"\n\nconst THEMES = {\n  dark: {\n    backgroundColor: \"#0A0E14\",\n    headerAccent: \"#00D4AA\",\n    headerMuted: \"#A8A8B2\",\n    textUnderAlpha: \"#FFB84D\",\n    moreTextUnder: \"#7B68EE\",\n    boxLabelColor: RGBA.fromInts(255, 255, 255, 220),\n    label: \"dark\",\n  },\n  light: {\n    backgroundColor: \"#F6F1E5\",\n    headerAccent: \"#0F766E\",\n    headerMuted: \"#4B5563\",\n    textUnderAlpha: \"#B45309\",\n    moreTextUnder: \"#6D28D9\",\n    boxLabelColor: RGBA.fromInts(17, 24, 39, 220),\n    label: \"light\",\n  },\n  transparent: {\n    backgroundColor: \"transparent\",\n    headerAccent: \"#0284C7\",\n    headerMuted: \"#64748B\",\n    textUnderAlpha: \"#D97706\",\n    moreTextUnder: \"#7C3AED\",\n    boxLabelColor: RGBA.fromInts(255, 255, 255, 220),\n    label: \"transparent\",\n  },\n} as const\n\ntype ThemeName = keyof typeof THEMES\nconst THEME_ORDER: ThemeName[] = [\"dark\", \"light\", \"transparent\"]\n\nfunction getTransparentFallbackBackgroundColor(themeMode: ThemeMode): RGBA {\n  return themeMode === \"light\" ? RGBA.fromInts(255, 255, 255, 0) : RGBA.fromInts(0, 0, 0, 0)\n}\n\nfunction getThemeBackgroundColor(themeName: ThemeName, themeMode: ThemeMode): string | RGBA {\n  if (themeName === \"transparent\") {\n    return getTransparentFallbackBackgroundColor(themeMode)\n  }\n\n  return THEMES[themeName].backgroundColor\n}\n\nfunction getHeaderText(themeName: ThemeName) {\n  const theme = THEMES[themeName]\n\n  return t`${bold(underline(fg(theme.headerAccent)(\"Interactive Alpha Transparency & Blending Demo - Drag the boxes!\")))}\n${fg(theme.headerMuted)(`Drag boxes with the mouse • Press B to cycle dark/light/transparent (current: ${theme.label})`)}`\n}\n\nclass DraggableTransparentBox extends BoxRenderable {\n  private isDragging = false\n  private dragOffsetX = 0\n  private dragOffsetY = 0\n  private alphaPercentage: number\n  private labelColor: RGBA\n\n  constructor(\n    ctx: RenderContext,\n    id: string,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    bg: RGBA,\n    zIndex: number,\n  ) {\n    super(ctx, {\n      id,\n      width,\n      height,\n      zIndex,\n      backgroundColor: bg,\n      position: \"absolute\",\n      left: x,\n      top: y,\n    })\n    this.alphaPercentage = Math.round(bg.a * 100)\n    this.labelColor = THEMES.dark.boxLabelColor\n  }\n\n  public setLabelColor(color: RGBA): void {\n    this.labelColor = color\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    super.renderSelf(buffer)\n\n    const alphaText = `${this.alphaPercentage}%`\n    const centerX = this.x + Math.floor(this.width / 2 - alphaText.length / 2)\n    const centerY = this.y + Math.floor(this.height / 2)\n\n    buffer.drawText(alphaText, centerX, centerY, this.labelColor)\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    switch (event.type) {\n      case \"down\":\n        this.isDragging = true\n        this.dragOffsetX = event.x - this.x\n        this.dragOffsetY = event.y - this.y\n        this.zIndex = nextZIndex++\n        event.stopPropagation()\n        break\n\n      case \"drag-end\":\n        if (this.isDragging) {\n          this.isDragging = false\n          event.stopPropagation()\n        }\n        break\n\n      case \"drag\":\n        if (this.isDragging) {\n          const newX = event.x - this.dragOffsetX\n          const newY = event.y - this.dragOffsetY\n\n          this.x = Math.max(0, Math.min(newX, this._ctx.width - this.width))\n          this.y = Math.max(4, Math.min(newY, this._ctx.height - this.height))\n\n          event.stopPropagation()\n        }\n        break\n    }\n  }\n}\n\nexport function run(renderer: CliRenderer): void {\n  renderer.start()\n\n  const currentRunVersion = ++demoRunVersion\n  let currentTheme: ThemeName = \"dark\"\n  let currentThemeMode: ThemeMode = renderer.themeMode ?? DEFAULT_THEME_MODE\n  let transparentBackgroundColor = getTransparentFallbackBackgroundColor(currentThemeMode)\n  let transparentPaletteRequestVersion = 0\n  renderer.setBackgroundColor(getThemeBackgroundColor(currentTheme, currentThemeMode))\n\n  const parentContainer = new BoxRenderable(renderer, {\n    id: \"parent-container\",\n    zIndex: 10,\n  })\n  renderer.root.add(parentContainer)\n\n  const headerDisplay = new TextRenderable(renderer, {\n    id: \"header-text\",\n    content: getHeaderText(currentTheme),\n    width: 85,\n    height: 3,\n    position: \"absolute\",\n    left: 10,\n    top: 2,\n    zIndex: 1,\n    selectable: false,\n  })\n  parentContainer.add(headerDisplay)\n\n  const textUnderAlpha = new TextRenderable(renderer, {\n    id: \"text-under-alpha\",\n    content: \"This text should not be selectable\",\n    position: \"absolute\",\n    left: 10,\n    top: 6,\n    fg: THEMES[currentTheme].textUnderAlpha,\n    attributes: TextAttributes.BOLD,\n    zIndex: 4,\n    selectable: false,\n  })\n  parentContainer.add(textUnderAlpha)\n\n  const moreTextUnder = new TextRenderable(renderer, {\n    id: \"more-text-under\",\n    content: \"Selectable text to show character preservation\",\n    position: \"absolute\",\n    left: 15,\n    top: 10,\n    fg: THEMES[currentTheme].moreTextUnder,\n    attributes: TextAttributes.BOLD,\n    zIndex: 1,\n  })\n  parentContainer.add(moreTextUnder)\n\n  const alphaBox50 = new DraggableTransparentBox(\n    renderer,\n    \"alpha-box-50\",\n    15,\n    5,\n    25,\n    8,\n    RGBA.fromValues(64 / 255, 176 / 255, 255 / 255, 128 / 255),\n    50,\n  )\n  parentContainer.add(alphaBox50)\n  draggableBoxes.push(alphaBox50)\n\n  const alphaBox75 = new DraggableTransparentBox(\n    renderer,\n    \"alpha-box-75\",\n    30,\n    7,\n    25,\n    8,\n    RGBA.fromValues(255 / 255, 107 / 255, 129 / 255, 192 / 255),\n    30,\n  )\n  parentContainer.add(alphaBox75)\n  draggableBoxes.push(alphaBox75)\n\n  const alphaBox25 = new DraggableTransparentBox(\n    renderer,\n    \"alpha-box-25\",\n    45,\n    9,\n    25,\n    8,\n    RGBA.fromValues(139 / 255, 69 / 255, 193 / 255, 64 / 255),\n    10,\n  )\n  parentContainer.add(alphaBox25)\n  draggableBoxes.push(alphaBox25)\n\n  const alphaGreen = new DraggableTransparentBox(\n    renderer,\n    \"alpha-green\",\n    20,\n    11,\n    30,\n    5,\n    RGBA.fromValues(88 / 255, 214 / 255, 141 / 255, 96 / 255),\n    20,\n  )\n  parentContainer.add(alphaGreen)\n  draggableBoxes.push(alphaGreen)\n\n  const alphaYellow = new DraggableTransparentBox(\n    renderer,\n    \"alpha-yellow\",\n    25,\n    13,\n    20,\n    6,\n    RGBA.fromValues(255 / 255, 183 / 255, 77 / 255, 128 / 255),\n    40,\n  )\n  parentContainer.add(alphaYellow)\n  draggableBoxes.push(alphaYellow)\n\n  const alphaOverlay = new DraggableTransparentBox(\n    renderer,\n    \"alpha-overlay\",\n    10,\n    17,\n    65,\n    4,\n    RGBA.fromValues(200 / 255, 162 / 255, 255 / 255, 32 / 255),\n    60,\n  )\n  parentContainer.add(alphaOverlay)\n  draggableBoxes.push(alphaOverlay)\n\n  const applyTheme = (themeName: ThemeName): void => {\n    currentTheme = themeName\n\n    const theme = THEMES[themeName]\n    renderer.setBackgroundColor(themeName === \"transparent\" ? transparentBackgroundColor : theme.backgroundColor)\n    headerDisplay.content = getHeaderText(themeName)\n    textUnderAlpha.fg = theme.textUnderAlpha\n    moreTextUnder.fg = theme.moreTextUnder\n\n    for (const box of draggableBoxes) {\n      box.setLabelColor(theme.boxLabelColor)\n    }\n\n    if (themeName === \"transparent\") {\n      void updateTransparentBackgroundColor()\n    }\n  }\n\n  const updateTransparentBackgroundColor = async (): Promise<void> => {\n    const requestVersion = ++transparentPaletteRequestVersion\n    transparentBackgroundColor = getTransparentFallbackBackgroundColor(currentThemeMode)\n\n    if (currentTheme === \"transparent\") {\n      renderer.setBackgroundColor(transparentBackgroundColor)\n    }\n\n    try {\n      const palette = await renderer.getPalette()\n\n      if (currentRunVersion !== demoRunVersion || requestVersion !== transparentPaletteRequestVersion) {\n        return\n      }\n\n      if (palette.defaultBackground) {\n        transparentBackgroundColor = RGBA.fromHex(palette.defaultBackground)\n        transparentBackgroundColor.a = 0\n      }\n    } catch {\n      if (currentRunVersion !== demoRunVersion || requestVersion !== transparentPaletteRequestVersion) {\n        return\n      }\n    }\n\n    if (currentTheme === \"transparent\") {\n      renderer.setBackgroundColor(transparentBackgroundColor)\n    }\n  }\n\n  applyTheme(currentTheme)\n\n  if (keyListener) {\n    renderer.keyInput.off(\"keypress\", keyListener)\n  }\n\n  if (themeModeListener) {\n    renderer.off(\"theme_mode\", themeModeListener)\n  }\n\n  keyListener = (key: KeyEvent) => {\n    if (key.name !== \"b\") {\n      return\n    }\n\n    const currentThemeIndex = THEME_ORDER.indexOf(currentTheme)\n    const nextTheme = THEME_ORDER[(currentThemeIndex + 1) % THEME_ORDER.length]\n    applyTheme(nextTheme)\n  }\n\n  renderer.keyInput.on(\"keypress\", keyListener)\n\n  themeModeListener = (mode: ThemeMode) => {\n    currentThemeMode = mode\n    renderer.clearPaletteCache()\n\n    if (currentTheme === \"transparent\") {\n      void updateTransparentBackgroundColor()\n    }\n  }\n\n  renderer.on(\"theme_mode\", themeModeListener)\n}\n\nexport function destroy(renderer: CliRenderer): void {\n  demoRunVersion += 1\n  renderer.clearFrameCallbacks()\n\n  if (keyListener) {\n    renderer.keyInput.off(\"keypress\", keyListener)\n    keyListener = null\n  }\n\n  if (themeModeListener) {\n    renderer.off(\"theme_mode\", themeModeListener)\n    themeModeListener = null\n  }\n\n  for (const box of draggableBoxes) {\n    renderer.root.remove(box.id)\n  }\n  draggableBoxes = []\n\n  renderer.root.remove(\"parent-container\")\n  renderer.setCursorPosition(0, 0, false)\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  renderer.start()\n}\n"
  },
  {
    "path": "packages/core/src/examples/vnode-composition-demo.ts",
    "content": "import { createCliRenderer, MouseEvent, type CliRenderer } from \"../renderer.js\"\nimport {\n  Box,\n  Text,\n  Generic,\n  type VNode,\n  instantiate,\n  delegate,\n  Input,\n  BoxRenderable,\n  type BoxOptions,\n  vstyles,\n} from \"../renderables/index.js\"\nimport type { RenderContext } from \"../types.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport { setupCommonDemoKeys } from \"./lib/standalone-keys.js\"\nimport { RGBA, parseColor } from \"../lib/index.js\"\nimport type { Renderable } from \"../Renderable.js\"\nimport { TextAttributes } from \"../types.js\"\n\nconst textColor = parseColor(\"#FFFFFF\")\nconst globalbgColor = parseColor(\"#333333\")\nconst transparent = parseColor(\"transparent\")\n\nconst { bold, italic, underline, dim, boldItalic, boldUnderline, italicUnderline, color, bgColor, styled } = vstyles\n\n// This is NOT react and not reactive, it's just a declarative way to compose renderables\n// and mount them into a parent container.\nfunction MyRenderable(props: any, children: VNode[] = []) {\n  const mouseHandler = (event: MouseEvent) => {\n    console.log(\"mouseHandler\", event.type)\n  }\n\n  return Box({ id: \"inner\" }, [\n    Box(\n      {\n        border: true,\n        borderStyle: \"double\",\n        padding: 1,\n        onMouseDown: mouseHandler,\n        flexDirection: \"row\",\n      },\n      children,\n    ),\n  ])\n}\n\nfunction Button(\n  props: {\n    title: string\n    onClick: () => void\n    borderColor?: string | RGBA\n  },\n  children: VNode[] = [],\n) {\n  return Box(\n    {\n      id: \"button\",\n      border: true,\n      onMouseDown: props.onClick,\n      borderColor: props.borderColor,\n    },\n    Text({ content: props.title, selectable: false }),\n    ...children,\n  )\n}\n\n// Custom Rendering Functional Construct\nfunction VNodeButton(\n  props: {\n    title: string\n    onClick: () => void\n    borderColor?: RGBA\n  },\n  children: VNode[] = [],\n) {\n  return Generic(\n    {\n      render: (buffer, deltaTime, renderable) => demoRenderFn(props, buffer, deltaTime, renderable),\n      maxWidth: props.title.length + 4,\n      margin: 1,\n    },\n    Box(\n      {\n        id: \"button\",\n        height: 3,\n        onMouseDown: props.onClick,\n      },\n      children,\n    ),\n  )\n}\n\n// Custom Rendering - Class Method Example\nclass MyRoot {\n  width: number\n\n  constructor(private readonly props: { title: string; borderColor?: RGBA }) {\n    this.width = Math.max(props.title.length + 4, 12)\n    Object.assign(this, props)\n  }\n\n  render(buffer: OptimizedBuffer, deltaTime: number, renderable: Renderable) {\n    demoRenderFn(this.props, buffer, deltaTime, renderable)\n  }\n}\n\nfunction ButtonWithClassRender(\n  props: { title: string; onClick: () => void; borderColor?: RGBA; marginLeft?: number },\n  children: VNode[] = [],\n) {\n  return Generic(\n    new MyRoot(props),\n    Box(\n      {\n        id: \"button\",\n        height: 3,\n        onMouseDown: props.onClick,\n      },\n      ...children,\n    ),\n  )\n}\n\n// Host Override Example\nfunction MyDelegateToVNodeRenderable(props: any, children: VNode[] = []) {\n  return delegate(\n    {\n      add: `${props.id}_box3`,\n      remove: `${props.id}_box3`,\n    },\n    Box({ id: `${props.id}_outer3`, border: true, borderColor: \"blue\" }, [\n      Box({ id: `${props.id}_inner3`, border: true, borderColor: \"magenta\" }, [\n        Box({ id: `${props.id}_box3`, flexDirection: \"row\", border: true, padding: 1 }, children),\n      ]),\n    ]),\n  )\n}\n\nfunction MyDelegateToRenderableComponent(renderer: RenderContext, props: any, children: VNode[] = []) {\n  return delegate(\n    {\n      add: \"__box4\",\n      remove: \"__box4\",\n    },\n    instantiate(\n      renderer,\n      Box({ id: \"__outer4\", border: true, borderColor: \"blue\" }, [\n        Box({ id: \"__inner4\", border: true, borderColor: \"magenta\" }, [\n          Box({ id: \"__box4\", flexDirection: \"row\", border: true, padding: 1 }, children),\n        ]),\n      ]),\n    ),\n  )\n}\n\nfunction MyInstancedRenderable(renderer: RenderContext, props: any, children: VNode[] = []) {\n  return instantiate(renderer, MyDelegateToVNodeRenderable(props, children))\n}\n\nfunction LabeledInput(props: { id: string; label: string; placeholder: string }) {\n  return delegate(\n    {\n      focus: `${props.id}-input`,\n    },\n    Box(\n      { flexDirection: \"row\", id: `${props.id}-labeled-outer` },\n      Text({ content: props.label + \" \" }),\n      Input({\n        id: `${props.id}-input`,\n        placeholder: props.placeholder,\n        width: 20,\n        backgroundColor: \"white\",\n        textColor: \"black\",\n        cursorColor: \"blue\",\n        focusedBackgroundColor: \"orange\",\n      }),\n    ),\n  )\n}\n\nfunction BaseBox(props: BoxOptions, children: VNode[] = []) {\n  return Box(\n    {\n      id: \"base-box\",\n      border: true,\n      borderColor: \"blue\",\n      backgroundColor: \"orange\",\n      ...props,\n      renderAfter(buffer: OptimizedBuffer, deltaTime: number) {\n        buffer.drawText(\"Hello\", this.x + 1, this.y + 1, RGBA.fromInts(255, 255, 255, 255))\n        props.renderAfter?.call(this, buffer, deltaTime)\n      },\n    },\n    children,\n  )\n}\n\nfunction ExtendedBaseBox(props: BoxOptions, children: VNode[] = []) {\n  return BaseBox(\n    {\n      id: \"extended-base-box\",\n      ...props,\n      renderAfter(buffer: OptimizedBuffer, deltaTime: number) {\n        buffer.drawText(\"Extended\", this.x + 1, this.y + 2, RGBA.fromInts(255, 255, 255, 255))\n      },\n    },\n    children,\n  )\n}\n\nexport function run(renderer: CliRenderer) {\n  renderer.start()\n  const mainGroup = new BoxRenderable(renderer, {\n    id: \"main-group\",\n  })\n  renderer.root.add(mainGroup)\n\n  // BaseBox example\n  mainGroup.add(ExtendedBaseBox({ width: 20, height: 10, position: \"absolute\", left: 55, top: 10, zIndex: 1000 }))\n\n  // Proxied VNode example\n  const tree = MyRenderable({ id: \"demo-root\" }, [\n    Box({ id: \"child-1\", width: 20, height: 3, border: true, marginBottom: 1 }, [Text({ content: \"Hello\" })]),\n    Box({ id: \"child-2\", width: 24, height: 3, border: true }, [Text({ content: \"VNode world\" })]),\n  ])\n  tree.backgroundColor = RGBA.fromInts(0, 155, 155, 100)\n\n  mainGroup.add(tree)\n\n  const input = LabeledInput({ id: \"labeled-input\", label: \"Label:\", placeholder: \"Enter your text...\" })\n  input.focus()\n  mainGroup.add(input)\n\n  //\n  // VNode delegated version\n  const instance1 = MyDelegateToVNodeRenderable({ id: \"delegated-demo-root\" }, [\n    Box({ id: \"child-1\", width: 20, height: 3, border: true, marginBottom: 1 }, [\n      Text({ content: \"Hello delegated 1\" }),\n    ]),\n    Box({ id: \"child-2\", width: 24, height: 3, border: true }, [Text({ content: \"VNode world delegated 1\" })]),\n  ])\n  instance1.backgroundColor = RGBA.fromInts(155, 0, 155, 100)\n\n  mainGroup.add(instance1)\n\n  //\n  // Instaced Delegated version\n  const instance = MyInstancedRenderable(renderer, { id: \"demo-root\" }, [\n    Box({ id: \"child-1\", width: 20, height: 3, border: true, marginBottom: 1 }, [Text({ content: \"Hello 2\" })]),\n    Box({ id: \"child-2\", width: 24, height: 3, border: true }, [Text({ content: \"VNode world 2\" })]),\n  ])\n\n  mainGroup.add(instance)\n\n  // Delegated to __box3, would otherwise end up in the top-level group!\n  instance.add(Box({ id: \"child-3\", width: 24, height: 3, border: true }, [Text({ content: \"VNode world 3\" })]))\n  instance.add(Button({ title: \"Click me\", onClick: () => console.log(\"clicked\"), borderColor: \"red\" }))\n\n  //\n  // Renderable delegated version\n  const renderableInstance = MyDelegateToRenderableComponent(renderer, { id: \"demo-root\" }, [\n    Box({ id: \"child-1\", width: 20, height: 3, border: true, marginBottom: 1 }, [Text({ content: \"Hello 4\" })]),\n    Box({ id: \"child-2\", width: 24, height: 3, border: true }, [Text({ content: \"VNode world 4\" })]),\n  ])\n  mainGroup.add(renderableInstance)\n\n  // Delegated to __box4, would otherwise end up in the top-level group!\n  renderableInstance.add(Button({ title: \"Click me too!\", onClick: () => console.log(\"clicked\"), borderColor: \"red\" }))\n\n  //\n  // Add animated VNode button\n  mainGroup.add(\n    VNodeButton({\n      title: \"Animated VNode\",\n      onClick: () => console.log(\"vnode 1 clicked\"),\n      borderColor: RGBA.fromInts(0, 0, 255, 255),\n    }),\n  )\n  mainGroup.add(\n    VNodeButton({\n      title: \"Same VNode, different props\",\n      onClick: () => console.log(\"vnode 2 clicked\"),\n      borderColor: RGBA.fromInts(255, 0, 255, 255),\n    }),\n  )\n\n  //\n  // Add button with class render function\n  mainGroup.add(\n    ButtonWithClassRender({\n      marginLeft: 1,\n      title: \"ClassRender\",\n      onClick: () => console.log(\"clicked\"),\n      borderColor: RGBA.fromInts(0, 0, 255, 255),\n    }),\n  )\n\n  mainGroup.add(\n    Box({ flexDirection: \"column\", marginTop: 2 }, [\n      // Basic styles\n      Text({}, bold(\"Bold Text\")),\n      Text({}, italic(\"Italic Text\")),\n      Text({}, underline(\"Underlined Text\")),\n      Text({}, dim(\"Dim Text\")),\n\n      // Combined styles\n      Text({}, boldItalic(\"Bold and Italic\")),\n      Text({}, boldUnderline(\"Bold and Underlined\")),\n      Text({}, italicUnderline(\"Italic and Underlined\")),\n\n      // Colors\n      Text({}, color(\"#ff6b6b\", \"Red Text\")),\n      Text({}, bgColor(\"#4ecdc4\", \"Text with Background\")),\n\n      // Custom styling\n      Text({}, styled(TextAttributes.BOLD | TextAttributes.UNDERLINE, \"Custom Styled\")),\n\n      // Stacked styles\n      Text({}, bold(underline(\"hello\"), \" world\")),\n      Text({}, color(\"#ff6b6b\", bold(\"Bold Red\"), \" normal\")),\n      Text({}, italic(color(\"#4ecdc4\", \"Green Italic\"), \" normal again\")),\n    ]),\n  )\n}\n\nexport function destroy(renderer: CliRenderer) {\n  renderer.root.getRenderable(\"main-group\")?.destroyRecursively()\n  renderer.requestRender()\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({\n    exitOnCtrlC: true,\n  })\n\n  run(renderer)\n  setupCommonDemoKeys(renderer)\n  renderer.start()\n}\n\nfunction demoRenderFn(\n  props: { title: string; borderColor?: RGBA },\n  buffer: OptimizedBuffer,\n  deltaTime: number,\n  renderable: Renderable,\n) {\n  const x = renderable.x\n  const y = renderable.y\n  const width = renderable.width\n  const height = renderable.height\n\n  const borderColor = props.borderColor ?? RGBA.fromInts(255, 255, 0, 255)\n\n  // Draw a simple animated button with pulsing border\n  const timeInSeconds = Date.now() / 1000\n  const pulse = Math.sin(timeInSeconds * 4) * 0.5 + 0.5 // Fast pulsing, 0-1 oscillation\n\n  const pulsingBorderColor = RGBA.fromValues(\n    borderColor.r * (0.1 + pulse * 0.9),\n    borderColor.g * (0.1 + pulse * 0.9),\n    borderColor.b * (0.1 + pulse * 0.9),\n    borderColor.a,\n  )\n\n  const bgPulse = Math.sin(timeInSeconds * 2 + Math.PI / 2) * 0.4 + 0.6 // Different frequency and phase\n  const pulsingBgColor = RGBA.fromValues(\n    globalbgColor.r * bgPulse,\n    globalbgColor.g * bgPulse,\n    globalbgColor.b * bgPulse,\n    globalbgColor.a,\n  )\n\n  for (let row = 0; row < height; row++) {\n    for (let col = 0; col < width; col++) {\n      const isTop = row === 0\n      const isBottom = row === height - 1\n      const isLeft = col === 0\n      const isRight = col === width - 1\n      const isBorder = isTop || isBottom || isLeft || isRight\n\n      if (isBorder) {\n        buffer.setCell(x + col, y + row, \"█\", pulsingBorderColor, pulsingBgColor)\n      } else {\n        buffer.setCell(x + col, y + row, \" \", textColor, pulsingBgColor)\n      }\n    }\n  }\n\n  const titlePulse = Math.sin(timeInSeconds * 6) * 0.5 + 0.5 // Even faster text pulse\n  const textScale = 0.3 + titlePulse * 0.7\n  const pulsingTextColor = RGBA.fromValues(\n    textColor.r * textScale,\n    textColor.g * textScale,\n    textColor.b * textScale,\n    textColor.a,\n  )\n\n  const titleX = x + Math.floor((width - props.title.length) / 2)\n  const titleY = y + Math.floor(height / 2)\n  if (titleY >= y && titleY < y + height) {\n    buffer.drawText(props.title, titleX, titleY, pulsingTextColor, transparent)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/index.ts",
    "content": "// Core exports without 3D dependencies\nexport * from \"./Renderable.js\"\nexport * from \"./types.js\"\nexport * from \"./utils.js\"\nexport * from \"./buffer.js\"\nexport * from \"./text-buffer.js\"\nexport * from \"./text-buffer-view.js\"\nexport * from \"./edit-buffer.js\"\nexport * from \"./editor-view.js\"\nexport * from \"./syntax-style.js\"\nexport * from \"./post/effects.js\"\nexport * from \"./post/filters.js\"\nexport * from \"./post/matrices.js\"\nexport * from \"./animation/Timeline.js\"\nexport * from \"./lib/index.js\"\nexport * from \"./renderer.js\"\nexport * from \"./plugins/types.js\"\nexport * from \"./plugins/registry.js\"\nexport * from \"./plugins/core-slot.js\"\nexport * from \"./NativeSpanFeed.js\"\nexport * from \"./renderables/index.js\"\nexport * from \"./zig.js\"\nexport * from \"./console.js\"\nexport * as Yoga from \"yoga-layout\"\n"
  },
  {
    "path": "packages/core/src/lib/KeyHandler.integration.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { InternalKeyHandler, KeyEvent } from \"./KeyHandler.js\"\nimport { parseKeypress } from \"./parse.keypress.js\"\n\n/**\n * Integration tests demonstrating real-world scenarios with stopPropagation\n */\n\nfunction createKeyHandler(): InternalKeyHandler {\n  return new InternalKeyHandler()\n}\n\nfunction dispatchInput(handler: InternalKeyHandler, data: string): boolean {\n  const parsedKey = parseKeypress(data)\n  if (!parsedKey) {\n    return false\n  }\n\n  return handler.processParsedKey(parsedKey)\n}\n\ntest(\"Integration - Modal ESC handler prevents subsequent handlers\", () => {\n  const handler = createKeyHandler()\n\n  let modalOpen = true\n  let modalHandledEsc = false\n  let backgroundHandledEsc = false\n\n  // Modal ESC handler (registered first, so it runs first)\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"escape\" && modalOpen) {\n      modalHandledEsc = true\n      modalOpen = false\n      key.stopPropagation() // Stop other handlers from running\n    }\n  })\n\n  // Background/app-level ESC handler (registered second, should not run)\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"escape\") {\n      backgroundHandledEsc = true\n    }\n  })\n\n  // Simulate ESC key press while modal is open\n  dispatchInput(handler, \"\\x1b\")\n\n  expect(modalOpen).toBe(false)\n  expect(modalHandledEsc).toBe(true)\n  expect(backgroundHandledEsc).toBe(false) // Modal stopped propagation\n})\n\ntest(\"Integration - Focused input field handles key, stops parent handlers\", () => {\n  const handler = createKeyHandler()\n\n  const inputValue: string[] = []\n  let parentHandledKey = false\n\n  // Parent container handler\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (!key.propagationStopped) {\n      parentHandledKey = true\n    }\n  })\n\n  // Focused input field handler (internal/renderable)\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"a\" || key.name === \"b\" || key.name === \"c\") {\n      inputValue.push(key.name)\n      key.stopPropagation() // Input consumed the key\n    }\n  })\n\n  // Type some keys\n  dispatchInput(handler, \"a\")\n  dispatchInput(handler, \"b\")\n  dispatchInput(handler, \"c\")\n\n  expect(inputValue).toEqual([\"a\", \"b\", \"c\"])\n  expect(parentHandledKey).toBe(true) // Parent ran first (global priority)\n\n  // But internal handler got to consume the keys and stop propagation\n  // doesn't prevent parent from seeing them first (global runs before internal)\n})\n\ntest(\"Integration - Dialog system with priority: innermost modal wins\", () => {\n  const handler = createKeyHandler()\n\n  let outerModalClosed = false\n  let innerModalClosed = false\n  const closeLog: string[] = []\n\n  // Outer modal ESC handler\n  const outerHandler = (key: KeyEvent) => {\n    if (key.name === \"escape\" && !key.propagationStopped) {\n      closeLog.push(\"outer\")\n      outerModalClosed = true\n      key.stopPropagation()\n    }\n  }\n\n  // Inner modal ESC handler (registered later, so it comes first in listener order)\n  const innerHandler = (key: KeyEvent) => {\n    if (key.name === \"escape\") {\n      closeLog.push(\"inner\")\n      innerModalClosed = true\n      key.stopPropagation()\n    }\n  }\n\n  // Register outer first\n  handler.on(\"keypress\", outerHandler)\n\n  // Then inner (but we want inner to handle first)\n  // In a real app, we'd use prependInputHandler or similar\n  // For now, let's simulate by removing outer and re-adding in correct order\n  handler.removeListener(\"keypress\", outerHandler)\n  handler.on(\"keypress\", innerHandler)\n  handler.on(\"keypress\", outerHandler)\n\n  // Press ESC\n  dispatchInput(handler, \"\\x1b\")\n\n  expect(closeLog).toEqual([\"inner\"])\n  expect(innerModalClosed).toBe(true)\n  expect(outerModalClosed).toBe(false) // Inner stopped propagation\n})\n\ntest(\"Integration - Keyboard shortcut system with priorities\", () => {\n  const handler = createKeyHandler()\n\n  const actions: string[] = []\n\n  // Global shortcuts (Ctrl+S = Save, Ctrl+O = Open)\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.ctrl && key.name === \"s\") {\n      actions.push(\"save\")\n      // Don't stop propagation - allow other handlers to see it\n    }\n    if (key.ctrl && key.name === \"o\") {\n      actions.push(\"open\")\n    }\n  })\n\n  // Text editor overrides Ctrl+S when focused\n  let editorFocused = true\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    if (editorFocused && key.ctrl && key.name === \"s\") {\n      actions.push(\"save-document\")\n      key.stopPropagation() // Override global save\n    }\n  })\n\n  // Ctrl+S with editor focused\n  dispatchInput(handler, \"\\x13\") // Ctrl+S\n\n  expect(actions).toEqual([\"save\", \"save-document\"])\n  // Note: global runs first, then internal. To truly override,\n  // the editor would need to be a global handler registered first\n})\n\ntest(\"Integration - preventDefault vs stopPropagation behavior\", () => {\n  const handler = createKeyHandler()\n\n  const log: string[] = []\n\n  // Handler 1: preventDefault only\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"a\") {\n      log.push(\"handler1-saw-a\")\n      key.preventDefault()\n    }\n  })\n\n  // Handler 2: Should still run (preventDefault doesn't stop global handlers)\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"a\") {\n      log.push(\"handler2-saw-a\")\n      if (key.defaultPrevented) {\n        log.push(\"handler2-saw-prevented\")\n      }\n    }\n  })\n\n  // Handler 3: Internal handler should not run (preventDefault stops internal)\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"a\") {\n      log.push(\"handler3-internal-saw-a\")\n    }\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(log).toEqual([\n    \"handler1-saw-a\",\n    \"handler2-saw-a\",\n    \"handler2-saw-prevented\",\n    // handler3 doesn't run because preventDefault stops internal handlers\n  ])\n\n  // Now test with stopPropagation\n  log.length = 0\n\n  handler.removeAllListeners(\"keypress\")\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"b\") {\n      log.push(\"handler1-saw-b\")\n      key.stopPropagation()\n    }\n  })\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"b\") {\n      log.push(\"handler2-saw-b\")\n    }\n  })\n\n  dispatchInput(handler, \"b\")\n\n  expect(log).toEqual([\n    \"handler1-saw-b\",\n    // handler2 doesn't run because stopPropagation stops all subsequent handlers\n  ])\n})\n\ntest(\"Integration - Form submission with Enter key\", () => {\n  const handler = createKeyHandler()\n\n  let formSubmitted = false\n  let inputValue = \"\"\n\n  // Form's Enter handler\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"return\" && !key.propagationStopped) {\n      formSubmitted = true\n    }\n  })\n\n  // Input field's Enter handler\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"return\") {\n      // Multi-line input: add newline and stop propagation\n      inputValue += \"\\n\"\n      key.stopPropagation()\n    }\n  })\n\n  // Press Enter\n  dispatchInput(handler, \"\\r\")\n\n  expect(inputValue).toBe(\"\\n\")\n  expect(formSubmitted).toBe(true) // Global handler ran first\n\n  // In a real app, you'd check defaultPrevented in the form handler\n  // or the input would be registered as a global handler first\n})\n\ntest(\"Integration - Event bubbling with multiple nested components\", () => {\n  const handler = createKeyHandler()\n\n  const eventLog: Array<{ component: string; stopped: boolean }> = []\n\n  // Root component\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    eventLog.push({ component: \"root\", stopped: key.propagationStopped })\n  })\n\n  // Child component (registered as internal, represents focused element)\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    eventLog.push({ component: \"child\", stopped: key.propagationStopped })\n\n    // Child handles space key and stops propagation\n    if (key.name === \"space\") {\n      key.stopPropagation()\n    }\n  })\n\n  // Another internal handler (sibling or parent)\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    eventLog.push({ component: \"sibling\", stopped: key.propagationStopped })\n  })\n\n  dispatchInput(handler, \" \") // Space key\n\n  expect(eventLog).toEqual([\n    { component: \"root\", stopped: false },\n    { component: \"child\", stopped: false },\n    // sibling doesn't run because child stopped propagation\n  ])\n  expect(eventLog).toHaveLength(2)\n})\n"
  },
  {
    "path": "packages/core/src/lib/KeyHandler.stopPropagation.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { InternalKeyHandler, KeyEvent } from \"./KeyHandler.js\"\nimport { parseKeypress } from \"./parse.keypress.js\"\nimport { pasteBytes } from \"../testing/mock-keys.js\"\n\nfunction createKeyHandler(): InternalKeyHandler {\n  return new InternalKeyHandler()\n}\n\nfunction dispatchInput(handler: InternalKeyHandler, data: string): boolean {\n  const parsedKey = parseKeypress(data)\n  if (!parsedKey) {\n    return false\n  }\n\n  return handler.processParsedKey(parsedKey)\n}\n\ntest(\"stopPropagation - stops subsequent global handlers\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"global1\")\n    key.stopPropagation()\n  })\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"global2\")\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(callOrder).toEqual([\"global1\"])\n})\n\ntest(\"stopPropagation - stops internal handlers from running\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"global\")\n    key.stopPropagation()\n  })\n\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"internal\")\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(callOrder).toEqual([\"global\"])\n})\n\ntest(\"stopPropagation - internal handler can stop other internal handlers\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"internal1\")\n    key.stopPropagation()\n  })\n\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"internal2\")\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(callOrder).toEqual([\"internal1\"])\n})\n\ntest(\"stopPropagation - does not affect preventDefault\", () => {\n  const handler = createKeyHandler()\n\n  let stoppedPropagation = false\n  let preventedDefault = false\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    key.stopPropagation()\n    key.preventDefault()\n    stoppedPropagation = key.propagationStopped\n    preventedDefault = key.defaultPrevented\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(stoppedPropagation).toBe(true)\n  expect(preventedDefault).toBe(true)\n})\n\ntest(\"stopPropagation - without calling it, all handlers run\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"global1\")\n  })\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"global2\")\n  })\n\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"internal1\")\n  })\n\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"internal2\")\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(callOrder).toEqual([\"global1\", \"global2\", \"internal1\", \"internal2\"])\n})\n\ntest(\"stopPropagation - paste events support stopPropagation\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.on(\"paste\", (event) => {\n    callOrder.push(\"global\")\n    event.stopPropagation()\n  })\n\n  handler.onInternal(\"paste\", (event) => {\n    callOrder.push(\"internal\")\n  })\n\n  handler.processPaste(pasteBytes(\"hello\"))\n\n  expect(callOrder).toEqual([\"global\"])\n})\n\ntest(\"stopPropagation - works with keyrelease events\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.on(\"keyrelease\", (key: KeyEvent) => {\n    callOrder.push(\"global\")\n    key.stopPropagation()\n  })\n\n  handler.onInternal(\"keyrelease\", (key: KeyEvent) => {\n    callOrder.push(\"internal\")\n  })\n\n  // Emit a release event directly since we need kitty protocol\n  handler.emit(\n    \"keyrelease\",\n    new KeyEvent({\n      name: \"a\",\n      ctrl: false,\n      meta: false,\n      shift: false,\n      option: false,\n      sequence: \"a\",\n      number: false,\n      raw: \"a\",\n      eventType: \"release\",\n      source: \"kitty\",\n    }),\n  )\n\n  expect(callOrder).toEqual([\"global\"])\n})\n\ntest(\"stopPropagation - error in handler does not affect propagation stopped state\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"global1\")\n    key.stopPropagation()\n    throw new Error(\"Test error\")\n  })\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"global2\")\n  })\n\n  expect(() => dispatchInput(handler, \"a\")).not.toThrow()\n\n  expect(callOrder).toEqual([\"global1\"])\n})\n\ntest(\"stopPropagation - modal scenario: ESC key handled by modal, stops at modal\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n  let modalClosed = false\n  let appHandledEsc = false\n\n  // Modal handler (internal, should be focused element) - runs BEFORE app handler\n  // In a real app, the focused modal element would use onInternal\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"escape\") {\n      callOrder.push(\"modal\")\n      modalClosed = true\n      key.stopPropagation()\n    }\n  })\n\n  // App-level global ESC handler (should NOT run if modal stops propagation)\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"escape\") {\n      callOrder.push(\"app\")\n      appHandledEsc = true\n    }\n  })\n\n  dispatchInput(handler, \"\\x1b\")\n\n  // Global handlers run before internal handlers\n  // So app handler runs first, but modal can still stop further internal handlers\n  expect(callOrder).toEqual([\"app\", \"modal\"])\n  expect(modalClosed).toBe(true)\n  expect(appHandledEsc).toBe(true)\n})\n\ntest(\"stopPropagation - modal scenario: global modal handler prevents app handler\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n  let modalClosed = false\n  let appHandledEsc = false\n\n  // Modal as a global handler (registered first) - to stop before app handler\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"escape\") {\n      callOrder.push(\"modal\")\n      modalClosed = true\n      key.stopPropagation()\n    }\n  })\n\n  // App-level ESC handler (should NOT run due to stopPropagation)\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (key.name === \"escape\") {\n      callOrder.push(\"app\")\n      appHandledEsc = true\n    }\n  })\n\n  dispatchInput(handler, \"\\x1b\")\n\n  // When modal is registered as a global handler first, it can stop the app handler\n  expect(callOrder).toEqual([\"modal\"])\n  expect(modalClosed).toBe(true)\n  expect(appHandledEsc).toBe(false)\n})\n\ntest(\"stopPropagation - event flow without stopPropagation shows order\", () => {\n  const handler = createKeyHandler()\n\n  const events: string[] = []\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    events.push(\"global1\")\n    expect(key.propagationStopped).toBe(false)\n  })\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    events.push(\"global2\")\n    expect(key.propagationStopped).toBe(false)\n  })\n\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    events.push(\"internal1\")\n    expect(key.propagationStopped).toBe(false)\n  })\n\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    events.push(\"internal2\")\n    expect(key.propagationStopped).toBe(false)\n  })\n\n  dispatchInput(handler, \"a\")\n\n  // Verify execution order: global handlers first, then internal handlers\n  expect(events).toEqual([\"global1\", \"global2\", \"internal1\", \"internal2\"])\n})\n"
  },
  {
    "path": "packages/core/src/lib/KeyHandler.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { InternalKeyHandler, KeyEvent } from \"./KeyHandler.js\"\nimport { type ParseKeypressOptions, parseKeypress } from \"./parse.keypress.js\"\nimport { decodePasteBytes } from \"./paste.js\"\nimport { createTestRenderer } from \"../testing/test-renderer.js\"\nimport { pasteBytes } from \"../testing/mock-keys.js\"\n\nconst { renderer, mockInput } = await createTestRenderer({})\n\nfunction createKeyHandler(): InternalKeyHandler {\n  return new InternalKeyHandler()\n}\n\nfunction dispatchInput(handler: InternalKeyHandler, data: string, options: ParseKeypressOptions = {}): boolean {\n  const parsedKey = parseKeypress(data, options)\n  if (!parsedKey) {\n    return false\n  }\n\n  return handler.processParsedKey(parsedKey)\n}\n\ntest(\"KeyHandler - parsed input emits keypress events\", () => {\n  const handler = new InternalKeyHandler()\n\n  let receivedKey: KeyEvent | undefined\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    receivedKey = key\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(receivedKey).toMatchObject({\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"a\",\n    eventType: \"press\",\n  })\n})\n\ntest(\"KeyHandler - emits keypress events\", () => {\n  const handler = createKeyHandler()\n\n  let receivedKey: KeyEvent | undefined\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    receivedKey = key\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(receivedKey).toMatchObject({\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"a\",\n    eventType: \"press\",\n  })\n})\n\ntest(\"KeyHandler - handles paste via processPaste\", async () => {\n  const handler = createKeyHandler()\n\n  let receivedPaste: Uint8Array | undefined\n  handler.on(\"paste\", (event) => {\n    receivedPaste = event.bytes\n  })\n\n  handler.processPaste(pasteBytes(\"pasted content\"))\n\n  expect(receivedPaste).toEqual(pasteBytes(\"pasted content\"))\n})\n\ntest(\"KeyHandler - processPaste handles content directly\", () => {\n  const handler = createKeyHandler()\n\n  let receivedPaste: Uint8Array | undefined\n  handler.on(\"paste\", (event) => {\n    receivedPaste = event.bytes\n  })\n\n  // processPaste receives the full content, no chunking\n  handler.processPaste(pasteBytes(\"chunk1chunk2chunk3\"))\n\n  expect(receivedPaste).toEqual(pasteBytes(\"chunk1chunk2chunk3\"))\n})\n\ntest(\"KeyHandler - preserves raw ANSI bytes in paste\", () => {\n  const handler = createKeyHandler()\n\n  let receivedPaste: Uint8Array | undefined\n  handler.on(\"paste\", (event) => {\n    receivedPaste = event.bytes\n  })\n\n  handler.processPaste(pasteBytes(\"text with \\x1b[31mred\\x1b[0m color\"))\n\n  expect(receivedPaste).toEqual(pasteBytes(\"text with \\x1b[31mred\\x1b[0m color\"))\n})\n\ntest(\"KeyHandler - constructor creates a handler\", () => {\n  const handler = createKeyHandler()\n\n  expect(handler).toBeDefined()\n})\n\ntest(\"KeyHandler - handles string input\", () => {\n  const handler = createKeyHandler()\n\n  let receivedKey: KeyEvent | undefined\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    receivedKey = key\n  })\n\n  dispatchInput(handler, \"c\")\n\n  expect(receivedKey).toMatchObject({\n    name: \"c\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"c\",\n    raw: \"c\",\n    eventType: \"press\",\n  })\n})\n\ntest(\"KeyHandler - event inheritance from EventEmitter\", () => {\n  const handler = createKeyHandler()\n\n  expect(typeof handler.on).toBe(\"function\")\n  expect(typeof handler.emit).toBe(\"function\")\n  expect(typeof handler.removeListener).toBe(\"function\")\n})\n\ntest(\"KeyHandler - preventDefault stops propagation\", () => {\n  const handler = createKeyHandler()\n\n  let globalHandlerCalled = false\n  let secondHandlerCalled = false\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    globalHandlerCalled = true\n    key.preventDefault()\n  })\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    if (!key.defaultPrevented) {\n      secondHandlerCalled = true\n    }\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(globalHandlerCalled).toBe(true)\n  expect(secondHandlerCalled).toBe(false)\n})\n\ntest(\"InternalKeyHandler - onInternal handlers run after regular handlers\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"internal\")\n  })\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    callOrder.push(\"regular\")\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(callOrder).toEqual([\"regular\", \"internal\"])\n})\n\ntest(\"InternalKeyHandler - preventDefault prevents internal handlers from running\", () => {\n  const handler = createKeyHandler()\n\n  let regularHandlerCalled = false\n  let internalHandlerCalled = false\n\n  // Register regular handler that prevents default\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    regularHandlerCalled = true\n    key.preventDefault()\n  })\n\n  // Register internal handler (should not run if prevented)\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    internalHandlerCalled = true\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(regularHandlerCalled).toBe(true)\n  expect(internalHandlerCalled).toBe(false)\n})\n\ntest(\"InternalKeyHandler - multiple internal handlers can be registered\", () => {\n  const handler = createKeyHandler()\n\n  let handler1Called = false\n  let handler2Called = false\n  let handler3Called = false\n\n  const internalHandler1 = () => {\n    handler1Called = true\n  }\n  const internalHandler2 = () => {\n    handler2Called = true\n  }\n  const internalHandler3 = () => {\n    handler3Called = true\n  }\n\n  handler.onInternal(\"keypress\", internalHandler1)\n  handler.onInternal(\"keypress\", internalHandler2)\n  handler.onInternal(\"keypress\", internalHandler3)\n\n  dispatchInput(handler, \"a\")\n\n  expect(handler1Called).toBe(true)\n  expect(handler2Called).toBe(true)\n  expect(handler3Called).toBe(true)\n})\n\ntest(\"InternalKeyHandler - offInternal removes specific handlers\", () => {\n  const handler = createKeyHandler()\n\n  let handler1Called = false\n  let handler2Called = false\n\n  const internalHandler1 = () => {\n    handler1Called = true\n  }\n  const internalHandler2 = () => {\n    handler2Called = true\n  }\n\n  handler.onInternal(\"keypress\", internalHandler1)\n  handler.onInternal(\"keypress\", internalHandler2)\n\n  // Remove only handler1\n  handler.offInternal(\"keypress\", internalHandler1)\n\n  dispatchInput(handler, \"a\")\n\n  expect(handler1Called).toBe(false)\n  expect(handler2Called).toBe(true)\n})\n\ntest(\"InternalKeyHandler - emit returns true when there are listeners\", () => {\n  const handler = createKeyHandler()\n\n  // No listeners initially\n  let hasListeners = handler.emit(\n    \"keypress\",\n    new KeyEvent({\n      name: \"a\",\n      ctrl: false,\n      meta: false,\n      shift: false,\n      option: false,\n      sequence: \"a\",\n      number: false,\n      raw: \"a\",\n      eventType: \"press\",\n      source: \"raw\",\n    }),\n  )\n  expect(hasListeners).toBe(false)\n\n  // Add regular listener\n  handler.on(\"keypress\", () => {})\n  hasListeners = handler.emit(\n    \"keypress\",\n    new KeyEvent({\n      name: \"b\",\n      ctrl: false,\n      meta: false,\n      shift: false,\n      option: false,\n      sequence: \"b\",\n      number: false,\n      raw: \"b\",\n      eventType: \"press\",\n      source: \"raw\",\n    }),\n  )\n  expect(hasListeners).toBe(true)\n\n  // Remove regular listener, add internal listener\n  handler.removeAllListeners(\"keypress\")\n  handler.onInternal(\"keypress\", () => {})\n  hasListeners = handler.emit(\n    \"keypress\",\n    new KeyEvent({\n      name: \"c\",\n      ctrl: false,\n      meta: false,\n      shift: false,\n      option: false,\n      sequence: \"c\",\n      number: false,\n      raw: \"c\",\n      eventType: \"press\",\n      source: \"raw\",\n    }),\n  )\n  expect(hasListeners).toBe(true)\n})\n\ntest(\"InternalKeyHandler - paste events work with priority system\", () => {\n  const handler = createKeyHandler()\n\n  const callOrder: string[] = []\n\n  handler.on(\"paste\", (event) => {\n    callOrder.push(`regular:${decodePasteBytes(event.bytes)}`)\n  })\n\n  handler.onInternal(\"paste\", (event) => {\n    callOrder.push(`internal:${decodePasteBytes(event.bytes)}`)\n  })\n\n  handler.processPaste(pasteBytes(\"hello\"))\n\n  expect(callOrder).toEqual([\"regular:hello\", \"internal:hello\"])\n})\n\ntest(\"InternalKeyHandler - paste preventDefault prevents internal handlers\", () => {\n  const handler = createKeyHandler()\n\n  let regularHandlerCalled = false\n  let internalHandlerCalled = false\n  let receivedText = \"\"\n\n  handler.on(\"paste\", (event) => {\n    regularHandlerCalled = true\n    receivedText = decodePasteBytes(event.bytes)\n    event.preventDefault()\n  })\n\n  handler.onInternal(\"paste\", (event) => {\n    internalHandlerCalled = true\n  })\n\n  handler.processPaste(pasteBytes(\"test paste\"))\n\n  expect(regularHandlerCalled).toBe(true)\n  expect(receivedText).toBe(\"test paste\")\n  expect(internalHandlerCalled).toBe(false)\n})\n\ntest(\"KeyHandler - emits paste event even with empty content\", () => {\n  const handler = createKeyHandler()\n\n  let pasteEventReceived = false\n  let receivedPaste: Uint8Array = pasteBytes(\"not-empty\")\n\n  handler.on(\"paste\", (event) => {\n    pasteEventReceived = true\n    receivedPaste = event.bytes\n  })\n\n  handler.processPaste(pasteBytes(\"\"))\n\n  expect(pasteEventReceived).toBe(true)\n  expect(receivedPaste).toEqual(pasteBytes(\"\"))\n})\n\ntest(\"KeyHandler - filters out mouse events\", () => {\n  const handler = createKeyHandler()\n\n  let keypressCount = 0\n  handler.on(\"keypress\", () => {\n    keypressCount++\n  })\n\n  // Mouse events should not generate keypresses\n  dispatchInput(handler, \"\\x1b[<0;10;5M\")\n  expect(keypressCount).toBe(0)\n\n  dispatchInput(handler, \"\\x1b[<0;10;5m\")\n  expect(keypressCount).toBe(0)\n\n  // Old-style mouse: \\x1b[M + 3 bytes, then \"c\" is a separate keypress\n  dispatchInput(handler, \"\\x1b[M ab\")\n  expect(keypressCount).toBe(0)\n\n  dispatchInput(handler, \"c\")\n  expect(keypressCount).toBe(1)\n\n  dispatchInput(handler, \"a\")\n  expect(keypressCount).toBe(2) // Now we have \"c\" and \"a\"\n})\n\ntest(\"KeyHandler - KeyEvent has source field set to 'raw' by default\", () => {\n  if (!renderer) {\n    throw new Error(\"Renderer not initialized\")\n  }\n\n  let receivedKey: KeyEvent | undefined\n  renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n    receivedKey = key\n  })\n\n  mockInput.pressKey(\"a\")\n\n  expect(receivedKey).toBeDefined()\n  expect(receivedKey?.source).toBe(\"raw\")\n  expect(receivedKey?.name).toBe(\"a\")\n\n  renderer.keyInput.removeAllListeners(\"keypress\")\n})\n\ntest(\"KeyHandler - KeyEvent has source field for different key types\", () => {\n  if (!renderer) {\n    throw new Error(\"Renderer not initialized\")\n  }\n\n  const receivedKeys: KeyEvent[] = []\n  renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n    receivedKeys.push(key)\n  })\n\n  // Test various key types\n  mockInput.pressKey(\"a\")\n  mockInput.pressKey(\"A\")\n  mockInput.pressKey(\"\\x1b[A\") // Up arrow\n  mockInput.pressKey(\"\\x01\") // Ctrl+A\n\n  expect(receivedKeys).toHaveLength(4)\n  expect(receivedKeys[0]?.source).toBe(\"raw\")\n  expect(receivedKeys[1]?.source).toBe(\"raw\")\n  expect(receivedKeys[2]?.source).toBe(\"raw\")\n  expect(receivedKeys[3]?.source).toBe(\"raw\")\n\n  renderer.keyInput.removeAllListeners(\"keypress\")\n})\n\ntest(\"KeyHandler - KeyEvent source is 'kitty' when using Kitty keyboard protocol\", () => {\n  const handler = createKeyHandler()\n\n  let receivedKey: KeyEvent | undefined\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    receivedKey = key\n  })\n\n  // Send a Kitty keyboard protocol sequence for 'a' (codepoint 97)\n  dispatchInput(handler, \"\\x1b[97u\", { useKittyKeyboard: true })\n\n  expect(receivedKey).toBeDefined()\n  expect(receivedKey?.source).toBe(\"kitty\")\n  expect(receivedKey?.name).toBe(\"a\")\n})\n\ntest(\"KeyHandler - KeyEvent source is 'raw' for non-Kitty sequences even with Kitty enabled\", () => {\n  if (!renderer) {\n    throw new Error(\"Renderer not initialized\")\n  }\n\n  const receivedKeys: KeyEvent[] = []\n  renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n    receivedKeys.push(key)\n  })\n\n  // Send regular sequences that don't match Kitty protocol\n  mockInput.pressKey(\"a\")\n  mockInput.pressKey(\"\\x1b[A\") // Up arrow (standard ANSI)\n\n  expect(receivedKeys).toHaveLength(2)\n  expect(receivedKeys[0]?.source).toBe(\"raw\")\n  expect(receivedKeys[0]?.name).toBe(\"a\")\n  expect(receivedKeys[1]?.source).toBe(\"raw\")\n  expect(receivedKeys[1]?.name).toBe(\"up\")\n\n  renderer.keyInput.removeAllListeners(\"keypress\")\n})\n\ntest(\"KeyHandler - source field persists through KeyEvent wrapper\", () => {\n  if (!renderer) {\n    throw new Error(\"Renderer not initialized\")\n  }\n\n  let receivedKey: KeyEvent | undefined\n  renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n    receivedKey = key\n  })\n\n  mockInput.pressKey(\"x\")\n\n  expect(receivedKey).toBeInstanceOf(KeyEvent)\n  expect(receivedKey?.source).toBe(\"raw\")\n  expect(receivedKey?.name).toBe(\"x\")\n\n  // Verify it implements ParsedKey interface\n  const parsedKey: typeof receivedKey = receivedKey\n  expect(parsedKey?.source).toBe(\"raw\")\n\n  renderer.keyInput.removeAllListeners(\"keypress\")\n})\n\ntest(\"KeyHandler - global handler error is caught and logged\", () => {\n  const handler = createKeyHandler()\n\n  let handlerCalled = false\n  let errorThrown = false\n\n  // Handler throws an error\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    handlerCalled = true\n    errorThrown = true\n    throw new Error(\"Test error in global handler\")\n  })\n\n  // Should not throw - error is caught and logged\n  expect(() => dispatchInput(handler, \"a\")).not.toThrow()\n\n  expect(handlerCalled).toBe(true)\n  expect(errorThrown).toBe(true)\n})\n\ntest(\"KeyHandler - renderable handler error does not stop processing\", () => {\n  const handler = createKeyHandler()\n\n  let firstInternalCalled = false\n  let secondInternalCalled = false\n  let errorThrown = false\n\n  // First internal handler throws\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    firstInternalCalled = true\n    errorThrown = true\n    throw new Error(\"Test error in internal handler\")\n  })\n\n  // Second internal handler should still be called\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    secondInternalCalled = true\n  })\n\n  // Should not throw\n  expect(() => dispatchInput(handler, \"a\")).not.toThrow()\n\n  expect(firstInternalCalled).toBe(true)\n  expect(errorThrown).toBe(true)\n  expect(secondInternalCalled).toBe(true)\n})\n\ntest(\"KeyHandler - global handler error stops further global handlers but allows internal handlers\", () => {\n  const handler = createKeyHandler()\n\n  let globalCalled = false\n  let internalCalled = false\n\n  // Global handler throws - stops further global handlers\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    globalCalled = true\n    throw new Error(\"Global handler error\")\n  })\n\n  // Internal handler should still be called (different priority level)\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    internalCalled = true\n  })\n\n  // Should not throw - errors are caught\n  expect(() => dispatchInput(handler, \"a\")).not.toThrow()\n\n  expect(globalCalled).toBe(true)\n  expect(internalCalled).toBe(true)\n})\n\ntest(\"KeyHandler - paste handler error is caught and logged\", () => {\n  const handler = createKeyHandler()\n\n  let handlerCalled = false\n\n  // Paste handler throws\n  handler.on(\"paste\", (event) => {\n    handlerCalled = true\n    throw new Error(\"Test error in paste handler\")\n  })\n\n  // Should not throw - error is caught and logged\n  expect(() => handler.processPaste(pasteBytes(\"test\"))).not.toThrow()\n\n  expect(handlerCalled).toBe(true)\n})\n\ntest(\"KeyHandler - processParsedKey returns true even when handler throws\", () => {\n  const handler = createKeyHandler()\n\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    throw new Error(\"Handler error\")\n  })\n\n  // Should return true indicating the input was handled (even if handler errored)\n  const result = dispatchInput(handler, \"a\")\n  expect(result).toBe(true)\n})\n\ntest(\"KeyHandler - internal handler error with preventDefault still respects prevention\", () => {\n  const handler = createKeyHandler()\n\n  let globalCalled = false\n  let internalCalled = false\n\n  // Global handler prevents default\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    globalCalled = true\n    key.preventDefault()\n  })\n\n  // Internal handler should not be called (due to preventDefault)\n  handler.onInternal(\"keypress\", (key: KeyEvent) => {\n    internalCalled = true\n    throw new Error(\"Should not reach here\")\n  })\n\n  dispatchInput(handler, \"a\")\n\n  expect(globalCalled).toBe(true)\n  expect(internalCalled).toBe(false)\n})\n\ntest(\"KeyHandler - error in one event type does not prevent other event types from working\", () => {\n  const handler = createKeyHandler()\n\n  let keypressCalled = false\n  let pasteCalled = false\n\n  // Keypress handler throws\n  handler.on(\"keypress\", (key: KeyEvent) => {\n    keypressCalled = true\n    throw new Error(\"Keypress error\")\n  })\n\n  // Paste handler should work fine\n  handler.on(\"paste\", (event) => {\n    pasteCalled = true\n  })\n\n  // Both should not throw - errors are caught and logged\n  expect(() => dispatchInput(handler, \"a\")).not.toThrow()\n  expect(() => handler.processPaste(pasteBytes(\"test\"))).not.toThrow()\n\n  expect(keypressCalled).toBe(true)\n  expect(pasteCalled).toBe(true)\n})\n"
  },
  {
    "path": "packages/core/src/lib/KeyHandler.ts",
    "content": "import { EventEmitter } from \"events\"\nimport { type KeyEventType, type ParsedKey } from \"./parse.keypress.js\"\nimport type { PasteMetadata } from \"./paste.js\"\n\nexport class KeyEvent implements ParsedKey {\n  name: string\n  ctrl: boolean\n  meta: boolean\n  shift: boolean\n  option: boolean\n  sequence: string\n  number: boolean\n  raw: string\n  eventType: KeyEventType\n  source: \"raw\" | \"kitty\"\n  code?: string\n  super?: boolean\n  hyper?: boolean\n  capsLock?: boolean\n  numLock?: boolean\n  baseCode?: number\n  repeated?: boolean\n\n  private _defaultPrevented: boolean = false\n  private _propagationStopped: boolean = false\n\n  constructor(key: ParsedKey) {\n    this.name = key.name\n    this.ctrl = key.ctrl\n    this.meta = key.meta\n    this.shift = key.shift\n    this.option = key.option\n    this.sequence = key.sequence\n    this.number = key.number\n    this.raw = key.raw\n    this.eventType = key.eventType\n    this.source = key.source\n    this.code = key.code\n    this.super = key.super\n    this.hyper = key.hyper\n    this.capsLock = key.capsLock\n    this.numLock = key.numLock\n    this.baseCode = key.baseCode\n    this.repeated = key.repeated\n  }\n\n  get defaultPrevented(): boolean {\n    return this._defaultPrevented\n  }\n\n  get propagationStopped(): boolean {\n    return this._propagationStopped\n  }\n\n  preventDefault(): void {\n    this._defaultPrevented = true\n  }\n\n  stopPropagation(): void {\n    this._propagationStopped = true\n  }\n}\n\nexport class PasteEvent {\n  type: \"paste\" = \"paste\"\n  bytes: Uint8Array\n  metadata?: PasteMetadata\n  private _defaultPrevented: boolean = false\n  private _propagationStopped: boolean = false\n\n  constructor(bytes: Uint8Array, metadata?: PasteMetadata) {\n    this.bytes = bytes\n    this.metadata = metadata\n  }\n\n  get defaultPrevented(): boolean {\n    return this._defaultPrevented\n  }\n\n  get propagationStopped(): boolean {\n    return this._propagationStopped\n  }\n\n  preventDefault(): void {\n    this._defaultPrevented = true\n  }\n\n  stopPropagation(): void {\n    this._propagationStopped = true\n  }\n}\n\nexport type KeyHandlerEventMap = {\n  keypress: [KeyEvent]\n  keyrelease: [KeyEvent]\n  paste: [PasteEvent]\n}\n\nexport class KeyHandler extends EventEmitter<KeyHandlerEventMap> {\n  public processParsedKey(parsedKey: ParsedKey): boolean {\n    try {\n      switch (parsedKey.eventType) {\n        case \"press\":\n          this.emit(\"keypress\", new KeyEvent(parsedKey))\n          break\n        case \"release\":\n          this.emit(\"keyrelease\", new KeyEvent(parsedKey))\n          break\n        default:\n          this.emit(\"keypress\", new KeyEvent(parsedKey))\n          break\n      }\n    } catch (error) {\n      console.error(`[KeyHandler] Error processing parsed key:`, error)\n      return true\n    }\n\n    return true\n  }\n\n  public processPaste(bytes: Uint8Array, metadata?: PasteMetadata): void {\n    try {\n      this.emit(\"paste\", new PasteEvent(bytes, metadata))\n    } catch (error) {\n      console.error(`[KeyHandler] Error processing paste:`, error)\n    }\n  }\n}\n\n/**\n * This class is used internally by the renderer to ensure global handlers\n * can preventDefault before renderable handlers process events.\n */\nexport class InternalKeyHandler extends KeyHandler {\n  private renderableHandlers: Map<keyof KeyHandlerEventMap, Set<Function>> = new Map()\n\n  public emit<K extends keyof KeyHandlerEventMap>(event: K, ...args: KeyHandlerEventMap[K]): boolean {\n    return this.emitWithPriority(event, ...args)\n  }\n\n  private emitWithPriority<K extends keyof KeyHandlerEventMap>(event: K, ...args: KeyHandlerEventMap[K]): boolean {\n    let hasGlobalListeners = false\n\n    // Check if we should emit to global handlers\n    // Global handlers are emitted using the parent EventEmitter which calls all listeners\n    // We need to manually iterate to check for stopPropagation between handlers\n    const globalListeners = this.listeners(event as any)\n    if (globalListeners.length > 0) {\n      hasGlobalListeners = true\n\n      for (const listener of globalListeners) {\n        try {\n          listener(...args)\n        } catch (error) {\n          console.error(`[KeyHandler] Error in global ${event} handler:`, error)\n        }\n\n        // Check if propagation was stopped after this handler\n        if (event === \"keypress\" || event === \"keyrelease\" || event === \"paste\") {\n          const keyEvent = args[0]\n          if (keyEvent.propagationStopped) {\n            return hasGlobalListeners\n          }\n        }\n      }\n    }\n\n    const renderableSet = this.renderableHandlers.get(event)\n    // Snapshot the handler list so listeners added during dispatch (e.g., via focus changes)\n    // do not receive the in-flight key event.\n    const renderableHandlers = renderableSet && renderableSet.size > 0 ? [...renderableSet] : []\n    let hasRenderableListeners = false\n\n    if (renderableSet && renderableSet.size > 0) {\n      hasRenderableListeners = true\n\n      if (event === \"keypress\" || event === \"keyrelease\" || event === \"paste\") {\n        const keyEvent = args[0]\n        if (keyEvent.defaultPrevented) return hasGlobalListeners || hasRenderableListeners\n        if (keyEvent.propagationStopped) return hasGlobalListeners || hasRenderableListeners\n      }\n\n      for (const handler of renderableHandlers) {\n        try {\n          handler(...args)\n        } catch (error) {\n          console.error(`[KeyHandler] Error in renderable ${event} handler:`, error)\n        }\n\n        // Check if propagation was stopped after this handler\n        if (event === \"keypress\" || event === \"keyrelease\" || event === \"paste\") {\n          const keyEvent = args[0]\n          if (keyEvent.propagationStopped) {\n            return hasGlobalListeners || hasRenderableListeners\n          }\n        }\n      }\n    }\n\n    return hasGlobalListeners || hasRenderableListeners\n  }\n\n  public onInternal<K extends keyof KeyHandlerEventMap>(\n    event: K,\n    handler: (...args: KeyHandlerEventMap[K]) => void,\n  ): void {\n    if (!this.renderableHandlers.has(event)) {\n      this.renderableHandlers.set(event, new Set())\n    }\n    this.renderableHandlers.get(event)!.add(handler)\n  }\n\n  public offInternal<K extends keyof KeyHandlerEventMap>(\n    event: K,\n    handler: (...args: KeyHandlerEventMap[K]) => void,\n  ): void {\n    const handlers = this.renderableHandlers.get(event)\n    if (handlers) {\n      handlers.delete(handler)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/RGBA.test.ts",
    "content": "import { test, expect, describe } from \"bun:test\"\nimport { RGBA, hexToRgb, rgbToHex, hsvToRgb, parseColor } from \"./RGBA.js\"\n\ndescribe(\"RGBA class\", () => {\n  describe(\"constructor\", () => {\n    test(\"creates RGBA with Float32Array buffer\", () => {\n      const buffer = new Float32Array([0.5, 0.6, 0.7, 0.8])\n      const rgba = new RGBA(buffer)\n      expect(rgba.buffer).toBe(buffer)\n    })\n\n    test(\"buffer is mutable reference\", () => {\n      const buffer = new Float32Array([0.5, 0.6, 0.7, 0.8])\n      const rgba = new RGBA(buffer)\n      buffer[0] = 0.9\n      expect(rgba.r).toBeCloseTo(0.9, 5)\n    })\n  })\n\n  describe(\"fromArray\", () => {\n    test(\"creates RGBA from Float32Array\", () => {\n      const array = new Float32Array([0.1, 0.2, 0.3, 0.4])\n      const rgba = RGBA.fromArray(array)\n      expect(rgba.r).toBeCloseTo(0.1, 5)\n      expect(rgba.g).toBeCloseTo(0.2, 5)\n      expect(rgba.b).toBeCloseTo(0.3, 5)\n      expect(rgba.a).toBeCloseTo(0.4, 5)\n    })\n\n    test(\"uses same buffer reference\", () => {\n      const array = new Float32Array([0.1, 0.2, 0.3, 0.4])\n      const rgba = RGBA.fromArray(array)\n      expect(rgba.buffer).toBe(array)\n    })\n  })\n\n  describe(\"fromValues\", () => {\n    test(\"creates RGBA from individual values\", () => {\n      const rgba = RGBA.fromValues(0.2, 0.4, 0.6, 0.8)\n      expect(rgba.r).toBeCloseTo(0.2, 5)\n      expect(rgba.g).toBeCloseTo(0.4, 5)\n      expect(rgba.b).toBeCloseTo(0.6, 5)\n      expect(rgba.a).toBeCloseTo(0.8, 5)\n    })\n\n    test(\"defaults alpha to 1.0 when not provided\", () => {\n      const rgba = RGBA.fromValues(0.5, 0.5, 0.5)\n      expect(rgba.a).toBe(1.0)\n    })\n\n    test(\"handles zero values\", () => {\n      const rgba = RGBA.fromValues(0, 0, 0, 0)\n      expect(rgba.r).toBe(0)\n      expect(rgba.g).toBe(0)\n      expect(rgba.b).toBe(0)\n      expect(rgba.a).toBe(0)\n    })\n\n    test(\"handles values greater than 1\", () => {\n      const rgba = RGBA.fromValues(1.5, 2.0, 2.5, 3.0)\n      expect(rgba.r).toBe(1.5)\n      expect(rgba.g).toBe(2.0)\n      expect(rgba.b).toBe(2.5)\n      expect(rgba.a).toBe(3.0)\n    })\n\n    test(\"handles negative values\", () => {\n      const rgba = RGBA.fromValues(-0.5, -0.2, -0.1, -0.3)\n      expect(rgba.r).toBeCloseTo(-0.5, 5)\n      expect(rgba.g).toBeCloseTo(-0.2, 5)\n      expect(rgba.b).toBeCloseTo(-0.1, 5)\n      expect(rgba.a).toBeCloseTo(-0.3, 5)\n    })\n  })\n\n  describe(\"fromInts\", () => {\n    test(\"creates RGBA from integer values (0-255)\", () => {\n      const rgba = RGBA.fromInts(255, 128, 64, 255)\n      expect(rgba.r).toBeCloseTo(1.0, 2)\n      expect(rgba.g).toBeCloseTo(0.502, 2)\n      expect(rgba.b).toBeCloseTo(0.251, 2)\n      expect(rgba.a).toBeCloseTo(1.0, 2)\n    })\n\n    test(\"defaults alpha to 255 when not provided\", () => {\n      const rgba = RGBA.fromInts(100, 150, 200)\n      expect(rgba.a).toBeCloseTo(1.0, 2)\n    })\n\n    test(\"handles zero values\", () => {\n      const rgba = RGBA.fromInts(0, 0, 0, 0)\n      expect(rgba.r).toBe(0)\n      expect(rgba.g).toBe(0)\n      expect(rgba.b).toBe(0)\n      expect(rgba.a).toBe(0)\n    })\n\n    test(\"handles max values (255)\", () => {\n      const rgba = RGBA.fromInts(255, 255, 255, 255)\n      expect(rgba.r).toBeCloseTo(1.0, 2)\n      expect(rgba.g).toBeCloseTo(1.0, 2)\n      expect(rgba.b).toBeCloseTo(1.0, 2)\n      expect(rgba.a).toBeCloseTo(1.0, 2)\n    })\n\n    test(\"converts mid-range values correctly\", () => {\n      const rgba = RGBA.fromInts(127, 127, 127, 127)\n      expect(rgba.r).toBeCloseTo(0.498, 2)\n      expect(rgba.g).toBeCloseTo(0.498, 2)\n      expect(rgba.b).toBeCloseTo(0.498, 2)\n      expect(rgba.a).toBeCloseTo(0.498, 2)\n    })\n\n    test(\"handles values greater than 255\", () => {\n      const rgba = RGBA.fromInts(300, 400, 500, 600)\n      expect(rgba.r).toBeCloseTo(1.176, 2)\n      expect(rgba.g).toBeCloseTo(1.569, 2)\n      expect(rgba.b).toBeCloseTo(1.961, 2)\n      expect(rgba.a).toBeCloseTo(2.353, 2)\n    })\n  })\n\n  describe(\"fromHex\", () => {\n    test(\"creates RGBA from hex string\", () => {\n      const rgba = RGBA.fromHex(\"#FF8040\")\n      expect(rgba.r).toBeCloseTo(1.0, 2)\n      expect(rgba.g).toBeCloseTo(0.502, 2)\n      expect(rgba.b).toBeCloseTo(0.251, 2)\n      expect(rgba.a).toBe(1)\n    })\n\n    test(\"creates RGBA from 8-digit hex with alpha\", () => {\n      const rgba = RGBA.fromHex(\"#FF804080\")\n      expect(rgba.r).toBeCloseTo(1.0, 2)\n      expect(rgba.g).toBeCloseTo(0.502, 2)\n      expect(rgba.b).toBeCloseTo(0.251, 2)\n      expect(rgba.a).toBeCloseTo(0.502, 2)\n    })\n\n    test(\"creates RGBA from 4-digit hex with alpha\", () => {\n      const rgba = RGBA.fromHex(\"#F808\")\n      expect(rgba.r).toBeCloseTo(1.0, 2)\n      expect(rgba.g).toBeCloseTo(0.533, 2)\n      expect(rgba.b).toBeCloseTo(0.0, 2)\n      expect(rgba.a).toBeCloseTo(0.533, 2)\n    })\n  })\n\n  describe(\"toInts\", () => {\n    test(\"converts float values to integers (0-255)\", () => {\n      const rgba = RGBA.fromValues(1.0, 0.5, 0.25, 0.75)\n      const ints = rgba.toInts()\n      expect(ints).toEqual([255, 128, 64, 191])\n    })\n\n    test(\"handles zero values\", () => {\n      const rgba = RGBA.fromValues(0, 0, 0, 0)\n      const ints = rgba.toInts()\n      expect(ints).toEqual([0, 0, 0, 0])\n    })\n\n    test(\"rounds to nearest integer\", () => {\n      const rgba = RGBA.fromValues(0.501, 0.499, 0.5, 1.0)\n      const ints = rgba.toInts()\n      expect(ints).toEqual([128, 127, 128, 255])\n    })\n\n    test(\"handles out of range values when converting\", () => {\n      const rgba = RGBA.fromValues(1.5, -0.5, 2.0, 0.5)\n      const ints = rgba.toInts()\n      expect(ints[0]).toBe(383)\n      expect(ints[1]).toBe(-127)\n      expect(ints[2]).toBe(510)\n      expect(ints[3]).toBe(128)\n    })\n  })\n\n  describe(\"getters\", () => {\n    test(\"r getter returns red value\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      expect(rgba.r).toBeCloseTo(0.1, 5)\n    })\n\n    test(\"g getter returns green value\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      expect(rgba.g).toBeCloseTo(0.2, 5)\n    })\n\n    test(\"b getter returns blue value\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      expect(rgba.b).toBeCloseTo(0.3, 5)\n    })\n\n    test(\"a getter returns alpha value\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      expect(rgba.a).toBeCloseTo(0.4, 5)\n    })\n  })\n\n  describe(\"setters\", () => {\n    test(\"r setter updates red value\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      rgba.r = 0.9\n      expect(rgba.r).toBeCloseTo(0.9, 5)\n      expect(rgba.buffer[0]).toBeCloseTo(0.9, 5)\n    })\n\n    test(\"g setter updates green value\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      rgba.g = 0.9\n      expect(rgba.g).toBeCloseTo(0.9, 5)\n      expect(rgba.buffer[1]).toBeCloseTo(0.9, 5)\n    })\n\n    test(\"b setter updates blue value\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      rgba.b = 0.9\n      expect(rgba.b).toBeCloseTo(0.9, 5)\n      expect(rgba.buffer[2]).toBeCloseTo(0.9, 5)\n    })\n\n    test(\"a setter updates alpha value\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      rgba.a = 0.9\n      expect(rgba.a).toBeCloseTo(0.9, 5)\n      expect(rgba.buffer[3]).toBeCloseTo(0.9, 5)\n    })\n\n    test(\"setters modify underlying buffer\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      rgba.r = 0.5\n      rgba.g = 0.6\n      rgba.b = 0.7\n      rgba.a = 0.8\n      expect(rgba.buffer[0]).toBeCloseTo(0.5, 5)\n      expect(rgba.buffer[1]).toBeCloseTo(0.6, 5)\n      expect(rgba.buffer[2]).toBeCloseTo(0.7, 5)\n      expect(rgba.buffer[3]).toBeCloseTo(0.8, 5)\n    })\n  })\n\n  describe(\"map\", () => {\n    test(\"applies function to all components\", () => {\n      const rgba = RGBA.fromValues(0.5, 0.6, 0.7, 0.8)\n      const result = rgba.map((x) => x * 2)\n      expect(result[0]).toBeCloseTo(1.0, 5)\n      expect(result[1]).toBeCloseTo(1.2, 5)\n      expect(result[2]).toBeCloseTo(1.4, 5)\n      expect(result[3]).toBeCloseTo(1.6, 5)\n    })\n\n    test(\"can return different types\", () => {\n      const rgba = RGBA.fromValues(0.1, 0.2, 0.3, 0.4)\n      const result = rgba.map((x) => Math.round(x * 255).toString())\n      expect(result).toEqual([\"26\", \"51\", \"77\", \"102\"])\n    })\n\n    test(\"works with identity function\", () => {\n      const rgba = RGBA.fromValues(0.5, 0.6, 0.7, 0.8)\n      const result = rgba.map((x) => x)\n      expect(result[0]).toBeCloseTo(0.5, 5)\n      expect(result[1]).toBeCloseTo(0.6, 5)\n      expect(result[2]).toBeCloseTo(0.7, 5)\n      expect(result[3]).toBeCloseTo(0.8, 5)\n    })\n\n    test(\"returns array in correct order (r, g, b, a)\", () => {\n      const rgba = RGBA.fromValues(1, 2, 3, 4)\n      const result = rgba.map((x) => x)\n      expect(result[0]).toBe(1)\n      expect(result[1]).toBe(2)\n      expect(result[2]).toBe(3)\n      expect(result[3]).toBe(4)\n    })\n  })\n\n  describe(\"toString\", () => {\n    test(\"formats as rgba string with 2 decimal places\", () => {\n      const rgba = RGBA.fromValues(0.5, 0.6, 0.7, 0.8)\n      expect(rgba.toString()).toBe(\"rgba(0.50, 0.60, 0.70, 0.80)\")\n    })\n\n    test(\"handles zero values\", () => {\n      const rgba = RGBA.fromValues(0, 0, 0, 0)\n      expect(rgba.toString()).toBe(\"rgba(0.00, 0.00, 0.00, 0.00)\")\n    })\n\n    test(\"handles max values\", () => {\n      const rgba = RGBA.fromValues(1, 1, 1, 1)\n      expect(rgba.toString()).toBe(\"rgba(1.00, 1.00, 1.00, 1.00)\")\n    })\n\n    test(\"rounds to 2 decimal places\", () => {\n      const rgba = RGBA.fromValues(0.12345, 0.6789, 0.11111, 0.99999)\n      expect(rgba.toString()).toBe(\"rgba(0.12, 0.68, 0.11, 1.00)\")\n    })\n\n    test(\"handles negative values\", () => {\n      const rgba = RGBA.fromValues(-0.5, -0.2, -0.1, -0.3)\n      expect(rgba.toString()).toBe(\"rgba(-0.50, -0.20, -0.10, -0.30)\")\n    })\n\n    test(\"handles values greater than 1\", () => {\n      const rgba = RGBA.fromValues(1.5, 2.0, 2.5, 3.0)\n      expect(rgba.toString()).toBe(\"rgba(1.50, 2.00, 2.50, 3.00)\")\n    })\n  })\n})\n\ndescribe(\"hexToRgb\", () => {\n  test(\"converts 6-digit hex with # prefix\", () => {\n    const rgba = hexToRgb(\"#FF8040\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts 6-digit hex without # prefix\", () => {\n    const rgba = hexToRgb(\"FF8040\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"expands 3-digit hex to 6-digit\", () => {\n    const rgba = hexToRgb(\"#F80\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.533, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"expands 3-digit hex without # prefix\", () => {\n    const rgba = hexToRgb(\"F80\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.533, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"handles lowercase hex\", () => {\n    const rgba = hexToRgb(\"#ff8040\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"handles mixed case hex\", () => {\n    const rgba = hexToRgb(\"#Ff8040\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts black (#000000)\", () => {\n    const rgba = hexToRgb(\"#000000\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts white (#FFFFFF)\", () => {\n    const rgba = hexToRgb(\"#FFFFFF\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts red (#FF0000)\", () => {\n    const rgba = hexToRgb(\"#FF0000\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts green (#00FF00)\", () => {\n    const rgba = hexToRgb(\"#00FF00\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts blue (#0000FF)\", () => {\n    const rgba = hexToRgb(\"#0000FF\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"returns magenta for invalid hex\", () => {\n    const rgba = hexToRgb(\"GGGGGG\")\n    expect(rgba.r).toBe(1)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(1)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"returns magenta for too short hex\", () => {\n    const rgba = hexToRgb(\"FF\")\n    expect(rgba.r).toBe(1)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(1)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"returns magenta for too long hex\", () => {\n    const rgba = hexToRgb(\"FF80401234\")\n    expect(rgba.r).toBe(1)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(1)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"returns magenta for empty string\", () => {\n    const rgba = hexToRgb(\"\")\n    expect(rgba.r).toBe(1)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(1)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"returns magenta for special characters\", () => {\n    const rgba = hexToRgb(\"#FF@040\")\n    expect(rgba.r).toBe(1)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(1)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts 8-digit hex with alpha channel\", () => {\n    const rgba = hexToRgb(\"#FF804080\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBeCloseTo(0.502, 2)\n  })\n\n  test(\"converts 8-digit hex without # prefix\", () => {\n    const rgba = hexToRgb(\"FF804080\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBeCloseTo(0.502, 2)\n  })\n\n  test(\"converts 4-digit hex with alpha channel\", () => {\n    const rgba = hexToRgb(\"#F808\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.533, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBeCloseTo(0.533, 2)\n  })\n\n  test(\"converts 4-digit hex without # prefix\", () => {\n    const rgba = hexToRgb(\"F808\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.533, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBeCloseTo(0.533, 2)\n  })\n\n  test(\"converts 8-digit hex with full alpha (FF)\", () => {\n    const rgba = hexToRgb(\"#FF8040FF\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBeCloseTo(1.0, 2)\n  })\n\n  test(\"converts 8-digit hex with zero alpha\", () => {\n    const rgba = hexToRgb(\"#FF804000\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBe(0)\n  })\n\n  test(\"converts 4-digit hex with full alpha (F)\", () => {\n    const rgba = hexToRgb(\"#F80F\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.533, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBeCloseTo(1.0, 2)\n  })\n\n  test(\"converts 4-digit hex with zero alpha\", () => {\n    const rgba = hexToRgb(\"#F800\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.533, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBe(0)\n  })\n})\n\ndescribe(\"rgbToHex\", () => {\n  test(\"converts RGBA to hex string\", () => {\n    const rgba = RGBA.fromInts(255, 128, 64, 255)\n    expect(rgbToHex(rgba)).toBe(\"#ff8040\")\n  })\n\n  test(\"converts black to #000000\", () => {\n    const rgba = RGBA.fromValues(0, 0, 0, 1)\n    expect(rgbToHex(rgba)).toBe(\"#000000\")\n  })\n\n  test(\"converts white to #ffffff\", () => {\n    const rgba = RGBA.fromValues(1, 1, 1, 1)\n    expect(rgbToHex(rgba)).toBe(\"#ffffff\")\n  })\n\n  test(\"converts red to #ff0000\", () => {\n    const rgba = RGBA.fromValues(1, 0, 0, 1)\n    expect(rgbToHex(rgba)).toBe(\"#ff0000\")\n  })\n\n  test(\"converts green to #00ff00\", () => {\n    const rgba = RGBA.fromValues(0, 1, 0, 1)\n    expect(rgbToHex(rgba)).toBe(\"#00ff00\")\n  })\n\n  test(\"converts blue to #0000ff\", () => {\n    const rgba = RGBA.fromValues(0, 0, 1, 1)\n    expect(rgbToHex(rgba)).toBe(\"#0000ff\")\n  })\n\n  test(\"includes alpha channel when not fully opaque\", () => {\n    const rgba = RGBA.fromInts(255, 128, 64, 128)\n    expect(rgbToHex(rgba)).toBe(\"#ff804080\")\n  })\n\n  test(\"clamps values below 0 to 0\", () => {\n    const rgba = RGBA.fromValues(-0.5, -0.2, -0.1, 1)\n    expect(rgbToHex(rgba)).toBe(\"#000000\")\n  })\n\n  test(\"clamps values above 1 to 1\", () => {\n    const rgba = RGBA.fromValues(1.5, 2.0, 3.0, 1)\n    expect(rgbToHex(rgba)).toBe(\"#ffffff\")\n  })\n\n  test(\"rounds mid-range values correctly\", () => {\n    const rgba = RGBA.fromInts(127, 127, 127, 255)\n    expect(rgbToHex(rgba)).toBe(\"#7f7f7f\")\n  })\n\n  test(\"pads single digit hex with leading zero\", () => {\n    const rgba = RGBA.fromValues(0.02, 0.02, 0.02, 1)\n    expect(rgbToHex(rgba)).toBe(\"#050505\")\n  })\n\n  test(\"converts gray values correctly\", () => {\n    const rgba = RGBA.fromValues(0.5, 0.5, 0.5, 1)\n    expect(rgbToHex(rgba)).toBe(\"#7f7f7f\")\n  })\n\n  test(\"includes alpha channel when alpha is not 1.0\", () => {\n    const rgba = RGBA.fromInts(255, 128, 64, 128)\n    expect(rgbToHex(rgba)).toBe(\"#ff804080\")\n  })\n\n  test(\"excludes alpha channel when alpha is 1.0\", () => {\n    const rgba = RGBA.fromInts(255, 128, 64, 255)\n    expect(rgbToHex(rgba)).toBe(\"#ff8040\")\n  })\n\n  test(\"includes alpha channel for transparent color\", () => {\n    const rgba = RGBA.fromValues(1, 0, 0, 0)\n    expect(rgbToHex(rgba)).toBe(\"#ff000000\")\n  })\n\n  test(\"includes alpha channel for semi-transparent\", () => {\n    const rgba = RGBA.fromValues(0, 1, 0, 0.5)\n    expect(rgbToHex(rgba)).toBe(\"#00ff007f\")\n  })\n\n  test(\"excludes alpha for fully opaque black\", () => {\n    const rgba = RGBA.fromValues(0, 0, 0, 1)\n    expect(rgbToHex(rgba)).toBe(\"#000000\")\n  })\n})\n\ndescribe(\"hsvToRgb\", () => {\n  test(\"converts HSV to RGB (red)\", () => {\n    const rgba = hsvToRgb(0, 1, 1)\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.0, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV to RGB (green)\", () => {\n    const rgba = hsvToRgb(120, 1, 1)\n    expect(rgba.r).toBeCloseTo(0.0, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV to RGB (blue)\", () => {\n    const rgba = hsvToRgb(240, 1, 1)\n    expect(rgba.r).toBeCloseTo(0.0, 2)\n    expect(rgba.g).toBeCloseTo(0.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV to RGB (yellow)\", () => {\n    const rgba = hsvToRgb(60, 1, 1)\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV to RGB (cyan)\", () => {\n    const rgba = hsvToRgb(180, 1, 1)\n    expect(rgba.r).toBeCloseTo(0.0, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV to RGB (magenta)\", () => {\n    const rgba = hsvToRgb(300, 1, 1)\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV with zero saturation to gray\", () => {\n    const rgba = hsvToRgb(180, 0, 0.5)\n    expect(rgba.r).toBeCloseTo(0.5, 2)\n    expect(rgba.g).toBeCloseTo(0.5, 2)\n    expect(rgba.b).toBeCloseTo(0.5, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV with zero value to black\", () => {\n    const rgba = hsvToRgb(180, 1, 0)\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV to RGB (orange)\", () => {\n    const rgba = hsvToRgb(30, 1, 1)\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.5, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV with partial saturation\", () => {\n    const rgba = hsvToRgb(0, 0.5, 1)\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.5, 2)\n    expect(rgba.b).toBeCloseTo(0.5, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"converts HSV with partial value\", () => {\n    const rgba = hsvToRgb(0, 1, 0.5)\n    expect(rgba.r).toBeCloseTo(0.5, 2)\n    expect(rgba.g).toBeCloseTo(0.0, 2)\n    expect(rgba.b).toBeCloseTo(0.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"handles hue > 360 (wraps around)\", () => {\n    const rgba1 = hsvToRgb(0, 1, 1)\n    const rgba2 = hsvToRgb(360, 1, 1)\n    expect(rgba1.r).toBeCloseTo(rgba2.r, 2)\n    expect(rgba1.g).toBeCloseTo(rgba2.g, 2)\n    expect(rgba1.b).toBeCloseTo(rgba2.b, 2)\n  })\n\n  test(\"handles hue = 359\", () => {\n    const rgba = hsvToRgb(359, 1, 1)\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.0, 2)\n    expect(rgba.b).toBeCloseTo(0.0166, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"always sets alpha to 1\", () => {\n    const rgba1 = hsvToRgb(0, 0, 0)\n    const rgba2 = hsvToRgb(180, 0.5, 0.5)\n    const rgba3 = hsvToRgb(360, 1, 1)\n    expect(rgba1.a).toBe(1)\n    expect(rgba2.a).toBe(1)\n    expect(rgba3.a).toBe(1)\n  })\n})\n\ndescribe(\"parseColor\", () => {\n  test(\"parses RGBA object directly\", () => {\n    const input = RGBA.fromValues(0.5, 0.6, 0.7, 0.8)\n    const result = parseColor(input)\n    expect(result).toBe(input)\n  })\n\n  test(\"parses hex string\", () => {\n    const rgba = parseColor(\"#FF8040\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.251, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses transparent keyword\", () => {\n    const rgba = parseColor(\"transparent\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(0)\n  })\n\n  test(\"parses TRANSPARENT (uppercase)\", () => {\n    const rgba = parseColor(\"TRANSPARENT\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(0)\n  })\n\n  test(\"parses black color name\", () => {\n    const rgba = parseColor(\"black\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses white color name\", () => {\n    const rgba = parseColor(\"white\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses red color name\", () => {\n    const rgba = parseColor(\"red\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses green color name\", () => {\n    const rgba = parseColor(\"green\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses blue color name\", () => {\n    const rgba = parseColor(\"blue\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses yellow color name\", () => {\n    const rgba = parseColor(\"yellow\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses cyan color name\", () => {\n    const rgba = parseColor(\"cyan\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses magenta color name\", () => {\n    const rgba = parseColor(\"magenta\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses silver color name\", () => {\n    const rgba = parseColor(\"silver\")\n    expect(rgba.r).toBeCloseTo(0.753, 2)\n    expect(rgba.g).toBeCloseTo(0.753, 2)\n    expect(rgba.b).toBeCloseTo(0.753, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses gray color name\", () => {\n    const rgba = parseColor(\"gray\")\n    expect(rgba.r).toBeCloseTo(0.502, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.502, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses grey color name (alternate spelling)\", () => {\n    const rgba = parseColor(\"grey\")\n    expect(rgba.r).toBeCloseTo(0.502, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.502, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses maroon color name\", () => {\n    const rgba = parseColor(\"maroon\")\n    expect(rgba.r).toBeCloseTo(0.502, 2)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses olive color name\", () => {\n    const rgba = parseColor(\"olive\")\n    expect(rgba.r).toBeCloseTo(0.502, 2)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses lime color name\", () => {\n    const rgba = parseColor(\"lime\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses aqua color name\", () => {\n    const rgba = parseColor(\"aqua\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses teal color name\", () => {\n    const rgba = parseColor(\"teal\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBeCloseTo(0.502, 2)\n    expect(rgba.b).toBeCloseTo(0.502, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses navy color name\", () => {\n    const rgba = parseColor(\"navy\")\n    expect(rgba.r).toBe(0)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBeCloseTo(0.502, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses fuchsia color name\", () => {\n    const rgba = parseColor(\"fuchsia\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses purple color name\", () => {\n    const rgba = parseColor(\"purple\")\n    expect(rgba.r).toBeCloseTo(0.502, 2)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBeCloseTo(0.502, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses orange color name\", () => {\n    const rgba = parseColor(\"orange\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.647, 2)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses brightblack color name\", () => {\n    const rgba = parseColor(\"brightblack\")\n    expect(rgba.r).toBeCloseTo(0.4, 2)\n    expect(rgba.g).toBeCloseTo(0.4, 2)\n    expect(rgba.b).toBeCloseTo(0.4, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses brightred color name\", () => {\n    const rgba = parseColor(\"brightred\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.4, 2)\n    expect(rgba.b).toBeCloseTo(0.4, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses brightgreen color name\", () => {\n    const rgba = parseColor(\"brightgreen\")\n    expect(rgba.r).toBeCloseTo(0.4, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(0.4, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses brightblue color name\", () => {\n    const rgba = parseColor(\"brightblue\")\n    expect(rgba.r).toBeCloseTo(0.4, 2)\n    expect(rgba.g).toBeCloseTo(0.4, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses brightyellow color name\", () => {\n    const rgba = parseColor(\"brightyellow\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(0.4, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses brightcyan color name\", () => {\n    const rgba = parseColor(\"brightcyan\")\n    expect(rgba.r).toBeCloseTo(0.4, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses brightmagenta color name\", () => {\n    const rgba = parseColor(\"brightmagenta\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.4, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"parses brightwhite color name\", () => {\n    const rgba = parseColor(\"brightwhite\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(1.0, 2)\n    expect(rgba.b).toBeCloseTo(1.0, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"handles uppercase color names\", () => {\n    const rgba = parseColor(\"RED\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(0)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"handles mixed case color names\", () => {\n    const rgba = parseColor(\"BrightRed\")\n    expect(rgba.r).toBeCloseTo(1.0, 2)\n    expect(rgba.g).toBeCloseTo(0.4, 2)\n    expect(rgba.b).toBeCloseTo(0.4, 2)\n    expect(rgba.a).toBe(1)\n  })\n\n  test(\"falls back to hex parser for unknown color names\", () => {\n    const rgba = parseColor(\"unknowncolor\")\n    expect(rgba.r).toBe(1)\n    expect(rgba.g).toBe(0)\n    expect(rgba.b).toBe(1)\n    expect(rgba.a).toBe(1)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/RGBA.ts",
    "content": "export class RGBA {\n  buffer: Float32Array\n\n  constructor(buffer: Float32Array) {\n    this.buffer = buffer\n  }\n\n  static fromArray(array: Float32Array) {\n    return new RGBA(array)\n  }\n\n  static fromValues(r: number, g: number, b: number, a: number = 1.0) {\n    return new RGBA(new Float32Array([r, g, b, a]))\n  }\n\n  static fromInts(r: number, g: number, b: number, a: number = 255) {\n    return new RGBA(new Float32Array([r / 255, g / 255, b / 255, a / 255]))\n  }\n\n  static fromHex(hex: string): RGBA {\n    return hexToRgb(hex)\n  }\n\n  toInts(): [number, number, number, number] {\n    return [Math.round(this.r * 255), Math.round(this.g * 255), Math.round(this.b * 255), Math.round(this.a * 255)]\n  }\n\n  get r(): number {\n    return this.buffer[0]\n  }\n\n  set r(value: number) {\n    this.buffer[0] = value\n  }\n\n  get g(): number {\n    return this.buffer[1]\n  }\n\n  set g(value: number) {\n    this.buffer[1] = value\n  }\n\n  get b(): number {\n    return this.buffer[2]\n  }\n\n  set b(value: number) {\n    this.buffer[2] = value\n  }\n\n  get a(): number {\n    return this.buffer[3]\n  }\n\n  set a(value: number) {\n    this.buffer[3] = value\n  }\n\n  map<R>(fn: (value: number) => R) {\n    return [fn(this.r), fn(this.g), fn(this.b), fn(this.a)]\n  }\n\n  toString() {\n    return `rgba(${this.r.toFixed(2)}, ${this.g.toFixed(2)}, ${this.b.toFixed(2)}, ${this.a.toFixed(2)})`\n  }\n\n  equals(other?: RGBA): boolean {\n    if (!other) return false\n    return this.r === other.r && this.g === other.g && this.b === other.b && this.a === other.a\n  }\n}\n\nexport type ColorInput = string | RGBA\n\nexport function hexToRgb(hex: string): RGBA {\n  hex = hex.replace(/^#/, \"\")\n\n  if (hex.length === 3) {\n    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]\n  } else if (hex.length === 4) {\n    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]\n  }\n\n  if (!/^[0-9A-Fa-f]{6}$/.test(hex) && !/^[0-9A-Fa-f]{8}$/.test(hex)) {\n    console.warn(`Invalid hex color: ${hex}, defaulting to magenta`)\n    return RGBA.fromValues(1, 0, 1, 1)\n  }\n\n  const r = parseInt(hex.substring(0, 2), 16) / 255\n  const g = parseInt(hex.substring(2, 4), 16) / 255\n  const b = parseInt(hex.substring(4, 6), 16) / 255\n  const a = hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1\n\n  return RGBA.fromValues(r, g, b, a)\n}\n\nexport function rgbToHex(rgb: RGBA): string {\n  const components = rgb.a === 1 ? [rgb.r, rgb.g, rgb.b] : [rgb.r, rgb.g, rgb.b, rgb.a]\n  return (\n    \"#\" +\n    components\n      .map((x) => {\n        const hex = Math.floor(Math.max(0, Math.min(1, x) * 255)).toString(16)\n        return hex.length === 1 ? \"0\" + hex : hex\n      })\n      .join(\"\")\n  )\n}\n\nexport function hsvToRgb(h: number, s: number, v: number): RGBA {\n  let r = 0,\n    g = 0,\n    b = 0\n\n  const i = Math.floor(h / 60) % 6\n  const f = h / 60 - Math.floor(h / 60)\n  const p = v * (1 - s)\n  const q = v * (1 - f * s)\n  const t = v * (1 - (1 - f) * s)\n\n  switch (i) {\n    case 0:\n      r = v\n      g = t\n      b = p\n      break\n    case 1:\n      r = q\n      g = v\n      b = p\n      break\n    case 2:\n      r = p\n      g = v\n      b = t\n      break\n    case 3:\n      r = p\n      g = q\n      b = v\n      break\n    case 4:\n      r = t\n      g = p\n      b = v\n      break\n    case 5:\n      r = v\n      g = p\n      b = q\n      break\n  }\n\n  return RGBA.fromValues(r, g, b, 1)\n}\n\nconst CSS_COLOR_NAMES: Record<string, string> = {\n  black: \"#000000\",\n  white: \"#FFFFFF\",\n  red: \"#FF0000\",\n  green: \"#008000\",\n  blue: \"#0000FF\",\n  yellow: \"#FFFF00\",\n  cyan: \"#00FFFF\",\n  magenta: \"#FF00FF\",\n  silver: \"#C0C0C0\",\n  gray: \"#808080\",\n  grey: \"#808080\",\n  maroon: \"#800000\",\n  olive: \"#808000\",\n  lime: \"#00FF00\",\n  aqua: \"#00FFFF\",\n  teal: \"#008080\",\n  navy: \"#000080\",\n  fuchsia: \"#FF00FF\",\n  purple: \"#800080\",\n  orange: \"#FFA500\",\n  brightblack: \"#666666\",\n  brightred: \"#FF6666\",\n  brightgreen: \"#66FF66\",\n  brightblue: \"#6666FF\",\n  brightyellow: \"#FFFF66\",\n  brightcyan: \"#66FFFF\",\n  brightmagenta: \"#FF66FF\",\n  brightwhite: \"#FFFFFF\",\n}\n\nexport function parseColor(color: ColorInput): RGBA {\n  if (typeof color === \"string\") {\n    const lowerColor = color.toLowerCase()\n\n    if (lowerColor === \"transparent\") {\n      return RGBA.fromValues(0, 0, 0, 0)\n    }\n\n    if (CSS_COLOR_NAMES[lowerColor]) {\n      return hexToRgb(CSS_COLOR_NAMES[lowerColor])\n    }\n\n    return hexToRgb(color)\n  }\n  return color\n}\n"
  },
  {
    "path": "packages/core/src/lib/ascii.font.ts",
    "content": "import { OptimizedBuffer } from \"../buffer.js\"\nimport { parseColor, RGBA, type ColorInput } from \"./RGBA.js\"\nimport block from \"./fonts/block.json\"\nimport shade from \"./fonts/shade.json\"\nimport slick from \"./fonts/slick.json\"\nimport tiny from \"./fonts/tiny.json\"\nimport huge from \"./fonts/huge.json\"\nimport grid from \"./fonts/grid.json\"\nimport pallet from \"./fonts/pallet.json\"\n\n/*\n * Renders ASCII fonts to a buffer.\n * Font definitions plugged from cfonts - https://github.com/dominikwilkowski/cfonts\n */\n\nexport type ASCIIFontName = \"tiny\" | \"block\" | \"shade\" | \"slick\" | \"huge\" | \"grid\" | \"pallet\"\n\nexport const fonts = {\n  tiny,\n  block,\n  shade,\n  slick,\n  huge,\n  grid,\n  pallet,\n}\n\ntype FontSegment = {\n  text: string\n  colorIndex: number\n}\n\ntype FontDefinition = {\n  name: string\n  lines: number\n  letterspace_size: number\n  letterspace: string[]\n  colors?: number\n  chars: Record<string, string[]>\n}\n\ntype ParsedFontDefinition = {\n  name: string\n  lines: number\n  letterspace_size: number\n  letterspace: string[]\n  colors: number\n  chars: Record<string, FontSegment[][]>\n}\n\nconst parsedFonts: Record<string, ParsedFontDefinition> = {}\n\nfunction parseColorTags(text: string): FontSegment[] {\n  const segments: FontSegment[] = []\n  let currentIndex = 0\n\n  const colorTagRegex = /<c(\\d+)>(.*?)<\\/c\\d+>/g\n  let lastIndex = 0\n  let match\n\n  while ((match = colorTagRegex.exec(text)) !== null) {\n    if (match.index > lastIndex) {\n      const plainText = text.slice(lastIndex, match.index)\n      if (plainText) {\n        segments.push({ text: plainText, colorIndex: 0 })\n      }\n    }\n\n    const colorIndex = parseInt(match[1]) - 1\n    const taggedText = match[2]\n    segments.push({ text: taggedText, colorIndex: Math.max(0, colorIndex) })\n\n    lastIndex = match.index + match[0].length\n  }\n\n  if (lastIndex < text.length) {\n    const remainingText = text.slice(lastIndex)\n    if (remainingText) {\n      segments.push({ text: remainingText, colorIndex: 0 })\n    }\n  }\n\n  return segments\n}\n\nfunction getParsedFont(fontKey: keyof typeof fonts): ParsedFontDefinition {\n  if (!parsedFonts[fontKey]) {\n    const fontDef = fonts[fontKey] as FontDefinition\n    const parsedChars: Record<string, FontSegment[][]> = {}\n\n    for (const [char, lines] of Object.entries(fontDef.chars)) {\n      parsedChars[char] = lines.map((line) => parseColorTags(line))\n    }\n\n    parsedFonts[fontKey] = {\n      ...fontDef,\n      colors: fontDef.colors || 1,\n      chars: parsedChars,\n    }\n  }\n\n  return parsedFonts[fontKey]\n}\n\nexport function measureText({ text, font = \"tiny\" }: { text: string; font?: keyof typeof fonts }): {\n  width: number\n  height: number\n} {\n  const fontDef = getParsedFont(font)\n  if (!fontDef) {\n    console.warn(`Font '${font}' not found`)\n    return { width: 0, height: 0 }\n  }\n\n  let currentX = 0\n\n  for (let i = 0; i < text.length; i++) {\n    const char = text[i].toUpperCase()\n    const charDef = fontDef.chars[char]\n\n    if (!charDef) {\n      const spaceChar = fontDef.chars[\" \"]\n      if (spaceChar && spaceChar[0]) {\n        let spaceWidth = 0\n        for (const segment of spaceChar[0]) {\n          spaceWidth += segment.text.length\n        }\n        currentX += spaceWidth\n      } else {\n        currentX += 1\n      }\n      continue\n    }\n\n    let charWidth = 0\n    if (charDef[0]) {\n      for (const segment of charDef[0]) {\n        charWidth += segment.text.length\n      }\n    }\n\n    currentX += charWidth\n\n    if (i < text.length - 1) {\n      currentX += fontDef.letterspace_size\n    }\n  }\n\n  return {\n    width: currentX,\n    height: fontDef.lines,\n  }\n}\n\nexport function getCharacterPositions(text: string, font: keyof typeof fonts = \"tiny\"): number[] {\n  const fontDef = getParsedFont(font)\n  if (!fontDef) {\n    return [0]\n  }\n\n  const positions: number[] = [0]\n  let currentX = 0\n\n  for (let i = 0; i < text.length; i++) {\n    const char = text[i].toUpperCase()\n    const charDef = fontDef.chars[char]\n\n    let charWidth = 0\n    if (!charDef) {\n      const spaceChar = fontDef.chars[\" \"]\n      if (spaceChar && spaceChar[0]) {\n        for (const segment of spaceChar[0]) {\n          charWidth += segment.text.length\n        }\n      } else {\n        charWidth = 1\n      }\n    } else if (charDef[0]) {\n      for (const segment of charDef[0]) {\n        charWidth += segment.text.length\n      }\n    }\n\n    currentX += charWidth\n\n    if (i < text.length - 1) {\n      currentX += fontDef.letterspace_size\n    }\n\n    positions.push(currentX)\n  }\n\n  return positions\n}\n\nexport function coordinateToCharacterIndex(x: number, text: string, font: keyof typeof fonts = \"tiny\"): number {\n  const positions = getCharacterPositions(text, font)\n\n  if (x < 0) {\n    return 0\n  }\n\n  for (let i = 0; i < positions.length - 1; i++) {\n    const currentPos = positions[i]\n    const nextPos = positions[i + 1]\n\n    if (x >= currentPos && x < nextPos) {\n      const charMidpoint = currentPos + (nextPos - currentPos) / 2\n      return x < charMidpoint ? i : i + 1\n    }\n  }\n\n  if (positions.length > 0 && x >= positions[positions.length - 1]) {\n    return text.length\n  }\n\n  return 0\n}\n\nexport function renderFontToFrameBuffer(\n  buffer: OptimizedBuffer,\n  {\n    text,\n    x = 0,\n    y = 0,\n    color = [RGBA.fromInts(255, 255, 255, 255)],\n    backgroundColor = RGBA.fromInts(0, 0, 0, 255),\n    font = \"tiny\",\n  }: {\n    text: string\n    x?: number\n    y?: number\n    color?: ColorInput | ColorInput[]\n    backgroundColor?: ColorInput\n    font?: keyof typeof fonts\n  },\n): { width: number; height: number } {\n  const width = buffer.width\n  const height = buffer.height\n\n  const fontDef = getParsedFont(font)\n  if (!fontDef) {\n    console.warn(`Font '${font}' not found`)\n    return { width: 0, height: 0 }\n  }\n\n  const colors = Array.isArray(color) ? color : [color]\n\n  if (y < 0 || y + fontDef.lines > height) {\n    return { width: 0, height: fontDef.lines }\n  }\n\n  let currentX = x\n  const startX = x\n\n  for (let i = 0; i < text.length; i++) {\n    const char = text[i].toUpperCase()\n    const charDef = fontDef.chars[char]\n\n    if (!charDef) {\n      const spaceChar = fontDef.chars[\" \"]\n      if (spaceChar && spaceChar[0]) {\n        let spaceWidth = 0\n        for (const segment of spaceChar[0]) {\n          spaceWidth += segment.text.length\n        }\n        currentX += spaceWidth\n      } else {\n        currentX += 1\n      }\n      continue\n    }\n\n    let charWidth = 0\n    if (charDef[0]) {\n      for (const segment of charDef[0]) {\n        charWidth += segment.text.length\n      }\n    }\n\n    if (currentX >= width) break\n    if (currentX + charWidth < 0) {\n      currentX += charWidth + fontDef.letterspace_size\n      continue\n    }\n\n    for (let lineIdx = 0; lineIdx < fontDef.lines && lineIdx < charDef.length; lineIdx++) {\n      const segments = charDef[lineIdx]\n      const renderY = y + lineIdx\n\n      if (renderY >= 0 && renderY < height) {\n        let segmentX = currentX\n\n        for (const segment of segments) {\n          const segmentColor = colors[segment.colorIndex] || colors[0]\n\n          for (let charIdx = 0; charIdx < segment.text.length; charIdx++) {\n            const renderX = segmentX + charIdx\n\n            if (renderX >= 0 && renderX < width) {\n              const fontChar = segment.text[charIdx]\n              if (fontChar !== \" \") {\n                buffer.setCellWithAlphaBlending(\n                  renderX,\n                  renderY,\n                  fontChar,\n                  parseColor(segmentColor),\n                  parseColor(backgroundColor),\n                )\n              }\n            }\n          }\n\n          segmentX += segment.text.length\n        }\n      }\n    }\n\n    currentX += charWidth\n\n    if (i < text.length - 1) {\n      currentX += fontDef.letterspace_size\n    }\n  }\n\n  return {\n    width: currentX - startX,\n    height: fontDef.lines,\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/border.test.ts",
    "content": "import { test, expect, describe, spyOn, afterEach } from \"bun:test\"\nimport { isValidBorderStyle, parseBorderStyle, type BorderStyle } from \"./border.js\"\n\ndescribe(\"isValidBorderStyle\", () => {\n  test(\"returns true for valid border styles\", () => {\n    expect(isValidBorderStyle(\"single\")).toBe(true)\n    expect(isValidBorderStyle(\"double\")).toBe(true)\n    expect(isValidBorderStyle(\"rounded\")).toBe(true)\n    expect(isValidBorderStyle(\"heavy\")).toBe(true)\n  })\n\n  test(\"returns false for invalid border styles\", () => {\n    expect(isValidBorderStyle(\"invalid\")).toBe(false)\n    expect(isValidBorderStyle(\"\")).toBe(false)\n    expect(isValidBorderStyle(null)).toBe(false)\n    expect(isValidBorderStyle(undefined)).toBe(false)\n    expect(isValidBorderStyle(123)).toBe(false)\n    expect(isValidBorderStyle({})).toBe(false)\n    expect(isValidBorderStyle([])).toBe(false)\n  })\n})\n\ndescribe(\"parseBorderStyle\", () => {\n  let warnSpy: ReturnType<typeof spyOn>\n\n  afterEach(() => {\n    warnSpy?.mockRestore()\n  })\n\n  test(\"returns valid border styles unchanged\", () => {\n    expect(parseBorderStyle(\"single\")).toBe(\"single\")\n    expect(parseBorderStyle(\"double\")).toBe(\"double\")\n    expect(parseBorderStyle(\"rounded\")).toBe(\"rounded\")\n    expect(parseBorderStyle(\"heavy\")).toBe(\"heavy\")\n  })\n\n  test(\"falls back to 'single' for invalid string values\", () => {\n    warnSpy = spyOn(console, \"warn\").mockImplementation(() => {})\n\n    expect(parseBorderStyle(\"invalid\")).toBe(\"single\")\n    expect(parseBorderStyle(\"\")).toBe(\"single\")\n    expect(parseBorderStyle(\"SINGLE\")).toBe(\"single\") // case sensitive\n    expect(parseBorderStyle(\"Single\")).toBe(\"single\")\n  })\n\n  test(\"falls back to custom fallback for invalid values\", () => {\n    warnSpy = spyOn(console, \"warn\").mockImplementation(() => {})\n\n    expect(parseBorderStyle(\"invalid\", \"double\")).toBe(\"double\")\n    expect(parseBorderStyle(\"invalid\", \"rounded\")).toBe(\"rounded\")\n    expect(parseBorderStyle(\"invalid\", \"heavy\")).toBe(\"heavy\")\n  })\n\n  test(\"falls back silently for undefined/null without warning\", () => {\n    warnSpy = spyOn(console, \"warn\").mockImplementation(() => {})\n\n    expect(parseBorderStyle(undefined)).toBe(\"single\")\n    expect(parseBorderStyle(null)).toBe(\"single\")\n    expect(warnSpy).not.toHaveBeenCalled()\n  })\n\n  test(\"logs warning for invalid non-null/undefined values\", () => {\n    warnSpy = spyOn(console, \"warn\").mockImplementation(() => {})\n\n    parseBorderStyle(\"invalid-style\")\n\n    expect(warnSpy).toHaveBeenCalledTimes(1)\n    expect(warnSpy).toHaveBeenCalledWith(\n      'Invalid borderStyle \"invalid-style\", falling back to \"single\". Valid values are: single, double, rounded, heavy',\n    )\n  })\n\n  describe(\"regression: does not crash with unexpected value types\", () => {\n    test(\"handles invalid values\", () => {\n      warnSpy = spyOn(console, \"warn\").mockImplementation(() => {})\n\n      expect(parseBorderStyle(123 as unknown as BorderStyle)).toBe(\"single\")\n      expect(parseBorderStyle({} as unknown as BorderStyle)).toBe(\"single\")\n      expect(parseBorderStyle(true as unknown as BorderStyle)).toBe(\"single\")\n      expect(parseBorderStyle((() => \"single\") as unknown as BorderStyle)).toBe(\"single\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/border.ts",
    "content": "import type { ColorInput } from \"./RGBA.js\"\n\nexport interface BorderCharacters {\n  topLeft: string\n  topRight: string\n  bottomLeft: string\n  bottomRight: string\n  horizontal: string\n  vertical: string\n  topT: string\n  bottomT: string\n  leftT: string\n  rightT: string\n  cross: string\n}\n\nexport type BorderStyle = \"single\" | \"double\" | \"rounded\" | \"heavy\"\nexport type BorderSides = \"top\" | \"right\" | \"bottom\" | \"left\"\n\nconst VALID_BORDER_STYLES: readonly BorderStyle[] = [\"single\", \"double\", \"rounded\", \"heavy\"] as const\n\nexport function isValidBorderStyle(value: unknown): value is BorderStyle {\n  return typeof value === \"string\" && VALID_BORDER_STYLES.includes(value as BorderStyle)\n}\n\nexport function parseBorderStyle(value: unknown, fallback: BorderStyle = \"single\"): BorderStyle {\n  if (isValidBorderStyle(value)) {\n    return value\n  }\n\n  if (value !== undefined && value !== null) {\n    console.warn(\n      `Invalid borderStyle \"${value}\", falling back to \"${fallback}\". Valid values are: ${VALID_BORDER_STYLES.join(\", \")}`,\n    )\n  }\n  return fallback\n}\n\nexport const BorderChars: Record<BorderStyle, BorderCharacters> = {\n  single: {\n    topLeft: \"┌\",\n    topRight: \"┐\",\n    bottomLeft: \"└\",\n    bottomRight: \"┘\",\n    horizontal: \"─\",\n    vertical: \"│\",\n    topT: \"┬\",\n    bottomT: \"┴\",\n    leftT: \"├\",\n    rightT: \"┤\",\n    cross: \"┼\",\n  },\n  double: {\n    topLeft: \"╔\",\n    topRight: \"╗\",\n    bottomLeft: \"╚\",\n    bottomRight: \"╝\",\n    horizontal: \"═\",\n    vertical: \"║\",\n    topT: \"╦\",\n    bottomT: \"╩\",\n    leftT: \"╠\",\n    rightT: \"╣\",\n    cross: \"╬\",\n  },\n  rounded: {\n    topLeft: \"╭\",\n    topRight: \"╮\",\n    bottomLeft: \"╰\",\n    bottomRight: \"╯\",\n    horizontal: \"─\",\n    vertical: \"│\",\n    topT: \"┬\",\n    bottomT: \"┴\",\n    leftT: \"├\",\n    rightT: \"┤\",\n    cross: \"┼\",\n  },\n  heavy: {\n    topLeft: \"┏\",\n    topRight: \"┓\",\n    bottomLeft: \"┗\",\n    bottomRight: \"┛\",\n    horizontal: \"━\",\n    vertical: \"┃\",\n    topT: \"┳\",\n    bottomT: \"┻\",\n    leftT: \"┣\",\n    rightT: \"┫\",\n    cross: \"╋\",\n  },\n}\n\nexport interface BorderConfig {\n  borderStyle: BorderStyle\n  border: boolean | BorderSides[]\n  borderColor?: ColorInput\n  customBorderChars?: BorderCharacters\n}\n\nexport interface BoxDrawOptions {\n  x: number\n  y: number\n  width: number\n  height: number\n  borderStyle: BorderStyle\n  border: boolean | BorderSides[]\n  borderColor: ColorInput\n  customBorderChars?: BorderCharacters\n  backgroundColor: ColorInput\n  shouldFill?: boolean\n  title?: string\n  titleAlignment?: \"left\" | \"center\" | \"right\"\n}\n\nexport interface BorderSidesConfig {\n  top: boolean\n  right: boolean\n  bottom: boolean\n  left: boolean\n}\n\nexport function getBorderFromSides(sides: BorderSidesConfig): boolean | BorderSides[] {\n  const result: BorderSides[] = []\n  if (sides.top) result.push(\"top\")\n  if (sides.right) result.push(\"right\")\n  if (sides.bottom) result.push(\"bottom\")\n  if (sides.left) result.push(\"left\")\n  return result.length > 0 ? result : false\n}\n\nexport function getBorderSides(border: boolean | BorderSides[]): BorderSidesConfig {\n  return border === true\n    ? { top: true, right: true, bottom: true, left: true }\n    : Array.isArray(border)\n      ? {\n          top: border.includes(\"top\"),\n          right: border.includes(\"right\"),\n          bottom: border.includes(\"bottom\"),\n          left: border.includes(\"left\"),\n        }\n      : { top: false, right: false, bottom: false, left: false }\n}\n\n// Convert BorderCharacters to Uint32Array for passing to Zig\nexport function borderCharsToArray(chars: BorderCharacters): Uint32Array {\n  const array = new Uint32Array(11)\n  array[0] = chars.topLeft.codePointAt(0)!\n  array[1] = chars.topRight.codePointAt(0)!\n  array[2] = chars.bottomLeft.codePointAt(0)!\n  array[3] = chars.bottomRight.codePointAt(0)!\n  array[4] = chars.horizontal.codePointAt(0)!\n  array[5] = chars.vertical.codePointAt(0)!\n  array[6] = chars.topT.codePointAt(0)!\n  array[7] = chars.bottomT.codePointAt(0)!\n  array[8] = chars.leftT.codePointAt(0)!\n  array[9] = chars.rightT.codePointAt(0)!\n  array[10] = chars.cross.codePointAt(0)!\n  return array\n}\n\n// Pre-converted border character arrays for performance\nexport const BorderCharArrays: Record<BorderStyle, Uint32Array> = {\n  single: borderCharsToArray(BorderChars.single),\n  double: borderCharsToArray(BorderChars.double),\n  rounded: borderCharsToArray(BorderChars.rounded),\n  heavy: borderCharsToArray(BorderChars.heavy),\n}\n"
  },
  {
    "path": "packages/core/src/lib/bunfs.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\"\nimport { isBunfsPath, getBunfsRootPath } from \"./bunfs.js\"\n\ndescribe(\"bunfs\", () => {\n  test(\"isBunfsPath detects $bunfs paths\", () => {\n    expect(isBunfsPath(\"/$bunfs/root/file.wasm\")).toBe(true)\n  })\n\n  test(\"isBunfsPath detects Windows B: paths\", () => {\n    expect(isBunfsPath(\"B:\\\\~BUN\\\\root\\\\file.wasm\")).toBe(true)\n    expect(isBunfsPath(\"B:/~BUN/root/file.wasm\")).toBe(true)\n  })\n\n  test(\"isBunfsPath ignores regular paths\", () => {\n    expect(isBunfsPath(\"/usr/local/bin/file\")).toBe(false)\n    expect(isBunfsPath(\"C:/Users/file.wasm\")).toBe(false)\n  })\n\n  test(\"getBunfsRootPath\", () => {\n    const root = getBunfsRootPath()\n    if (process.platform === \"win32\") {\n      expect(root).toBe(\"B:\\\\~BUN\\\\root\")\n    } else {\n      expect(root).toBe(\"/$bunfs/root\")\n    }\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/bunfs.ts",
    "content": "import { basename, join } from \"node:path\"\n\nexport function isBunfsPath(path: string): boolean {\n  // Removed ambiguous '//' check\n  return path.includes(\"$bunfs\") || /^B:[\\\\/]~BUN/i.test(path)\n}\n\nexport function getBunfsRootPath(): string {\n  return process.platform === \"win32\" ? \"B:\\\\~BUN\\\\root\" : \"/$bunfs/root\"\n}\n\n/**\n * Normalizes a path to the embedded root.\n * Flattens directory structure to ensure file exists at root.\n */\nexport function normalizeBunfsPath(fileName: string): string {\n  return join(getBunfsRootPath(), basename(fileName))\n}\n"
  },
  {
    "path": "packages/core/src/lib/clipboard.test.ts",
    "content": "import { describe, expect, it, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { ClipboardTarget, encodeOsc52Payload } from \"./clipboard.js\"\nimport type { RenderLib } from \"../zig.js\"\n\ndescribe(\"clipboard\", () => {\n  let renderer: TestRenderer | null = null\n\n  const enableOsc52 = (testRenderer: TestRenderer) => {\n    const lib = (testRenderer as unknown as { lib: RenderLib }).lib\n    lib.processCapabilityResponse(testRenderer.rendererPtr, \"\\x1bP>|kitty(0.40.1)\\x1b\\\\\")\n  }\n\n  afterEach(() => {\n    renderer?.destroy()\n    renderer = null\n  })\n\n  it(\"encodes payload as base64\", () => {\n    const payload = encodeOsc52Payload(\"hello\")\n    const decoded = new TextDecoder().decode(payload)\n    expect(decoded).toBe(Buffer.from(\"hello\").toString(\"base64\"))\n  })\n\n  it(\"gates clipboard writes on OSC 52 support\", async () => {\n    ;({ renderer } = await createTestRenderer({ remote: true }))\n\n    expect(renderer.isOsc52Supported()).toBe(false)\n    expect(renderer.copyToClipboardOSC52(\"test\")).toBe(false)\n    expect(renderer.clearClipboardOSC52()).toBe(false)\n\n    enableOsc52(renderer)\n\n    expect(renderer.isOsc52Supported()).toBe(true)\n    expect(renderer.copyToClipboardOSC52(\"test\")).toBe(true)\n    expect(renderer.copyToClipboardOSC52(\"test\", ClipboardTarget.Primary)).toBe(true)\n    expect(renderer.copyToClipboardOSC52(\"test\", ClipboardTarget.Secondary)).toBe(true)\n    expect(renderer.copyToClipboardOSC52(\"test\", ClipboardTarget.Query)).toBe(true)\n    expect(renderer.clearClipboardOSC52()).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/clipboard.ts",
    "content": "// OSC 52 clipboard support for terminal applications.\n// Delegates to native Zig implementation for ANSI sequence generation.\n\nimport type { Pointer } from \"bun:ffi\"\nimport type { RenderLib } from \"../zig.js\"\n\nexport enum ClipboardTarget {\n  Clipboard = 0,\n  Primary = 1,\n  Secondary = 2,\n  Query = 3,\n}\n\nexport function encodeOsc52Payload(text: string, encoder: TextEncoder = new TextEncoder()): Uint8Array {\n  const base64 = Buffer.from(text).toString(\"base64\")\n  return encoder.encode(base64)\n}\n\nexport class Clipboard {\n  private lib: RenderLib\n  private rendererPtr: Pointer\n\n  constructor(lib: RenderLib, rendererPtr: Pointer) {\n    this.lib = lib\n    this.rendererPtr = rendererPtr\n  }\n\n  public copyToClipboardOSC52(text: string, target: ClipboardTarget = ClipboardTarget.Clipboard): boolean {\n    if (!this.isOsc52Supported()) {\n      return false\n    }\n    const payload = encodeOsc52Payload(text, this.lib.encoder)\n    return this.lib.copyToClipboardOSC52(this.rendererPtr, target, payload)\n  }\n\n  public clearClipboardOSC52(target: ClipboardTarget = ClipboardTarget.Clipboard): boolean {\n    if (!this.isOsc52Supported()) {\n      return false\n    }\n    return this.lib.clearClipboardOSC52(this.rendererPtr, target)\n  }\n\n  public isOsc52Supported(): boolean {\n    const caps = this.lib.getTerminalCapabilities(this.rendererPtr)\n    return Boolean(caps?.osc52)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/clock.ts",
    "content": "export type TimerHandle = ReturnType<typeof globalThis.setTimeout> | number\n\nexport interface Clock {\n  now(): number\n  setTimeout(fn: () => void, delayMs: number): TimerHandle\n  clearTimeout(handle: TimerHandle): void\n  setInterval(fn: () => void, delayMs: number): TimerHandle\n  clearInterval(handle: TimerHandle): void\n}\n\nexport class SystemClock implements Clock {\n  public now(): number {\n    if (!globalThis.performance || typeof globalThis.performance.now !== \"function\") {\n      throw new Error(\"SystemClock requires globalThis.performance.now()\")\n    }\n\n    return globalThis.performance.now()\n  }\n\n  public setTimeout(fn: () => void, delayMs: number): TimerHandle {\n    return globalThis.setTimeout(fn, delayMs)\n  }\n\n  public clearTimeout(handle: TimerHandle): void {\n    globalThis.clearTimeout(handle as ReturnType<typeof globalThis.setTimeout>)\n  }\n\n  public setInterval(fn: () => void, delayMs: number): TimerHandle {\n    return globalThis.setInterval(fn, delayMs)\n  }\n\n  public clearInterval(handle: TimerHandle): void {\n    globalThis.clearInterval(handle as ReturnType<typeof globalThis.setTimeout>)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/data-paths.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { DataPathsManager } from \"./data-paths.js\"\n\ntest(\"DataPathsManager validates appName\", () => {\n  const manager = new DataPathsManager()\n\n  // Valid names should work\n  expect(() => {\n    manager.appName = \"myapp\"\n  }).not.toThrow()\n\n  expect(() => {\n    manager.appName = \"my-app\"\n  }).not.toThrow()\n\n  expect(() => {\n    manager.appName = \"my_app\"\n  }).not.toThrow()\n\n  expect(() => {\n    manager.appName = \"MyApp123\"\n  }).not.toThrow()\n\n  // Invalid names should throw\n  expect(() => {\n    manager.appName = \"\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"   \"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app/name\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app\\\\name\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app<name\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app>name\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = 'app\"name'\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app|name\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app?name\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app*name\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"CON\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"PRN\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app.\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"app \"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \".\"\n  }).toThrow(\"Invalid app name\")\n\n  expect(() => {\n    manager.appName = \"..\"\n  }).toThrow(\"Invalid app name\")\n})\n\ntest(\"DataPathsManager constructor uses valid default appName\", () => {\n  // Should not throw when creating a new instance\n  expect(() => {\n    new DataPathsManager()\n  }).not.toThrow()\n\n  const manager = new DataPathsManager()\n  expect(manager.appName).toBe(\"opentui\")\n})\n\ntest(\"DataPathsManager emits paths:changed event when appName changes\", async () => {\n  const manager = new DataPathsManager()\n  let eventFired = false\n  let eventPaths: any = null\n\n  manager.on(\"paths:changed\", (paths) => {\n    eventFired = true\n    eventPaths = paths\n  })\n\n  const originalAppName = manager.appName\n  manager.appName = \"test-app-event\"\n\n  expect(eventFired).toBe(true)\n  expect(eventPaths).toBeDefined()\n  expect(eventPaths.globalDataPath).toContain(\"test-app-event\")\n  expect(eventPaths.globalConfigPath).toContain(\"test-app-event\")\n  expect(eventPaths.globalConfigFile).toContain(\"test-app-event\")\n  expect(eventPaths.localConfigFile).toContain(\"test-app-event\")\n})\n\ntest(\"DataPathsManager does not emit event when appName is set to same value\", () => {\n  const manager = new DataPathsManager()\n  let eventFired = false\n\n  manager.on(\"paths:changed\", () => {\n    eventFired = true\n  })\n\n  // Set to the same value\n  manager.appName = manager.appName\n\n  expect(eventFired).toBe(false)\n})\n"
  },
  {
    "path": "packages/core/src/lib/data-paths.ts",
    "content": "import os from \"os\"\nimport path from \"path\"\nimport { EventEmitter } from \"events\"\nimport { singleton } from \"./singleton.js\"\nimport { env, registerEnvVar } from \"./env.js\"\nimport { isValidDirectoryName } from \"./validate-dir-name.js\"\n\n// Register environment variables for XDG directories\nregisterEnvVar({\n  name: \"XDG_CONFIG_HOME\",\n  description: \"Base directory for user-specific configuration files\",\n  type: \"string\",\n  default: \"\",\n})\n\nregisterEnvVar({\n  name: \"XDG_DATA_HOME\",\n  description: \"Base directory for user-specific data files\",\n  type: \"string\",\n  default: \"\",\n})\n\nexport interface DataPaths {\n  globalConfigPath: string\n  globalConfigFile: string\n  localConfigFile: string\n  globalDataPath: string\n}\n\nexport interface DataPathsEvents {\n  \"paths:changed\": [paths: DataPaths]\n}\n\nexport class DataPathsManager extends EventEmitter<DataPathsEvents> {\n  private _appName: string\n  private _globalConfigPath?: string\n  private _globalConfigFile?: string\n  private _localConfigFile?: string\n  private _globalDataPath?: string\n  constructor() {\n    super()\n    this._appName = \"opentui\"\n  }\n\n  get appName(): string {\n    return this._appName\n  }\n\n  set appName(value: string) {\n    if (!isValidDirectoryName(value)) {\n      throw new Error(`Invalid app name \"${value}\": must be a valid directory name`)\n    }\n    if (this._appName !== value) {\n      this._appName = value\n      this._globalConfigPath = undefined\n      this._globalConfigFile = undefined\n      this._localConfigFile = undefined\n      this._globalDataPath = undefined\n      this.emit(\"paths:changed\", this.toObject())\n    }\n  }\n\n  get globalConfigPath(): string {\n    if (this._globalConfigPath === undefined) {\n      const homeDir = os.homedir()\n      const xdgConfigHome = env.XDG_CONFIG_HOME\n      const baseConfigDir = xdgConfigHome || path.join(homeDir, \".config\")\n      this._globalConfigPath = path.join(baseConfigDir, this._appName)\n    }\n    return this._globalConfigPath\n  }\n\n  get globalConfigFile(): string {\n    if (this._globalConfigFile === undefined) {\n      this._globalConfigFile = path.join(this.globalConfigPath, \"init.ts\")\n    }\n    return this._globalConfigFile\n  }\n\n  get localConfigFile(): string {\n    if (this._localConfigFile === undefined) {\n      this._localConfigFile = path.join(process.cwd(), `.${this._appName}.ts`)\n    }\n    return this._localConfigFile\n  }\n\n  get globalDataPath(): string {\n    if (this._globalDataPath === undefined) {\n      const homeDir = os.homedir()\n      const xdgDataHome = env.XDG_DATA_HOME\n      const baseDataDir = xdgDataHome || path.join(homeDir, \".local/share\")\n      this._globalDataPath = path.join(baseDataDir, this._appName)\n    }\n    return this._globalDataPath\n  }\n\n  toObject(): DataPaths {\n    return {\n      globalConfigPath: this.globalConfigPath,\n      globalConfigFile: this.globalConfigFile,\n      localConfigFile: this.localConfigFile,\n      globalDataPath: this.globalDataPath,\n    }\n  }\n}\n\nexport function getDataPaths(): DataPathsManager {\n  return singleton(\"data-paths-opentui\", () => new DataPathsManager())\n}\n"
  },
  {
    "path": "packages/core/src/lib/debounce.ts",
    "content": "/**\n * A module-level map to store timeout IDs for all debounced functions\n * Structure: Map<scopeId, Map<debounceId, timerId>>\n */\nconst TIMERS_MAP = new Map<string | number, Map<string | number, ReturnType<typeof setTimeout>>>()\n\n/**\n * Debounce controller that manages debounce instances for a specific scope\n */\nexport class DebounceController {\n  constructor(private scopeId: string | number) {\n    // Initialize the scope map if it doesn't exist\n    if (!TIMERS_MAP.has(this.scopeId)) {\n      TIMERS_MAP.set(this.scopeId, new Map())\n    }\n  }\n\n  /**\n   * Debounces the provided function with the given ID\n   *\n   * @param id Unique identifier within this scope\n   * @param ms Milliseconds to wait before executing\n   * @param fn Function to execute\n   */\n  debounce<R>(id: string | number, ms: number, fn: () => Promise<R>): Promise<R> {\n    const scopeMap = TIMERS_MAP.get(this.scopeId)!\n\n    return new Promise((resolve, reject) => {\n      // Clear any existing timeout for this ID\n      if (scopeMap.has(id)) {\n        clearTimeout(scopeMap.get(id))\n      }\n\n      // Set a new timeout\n      const timerId = setTimeout(() => {\n        try {\n          resolve(fn())\n        } catch (error) {\n          reject(error)\n        }\n        scopeMap.delete(id)\n      }, ms)\n\n      // Store the new timeout ID\n      scopeMap.set(id, timerId)\n    })\n  }\n\n  /**\n   * Clear a specific debounce timer in this scope\n   *\n   * @param id The debounce ID to clear\n   */\n  clearDebounce(id: string | number): void {\n    const scopeMap = TIMERS_MAP.get(this.scopeId)\n    if (scopeMap && scopeMap.has(id)) {\n      clearTimeout(scopeMap.get(id))\n      scopeMap.delete(id)\n    }\n  }\n\n  /**\n   * Clear all debounce timers in this scope\n   */\n  clear(): void {\n    const scopeMap = TIMERS_MAP.get(this.scopeId)\n    if (scopeMap) {\n      scopeMap.forEach((timerId) => clearTimeout(timerId))\n      scopeMap.clear()\n    }\n  }\n}\n\n/**\n * Creates a new debounce controller for a specific scope\n *\n * @param scopeId Unique identifier for this debounce scope\n * @returns A DebounceController for the specified scope\n */\nexport function createDebounce(scopeId: string | number): DebounceController {\n  return new DebounceController(scopeId)\n}\n\n/**\n * Clears all debounce timers for a specific scope\n *\n * @param scopeId The scope identifier\n */\nexport function clearDebounceScope(scopeId: string | number): void {\n  const scopeMap = TIMERS_MAP.get(scopeId)\n  if (scopeMap) {\n    scopeMap.forEach((timerId) => clearTimeout(timerId))\n    scopeMap.clear()\n  }\n}\n\n/**\n * Clears all active debounce timers across all scopes\n */\nexport function clearAllDebounces(): void {\n  TIMERS_MAP.forEach((scopeMap) => {\n    scopeMap.forEach((timerId) => clearTimeout(timerId))\n    scopeMap.clear()\n  })\n  TIMERS_MAP.clear()\n}\n"
  },
  {
    "path": "packages/core/src/lib/detect-links.test.ts",
    "content": "import { test, expect, describe } from \"bun:test\"\nimport { detectLinks } from \"./detect-links\"\nimport type { TextChunk } from \"../text-buffer\"\nimport type { SimpleHighlight } from \"./tree-sitter/types\"\nimport { RGBA } from \"./RGBA\"\n\nfunction chunk(text: string): TextChunk {\n  return { __isChunk: true, text, fg: RGBA.fromInts(255, 255, 255, 255), attributes: 0 }\n}\n\ndescribe(\"detectLinks\", () => {\n  test(\"should set link on markup.link.url chunks\", () => {\n    const content = \"[Click here](https://example.com)\"\n    const highlights: SimpleHighlight[] = [\n      [0, 1, \"markup.link\"],\n      [1, 11, \"markup.link.label\"],\n      [11, 13, \"markup.link\"],\n      [13, 32, \"markup.link.url\"],\n      [32, 33, \"markup.link\"],\n    ]\n    const chunks = [chunk(\"[\"), chunk(\"Click here\"), chunk(\"](\"), chunk(\"https://example.com\"), chunk(\")\")]\n\n    const result = detectLinks(chunks, { content, highlights })\n\n    expect(result.find((c) => c.text === \"https://example.com\")!.link).toEqual({ url: \"https://example.com\" })\n    expect(result.find((c) => c.text === \"Click here\")!.link).toEqual({ url: \"https://example.com\" })\n  })\n\n  test(\"should set link on string.special.url chunks\", () => {\n    const content = \"// see https://example.com for details\"\n    const highlights: SimpleHighlight[] = [\n      [0, 38, \"comment\"],\n      [7, 26, \"string.special.url\"],\n    ]\n    const chunks = [chunk(\"// see \"), chunk(\"https://example.com\"), chunk(\" for details\")]\n\n    const result = detectLinks(chunks, { content, highlights })\n\n    expect(result.find((c) => c.text === \"https://example.com\")!.link).toEqual({ url: \"https://example.com\" })\n  })\n\n  test(\"should not set link on non-URL chunks\", () => {\n    const content = \"const x = 42\"\n    const highlights: SimpleHighlight[] = [\n      [0, 5, \"keyword\"],\n      [6, 7, \"variable\"],\n      [10, 12, \"number\"],\n    ]\n    const chunks = [chunk(\"const\"), chunk(\" \"), chunk(\"x\"), chunk(\" = \"), chunk(\"42\")]\n\n    const result = detectLinks(chunks, { content, highlights })\n\n    for (const c of result) {\n      expect(c.link).toBeUndefined()\n    }\n  })\n\n  test(\"should return chunks unchanged when no URL scopes exist\", () => {\n    const content = \"hello world\"\n    const highlights: SimpleHighlight[] = [[0, 5, \"keyword\"]]\n    const chunks = [chunk(\"hello\"), chunk(\" world\")]\n\n    const result = detectLinks(chunks, { content, highlights })\n\n    expect(result).toBe(chunks)\n  })\n\n  test(\"should detect links when chunks have concealed text\", () => {\n    // Original content: [Click here](https://example.com)\n    // With concealment, `[` and `]` are concealed to empty strings,\n    // and `(` and `)` are concealed to empty strings.\n    // This means chunk text lengths don't match original byte offsets.\n    const content = \"[Click here](https://example.com)\"\n    const highlights: SimpleHighlight[] = [\n      [0, 1, \"markup.link\"], // [\n      [1, 11, \"markup.link.label\"], // Click here\n      [11, 13, \"markup.link\"], // ](\n      [13, 32, \"markup.link.url\"], // https://example.com\n      [32, 33, \"markup.link\"], // )\n    ]\n    // Simulate concealed chunks: `[` -> \"\", `](` -> \" \", `)` -> \"\"\n    // The URL and label chunks remain unchanged.\n    const chunks = [\n      chunk(\"\"), // concealed `[`\n      chunk(\"Click here\"), // label, unchanged\n      chunk(\" \"), // concealed `](`\n      chunk(\"https://example.com\"), // URL, unchanged\n      chunk(\"\"), // concealed `)`\n    ]\n\n    const result = detectLinks(chunks, { content, highlights })\n\n    // The URL chunk should still get its link despite concealed offsets\n    expect(result.find((c) => c.text === \"https://example.com\")!.link).toEqual({ url: \"https://example.com\" })\n    // The label chunk should also get the link\n    expect(result.find((c) => c.text === \"Click here\")!.link).toEqual({ url: \"https://example.com\" })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/detect-links.ts",
    "content": "import type { TextChunk } from \"../text-buffer\"\nimport type { SimpleHighlight } from \"./tree-sitter/types\"\n\nconst URL_SCOPES = [\"markup.link.url\", \"string.special.url\"]\n\nexport function detectLinks(\n  chunks: TextChunk[],\n  context: { content: string; highlights: SimpleHighlight[] },\n): TextChunk[] {\n  const content = context.content\n  const highlights = context.highlights\n\n  const ranges: Array<{ start: number; end: number; url: string }> = []\n\n  for (let i = 0; i < highlights.length; i++) {\n    const [start, end, group] = highlights[i]\n    if (!URL_SCOPES.includes(group)) continue\n\n    const url = content.slice(start, end)\n    ranges.push({ start, end, url })\n\n    for (let j = i - 1; j >= 0; j--) {\n      const [labelStart, labelEnd, prev] = highlights[j]\n      if (prev === \"markup.link.label\") {\n        ranges.push({ start: labelStart, end: labelEnd, url })\n        break\n      }\n      if (!prev.startsWith(\"markup.link\")) break\n    }\n  }\n\n  if (ranges.length === 0) return chunks\n\n  // Use content.indexOf to find each chunk's position in the original content.\n  // This handles concealed text correctly because concealed chunks are either\n  // empty (length 0, skipped) or single-char replacements (length 1, skipped).\n  // Non-concealed chunks with length > 1 are exact substrings of content in order.\n  let contentPos = 0\n  for (const chunk of chunks) {\n    if (chunk.text.length <= 1) continue\n\n    const idx = content.indexOf(chunk.text, contentPos)\n    if (idx < 0) continue\n\n    for (const range of ranges) {\n      if (idx < range.end && idx + chunk.text.length > range.start) {\n        chunk.link = { url: range.url }\n        break\n      }\n    }\n\n    contentPos = idx + chunk.text.length\n  }\n\n  return chunks\n}\n"
  },
  {
    "path": "packages/core/src/lib/env.test.ts",
    "content": "import { describe, test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { envRegistry, registerEnvVar, env, clearEnvCache } from \"./env.ts\"\n\n// Backup and restore registry to avoid interfering with module-level registrations\nlet registryBackup: Record<string, any> = {}\n\nbeforeEach(() => {\n  registryBackup = { ...envRegistry }\n\n  clearEnvCache()\n\n  Object.keys(process.env).forEach((key) => {\n    if (key.startsWith(\"TEST_\")) {\n      delete process.env[key]\n    }\n  })\n})\n\nafterEach(() => {\n  Object.keys(envRegistry).forEach((key) => {\n    if (key.startsWith(\"TEST_\") && !(key in registryBackup)) {\n      delete envRegistry[key]\n    }\n  })\n\n  Object.keys(process.env).forEach((key) => {\n    if (key.startsWith(\"TEST_\")) {\n      delete process.env[key]\n    }\n  })\n})\n\ndescribe(\"env registry\", () => {\n  test(\"should register and access string env vars\", () => {\n    registerEnvVar({\n      name: \"TEST_STRING\",\n      description: \"A test string variable\",\n      type: \"string\",\n      default: \"default_value\",\n    })\n\n    // Set env var\n    process.env.TEST_STRING = \"test_value\"\n\n    expect(env.TEST_STRING).toBe(\"test_value\")\n  })\n\n  test(\"should handle boolean env vars with various true values\", () => {\n    registerEnvVar({\n      name: \"TEST_BOOL_TRUE\",\n      description: \"A test boolean variable\",\n      type: \"boolean\",\n    })\n\n    // Test various true values\n    process.env.TEST_BOOL_TRUE = \"true\"\n    expect(env.TEST_BOOL_TRUE).toBe(true)\n\n    process.env.TEST_BOOL_TRUE = \"1\"\n    expect(env.TEST_BOOL_TRUE).toBe(true)\n\n    process.env.TEST_BOOL_TRUE = \"on\"\n    expect(env.TEST_BOOL_TRUE).toBe(true)\n\n    process.env.TEST_BOOL_TRUE = \"yes\"\n    expect(env.TEST_BOOL_TRUE).toBe(true)\n  })\n\n  test(\"should handle boolean env vars with various false values\", () => {\n    registerEnvVar({\n      name: \"TEST_BOOL_FALSE\",\n      description: \"A test boolean variable\",\n      type: \"boolean\",\n    })\n\n    // Test various false values\n    process.env.TEST_BOOL_FALSE = \"false\"\n    expect(env.TEST_BOOL_FALSE).toBe(false)\n\n    process.env.TEST_BOOL_FALSE = \"0\"\n    expect(env.TEST_BOOL_FALSE).toBe(false)\n\n    process.env.TEST_BOOL_FALSE = \"off\"\n    expect(env.TEST_BOOL_FALSE).toBe(false)\n  })\n\n  test(\"should handle number env vars\", () => {\n    registerEnvVar({\n      name: \"TEST_NUMBER\",\n      description: \"A test number variable\",\n      type: \"number\",\n    })\n\n    process.env.TEST_NUMBER = \"42\"\n    expect(env.TEST_NUMBER).toBe(42)\n  })\n\n  test(\"should throw error for invalid number\", () => {\n    registerEnvVar({\n      name: \"TEST_INVALID_NUMBER\",\n      description: \"A test number variable\",\n      type: \"number\",\n    })\n\n    process.env.TEST_INVALID_NUMBER = \"not_a_number\"\n\n    expect(() => env.TEST_INVALID_NUMBER).toThrow(\"must be a valid number\")\n  })\n\n  test(\"should use default values when env var not set\", () => {\n    registerEnvVar({\n      name: \"TEST_DEFAULT\",\n      description: \"A test variable with default\",\n      type: \"string\",\n      default: \"default_value\",\n    })\n\n    // Don't set the env var\n    expect(env.TEST_DEFAULT).toBe(\"default_value\")\n  })\n\n  test(\"should throw error for required env var not set\", () => {\n    registerEnvVar({\n      name: \"TEST_REQUIRED\",\n      description: \"A required test variable\",\n    })\n\n    expect(() => env.TEST_REQUIRED).toThrow(\"Required environment variable TEST_REQUIRED is not set\")\n  })\n\n  test(\"should throw error for unregistered env var\", () => {\n    expect(() => env.UNREGISTERED_VAR).toThrow(\"Environment variable UNREGISTERED_VAR is not registered\")\n  })\n\n  test(\"should support proxy enumeration\", () => {\n    registerEnvVar({\n      name: \"TEST_ENUM_1\",\n      description: \"First test var\",\n      default: \"value1\",\n    })\n\n    registerEnvVar({\n      name: \"TEST_ENUM_2\",\n      description: \"Second test var\",\n      default: \"value2\",\n    })\n\n    const keys = Object.keys(env)\n    expect(keys).toContain(\"TEST_ENUM_1\")\n    expect(keys).toContain(\"TEST_ENUM_2\")\n  })\n\n  test(\"should support 'in' operator\", () => {\n    registerEnvVar({\n      name: \"TEST_IN_OPERATOR\",\n      description: \"Test for 'in' operator\",\n      default: \"test\",\n    })\n\n    expect(\"TEST_IN_OPERATOR\" in env).toBe(true)\n    expect(\"NON_EXISTENT\" in env).toBe(false)\n  })\n\n  test(\"should allow re-registering identical configuration\", () => {\n    const config = {\n      name: \"TEST_IDENTICAL\",\n      description: \"Test for identical re-registration\",\n      type: \"boolean\" as const,\n      default: false,\n    }\n\n    registerEnvVar(config)\n    // Should not throw\n    registerEnvVar(config)\n\n    expect(\"TEST_IDENTICAL\" in env).toBe(true)\n  })\n\n  test(\"should throw when re-registering with different type\", () => {\n    registerEnvVar({\n      name: \"TEST_DIFFERENT_TYPE\",\n      description: \"Test for different type\",\n      type: \"string\",\n    })\n\n    expect(() => {\n      registerEnvVar({\n        name: \"TEST_DIFFERENT_TYPE\",\n        description: \"Test for different type\",\n        type: \"boolean\",\n      })\n    }).toThrow(\"already registered with different configuration\")\n  })\n\n  test(\"should throw when re-registering with different default\", () => {\n    registerEnvVar({\n      name: \"TEST_DIFFERENT_DEFAULT\",\n      description: \"Test for different default\",\n      type: \"string\",\n      default: \"first\",\n    })\n\n    expect(() => {\n      registerEnvVar({\n        name: \"TEST_DIFFERENT_DEFAULT\",\n        description: \"Test for different default\",\n        type: \"string\",\n        default: \"second\",\n      })\n    }).toThrow(\"already registered with different configuration\")\n  })\n\n  test(\"should throw when re-registering with different description\", () => {\n    registerEnvVar({\n      name: \"TEST_DIFFERENT_DESC\",\n      description: \"First description\",\n      type: \"string\",\n    })\n\n    expect(() => {\n      registerEnvVar({\n        name: \"TEST_DIFFERENT_DESC\",\n        description: \"Second description\",\n        type: \"string\",\n      })\n    }).toThrow(\"already registered with different configuration\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/env.ts",
    "content": "import { singleton } from \"./singleton.ts\"\n\n/**\n * Environment variable registry\n *\n * Usage:\n * ```ts\n * import { registerEnvVar, env } from \"./lib/env.ts\";\n *\n * // Register environment variables\n * registerEnvVar({\n *   name: \"DEBUG\",\n *   description: \"Enable debug logging\",\n *   type: \"boolean\",\n *   default: false\n * });\n *\n * registerEnvVar({\n *   name: \"PORT\",\n *   description: \"Server port number\",\n *   type: \"number\",\n *   default: 3000\n * });\n *\n * // Access environment variables\n * if (env.DEBUG) {\n *   console.log(\"Debug mode enabled\");\n * }\n *\n * const port = env.PORT; // number\n * ```\n */\n\nexport interface EnvVarConfig {\n  name: string\n  description: string\n  default?: string | boolean | number\n  type?: \"string\" | \"boolean\" | \"number\"\n}\n\nexport const envRegistry: Record<string, EnvVarConfig> = singleton(\"env-registry\", () => ({}))\n\nexport function registerEnvVar(config: EnvVarConfig): void {\n  const existing = envRegistry[config.name]\n  if (existing) {\n    if (\n      existing.description !== config.description ||\n      existing.type !== config.type ||\n      existing.default !== config.default\n    ) {\n      throw new Error(\n        `Environment variable \"${config.name}\" is already registered with different configuration. ` +\n          `Existing: ${JSON.stringify(existing)}, New: ${JSON.stringify(config)}`,\n      )\n    }\n    return\n  }\n  envRegistry[config.name] = config\n}\n\nfunction normalizeBoolean(value: string): boolean {\n  const lowerValue = value.toLowerCase()\n  return [\"true\", \"1\", \"on\", \"yes\"].includes(lowerValue)\n}\n\nfunction parseEnvValue(config: EnvVarConfig): string | boolean | number {\n  const envValue = process.env[config.name]\n\n  if (envValue === undefined && config.default !== undefined) {\n    return config.default\n  }\n\n  if (envValue === undefined) {\n    throw new Error(`Required environment variable ${config.name} is not set. ${config.description}`)\n  }\n\n  switch (config.type) {\n    case \"boolean\":\n      return typeof envValue === \"boolean\" ? envValue : normalizeBoolean(envValue)\n    case \"number\":\n      const numValue = Number(envValue)\n      if (isNaN(numValue)) {\n        throw new Error(`Environment variable ${config.name} must be a valid number, got: ${envValue}`)\n      }\n      return numValue\n    case \"string\":\n    default:\n      return envValue\n  }\n}\n\nclass EnvStore {\n  private parsedValues: Map<string, string | boolean | number> = new Map()\n\n  get(key: string): any {\n    if (this.parsedValues.has(key)) {\n      return this.parsedValues.get(key)!\n    }\n\n    if (!(key in envRegistry)) {\n      throw new Error(`Environment variable ${key} is not registered.`)\n    }\n\n    try {\n      const value = parseEnvValue(envRegistry[key])\n      this.parsedValues.set(key, value)\n      return value\n    } catch (error) {\n      throw new Error(`Failed to parse env var ${key}: ${error instanceof Error ? error.message : String(error)}`)\n    }\n  }\n\n  has(key: string): boolean {\n    return key in envRegistry\n  }\n\n  clearCache(): void {\n    this.parsedValues.clear()\n  }\n}\n\nconst envStore = singleton(\"env-store\", () => new EnvStore())\n\nexport function clearEnvCache(): void {\n  envStore.clearCache()\n}\n\nexport function generateEnvMarkdown(): string {\n  const configs = Object.values(envRegistry)\n\n  if (configs.length === 0) {\n    return \"# Environment Variables\\n\\nNo environment variables registered.\\n\"\n  }\n\n  let markdown = \"# Environment Variables\\n\\n\"\n\n  for (const config of configs) {\n    markdown += `## ${config.name}\\n\\n`\n    markdown += `${config.description}\\n\\n`\n\n    markdown += `**Type:** \\`${config.type || \"string\"}\\`  \\n`\n\n    if (config.default !== undefined) {\n      const defaultValue = typeof config.default === \"string\" ? `\"${config.default}\"` : String(config.default)\n      markdown += `**Default:** \\`${defaultValue}\\`\\n`\n    } else {\n      markdown += \"**Default:** *Required*\\n\"\n    }\n\n    markdown += \"\\n\"\n  }\n\n  return markdown\n}\n\nexport function generateEnvColored(): string {\n  const configs = Object.values(envRegistry)\n\n  if (configs.length === 0) {\n    return \"\\x1b[1;36mEnvironment Variables\\x1b[0m\\n\\nNo environment variables registered.\\n\"\n  }\n\n  let output = \"\\x1b[1;36mEnvironment Variables\\x1b[0m\\n\\n\"\n\n  for (const config of configs) {\n    output += `\\x1b[1;33m${config.name}\\x1b[0m\\n`\n    output += `${config.description}\\n`\n    output += `\\x1b[32mType:\\x1b[0m \\x1b[36m${config.type || \"string\"}\\x1b[0m\\n`\n\n    if (config.default !== undefined) {\n      const defaultValue = typeof config.default === \"string\" ? `\"${config.default}\"` : String(config.default)\n      output += `\\x1b[32mDefault:\\x1b[0m \\x1b[35m${defaultValue}\\x1b[0m\\n`\n    } else {\n      output += `\\x1b[32mDefault:\\x1b[0m \\x1b[31mRequired\\x1b[0m\\n`\n    }\n\n    output += \"\\n\"\n  }\n\n  return output\n}\n\nexport const env = new Proxy({} as Record<string, any>, {\n  get(target, prop: string) {\n    if (typeof prop !== \"string\") {\n      return undefined\n    }\n    return envStore.get(prop)\n  },\n\n  has(target, prop: string) {\n    return envStore.has(prop)\n  },\n\n  ownKeys() {\n    return Object.keys(envRegistry)\n  },\n\n  getOwnPropertyDescriptor(target, prop: string) {\n    if (envStore.has(prop)) {\n      return {\n        enumerable: true,\n        configurable: true,\n        get: () => envStore.get(prop),\n      }\n    }\n    return undefined\n  },\n})\n"
  },
  {
    "path": "packages/core/src/lib/extmarks-history.ts",
    "content": "import type { Extmark } from \"./extmarks.js\"\n\nexport interface ExtmarksSnapshot {\n  extmarks: Map<number, Extmark>\n  nextId: number\n}\n\nexport class ExtmarksHistory {\n  private undoStack: ExtmarksSnapshot[] = []\n  private redoStack: ExtmarksSnapshot[] = []\n\n  saveSnapshot(extmarks: Map<number, Extmark>, nextId: number): void {\n    const snapshot: ExtmarksSnapshot = {\n      extmarks: new Map(Array.from(extmarks.entries()).map(([id, extmark]) => [id, { ...extmark }])),\n      nextId,\n    }\n    this.undoStack.push(snapshot)\n    this.redoStack = []\n  }\n\n  undo(): ExtmarksSnapshot | null {\n    if (this.undoStack.length === 0) return null\n    return this.undoStack.pop()!\n  }\n\n  redo(): ExtmarksSnapshot | null {\n    if (this.redoStack.length === 0) return null\n    return this.redoStack.pop()!\n  }\n\n  pushRedo(snapshot: ExtmarksSnapshot): void {\n    this.redoStack.push(snapshot)\n  }\n\n  pushUndo(snapshot: ExtmarksSnapshot): void {\n    this.undoStack.push(snapshot)\n  }\n\n  clear(): void {\n    this.undoStack = []\n    this.redoStack = []\n  }\n\n  canUndo(): boolean {\n    return this.undoStack.length > 0\n  }\n\n  canRedo(): boolean {\n    return this.redoStack.length > 0\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/extmarks-multiwidth.test.ts",
    "content": "import { describe, expect, it, afterEach } from \"bun:test\"\nimport { TextareaRenderable } from \"../renderables/Textarea.js\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../testing/test-renderer.js\"\nimport { type ExtmarksController } from \"./extmarks.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { RGBA } from \"./RGBA.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\nlet textarea: TextareaRenderable\nlet extmarks: ExtmarksController\n\nasync function setup(initialValue: string = \"Hello World\") {\n  const result = await createTestRenderer({ width: 80, height: 24 })\n  currentRenderer = result.renderer\n  renderOnce = result.renderOnce\n  currentMockInput = result.mockInput\n\n  textarea = new TextareaRenderable(currentRenderer, {\n    left: 0,\n    top: 0,\n    width: 40,\n    height: 10,\n    initialValue,\n  })\n\n  currentRenderer.root.add(textarea)\n  await renderOnce()\n\n  extmarks = textarea.extmarks\n\n  return { textarea, extmarks }\n}\n\ndescribe(\"ExtmarksController - Multi-width Graphemes\", () => {\n  afterEach(() => {\n    if (extmarks) extmarks.destroy()\n    if (currentRenderer) currentRenderer.destroy()\n  })\n\n  describe(\"Basic Multi-width Highlighting\", () => {\n    it(\"should correctly highlight text AFTER multi-width characters\", async () => {\n      // Text: \"前后端分离 @git-committer\"\n      // Chinese chars are multi-width, @ onwards should highlight correctly\n      await setup(\"前后端分离 @git-committer\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"mention\", {\n        fg: RGBA.fromValues(0, 0, 1, 1),\n        bg: RGBA.fromValues(0.9, 0.9, 1, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      const text = textarea.plainText\n\n      // Calculate CORRECT display-width offsets\n      // \"前\" = 2 cols, \"后\" = 2 cols, \"端\" = 2 cols, \"分\" = 2 cols, \"离\" = 2 cols, \" \" = 1 col\n      // Total before \"@\": 10 + 1 = 11 display-width columns\n      let displayOffset = 0\n      const atJsIndex = text.indexOf(\"@\")\n      for (let i = 0; i < atJsIndex; i++) {\n        if (text[i] === \"\\n\") {\n          displayOffset += 1\n        } else {\n          displayOffset += Bun.stringWidth(text[i])\n        }\n      }\n\n      const mentionText = \"@git-committer\"\n      const mentionDisplayWidth = Bun.stringWidth(mentionText)\n      const mentionStart = displayOffset // Should be 11\n      const mentionEnd = displayOffset + mentionDisplayWidth // Should be 25\n\n      extmarks.create({\n        start: mentionStart,\n        end: mentionEnd,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n      expect(highlights.length).toBe(1)\n      expect(highlights[0].start).toBe(11)\n      expect(highlights[0].end).toBe(25)\n    })\n\n    it(\"should correctly highlight text BEFORE multi-width characters\", async () => {\n      await setup(\"hello 前后端分离\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      // Highlight \"hello\" which is at offsets 0-5\n      extmarks.create({\n        start: 0,\n        end: 5,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n\n      expect(highlights.length).toBe(1)\n      expect(highlights[0].start).toBe(0)\n      expect(highlights[0].end).toBe(5)\n    })\n\n    it(\"should correctly highlight BETWEEN multi-width characters\", async () => {\n      await setup(\"前后 test 端分离\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      // \"前后 test 端分离\"\n      // Offsets: 前=0, 后=1, space=2, t=3, e=4, s=5, t=6, space=7, 端=8, 分=9, 离=10\n      const testStart = 3\n      const testEnd = 7\n\n      extmarks.create({\n        start: testStart,\n        end: testEnd,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n\n      if (highlights.length > 0) {\n        const lineText = textarea.plainText.split(\"\\n\")[0]\n        const actualHighlightedText = lineText.substring(highlights[0].start, highlights[0].end)\n        expect(actualHighlightedText).toBe(\"test\")\n      }\n\n      expect(highlights.length).toBe(1)\n    })\n\n    it(\"should correctly highlight the multi-width characters themselves\", async () => {\n      await setup(\"hello 前后端分离 world\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      // \"hello 前后端分离 world\"\n      // Offsets: h=0,e=1,l=2,l=3,o=4,space=5,前=6,后=7,端=8,分=9,离=10,space=11,w=12...\n      const chineseStart = 6\n      const chineseEnd = 11\n\n      extmarks.create({\n        start: chineseStart,\n        end: chineseEnd,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n\n      if (highlights.length > 0) {\n        const lineText = textarea.plainText.split(\"\\n\")[0]\n        const actualHighlightedText = lineText.substring(highlights[0].start, highlights[0].end)\n        expect(actualHighlightedText).toBe(\"前后端分离\")\n      }\n\n      expect(highlights.length).toBe(1)\n    })\n  })\n\n  describe(\"Complex Multi-width Scenarios\", () => {\n    it(\"should handle emoji and multi-width characters together\", async () => {\n      await setup(\"前后 🌟 test\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      // Highlight \"test\" at the end\n      const text = textarea.plainText\n      const testPos = text.indexOf(\"test\")\n\n      extmarks.create({\n        start: testPos,\n        end: testPos + 4,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n\n      expect(highlights.length).toBe(1)\n\n      const lineText = textarea.plainText.split(\"\\n\")[0]\n      const actualHighlightedText = lineText.substring(highlights[0].start, highlights[0].end)\n      expect(actualHighlightedText).toBe(\"test\")\n    })\n\n    it(\"should handle multiple highlights with multi-width characters\", async () => {\n      await setup(\"前后端 @user1 分离 @user2 end\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"mention\", {\n        fg: RGBA.fromValues(0, 0, 1, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      const text = textarea.plainText\n\n      const user1Start = text.indexOf(\"@user1\")\n      const user1End = user1Start + 6\n      const user2Start = text.indexOf(\"@user2\")\n      const user2End = user2Start + 6\n\n      extmarks.create({\n        start: user1Start,\n        end: user1End,\n        styleId,\n      })\n\n      extmarks.create({\n        start: user2Start,\n        end: user2End,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n\n      highlights.forEach((h, i) => {\n        const lineText = textarea.plainText.split(\"\\n\")[0]\n        const highlightedText = lineText.substring(h.start, h.end)\n      })\n\n      expect(highlights.length).toBe(2)\n    })\n  })\n\n  describe(\"Cursor Movement with Multi-width Characters\", () => {\n    it(\"should correctly position cursor after multi-width characters\", async () => {\n      await setup(\"前后 test\")\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      // Text: \"前后 test\"\n      // \"前\" = display width 2, \"后\" = display width 2, \" \" = display width 1\n      // After 3 arrow right presses from position 0:\n      //   Press 1: move to display-width 2 (after \"前\")\n      //   Press 2: move to display-width 4 (after \"后\")\n      //   Press 3: move to display-width 5 (after \" \")\n\n      for (let i = 0; i < 3; i++) {\n        currentMockInput.pressArrow(\"right\")\n      }\n\n      const cursorPos = textarea.cursorOffset\n\n      // Cursor should be at display-width offset 5 (after \"前后 \")\n      expect(cursorPos).toBe(5)\n    })\n  })\n\n  describe(\"Visual vs Byte Offset Issues\", () => {\n    it(\"should demonstrate the offset to char offset conversion issue\", async () => {\n      // This is the CRITICAL test - offsetToCharOffset doesn't account for display width\n      await setup(\"前后端分离 @git-committer\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"mention\", {\n        fg: RGBA.fromValues(0, 0, 1, 1),\n        bg: RGBA.fromValues(0.9, 0.9, 1, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      const text = textarea.plainText\n\n      // The @ symbol is at cursor offset 6\n      const atPos = text.indexOf(\"@\")\n\n      // We want to highlight from @ to the end of \"committer\"\n      const start = atPos\n      const end = atPos + 14 // \"@git-committer\" is 14 chars\n\n      const extmarkId = extmarks.create({\n        start: start,\n        end: end,\n        styleId,\n      })\n\n      const extmark = extmarks.get(extmarkId)\n\n      const highlights = textarea.getLineHighlights(0)\n\n      if (highlights.length > 0) {\n        const h = highlights[0]\n\n        // This is where the bug manifests:\n        // The offsetToCharOffset in extmarks.ts doesn't account for multi-width display\n        // So the highlight char offset will be wrong\n\n        const lineText = text.split(\"\\n\")[0]\n\n        // Try to extract what's actually highlighted using the char offsets\n        const actualText = lineText.substring(h.start, Math.min(h.end, lineText.length))\n\n        // This will likely FAIL because the char offsets don't account for display width\n      }\n\n      expect(highlights.length).toBe(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/extmarks.test.ts",
    "content": "import { describe, expect, it, afterEach } from \"bun:test\"\nimport { TextareaRenderable } from \"../renderables/Textarea.js\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../testing/test-renderer.js\"\nimport { type ExtmarksController } from \"./extmarks.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { RGBA } from \"./RGBA.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\nlet textarea: TextareaRenderable\nlet extmarks: ExtmarksController\n\nasync function setup(initialValue: string = \"Hello World\") {\n  const result = await createTestRenderer({ width: 80, height: 24 })\n  currentRenderer = result.renderer\n  renderOnce = result.renderOnce\n  currentMockInput = result.mockInput\n\n  textarea = new TextareaRenderable(currentRenderer, {\n    left: 0,\n    top: 0,\n    width: 40,\n    height: 10,\n    initialValue,\n  })\n\n  currentRenderer.root.add(textarea)\n  await renderOnce()\n\n  extmarks = textarea.extmarks\n\n  return { textarea, extmarks }\n}\n\ndescribe(\"ExtmarksController\", () => {\n  afterEach(() => {\n    if (extmarks) extmarks.destroy()\n    if (currentRenderer) currentRenderer.destroy()\n  })\n\n  describe(\"Creation and Basic Operations\", () => {\n    it(\"should create extmark with basic options\", async () => {\n      await setup()\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      expect(id).toBe(1)\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n      expect(extmark?.start).toBe(0)\n      expect(extmark?.end).toBe(5)\n      expect(extmark?.virtual).toBe(false)\n    })\n\n    it(\"should create virtual extmark\", async () => {\n      await setup()\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n        virtual: true,\n      })\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.virtual).toBe(true)\n    })\n\n    it(\"should create multiple extmarks with unique IDs\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({ start: 0, end: 5 })\n      const id2 = extmarks.create({ start: 6, end: 11 })\n\n      expect(id1).toBe(1)\n      expect(id2).toBe(2)\n      expect(extmarks.getAll().length).toBe(2)\n    })\n\n    it(\"should store custom data with extmark\", async () => {\n      await setup()\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        data: { type: \"link\", url: \"https://example.com\" },\n      })\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.data).toEqual({ type: \"link\", url: \"https://example.com\" })\n    })\n  })\n\n  describe(\"Delete Operations\", () => {\n    it(\"should delete extmark\", async () => {\n      await setup()\n\n      const id = extmarks.create({ start: 0, end: 5 })\n      const result = extmarks.delete(id)\n\n      expect(result).toBe(true)\n      expect(extmarks.get(id)).toBeNull()\n    })\n\n    it(\"should return false when deleting non-existent extmark\", async () => {\n      await setup()\n\n      const result = extmarks.delete(999)\n      expect(result).toBe(false)\n    })\n\n    it(\"should delete extmark without emitting events\", async () => {\n      await setup()\n\n      const id = extmarks.create({ start: 0, end: 5 })\n      extmarks.delete(id)\n      expect(extmarks.get(id)).toBeNull()\n    })\n\n    it(\"should clear all extmarks\", async () => {\n      await setup()\n\n      extmarks.create({ start: 0, end: 5 })\n      extmarks.create({ start: 6, end: 11 })\n\n      expect(extmarks.getAll().length).toBe(2)\n\n      extmarks.clear()\n\n      expect(extmarks.getAll().length).toBe(0)\n    })\n  })\n\n  describe(\"Query Operations\", () => {\n    it(\"should get all extmarks\", async () => {\n      await setup()\n\n      extmarks.create({ start: 0, end: 5 })\n      extmarks.create({ start: 6, end: 11 })\n\n      const all = extmarks.getAll()\n      expect(all.length).toBe(2)\n    })\n\n    it(\"should get only virtual extmarks\", async () => {\n      await setup()\n\n      extmarks.create({ start: 0, end: 5, virtual: false })\n      extmarks.create({ start: 6, end: 11, virtual: true })\n      extmarks.create({ start: 12, end: 15, virtual: true })\n\n      const virtual = extmarks.getVirtual()\n      expect(virtual.length).toBe(2)\n      expect(virtual.every((e) => e.virtual)).toBe(true)\n    })\n\n    it(\"should get extmarks at specific offset\", async () => {\n      await setup()\n\n      extmarks.create({ start: 0, end: 5 })\n      extmarks.create({ start: 3, end: 8 })\n      extmarks.create({ start: 10, end: 15 })\n\n      const atOffset4 = extmarks.getAtOffset(4)\n      expect(atOffset4.length).toBe(2)\n\n      const atOffset10 = extmarks.getAtOffset(10)\n      expect(atOffset10.length).toBe(1)\n    })\n  })\n\n  describe(\"Virtual Extmark - Cursor Jumping Right\", () => {\n    it(\"should jump cursor over virtual extmark when moving right\", async () => {\n      await setup(\"abcdefgh\")\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      extmarks.create({\n        start: 3,\n        end: 6,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(2)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(6)\n    })\n\n    it(\"should jump to position AFTER extmark end when moving right from before extmark\", async () => {\n      await setup(\"abcdefgh\")\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      extmarks.create({\n        start: 3,\n        end: 6,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(2)\n\n      // When moving right from position 2 (before extmark start at 3),\n      // should jump to position 6 (after extmark end)\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(6)\n    })\n\n    it(\"should allow cursor to move normally outside virtual extmark\", async () => {\n      await setup(\"abcdefgh\")\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      extmarks.create({\n        start: 3,\n        end: 6,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(1)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(2)\n    })\n\n    it(\"should jump over multiple virtual extmarks\", async () => {\n      await setup(\"abcdefghij\")\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      extmarks.create({ start: 2, end: 4, virtual: true })\n      extmarks.create({ start: 5, end: 7, virtual: true })\n\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(1)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(4)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(7)\n    })\n  })\n\n  describe(\"Virtual Extmark - Cursor Jumping Left\", () => {\n    it(\"should jump cursor over virtual extmark when moving left\", async () => {\n      await setup(\"abcdefgh\")\n\n      textarea.focus()\n      textarea.cursorOffset = 7\n\n      extmarks.create({\n        start: 3,\n        end: 6,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(7)\n\n      currentMockInput.pressArrow(\"left\")\n      expect(textarea.cursorOffset).toBe(6)\n\n      currentMockInput.pressArrow(\"left\")\n      expect(textarea.cursorOffset).toBe(2)\n    })\n\n    it(\"should jump to position BEFORE extmark start when moving left from after extmark\", async () => {\n      await setup(\"abcdefgh\")\n\n      textarea.focus()\n      textarea.cursorOffset = 6\n\n      extmarks.create({\n        start: 3,\n        end: 6,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(6)\n\n      // When moving left from position 6 (right after extmark end),\n      // should jump to position 2 (before extmark start at 3)\n      currentMockInput.pressArrow(\"left\")\n      expect(textarea.cursorOffset).toBe(2)\n    })\n\n    it(\"should allow normal cursor movement left outside virtual extmark\", async () => {\n      await setup(\"abcdefgh\")\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      extmarks.create({\n        start: 3,\n        end: 6,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"left\")\n      expect(textarea.cursorOffset).toBe(1)\n\n      currentMockInput.pressArrow(\"left\")\n      expect(textarea.cursorOffset).toBe(0)\n    })\n  })\n\n  describe(\"Virtual Extmark - Selection Mode\", () => {\n    it(\"should allow selection through virtual extmark\", async () => {\n      await setup(\"abcdefgh\")\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      extmarks.create({\n        start: 2,\n        end: 5,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n\n      expect(textarea.cursorOffset).toBe(3)\n      expect(textarea.hasSelection()).toBe(true)\n    })\n  })\n\n  describe(\"Virtual Extmark - Backspace Deletion\", () => {\n    it(\"should delete entire virtual extmark on backspace at end\", async () => {\n      await setup(\"abc[LINK]def\")\n\n      textarea.focus()\n      textarea.cursorOffset = 9\n\n      const id = extmarks.create({\n        start: 3,\n        end: 9,\n        virtual: true,\n      })\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"abcdef\")\n      expect(textarea.cursorOffset).toBe(3)\n      expect(extmarks.get(id)).toBeNull()\n    })\n\n    it(\"should not delete virtual extmark on backspace outside range\", async () => {\n      await setup(\"abc[LINK]def\")\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      const id = extmarks.create({\n        start: 3,\n        end: 9,\n        virtual: true,\n      })\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"ac[LINK]def\")\n      expect(extmarks.get(id)).not.toBeNull()\n    })\n\n    it(\"should delete normal character inside virtual extmark\", async () => {\n      await setup(\"abc[LINK]def\")\n\n      textarea.focus()\n      textarea.cursorOffset = 5\n\n      extmarks.create({\n        start: 3,\n        end: 9,\n        virtual: true,\n      })\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"abc[INK]def\")\n    })\n  })\n\n  describe(\"Virtual Extmark - Delete Key\", () => {\n    it(\"should delete entire virtual extmark on delete at start\", async () => {\n      await setup(\"abc[LINK]def\")\n\n      textarea.focus()\n      textarea.cursorOffset = 3\n\n      const id = extmarks.create({\n        start: 3,\n        end: 9,\n        virtual: true,\n      })\n\n      currentMockInput.pressKey(\"DELETE\")\n\n      expect(textarea.plainText).toBe(\"abcdef\")\n      expect(textarea.cursorOffset).toBe(3)\n      expect(extmarks.get(id)).toBeNull()\n    })\n  })\n\n  describe(\"Extmark Position Adjustment - Insertion\", () => {\n    it(\"should adjust extmark positions after insertion before extmark\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      currentMockInput.pressKey(\"X\")\n      currentMockInput.pressKey(\"X\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(8)\n      expect(extmark?.end).toBe(13)\n    })\n\n    it(\"should expand extmark when inserting inside\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 8\n\n      currentMockInput.pressKey(\"X\")\n      currentMockInput.pressKey(\"X\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(6)\n      expect(extmark?.end).toBe(13)\n    })\n\n    it(\"should not adjust extmark when inserting after\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 11\n\n      currentMockInput.pressKey(\"X\")\n      currentMockInput.pressKey(\"X\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(0)\n      expect(extmark?.end).toBe(5)\n    })\n  })\n\n  describe(\"Extmark Position Adjustment - Deletion\", () => {\n    it(\"should adjust extmark positions after deletion before extmark\", async () => {\n      await setup(\"XXHello World\")\n\n      const id = extmarks.create({\n        start: 8,\n        end: 13,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      currentMockInput.pressBackspace()\n      currentMockInput.pressBackspace()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(6)\n      expect(extmark?.end).toBe(11)\n    })\n\n    it(\"should remove extmark when its range is deleted\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.deleteRange(0, 6, 0, 11)\n\n      expect(extmarks.get(id)).toBeNull()\n    })\n  })\n\n  describe(\"Highlighting Integration\", () => {\n    it(\"should apply highlight for extmark with styleId\", async () => {\n      await setup(\"Hello World\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"link\", {\n        fg: RGBA.fromValues(0, 0, 1, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      extmarks.create({\n        start: 0,\n        end: 5,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n      expect(highlights.length).toBe(1)\n      expect(highlights[0].start).toBe(0)\n      expect(highlights[0].end).toBe(5)\n      expect(highlights[0].styleId).toBe(styleId)\n    })\n\n    it(\"should correctly position highlights in middle of single line\", async () => {\n      await setup(\"AAAA\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      // Highlight just the middle two chars (positions 1-2, which is \"AA\")\n      extmarks.create({\n        start: 1,\n        end: 3,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n      expect(highlights.length).toBe(1)\n      expect(highlights[0].start).toBe(1)\n      expect(highlights[0].end).toBe(3)\n    })\n\n    it(\"should correctly position highlights across newlines\", async () => {\n      await setup(\"AAAA\\nBBBB\\nCCCC\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      // Text: \"AAAA\\nBBBB\\nCCCC\"\n      // Cursor offsets (with newlines): 0-3=\"AAAA\", 4=\"\\n\", 5-8=\"BBBB\", 9=\"\\n\", 10-13=\"CCCC\"\n      // Want to highlight just \"BBBB\" which is cursor offset 5-9\n      extmarks.create({\n        start: 5,\n        end: 9,\n        styleId,\n      })\n\n      const hl0 = textarea.getLineHighlights(0)\n      const hl1 = textarea.getLineHighlights(1)\n      const hl2 = textarea.getLineHighlights(2)\n\n      // Line 0 should have no highlights\n      expect(hl0.length).toBe(0)\n\n      // Line 1 should have the entire \"BBBB\" highlighted\n      expect(hl1.length).toBe(1)\n      expect(hl1[0].start).toBe(0)\n      expect(hl1[0].end).toBe(4)\n\n      // Line 2 should have no highlights\n      expect(hl2.length).toBe(0)\n    })\n\n    it(\"should correctly position multiline highlights\", async () => {\n      await setup(\"AAA\\nBBB\\nCCC\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(0, 1, 0, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      // Text: \"AAA\\nBBB\\nCCC\"\n      // Cursor offsets: 0-2=\"AAA\", 3=\"\\n\", 4-6=\"BBB\", 7=\"\\n\", 8-10=\"CCC\"\n      // Want to highlight from middle of line 0 to middle of line 2\n      // From cursor offset 1 (second 'A') to 9 (second 'C')\n      extmarks.create({\n        start: 1,\n        end: 9,\n        styleId,\n      })\n\n      const hl0 = textarea.getLineHighlights(0)\n      const hl1 = textarea.getLineHighlights(1)\n      const hl2 = textarea.getLineHighlights(2)\n\n      // Line 0: should highlight from position 1 to end (last two A's)\n      expect(hl0.length).toBe(1)\n      expect(hl0[0].start).toBe(1)\n      expect(hl0[0].end).toBe(3)\n\n      // Line 1: should highlight entire line (all of BBB)\n      expect(hl1.length).toBe(1)\n      expect(hl1[0].start).toBe(0)\n      expect(hl1[0].end).toBe(3)\n\n      // Line 2: should highlight from start to position 1 (first C only)\n      // Cursor offset 9 = char offset 7 = second 'C'\n      // Line 2 starts at char offset 6, so we highlight positions 0-1 (first 'C')\n      expect(hl2.length).toBe(1)\n      expect(hl2[0].start).toBe(0)\n      expect(hl2[0].end).toBe(1)\n    })\n\n    it(\"should update highlights when extmark position changes\", async () => {\n      await setup(\"Hello World\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"link\", {\n        fg: RGBA.fromValues(0, 0, 1, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        styleId,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"X\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(1)\n      expect(extmark?.end).toBe(6)\n    })\n\n    it(\"should remove highlight when extmark is deleted\", async () => {\n      await setup(\"Hello World\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"link\", {\n        fg: RGBA.fromValues(0, 0, 1, 1),\n      })\n\n      textarea.syntaxStyle = style\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        styleId,\n      })\n\n      const highlightsBefore = textarea.getLineHighlights(0)\n      expect(highlightsBefore.length).toBeGreaterThan(0)\n\n      extmarks.delete(id)\n\n      const highlightsAfter = textarea.getLineHighlights(0)\n      expect(highlightsAfter.length).toBe(0)\n    })\n  })\n\n  describe(\"Multiline Text Support\", () => {\n    it(\"should handle extmarks in multiline text\", async () => {\n      await setup(\"Line 1\\nLine 2\\nLine 3\")\n\n      const id = extmarks.create({\n        start: 7,\n        end: 13,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"X\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(8)\n      expect(extmark?.end).toBe(14)\n    })\n\n    it(\"should handle virtual extmark across lines\", async () => {\n      await setup(\"Line 1\\nLine 2\\nLine 3\")\n\n      textarea.focus()\n      textarea.cursorOffset = 5\n\n      extmarks.create({\n        start: 7,\n        end: 13,\n        virtual: true,\n      })\n\n      for (let i = 0; i < 3; i++) {\n        currentMockInput.pressArrow(\"right\")\n      }\n\n      expect(textarea.cursorOffset).toBe(14)\n    })\n  })\n\n  describe(\"Destroy\", () => {\n    it(\"should restore original methods on destroy\", async () => {\n      await setup(\"Hello World\")\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      extmarks.create({\n        start: 3,\n        end: 6,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(6)\n\n      extmarks.destroy()\n\n      textarea.cursorOffset = 2\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(3)\n    })\n\n    it(\"should clear all extmarks on destroy\", async () => {\n      await setup()\n\n      extmarks.create({ start: 0, end: 5 })\n      extmarks.create({ start: 6, end: 11 })\n\n      expect(extmarks.getAll().length).toBe(2)\n\n      extmarks.destroy()\n\n      expect(extmarks.getAll().length).toBe(0)\n    })\n\n    it(\"should throw error when using destroyed controller\", async () => {\n      await setup()\n\n      extmarks.destroy()\n\n      expect(() => {\n        extmarks.create({ start: 0, end: 5 })\n      }).toThrow(\"ExtmarksController is destroyed\")\n    })\n  })\n\n  describe(\"Highlight Boundaries\", () => {\n    it(\"should highlight only virtual marker without extending to end of line\", async () => {\n      await setup(\"text [VIRTUAL] more text\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"virtual\", {\n        fg: RGBA.fromValues(0.3, 0.7, 1.0, 1.0),\n        bg: RGBA.fromValues(0.1, 0.2, 0.3, 1.0),\n      })\n\n      textarea.syntaxStyle = style\n\n      const virtualStart = 5\n      const virtualEnd = 14\n\n      extmarks.create({\n        start: virtualStart,\n        end: virtualEnd,\n        virtual: true,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n\n      expect(highlights.length).toBe(1)\n      expect(highlights[0].start).toBe(virtualStart)\n      expect(highlights[0].end).toBe(virtualEnd)\n    })\n\n    it(\"should highlight virtual marker in middle with text after\", async () => {\n      await setup(\"abc [MARKER] def\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"virtual\", {\n        fg: RGBA.fromValues(0.3, 0.7, 1.0, 1.0),\n      })\n\n      textarea.syntaxStyle = style\n\n      const start = 4\n      const end = 12\n\n      extmarks.create({\n        start,\n        end,\n        virtual: true,\n        styleId,\n      })\n\n      const highlights = textarea.getLineHighlights(0)\n\n      expect(highlights.length).toBe(1)\n      expect(highlights[0].start).toBe(start)\n      expect(highlights[0].end).toBe(end)\n    })\n\n    it(\"should highlight virtual marker in multiline text correctly\", async () => {\n      const text = `Try moving your cursor through the [VIRTUAL] markers below:\n- Use arrow keys to navigate`\n\n      await setup(text)\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"virtual\", {\n        fg: RGBA.fromValues(0.3, 0.7, 1.0, 1.0),\n        bg: RGBA.fromValues(0.1, 0.2, 0.3, 1.0),\n      })\n\n      textarea.syntaxStyle = style\n\n      const pattern = /\\[VIRTUAL\\]/g\n      const match = pattern.exec(text)\n\n      if (!match) {\n        throw new Error(\"Pattern not found\")\n      }\n\n      const start = match.index\n      const end = match.index + match[0].length\n\n      extmarks.create({\n        start,\n        end,\n        virtual: true,\n        styleId,\n      })\n\n      const hl0 = textarea.getLineHighlights(0)\n      const hl1 = textarea.getLineHighlights(1)\n\n      expect(hl0.length).toBe(1)\n      expect(hl0[0].start).toBe(35)\n      expect(hl0[0].end).toBe(44)\n      expect(hl1.length).toBe(0)\n    })\n\n    it(\"should correctly highlight multiple virtual markers with pattern matching\", async () => {\n      const initialContent = `Welcome to the Extmarks Demo!\n\nThis demo showcases virtual extmarks - text ranges that the cursor jumps over.\n\nTry moving your cursor through the [VIRTUAL] markers below:\n- Use arrow keys to navigate\n- Notice how the cursor skips over [VIRTUAL] ranges`\n\n      await setup(initialContent)\n\n      const style = SyntaxStyle.create()\n      const virtualStyleId = style.registerStyle(\"virtual\", {\n        fg: RGBA.fromValues(0.3, 0.7, 1.0, 1.0),\n        bg: RGBA.fromValues(0.1, 0.2, 0.3, 1.0),\n      })\n\n      textarea.syntaxStyle = style\n\n      const text = textarea.plainText\n      const pattern = /\\[(VIRTUAL|LINK:[^\\]]+|TAG:[^\\]]+|MARKER)\\]/g\n      let match: RegExpExecArray | null\n\n      while ((match = pattern.exec(text)) !== null) {\n        const start = match.index\n        const end = match.index + match[0].length\n\n        extmarks.create({\n          start,\n          end,\n          virtual: true,\n          styleId: virtualStyleId,\n          data: { type: \"auto-detected\", content: match[0] },\n        })\n      }\n\n      const line4Highlights = textarea.getLineHighlights(4)\n      const line6Highlights = textarea.getLineHighlights(6)\n      const lines = text.split(\"\\n\")\n\n      expect(line4Highlights.length).toBeGreaterThan(0)\n      expect(line6Highlights.length).toBeGreaterThan(0)\n\n      const line4FirstHighlight = line4Highlights[0]\n      const line6FirstHighlight = line6Highlights[0]\n\n      expect(line4FirstHighlight.end).toBe(44)\n      expect(line4FirstHighlight.end).toBeLessThan(lines[4].length)\n\n      expect(line6FirstHighlight.end).toBe(44)\n      expect(line6FirstHighlight.end).toBeLessThan(lines[6].length)\n    })\n  })\n\n  describe(\"Multiple Extmarks\", () => {\n    it(\"should maintain correct positions after deleting first extmark\", async () => {\n      await setup(\"abc [VIRTUAL] def [VIRTUAL] ghi\")\n\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"virtual\", {\n        fg: RGBA.fromValues(0.3, 0.7, 1.0, 1.0),\n      })\n\n      textarea.syntaxStyle = style\n\n      const id1 = extmarks.create({\n        start: 4,\n        end: 13,\n        virtual: true,\n        styleId,\n      })\n\n      const id2 = extmarks.create({\n        start: 18,\n        end: 27,\n        virtual: true,\n        styleId,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 13\n      currentMockInput.pressBackspace()\n\n      expect(extmarks.get(id1)).toBeNull()\n\n      const em2 = extmarks.get(id2)\n      expect(em2).not.toBeNull()\n\n      expect(textarea.plainText.substring(em2!.start, em2!.end)).toBe(\"[VIRTUAL]\")\n    })\n  })\n\n  describe(\"Complex Multiline Scenarios\", () => {\n    it(\"should handle multiple marker types across many lines\", async () => {\n      const initialContent = `Welcome to the Extmarks Demo!\n\nThis demo showcases virtual extmarks - text ranges that the cursor jumps over.\n\nTry moving your cursor through the [VIRTUAL] markers below:\n- Use arrow keys to navigate\n- Notice how the cursor skips over [VIRTUAL] ranges\n- Try backspacing at the end of a [VIRTUAL] marker\n- It will delete the entire marker!\n\nExample text with [LINK:https://example.com] embedded links.\nYou can also have [TAG:important] tags that act like atoms.\n\nRegular text here can be edited normally.\n\nPress Ctrl+L to add a new [MARKER] at cursor position.\nPress ESC to return to main menu.`\n\n      await setup(initialContent)\n\n      const style = SyntaxStyle.create()\n      const virtualStyleId = style.registerStyle(\"virtual\", {\n        fg: RGBA.fromValues(0.3, 0.7, 1.0, 1.0),\n        bg: RGBA.fromValues(0.1, 0.2, 0.3, 1.0),\n      })\n\n      textarea.syntaxStyle = style\n\n      const text = textarea.plainText\n      const pattern = /\\[(VIRTUAL|LINK:[^\\]]+|TAG:[^\\]]+|MARKER)\\]/g\n      let match: RegExpExecArray | null\n      const markedRanges: Array<{ start: number; end: number; text: string; line: number }> = []\n\n      const lines = text.split(\"\\n\")\n\n      while ((match = pattern.exec(text)) !== null) {\n        const start = match.index\n        const end = match.index + match[0].length\n\n        let lineIdx = 0\n        let charCount = 0\n        for (let i = 0; i < lines.length; i++) {\n          if (charCount + lines[i].length >= start) {\n            lineIdx = i\n            break\n          }\n          charCount += lines[i].length + 1\n        }\n\n        markedRanges.push({ start, end, text: match[0], line: lineIdx })\n\n        extmarks.create({\n          start,\n          end,\n          virtual: true,\n          styleId: virtualStyleId,\n          data: { type: \"auto-detected\", content: match[0] },\n        })\n      }\n\n      for (const range of markedRanges) {\n        const highlights = textarea.getLineHighlights(range.line)\n        const lineText = lines[range.line]\n\n        expect(highlights.length).toBeGreaterThan(0)\n\n        const matchingHighlight = highlights.find((h) => {\n          const hlText = lineText.substring(h.start, Math.min(h.end, lineText.length))\n          return hlText.includes(range.text.substring(0, Math.min(5, range.text.length)))\n        })\n\n        expect(matchingHighlight).not.toBeUndefined()\n        expect(matchingHighlight!.end).toBeLessThanOrEqual(lineText.length)\n      }\n    })\n  })\n\n  describe(\"Virtual Extmark - Word Boundary Movement\", () => {\n    it(\"should not land inside virtual extmark when moving backward by word from after extmark\", async () => {\n      await setup(\"bla [VIRTUAL] bla\")\n\n      textarea.focus()\n      textarea.cursorOffset = 13\n\n      extmarks.create({\n        start: 4,\n        end: 13,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(13)\n\n      textarea.moveWordBackward()\n      expect(textarea.cursorOffset).toBe(3)\n    })\n\n    it(\"should jump cursor over virtual extmark when moving forward by word\", async () => {\n      await setup(\"hello [VIRTUAL] world test\")\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      const id = extmarks.create({\n        start: 6,\n        end: 16,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(0)\n\n      textarea.moveWordForward()\n      expect(textarea.cursorOffset).toBe(16)\n\n      textarea.moveWordForward()\n      expect(textarea.cursorOffset).toBe(22)\n\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n    })\n\n    it(\"should jump cursor over virtual extmark when moving backward by word\", async () => {\n      await setup(\"hello [VIRTUAL] world test\")\n\n      textarea.focus()\n      textarea.cursorOffset = 22\n\n      const id = extmarks.create({\n        start: 6,\n        end: 16,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(22)\n\n      textarea.moveWordBackward()\n      expect(textarea.cursorOffset).toBe(16)\n\n      textarea.moveWordBackward()\n      expect(textarea.cursorOffset).toBe(5)\n\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n    })\n\n    it(\"should jump over multiple virtual extmarks when moving forward by word\", async () => {\n      await setup(\"one [V1] two [V2] three\")\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      extmarks.create({ start: 4, end: 9, virtual: true })\n      extmarks.create({ start: 13, end: 18, virtual: true })\n\n      textarea.moveWordForward()\n      expect(textarea.cursorOffset).toBe(9)\n\n      textarea.moveWordForward()\n      expect(textarea.cursorOffset).toBe(18)\n\n      textarea.moveWordForward()\n      expect(textarea.cursorOffset).toBe(23)\n    })\n\n    it(\"should jump over multiple virtual extmarks when moving backward by word\", async () => {\n      await setup(\"one [V1] two [V2] three\")\n\n      textarea.focus()\n      textarea.cursorOffset = 23\n\n      extmarks.create({ start: 4, end: 9, virtual: true })\n      extmarks.create({ start: 13, end: 18, virtual: true })\n\n      textarea.moveWordBackward()\n      expect(textarea.cursorOffset).toBe(18)\n\n      textarea.moveWordBackward()\n      expect(textarea.cursorOffset).toBe(12)\n\n      textarea.moveWordBackward()\n      expect(textarea.cursorOffset).toBe(9)\n\n      textarea.moveWordBackward()\n      expect(textarea.cursorOffset).toBe(3)\n    })\n  })\n\n  describe(\"setText() Operations\", () => {\n    it(\"should clear all extmarks when setText is called\", async () => {\n      await setup(\"Hello World\")\n\n      const id1 = extmarks.create({ start: 0, end: 5 })\n      const id2 = extmarks.create({ start: 6, end: 11, virtual: true })\n\n      expect(extmarks.getAll().length).toBe(2)\n\n      textarea.setText(\"New Text\")\n\n      expect(extmarks.getAll().length).toBe(0)\n      expect(extmarks.get(id1)).toBeNull()\n      expect(extmarks.get(id2)).toBeNull()\n    })\n\n    it(\"should clear all extmarks on setText\", async () => {\n      await setup(\"Hello World\")\n\n      extmarks.create({ start: 0, end: 5 })\n      extmarks.create({ start: 6, end: 11 })\n\n      expect(extmarks.getAll().length).toBe(2)\n\n      textarea.setText(\"New Text\")\n\n      expect(extmarks.getAll().length).toBe(0)\n    })\n\n    it(\"should allow new extmarks after setText\", async () => {\n      await setup(\"Hello World\")\n\n      extmarks.create({ start: 0, end: 5 })\n      textarea.setText(\"New Text\")\n\n      const newId = extmarks.create({ start: 0, end: 3 })\n      const extmark = extmarks.get(newId)\n\n      expect(extmark).not.toBeNull()\n      expect(extmark?.start).toBe(0)\n      expect(extmark?.end).toBe(3)\n    })\n  })\n\n  describe(\"deleteWordForward() Operations\", () => {\n    it(\"should adjust extmark positions after deleteWordForward before extmark\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 16,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      textarea.deleteWordForward()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(6)\n      expect(extmark?.end).toBe(10)\n      expect(textarea.plainText).toBe(\"world test\")\n    })\n\n    it(\"should remove extmark when deleteWordForward covers it\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      textarea.deleteWordForward()\n\n      expect(extmarks.get(id)).toBeNull()\n      expect(textarea.plainText).toBe(\"world test\")\n    })\n\n    it(\"should not adjust extmark when deleteWordForward after\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 6\n\n      textarea.deleteWordForward()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(0)\n      expect(extmark?.end).toBe(5)\n    })\n  })\n\n  describe(\"deleteWordBackward() Operations\", () => {\n    it(\"should adjust extmark positions after deleteWordBackward before extmark\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 16,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 11\n\n      textarea.deleteWordBackward()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(7)\n      expect(extmark?.end).toBe(11)\n      expect(textarea.plainText).toBe(\"hello  test\")\n    })\n\n    it(\"should remove extmark when deleteWordBackward covers it\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 11\n\n      textarea.deleteWordBackward()\n\n      expect(extmarks.get(id)).toBeNull()\n      expect(textarea.plainText).toBe(\"hello  test\")\n    })\n\n    it(\"should not adjust extmark when deleteWordBackward after\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 16,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 5\n\n      textarea.deleteWordBackward()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(7)\n      expect(extmark?.end).toBe(11)\n      expect(textarea.plainText).toBe(\" world test\")\n    })\n  })\n\n  describe(\"deleteToLineEnd() Operations\", () => {\n    it(\"should remove extmark when deleteToLineEnd covers it\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      textarea.deleteToLineEnd()\n\n      expect(extmarks.get(id)).toBeNull()\n      expect(textarea.plainText).toBe(\"He\")\n    })\n\n    it(\"should partially trim extmark when deleteToLineEnd overlaps end\", async () => {\n      await setup(\"Hello World Extra\")\n\n      const id = extmarks.create({\n        start: 3,\n        end: 8,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 6\n\n      textarea.deleteToLineEnd()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(3)\n      expect(extmark?.end).toBe(6)\n      expect(textarea.plainText).toBe(\"Hello \")\n    })\n\n    it(\"should not adjust extmark when deleteToLineEnd after\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 2,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 5\n\n      textarea.deleteToLineEnd()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(0)\n      expect(extmark?.end).toBe(2)\n      expect(textarea.plainText).toBe(\"Hello\")\n    })\n  })\n\n  describe(\"deleteLine() Operations\", () => {\n    it(\"should adjust extmark positions after deleteLine before extmark\", async () => {\n      await setup(\"Line1\\nLine2\\nLine3\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 17,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 3\n\n      textarea.deleteLine()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(6)\n      expect(extmark?.end).toBe(11)\n      expect(textarea.plainText).toBe(\"Line2\\nLine3\")\n    })\n\n    it(\"should remove extmark when deleteLine on line containing it\", async () => {\n      await setup(\"Line1\\nLine2\\nLine3\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 8\n\n      textarea.deleteLine()\n\n      expect(extmarks.get(id)).toBeNull()\n      expect(textarea.plainText).toBe(\"Line1\\nLine3\")\n    })\n\n    it(\"should not adjust extmark when deleteLine after\", async () => {\n      await setup(\"Line1\\nLine2\\nLine3\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 8\n\n      textarea.deleteLine()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(0)\n      expect(extmark?.end).toBe(5)\n    })\n  })\n\n  describe(\"newLine() Operations\", () => {\n    it(\"should adjust extmark positions after newLine before extmark\", async () => {\n      await setup(\"HelloWorld\")\n\n      const id = extmarks.create({\n        start: 5,\n        end: 10,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      textarea.newLine()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(6)\n      expect(extmark?.end).toBe(11)\n      expect(textarea.plainText).toBe(\"He\\nlloWorld\")\n    })\n\n    it(\"should expand extmark when newLine inside\", async () => {\n      await setup(\"HelloWorld\")\n\n      const id = extmarks.create({\n        start: 2,\n        end: 8,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 5\n\n      textarea.newLine()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(2)\n      expect(extmark?.end).toBe(9)\n    })\n\n    it(\"should not adjust extmark when newLine after\", async () => {\n      await setup(\"HelloWorld\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 10\n\n      textarea.newLine()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(0)\n      expect(extmark?.end).toBe(5)\n    })\n  })\n\n  describe(\"clear() Operations\", () => {\n    it(\"should clear all extmarks when clear is called\", async () => {\n      await setup(\"Hello World\")\n\n      const id1 = extmarks.create({ start: 0, end: 5 })\n      const id2 = extmarks.create({ start: 6, end: 11, virtual: true })\n\n      expect(extmarks.getAll().length).toBe(2)\n\n      textarea.clear()\n\n      expect(extmarks.getAll().length).toBe(0)\n      expect(extmarks.get(id1)).toBeNull()\n      expect(extmarks.get(id2)).toBeNull()\n      expect(textarea.plainText).toBe(\"\")\n    })\n\n    it(\"should clear all extmarks on clear\", async () => {\n      await setup(\"Hello World\")\n\n      extmarks.create({ start: 0, end: 5 })\n      extmarks.create({ start: 6, end: 11 })\n\n      expect(extmarks.getAll().length).toBe(2)\n\n      textarea.clear()\n\n      expect(extmarks.getAll().length).toBe(0)\n    })\n\n    it(\"should allow new extmarks after clear\", async () => {\n      await setup(\"Hello World\")\n\n      extmarks.create({ start: 0, end: 5 })\n      textarea.clear()\n      textarea.insertText(\"New\")\n\n      const newId = extmarks.create({ start: 0, end: 3 })\n      const extmark = extmarks.get(newId)\n\n      expect(extmark).not.toBeNull()\n      expect(extmark?.start).toBe(0)\n      expect(extmark?.end).toBe(3)\n    })\n  })\n\n  describe(\"Selection Deletion\", () => {\n    it(\"should adjust extmarks when deleting selection with backspace\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 16,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"o world test\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(8)\n      expect(extmark?.end).toBe(12)\n    })\n\n    it(\"should adjust extmarks when deleting selection with delete key\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 16,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressKey(\"DELETE\")\n\n      expect(textarea.plainText).toBe(\"o world test\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(8)\n      expect(extmark?.end).toBe(12)\n    })\n\n    it(\"should adjust extmarks when replacing selection with text\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 16,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressKey(\"X\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(8)\n      expect(extmark?.end).toBe(12)\n      expect(textarea.plainText).toBe(\"X world test\")\n    })\n\n    it(\"should remove extmark when selection covers it\", async () => {\n      await setup(\"hello world test\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      for (let i = 0; i < 12; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressBackspace()\n\n      expect(extmarks.get(id)).toBeNull()\n      expect(textarea.plainText).toBe(\"test\")\n    })\n  })\n\n  describe(\"Multiline Selection Deletion\", () => {\n    it(\"should adjust extmarks after deleting multiline selection\", async () => {\n      await setup(\"Line 1\\nLine 2\\nLine 3\\nLine 4\")\n\n      const id = extmarks.create({\n        start: 21,\n        end: 27,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 7\n\n      for (let i = 0; i < 7; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"Line 1\\nLine 3\\nLine 4\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n      expect(extmark?.start).toBe(14)\n      expect(extmark?.end).toBe(20)\n    })\n\n    it(\"should adjust multiple extmarks after deleting multiline selection\", async () => {\n      await setup(\"AAA\\nBBB\\nCCC\\nDDD\")\n\n      const id1 = extmarks.create({\n        start: 8,\n        end: 11,\n      })\n\n      const id2 = extmarks.create({\n        start: 12,\n        end: 15,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      for (let i = 0; i < 8; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"CCC\\nDDD\")\n\n      const extmark1 = extmarks.get(id1)\n      expect(extmark1).not.toBeNull()\n      expect(extmark1?.start).toBe(0)\n      expect(extmark1?.end).toBe(3)\n      expect(textarea.plainText.substring(extmark1!.start, extmark1!.end)).toBe(\"CCC\")\n\n      const extmark2 = extmarks.get(id2)\n      expect(extmark2).not.toBeNull()\n      expect(extmark2?.start).toBe(4)\n      expect(extmark2?.end).toBe(7)\n      expect(textarea.plainText.substring(extmark2!.start, extmark2!.end)).toBe(\"DDD\")\n    })\n\n    it(\"should correctly adjust extmark spanning multiple lines after multiline deletion\", async () => {\n      await setup(\"AAA\\nBBB\\nCCC\\nDDD\\nEEE\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 19,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      for (let i = 0; i < 8; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"CCC\\nDDD\\nEEE\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n      expect(extmark?.start).toBe(4)\n      expect(extmark?.end).toBe(11)\n      expect(textarea.plainText.substring(extmark!.start, extmark!.end)).toBe(\"DDD\\nEEE\")\n    })\n\n    it(\"should handle deletion of selection that partially overlaps extmark start\", async () => {\n      await setup(\"AAA\\nBBB\\nCCC\\nDDD\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 4\n\n      for (let i = 0; i < 6; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"AAA\\nC\\nDDD\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n      expect(extmark?.start).toBe(4)\n      expect(extmark?.end).toBe(5)\n    })\n\n    it(\"should handle deletion across three lines with extmarks after\", async () => {\n      await setup(\"Line1\\nLine2\\nLine3\\nLine4\\nLine5\")\n\n      const id1 = extmarks.create({\n        start: 18,\n        end: 23,\n      })\n\n      const id2 = extmarks.create({\n        start: 24,\n        end: 29,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      for (let i = 0; i < 18; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(textarea.hasSelection()).toBe(true)\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"Line4\\nLine5\")\n\n      const extmark1 = extmarks.get(id1)\n      expect(extmark1).not.toBeNull()\n      expect(extmark1?.start).toBe(0)\n      expect(extmark1?.end).toBe(5)\n      expect(textarea.plainText.substring(extmark1!.start, extmark1!.end)).toBe(\"Line4\")\n\n      const extmark2 = extmarks.get(id2)\n      expect(extmark2).not.toBeNull()\n      expect(extmark2?.start).toBe(6)\n      expect(extmark2?.end).toBe(11)\n      expect(textarea.plainText.substring(extmark2!.start, extmark2!.end)).toBe(\"Line5\")\n    })\n  })\n\n  describe(\"Edge Cases\", () => {\n    it(\"should handle extmark at start of text\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        virtual: true,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      currentMockInput.pressArrow(\"right\")\n      expect(textarea.cursorOffset).toBe(5)\n\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n    })\n\n    it(\"should handle extmark at end of text\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n        virtual: true,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 11\n\n      currentMockInput.pressArrow(\"left\")\n      expect(textarea.cursorOffset).toBe(5)\n\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n    })\n\n    it(\"should handle zero-width extmark\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 5,\n        end: 5,\n      })\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(5)\n      expect(extmark?.end).toBe(5)\n    })\n\n    it(\"should handle overlapping extmarks\", async () => {\n      await setup(\"Hello World\")\n\n      const id1 = extmarks.create({ start: 0, end: 7 })\n      const id2 = extmarks.create({ start: 3, end: 9 })\n\n      const atOffset5 = extmarks.getAtOffset(5)\n      expect(atOffset5.length).toBe(2)\n      expect(atOffset5.map((e) => e.id).sort()).toEqual([id1, id2])\n    })\n\n    it(\"should handle empty text\", async () => {\n      await setup(\"\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 0,\n      })\n\n      const extmark = extmarks.get(id)\n      expect(extmark).not.toBeNull()\n    })\n  })\n\n  describe(\"Virtual Extmark - Cursor Up/Down Movement\", () => {\n    it(\"should not land inside virtual extmark when moving down\", async () => {\n      await setup(\"abc\\n[VIRTUAL]\\ndef\")\n\n      textarea.focus()\n      textarea.cursorOffset = 1\n\n      extmarks.create({\n        start: 4,\n        end: 13,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(1)\n\n      currentMockInput.pressArrow(\"down\")\n      const cursorAfterDown = textarea.cursorOffset\n\n      const isInsideExtmark = cursorAfterDown >= 4 && cursorAfterDown < 13\n      expect(isInsideExtmark).toBe(false)\n    })\n\n    it(\"should not land inside virtual extmark when moving up\", async () => {\n      await setup(\"abc\\n[VIRTUAL]\\ndef\")\n\n      textarea.focus()\n      textarea.cursorOffset = 15\n\n      extmarks.create({\n        start: 4,\n        end: 13,\n        virtual: true,\n      })\n\n      expect(textarea.cursorOffset).toBe(15)\n\n      currentMockInput.pressArrow(\"up\")\n      const cursorAfterUp = textarea.cursorOffset\n\n      const isInsideExtmark = cursorAfterUp >= 4 && cursorAfterUp < 13\n      expect(isInsideExtmark).toBe(false)\n    })\n\n    it(\"should jump to closest boundary when moving down into virtual extmark\", async () => {\n      await setup(\"abc\\n[VIRTUAL]\\ndef\")\n\n      textarea.focus()\n      textarea.cursorOffset = 1\n\n      extmarks.create({\n        start: 4,\n        end: 13,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"down\")\n      const cursorAfterDown = textarea.cursorOffset\n\n      expect(cursorAfterDown === 3 || cursorAfterDown === 13).toBe(true)\n    })\n\n    it(\"should jump to closest boundary when moving up into virtual extmark\", async () => {\n      await setup(\"abc\\n[VIRTUAL]\\ndef\")\n\n      textarea.focus()\n      textarea.cursorOffset = 15\n\n      extmarks.create({\n        start: 4,\n        end: 13,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"up\")\n      const cursorAfterUp = textarea.cursorOffset\n\n      expect(cursorAfterUp === 3 || cursorAfterUp === 13).toBe(true)\n    })\n\n    it(\"should handle multiline virtual extmarks when moving up\", async () => {\n      await setup(\"line1\\n[VIRTUAL\\nMULTILINE]\\nline4\")\n\n      textarea.focus()\n      textarea.cursorOffset = 28\n\n      extmarks.create({\n        start: 6,\n        end: 25,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"up\")\n      currentMockInput.pressArrow(\"up\")\n      const cursorAfterUp = textarea.cursorOffset\n\n      const isInsideExtmark = cursorAfterUp >= 6 && cursorAfterUp < 25\n      expect(isInsideExtmark).toBe(false)\n    })\n\n    it(\"should handle multiline virtual extmarks when moving down\", async () => {\n      await setup(\"line1\\n[VIRTUAL\\nMULTILINE]\\nline4\")\n\n      textarea.focus()\n      textarea.cursorOffset = 3\n\n      extmarks.create({\n        start: 6,\n        end: 25,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"down\")\n      currentMockInput.pressArrow(\"down\")\n      const cursorAfterDown = textarea.cursorOffset\n\n      const isInsideExtmark = cursorAfterDown >= 6 && cursorAfterDown < 25\n      expect(isInsideExtmark).toBe(false)\n    })\n\n    it(\"should not get stuck when moving down into virtual extmark at start of line\", async () => {\n      // Regression test for cursor getting stuck when moving down over\n      // virtual extmarks at the beginning of lines.\n      // Setup:\n      //   Line 0: \"a\"\n      //   Line 1: \"\" (empty)\n      //   Line 2: \"[EXT]\" (virtual extmark starting at column 0)\n      //   Line 3: \"b\"\n      await setup(\"a\\n\\n[EXT]\\nb\")\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      const virtualStart = 3\n      const virtualEnd = 8\n\n      extmarks.create({\n        start: virtualStart,\n        end: virtualEnd,\n        virtual: true,\n      })\n\n      const initialOffset = textarea.cursorOffset\n      expect(initialOffset).toBe(2)\n\n      currentMockInput.pressArrow(\"down\")\n      const cursorAfterDown = textarea.cursorOffset\n\n      expect(cursorAfterDown).toBe(virtualEnd)\n    })\n\n    it(\"should land at trailing text when moving down into line-start virtual extmark\", async () => {\n      await setup(\"a\\n\\n[EXT]tail\\nb\")\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      const virtualStart = 3\n      const virtualEnd = 8\n\n      extmarks.create({\n        start: virtualStart,\n        end: virtualEnd,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"down\")\n\n      const cursorAfterDown = textarea.cursorOffset\n\n      expect(cursorAfterDown).toBe(virtualEnd)\n      expect(textarea.plainText.slice(cursorAfterDown, cursorAfterDown + 4)).toBe(\"tail\")\n    })\n\n    it(\"should not jump past buffer end when moving down into line-start virtual extmark at EOF\", async () => {\n      await setup(\"a\\n\\n[EXT]\")\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      const virtualStart = 3\n      const virtualEnd = 8\n\n      extmarks.create({\n        start: virtualStart,\n        end: virtualEnd,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"down\")\n\n      const cursorAfterDown = textarea.cursorOffset\n\n      expect(cursorAfterDown).toBe(virtualEnd)\n      expect(cursorAfterDown).toBe(textarea.plainText.length)\n    })\n\n    it(\"should navigate past virtual extmark at line start with repeated down presses\", async () => {\n      await setup(\"abc\\n\\n[EXTMARK]\\n\\nxyz\")\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      const virtualStart = 5\n      const virtualEnd = 14\n\n      extmarks.create({\n        start: virtualStart,\n        end: virtualEnd,\n        virtual: true,\n      })\n\n      currentMockInput.pressArrow(\"down\")\n      currentMockInput.pressArrow(\"down\")\n      const afterExtmark = textarea.cursorOffset\n\n      expect(afterExtmark).toBe(virtualEnd)\n\n      currentMockInput.pressArrow(\"down\")\n      currentMockInput.pressArrow(\"down\")\n      const finalOffset = textarea.cursorOffset\n\n      const xyzStart = textarea.plainText.indexOf(\"xyz\")\n      expect(finalOffset).toBeGreaterThanOrEqual(xyzStart)\n      expect(finalOffset).toBeLessThanOrEqual(textarea.plainText.length)\n    })\n  })\n\n  describe(\"TypeId Operations\", () => {\n    it(\"should create extmark with default typeId 0\", async () => {\n      await setup()\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.typeId).toBe(0)\n    })\n\n    it(\"should create extmark with custom typeId\", async () => {\n      await setup()\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        typeId: 42,\n      })\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.typeId).toBe(42)\n    })\n\n    it(\"should retrieve all extmarks for a specific typeId\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({ start: 0, end: 5, typeId: 1 })\n      const id2 = extmarks.create({ start: 6, end: 11, typeId: 1 })\n      const id3 = extmarks.create({ start: 12, end: 15, typeId: 2 })\n\n      const type1Marks = extmarks.getAllForTypeId(1)\n      expect(type1Marks.length).toBe(2)\n      expect(type1Marks.map((e) => e.id).sort()).toEqual([id1, id2])\n\n      const type2Marks = extmarks.getAllForTypeId(2)\n      expect(type2Marks.length).toBe(1)\n      expect(type2Marks[0].id).toBe(id3)\n    })\n\n    it(\"should return empty array for non-existent typeId\", async () => {\n      await setup()\n\n      extmarks.create({ start: 0, end: 5, typeId: 1 })\n\n      const noMarks = extmarks.getAllForTypeId(999)\n      expect(noMarks.length).toBe(0)\n    })\n\n    it(\"should handle multiple extmarks with same typeId\", async () => {\n      await setup()\n\n      const ids = []\n      for (let i = 0; i < 10; i++) {\n        ids.push(extmarks.create({ start: i, end: i + 1, typeId: 5 }))\n      }\n\n      const type5Marks = extmarks.getAllForTypeId(5)\n      expect(type5Marks.length).toBe(10)\n      expect(type5Marks.map((e) => e.id).sort()).toEqual(ids.sort())\n    })\n\n    it(\"should remove extmark from typeId index when deleted\", async () => {\n      await setup()\n\n      const id = extmarks.create({ start: 0, end: 5, typeId: 3 })\n\n      let type3Marks = extmarks.getAllForTypeId(3)\n      expect(type3Marks.length).toBe(1)\n\n      extmarks.delete(id)\n\n      type3Marks = extmarks.getAllForTypeId(3)\n      expect(type3Marks.length).toBe(0)\n    })\n\n    it(\"should clear all typeId indexes when clear is called\", async () => {\n      await setup()\n\n      extmarks.create({ start: 0, end: 5, typeId: 1 })\n      extmarks.create({ start: 6, end: 11, typeId: 2 })\n      extmarks.create({ start: 12, end: 15, typeId: 3 })\n\n      extmarks.clear()\n\n      expect(extmarks.getAllForTypeId(1).length).toBe(0)\n      expect(extmarks.getAllForTypeId(2).length).toBe(0)\n      expect(extmarks.getAllForTypeId(3).length).toBe(0)\n    })\n\n    it(\"should maintain typeId through text operations\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n        typeId: 7,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"X\")\n      currentMockInput.pressKey(\"X\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.typeId).toBe(7)\n\n      const type7Marks = extmarks.getAllForTypeId(7)\n      expect(type7Marks.length).toBe(1)\n      expect(type7Marks[0].id).toBe(id)\n    })\n\n    it(\"should group virtual and non-virtual extmarks by typeId\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({ start: 0, end: 5, typeId: 10, virtual: false })\n      const id2 = extmarks.create({ start: 6, end: 11, typeId: 10, virtual: true })\n      const id3 = extmarks.create({ start: 12, end: 15, typeId: 10, virtual: false })\n\n      const type10Marks = extmarks.getAllForTypeId(10)\n      expect(type10Marks.length).toBe(3)\n\n      const virtualMarks = type10Marks.filter((e) => e.virtual)\n      const nonVirtualMarks = type10Marks.filter((e) => !e.virtual)\n\n      expect(virtualMarks.length).toBe(1)\n      expect(nonVirtualMarks.length).toBe(2)\n    })\n\n    it(\"should handle typeId 0 as default\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({ start: 0, end: 5 })\n      const id2 = extmarks.create({ start: 6, end: 11, typeId: 0 })\n      const id3 = extmarks.create({ start: 12, end: 15 })\n\n      const type0Marks = extmarks.getAllForTypeId(0)\n      expect(type0Marks.length).toBe(3)\n      expect(type0Marks.map((e) => e.id).sort()).toEqual([id1, id2, id3])\n    })\n\n    it(\"should remove extmark from typeId index on deletion during backspace\", async () => {\n      await setup(\"abc[LINK]def\")\n\n      textarea.focus()\n      textarea.cursorOffset = 9\n\n      const id = extmarks.create({\n        start: 3,\n        end: 9,\n        virtual: true,\n        typeId: 15,\n      })\n\n      let type15Marks = extmarks.getAllForTypeId(15)\n      expect(type15Marks.length).toBe(1)\n\n      currentMockInput.pressBackspace()\n\n      expect(extmarks.get(id)).toBeNull()\n\n      type15Marks = extmarks.getAllForTypeId(15)\n      expect(type15Marks.length).toBe(0)\n    })\n\n    it(\"should remove extmark from typeId index on deletion during delete key\", async () => {\n      await setup(\"abc[LINK]def\")\n\n      textarea.focus()\n      textarea.cursorOffset = 3\n\n      const id = extmarks.create({\n        start: 3,\n        end: 9,\n        virtual: true,\n        typeId: 20,\n      })\n\n      let type20Marks = extmarks.getAllForTypeId(20)\n      expect(type20Marks.length).toBe(1)\n\n      currentMockInput.pressKey(\"DELETE\")\n\n      expect(extmarks.get(id)).toBeNull()\n\n      type20Marks = extmarks.getAllForTypeId(20)\n      expect(type20Marks.length).toBe(0)\n    })\n\n    it(\"should handle getAllForTypeId on destroyed controller\", async () => {\n      await setup()\n\n      extmarks.create({ start: 0, end: 5, typeId: 1 })\n\n      extmarks.destroy()\n\n      const type1Marks = extmarks.getAllForTypeId(1)\n      expect(type1Marks.length).toBe(0)\n    })\n\n    it(\"should support multiple different typeIds simultaneously\", async () => {\n      await setup(\"The quick brown fox jumps over the lazy dog\")\n\n      const linkId1 = extmarks.create({ start: 0, end: 3, typeId: 1 })\n      const linkId2 = extmarks.create({ start: 10, end: 15, typeId: 1 })\n\n      const tagId1 = extmarks.create({ start: 4, end: 9, typeId: 2 })\n      const tagId2 = extmarks.create({ start: 16, end: 19, typeId: 2 })\n\n      const markerId = extmarks.create({ start: 20, end: 25, typeId: 3 })\n\n      const links = extmarks.getAllForTypeId(1)\n      expect(links.length).toBe(2)\n      expect(links.map((e) => e.id).sort()).toEqual([linkId1, linkId2])\n\n      const tags = extmarks.getAllForTypeId(2)\n      expect(tags.length).toBe(2)\n      expect(tags.map((e) => e.id).sort()).toEqual([tagId1, tagId2])\n\n      const markers = extmarks.getAllForTypeId(3)\n      expect(markers.length).toBe(1)\n      expect(markers[0].id).toBe(markerId)\n\n      const allExtmarks = extmarks.getAll()\n      expect(allExtmarks.length).toBe(5)\n    })\n\n    it(\"should preserve typeId when extmark is adjusted after insertion\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n        typeId: 50,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"Z\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.typeId).toBe(50)\n      expect(extmark?.start).toBe(7)\n      expect(extmark?.end).toBe(12)\n\n      const type50Marks = extmarks.getAllForTypeId(50)\n      expect(type50Marks.length).toBe(1)\n    })\n\n    it(\"should preserve typeId when extmark is adjusted after deletion\", async () => {\n      await setup(\"XXHello World\")\n\n      const id = extmarks.create({\n        start: 8,\n        end: 13,\n        typeId: 60,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n      currentMockInput.pressBackspace()\n      currentMockInput.pressBackspace()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.typeId).toBe(60)\n      expect(extmark?.start).toBe(6)\n      expect(extmark?.end).toBe(11)\n\n      const type60Marks = extmarks.getAllForTypeId(60)\n      expect(type60Marks.length).toBe(1)\n    })\n  })\n\n  describe(\"Undo/Redo with Extmarks\", () => {\n    it(\"should restore extmark after undo of text insertion\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        styleId: 1,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 3\n      currentMockInput.pressKey(\"X\")\n\n      const extmarkAfterInsert = extmarks.get(id)\n      expect(extmarkAfterInsert?.start).toBe(0)\n      expect(extmarkAfterInsert?.end).toBe(6)\n\n      textarea.undo()\n\n      const extmarkAfterUndo = extmarks.get(id)\n      expect(extmarkAfterUndo?.start).toBe(0)\n      expect(extmarkAfterUndo?.end).toBe(5)\n    })\n\n    it(\"should restore extmark after undo of text deletion\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n        styleId: 1,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"DELETE\")\n\n      const extmarkAfterDelete = extmarks.get(id)\n      expect(extmarkAfterDelete?.start).toBe(5)\n      expect(extmarkAfterDelete?.end).toBe(10)\n\n      textarea.undo()\n\n      const extmarkAfterUndo = extmarks.get(id)\n      expect(extmarkAfterUndo?.start).toBe(6)\n      expect(extmarkAfterUndo?.end).toBe(11)\n    })\n\n    it(\"should restore extmark after redo\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        styleId: 1,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 3\n      currentMockInput.pressKey(\"X\")\n\n      const extmarkAfterInsert = extmarks.get(id)\n      expect(extmarkAfterInsert?.start).toBe(0)\n      expect(extmarkAfterInsert?.end).toBe(6)\n\n      textarea.undo()\n\n      const extmarkAfterUndo = extmarks.get(id)\n      expect(extmarkAfterUndo?.start).toBe(0)\n      expect(extmarkAfterUndo?.end).toBe(5)\n\n      textarea.redo()\n\n      const extmarkAfterRedo = extmarks.get(id)\n      expect(extmarkAfterRedo?.start).toBe(0)\n      expect(extmarkAfterRedo?.end).toBe(6)\n    })\n\n    it(\"should restore deleted virtual extmark after undo\", async () => {\n      await setup(\"abc[LINK]def\")\n\n      textarea.focus()\n      textarea.cursorOffset = 9\n\n      const id = extmarks.create({\n        start: 3,\n        end: 9,\n        virtual: true,\n      })\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"abcdef\")\n      expect(extmarks.get(id)).toBeNull()\n\n      textarea.undo()\n\n      const extmarkAfterUndo = extmarks.get(id)\n      expect(extmarkAfterUndo).not.toBeNull()\n      expect(extmarkAfterUndo?.start).toBe(3)\n      expect(extmarkAfterUndo?.end).toBe(9)\n      expect(extmarkAfterUndo?.virtual).toBe(true)\n      expect(textarea.plainText).toBe(\"abc[LINK]def\")\n    })\n\n    it(\"should handle multiple undo/redo operations\", async () => {\n      await setup(\"Test\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 4,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n\n      currentMockInput.pressKey(\"1\")\n      expect(extmarks.get(id)?.end).toBe(5)\n\n      currentMockInput.pressKey(\"2\")\n      expect(extmarks.get(id)?.end).toBe(6)\n\n      currentMockInput.pressKey(\"3\")\n      expect(extmarks.get(id)?.end).toBe(7)\n\n      textarea.undo()\n      expect(extmarks.get(id)?.end).toBe(6)\n\n      textarea.undo()\n      expect(extmarks.get(id)?.end).toBe(5)\n\n      textarea.undo()\n      expect(extmarks.get(id)?.end).toBe(4)\n\n      textarea.redo()\n      expect(extmarks.get(id)?.end).toBe(5)\n\n      textarea.redo()\n      expect(extmarks.get(id)?.end).toBe(6)\n\n      textarea.redo()\n      expect(extmarks.get(id)?.end).toBe(7)\n    })\n\n    it(\"should restore multiple extmarks after undo\", async () => {\n      await setup(\"Hello World Test\")\n\n      const id1 = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      const id2 = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"X\")\n\n      expect(extmarks.get(id1)?.start).toBe(1)\n      expect(extmarks.get(id1)?.end).toBe(6)\n      expect(extmarks.get(id2)?.start).toBe(7)\n      expect(extmarks.get(id2)?.end).toBe(12)\n\n      textarea.undo()\n\n      expect(extmarks.get(id1)?.start).toBe(0)\n      expect(extmarks.get(id1)?.end).toBe(5)\n      expect(extmarks.get(id2)?.start).toBe(6)\n      expect(extmarks.get(id2)?.end).toBe(11)\n    })\n\n    it(\"should handle undo after backspace that deleted virtual extmark\", async () => {\n      await setup(\"text[VIRTUAL]more\")\n\n      textarea.focus()\n      textarea.cursorOffset = 13\n\n      const id = extmarks.create({\n        start: 4,\n        end: 13,\n        virtual: true,\n      })\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\"textmore\")\n      expect(extmarks.get(id)).toBeNull()\n\n      textarea.undo()\n\n      const restoredExtmark = extmarks.get(id)\n      expect(restoredExtmark).not.toBeNull()\n      expect(restoredExtmark?.start).toBe(4)\n      expect(restoredExtmark?.end).toBe(13)\n      expect(restoredExtmark?.virtual).toBe(true)\n    })\n\n    it(\"should restore extmark IDs correctly after undo\", async () => {\n      await setup(\"Test\")\n\n      const id1 = extmarks.create({\n        start: 0,\n        end: 2,\n      })\n\n      const id2 = extmarks.create({\n        start: 2,\n        end: 4,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"X\")\n\n      textarea.undo()\n\n      expect(extmarks.get(id1)).not.toBeNull()\n      expect(extmarks.get(id2)).not.toBeNull()\n      expect(extmarks.get(id1)?.id).toBe(id1)\n      expect(extmarks.get(id2)?.id).toBe(id2)\n    })\n\n    it(\"should preserve extmark data after undo/redo\", async () => {\n      await setup(\"Hello\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        data: { type: \"link\", url: \"https://example.com\" },\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 5\n      currentMockInput.pressKey(\"X\")\n\n      textarea.undo()\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.data).toEqual({ type: \"link\", url: \"https://example.com\" })\n\n      textarea.redo()\n\n      const extmarkAfterRedo = extmarks.get(id)\n      expect(extmarkAfterRedo?.data).toEqual({ type: \"link\", url: \"https://example.com\" })\n    })\n\n    it(\"should handle undo/redo with multiline extmarks\", async () => {\n      await setup(\"Line1\\nLine2\\nLine3\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"X\")\n\n      expect(extmarks.get(id)?.start).toBe(7)\n      expect(extmarks.get(id)?.end).toBe(12)\n\n      textarea.undo()\n\n      expect(extmarks.get(id)?.start).toBe(6)\n      expect(extmarks.get(id)?.end).toBe(11)\n\n      textarea.redo()\n\n      expect(extmarks.get(id)?.start).toBe(7)\n      expect(extmarks.get(id)?.end).toBe(12)\n    })\n\n    it(\"should handle undo after deleteRange\", async () => {\n      await setup(\"Hello World Test\")\n\n      const id = extmarks.create({\n        start: 12,\n        end: 16,\n      })\n\n      textarea.focus()\n      textarea.deleteRange(0, 0, 0, 6)\n\n      expect(extmarks.get(id)?.start).toBe(6)\n      expect(extmarks.get(id)?.end).toBe(10)\n\n      textarea.undo()\n\n      expect(extmarks.get(id)?.start).toBe(12)\n      expect(extmarks.get(id)?.end).toBe(16)\n    })\n\n    it(\"should maintain correct nextId after undo/redo\", async () => {\n      await setup(\"Test\")\n\n      extmarks.create({ start: 0, end: 2 })\n\n      textarea.focus()\n      textarea.cursorOffset = 4\n      currentMockInput.pressKey(\"X\")\n\n      textarea.undo()\n\n      const newId = extmarks.create({ start: 2, end: 4 })\n\n      expect(newId).toBe(2)\n    })\n\n    it(\"should handle undo/redo of selection deletion\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      currentMockInput.pressBackspace()\n\n      expect(textarea.plainText).toBe(\" World\")\n      expect(extmarks.get(id)?.start).toBe(1)\n      expect(extmarks.get(id)?.end).toBe(6)\n\n      textarea.undo()\n\n      expect(textarea.plainText).toBe(\"Hello World\")\n      expect(extmarks.get(id)?.start).toBe(6)\n      expect(extmarks.get(id)?.end).toBe(11)\n    })\n  })\n\n  describe(\"Type Registry\", () => {\n    it(\"should register a type name and return a unique typeId\", async () => {\n      await setup()\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      expect(linkTypeId).toBe(1)\n\n      const tagTypeId = extmarks.registerType(\"tag\")\n      expect(tagTypeId).toBe(2)\n\n      expect(linkTypeId).not.toBe(tagTypeId)\n    })\n\n    it(\"should return the same typeId for duplicate type name registration\", async () => {\n      await setup()\n\n      const firstId = extmarks.registerType(\"link\")\n      const secondId = extmarks.registerType(\"link\")\n\n      expect(firstId).toBe(secondId)\n    })\n\n    it(\"should resolve typeName to typeId\", async () => {\n      await setup()\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      const resolvedId = extmarks.getTypeId(\"link\")\n\n      expect(resolvedId).toBe(linkTypeId)\n    })\n\n    it(\"should return null for unregistered typeName\", async () => {\n      await setup()\n\n      const resolvedId = extmarks.getTypeId(\"nonexistent\")\n      expect(resolvedId).toBeNull()\n    })\n\n    it(\"should resolve typeId to typeName\", async () => {\n      await setup()\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      const resolvedName = extmarks.getTypeName(linkTypeId)\n\n      expect(resolvedName).toBe(\"link\")\n    })\n\n    it(\"should return null for unregistered typeId\", async () => {\n      await setup()\n\n      const resolvedName = extmarks.getTypeName(999)\n      expect(resolvedName).toBeNull()\n    })\n\n    it(\"should create extmark with registered type\", async () => {\n      await setup()\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      const extmarkId = extmarks.create({\n        start: 0,\n        end: 5,\n        typeId: linkTypeId,\n      })\n\n      const extmark = extmarks.get(extmarkId)\n      expect(extmark?.typeId).toBe(linkTypeId)\n    })\n\n    it(\"should retrieve extmarks by registered type name\", async () => {\n      await setup()\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      const tagTypeId = extmarks.registerType(\"tag\")\n\n      const linkId1 = extmarks.create({ start: 0, end: 5, typeId: linkTypeId })\n      const linkId2 = extmarks.create({ start: 6, end: 11, typeId: linkTypeId })\n      const tagId = extmarks.create({ start: 12, end: 15, typeId: tagTypeId })\n\n      const linkExtmarks = extmarks.getAllForTypeId(linkTypeId)\n      expect(linkExtmarks.length).toBe(2)\n      expect(linkExtmarks.map((e) => e.id).sort()).toEqual([linkId1, linkId2])\n\n      const tagExtmarks = extmarks.getAllForTypeId(tagTypeId)\n      expect(tagExtmarks.length).toBe(1)\n      expect(tagExtmarks[0].id).toBe(tagId)\n    })\n\n    it(\"should handle multiple type registrations\", async () => {\n      await setup()\n\n      const types = [\"link\", \"tag\", \"marker\", \"highlight\", \"error\"]\n      const typeIds = types.map((type) => extmarks.registerType(type))\n\n      expect(new Set(typeIds).size).toBe(types.length)\n\n      for (let i = 0; i < types.length; i++) {\n        expect(extmarks.getTypeId(types[i])).toBe(typeIds[i])\n        expect(extmarks.getTypeName(typeIds[i])).toBe(types[i])\n      }\n    })\n\n    it(\"should preserve type registry across text operations\", async () => {\n      await setup(\"Hello World\")\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      const extmarkId = extmarks.create({\n        start: 0,\n        end: 5,\n        typeId: linkTypeId,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"X\")\n\n      expect(extmarks.getTypeId(\"link\")).toBe(linkTypeId)\n      expect(extmarks.getTypeName(linkTypeId)).toBe(\"link\")\n\n      const extmark = extmarks.get(extmarkId)\n      expect(extmark?.typeId).toBe(linkTypeId)\n    })\n\n    it(\"should clear type registry on destroy\", async () => {\n      await setup()\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      extmarks.registerType(\"tag\")\n\n      extmarks.destroy()\n\n      expect(extmarks.getTypeId(\"link\")).toBeNull()\n      expect(extmarks.getTypeName(linkTypeId)).toBeNull()\n    })\n\n    it(\"should throw error when registering type on destroyed controller\", async () => {\n      await setup()\n\n      extmarks.destroy()\n\n      expect(() => {\n        extmarks.registerType(\"link\")\n      }).toThrow(\"ExtmarksController is destroyed\")\n    })\n\n    it(\"should support workflow of register then create extmarks\", async () => {\n      await setup(\"The quick brown fox\")\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      const emphasisTypeId = extmarks.registerType(\"emphasis\")\n\n      const link1 = extmarks.create({ start: 0, end: 3, typeId: linkTypeId, virtual: true })\n      const link2 = extmarks.create({ start: 10, end: 15, typeId: linkTypeId, virtual: true })\n      const emphasis1 = extmarks.create({ start: 4, end: 9, typeId: emphasisTypeId })\n\n      const links = extmarks.getAllForTypeId(linkTypeId)\n      expect(links.length).toBe(2)\n      expect(links.map((e) => e.id).sort()).toEqual([link1, link2])\n\n      const emphases = extmarks.getAllForTypeId(emphasisTypeId)\n      expect(emphases.length).toBe(1)\n      expect(emphases[0].id).toBe(emphasis1)\n\n      expect(extmarks.getTypeName(linkTypeId)).toBe(\"link\")\n      expect(extmarks.getTypeName(emphasisTypeId)).toBe(\"emphasis\")\n    })\n\n    it(\"should handle type names with special characters\", async () => {\n      await setup()\n\n      const typeId1 = extmarks.registerType(\"my-type\")\n      const typeId2 = extmarks.registerType(\"my_type\")\n      const typeId3 = extmarks.registerType(\"my.type\")\n      const typeId4 = extmarks.registerType(\"my:type\")\n\n      expect(extmarks.getTypeId(\"my-type\")).toBe(typeId1)\n      expect(extmarks.getTypeId(\"my_type\")).toBe(typeId2)\n      expect(extmarks.getTypeId(\"my.type\")).toBe(typeId3)\n      expect(extmarks.getTypeId(\"my:type\")).toBe(typeId4)\n\n      expect(typeId1).not.toBe(typeId2)\n      expect(typeId2).not.toBe(typeId3)\n      expect(typeId3).not.toBe(typeId4)\n    })\n\n    it(\"should handle empty string as type name\", async () => {\n      await setup()\n\n      const typeId = extmarks.registerType(\"\")\n      expect(typeId).toBe(1)\n      expect(extmarks.getTypeId(\"\")).toBe(typeId)\n      expect(extmarks.getTypeName(typeId)).toBe(\"\")\n    })\n\n    it(\"should return null for getTypeId and getTypeName on destroyed controller\", async () => {\n      await setup()\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      extmarks.destroy()\n\n      expect(extmarks.getTypeId(\"link\")).toBeNull()\n      expect(extmarks.getTypeName(linkTypeId)).toBeNull()\n    })\n\n    it(\"should allow re-registration after clear\", async () => {\n      await setup()\n\n      const firstLinkId = extmarks.registerType(\"link\")\n      extmarks.create({ start: 0, end: 5, typeId: firstLinkId })\n\n      extmarks.clear()\n\n      expect(extmarks.getTypeId(\"link\")).toBe(firstLinkId)\n\n      const newExtmarkId = extmarks.create({ start: 0, end: 3, typeId: firstLinkId })\n      expect(extmarks.get(newExtmarkId)?.typeId).toBe(firstLinkId)\n    })\n\n    it(\"should support case-sensitive type names\", async () => {\n      await setup()\n\n      const lowerId = extmarks.registerType(\"link\")\n      const upperId = extmarks.registerType(\"Link\")\n      const upperCaseId = extmarks.registerType(\"LINK\")\n\n      expect(lowerId).not.toBe(upperId)\n      expect(upperId).not.toBe(upperCaseId)\n      expect(lowerId).not.toBe(upperCaseId)\n\n      expect(extmarks.getTypeId(\"link\")).toBe(lowerId)\n      expect(extmarks.getTypeId(\"Link\")).toBe(upperId)\n      expect(extmarks.getTypeId(\"LINK\")).toBe(upperCaseId)\n    })\n\n    it(\"should maintain typeId sequence independent of extmark IDs\", async () => {\n      await setup()\n\n      const extmarkId1 = extmarks.create({ start: 0, end: 1 })\n      const extmarkId2 = extmarks.create({ start: 1, end: 2 })\n\n      const linkTypeId = extmarks.registerType(\"link\")\n      const tagTypeId = extmarks.registerType(\"tag\")\n\n      expect(linkTypeId).toBe(1)\n      expect(tagTypeId).toBe(2)\n      expect(extmarkId1).toBeGreaterThanOrEqual(1)\n      expect(extmarkId2).toBeGreaterThanOrEqual(2)\n    })\n\n    it(\"should handle numeric-like string type names\", async () => {\n      await setup()\n\n      const typeId1 = extmarks.registerType(\"123\")\n      const typeId2 = extmarks.registerType(\"456\")\n\n      expect(extmarks.getTypeId(\"123\")).toBe(typeId1)\n      expect(extmarks.getTypeId(\"456\")).toBe(typeId2)\n      expect(typeId1).not.toBe(typeId2)\n    })\n\n    it(\"should support long type names\", async () => {\n      await setup()\n\n      const longName = \"a\".repeat(1000)\n      const typeId = extmarks.registerType(longName)\n\n      expect(extmarks.getTypeId(longName)).toBe(typeId)\n      expect(extmarks.getTypeName(typeId)).toBe(longName)\n    })\n  })\n\n  describe(\"Metadata Operations\", () => {\n    it(\"should store and retrieve metadata for extmark\", async () => {\n      await setup()\n\n      const metadata = { url: \"https://example.com\", title: \"Example\" }\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata,\n      })\n\n      const retrieved = extmarks.getMetadataFor(id)\n      expect(retrieved).toEqual(metadata)\n    })\n\n    it(\"should return undefined for extmark without metadata\", async () => {\n      await setup()\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n      })\n\n      const retrieved = extmarks.getMetadataFor(id)\n      expect(retrieved).toBeUndefined()\n    })\n\n    it(\"should return undefined for non-existent extmark\", async () => {\n      await setup()\n\n      const retrieved = extmarks.getMetadataFor(999)\n      expect(retrieved).toBeUndefined()\n    })\n\n    it(\"should handle different metadata types\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: { type: \"object\", value: 42 },\n      })\n\n      const id2 = extmarks.create({\n        start: 6,\n        end: 11,\n        metadata: \"string metadata\",\n      })\n\n      const id3 = extmarks.create({\n        start: 12,\n        end: 15,\n        metadata: 123,\n      })\n\n      const id4 = extmarks.create({\n        start: 16,\n        end: 20,\n        metadata: true,\n      })\n\n      const id5 = extmarks.create({\n        start: 21,\n        end: 25,\n        metadata: [\"array\", \"metadata\"],\n      })\n\n      expect(extmarks.getMetadataFor(id1)).toEqual({ type: \"object\", value: 42 })\n      expect(extmarks.getMetadataFor(id2)).toBe(\"string metadata\")\n      expect(extmarks.getMetadataFor(id3)).toBe(123)\n      expect(extmarks.getMetadataFor(id4)).toBe(true)\n      expect(extmarks.getMetadataFor(id5)).toEqual([\"array\", \"metadata\"])\n    })\n\n    it(\"should handle null metadata\", async () => {\n      await setup()\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: null,\n      })\n\n      const retrieved = extmarks.getMetadataFor(id)\n      expect(retrieved).toBeNull()\n    })\n\n    it(\"should preserve metadata when extmark is adjusted\", async () => {\n      await setup(\"Hello World\")\n\n      const metadata = { label: \"important\" }\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n        metadata,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 0\n      currentMockInput.pressKey(\"X\")\n      currentMockInput.pressKey(\"X\")\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.start).toBe(8)\n      expect(extmark?.end).toBe(13)\n\n      const retrieved = extmarks.getMetadataFor(id)\n      expect(retrieved).toEqual(metadata)\n    })\n\n    it(\"should remove metadata when extmark is deleted\", async () => {\n      await setup()\n\n      const metadata = { data: \"test\" }\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata,\n      })\n\n      expect(extmarks.getMetadataFor(id)).toEqual(metadata)\n\n      extmarks.delete(id)\n\n      expect(extmarks.getMetadataFor(id)).toBeUndefined()\n    })\n\n    it(\"should clear all metadata when clear is called\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: { key: \"value1\" },\n      })\n\n      const id2 = extmarks.create({\n        start: 6,\n        end: 11,\n        metadata: { key: \"value2\" },\n      })\n\n      extmarks.clear()\n\n      expect(extmarks.getMetadataFor(id1)).toBeUndefined()\n      expect(extmarks.getMetadataFor(id2)).toBeUndefined()\n    })\n\n    it(\"should remove metadata when virtual extmark is deleted via backspace\", async () => {\n      await setup(\"abc[LINK]def\")\n\n      textarea.focus()\n      textarea.cursorOffset = 9\n\n      const metadata = { url: \"https://test.com\" }\n      const id = extmarks.create({\n        start: 3,\n        end: 9,\n        virtual: true,\n        metadata,\n      })\n\n      expect(extmarks.getMetadataFor(id)).toEqual(metadata)\n\n      currentMockInput.pressBackspace()\n\n      expect(extmarks.get(id)).toBeNull()\n      expect(extmarks.getMetadataFor(id)).toBeUndefined()\n    })\n\n    it(\"should handle metadata with nested objects\", async () => {\n      await setup()\n\n      const metadata = {\n        user: {\n          id: 123,\n          name: \"John Doe\",\n          settings: {\n            theme: \"dark\",\n            notifications: true,\n          },\n        },\n        timestamp: Date.now(),\n      }\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata,\n      })\n\n      const retrieved = extmarks.getMetadataFor(id)\n      expect(retrieved).toEqual(metadata)\n    })\n\n    it(\"should store independent metadata for multiple extmarks\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: { id: 1, color: \"red\" },\n      })\n\n      const id2 = extmarks.create({\n        start: 6,\n        end: 11,\n        metadata: { id: 2, color: \"blue\" },\n      })\n\n      const id3 = extmarks.create({\n        start: 12,\n        end: 15,\n        metadata: { id: 3, color: \"green\" },\n      })\n\n      expect(extmarks.getMetadataFor(id1)).toEqual({ id: 1, color: \"red\" })\n      expect(extmarks.getMetadataFor(id2)).toEqual({ id: 2, color: \"blue\" })\n      expect(extmarks.getMetadataFor(id3)).toEqual({ id: 3, color: \"green\" })\n    })\n\n    it(\"should handle metadata with both metadata and data fields\", async () => {\n      await setup()\n\n      const data = { oldField: \"data\" }\n      const metadata = { newField: \"metadata\" }\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        data,\n        metadata,\n      })\n\n      const extmark = extmarks.get(id)\n      expect(extmark?.data).toEqual(data)\n      expect(extmarks.getMetadataFor(id)).toEqual(metadata)\n    })\n\n    it(\"should return undefined when getting metadata on destroyed controller\", async () => {\n      await setup()\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: { test: \"data\" },\n      })\n\n      extmarks.destroy()\n\n      expect(extmarks.getMetadataFor(id)).toBeUndefined()\n    })\n\n    it(\"should handle metadata with special values\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: undefined,\n      })\n\n      const id2 = extmarks.create({\n        start: 6,\n        end: 11,\n        metadata: 0,\n      })\n\n      const id3 = extmarks.create({\n        start: 12,\n        end: 15,\n        metadata: \"\",\n      })\n\n      const id4 = extmarks.create({\n        start: 16,\n        end: 20,\n        metadata: false,\n      })\n\n      expect(extmarks.getMetadataFor(id1)).toBeUndefined()\n      expect(extmarks.getMetadataFor(id2)).toBe(0)\n      expect(extmarks.getMetadataFor(id3)).toBe(\"\")\n      expect(extmarks.getMetadataFor(id4)).toBe(false)\n    })\n\n    it(\"should handle metadata for extmarks with same range\", async () => {\n      await setup()\n\n      const id1 = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: { layer: 1 },\n      })\n\n      const id2 = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: { layer: 2 },\n      })\n\n      expect(extmarks.getMetadataFor(id1)).toEqual({ layer: 1 })\n      expect(extmarks.getMetadataFor(id2)).toEqual({ layer: 2 })\n    })\n\n    it(\"should preserve metadata through text insertion\", async () => {\n      await setup(\"Hello World\")\n\n      const metadata = { type: \"highlight\", priority: 10 }\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n      currentMockInput.pressKey(\"Z\")\n\n      expect(extmarks.getMetadataFor(id)).toEqual(metadata)\n    })\n\n    it(\"should preserve metadata through text deletion\", async () => {\n      await setup(\"XXHello World\")\n\n      const metadata = { category: \"text\" }\n      const id = extmarks.create({\n        start: 8,\n        end: 13,\n        metadata,\n      })\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n      currentMockInput.pressBackspace()\n      currentMockInput.pressBackspace()\n\n      expect(extmarks.getMetadataFor(id)).toEqual(metadata)\n    })\n\n    it(\"should remove metadata when extmark range is deleted\", async () => {\n      await setup(\"Hello World\")\n\n      const metadata = { info: \"will be deleted\" }\n      const id = extmarks.create({\n        start: 6,\n        end: 11,\n        metadata,\n      })\n\n      textarea.deleteRange(0, 6, 0, 11)\n\n      expect(extmarks.get(id)).toBeNull()\n      expect(extmarks.getMetadataFor(id)).toBeUndefined()\n    })\n\n    it(\"should handle metadata for virtual extmarks\", async () => {\n      await setup(\"abcdefgh\")\n\n      const metadata = { virtual: true, link: \"https://example.com\" }\n      const id = extmarks.create({\n        start: 3,\n        end: 6,\n        virtual: true,\n        metadata,\n      })\n\n      expect(extmarks.getMetadataFor(id)).toEqual(metadata)\n\n      textarea.focus()\n      textarea.cursorOffset = 2\n      currentMockInput.pressArrow(\"right\")\n\n      expect(textarea.cursorOffset).toBe(6)\n      expect(extmarks.getMetadataFor(id)).toEqual(metadata)\n    })\n\n    it(\"should handle large metadata objects\", async () => {\n      await setup()\n\n      const largeMetadata = {\n        items: Array.from({ length: 1000 }, (_, i) => ({ id: i, value: `item-${i}` })),\n        description: \"A\".repeat(10000),\n      }\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: largeMetadata,\n      })\n\n      const retrieved = extmarks.getMetadataFor(id)\n      expect(retrieved).toEqual(largeMetadata)\n      expect(retrieved.items.length).toBe(1000)\n      expect(retrieved.description.length).toBe(10000)\n    })\n\n    it(\"should handle metadata with functions\", async () => {\n      await setup()\n\n      const metadata = {\n        onClick: () => \"clicked\",\n        onHover: (x: number) => x * 2,\n      }\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata,\n      })\n\n      const retrieved = extmarks.getMetadataFor(id)\n      expect(typeof retrieved.onClick).toBe(\"function\")\n      expect(typeof retrieved.onHover).toBe(\"function\")\n      expect(retrieved.onClick()).toBe(\"clicked\")\n      expect(retrieved.onHover(5)).toBe(10)\n    })\n\n    it(\"should store metadata by reference\", async () => {\n      await setup()\n\n      const original = { value: 1, nested: { count: 0 } }\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: original,\n      })\n\n      const retrieved = extmarks.getMetadataFor(id)\n      retrieved.value = 999\n      retrieved.nested.count = 100\n\n      expect(original.value).toBe(999)\n      expect(original.nested.count).toBe(100)\n      expect(extmarks.getMetadataFor(id).value).toBe(999)\n    })\n\n    it(\"should handle metadata for extmarks with typeId\", async () => {\n      await setup()\n\n      const linkTypeId = extmarks.registerType(\"link\")\n\n      const id1 = extmarks.create({\n        start: 0,\n        end: 5,\n        typeId: linkTypeId,\n        metadata: { url: \"https://first.com\" },\n      })\n\n      const id2 = extmarks.create({\n        start: 6,\n        end: 11,\n        typeId: linkTypeId,\n        metadata: { url: \"https://second.com\" },\n      })\n\n      expect(extmarks.getMetadataFor(id1)).toEqual({ url: \"https://first.com\" })\n      expect(extmarks.getMetadataFor(id2)).toEqual({ url: \"https://second.com\" })\n\n      const links = extmarks.getAllForTypeId(linkTypeId)\n      expect(links.length).toBe(2)\n\n      for (const link of links) {\n        const meta = extmarks.getMetadataFor(link.id)\n        expect(meta).toHaveProperty(\"url\")\n        expect(meta.url).toMatch(/^https:\\/\\//)\n      }\n    })\n\n    it(\"should preserve metadata after setText clears extmarks\", async () => {\n      await setup(\"Hello World\")\n\n      const id = extmarks.create({\n        start: 0,\n        end: 5,\n        metadata: { persisted: false },\n      })\n\n      textarea.setText(\"New Text\")\n\n      expect(extmarks.get(id)).toBeNull()\n      expect(extmarks.getMetadataFor(id)).toBeUndefined()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/extmarks.ts",
    "content": "import type { EditBuffer } from \"../edit-buffer.js\"\nimport type { EditorView } from \"../editor-view.js\"\nimport { ExtmarksHistory, type ExtmarksSnapshot } from \"./extmarks-history.js\"\n\nexport interface Extmark {\n  id: number\n  start: number // Display-width offset (including newlines), NOT JS string index\n  end: number // Display-width offset (including newlines), NOT JS string index\n  virtual: boolean\n  styleId?: number\n  priority?: number\n  data?: any\n  typeId: number\n}\n\nexport interface ExtmarkOptions {\n  start: number // Display-width offset (including newlines), NOT JS string index\n  end: number // Display-width offset (including newlines), NOT JS string index\n  virtual?: boolean\n  styleId?: number\n  priority?: number\n  data?: any\n  typeId?: number\n  metadata?: any\n}\n\n/**\n * WARNING: This is simulating extmarks in the edit buffer\n * and will move to a real native implementation in the future.\n * Use with caution.\n */\nexport class ExtmarksController {\n  private editBuffer: EditBuffer\n  private editorView: EditorView\n  private extmarks = new Map<number, Extmark>()\n  private extmarksByTypeId = new Map<number, Set<number>>()\n  private metadata = new Map<number, any>()\n  private nextId = 1\n  private destroyed = false\n  private history = new ExtmarksHistory()\n  private typeNameToId = new Map<string, number>()\n  private typeIdToName = new Map<number, string>()\n  private nextTypeId = 1\n\n  private originalMoveCursorLeft: typeof EditBuffer.prototype.moveCursorLeft\n  private originalMoveCursorRight: typeof EditBuffer.prototype.moveCursorRight\n  private originalSetCursorByOffset: typeof EditBuffer.prototype.setCursorByOffset\n  private originalMoveUpVisual: typeof EditorView.prototype.moveUpVisual\n  private originalMoveDownVisual: typeof EditorView.prototype.moveDownVisual\n  private originalDeleteCharBackward: typeof EditBuffer.prototype.deleteCharBackward\n  private originalDeleteChar: typeof EditBuffer.prototype.deleteChar\n  private originalInsertText: typeof EditBuffer.prototype.insertText\n  private originalInsertChar: typeof EditBuffer.prototype.insertChar\n  private originalDeleteRange: typeof EditBuffer.prototype.deleteRange\n  private originalSetText: typeof EditBuffer.prototype.setText\n  private originalReplaceText: typeof EditBuffer.prototype.replaceText\n  private originalClear: typeof EditBuffer.prototype.clear\n  private originalNewLine: typeof EditBuffer.prototype.newLine\n  private originalDeleteLine: typeof EditBuffer.prototype.deleteLine\n  private originalEditorViewDeleteSelectedText: typeof EditorView.prototype.deleteSelectedText\n  private originalUndo: typeof EditBuffer.prototype.undo\n  private originalRedo: typeof EditBuffer.prototype.redo\n\n  constructor(editBuffer: EditBuffer, editorView: EditorView) {\n    this.editBuffer = editBuffer\n    this.editorView = editorView\n\n    this.originalMoveCursorLeft = editBuffer.moveCursorLeft.bind(editBuffer)\n    this.originalMoveCursorRight = editBuffer.moveCursorRight.bind(editBuffer)\n    this.originalSetCursorByOffset = editBuffer.setCursorByOffset.bind(editBuffer)\n    this.originalMoveUpVisual = editorView.moveUpVisual.bind(editorView)\n    this.originalMoveDownVisual = editorView.moveDownVisual.bind(editorView)\n    this.originalDeleteCharBackward = editBuffer.deleteCharBackward.bind(editBuffer)\n    this.originalDeleteChar = editBuffer.deleteChar.bind(editBuffer)\n    this.originalInsertText = editBuffer.insertText.bind(editBuffer)\n    this.originalInsertChar = editBuffer.insertChar.bind(editBuffer)\n    this.originalDeleteRange = editBuffer.deleteRange.bind(editBuffer)\n    this.originalSetText = editBuffer.setText.bind(editBuffer)\n    this.originalReplaceText = editBuffer.replaceText.bind(editBuffer)\n    this.originalClear = editBuffer.clear.bind(editBuffer)\n    this.originalNewLine = editBuffer.newLine.bind(editBuffer)\n    this.originalDeleteLine = editBuffer.deleteLine.bind(editBuffer)\n    this.originalEditorViewDeleteSelectedText = editorView.deleteSelectedText.bind(editorView)\n    this.originalUndo = editBuffer.undo.bind(editBuffer)\n    this.originalRedo = editBuffer.redo.bind(editBuffer)\n\n    this.wrapCursorMovement()\n    this.wrapDeletion()\n    this.wrapInsertion()\n    this.wrapEditorViewDeleteSelectedText()\n    this.wrapUndoRedo()\n    this.setupContentChangeListener()\n  }\n\n  private wrapCursorMovement(): void {\n    this.editBuffer.moveCursorLeft = (): void => {\n      if (this.destroyed) {\n        this.originalMoveCursorLeft()\n        return\n      }\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      const hasSelection = this.editorView.hasSelection()\n\n      if (hasSelection) {\n        this.originalMoveCursorLeft()\n        return\n      }\n\n      const targetOffset = currentOffset - 1\n      if (targetOffset < 0) {\n        this.originalMoveCursorLeft()\n        return\n      }\n\n      const virtualExtmark = this.findVirtualExtmarkContaining(targetOffset)\n      if (virtualExtmark && currentOffset >= virtualExtmark.end) {\n        this.editBuffer.setCursorByOffset(virtualExtmark.start - 1)\n        return\n      }\n\n      this.originalMoveCursorLeft()\n    }\n\n    this.editBuffer.moveCursorRight = (): void => {\n      if (this.destroyed) {\n        this.originalMoveCursorRight()\n        return\n      }\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      const hasSelection = this.editorView.hasSelection()\n\n      if (hasSelection) {\n        this.originalMoveCursorRight()\n        return\n      }\n\n      const targetOffset = currentOffset + 1\n      const textLength = this.editBuffer.getText().length\n\n      if (targetOffset > textLength) {\n        this.originalMoveCursorRight()\n        return\n      }\n\n      const virtualExtmark = this.findVirtualExtmarkContaining(targetOffset)\n      if (virtualExtmark && currentOffset <= virtualExtmark.start) {\n        this.editBuffer.setCursorByOffset(virtualExtmark.end)\n        return\n      }\n\n      this.originalMoveCursorRight()\n    }\n\n    this.editorView.moveUpVisual = (): void => {\n      if (this.destroyed) {\n        this.originalMoveUpVisual()\n        return\n      }\n\n      const hasSelection = this.editorView.hasSelection()\n\n      if (hasSelection) {\n        this.originalMoveUpVisual()\n        return\n      }\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      this.originalMoveUpVisual()\n      const newOffset = this.editorView.getVisualCursor().offset\n\n      const virtualExtmark = this.findVirtualExtmarkContaining(newOffset)\n      if (virtualExtmark) {\n        const distanceToStart = newOffset - virtualExtmark.start\n        const distanceToEnd = virtualExtmark.end - newOffset\n\n        if (distanceToStart < distanceToEnd) {\n          this.editorView.setCursorByOffset(virtualExtmark.start - 1)\n        } else {\n          this.editorView.setCursorByOffset(virtualExtmark.end)\n        }\n      }\n    }\n\n    this.editorView.moveDownVisual = (): void => {\n      if (this.destroyed) {\n        this.originalMoveDownVisual()\n        return\n      }\n\n      const hasSelection = this.editorView.hasSelection()\n\n      if (hasSelection) {\n        this.originalMoveDownVisual()\n        return\n      }\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      this.originalMoveDownVisual()\n      const newOffset = this.editorView.getVisualCursor().offset\n\n      const virtualExtmark = this.findVirtualExtmarkContaining(newOffset)\n      if (virtualExtmark) {\n        const distanceToStart = newOffset - virtualExtmark.start\n        const distanceToEnd = virtualExtmark.end - newOffset\n\n        if (distanceToStart < distanceToEnd) {\n          const adjustedOffset = virtualExtmark.start - 1\n          const targetOffset = adjustedOffset <= currentOffset ? virtualExtmark.end : adjustedOffset\n          this.editorView.setCursorByOffset(targetOffset)\n        } else {\n          this.editorView.setCursorByOffset(virtualExtmark.end)\n        }\n      }\n    }\n\n    this.editBuffer.setCursorByOffset = (offset: number): void => {\n      if (this.destroyed) {\n        this.originalSetCursorByOffset(offset)\n        return\n      }\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      const hasSelection = this.editorView.hasSelection()\n\n      if (hasSelection) {\n        this.originalSetCursorByOffset(offset)\n        return\n      }\n\n      const movingForward = offset > currentOffset\n\n      if (movingForward) {\n        const virtualExtmark = this.findVirtualExtmarkContaining(offset)\n        if (virtualExtmark && currentOffset <= virtualExtmark.start) {\n          this.originalSetCursorByOffset(virtualExtmark.end)\n          return\n        }\n      } else {\n        for (const extmark of this.extmarks.values()) {\n          if (extmark.virtual && currentOffset >= extmark.end && offset < extmark.end && offset >= extmark.start) {\n            this.originalSetCursorByOffset(extmark.start - 1)\n            return\n          }\n        }\n      }\n\n      this.originalSetCursorByOffset(offset)\n    }\n  }\n\n  private wrapDeletion(): void {\n    this.editBuffer.deleteCharBackward = (): void => {\n      if (this.destroyed) {\n        this.originalDeleteCharBackward()\n        return\n      }\n\n      this.saveSnapshot()\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      const hadSelection = this.editorView.hasSelection()\n\n      if (currentOffset === 0) {\n        this.originalDeleteCharBackward()\n        return\n      }\n\n      if (hadSelection) {\n        this.originalDeleteCharBackward()\n        return\n      }\n\n      const targetOffset = currentOffset - 1\n      const virtualExtmark = this.findVirtualExtmarkContaining(targetOffset)\n\n      if (virtualExtmark && currentOffset === virtualExtmark.end) {\n        const startCursor = this.offsetToPosition(virtualExtmark.start)\n        const endCursor = this.offsetToPosition(virtualExtmark.end)\n        const deleteOffset = virtualExtmark.start\n        const deleteLength = virtualExtmark.end - virtualExtmark.start\n\n        this.deleteExtmarkById(virtualExtmark.id)\n\n        this.originalDeleteRange(startCursor.row, startCursor.col, endCursor.row, endCursor.col)\n        this.adjustExtmarksAfterDeletion(deleteOffset, deleteLength)\n\n        this.updateHighlights()\n\n        return\n      }\n\n      this.originalDeleteCharBackward()\n      this.adjustExtmarksAfterDeletion(targetOffset, 1)\n    }\n\n    this.editBuffer.deleteChar = (): void => {\n      if (this.destroyed) {\n        this.originalDeleteChar()\n        return\n      }\n\n      this.saveSnapshot()\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      const textLength = this.editBuffer.getText().length\n      const hadSelection = this.editorView.hasSelection()\n\n      if (currentOffset >= textLength) {\n        this.originalDeleteChar()\n        return\n      }\n\n      if (hadSelection) {\n        this.originalDeleteChar()\n        return\n      }\n\n      const targetOffset = currentOffset\n      const virtualExtmark = this.findVirtualExtmarkContaining(targetOffset)\n\n      if (virtualExtmark && currentOffset === virtualExtmark.start) {\n        const startCursor = this.offsetToPosition(virtualExtmark.start)\n        const endCursor = this.offsetToPosition(virtualExtmark.end)\n        const deleteOffset = virtualExtmark.start\n        const deleteLength = virtualExtmark.end - virtualExtmark.start\n\n        this.deleteExtmarkById(virtualExtmark.id)\n\n        this.originalDeleteRange(startCursor.row, startCursor.col, endCursor.row, endCursor.col)\n        this.adjustExtmarksAfterDeletion(deleteOffset, deleteLength)\n\n        this.updateHighlights()\n\n        return\n      }\n\n      this.originalDeleteChar()\n      this.adjustExtmarksAfterDeletion(targetOffset, 1)\n    }\n\n    this.editBuffer.deleteRange = (startLine: number, startCol: number, endLine: number, endCol: number): void => {\n      if (this.destroyed) {\n        this.originalDeleteRange(startLine, startCol, endLine, endCol)\n        return\n      }\n\n      this.saveSnapshot()\n\n      const startOffset = this.positionToOffset(startLine, startCol)\n      const endOffset = this.positionToOffset(endLine, endCol)\n      const length = endOffset - startOffset\n\n      this.originalDeleteRange(startLine, startCol, endLine, endCol)\n      this.adjustExtmarksAfterDeletion(startOffset, length)\n    }\n\n    this.editBuffer.deleteLine = (): void => {\n      if (this.destroyed) {\n        this.originalDeleteLine()\n        return\n      }\n\n      this.saveSnapshot()\n\n      const text = this.editBuffer.getText()\n      const currentOffset = this.editorView.getVisualCursor().offset\n\n      let lineStart = 0\n      for (let i = currentOffset - 1; i >= 0; i--) {\n        if (text[i] === \"\\n\") {\n          lineStart = i + 1\n          break\n        }\n      }\n\n      let lineEnd = text.length\n      for (let i = currentOffset; i < text.length; i++) {\n        if (text[i] === \"\\n\") {\n          lineEnd = i + 1\n          break\n        }\n      }\n\n      const deleteLength = lineEnd - lineStart\n\n      this.originalDeleteLine()\n      this.adjustExtmarksAfterDeletion(lineStart, deleteLength)\n    }\n  }\n\n  private wrapInsertion(): void {\n    this.editBuffer.insertText = (text: string): void => {\n      if (this.destroyed) {\n        this.originalInsertText(text)\n        return\n      }\n\n      this.saveSnapshot()\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      this.originalInsertText(text)\n      this.adjustExtmarksAfterInsertion(currentOffset, text.length)\n    }\n\n    this.editBuffer.insertChar = (char: string): void => {\n      if (this.destroyed) {\n        this.originalInsertChar(char)\n        return\n      }\n\n      this.saveSnapshot()\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      this.originalInsertChar(char)\n      this.adjustExtmarksAfterInsertion(currentOffset, 1)\n    }\n\n    this.editBuffer.setText = (text: string): void => {\n      if (this.destroyed) {\n        this.originalSetText(text)\n        return\n      }\n\n      this.clear()\n      this.originalSetText(text)\n    }\n\n    this.editBuffer.replaceText = (text: string): void => {\n      if (this.destroyed) {\n        this.originalReplaceText(text)\n        return\n      }\n\n      this.saveSnapshot()\n      this.clear()\n      this.originalReplaceText(text)\n    }\n\n    this.editBuffer.clear = (): void => {\n      if (this.destroyed) {\n        this.originalClear()\n        return\n      }\n\n      this.saveSnapshot()\n\n      this.clear()\n      this.originalClear()\n    }\n\n    this.editBuffer.newLine = (): void => {\n      if (this.destroyed) {\n        this.originalNewLine()\n        return\n      }\n\n      this.saveSnapshot()\n\n      const currentOffset = this.editorView.getVisualCursor().offset\n      this.originalNewLine()\n      this.adjustExtmarksAfterInsertion(currentOffset, 1)\n    }\n  }\n\n  private wrapEditorViewDeleteSelectedText(): void {\n    this.editorView.deleteSelectedText = (): void => {\n      if (this.destroyed) {\n        this.originalEditorViewDeleteSelectedText()\n        return\n      }\n\n      this.saveSnapshot()\n\n      const selection = this.editorView.getSelection()\n      if (!selection) {\n        this.originalEditorViewDeleteSelectedText()\n        return\n      }\n\n      const deleteOffset = Math.min(selection.start, selection.end)\n      const deleteLength = Math.abs(selection.end - selection.start)\n\n      this.originalEditorViewDeleteSelectedText()\n\n      if (deleteLength > 0) {\n        this.adjustExtmarksAfterDeletion(deleteOffset, deleteLength)\n      }\n    }\n  }\n\n  private setupContentChangeListener(): void {\n    this.editBuffer.on(\"content-changed\", () => {\n      if (this.destroyed) return\n      this.updateHighlights()\n    })\n  }\n\n  private deleteExtmarkById(id: number): void {\n    const extmark = this.extmarks.get(id)\n    if (extmark) {\n      this.extmarks.delete(id)\n      this.extmarksByTypeId.get(extmark.typeId)?.delete(id)\n      this.metadata.delete(id)\n    }\n  }\n\n  private findVirtualExtmarkContaining(offset: number): Extmark | null {\n    for (const extmark of this.extmarks.values()) {\n      if (extmark.virtual && offset >= extmark.start && offset < extmark.end) {\n        return extmark\n      }\n    }\n    return null\n  }\n\n  private adjustExtmarksAfterInsertion(insertOffset: number, length: number): void {\n    for (const extmark of this.extmarks.values()) {\n      if (extmark.start >= insertOffset) {\n        extmark.start += length\n        extmark.end += length\n      } else if (extmark.end > insertOffset) {\n        extmark.end += length\n      }\n    }\n    this.updateHighlights()\n  }\n\n  public adjustExtmarksAfterDeletion(deleteOffset: number, length: number): void {\n    const toDelete: number[] = []\n\n    for (const extmark of this.extmarks.values()) {\n      if (extmark.end <= deleteOffset) {\n        continue\n      }\n\n      if (extmark.start >= deleteOffset + length) {\n        extmark.start -= length\n        extmark.end -= length\n      } else if (extmark.start >= deleteOffset && extmark.end <= deleteOffset + length) {\n        toDelete.push(extmark.id)\n      } else if (extmark.start < deleteOffset && extmark.end > deleteOffset + length) {\n        extmark.end -= length\n      } else if (extmark.start < deleteOffset && extmark.end > deleteOffset) {\n        extmark.end -= Math.min(extmark.end, deleteOffset + length) - deleteOffset\n      } else if (extmark.start < deleteOffset + length && extmark.end > deleteOffset + length) {\n        const overlap = deleteOffset + length - extmark.start\n        extmark.start = deleteOffset\n        extmark.end -= length\n      }\n    }\n\n    for (const id of toDelete) {\n      this.deleteExtmarkById(id)\n    }\n\n    this.updateHighlights()\n  }\n\n  private offsetToPosition(offset: number): { row: number; col: number } {\n    const result = this.editBuffer.offsetToPosition(offset)\n    if (!result) {\n      return { row: 0, col: 0 }\n    }\n    return result\n  }\n\n  private positionToOffset(row: number, col: number): number {\n    return this.editBuffer.positionToOffset(row, col)\n  }\n\n  private updateHighlights(): void {\n    this.editBuffer.clearAllHighlights()\n\n    for (const extmark of this.extmarks.values()) {\n      if (extmark.styleId !== undefined) {\n        // extmark.start/end are display-width offsets including newlines (from cursor operations)\n        // addHighlightByCharRange expects display-width offsets excluding newlines\n        // So we need to subtract the number of newlines before each position\n        const startWithoutNewlines = this.offsetExcludingNewlines(extmark.start)\n        const endWithoutNewlines = this.offsetExcludingNewlines(extmark.end)\n\n        this.editBuffer.addHighlightByCharRange({\n          start: startWithoutNewlines,\n          end: endWithoutNewlines,\n          styleId: extmark.styleId,\n          priority: extmark.priority ?? 0,\n          hlRef: extmark.id,\n        })\n      }\n    }\n  }\n\n  private offsetExcludingNewlines(offset: number): number {\n    // offset is a display-width offset from the start of the buffer (includes newlines)\n    // We need to convert to display-width excluding newlines\n    // This means: subtract 1 for each newline encountered before this offset\n    const text = this.editBuffer.getText()\n    let displayWidthSoFar = 0\n    let newlineCount = 0\n\n    // Walk through the text and calculate display widths\n    let i = 0\n    while (i < text.length && displayWidthSoFar < offset) {\n      if (text[i] === \"\\n\") {\n        displayWidthSoFar++ // newline counts as width 1 in cursor offset\n        newlineCount++\n        i++\n      } else {\n        // Find the next newline or end of string\n        let j = i\n        while (j < text.length && text[j] !== \"\\n\") {\n          j++\n        }\n        const chunk = text.substring(i, j)\n        const chunkWidth = Bun.stringWidth(chunk)\n\n        if (displayWidthSoFar + chunkWidth < offset) {\n          // Entire chunk fits before offset\n          displayWidthSoFar += chunkWidth\n          i = j\n        } else {\n          // Offset is within this chunk - need to find exact position\n          // Walk character by character\n          for (let k = i; k < j && displayWidthSoFar < offset; k++) {\n            const charWidth = Bun.stringWidth(text[k])\n            displayWidthSoFar += charWidth\n          }\n          break\n        }\n      }\n    }\n\n    return offset - newlineCount\n  }\n\n  public create(options: ExtmarkOptions): number {\n    if (this.destroyed) {\n      throw new Error(\"ExtmarksController is destroyed\")\n    }\n\n    const id = this.nextId++\n    const typeId = options.typeId ?? 0\n    const extmark: Extmark = {\n      id,\n      start: options.start,\n      end: options.end,\n      virtual: options.virtual ?? false,\n      styleId: options.styleId,\n      priority: options.priority,\n      data: options.data,\n      typeId,\n    }\n\n    this.extmarks.set(id, extmark)\n\n    if (!this.extmarksByTypeId.has(typeId)) {\n      this.extmarksByTypeId.set(typeId, new Set())\n    }\n    this.extmarksByTypeId.get(typeId)!.add(id)\n\n    if (options.metadata !== undefined) {\n      this.metadata.set(id, options.metadata)\n    }\n\n    this.updateHighlights()\n\n    return id\n  }\n\n  public delete(id: number): boolean {\n    if (this.destroyed) {\n      throw new Error(\"ExtmarksController is destroyed\")\n    }\n\n    const extmark = this.extmarks.get(id)\n    if (!extmark) return false\n\n    this.deleteExtmarkById(id)\n    this.updateHighlights()\n\n    return true\n  }\n\n  public get(id: number): Extmark | null {\n    if (this.destroyed) return null\n    return this.extmarks.get(id) ?? null\n  }\n\n  public getAll(): Extmark[] {\n    if (this.destroyed) return []\n    return Array.from(this.extmarks.values())\n  }\n\n  public getVirtual(): Extmark[] {\n    if (this.destroyed) return []\n    return Array.from(this.extmarks.values()).filter((e) => e.virtual)\n  }\n\n  public getAtOffset(offset: number): Extmark[] {\n    if (this.destroyed) return []\n    return Array.from(this.extmarks.values()).filter((e) => offset >= e.start && offset < e.end)\n  }\n\n  public getAllForTypeId(typeId: number): Extmark[] {\n    if (this.destroyed) return []\n    const ids = this.extmarksByTypeId.get(typeId)\n    if (!ids) return []\n    return Array.from(ids)\n      .map((id) => this.extmarks.get(id))\n      .filter((e): e is Extmark => e !== undefined)\n  }\n\n  public clear(): void {\n    if (this.destroyed) return\n\n    this.extmarks.clear()\n    this.extmarksByTypeId.clear()\n    this.metadata.clear()\n    this.updateHighlights()\n  }\n\n  private saveSnapshot(): void {\n    this.history.saveSnapshot(this.extmarks, this.nextId)\n  }\n\n  private restoreSnapshot(snapshot: ExtmarksSnapshot): void {\n    this.extmarks = new Map(Array.from(snapshot.extmarks.entries()).map(([id, extmark]) => [id, { ...extmark }]))\n    this.nextId = snapshot.nextId\n    this.updateHighlights()\n  }\n\n  private wrapUndoRedo(): void {\n    this.editBuffer.undo = (): string | null => {\n      if (this.destroyed) {\n        return this.originalUndo()\n      }\n\n      if (!this.history.canUndo()) {\n        return this.originalUndo()\n      }\n\n      const currentSnapshot: ExtmarksSnapshot = {\n        extmarks: new Map(Array.from(this.extmarks.entries()).map(([id, extmark]) => [id, { ...extmark }])),\n        nextId: this.nextId,\n      }\n      this.history.pushRedo(currentSnapshot)\n\n      const snapshot = this.history.undo()!\n      this.restoreSnapshot(snapshot)\n\n      return this.originalUndo()\n    }\n\n    this.editBuffer.redo = (): string | null => {\n      if (this.destroyed) {\n        return this.originalRedo()\n      }\n\n      if (!this.history.canRedo()) {\n        return this.originalRedo()\n      }\n\n      const currentSnapshot: ExtmarksSnapshot = {\n        extmarks: new Map(Array.from(this.extmarks.entries()).map(([id, extmark]) => [id, { ...extmark }])),\n        nextId: this.nextId,\n      }\n      this.history.pushUndo(currentSnapshot)\n\n      const snapshot = this.history.redo()!\n      this.restoreSnapshot(snapshot)\n\n      return this.originalRedo()\n    }\n  }\n\n  public registerType(typeName: string): number {\n    if (this.destroyed) {\n      throw new Error(\"ExtmarksController is destroyed\")\n    }\n\n    const existing = this.typeNameToId.get(typeName)\n    if (existing !== undefined) {\n      return existing\n    }\n\n    const typeId = this.nextTypeId++\n    this.typeNameToId.set(typeName, typeId)\n    this.typeIdToName.set(typeId, typeName)\n    return typeId\n  }\n\n  public getTypeId(typeName: string): number | null {\n    if (this.destroyed) return null\n    return this.typeNameToId.get(typeName) ?? null\n  }\n\n  public getTypeName(typeId: number): string | null {\n    if (this.destroyed) return null\n    return this.typeIdToName.get(typeId) ?? null\n  }\n\n  public getMetadataFor(extmarkId: number): any {\n    if (this.destroyed) return undefined\n    return this.metadata.get(extmarkId)\n  }\n\n  public destroy(): void {\n    if (this.destroyed) return\n\n    this.editBuffer.moveCursorLeft = this.originalMoveCursorLeft\n    this.editBuffer.moveCursorRight = this.originalMoveCursorRight\n    this.editBuffer.setCursorByOffset = this.originalSetCursorByOffset\n    this.editorView.moveUpVisual = this.originalMoveUpVisual\n    this.editorView.moveDownVisual = this.originalMoveDownVisual\n    this.editBuffer.deleteCharBackward = this.originalDeleteCharBackward\n    this.editBuffer.deleteChar = this.originalDeleteChar\n    this.editBuffer.insertText = this.originalInsertText\n    this.editBuffer.insertChar = this.originalInsertChar\n    this.editBuffer.deleteRange = this.originalDeleteRange\n    this.editBuffer.setText = this.originalSetText\n    this.editBuffer.replaceText = this.originalReplaceText\n    this.editBuffer.clear = this.originalClear\n    this.editBuffer.newLine = this.originalNewLine\n    this.editBuffer.deleteLine = this.originalDeleteLine\n    this.editorView.deleteSelectedText = this.originalEditorViewDeleteSelectedText\n    this.editBuffer.undo = this.originalUndo\n    this.editBuffer.redo = this.originalRedo\n\n    this.extmarks.clear()\n    this.extmarksByTypeId.clear()\n    this.metadata.clear()\n    this.typeNameToId.clear()\n    this.typeIdToName.clear()\n    this.history.clear()\n    this.destroyed = true\n  }\n}\n\nexport function createExtmarksController(editBuffer: EditBuffer, editorView: EditorView): ExtmarksController {\n  return new ExtmarksController(editBuffer, editorView)\n}\n"
  },
  {
    "path": "packages/core/src/lib/fonts/block.json",
    "content": "{\n  \"name\": \"block\",\n  \"version\": \"0.2.0\",\n  \"homepage\": \"https://github.com/dominikwilkowski/cfonts\",\n  \"colors\": 2,\n  \"lines\": 6,\n  \"buffer\": [\"\", \"\", \"\", \"\", \"\", \"\"],\n  \"letterspace\": [\" \", \" \", \" \", \" \", \" \", \" \"],\n  \"letterspace_size\": 1,\n  \"chars\": {\n    \"A\": [\n      \" <c1>█████</c1><c2>╗</c2> \",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>███████</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>║</c2>\",\n      \"<c2>╚═╝  ╚═╝</c2>\"\n    ],\n    \"B\": [\n      \"<c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██████</c1><c2>╔╝</c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2>╚═════╝ </c2>\"\n    ],\n    \"C\": [\n      \" <c1>██████</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>╔════╝</c2>\",\n      \"<c1>██</c1><c2>║     </c2>\",\n      \"<c1>██</c1><c2>║     </c2>\",\n      \"<c2>╚</c2><c1>██████</c1><c2>╗</c2>\",\n      \"<c2> ╚═════╝</c2>\"\n    ],\n    \"D\": [\n      \"<c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>║</c2>\",\n      \"<c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2>╚═════╝ </c2>\"\n    ],\n    \"E\": [\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>╔════╝</c2>\",\n      \"<c1>█████</c1><c2>╗  </c2>\",\n      \"<c1>██</c1><c2>╔══╝  </c2>\",\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚══════╝</c2>\"\n    ],\n    \"F\": [\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>╔════╝</c2>\",\n      \"<c1>█████</c1><c2>╗  </c2>\",\n      \"<c1>██</c1><c2>╔══╝  </c2>\",\n      \"<c1>██</c1><c2>║     </c2>\",\n      \"<c2>╚═╝     </c2>\"\n    ],\n    \"G\": [\n      \" <c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔════╝ </c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ███</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚═════╝ </c2>\"\n    ],\n    \"H\": [\n      \"<c1>██</c1><c2>╗</c2><c1>  ██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>║</c2>\",\n      \"<c1>███████</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>║</c2>\",\n      \"<c2>╚═╝  ╚═╝</c2>\"\n    ],\n    \"I\": [\n      \"<c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2>\",\n      \"<c2>╚═╝</c2>\"\n    ],\n    \"J\": [\n      \"<c1>     ██</c1><c2>╗</c2>\",\n      \"<c1>     ██</c1><c2>║</c2>\",\n      \"<c1>     ██</c1><c2>║</c2>\",\n      \"<c1>██   ██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>█████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚════╝ </c2>\"\n    ],\n    \"K\": [\n      \"<c1>██</c1><c2>╗</c2><c1>  ██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1> ██</c1><c2>╔╝</c2>\",\n      \"<c1>█████</c1><c2>╔╝ </c2>\",\n      \"<c1>██</c1><c2>╔═</c2><c1>██</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>╗</c2>\",\n      \"<c2>╚═╝  ╚═╝</c2>\"\n    ],\n    \"L\": [\n      \"<c1>██</c1><c2>╗     </c2>\",\n      \"<c1>██</c1><c2>║     </c2>\",\n      \"<c1>██</c1><c2>║     </c2>\",\n      \"<c1>██</c1><c2>║     </c2>\",\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚══════╝</c2>\"\n    ],\n    \"M\": [\n      \"<c1>███</c1><c2>╗</c2><c1>   ███</c1><c2>╗</c2>\",\n      \"<c1>████</c1><c2>╗</c2><c1> ████</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>╔</c2><c1>████</c1><c2>╔</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║╚</c2><c1>██</c1><c2>╔╝</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║ ╚═╝</c2><c1> ██</c1><c2>║</c2>\",\n      \"<c2>╚═╝     ╚═╝</c2>\"\n    ],\n    \"N\": [\n      \"<c1>███</c1><c2>╗</c2><c1>   ██</c1><c2>╗</c2>\",\n      \"<c1>████</c1><c2>╗</c2><c1>  ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>╔</c2><c1>██</c1><c2>╗</c2><c1> ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║╚</c2><c1>██</c1><c2>╗</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║ ╚</c2><c1>████</c1><c2>║</c2>\",\n      \"<c2>╚═╝  ╚═══╝</c2>\"\n    ],\n    \"O\": [\n      \" <c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔═══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚═════╝ </c2>\"\n    ],\n    \"P\": [\n      \"<c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██████</c1><c2>╔╝</c2>\",\n      \"<c1>██</c1><c2>╔═══╝ </c2>\",\n      \"<c1>██</c1><c2>║     </c2>\",\n      \"<c2>╚═╝     </c2>\"\n    ],\n    \"Q\": [\n      \" <c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔═══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>▄▄ ██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚══</c2><c1>▀▀</c1><c2>═╝ </c2>\"\n    ],\n    \"R\": [\n      \"<c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██████</c1><c2>╔╝</c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>║</c2>\",\n      \"<c2>╚═╝  ╚═╝</c2>\"\n    ],\n    \"S\": [\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>╔════╝</c2>\",\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚════</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>███████</c1><c2>║</c2>\",\n      \"<c2>╚══════╝</c2>\"\n    ],\n    \"T\": [\n      \"<c1>████████</c1><c2>╗</c2>\",\n      \"<c2>╚══</c2><c1>██</c1><c2>╔══╝</c2>\",\n      \"<c1>   ██</c1><c2>║   </c2>\",\n      \"<c1>   ██</c1><c2>║   </c2>\",\n      \"<c1>   ██</c1><c2>║   </c2>\",\n      \"<c2>   ╚═╝   </c2>\"\n    ],\n    \"U\": [\n      \"<c1>██</c1><c2>╗</c2><c1>   ██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚═════╝ </c2>\"\n    ],\n    \"V\": [\n      \"<c1>██</c1><c2>╗</c2><c1>   ██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>   ██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>██</c1><c2>╗</c2><c1> ██</c1><c2>╔╝</c2>\",\n      \"<c2> ╚</c2><c1>████</c1><c2>╔╝ </c2>\",\n      \"<c2>  ╚═══╝  </c2>\"\n    ],\n    \"W\": [\n      \"<c1>██</c1><c2>╗    </c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║    </c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1> █</c1><c2>╗</c2><c1> ██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>███</c1><c2>╗</c2><c1>██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>███</c1><c2>╔</c2><c1>███</c1><c2>╔╝</c2>\",\n      \"<c2> ╚══╝╚══╝ </c2>\"\n    ],\n    \"X\": [\n      \"<c1>██</c1><c2>╗</c2><c1>  ██</c1><c2>╗</c2>\",\n      \"<c2>╚</c2><c1>██</c1><c2>╗</c2><c1>██</c1><c2>╔╝</c2>\",\n      \"<c2> ╚</c2><c1>███</c1><c2>╔╝ </c2>\",\n      \" <c1>██</c1><c2>╔</c2><c1>██</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔╝</c2><c1> ██</c1><c2>╗</c2>\",\n      \"<c2>╚═╝  ╚═╝</c2>\"\n    ],\n    \"Y\": [\n      \"<c1>██</c1><c2>╗</c2><c1>   ██</c1><c2>╗</c2>\",\n      \"<c2>╚</c2><c1>██</c1><c2>╗</c2><c1> ██</c1><c2>╔╝</c2>\",\n      \"<c2> ╚</c2><c1>████</c1><c2>╔╝ </c2>\",\n      \"<c2>  ╚</c2><c1>██</c1><c2>╔╝  </c2>\",\n      \"<c1>   ██</c1><c2>║   </c2>\",\n      \"<c2>   ╚═╝   </c2>\"\n    ],\n    \"Z\": [\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚══</c2><c1>███</c1><c2>╔╝</c2>\",\n      \"<c1>  ███</c1><c2>╔╝ </c2>\",\n      \" <c1>███</c1><c2>╔╝  </c2>\",\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚══════╝</c2>\"\n    ],\n    \"0\": [\n      \" <c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔═</c2><c1>████</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>██</c1><c2>╔</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>████</c1><c2>╔╝</c2><c1>██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚═════╝ </c2>\"\n    ],\n    \"1\": [\n      \" <c1>██</c1><c2>╗</c2>\",\n      \"<c1>███</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>██</c1><c2>║</c2>\",\n      \" <c1>██</c1><c2>║</c2>\",\n      \" <c1>██</c1><c2>║</c2>\",\n      \"<c2> ╚═╝</c2>\"\n    ],\n    \"2\": [\n      \"<c1>██████</c1><c2>╗ </c2>\",\n      \"<c2>╚════</c2><c1>██</c1><c2>╗</c2>\",\n      \" <c1>█████</c1><c2>╔╝</c2>\",\n      \"<c1>██</c1><c2>╔═══╝ </c2>\",\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚══════╝</c2>\"\n    ],\n    \"3\": [\n      \"<c1>██████</c1><c2>╗ </c2>\",\n      \"<c2>╚════</c2><c1>██</c1><c2>╗</c2>\",\n      \" <c1>█████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚═══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2>╚═════╝ </c2>\"\n    ],\n    \"4\": [\n      \"<c1>██</c1><c2>╗</c2><c1>  ██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>  ██</c1><c2>║</c2>\",\n      \"<c1>███████</c1><c2>║</c2>\",\n      \"<c2>╚════</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>     ██</c1><c2>║</c2>\",\n      \"<c2>     ╚═╝</c2>\"\n    ],\n    \"5\": [\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>╔════╝</c2>\",\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚════</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>███████</c1><c2>║</c2>\",\n      \"<c2>╚══════╝</c2>\"\n    ],\n    \"6\": [\n      \" <c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔════╝ </c2>\",\n      \"<c1>███████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔═══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c2>╚</c2><c1>██████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚═════╝ </c2>\"\n    ],\n    \"7\": [\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚════</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>    ██</c1><c2>╔╝</c2>\",\n      \"<c1>   ██</c1><c2>╔╝ </c2>\",\n      \"<c1>   ██</c1><c2>║  </c2>\",\n      \"<c2>   ╚═╝  </c2>\"\n    ],\n    \"8\": [\n      \" <c1>█████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c2>╚</c2><c1>█████</c1><c2>╔╝</c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c2>╚</c2><c1>█████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚════╝ </c2>\"\n    ],\n    \"9\": [\n      \" <c1>█████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c2>╚</c2><c1>██████</c1><c2>║</c2>\",\n      \"<c2> ╚═══</c2><c1>██</c1><c2>║</c2>\",\n      \" <c1>█████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚════╝ </c2>\"\n    ],\n    \"!\": [\n      \"<c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2>\",\n      \"<c2>╚═╝</c2>\",\n      \"<c1>██</c1><c2>╗</c2>\",\n      \"<c2>╚═╝</c2>\"\n    ],\n    \"?\": [\n      \"<c1>██████</c1><c2>╗ </c2>\",\n      \"<c2>╚════</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>  ▄███</c1><c2>╔╝</c2>\",\n      \"<c1>  ▀▀</c1><c2>══╝ </c2>\",\n      \"<c1>  ██</c1><c2>╗   </c2>\",\n      \"<c2>  ╚═╝   </c2>\"\n    ],\n    \".\": [\"   \", \"   \", \"   \", \"   \", \"<c1>██</c1><c2>╗</c2>\", \"<c2>╚═╝</c2>\"],\n    \"+\": [\n      \"       \",\n      \"<c1>  ██</c1><c2>╗  </c2>\",\n      \"<c1>██████</c1><c2>╗</c2>\",\n      \"<c2> ╚</c2><c1>██</c1><c2>╔═╝</c2>\",\n      \"<c2>  ╚═╝  </c2>\",\n      \"       \"\n    ],\n    \"-\": [\"      \", \"      \", \"<c1>█████</c1><c2>╗</c2>\", \"<c2>╚════╝</c2>\", \"      \", \"      \"],\n    \"_\": [\"        \", \"        \", \"        \", \"        \", \"<c1>███████</c1><c2>╗</c2>\", \"<c2>╚══════╝</c2>\"],\n    \"=\": [\n      \"       \",\n      \"<c1>██████</c1><c2>╗</c2>\",\n      \"<c2>╚═════╝</c2>\",\n      \"<c1>██████</c1><c2>╗</c2>\",\n      \"<c2>╚═════╝</c2>\",\n      \"       \"\n    ],\n    \"@\": [\n      \" <c1>██████</c1><c2>╗ </c2>\",\n      \"<c1>██</c1><c2>╔═══</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>██</c1><c2>╗</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>║</c2><c1>██</c1><c2>║</c2><c1>██</c1><c2>║</c2>\",\n      \"<c2>╚</c2><c1>█</c1><c2>║</c2><c1>████</c1><c2>╔╝</c2>\",\n      \"<c2> ╚╝╚═══╝ </c2>\"\n    ],\n    \"#\": [\n      \" <c1>██</c1><c2>╗</c2><c1> ██</c1><c2>╗ </c2>\",\n      \"<c1>████████</c1><c2>╗</c2>\",\n      \"<c2>╚</c2><c1>██</c1><c2>╔═</c2><c1>██</c1><c2>╔╝</c2>\",\n      \"<c1>████████</c1><c2>╗</c2>\",\n      \"<c2>╚</c2><c1>██</c1><c2>╔═</c2><c1>██</c1><c2>╔╝</c2>\",\n      \"<c2> ╚═╝ ╚═╝ </c2>\"\n    ],\n    \"$\": [\n      \"<c1>▄▄███▄▄</c1><c2>·</c2>\",\n      \"<c1>██</c1><c2>╔════╝</c2>\",\n      \"<c1>███████</c1><c2>╗</c2>\",\n      \"<c2>╚════</c2><c1>██</c1><c2>║</c2>\",\n      \"<c1>███████</c1><c2>║</c2>\",\n      \"<c2>╚═</c2><c1>▀▀▀</c1><c2>══╝</c2>\"\n    ],\n    \"%\": [\n      \"<c1>██</c1><c2>╗</c2><c1> ██</c1><c2>╗</c2>\",\n      \"<c2>╚═╝</c2><c1>██</c1><c2>╔╝</c2>\",\n      \"<c1>  ██</c1><c2>╔╝ </c2>\",\n      \" <c1>██</c1><c2>╔╝  </c2>\",\n      \"<c1>██</c1><c2>╔╝</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c2>╚═╝ ╚═╝</c2>\"\n    ],\n    \"&\": [\n      \"<c1>   ██</c1><c2>╗   </c2>\",\n      \"<c1>   ██</c1><c2>║   </c2>\",\n      \"<c1>████████</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>╔═</c2><c1>██</c1><c2>╔═╝</c2>\",\n      \"<c1>██████</c1><c2>║  </c2>\",\n      \"<c2>╚═════╝  </c2>\"\n    ],\n    \"(\": [\n      \" <c1>██</c1><c2>╗</c2>\",\n      \"<c1>██</c1><c2>╔╝</c2>\",\n      \"<c1>██</c1><c2>║ \",\n      \"<c1>██</c1><c2>║ \",\n      \"<c2>╚</c2><c1>██</c1><c2>╗</c2>\",\n      \"<c2> ╚═╝</c2>\"\n    ],\n    \")\": [\n      \"<c1>██</c1><c2>╗ </c2>\",\n      \"<c2>╚</c2><c1>██</c1><c2>╗</c2>\",\n      \" <c1>██</c1><c2>║</c2>\",\n      \" <c1>██</c1><c2>║</c2>\",\n      \"<c1>██</c1><c2>╔╝</c2>\",\n      \"<c2>╚═╝ </c2>\"\n    ],\n    \"/\": [\n      \"<c1>    ██</c1><c2>╗</c2>\",\n      \"<c1>   ██</c1><c2>╔╝</c2>\",\n      \"<c1>  ██</c1><c2>╔╝ </c2>\",\n      \" <c1>██</c1><c2>╔╝  </c2>\",\n      \"<c1>██</c1><c2>╔╝   </c2>\",\n      \"<c2>╚═╝    </c2>\"\n    ],\n    \":\": [\"   \", \"<c1>██</c1><c2>╗</c2>\", \"<c2>╚═╝</c2>\", \"<c1>██</c1><c2>╗</c2>\", \"<c2>╚═╝</c2>\", \"   \"],\n    \";\": [\"   \", \"   \", \"<c1>██</c1><c2>╗</c2>\", \"<c2>╚═╝</c2>\", \"<c1>▄█</c1><c2>╗</c2>\", \"<c1>▀</c1><c2>═╝</c2>\"],\n    \",\": [\"   \", \"   \", \"   \", \"   \", \"<c1>▄█</c1><c2>╗</c2>\", \"<c1>▀</c1><c2>═╝</c2>\"],\n    \"'\": [\"<c1>█</c1><c2>╗</c2> \", \"<c2>╚╝</c2> \", \"   \", \"   \", \"   \", \"   \"],\n    \"\\\"\": [\"<c1>█</c1><c2>╗</c2><c1>█</c1><c2>╗</c2> \", \"<c2>╚╝╚╝</c2> \", \"     \", \"     \", \"     \", \"     \"],\n    \" \": [\"   \", \"   \", \"   \", \"   \", \"   \", \"   \"]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/fonts/grid.json",
    "content": "{\n  \"name\": \"grid\",\n  \"version\": \"0.1.0\",\n  \"homepage\": \"https://github.com/dominikwilkowski/cfonts\",\n  \"colors\": 2,\n  \"lines\": 6,\n  \"buffer\": [\"\", \"\", \"\", \"\", \"\", \"\"],\n  \"letterspace\": [\"<c2>╋</c2>\", \"<c2>╋</c2>\", \"<c2>╋</c2>\", \"<c2>╋</c2>\", \"<c2>╋</c2>\", \"<c2>╋</c2>\"],\n  \"letterspace_size\": 1,\n  \"chars\": {\n    \"A\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏━━┓</c1>\", \"<c1>┃┏┓┃</c1>\", \"<c1>┃┏┓┃</c1>\", \"<c1>┗┛┗┛</c1>\", \"<c2>╋╋╋╋</c2>\"],\n    \"B\": [\n      \"<c1>┏┓</c1><c2>╋╋</c2>\",\n      \"<c1>┃┗━┓</c1>\",\n      \"<c1>┃┏┓┃</c1>\",\n      \"<c1>┃┗┛┃</c1>\",\n      \"<c1>┗━━┛</c1>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"C\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏━━┓</c1>\", \"<c1>┃┏━┛</c1>\", \"<c1>┃┗━┓</c1>\", \"<c1>┗━━┛</c1>\", \"<c2>╋╋╋╋</c2>\"],\n    \"D\": [\n      \"<c2>╋╋</c2><c1>┏┓</c1>\",\n      \"<c1>┏━┛┃</c1>\",\n      \"<c1>┃┏┓┃</c1>\",\n      \"<c1>┃┗┛┃</c1>\",\n      \"<c1>┗━━┛</c1>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"E\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏━━┓</c1>\", \"<c1>┃┃━┫</c1>\", \"<c1>┃┃━┫</c1>\", \"<c1>┗━━┛</c1>\", \"<c2>╋╋╋╋</c2>\"],\n    \"F\": [\n      \"<c2>╋</c2><c1>┏━┓</c1>\",\n      \"<c1>┏┛┗┓</c1>\",\n      \"<c1>┗┓┏┛</c1>\",\n      \"<c2>╋</c2><c1>┃┃</c1><c2>╋</c2>\",\n      \"<c2>╋</c2><c1>┗┛</c1><c2>╋</c2>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"G\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏━━┓</c1>\", \"<c1>┃┏┓┃</c1>\", \"<c1>┃┗┛┃</c1>\", \"<c1>┗━┓┃</c1>\", \"<c1>┗━━┛</c1>\"],\n    \"H\": [\n      \"<c1>┏┓</c1><c2>╋╋</c2>\",\n      \"<c1>┃┗━┓</c1>\",\n      \"<c1>┃┏┓┃</c1>\",\n      \"<c1>┃┃┃┃</c1>\",\n      \"<c1>┗┛┗┛</c1>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"I\": [\"<c1>┏┓</c1>\", \"<c1>┗┛</c1>\", \"<c1>┏┓</c1>\", \"<c1>┃┃</c1>\", \"<c1>┗┛</c1>\", \"<c2>╋╋</c2>\"],\n    \"J\": [\n      \"<c2>╋</c2><c1>┏┓</c1>\",\n      \"<c2>╋</c2><c1>┗┛</c1>\",\n      \"<c2>╋</c2><c1>┏┓</c1>\",\n      \"<c2>╋</c2><c1>┃┃</c1>\",\n      \"<c1>┏┛┃</c1>\",\n      \"<c1>┗━┛</c1>\"\n    ],\n    \"K\": [\n      \"<c1>┏┓</c1><c2>╋╋</c2>\",\n      \"<c1>┃┃┏┓</c1>\",\n      \"<c1>┃┗┛┛</c1>\",\n      \"<c1>┃┏┓┓</c1>\",\n      \"<c1>┗┛┗┛</c1>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"L\": [\n      \"<c1>┏┓</c1><c2>╋</c2>\",\n      \"<c1>┃┃</c1><c2>╋</c2>\",\n      \"<c1>┃┃</c1><c2>╋</c2>\",\n      \"<c1>┃┗┓</c1>\",\n      \"<c1>┗━┛</c1>\",\n      \"<c2>╋╋╋</c2>\"\n    ],\n    \"M\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏┓┏┓</c1>\", \"<c1>┃┗┛┃</c1>\", \"<c1>┃┃┃┃</c1>\", \"<c1>┗┻┻┛</c1>\", \"<c2>╋╋╋╋</c2>\"],\n    \"N\": [\n      \"<c2>╋╋╋╋</c2>\",\n      \"<c1>┏━┓</c1><c2>╋</c2>\",\n      \"<c1>┃┏┓┓</c1>\",\n      \"<c1>┃┃┃┃</c1>\",\n      \"<c1>┗┛┗┛</c1>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"O\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏━━┓</c1>\", \"<c1>┃┏┓┃</c1>\", \"<c1>┃┗┛┃</c1>\", \"<c1>┗━━┛</c1>\", \"<c2>╋╋╋╋</c2>\"],\n    \"P\": [\n      \"<c2>╋╋╋╋</c2>\",\n      \"<c1>┏━━┓</c1>\",\n      \"<c1>┃┏┓┃</c1>\",\n      \"<c1>┃┗┛┃</c1>\",\n      \"<c1>┃┏━┛</c1>\",\n      \"<c1>┗┛</c1><c2>╋╋</c2>\"\n    ],\n    \"Q\": [\n      \"<c2>╋╋╋╋</c2>\",\n      \"<c1>┏━━┓</c1>\",\n      \"<c1>┃┏┓┃</c1>\",\n      \"<c1>┃┗┛┃</c1>\",\n      \"<c1>┗━┓┃</c1>\",\n      \"<c2>╋╋</c2><c1>┗┛</c1>\"\n    ],\n    \"R\": [\n      \"<c2>╋╋╋</c2>\",\n      \"<c1>┏━┓</c1>\",\n      \"<c1>┃┏┛</c1>\",\n      \"<c1>┃┃</c1><c2>╋</c2>\",\n      \"<c1>┗┛</c1><c2>╋</c2>\",\n      \"<c2>╋╋╋</c2>\"\n    ],\n    \"S\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏━━┓</c1>\", \"<c1>┃━━┫</c1>\", \"<c1>┣━━┃</c1>\", \"<c1>┗━━┛</c1>\", \"<c2>╋╋╋╋</c2>\"],\n    \"T\": [\n      \"<c2>╋</c2><c1>┏┓</c1><c2>╋</c2>\",\n      \"<c1>┏┛┗┓</c1>\",\n      \"<c1>┗┓┏┛</c1>\",\n      \"<c2>╋</c2><c1>┃┗┓</c1>\",\n      \"<c2>╋</c2><c1>┗━┛</c1>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"U\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏┓┏┓</c1>\", \"<c1>┃┃┃┃</c1>\", \"<c1>┃┗┛┃</c1>\", \"<c1>┗━━┛</c1>\", \"<c2>╋╋╋╋</c2>\"],\n    \"V\": [\n      \"<c2>╋╋╋╋</c2>\",\n      \"<c1>┏┓┏┓</c1>\",\n      \"<c1>┃┗┛┃</c1>\",\n      \"<c1>┗┓┏┛</c1>\",\n      \"<c2>╋</c2><c1>┗┛</c1><c2>╋</c2>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"W\": [\n      \"<c2>╋╋╋╋╋╋</c2>\",\n      \"<c1>┏┓┏┓┏┓</c1>\",\n      \"<c1>┃┗┛┗┛┃</c1>\",\n      \"<c1>┗┓┏┓┏┛</c1>\",\n      \"<c2>╋</c2><c1>┗┛┗┛</c1><c2>╋</c2>\",\n      \"<c2>╋╋╋╋╋╋</c2>\"\n    ],\n    \"X\": [\"<c2>╋╋╋╋</c2>\", \"<c1>┏┓┏┓</c1>\", \"<c1>┗╋╋┛</c1>\", \"<c1>┏╋╋┓</c1>\", \"<c1>┗┛┗┛</c1>\", \"<c2>╋╋╋╋</c2>\"],\n    \"Y\": [\n      \"<c2>╋╋╋╋╋</c2>\",\n      \"<c1>┏┓</c1><c2>╋</c2><c1>┏┓</c1>\",\n      \"<c1>┃┗━┛┃</c1>\",\n      \"<c1>┗━┓┏┛</c1>\",\n      \"<c1>┗━━┛</c1><c2>╋</c2>\",\n      \"<c2>╋╋╋╋╋</c2>\"\n    ],\n    \"Z\": [\"<c2>╋╋╋╋╋</c2>\", \"<c1>┏━━━┓</c1>\", \"<c1>┣━━┃┃</c1>\", \"<c1>┃┃━━┫</c1>\", \"<c1>┗━━━┛</c1>\", \"<c2>╋╋╋╋╋</c2>\"],\n    \"0\": [\"<c1>┏━━━┓</c1>\", \"<c1>┃┏━┓┃</c1>\", \"<c1>┃┃┃┃┃</c1>\", \"<c1>┃┃┃┃┃</c1>\", \"<c1>┃┗━┛┃</c1>\", \"<c1>┗━━━┛</c1>\"],\n    \"1\": [\n      \"<c2>╋</c2><c1>┏┓</c1><c2>╋</c2>\",\n      \"<c1>┏┛┃</c1><c2>╋</c2>\",\n      \"<c1>┗┓┃</c1><c2>╋</c2>\",\n      \"<c2>╋</c2><c1>┃┃</c1><c2>╋</c2>\",\n      \"<c1>┏┛┗┓</c1>\",\n      \"<c1>┗━━┛</c1>\"\n    ],\n    \"2\": [\"<c1>┏━━━┓</c1>\", \"<c1>┃┏━┓┃</c1>\", \"<c1>┗┛┏┛┃</c1>\", \"<c1>┏━┛┏┛</c1>\", \"<c1>┃┗━┻┓</c1>\", \"<c1>┗━━━┛</c1>\"],\n    \"3\": [\"<c1>┏━━━┓</c1>\", \"<c1>┃┏━┓┃</c1>\", \"<c1>┗┛┏┛┃</c1>\", \"<c1>┏┓┗┓┃</c1>\", \"<c1>┃┗━┛┃</c1>\", \"<c1>┗━━━┛</c1>\"],\n    \"4\": [\n      \"<c1>┏┓</c1><c2>╋</c2><c1>┏┓</c1>\",\n      \"<c1>┃┃</c1><c2>╋</c2><c1>┃┃</c1>\",\n      \"<c1>┃┗━┛┃</c1>\",\n      \"<c1>┗━━┓┃</c1>\",\n      \"<c2>╋╋╋</c2><c1>┃┃</c1>\",\n      \"<c2>╋╋╋</c2><c1>┗┛</c1>\"\n    ],\n    \"5\": [\"<c1>┏━━━┓</c1>\", \"<c1>┃┏━━┛</c1>\", \"<c1>┃┗━━┓</c1>\", \"<c1>┗━━┓┃</c1>\", \"<c1>┏━━┛┃</c1>\", \"<c1>┗━━━┛</c1>\"],\n    \"6\": [\"<c1>┏━━━┓</c1>\", \"<c1>┃┏━━┛</c1>\", \"<c1>┃┗━━┓</c1>\", \"<c1>┃┏━┓┃</c1>\", \"<c1>┃┗━┛┃</c1>\", \"<c1>┗━━━┛</c1>\"],\n    \"7\": [\n      \"<c1>┏━━━┓</c1>\",\n      \"<c1>┃┏━┓┃</c1>\",\n      \"<c1>┗┛┏┛┃</c1>\",\n      \"<c2>╋╋</c2><c1>┃┏┛</c1>\",\n      \"<c2>╋╋</c2><c1>┃┃</c1><c2>╋</c2>\",\n      \"<c2>╋╋</c2><c1>┗┛</c1><c2>╋</c2>\"\n    ],\n    \"8\": [\"<c1>┏━━━┓</c1>\", \"<c1>┃┏━┓┃</c1>\", \"<c1>┃┗━┛┃</c1>\", \"<c1>┃┏━┓┃</c1>\", \"<c1>┃┗━┛┃</c1>\", \"<c1>┗━━━┛</c1>\"],\n    \"9\": [\"<c1>┏━━━┓</c1>\", \"<c1>┃┏━┓┃</c1>\", \"<c1>┃┗━┛┃</c1>\", \"<c1>┗━━┓┃</c1>\", \"<c1>┏━━┛┃</c1>\", \"<c1>┗━━━┛</c1>\"],\n    \"!\": [\"<c1>┏┓</c1>\", \"<c1>┃┃</c1>\", \"<c1>┃┃</c1>\", \"<c1>┗┛</c1>\", \"<c1>┏┓</c1>\", \"<c1>┗┛</c1>\"],\n    \"?\": [\n      \"<c1>┏━━━┓</c1>\",\n      \"<c1>┃┏━┓┃</c1>\",\n      \"<c1>┗┛┏┛┃</c1>\",\n      \"<c2>╋╋</c2><c1>┃┏┛</c1>\",\n      \"<c2>╋╋</c2><c1>┏┓</c1><c2>╋</c2>\",\n      \"<c2>╋╋</c2><c1>┗┛</c1><c2>╋</c2>\"\n    ],\n    \".\": [\"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c1>┏┓</c1>\", \"<c1>┗┛</c1>\"],\n    \"+\": [\n      \"<c2>╋╋╋╋</c2>\",\n      \"<c2>╋</c2><c1>┏┓</c1><c2>╋</c2>\",\n      \"<c1>┏┛┗┓</c1>\",\n      \"<c1>┗┓┏┛</c1>\",\n      \"<c2>╋</c2><c1>┗┛</c1><c2>╋</c2>\",\n      \"<c2>╋╋╋╋</c2>\"\n    ],\n    \"-\": [\"<c2>╋╋╋╋</c2>\", \"<c2>╋╋╋╋</c2>\", \"<c1>┏━━┓</c1>\", \"<c1>┗━━┛</c1>\", \"<c2>╋╋╋╋</c2>\", \"<c2>╋╋╋╋</c2>\"],\n    \"_\": [\"<c2>╋╋╋╋</c2>\", \"<c2>╋╋╋╋</c2>\", \"<c2>╋╋╋╋</c2>\", \"<c2>╋╋╋╋</c2>\", \"<c1>┏━━┓</c1>\", \"<c1>┗━━┛</c1>\"],\n    \"=\": [\"<c2>╋╋╋╋╋</c2>\", \"<c1>┏━━━┓</c1>\", \"<c1>┗━━━┛</c1>\", \"<c1>┏━━━┓</c1>\", \"<c1>┗━━━┛</c1>\", \"<c2>╋╋╋╋╋</c2>\"],\n    \"@\": [\n      \"<c1>┏━━━━┓</c1><c2>╋</c2>\",\n      \"<c1>┃┏━━┓┃</c1><c2>╋</c2>\",\n      \"<c1>┃┃┏━┃┃</c1><c2>╋</c2>\",\n      \"<c1>┃┃┗┛┃┃</c1><c2>╋</c2>\",\n      \"<c1>┃┗━━┛┗┓</c1>\",\n      \"<c1>┗━━━━━┛</c1>\"\n    ],\n    \"#\": [\n      \"<c2>╋</c2><c1>┏━━━┓</c1><c2>╋</c2>\",\n      \"<c1>┏┛┏━┓┗┓</c1>\",\n      \"<c1>┗┓┃┃┃┏┛</c1>\",\n      \"<c1>┏┛┃┃┃┗┓</c1>\",\n      \"<c1>┗┓┗━┛┏┛</c1>\",\n      \"<c2>╋</c2><c1>┗━━━┛</c1><c2>╋</c2>\"\n    ],\n    \"$\": [\n      \"<c2>╋</c2><c1>┏┓</c1><c2>╋</c2>\",\n      \"<c1>┏┛┗┓</c1>\",\n      \"<c1>┃━━┫</c1>\",\n      \"<c1>┣━━┃</c1>\",\n      \"<c1>┗┓┏┛</c1>\",\n      \"<c2>╋</c2><c1>┗┛</c1><c2>╋</c2>\"\n    ],\n    \"%\": [\n      \"<c1>┏┓</c1><c2>╋╋</c2><c1>┏━┓</c1>\",\n      \"<c1>┗┛</c1><c2>╋</c2><c1>┏┛┏┛</c1>\",\n      \"<c2>╋╋</c2><c1>┏┛┏┛</c1><c2>╋</c2>\",\n      \"<c2>╋</c2><c1>┏┛┏┛</c1><c2>╋╋</c2>\",\n      \"<c1>┏┛┏┛</c1><c2>╋</c2><c1>┏┓</c1>\",\n      \"<c1>┗━┛</c1><c2>╋╋</c2><c1>┗┛</c1>\"\n    ],\n    \"&\": [\n      \"<c2>╋╋</c2><c1>┏┓</c1><c2>╋</c2>\",\n      \"<c2>╋╋</c2><c1>┃┃</c1><c2>╋</c2>\",\n      \"<c1>┏━┛┗┓</c1>\",\n      \"<c1>┃┏┓┏┛</c1>\",\n      \"<c1>┃┗┛┃</c1><c2>╋</c2>\",\n      \"<c1>┗━━┛</c1><c2>╋</c2>\"\n    ],\n    \"(\": [\n      \"<c2>╋╋</c2><c1>┏━┓</c1>\",\n      \"<c2>╋</c2><c1>┏┛┏┛</c1>\",\n      \"<c1>┏┛┏┛</c1><c2>╋</c2>\",\n      \"<c1>┗┓┗┓</c1><c2>╋</c2>\",\n      \"<c2>╋</c2><c1>┗┓┗┓</c1>\",\n      \"<c2>╋╋</c2><c1>┗━┛</c1>\"\n    ],\n    \")\": [\n      \"<c1>┏━┓</c1><c2>╋╋</c2>\",\n      \"<c1>┗┓┗┓</c1><c2>╋</c2>\",\n      \"<c2>╋</c2><c1>┗┓┗┓</c1>\",\n      \"<c2>╋</c2><c1>┏┛┏┛</c1>\",\n      \"<c1>┏┛┏┛</c1><c2>╋</c2>\",\n      \"<c1>┗━┛</c1><c2>╋╋</c2>\"\n    ],\n    \"/\": [\n      \"<c2>╋╋╋╋</c2><c1>┏━┓</c1>\",\n      \"<c2>╋╋╋</c2><c1>┏┛┏┛</c1>\",\n      \"<c2>╋╋</c2><c1>┏┛┏┛</c1><c2>╋</c2>\",\n      \"<c2>╋</c2><c1>┏┛┏┛</c1><c2>╋╋</c2>\",\n      \"<c1>┏┛┏┛</c1><c2>╋╋╋</c2>\",\n      \"<c1>┗━┛</c1><c2>╋╋╋╋</c2>\"\n    ],\n    \":\": [\"<c2>╋╋</c2>\", \"<c1>┏┓</c1>\", \"<c1>┗┛</c1>\", \"<c1>┏┓</c1>\", \"<c1>┗┛</c1>\", \"<c2>╋╋</c2>\"],\n    \";\": [\"<c2>╋╋</c2>\", \"<c1>┏┓</c1>\", \"<c1>┗┛</c1>\", \"<c2>╋╋</c2>\", \"<c1>┏┓</c1>\", \"<c1>┗┫</c1>\"],\n    \",\": [\"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c1>┏┓</c1>\", \"<c1>┗┫</c1>\"],\n    \"'\": [\"<c1>┏┓</c1>\", \"<c1>┗┛</c1>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\"],\n    \"\\\"\": [\"<c1>┏┓┏┓</c1>\", \"<c1>┗┛┗┛</c1>\", \"<c2>╋╋╋╋</c2>\", \"<c2>╋╋╋╋</c2>\", \"<c2>╋╋╋╋</c2>\", \"<c2>╋╋╋╋</c2>\"],\n    \" \": [\"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\", \"<c2>╋╋</c2>\"]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/fonts/huge.json",
    "content": "{\n  \"name\": \"huge\",\n  \"version\": \"0.2.0\",\n  \"homepage\": \"https://github.com/dominikwilkowski/cfonts\",\n  \"colors\": 2,\n  \"lines\": 11,\n  \"buffer\": [\"\", \"\", \"\", \"\", \"\", \"\", \"\", \"\", \"\", \"\", \"\"],\n  \"letterspace\": [\" \", \" \", \" \", \" \", \" \", \" \", \" \", \" \", \" \", \" \", \" \"],\n  \"letterspace_size\": 1,\n  \"chars\": {\n    \"A\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▀         ▀</c1> \"\n    ],\n    \"B\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄</c1>  \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░</c2><c1>▌</c1> \",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀</c1>  \"\n    ],\n    \"C\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"D\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄</c1>  \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░</c2><c1>▌</c1> \",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀</c1>  \"\n    ],\n    \"E\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"F\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \" <c1>▀</c1>           \"\n    ],\n    \"G\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ ▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▐</c1><c2>░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ ▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"H\": [\n      \" <c1>▄         ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▀         ▀</c1> \"\n    ],\n    \"I\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀█</c1><c2>░</c2><c1>█▀▀▀▀</c1> \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \" <c1>▄▄▄▄█</c1><c2>░</c2><c1>█▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"J\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀█</c1><c2>░</c2><c1>█▀▀▀</c1> \",\n      \"      <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"      <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"      <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"      <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"      <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \" <c1>▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░░░░░░░</c2><c1>▌</c1>    \",\n      \" <c1>▀▀▀▀▀▀▀</c1>     \"\n    ],\n    \"K\": [\n      \" <c1>▄    ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1><c2>░</c2><c1>▌</c1>   \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1><c2>░</c2><c1>▌</c1>   \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▀    ▀</c1> \"\n    ],\n    \"L\": [\n      \" <c1>▄</c1>           \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"M\": [\n      \" <c1>▄▄       ▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌     ▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌   ▀   ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▀         ▀</c1> \"\n    ],\n    \"N\": [\n      \" <c1>▄▄        ▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌      ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1><c2>░</c2><c1>▌     ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌    ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌    ▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌     ▐</c1><c2>░</c2><c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌      ▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀        ▀▀</c1> \"\n    ],\n    \"O\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"P\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \" <c1>▀</c1>           \"\n    ],\n    \"Q\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀█</c1><c2>░</c2><c1>█▀▀</c1> \",\n      \"        <c1>▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"         <c1>▀</c1>   \"\n    ],\n    \"R\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀█</c1><c2>░</c2><c1>█▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌     ▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌      ▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▀         ▀</c1> \"\n    ],\n    \"S\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▄▄▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"T\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀█</c1><c2>░</c2><c1>█▀▀▀▀</c1> \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"      <c1>▀</c1>      \"\n    ],\n    \"U\": [\n      \" <c1>▄         ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"V\": [\n      \" <c1>▄               ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌             ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▐</c1><c2>░</c2><c1>▌           ▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌         ▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"   <c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>   \",\n      \"    <c1>▐</c1><c2>░</c2><c1>▌     ▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"      <c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1>      \",\n      \"       <c1>▐</c1><c2>░</c2><c1>▐</c1><c2>░</c2><c1>▌</c1>       \",\n      \"        <c1>▐</c1><c2>░</c2><c1>▌</c1>        \",\n      \"         <c1>▀</c1>         \"\n    ],\n    \"W\": [\n      \" <c1>▄         ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌   ▄   ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌     ▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀       ▀▀</c1> \"\n    ],\n    \"X\": [\n      \" <c1>▄       ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌     ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"   <c1>▐</c1><c2>░</c2><c1>▐</c1><c2>░</c2><c1>▌</c1>   \",\n      \"    <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"   <c1>▐</c1><c2>░</c2><c1>▌</c1><c2>░</c2><c1>▌</c1>   \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \" <c1>▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌     ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▀       ▀</c1> \"\n    ],\n    \"Y\": [\n      \" <c1>▄         ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀█</c1><c2>░</c2><c1>█▀▀▀▀</c1> \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"      <c1>▀</c1>      \"\n    ],\n    \"Z\": [\n      \" <c1>▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"    <c1>▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"   <c1>▐</c1><c2>░</c2><c1>▌</c1>   \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \" <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"0\": [\n      \"  <c1>▄▄▄▄▄▄▄▄▄</c1>  \",\n      \" <c1>▐</c1><c2>░░░░░░░░░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>█</c1><c2>░</c2><c1>█▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌    ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌  ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌ ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌    ▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄█</c1><c2>░</c2><c1>█</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▐</c1><c2>░░░░░░░░░</c2><c1>▌</c1> \",\n      \"  <c1>▀▀▀▀▀▀▀▀▀</c1>  \"\n    ],\n    \"1\": [\n      \"    <c1>▄▄▄▄</c1>     \",\n      \"  <c1>▄█</c1><c2>░░░░</c2><c1>▌</c1>    \",\n      \" <c1>▐</c1><c2>░░</c2><c1>▌▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"  <c1>▀▀ ▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"     <c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"     <c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"     <c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"     <c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \" <c1>▄▄▄▄█</c1><c2>░░</c2><c1>█▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"2\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▄▄▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"3\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▄▄▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▄▄▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"4\": [\n      \" <c1>▄         ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"           <c1>▀</c1> \"\n    ],\n    \"5\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▄▄▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"6\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>          \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"7\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"         <c1>▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"        <c1>▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"       <c1>▐</c1><c2>░</c2><c1>▌</c1>   \",\n      \"      <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"    <c1>▐</c1><c2>░</c2><c1>▌</c1>      \",\n      \"   <c1>▐</c1><c2>░</c2><c1>▌</c1>       \",\n      \"    <c1>▀</c1>        \"\n    ],\n    \"8\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▐</c1><c2>░░░░░░░░░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"9\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"          <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▄▄▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"!\": [\n      \" <c1>▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀</c1> \",\n      \" <c1>▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀</c1> \"\n    ],\n    \"?\": [\n      \"    <c1>▄▄▄▄▄▄▄</c1>  \",\n      \"  <c1>▄█</c1><c2>░░░░░░</c2><c1>█▄</c1> \",\n      \" <c1>▐</c1><c2>░░</c2><c1>▌▀▀▀▀█</c1><c2>░░</c2><c1>▌</c1>\",\n      \"  <c1>▀▀  ▄▄▄█</c1><c2>░░</c2><c1>▌</c1>\",\n      \"    <c1>▄█</c1><c2>░░░░░</c2><c1>█</c1> \",\n      \"   <c1>▐</c1><c2>░░</c2><c1>▌▀▀▀▀</c1>  \",\n      \"   <c1>▐</c1><c2>░░</c2><c1>▌</c1>      \",\n      \"    <c1>▀▀</c1>       \",\n      \"    <c1>▄▄</c1>       \",\n      \"   <c1>▐</c1><c2>░░</c2><c1>▌</c1>      \",\n      \"    <c1>▀▀</c1>       \"\n    ],\n    \".\": [\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \" <c1>▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀</c1> \"\n    ],\n    \"+\": [\n      \"          \",\n      \"          \",\n      \"    <c1>▄▄</c1>    \",\n      \"   <c1>▐</c1><c2>░░</c2><c1>▌</c1>   \",\n      \" <c1>▄▄█</c1><c2>░░</c2><c1>█▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀█</c1><c2>░░</c2><c1>█▀▀</c1> \",\n      \"   <c1>▐</c1><c2>░░</c2><c1>▌</c1>   \",\n      \"    <c1>▀▀</c1>    \",\n      \"          \",\n      \"          \"\n    ],\n    \"-\": [\n      \"       \",\n      \"       \",\n      \"       \",\n      \"       \",\n      \" <c1>▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀</c1> \",\n      \"       \",\n      \"       \",\n      \"       \",\n      \"       \"\n    ],\n    \"_\": [\n      \"       \",\n      \"       \",\n      \"       \",\n      \"       \",\n      \"       \",\n      \"       \",\n      \"       \",\n      \" <c1>▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀</c1> \",\n      \"       \"\n    ],\n    \"=\": [\n      \"       \",\n      \"       \",\n      \" <c1>▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀</c1> \",\n      \"       \",\n      \" <c1>▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀</c1> \",\n      \"       \",\n      \"       \"\n    ],\n    \"@\": [\n      \" <c1>▄▄▄▄▄▄▄▄▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌        ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌  ▄▄▄▄  ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ █</c1><c2>░░░░</c2><c1>█ ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▐</c1><c2>░</c2><c1>████</c1><c2>░</c2><c1>▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌ █</c1><c2>░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▌▄▄███████</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀▀▀▀▀▀▀▀▀</c1> \"\n    ],\n    \"#\": [\n      \"   <c1>▄         ▄</c1>   \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \" <c1>▄█</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>█▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀█</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>█▀</c1> \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \" <c1>▄█</c1><c2>░</c2><c1>█▄▄▄▄▄▄▄█</c1><c2>░</c2><c1>█▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀█</c1><c2>░</c2><c1>█▀▀▀▀▀▀▀█</c1><c2>░</c2><c1>█▀</c1> \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌       ▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"   <c1>▀         ▀</c1>   \"\n    ],\n    \"$\": [\n      \"      <c1>▄</c1>      \",\n      \" <c1>▄▄▄▄█</c1><c2>░</c2><c1>█▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀█</c1><c2>░</c2><c1>█▀▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄█</c1><c2>░</c2><c1>█▄▄▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀█</c1><c2>░</c2><c1>█▀▀█</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▄▄▄▄█</c1><c2>░</c2><c1>█▄▄█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░░░░░░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀▀▀█</c1><c2>░</c2><c1>█▀▀▀▀</c1> \",\n      \"      <c1>▀</c1>      \"\n    ],\n    \"%\": [\n      \"         <c1>▄</c1> \",\n      \"  <c1>▄     ▐</c1><c2>░</c2><c1>▌</c1>\",\n      \" <c1>▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"  <c1>▀   ▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>   \",\n      \"    <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"   <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌   ▄</c1>  \",\n      \" <c1>▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌     ▀</c1>  \",\n      \" <c1>▀</c1>         \"\n    ],\n    \"&\": [\n      \" <c1>▄▄▄▄▄▄▄</c1>     \",\n      \"<c1>▐</c1><c2>░░░░░░░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀█</c1><c2>░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌   ▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄█</c1><c2>░</c2><c1>▌</c1>    \",\n      \" <c1>▐</c1><c2>░░░░░░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░</c2><c1>█▀▀▀▀█</c1><c2>░</c2><c1>▌ ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌     ▐</c1><c2>░</c2><c1>█</c1><c2>░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>█▄▄▄▄█</c1><c2>░</c2><c1>▌</c1>   \",\n      \"<c1>▐</c1><c2>░░░░░░</c2><c1>▌▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \" <c1>▀▀▀▀▀▀  ▀</c1>   \"\n    ],\n    \"(\": [\n      \"  <c1>▄▄▄▄▄</c1> \",\n      \" <c1>▐</c1><c2>░░░░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░</c2><c1>█▀▀▀</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>    \",\n      \"<c1>▐</c1><c2>░░</c2><c1>█▄▄▄</c1> \",\n      \" <c1>▐</c1><c2>░░░░░</c2><c1>▌</c1>\",\n      \"  <c1>▀▀▀▀▀</c1> \"\n    ],\n    \")\": [\n      \" <c1>▄▄▄▄▄</c1>  \",\n      \"<c1>▐</c1><c2>░░░░░</c2><c1>▌</c1> \",\n      \" <c1>▀▀▀█</c1><c2>░░</c2><c1>▌</c1>\",\n      \"    <c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"    <c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"    <c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"    <c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \"    <c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▄▄▄█</c1><c2>░░</c2><c1>▌</c1>\",\n      \"<c1>▐</c1><c2>░░░░░</c2><c1>▌</c1> \",\n      \" <c1>▀▀▀▀▀</c1>  \"\n    ],\n    \"/\": [\n      \"         <c1>▄</c1> \",\n      \"        <c1>▐</c1><c2>░</c2><c1>▌</c1>\",\n      \"       <c1>▐</c1><c2>░</c2><c1>▌</c1> \",\n      \"      <c1>▐</c1><c2>░</c2><c1>▌</c1>  \",\n      \"     <c1>▐</c1><c2>░</c2><c1>▌</c1>   \",\n      \"    <c1>▐</c1><c2>░</c2><c1>▌</c1>    \",\n      \"   <c1>▐</c1><c2>░</c2><c1>▌</c1>     \",\n      \"  <c1>▐</c1><c2>░</c2><c1>▌</c1>      \",\n      \" <c1>▐</c1><c2>░</c2><c1>▌</c1>       \",\n      \"<c1>▐</c1><c2>░</c2><c1>▌</c1>        \",\n      \" <c1>▀</c1>         \"\n    ],\n    \":\": [\n      \"    \",\n      \"    \",\n      \" <c1>▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀</c1> \",\n      \"    \",\n      \" <c1>▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀</c1> \",\n      \"    \",\n      \"    \"\n    ],\n    \";\": [\n      \"    \",\n      \"    \",\n      \" <c1>▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▀</c1> \",\n      \"    \",\n      \" <c1>▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▌</c1> \",\n      \" <c1>▀</c1>  \",\n      \"    \"\n    ],\n    \",\": [\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \"    \",\n      \" <c1>▄▄</c1> \",\n      \"<c1>▐</c1><c2>░░</c2><c1>▌</c1>\",\n      \" <c1>▀▌</c1> \",\n      \" <c1>▀</c1>  \"\n    ],\n    \"'\": [\n      \" <c1>▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▐</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▐</c1>\",\n      \" <c1>▀</c1> \",\n      \"   \",\n      \"   \",\n      \"   \",\n      \"   \",\n      \"   \",\n      \"   \",\n      \"   \"\n    ],\n    \"\\\"\": [\n      \" <c1>▄  ▄</c1> \",\n      \"<c1>▐</c1><c2>░</c2><c1>▐▐</c1><c2>░</c2><c1>▐</c1>\",\n      \"<c1>▐</c1><c2>░</c2><c1>▐▐</c1><c2>░</c2><c1>▐</c1>\",\n      \" <c1>▀  ▀</c1> \",\n      \"      \",\n      \"      \",\n      \"      \",\n      \"      \",\n      \"      \",\n      \"      \",\n      \"      \"\n    ],\n    \" \": [\"    \", \"    \", \"    \", \"    \", \"    \", \"    \", \"    \", \"    \", \"    \", \"    \", \"    \"]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/fonts/pallet.json",
    "content": "{\n  \"name\": \"pallet\",\n  \"version\": \"0.1.0\",\n  \"homepage\": \"https://github.com/dominikwilkowski/cfonts\",\n  \"colors\": 2,\n  \"lines\": 6,\n  \"buffer\": [\"\", \"\", \"\", \"\", \"\", \"\"],\n  \"letterspace\": [\"<c2>─</c2>\", \"<c2>─</c2>\", \"<c2>─</c2>\", \"<c2>─</c2>\", \"<c2>─</c2>\", \"<c2>─</c2>\"],\n  \"letterspace_size\": 1,\n  \"chars\": {\n    \"A\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>╚╝</c1><c2>─</c2><c1>╚╝</c1>\"\n    ],\n    \"B\": [\n      \"<c1>╔══╗</c1><c2>─</c2>\",\n      \"<c1>║╔╗║</c1><c2>─</c2>\",\n      \"<c1>║╚╝╚╗</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>╚═══╝</c1>\"\n    ],\n    \"C\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>╚╝</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>╔╗</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>╚═══╝</c1>\"\n    ],\n    \"D\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>╚╗╔╗║</c1>\",\n      \"<c2>─</c2><c1>║║║║</c1>\",\n      \"<c2>─</c2><c1>║║║║</c1>\",\n      \"<c1>╔╝╚╝║</c1>\",\n      \"<c1>╚═══╝</c1>\"\n    ],\n    \"E\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔══╝</c1>\", \"<c1>║╚══╗</c1>\", \"<c1>║╔══╝</c1>\", \"<c1>║╚══╗</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"F\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>║╔══╝</c1>\",\n      \"<c1>║╚══╗</c1>\",\n      \"<c1>║╔══╝</c1>\",\n      \"<c1>║║</c1><c2>───</c2>\",\n      \"<c1>╚╝</c1><c2>───</c2>\"\n    ],\n    \"G\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>╚╝</c1>\",\n      \"<c1>║║╔═╗</c1>\",\n      \"<c1>║╚╩═║</c1>\",\n      \"<c1>╚═══╝</c1>\"\n    ],\n    \"H\": [\n      \"<c1>╔╗</c1><c2>─</c2><c1>╔╗</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>╚╝</c1><c2>─</c2><c1>╚╝</c1>\"\n    ],\n    \"I\": [\n      \"<c1>╔══╗</c1>\",\n      \"<c1>╚╣╠╝</c1>\",\n      \"<c2>─</c2><c1>║║</c1><c2>─</c2>\",\n      \"<c2>─</c2><c1>║║</c1><c2>─</c2>\",\n      \"<c1>╔╣╠╗</c1>\",\n      \"<c1>╚══╝</c1>\"\n    ],\n    \"J\": [\n      \"<c2>──</c2><c1>╔╗</c1>\",\n      \"<c2>──</c2><c1>║║</c1>\",\n      \"<c2>──</c2><c1>║║</c1>\",\n      \"<c1>╔╗║║</c1>\",\n      \"<c1>║╚╝║</c1>\",\n      \"<c1>╚══╝</c1>\"\n    ],\n    \"K\": [\n      \"<c1>╔╗╔═╗</c1>\",\n      \"<c1>║║║╔╝</c1>\",\n      \"<c1>║╚╝╝</c1><c2>─</c2>\",\n      \"<c1>║╔╗║</c1><c2>─</c2>\",\n      \"<c1>║║║╚╗</c1>\",\n      \"<c1>╚╝╚═╝</c1>\"\n    ],\n    \"L\": [\n      \"<c1>╔╗</c1><c2>───</c2>\",\n      \"<c1>║║</c1><c2>───</c2>\",\n      \"<c1>║║</c1><c2>───</c2>\",\n      \"<c1>║║</c1><c2>─</c2><c1>╔╗</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>╚═══╝</c1>\"\n    ],\n    \"M\": [\n      \"<c1>╔═╗╔═╗</c1>\",\n      \"<c1>║║╚╝║║</c1>\",\n      \"<c1>║╔╗╔╗║</c1>\",\n      \"<c1>║║║║║║</c1>\",\n      \"<c1>║║║║║║</c1>\",\n      \"<c1>╚╝╚╝╚╝</c1>\"\n    ],\n    \"N\": [\n      \"<c1>╔═╗</c1><c2>─</c2><c1>╔╗</c1>\",\n      \"<c1>║║╚╗║║</c1>\",\n      \"<c1>║╔╗╚╝║</c1>\",\n      \"<c1>║║╚╗║║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║║</c1>\",\n      \"<c1>╚╝</c1><c2>─</c2><c1>╚═╝</c1>\"\n    ],\n    \"O\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>╚═══╝</c1>\"\n    ],\n    \"P\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>║╔══╝</c1>\",\n      \"<c1>║║</c1><c2>───</c2>\",\n      \"<c1>╚╝</c1><c2>───</c2>\"\n    ],\n    \"Q\": [\n      \"<c1>╔═══╗</c1><c2>─</c2>\",\n      \"<c1>║╔═╗║</c1><c2>─</c2>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1><c2>─</c2>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1><c2>─</c2>\",\n      \"<c1>║╚═╝╠╗</c1>\",\n      \"<c1>╚════╝</c1>\"\n    ],\n    \"R\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>║╔╗╔╝</c1>\", \"<c1>║║║╚╗</c1>\", \"<c1>╚╝╚═╝</c1>\"],\n    \"S\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>║╚══╗</c1>\", \"<c1>╚══╗║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"T\": [\n      \"<c1>╔════╗</c1>\",\n      \"<c1>║╔╗╔╗║</c1>\",\n      \"<c1>╚╝║║╚╝</c1>\",\n      \"<c2>──</c2><c1>║║</c1><c2>──</c2>\",\n      \"<c2>──</c2><c1>║║</c1><c2>──</c2>\",\n      \"<c2>──</c2><c1>╚╝</c1><c2>──</c2>\"\n    ],\n    \"U\": [\n      \"<c1>╔╗</c1><c2>─</c2><c1>╔╗</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>╚═══╝</c1>\"\n    ],\n    \"V\": [\n      \"<c1>╔╗</c1><c2>──</c2><c1>╔╗</c1>\",\n      \"<c1>║╚╗╔╝║</c1>\",\n      \"<c1>╚╗║║╔╝</c1>\",\n      \"<c2>─</c2><c1>║╚╝║</c1><c2>─</c2>\",\n      \"<c2>─</c2><c1>╚╗╔╝</c1><c2>─</c2>\",\n      \"<c2>──</c2><c1>╚╝</c1><c2>──</c2>\"\n    ],\n    \"W\": [\n      \"<c1>╔╗╔╗╔╗</c1>\",\n      \"<c1>║║║║║║</c1>\",\n      \"<c1>║║║║║║</c1>\",\n      \"<c1>║╚╝╚╝║</c1>\",\n      \"<c1>╚╗╔╗╔╝</c1>\",\n      \"<c2>─</c2><c1>╚╝╚╝</c1><c2>─</c2>\"\n    ],\n    \"X\": [\n      \"<c1>╔═╗╔═╗</c1>\",\n      \"<c1>╚╗╚╝╔╝</c1>\",\n      \"<c2>─</c2><c1>╚╗╔╝</c1><c2>─</c2>\",\n      \"<c2>─</c2><c1>╔╝╚╗</c1><c2>─</c2>\",\n      \"<c1>╔╝╔╗╚╗</c1>\",\n      \"<c1>╚═╝╚═╝</c1>\"\n    ],\n    \"Y\": [\n      \"<c1>╔╗</c1><c2>──</c2><c1>╔╗</c1>\",\n      \"<c1>║╚╗╔╝║</c1>\",\n      \"<c1>╚╗╚╝╔╝</c1>\",\n      \"<c2>─</c2><c1>╚╗╔╝</c1><c2>─</c2>\",\n      \"<c2>──</c2><c1>║║</c1><c2>──</c2>\",\n      \"<c2>──</c2><c1>╚╝</c1><c2>──</c2>\"\n    ],\n    \"Z\": [\n      \"<c1>╔════╗</c1>\",\n      \"<c1>╚══╗═║</c1>\",\n      \"<c2>──</c2><c1>╔╝╔╝</c1>\",\n      \"<c2>─</c2><c1>╔╝╔╝</c1><c2>─</c2>\",\n      \"<c1>╔╝═╚═╗</c1>\",\n      \"<c1>╚════╝</c1>\"\n    ],\n    \"0\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>║║║║║</c1>\", \"<c1>║║║║║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"1\": [\n      \"<c2>─</c2><c1>╔╗</c1><c2>─</c2>\",\n      \"<c1>╔╝║</c1><c2>─</c2>\",\n      \"<c1>╚╗║</c1><c2>─</c2>\",\n      \"<c2>─</c2><c1>║║</c1><c2>─</c2>\",\n      \"<c1>╔╝╚╗</c1>\",\n      \"<c1>╚══╝</c1>\"\n    ],\n    \"2\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>╚╝╔╝║</c1>\", \"<c1>╔═╝╔╝</c1>\", \"<c1>║║╚═╗</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"3\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>╚╝╔╝║</c1>\", \"<c1>╔╗╚╗║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"4\": [\n      \"<c1>╔╗</c1><c2>─</c2><c1>╔╗</c1>\",\n      \"<c1>║║</c1><c2>─</c2><c1>║║</c1>\",\n      \"<c1>║╚═╝║</c1>\",\n      \"<c1>╚══╗║</c1>\",\n      \"<c2>───</c2><c1>║║</c1>\",\n      \"<c2>───</c2><c1>╚╝</c1>\"\n    ],\n    \"5\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔══╝</c1>\", \"<c1>║╚══╗</c1>\", \"<c1>╚══╗║</c1>\", \"<c1>╔══╝║</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"6\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔══╝</c1>\", \"<c1>║╚══╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"7\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>╚╝╔╝║</c1>\",\n      \"<c2>──</c2><c1>║╔╝</c1>\",\n      \"<c2>──</c2><c1>║║</c1><c2>─</c2>\",\n      \"<c2>──</c2><c1>╚╝</c1><c2>─</c2>\"\n    ],\n    \"8\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"9\": [\"<c1>╔═══╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>╚══╗║</c1>\", \"<c1>╔══╝║</c1>\", \"<c1>╚═══╝</c1>\"],\n    \"!\": [\"<c1>╔╗</c1>\", \"<c1>║║</c1>\", \"<c1>║║</c1>\", \"<c1>╚╝</c1>\", \"<c1>╔╗</c1>\", \"<c1>╚╝</c1>\"],\n    \"?\": [\n      \"<c1>╔═══╗</c1>\",\n      \"<c1>║╔═╗║</c1>\",\n      \"<c1>╚╝╔╝║</c1>\",\n      \"<c2>──</c2><c1>║╔╝</c1>\",\n      \"<c2>──</c2><c1>╔╗</c1><c2>─</c2>\",\n      \"<c2>──</c2><c1>╚╝</c1><c2>─</c2>\"\n    ],\n    \".\": [\"<c2>──</c2>\", \"<c2>──</c2>\", \"<c2>──</c2>\", \"<c2>──</c2>\", \"<c1>╔╗</c1>\", \"<c1>╚╝</c1>\"],\n    \"+\": [\n      \"<c2>────</c2>\",\n      \"<c2>────</c2>\",\n      \"<c2>─</c2><c1>╔╗</c1><c2>─</c2>\",\n      \"<c1>╔╝╚╗</c1>\",\n      \"<c1>╚╗╔╝</c1>\",\n      \"<c2>─</c2><c1>╚╝</c1><c2>─</c2>\"\n    ],\n    \"-\": [\"<c2>────</c2>\", \"<c2>────</c2>\", \"<c1>╔══╗</c1>\", \"<c1>╚══╝</c1>\", \"<c2>────</c2>\", \"<c2>────</c2>\"],\n    \"_\": [\"<c2>────</c2>\", \"<c2>────</c2>\", \"<c2>────</c2>\", \"<c2>────</c2>\", \"<c1>╔══╗</c1>\", \"<c1>╚══╝</c1>\"],\n    \"=\": [\"<c2>─────</c2>\", \"<c1>╔═══╗</c1>\", \"<c1>╚═══╝</c1>\", \"<c1>╔═══╗</c1>\", \"<c1>╚═══╝</c1>\", \"<c2>─────</c2>\"],\n    \"@\": [\n      \"<c1>╔════╗</c1><c2>─</c2>\",\n      \"<c1>║╔══╗║</c1><c2>─</c2>\",\n      \"<c1>║║╔═║║</c1><c2>─</c2>\",\n      \"<c1>║║╚╝║║</c1><c2>─</c2>\",\n      \"<c1>║╚══╝╠╗</c1>\",\n      \"<c1>╚═════╝</c1>\"\n    ],\n    \"#\": [\n      \"<c2>─</c2><c1>╔╩╩╩╗</c1><c2>─</c2>\",\n      \"<c1>╔╝╔═╗╚╗</c1>\",\n      \"<c1>╚╗╠═╣╔╝</c1>\",\n      \"<c1>╔╝╠═╣╚╗</c1>\",\n      \"<c1>╚╗╚═╝╔╝</c1>\",\n      \"<c2>─</c2><c1>╚╦╦╦╝</c1><c2>─</c2>\"\n    ],\n    \"$\": [\"<c1>╔╝╩╚╗</c1>\", \"<c1>║╔═╗║</c1>\", \"<c1>║╚══╗</c1>\", \"<c1>╚══╗║</c1>\", \"<c1>║╚═╝║</c1>\", \"<c1>╚╗╦╔╝</c1>\"],\n    \"%\": [\n      \"<c1>╔╗</c1><c2>──</c2><c1>╔═╗</c1>\",\n      \"<c1>╚╝</c1><c2>─</c2><c1>╔╝╔╝</c1>\",\n      \"<c2>──</c2><c1>╔╝╔╝</c1><c2>─</c2>\",\n      \"<c2>─</c2><c1>╔╝╔╝</c1><c2>──</c2>\",\n      \"<c1>╔╝╔╝</c1><c2>─</c2><c1>╔╗</c1>\",\n      \"<c1>╚═╝</c1><c2>──</c2><c1>╚╝</c1>\"\n    ],\n    \"&\": [\n      \"<c2>──</c2><c1>╔╗</c1><c2>─</c2>\",\n      \"<c2>──</c2><c1>║║</c1><c2>─</c2>\",\n      \"<c1>╔═╝╚╗</c1>\",\n      \"<c1>║╔╗╔╝</c1>\",\n      \"<c1>║╚╝║</c1><c2>─</c2>\",\n      \"<c1>╚══╝</c1><c2>─</c2>\"\n    ],\n    \"(\": [\n      \"<c2>──</c2><c1>╔═╗</c1>\",\n      \"<c2>─</c2><c1>╔╝╔╝</c1>\",\n      \"<c1>╔╝╔╝</c1><c2>─</c2>\",\n      \"<c1>╚╗╚╗</c1><c2>─</c2>\",\n      \"<c2>─</c2><c1>╚╗╚╗</c1>\",\n      \"<c2>──</c2><c1>╚═╝</c1>\"\n    ],\n    \")\": [\n      \"<c1>╔═╗</c1><c2>──</c2>\",\n      \"<c1>╚╗╚╗</c1><c2>─</c2>\",\n      \"<c2>─</c2><c1>╚╗╚╗</c1>\",\n      \"<c2>─</c2><c1>╔╝╔╝</c1>\",\n      \"<c1>╔╝╔╝</c1><c2>─</c2>\",\n      \"<c1>╚═╝</c1><c2>──</c2>\"\n    ],\n    \"/\": [\n      \"<c2>────</c2><c1>╔═╗</c1>\",\n      \"<c2>───</c2><c1>╔╝╔╝</c1>\",\n      \"<c2>──</c2><c1>╔╝╔╝</c1><c2>─</c2>\",\n      \"<c2>─</c2><c1>╔╝╔╝</c1><c2>──</c2>\",\n      \"<c1>╔╝╔╝</c1><c2>───</c2>\",\n      \"<c1>╚═╝</c1><c2>────</c2>\"\n    ],\n    \":\": [\"<c2>──</c2>\", \"<c1>╔╗</c1>\", \"<c1>╚╝</c1>\", \"<c1>╔╗</c1>\", \"<c1>╚╝</c1>\", \"<c2>──</c2>\"],\n    \";\": [\"<c2>──</c2>\", \"<c2>──</c2>\", \"<c2>──</c2>\", \"<c1>╔╗</c1>\", \"<c1>╚╣</c1>\", \"<c2>─</c2><c1>╝</c1>\"],\n    \",\": [\"<c1>╔╗</c1>\", \"<c1>║║</c1>\", \"<c1>╚╝</c1>\", \"<c1>╔╗</c1>\", \"<c1>╚╣</c1>\", \"<c2>─</c2><c1>╝</c1>\"],\n    \"'\": [\"<c1>╔╗</c1>\", \"<c1>║║</c1>\", \"<c1>╚╝</c1>\", \"<c2>──</c2>\", \"<c2>──</c2>\", \"<c2>──</c2>\"],\n    \"\\\"\": [\"<c1>╔╗╔╗</c1>\", \"<c1>║║║║</c1>\", \"<c1>╚╝╚╝</c1>\", \"<c2>────</c2>\", \"<c2>────</c2>\", \"<c2>────</c2>\"],\n    \" \": [\"<c2>───</c2>\", \"<c2>───</c2>\", \"<c2>───</c2>\", \"<c2>───</c2>\", \"<c2>───</c2>\", \"<c2>───</c2>\"]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/fonts/shade.json",
    "content": "{\n  \"name\": \"shade\",\n  \"version\": \"0.2.0\",\n  \"homepage\": \"https://github.com/dominikwilkowski/cfonts\",\n  \"colors\": 2,\n  \"lines\": 8,\n  \"buffer\": [\"\", \"\", \"\", \"\", \"\", \"\", \"\", \"\"],\n  \"letterspace\": [\n    \"<c2>░</c2>\",\n    \"<c2>░</c2>\",\n    \"<c2>░</c2>\",\n    \"<c2>░</c2>\",\n    \"<c2>░</c2>\",\n    \"<c2>░</c2>\",\n    \"<c2>░</c2>\",\n    \"<c2>░</c2>\"\n  ],\n  \"letterspace_size\": 1,\n  \"chars\": {\n    \"A\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>████</c1>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c2>░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"B\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>███</c1> \",\n      \"<c1>█  █</c1>\",\n      \"<c1>███</c1> \",\n      \"   <c2>░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"C\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"<c1>█</c1>   \",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"D\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>███</c1> \",\n      \"   <c2>░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"E\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"<c1>█</c1>   \",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"<c1>█</c1>  <c2>░</c2>\",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"F\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"<c1>█</c1>   \",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"<c1>█</c1>  <c2>░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \" <c2>░░░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"G\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>███</c1>\",\n      \"<c1>█</c1>   \",\n      \"<c1>█</c1><c2>░</c2><c1>██</c1>\",\n      \"<c1>█</c1><c2>░</c2> <c1>█</c1>\",\n      \"<c1>███</c1> \",\n      \"   <c2>░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"H\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>████</c1>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c2>░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"I\": [\n      \"<c2>░░░</c2>\",\n      \"<c1>███</c1>\",\n      \" <c1>█</c1> \",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c1>███</c1>\",\n      \"   \",\n      \"<c2>░░░</c2>\"\n    ],\n    \"J\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"  <c1>█</c1><c2>░</c2>\",\n      \"<c2>░░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c1>█</c1><c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"   <c2>░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"K\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>███</c1> \",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c2>░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"L\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"M\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>████</c1>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c2>░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"N\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>██</c1><c2>░</c2><c1>█</c1>\",\n      \"<c1>█ ██</c1>\",\n      \"<c1>█</c1><c2>░</c2> <c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c2>░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"O\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c1>██</c1> \",\n      \"<c2>░  ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"P\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>███</c1> \",\n      \"<c1>█</c1>  <c2>░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \" <c2>░░░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"Q\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c1>███</c1>\",\n      \"<c2>░</c2>   \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"R\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>███</c1> \",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c2>░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"S\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>███</c1>\",\n      \"<c1>█</c1>   \",\n      \" <c1>██</c1><c2>░</c2>\",\n      \"<c2>░</c2>  <c1>█</c1>\",\n      \"<c1>███</c1> \",\n      \"   <c2>░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"T\": [\n      \"<c2>░░░</c2>\",\n      \"<c1>███</c1>\",\n      \" <c1>█</c1> \",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░ ░</c2>\",\n      \"<c2>░░░</c2>\"\n    ],\n    \"U\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c1>██</c1> \",\n      \"<c2>░  ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"V\": [\n      \"<c2>░░░░░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░░</c2><c1>█</c1>\",\n      \" <c1>█</c1><c2>░</c2><c1>█</c1> \",\n      \"<c2>░</c2> <c1>█</c1> <c2>░</c2>\",\n      \"<c2>░░ ░░</c2>\",\n      \"<c2>░░░░░</c2>\"\n    ],\n    \"W\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>████</c1>\",\n      \"<c1>█  █</c1>\",\n      \" <c2>░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"X\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c1>██</c1> \",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░░</c2><c1>█</c1>\",\n      \" <c2>░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"Y\": [\n      \"<c2>░░░</c2>\",\n      \"<c1>█</c1><c2>░</c2><c1>█</c1>\",\n      \"<c1>███</c1>\",\n      \" <c1>█</c1> \",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░ ░</c2>\",\n      \"<c2>░░░</c2>\"\n    ],\n    \"Z\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"  <c1>█</c1> \",\n      \"<c2>░</c2><c1>█</c1> <c2>░</c2>\",\n      \"<c1>█</c1> <c2>░░</c2>\",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"0\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░</c2><c1>▌█</c1>\",\n      \"<c1>█</c1><c2>░</c2> <c1>█</c1>\",\n      \" <c1>██</c1> \",\n      \"<c2>░  ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"1\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>██</c1><c2>░░</c2>\",\n      \" <c1>█</c1><c2>░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"   <c2>░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"2\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>▐██</c1><c2>░</c2>\",\n      \"   <c1>█</c1>\",\n      \"<c2>░░</c2><c1>█</c1> \",\n      \"<c2>░</c2><c1>█</c1> <c2>░</c2>\",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"3\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"   <c1>█</c1>\",\n      \"<c2>░░</c2><c1>██</c1>\",\n      \"<c2>░░</c2> <c1>█</c1>\",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"4\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \"<c1>█</c1><c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c1>████</c1>\",\n      \"   <c1>█</c1>\",\n      \"<c2>░░░</c2><c1>█</c1>\",\n      \"<c2>░░░</c2> \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"5\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"<c1>█</c1>   \",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"   <c1>█</c1>\",\n      \"<c1>███</c1> \",\n      \"   <c2>░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"6\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>███</c1>\",\n      \"<c1>█</c1>   \",\n      \"<c1>███</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \" <c1>██</c1> \",\n      \"<c2>░  ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"7\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"   <c1>█</c1>\",\n      \"<c1>████</c1>\",\n      \" <c1>█</c1>  \",\n      \"<c1>█</c1> <c2>░░</c2>\",\n      \" <c2>░░░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"8\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \" <c1>██</c1> \",\n      \"<c1>█  █</c1>\",\n      \" <c1>██</c1> \",\n      \"<c2>░  ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"9\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \" <c1>███</c1>\",\n      \"<c2>░</c2>  <c1>█</c1>\",\n      \"<c2>░░</c2><c1>█</c1> \",\n      \"<c2>░░ ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"!\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c2>░  ░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c2>░  ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"?\": [\n      \"<c2>░░░░</c2>\",\n      \"<c1>▐██</c1><c2>░</c2>\",\n      \"   <c1>█</c1>\",\n      \"<c2>░░</c2><c1>█</c1> \",\n      \"<c2>░░ ░</c2>\",\n      \"<c2>░░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░░ ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \".\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░░ ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"+\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>███</c1>\",\n      \"<c2>░</c2> <c1>█</c1> \",\n      \"<c2>░░ ░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"-\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"_\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"    \"\n    ],\n    \"=\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c1>████</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"@\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>██</c1><c2>░</c2>\",\n      \"<c1>█  █</c1>\",\n      \"<c1>█</c1><c2>░</c2><c1>▌█</c1>\",\n      \"<c1>█</c1><c2>░</c2><c1>█</c1> \",\n      \" <c1>███</c1>\",\n      \"    \",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"#\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>▌▐</c1><c2>░</c2>\",\n      \"<c1>████</c1>\",\n      \" <c1>▌▐</c1> \",\n      \"<c1>████</c1>\",\n      \" <c1>▌▐</c1> \",\n      \"<c2>░  ░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"$\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░</c2><c1>▌</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>███</c1>\",\n      \"<c1>█ █</c1> \",\n      \" <c1>██</c1><c2>░</c2>\",\n      \"<c2>░░</c2><c1>▌█</c1>\",\n      \"<c1>███</c1> \",\n      \"   <c2>░</c2>\"\n    ],\n    \"%\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░</c2><c1>█</c1>\",\n      \"<c1>█</c1><c2>░</c2><c1>█</c1> \",\n      \" <c2>░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1> <c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2><c1>█</c1>\",\n      \"<c1>█</c1> <c2>░</c2> \",\n      \" <c2>░░░</c2>\"\n    ],\n    \"&\": [\n      \"<c2>░░░░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░░</c2>\",\n      \"<c1>█████</c1>\",\n      \"<c1>█  █</c1> \",\n      \"<c1>████</c1><c2>░</c2>\",\n      \"<c2>    </c2> \",\n      \"<c2>░░░░░</c2>\"\n    ],\n    \"(\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1> <c2>░</c2>\",\n      \"<c1>█</c1> <c2>░░</c2>\",\n      \"<c1>█</c1><c2>░░░</c2>\",\n      \" <c1>█</c1><c2>░░</c2>\",\n      \"<c2>░</c2> <c1>█</c1><c2>░</c2>\",\n      \"<c2>░░ ░</c2>\"\n    ],\n    \")\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c2>░</c2> <c1>█</c1><c2>░</c2>\",\n      \"<c2>░░</c2> <c1>█</c1>\",\n      \"<c2>░░░</c2><c1>█</c1>\",\n      \"<c2>░░</c2><c1>█</c1> \",\n      \"<c2>░</c2><c1>█</c1> <c2>░</c2>\",\n      \"<c2>░ ░░</c2>\"\n    ],\n    \"/\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░</c2><c1>█</c1>\",\n      \"<c2>░░</c2><c1>█</c1> \",\n      \"<c2>░░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1> <c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c1>█</c1> <c2>░░</c2>\",\n      \" <c2>░░░</c2>\"\n    ],\n    \":\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c2>░ ░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c2>░ ░░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \";\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c2>░ ░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\"\n    ],\n    \",\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░</c2><c1>█</c1><c2>░</c2>\",\n      \"<c2>░</c2><c1>█</c1> <c2>░</c2>\"\n    ],\n    \"'\": [\n      \"<c2>░░░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c2>░ ░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\",\n      \"<c2>░░░░</c2>\"\n    ],\n    \"\\\"\": [\n      \"<c2>░░░░░░</c2>\",\n      \"<c2>░</c2><c1>█</c1><c2>░</c2><c1>█</c1><c2>░░</c2>\",\n      \"<c2>░ ░ ░░</c2>\",\n      \"<c2>░░░░░░</c2>\",\n      \"<c2>░░░░░░</c2>\",\n      \"<c2>░░░░░░</c2>\",\n      \"<c2>░░░░░░</c2>\",\n      \"<c2>░░░░░░</c2>\"\n    ],\n    \" \": [\n      \"<c2>░░░</c2>\",\n      \"<c2>░░░</c2>\",\n      \"<c2>░░░</c2>\",\n      \"<c2>░░░</c2>\",\n      \"<c2>░░░</c2>\",\n      \"<c2>░░░</c2>\",\n      \"<c2>░░░</c2>\",\n      \"<c2>░░░</c2>\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/fonts/slick.json",
    "content": "{\n  \"name\": \"slick\",\n  \"version\": \"0.1.0\",\n  \"homepage\": \"https://github.com/dominikwilkowski/cfonts\",\n  \"colors\": 2,\n  \"lines\": 6,\n  \"buffer\": [\"\", \"\", \"\", \"\", \"\", \"\"],\n  \"letterspace\": [\"<c2>╱</c2>\", \"<c2>╱</c2>\", \"<c2>╱</c2>\", \"<c2>╱</c2>\", \"<c2>╱</c2>\", \"<c2>╱</c2>\"],\n  \"letterspace_size\": 1,\n  \"chars\": {\n    \"A\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>╰╯</c1><c2>╱</c2><c1>╰╯</c1>\"\n    ],\n    \"B\": [\n      \"<c1>╭━━╮</c1><c2>╱</c2>\",\n      \"<c1>┃╭╮┃</c1><c2>╱</c2>\",\n      \"<c1>┃╰╯╰╮</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>╰━━━╯</c1>\"\n    ],\n    \"C\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>╰╯</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>╭╮</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>╰━━━╯</c1>\"\n    ],\n    \"D\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>╰╮╭╮┃</c1>\",\n      \"<c2>╱</c2><c1>┃┃┃┃</c1>\",\n      \"<c2>╱</c2><c1>┃┃┃┃</c1>\",\n      \"<c1>╭╯╰╯┃</c1>\",\n      \"<c1>╰━━━╯</c1>\"\n    ],\n    \"E\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━━╯</c1>\", \"<c1>┃╰━━╮</c1>\", \"<c1>┃╭━━╯</c1>\", \"<c1>┃╰━━╮</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"F\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>┃╭━━╯</c1>\",\n      \"<c1>┃╰━━╮</c1>\",\n      \"<c1>┃╭━━╯</c1>\",\n      \"<c1>┃┃</c1><c2>╱╱╱</c2>\",\n      \"<c1>╰╯</c1><c2>╱╱╱</c2>\"\n    ],\n    \"G\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>╰╯</c1>\",\n      \"<c1>┃┃╭━╮</c1>\",\n      \"<c1>┃╰┻━┃</c1>\",\n      \"<c1>╰━━━╯</c1>\"\n    ],\n    \"H\": [\n      \"<c1>╭╮</c1><c2>╱</c2><c1>╭╮</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>╰╯</c1><c2>╱</c2><c1>╰╯</c1>\"\n    ],\n    \"I\": [\n      \"<c1>╭━━╮</c1>\",\n      \"<c1>╰┫┣╯</c1>\",\n      \"<c2>╱</c2><c1>┃┃</c1><c2>╱</c2>\",\n      \"<c2>╱</c2><c1>┃┃</c1><c2>╱</c2>\",\n      \"<c1>╭┫┣╮</c1>\",\n      \"<c1>╰━━╯</c1>\"\n    ],\n    \"J\": [\n      \"<c2>╱╱</c2><c1>╭╮</c1>\",\n      \"<c2>╱╱</c2><c1>┃┃</c1>\",\n      \"<c2>╱╱</c2><c1>┃┃</c1>\",\n      \"<c1>╭╮┃┃</c1>\",\n      \"<c1>┃╰╯┃</c1>\",\n      \"<c1>╰━━╯</c1>\"\n    ],\n    \"K\": [\n      \"<c1>╭╮╭━╮</c1>\",\n      \"<c1>┃┃┃╭╯</c1>\",\n      \"<c1>┃╰╯╯</c1><c2>╱</c2>\",\n      \"<c1>┃╭╮┃</c1><c2>╱</c2>\",\n      \"<c1>┃┃┃╰╮</c1>\",\n      \"<c1>╰╯╰━╯</c1>\"\n    ],\n    \"L\": [\n      \"<c1>╭╮</c1><c2>╱╱╱</c2>\",\n      \"<c1>┃┃</c1><c2>╱╱╱</c2>\",\n      \"<c1>┃┃</c1><c2>╱╱╱</c2>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>╭╮</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>╰━━━╯</c1>\"\n    ],\n    \"M\": [\n      \"<c1>╭━╮╭━╮</c1>\",\n      \"<c1>┃┃╰╯┃┃</c1>\",\n      \"<c1>┃╭╮╭╮┃</c1>\",\n      \"<c1>┃┃┃┃┃┃</c1>\",\n      \"<c1>┃┃┃┃┃┃</c1>\",\n      \"<c1>╰╯╰╯╰╯</c1>\"\n    ],\n    \"N\": [\n      \"<c1>╭━╮</c1><c2>╱</c2><c1>╭╮</c1>\",\n      \"<c1>┃┃╰╮┃┃</c1>\",\n      \"<c1>┃╭╮╰╯┃</c1>\",\n      \"<c1>┃┃╰╮┃┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃┃</c1>\",\n      \"<c1>╰╯</c1><c2>╱</c2><c1>╰━╯</c1>\"\n    ],\n    \"O\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>╰━━━╯</c1>\"\n    ],\n    \"P\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>┃╭━━╯</c1>\",\n      \"<c1>┃┃</c1><c2>╱╱╱</c2>\",\n      \"<c1>╰╯</c1><c2>╱╱╱</c2>\"\n    ],\n    \"Q\": [\n      \"<c1>╭━━━╮</c1><c2>╱</c2>\",\n      \"<c1>┃╭━╮┃</c1><c2>╱</c2>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1><c2>╱</c2>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1><c2>╱</c2>\",\n      \"<c1>┃╰━╯┃╮</c1>\",\n      \"<c1>╰━━━━╯</c1>\"\n    ],\n    \"R\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>┃╰━╯┃</c1>\", \"<c1>┃╭╮╭╯</c1>\", \"<c1>┃┃┃╰╮</c1>\", \"<c1>╰╯╰━╯</c1>\"],\n    \"S\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>┃╰━━╮</c1>\", \"<c1>╰━━╮┃</c1>\", \"<c1>┃╰━╯┃</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"T\": [\n      \"<c1>╭━━━━╮</c1>\",\n      \"<c1>┃╭╮╭╮┃</c1>\",\n      \"<c1>╰╯┃┃╰╯</c1>\",\n      \"<c2>╱╱</c2><c1>┃┃</c1><c2>╱╱</c2>\",\n      \"<c2>╱╱</c2><c1>┃┃</c1><c2>╱╱</c2>\",\n      \"<c2>╱╱</c2><c1>╰╯</c1><c2>╱╱</c2>\"\n    ],\n    \"U\": [\n      \"<c1>╭╮</c1><c2>╱</c2><c1>╭╮</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>╰━━━╯</c1>\"\n    ],\n    \"V\": [\n      \"<c1>╭╮</c1><c2>╱╱</c2><c1>╭╮</c1>\",\n      \"<c1>┃╰╮╭╯┃</c1>\",\n      \"<c1>╰╮┃┃╭╯</c1>\",\n      \"<c2>╱</c2><c1>┃╰╯┃</c1><c2>╱</c2>\",\n      \"<c2>╱</c2><c1>╰╮╭╯</c1><c2>╱</c2>\",\n      \"<c2>╱╱</c2><c1>╰╯</c1><c2>╱╱</c2>\"\n    ],\n    \"W\": [\n      \"<c1>╭╮╭╮╭╮</c1>\",\n      \"<c1>┃┃┃┃┃┃</c1>\",\n      \"<c1>┃┃┃┃┃┃</c1>\",\n      \"<c1>┃╰╯╰╯┃</c1>\",\n      \"<c1>╰╮╭╮╭╯</c1>\",\n      \"<c2>╱</c2><c1>╰╯╰╯</c1><c2>╱</c2>\"\n    ],\n    \"X\": [\n      \"<c1>╭━╮╭━╮</c1>\",\n      \"<c1>╰╮╰╯╭╯</c1>\",\n      \"<c2>╱</c2><c1>╰╮╭╯</c1><c2>╱</c2>\",\n      \"<c2>╱</c2><c1>╭╯╰╮</c1><c2>╱</c2>\",\n      \"<c1>╭╯╭╮╰╮</c1>\",\n      \"<c1>╰━╯╰━╯</c1>\"\n    ],\n    \"Y\": [\n      \"<c1>╭╮</c1><c2>╱╱</c2><c1>╭╮</c1>\",\n      \"<c1>┃╰╮╭╯┃</c1>\",\n      \"<c1>╰╮╰╯╭╯</c1>\",\n      \"<c2>╱</c2><c1>╰╮╭╯</c1><c2>╱</c2>\",\n      \"<c2>╱╱</c2><c1>┃┃</c1><c2>╱╱</c2>\",\n      \"<c2>╱╱</c2><c1>╰╯</c1><c2>╱╱</c2>\"\n    ],\n    \"Z\": [\n      \"<c1>╭━━━━╮</c1>\",\n      \"<c1>╰━━╮━┃</c1>\",\n      \"<c2>╱╱</c2><c1>╭╯╭╯</c1>\",\n      \"<c2>╱</c2><c1>╭╯╭╯</c1><c2>╱</c2>\",\n      \"<c1>╭╯━╰━╮</c1>\",\n      \"<c1>╰━━━━╯</c1>\"\n    ],\n    \"0\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>┃┃┃┃┃</c1>\", \"<c1>┃┃┃┃┃</c1>\", \"<c1>┃╰━╯┃</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"1\": [\n      \"<c2>╱</c2><c1>╭╮</c1><c2>╱</c2>\",\n      \"<c1>╭╯┃</c1><c2>╱</c2>\",\n      \"<c1>╰╮┃</c1><c2>╱</c2>\",\n      \"<c2>╱</c2><c1>┃┃</c1><c2>╱</c2>\",\n      \"<c1>╭╯╰╮</c1>\",\n      \"<c1>╰━━╯</c1>\"\n    ],\n    \"2\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>╰╯╭╯┃</c1>\", \"<c1>╭━╯╭╯</c1>\", \"<c1>┃╰━━╮</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"3\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>╰╯╭╯┃</c1>\", \"<c1>╭╮╰╮┃</c1>\", \"<c1>┃╰━╯┃</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"4\": [\n      \"<c1>╭╮</c1><c2>╱</c2><c1>╭╮</c1>\",\n      \"<c1>┃┃</c1><c2>╱</c2><c1>┃┃</c1>\",\n      \"<c1>┃╰━╯┃</c1>\",\n      \"<c1>╰━━╮┃</c1>\",\n      \"<c2>╱╱╱</c2><c1>┃┃</c1>\",\n      \"<c2>╱╱╱</c2><c1>╰╯</c1>\"\n    ],\n    \"5\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━━╯</c1>\", \"<c1>┃╰━━╮</c1>\", \"<c1>╰━━╮┃</c1>\", \"<c1>╭━━╯┃</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"6\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━━╯</c1>\", \"<c1>┃╰━━╮</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>┃╰━╯┃</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"7\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>╰╯╭╯┃</c1>\",\n      \"<c2>╱╱</c2><c1>┃╭╯</c1>\",\n      \"<c2>╱╱</c2><c1>┃┃</c1><c2>╱</c2>\",\n      \"<c2>╱╱</c2><c1>╰╯</c1><c2>╱</c2>\"\n    ],\n    \"8\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>┃╰━╯┃</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>┃╰━╯┃</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"9\": [\"<c1>╭━━━╮</c1>\", \"<c1>┃╭━╮┃</c1>\", \"<c1>┃╰━╯┃</c1>\", \"<c1>╰━━╮┃</c1>\", \"<c1>╭━━╯┃</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"!\": [\"<c1>╭╮</c1>\", \"<c1>┃┃</c1>\", \"<c1>┃┃</c1>\", \"<c1>╰╯</c1>\", \"<c1>╭╮</c1>\", \"<c1>╰╯</c1>\"],\n    \"?\": [\n      \"<c1>╭━━━╮</c1>\",\n      \"<c1>┃╭━╮┃</c1>\",\n      \"<c1>╰╯╭╯┃</c1>\",\n      \"<c2>╱╱</c2><c1>┃╭╯</c1>\",\n      \"<c2>╱╱</c2><c1>╭╮</c1><c2>╱</c2>\",\n      \"<c2>╱╱</c2><c1>╰╯</c1><c2>╱</c2>\"\n    ],\n    \".\": [\"<c2>╱╱</c2>\", \"<c2>╱╱</c2>\", \"<c2>╱╱</c2>\", \"<c2>╱╱</c2>\", \"<c1>╭╮</c1>\", \"<c1>╰╯</c1>\"],\n    \"+\": [\n      \"<c2>╱╱╱╱</c2>\",\n      \"<c2>╱╱╱╱</c2>\",\n      \"<c2>╱</c2><c1>╭╮</c1><c2>╱</c2>\",\n      \"<c1>╭╯╰╮</c1>\",\n      \"<c1>╰╮╭╯</c1>\",\n      \"<c2>╱</c2><c1>╰╯</c1><c2>╱</c2>\"\n    ],\n    \"-\": [\"<c2>╱╱╱╱</c2>\", \"<c2>╱╱╱╱</c2>\", \"<c2>╱╱╱╱</c2>\", \"<c1>╭━━╮</c1>\", \"<c1>╰━━╯</c1>\", \"<c2>╱╱╱╱</c2>\"],\n    \"_\": [\"<c2>╱╱╱╱</c2>\", \"<c2>╱╱╱╱</c2>\", \"<c2>╱╱╱╱</c2>\", \"<c2>╱╱╱╱</c2>\", \"<c1>╭━━╮</c1>\", \"<c1>╰━━╯</c1>\"],\n    \"=\": [\"<c2>╱╱╱╱╱</c2>\", \"<c2>╱╱╱╱╱</c2>\", \"<c1>╭━━━╮</c1>\", \"<c1>╰━━━╯</c1>\", \"<c1>╭━━━╮</c1>\", \"<c1>╰━━━╯</c1>\"],\n    \"@\": [\n      \"<c1>╭━━━━╮</c1><c2>╱</c2>\",\n      \"<c1>┃╭━━╮┃</c1><c2>╱</c2>\",\n      \"<c1>┃┃╭━┃┃</c1><c2>╱</c2>\",\n      \"<c1>┃┃╰╯┃┃</c1><c2>╱</c2>\",\n      \"<c1>┃╰━━╯━╮</c1>\",\n      \"<c1>╰━━━━━╯</c1>\"\n    ],\n    \"#\": [\n      \"<c2>╱</c2><c1>╭━━╮</c1><c2>╱</c2>\",\n      \"<c1>╭╯╭╮╰╮</c1>\",\n      \"<c1>╰╮┃┃╭╯</c1>\",\n      \"<c1>╭╯┃┃╰╮</c1>\",\n      \"<c1>╰╮╰╯╭╯</c1>\",\n      \"<c2>╱</c2><c1>╰━━╯</c1><c2>╱</c2>\"\n    ],\n    \"$\": [\n      \"<c2>╱╱</c2><c1>╭╮</c1><c2>╱</c2>\",\n      \"<c1>╭━╯╰╮</c1>\",\n      \"<c1>┃╰━━╮</c1>\",\n      \"<c1>╰━━╮┃</c1>\",\n      \"<c1>╰╮╭━╯</c1>\",\n      \"<c2>╱</c2><c1>╰╯</c1><c2>╱╱</c2>\"\n    ],\n    \"%\": [\n      \"<c1>╭╮</c1><c2>╱╱</c2><c1>╭━╮</c1>\",\n      \"<c1>╰╯</c1><c2>╱</c2><c1>╭╯╭╯</c1>\",\n      \"<c2>╱╱</c2><c1>╭╯╭╯</c1><c2>╱</c2>\",\n      \"<c2>╱</c2><c1>╭╯╭╯</c1><c2>╱╱</c2>\",\n      \"<c1>╭╯╭╯</c1><c2>╱</c2><c1>╭╮</c1>\",\n      \"<c1>╰━╯</c1><c2>╱╱</c2><c1>╰╯</c1>\"\n    ],\n    \"&\": [\n      \"<c2>╱</c2><c1>╭━━╮</c1>\",\n      \"<c2>╱</c2><c1>┃╭━╯</c1>\",\n      \"<c1>╭╯╰╮</c1><c2>╱</c2>\",\n      \"<c1>┃╭╮┃</c1><c2>╱</c2>\",\n      \"<c1>┃╰╯┃╮</c1>\",\n      \"<c1>╰━━━╯</c1>\"\n    ],\n    \"(\": [\n      \"<c2>╱╱</c2><c1>╭━╮</c1>\",\n      \"<c2>╱</c2><c1>╭╯╭╯</c1>\",\n      \"<c1>╭╯╭╯</c1><c2>╱</c2>\",\n      \"<c1>╰╮╰╮</c1><c2>╱</c2>\",\n      \"<c2>╱</c2><c1>╰╮╰╮</c1>\",\n      \"<c2>╱╱</c2><c1>╰━╯</c1>\"\n    ],\n    \")\": [\n      \"<c1>╭━╮</c1><c2>╱╱</c2>\",\n      \"<c1>╰╮╰╮</c1><c2>╱</c2>\",\n      \"<c2>╱</c2><c1>╰╮╰╮</c1>\",\n      \"<c2>╱</c2><c1>╭╯╭╯</c1>\",\n      \"<c1>╭╯╭╯</c1><c2>╱</c2>\",\n      \"<c1>╰━╯</c1><c2>╱╱</c2>\"\n    ],\n    \"/\": [\n      \"<c2>╱╱╱╱</c2><c1>╭━╮</c1>\",\n      \"<c2>╱╱╱</c2><c1>╭╯╭╯</c1>\",\n      \"<c2>╱╱</c2><c1>╭╯╭╯</c1><c2>╱</c2>\",\n      \"<c2>╱</c2><c1>╭╯╭╯</c1><c2>╱╱</c2>\",\n      \"<c1>╭╯╭╯</c1><c2>╱╱╱</c2>\",\n      \"<c1>╰━╯</c1><c2>╱╱╱╱</c2>\"\n    ],\n    \":\": [\"<c2>╱╱</c2>\", \"<c1>╭╮</c1>\", \"<c1>╰╯</c1>\", \"<c1>╭╮</c1>\", \"<c1>╰╯</c1>\", \"<c2>╱╱</c2>\"],\n    \";\": [\"<c1>╭╮</c1>\", \"<c1>┃┃</c1>\", \"<c1>╰╯</c1>\", \"<c1>╭╮</c1>\", \"<c1>╰┫</c1>\", \"<c2>╱</c2><c1>╯</c1>\"],\n    \",\": [\"<c2>╱╱</c2>\", \"<c2>╱╱</c2>\", \"<c2>╱╱</c2>\", \"<c1>╭╮</c1>\", \"<c1>╰┫</c1>\", \"<c2>╱</c2><c1>╯</c1>\"],\n    \"'\": [\"<c1>╭╮</c1>\", \"<c1>╰╯</c1>\", \"<c2>╱╱</c2>\", \"<c2>╱╱</c2>\", \"<c2>╱╱</c2>\", \"<c2>╱╱</c2>\"],\n    \"\\\"\": [\"<c1>╭╮╭╮</c1>\", \"<c1>╰╯╰╯</c1>\", \"<c2>╱╱╱╱</c2>\", \"<c2>╱╱╱╱</c2>\", \"<c2>╱╱╱╱</c2>\", \"<c2>╱╱╱╱</c2>\"],\n    \" \": [\"<c2>╱╱╱</c2>\", \"<c2>╱╱╱</c2>\", \"<c2>╱╱╱</c2>\", \"<c2>╱╱╱</c2>\", \"<c2>╱╱╱</c2>\", \"<c2>╱╱╱</c2>\"]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/fonts/tiny.json",
    "content": "{\n  \"name\": \"tiny\",\n  \"version\": \"0.2.0\",\n  \"homepage\": \"https://github.com/dominikwilkowski/cfonts\",\n  \"colors\": 1,\n  \"lines\": 2,\n  \"buffer\": [\"\", \"\"],\n  \"letterspace\": [\" \", \" \"],\n  \"letterspace_size\": 1,\n  \"chars\": {\n    \"A\": [\"▄▀█\", \"█▀█\"],\n    \"B\": [\"█▄▄\", \"█▄█\"],\n    \"C\": [\"█▀▀\", \"█▄▄\"],\n    \"D\": [\"█▀▄\", \"█▄▀\"],\n    \"E\": [\"█▀▀\", \"██▄\"],\n    \"F\": [\"█▀▀\", \"█▀ \"],\n    \"G\": [\"█▀▀\", \"█▄█\"],\n    \"H\": [\"█ █\", \"█▀█\"],\n    \"I\": [\"█\", \"█\"],\n    \"J\": [\"  █\", \"█▄█\"],\n    \"K\": [\"█▄▀\", \"█ █\"],\n    \"L\": [\"█  \", \"█▄▄\"],\n    \"M\": [\"█▀▄▀█\", \"█ ▀ █\"],\n    \"N\": [\"█▄ █\", \"█ ▀█\"],\n    \"O\": [\"█▀█\", \"█▄█\"],\n    \"P\": [\"█▀█\", \"█▀▀\"],\n    \"Q\": [\"█▀█\", \"▀▀█\"],\n    \"R\": [\"█▀█\", \"█▀▄\"],\n    \"S\": [\"█▀▀\", \"▄▄█\"],\n    \"T\": [\"▀█▀\", \" █ \"],\n    \"U\": [\"█ █\", \"█▄█\"],\n    \"V\": [\"█ █\", \"▀▄▀\"],\n    \"W\": [\"█ █ █\", \"▀▄▀▄▀\"],\n    \"X\": [\"▀▄▀\", \"█ █\"],\n    \"Y\": [\"█▄█\", \" █ \"],\n    \"Z\": [\"▀█\", \"█▄\"],\n    \"0\": [\"▞█▚\", \"▚█▞\"],\n    \"1\": [\"▄█\", \" █\"],\n    \"2\": [\"▀█\", \"█▄\"],\n    \"3\": [\"▀▀█\", \"▄██\"],\n    \"4\": [\"█ █\", \"▀▀█\"],\n    \"5\": [\"█▀\", \"▄█\"],\n    \"6\": [\"█▄▄\", \"█▄█\"],\n    \"7\": [\"▀▀█\", \"  █\"],\n    \"8\": [\"███\", \"█▄█\"],\n    \"9\": [\"█▀█\", \"▀▀█\"],\n    \"!\": [\"█\", \"▄\"],\n    \"?\": [\"▀█\", \" ▄\"],\n    \".\": [\" \", \"▄\"],\n    \"+\": [\"▄█▄\", \" ▀ \"],\n    \"-\": [\"▄▄\", \"  \"],\n    \"_\": [\"  \", \"▄▄\"],\n    \"=\": [\"▀▀\", \"▀▀\"],\n    \"@\": [\"▛█▜\", \"▙▟▃\"],\n    \"#\": [\"▟▄▙\", \"▜▀▛\"],\n    \"$\": [\"▖█▗\", \"▘█▝\"],\n    \"%\": [\"▀ ▄▀\", \"▄▀ ▄\"],\n    \"&\": [\"▄▄█\", \"█▄█\"],\n    \"(\": [\"▄▀\", \"▀▄\"],\n    \")\": [\"▀▄\", \"▄▀\"],\n    \"/\": [\"  ▄▀\", \"▄▀  \"],\n    \":\": [\"▀\", \"▄\"],\n    \";\": [\"  \", \"▄▀\"],\n    \",\": [\" \", \"█\"],\n    \"'\": [\"▀\", \" \"],\n    \"\\\"\": [\"▛ ▜\", \"   \"],\n    \" \": [\" \", \" \"]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/hast-styled-text.ts",
    "content": "import type { TextChunk } from \"../text-buffer.js\"\nimport { StyledText } from \"./styled-text.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\n\nexport interface HASTText {\n  type: \"text\"\n  value: string\n}\n\nexport interface HASTElement {\n  type: \"element\"\n  tagName: string\n  properties?: {\n    className?: string\n  }\n  children: HASTNode[]\n}\n\nexport type HASTNode = HASTText | HASTElement\n\nexport type { StyleDefinition } from \"../syntax-style.js\"\n\nfunction hastToTextChunks(node: HASTNode, syntaxStyle: SyntaxStyle, parentStyles: string[] = []): TextChunk[] {\n  const chunks: TextChunk[] = []\n\n  if (node.type === \"text\") {\n    const stylesToMerge = parentStyles.length > 0 ? parentStyles : [\"default\"]\n    const mergedStyle = syntaxStyle.mergeStyles(...stylesToMerge)\n\n    chunks.push({\n      __isChunk: true,\n      text: node.value,\n      fg: mergedStyle.fg,\n      bg: mergedStyle.bg,\n      attributes: mergedStyle.attributes,\n    })\n  } else if (node.type === \"element\") {\n    let currentStyles = [...parentStyles]\n\n    if (node.properties?.className) {\n      const classes = node.properties.className.split(\" \")\n\n      for (const cls of classes) {\n        currentStyles.push(cls)\n      }\n    }\n\n    for (const child of node.children) {\n      chunks.push(...hastToTextChunks(child, syntaxStyle, currentStyles))\n    }\n  }\n\n  return chunks\n}\n\nexport function hastToStyledText(hast: HASTNode, syntaxStyle: SyntaxStyle): StyledText {\n  const chunks = hastToTextChunks(hast, syntaxStyle)\n  return new StyledText(chunks)\n}\n"
  },
  {
    "path": "packages/core/src/lib/index.ts",
    "content": "export * from \"./border.js\"\nexport * from \"./KeyHandler.js\"\nexport * from \"./ascii.font.js\"\nexport * from \"./hast-styled-text.js\"\nexport * from \"./RGBA.js\"\nexport * from \"./clock.js\"\nexport * from \"./parse.keypress.js\"\nexport * from \"./scroll-acceleration.js\"\nexport * from \"./styled-text.js\"\nexport * from \"./yoga.options.js\"\nexport * from \"./parse.mouse.js\"\nexport * from \"./selection.js\"\nexport * from \"./env.js\"\nexport * from \"./stdin-parser.js\"\nexport * from \"./tree-sitter-styled-text.js\"\nexport * from \"./tree-sitter/index.js\"\nexport * from \"./data-paths.js\"\nexport * from \"./extmarks.js\"\nexport * from \"./terminal-palette.js\"\nexport * from \"./paste.js\"\nexport { detectLinks } from \"./detect-links.js\"\n"
  },
  {
    "path": "packages/core/src/lib/keymapping.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport {\n  mergeKeyBindings,\n  getKeyBindingKey,\n  buildKeyBindingsMap,\n  mergeKeyAliases,\n  defaultKeyAliases,\n  keyBindingToString,\n  type KeyAliasMap,\n} from \"./keymapping.js\"\n\ndescribe(\"keymapping\", () => {\n  describe(\"getKeyBindingKey\", () => {\n    it(\"should generate key with meta modifier\", () => {\n      const metaBinding = { name: \"a\", meta: true, action: \"test\" }\n      const key = getKeyBindingKey(metaBinding)\n      expect(key).toBe(\"a:0:0:1:0\")\n    })\n\n    it(\"should generate different keys for different modifiers\", () => {\n      const noMod = getKeyBindingKey({ name: \"a\", action: \"test\" })\n      const withMeta = getKeyBindingKey({ name: \"a\", meta: true, action: \"test\" })\n      const withCtrl = getKeyBindingKey({ name: \"a\", ctrl: true, action: \"test\" })\n      const withShift = getKeyBindingKey({ name: \"a\", shift: true, action: \"test\" })\n\n      expect(noMod).not.toBe(withMeta)\n      expect(noMod).not.toBe(withCtrl)\n      expect(noMod).not.toBe(withShift)\n      expect(withMeta).not.toBe(withCtrl)\n    })\n\n    it(\"should handle combined modifiers\", () => {\n      const key = getKeyBindingKey({ name: \"a\", ctrl: true, shift: true, meta: true, action: \"test\" })\n      expect(key).toBe(\"a:1:1:1:0\")\n    })\n\n    it(\"should generate key with super modifier\", () => {\n      const superBinding = { name: \"z\", super: true, action: \"test\" }\n      const key = getKeyBindingKey(superBinding)\n      expect(key).toBe(\"z:0:0:0:1\")\n    })\n  })\n\n  describe(\"mergeKeyBindings\", () => {\n    it(\"should merge defaults and custom bindings\", () => {\n      const defaults = [\n        { name: \"a\", action: \"action1\" as const },\n        { name: \"b\", action: \"action2\" as const },\n      ]\n      const custom = [{ name: \"c\", action: \"action3\" as const }]\n\n      const merged = mergeKeyBindings(defaults, custom)\n      expect(merged.length).toBe(3)\n    })\n\n    it(\"should allow custom to override defaults\", () => {\n      const defaults = [{ name: \"a\", action: \"action1\" as const }]\n      const custom = [{ name: \"a\", action: \"action2\" as const }]\n\n      const merged = mergeKeyBindings(defaults, custom)\n      expect(merged.length).toBe(1)\n      expect(merged[0]!.action).toBe(\"action2\")\n    })\n\n    it(\"should override when meta matches\", () => {\n      const defaults = [{ name: \"a\", meta: true, action: \"action1\" as const }]\n      const custom = [{ name: \"a\", meta: true, action: \"action2\" as const }]\n\n      const merged = mergeKeyBindings(defaults, custom)\n      expect(merged.length).toBe(1)\n      expect(merged[0]!.action).toBe(\"action2\")\n    })\n  })\n\n  describe(\"buildKeyBindingsMap\", () => {\n    it(\"should build map from bindings\", () => {\n      const bindings = [\n        { name: \"a\", action: \"action1\" as const },\n        { name: \"b\", meta: true, action: \"action2\" as const },\n      ]\n\n      const map = buildKeyBindingsMap(bindings)\n      expect(map.size).toBe(2)\n      expect(map.get(\"a:0:0:0:0\")).toBe(\"action1\")\n      expect(map.get(\"b:0:0:1:0\")).toBe(\"action2\")\n    })\n\n    it(\"should handle meta modifier correctly\", () => {\n      const bindings = [{ name: \"a\", meta: true, action: \"action1\" as const }]\n\n      const map = buildKeyBindingsMap(bindings)\n      expect(map.get(\"a:0:0:1:0\")).toBe(\"action1\")\n    })\n\n    it(\"should handle aliases and normalize key names\", () => {\n      const bindings = [{ name: \"return\", action: \"submit\" as const }]\n      const aliases: KeyAliasMap = { enter: \"return\" }\n\n      const map = buildKeyBindingsMap(bindings, aliases)\n\n      // Original binding should work\n      expect(map.get(\"return:0:0:0:0\")).toBe(\"submit\")\n      // Alias should not be added since \"enter\" wasn't in the binding\n      expect(map.get(\"enter:0:0:0:0\")).toBeUndefined()\n    })\n\n    it(\"should create aliased mappings for aliased key names\", () => {\n      const bindings = [{ name: \"enter\", action: \"submit\" as const }]\n      const aliases: KeyAliasMap = { enter: \"return\" }\n\n      const map = buildKeyBindingsMap(bindings, aliases)\n\n      // Original binding with \"enter\" name\n      expect(map.get(\"enter:0:0:0:0\")).toBe(\"submit\")\n      // Aliased version with normalized \"return\" name\n      expect(map.get(\"return:0:0:0:0\")).toBe(\"submit\")\n    })\n\n    it(\"should handle multiple aliases\", () => {\n      const bindings = [\n        { name: \"enter\", action: \"submit\" as const },\n        { name: \"esc\", action: \"cancel\" as const },\n      ]\n      const aliases: KeyAliasMap = { enter: \"return\", esc: \"escape\" }\n\n      const map = buildKeyBindingsMap(bindings, aliases)\n\n      expect(map.get(\"enter:0:0:0:0\")).toBe(\"submit\")\n      expect(map.get(\"return:0:0:0:0\")).toBe(\"submit\")\n      expect(map.get(\"esc:0:0:0:0\")).toBe(\"cancel\")\n      expect(map.get(\"escape:0:0:0:0\")).toBe(\"cancel\")\n    })\n\n    it(\"should handle aliases with modifiers\", () => {\n      const bindings = [{ name: \"enter\", meta: true, action: \"special-submit\" as const }]\n      const aliases: KeyAliasMap = { enter: \"return\" }\n\n      const map = buildKeyBindingsMap(bindings, aliases)\n\n      expect(map.get(\"enter:0:0:1:0\")).toBe(\"special-submit\")\n      expect(map.get(\"return:0:0:1:0\")).toBe(\"special-submit\")\n    })\n  })\n\n  describe(\"mergeKeyAliases\", () => {\n    it(\"should merge default and custom aliases\", () => {\n      const defaults: KeyAliasMap = { enter: \"return\" }\n      const custom: KeyAliasMap = { esc: \"escape\" }\n\n      const merged = mergeKeyAliases(defaults, custom)\n\n      expect(merged.enter).toBe(\"return\")\n      expect(merged.esc).toBe(\"escape\")\n    })\n\n    it(\"should allow custom aliases to override defaults\", () => {\n      const defaults: KeyAliasMap = { enter: \"return\" }\n      const custom: KeyAliasMap = { enter: \"custom-return\" }\n\n      const merged = mergeKeyAliases(defaults, custom)\n\n      expect(merged.enter).toBe(\"custom-return\")\n    })\n\n    it(\"should preserve defaults when no custom aliases provided\", () => {\n      const defaults: KeyAliasMap = { enter: \"return\", esc: \"escape\" }\n      const custom: KeyAliasMap = {}\n\n      const merged = mergeKeyAliases(defaults, custom)\n\n      expect(merged.enter).toBe(\"return\")\n      expect(merged.esc).toBe(\"escape\")\n    })\n  })\n\n  describe(\"defaultKeyAliases\", () => {\n    it(\"should have enter -> return alias\", () => {\n      expect(defaultKeyAliases.enter).toBe(\"return\")\n    })\n\n    it(\"should have esc -> escape alias\", () => {\n      expect(defaultKeyAliases.esc).toBe(\"escape\")\n    })\n  })\n\n  describe(\"alias override behavior\", () => {\n    it(\"should override 'return' binding when custom provides 'enter' binding with aliases\", () => {\n      const defaults = [{ name: \"return\", action: \"newline\" as const }]\n      const custom = [{ name: \"enter\", action: \"submit\" as const }]\n      const aliases: KeyAliasMap = { enter: \"return\" }\n\n      const merged = mergeKeyBindings(defaults, custom)\n      const map = buildKeyBindingsMap(merged, aliases)\n\n      const returnAction = map.get(\"return:0:0:0:0\")\n      const enterAction = map.get(\"enter:0:0:0:0\")\n\n      expect(returnAction).toBe(\"submit\")\n      expect(enterAction).toBe(\"submit\")\n    })\n\n    it(\"should also allow direct override using canonical name\", () => {\n      const defaults = [{ name: \"return\", action: \"newline\" as const }]\n      const custom = [{ name: \"return\", action: \"submit\" as const }]\n      const aliases: KeyAliasMap = { enter: \"return\" }\n\n      const merged = mergeKeyBindings(defaults, custom)\n      const map = buildKeyBindingsMap(merged, aliases)\n\n      const returnAction = map.get(\"return:0:0:0:0\")\n      const enterAction = map.get(\"enter:0:0:0:0\")\n\n      expect(returnAction).toBe(\"submit\")\n      expect(enterAction).toBeUndefined()\n    })\n\n    it(\"should handle the Textarea scenario: defaults with 'return', custom with 'enter'\", () => {\n      const defaults = [\n        { name: \"return\", action: \"newline\" as const },\n        { name: \"return\", meta: true, action: \"submit\" as const },\n      ]\n      const custom = [{ name: \"enter\", action: \"custom-submit\" as const }]\n      const aliases: KeyAliasMap = { enter: \"return\" }\n\n      const merged = mergeKeyBindings(defaults, custom)\n      const map = buildKeyBindingsMap(merged, aliases)\n\n      const returnNoMod = map.get(\"return:0:0:0:0\")\n      const returnWithMeta = map.get(\"return:0:0:1:0\")\n      const enterNoMod = map.get(\"enter:0:0:0:0\")\n\n      expect(returnNoMod).toBe(\"custom-submit\")\n      expect(enterNoMod).toBe(\"custom-submit\")\n      expect(returnWithMeta).toBe(\"submit\")\n    })\n  })\n\n  describe(\"keyBindingToString\", () => {\n    it(\"should convert simple key binding without modifiers\", () => {\n      const binding = { name: \"escape\", action: \"cancel\" as const }\n      expect(keyBindingToString(binding)).toBe(\"escape\")\n    })\n\n    it(\"should convert key binding with ctrl modifier\", () => {\n      const binding = { name: \"c\", ctrl: true, action: \"copy\" as const }\n      expect(keyBindingToString(binding)).toBe(\"ctrl+c\")\n    })\n\n    it(\"should convert key binding with shift modifier\", () => {\n      const binding = { name: \"up\", shift: true, action: \"scroll-fast\" as const }\n      expect(keyBindingToString(binding)).toBe(\"shift+up\")\n    })\n\n    it(\"should convert key binding with multiple modifiers\", () => {\n      const binding = { name: \"y\", ctrl: true, shift: true, action: \"copy\" as const }\n      expect(keyBindingToString(binding)).toBe(\"ctrl+shift+y\")\n    })\n\n    it(\"should convert key binding with all modifiers\", () => {\n      const binding = { name: \"a\", ctrl: true, shift: true, meta: true, super: true, action: \"all\" as const }\n      expect(keyBindingToString(binding)).toBe(\"ctrl+shift+meta+super+a\")\n    })\n\n    it(\"should convert key binding with meta modifier\", () => {\n      const binding = { name: \"s\", meta: true, action: \"save\" as const }\n      expect(keyBindingToString(binding)).toBe(\"meta+s\")\n    })\n\n    it(\"should convert key binding with super modifier\", () => {\n      const binding = { name: \"z\", super: true, action: \"undo\" as const }\n      expect(keyBindingToString(binding)).toBe(\"super+z\")\n    })\n\n    it(\"should handle special keys correctly\", () => {\n      expect(keyBindingToString({ name: \"return\", action: \"submit\" as const })).toBe(\"return\")\n      expect(keyBindingToString({ name: \"space\", action: \"select\" as const })).toBe(\"space\")\n      expect(keyBindingToString({ name: \"tab\", action: \"next\" as const })).toBe(\"tab\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/keymapping.ts",
    "content": "export interface KeyBinding<Action extends string = string> {\n  name: string\n  ctrl?: boolean\n  shift?: boolean\n  meta?: boolean\n  super?: boolean\n  action: Action\n}\n\nexport type KeyAliasMap = Record<string, string>\n\nexport const defaultKeyAliases: KeyAliasMap = {\n  enter: \"return\",\n  esc: \"escape\",\n}\n\nexport function mergeKeyAliases(defaults: KeyAliasMap, custom: KeyAliasMap): KeyAliasMap {\n  return { ...defaults, ...custom }\n}\n\nexport function mergeKeyBindings<Action extends string>(\n  defaults: KeyBinding<Action>[],\n  custom: KeyBinding<Action>[],\n): KeyBinding<Action>[] {\n  const map = new Map<string, KeyBinding<Action>>()\n  for (const binding of defaults) {\n    const key = getKeyBindingKey(binding)\n    map.set(key, binding)\n  }\n  for (const binding of custom) {\n    const key = getKeyBindingKey(binding)\n    map.set(key, binding)\n  }\n  return Array.from(map.values())\n}\n\nexport function getKeyBindingKey<Action extends string>(binding: KeyBinding<Action>): string {\n  return `${binding.name}:${binding.ctrl ? 1 : 0}:${binding.shift ? 1 : 0}:${binding.meta ? 1 : 0}:${binding.super ? 1 : 0}`\n}\n\nexport function buildKeyBindingsMap<Action extends string>(\n  bindings: KeyBinding<Action>[],\n  aliasMap?: KeyAliasMap,\n): Map<string, Action> {\n  const map = new Map<string, Action>()\n  const aliases = aliasMap || {}\n\n  for (const binding of bindings) {\n    const key = getKeyBindingKey(binding)\n    map.set(key, binding.action)\n  }\n\n  // Add aliased versions of all bindings\n  for (const binding of bindings) {\n    const normalizedName = aliases[binding.name] || binding.name\n    if (normalizedName !== binding.name) {\n      // Create aliased key with normalized name\n      const aliasedKey = getKeyBindingKey({ ...binding, name: normalizedName })\n      map.set(aliasedKey, binding.action)\n    }\n  }\n\n  return map\n}\n\n/**\n * Converts a key binding to a human-readable string representation\n * @param binding The key binding to stringify\n * @returns A string like \"ctrl+shift+y\" or just \"escape\"\n * @example\n * keyBindingToString({ name: \"y\", ctrl: true, shift: true }) // \"ctrl+shift+y\"\n * keyBindingToString({ name: \"escape\" }) // \"escape\"\n * keyBindingToString({ name: \"c\", ctrl: true }) // \"ctrl+c\"\n * keyBindingToString({ name: \"s\", super: true }) // \"super+s\"\n */\nexport function keyBindingToString<Action extends string>(binding: KeyBinding<Action>): string {\n  const parts: string[] = []\n\n  if (binding.ctrl) parts.push(\"ctrl\")\n  if (binding.shift) parts.push(\"shift\")\n  if (binding.meta) parts.push(\"meta\")\n  if (binding.super) parts.push(\"super\")\n\n  parts.push(binding.name)\n\n  return parts.join(\"+\")\n}\n"
  },
  {
    "path": "packages/core/src/lib/objects-in-viewport.test.ts",
    "content": "import { test, expect, describe } from \"bun:test\"\nimport { getObjectsInViewport } from \"./objects-in-viewport.js\"\nimport type { ViewportBounds } from \"../types.js\"\n\ninterface TestObject {\n  x: number\n  y: number\n  width: number\n  height: number\n  zIndex: number\n  id: string\n}\n\nfunction createObject(id: string, x: number, y: number, width: number, height: number, zIndex: number = 0): TestObject {\n  return { id, x, y, width, height, zIndex }\n}\n\ndescribe(\"getObjectsInViewport\", () => {\n  describe(\"basic functionality\", () => {\n    test(\"returns empty array for empty input\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 100, height: 100 }\n      const result = getObjectsInViewport(viewport, [])\n      expect(result).toEqual([])\n    })\n\n    test(\"returns all objects when count is below minTriggerSize\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 100, height: 100 }\n      const objects = [createObject(\"1\", 0, 0, 10, 10), createObject(\"2\", 200, 200, 10, 10)]\n      const result = getObjectsInViewport(viewport, objects, \"column\", 10, 16)\n      expect(result).toEqual(objects)\n    })\n\n    test(\"filters objects outside viewport in column direction\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"obj-5\")\n      expect(visibleIds).toContain(\"obj-6\")\n      expect(visibleIds).toContain(\"obj-9\")\n      expect(visibleIds).not.toContain(\"obj-0\")\n      expect(visibleIds).not.toContain(\"obj-15\")\n    })\n\n    test(\"filters objects outside viewport in row direction\", () => {\n      const viewport: ViewportBounds = { x: 100, y: 0, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, i * 20, 0, 20, 100))\n\n      const result = getObjectsInViewport(viewport, objects, \"row\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"obj-5\")\n      expect(visibleIds).toContain(\"obj-6\")\n      expect(visibleIds).toContain(\"obj-9\")\n      expect(visibleIds).not.toContain(\"obj-0\")\n      expect(visibleIds).not.toContain(\"obj-15\")\n    })\n  })\n\n  describe(\"padding behavior\", () => {\n    test(\"includes objects within padding distance\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 20, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"obj-4\")\n      expect(visibleIds).toContain(\"obj-10\")\n    })\n\n    test(\"respects custom padding values\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = Array.from({ length: 30 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20))\n\n      const resultNoPadding = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const resultWithPadding = getObjectsInViewport(viewport, objects, \"column\", 50, 16)\n\n      expect(resultWithPadding.length).toBeGreaterThan(resultNoPadding.length)\n    })\n  })\n\n  describe(\"zIndex sorting\", () => {\n    test(\"sorts visible objects by zIndex\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 10, 100, 10, 20 - i))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      for (let i = 1; i < result.length; i++) {\n        expect(result[i].zIndex).toBeGreaterThanOrEqual(result[i - 1].zIndex)\n      }\n    })\n\n    test(\"handles objects with same zIndex\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 10, 100, 10, 5))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      expect(result.every((obj) => obj.zIndex === 5)).toBe(true)\n    })\n\n    test(\"handles mixed zIndex values\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 10, 100, 10, i % 3))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      for (let i = 1; i < result.length; i++) {\n        expect(result[i].zIndex).toBeGreaterThanOrEqual(result[i - 1].zIndex)\n      }\n    })\n  })\n\n  describe(\"edge cases - boundary conditions\", () => {\n    test(\"includes object that starts at viewport top\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"obj-5\")\n    })\n\n    test(\"excludes object that ends exactly at viewport start (no padding)\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = [\n        createObject(\"before\", 0, 50, 100, 50),\n        ...Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, (i + 5) * 20, 100, 20)),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).not.toContain(\"before\")\n    })\n\n    test(\"excludes object that starts exactly at viewport end (no padding)\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = [\n        createObject(\"after\", 0, 200, 100, 20),\n        ...Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20)),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).not.toContain(\"after\")\n    })\n  })\n\n  describe(\"cross-axis filtering\", () => {\n    test(\"filters objects outside viewport on cross-axis (column mode)\", () => {\n      const viewport: ViewportBounds = { x: 50, y: 100, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) =>\n        createObject(`obj-${i}`, i % 2 === 0 ? 0 : 60, i * 20, 40, 20),\n      )\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      result.forEach((obj) => {\n        const objectRight = obj.x + obj.width\n        expect(objectRight).toBeGreaterThan(viewport.x)\n        expect(obj.x).toBeLessThan(viewport.x + viewport.width)\n      })\n    })\n\n    test(\"filters objects outside viewport on cross-axis (row mode)\", () => {\n      const viewport: ViewportBounds = { x: 100, y: 50, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) =>\n        createObject(`obj-${i}`, i * 20, i % 2 === 0 ? 0 : 60, 20, 40),\n      )\n\n      const result = getObjectsInViewport(viewport, objects, \"row\", 0, 16)\n\n      result.forEach((obj) => {\n        const objectBottom = obj.y + obj.height\n        expect(objectBottom).toBeGreaterThan(viewport.y)\n        expect(obj.y).toBeLessThan(viewport.y + viewport.height)\n      })\n    })\n  })\n\n  describe(\"scrolling simulation - vertical\", () => {\n    const createScrollList = () => {\n      return Array.from({ length: 100 }, (_, i) => createObject(`item-${i}`, 0, i * 50, 200, 50, i % 10))\n    }\n\n    test(\"viewport at top\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 200, height: 300 }\n      const objects = createScrollList()\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 10, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"item-0\")\n      expect(visibleIds).toContain(\"item-5\")\n      expect(visibleIds).not.toContain(\"item-20\")\n    })\n\n    test(\"viewport scrolled to middle\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 2000, width: 200, height: 300 }\n      const objects = createScrollList()\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 10, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"item-40\")\n      expect(visibleIds).toContain(\"item-45\")\n      expect(visibleIds).not.toContain(\"item-0\")\n      expect(visibleIds).not.toContain(\"item-99\")\n    })\n\n    test(\"viewport at bottom\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 4700, width: 200, height: 300 }\n      const objects = createScrollList()\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 10, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"item-94\")\n      expect(visibleIds).toContain(\"item-99\")\n      expect(visibleIds).not.toContain(\"item-0\")\n      expect(visibleIds).not.toContain(\"item-50\")\n    })\n\n    test(\"small incremental scrolls\", () => {\n      const objects = createScrollList()\n\n      for (let scrollY = 0; scrollY < 1000; scrollY += 10) {\n        const viewport: ViewportBounds = { x: 0, y: scrollY, width: 200, height: 300 }\n        const result = getObjectsInViewport(viewport, objects, \"column\", 10, 16)\n\n        result.forEach((obj) => {\n          const objectBottom = obj.y + obj.height\n          expect(objectBottom).toBeGreaterThan(viewport.y - 10)\n          expect(obj.y).toBeLessThan(viewport.y + viewport.height + 10)\n        })\n      }\n    })\n  })\n\n  describe(\"scrolling simulation - horizontal\", () => {\n    const createHorizontalList = () => {\n      return Array.from({ length: 100 }, (_, i) => createObject(`item-${i}`, i * 50, 0, 50, 200, i % 10))\n    }\n\n    test(\"viewport at left\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 300, height: 200 }\n      const objects = createHorizontalList()\n\n      const result = getObjectsInViewport(viewport, objects, \"row\", 10, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"item-0\")\n      expect(visibleIds).toContain(\"item-5\")\n      expect(visibleIds).not.toContain(\"item-20\")\n    })\n\n    test(\"viewport scrolled to middle\", () => {\n      const viewport: ViewportBounds = { x: 2000, y: 0, width: 300, height: 200 }\n      const objects = createHorizontalList()\n\n      const result = getObjectsInViewport(viewport, objects, \"row\", 10, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"item-40\")\n      expect(visibleIds).toContain(\"item-45\")\n      expect(visibleIds).not.toContain(\"item-0\")\n      expect(visibleIds).not.toContain(\"item-99\")\n    })\n\n    test(\"viewport at right\", () => {\n      const viewport: ViewportBounds = { x: 4700, y: 0, width: 300, height: 200 }\n      const objects = createHorizontalList()\n\n      const result = getObjectsInViewport(viewport, objects, \"row\", 10, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"item-94\")\n      expect(visibleIds).toContain(\"item-99\")\n      expect(visibleIds).not.toContain(\"item-0\")\n    })\n  })\n\n  describe(\"large objects\", () => {\n    test(\"handles objects much larger than viewport\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 500, width: 100, height: 100 }\n      const objects = [\n        ...Array.from({ length: 20 }, (_, i) => createObject(`filler-${i}`, 0, i * 100, 100, 50)),\n        createObject(\"huge\", 0, 100, 100, 1000),\n        createObject(\"tiny-after\", 0, 1200, 100, 10),\n      ].sort((a, b) => a.y - b.y)\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"huge\")\n    })\n\n    test(\"large object with many small items before viewport (realistic background panel)\", () => {\n      // Simulates a background panel spanning entire list with small list items\n      const objects = [\n        createObject(\"background-panel\", 0, 0, 100, 3000), // Large spanning background\n        ...Array.from({ length: 30 }, (_, i) => createObject(`item-${i}`, 0, i * 60, 100, 50)), // List items\n        ...Array.from({ length: 20 }, (_, i) => createObject(`filler-${i}`, 0, i * 100 + 3000, 100, 50)),\n      ]\n\n      // Viewport at y=1500, background spans 0-3000, with ~25 items between them\n      const viewport: ViewportBounds = { x: 0, y: 1500, width: 100, height: 100 }\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"background-panel\")\n    })\n\n    test(\"handles very tall objects in vertical scroll\", () => {\n      const objects = [\n        createObject(\"small-1\", 0, 0, 100, 50),\n        createObject(\"tall-1\", 0, 100, 100, 500),\n        createObject(\"small-2\", 0, 650, 100, 50),\n        createObject(\"tall-2\", 0, 750, 100, 800),\n        createObject(\"small-3\", 0, 1600, 100, 50),\n        ...Array.from({ length: 20 }, (_, i) => createObject(`filler-${i}`, 0, i * 100 + 2000, 100, 50)),\n      ]\n\n      for (let scrollY = 0; scrollY < 2000; scrollY += 100) {\n        const viewport: ViewportBounds = { x: 0, y: scrollY, width: 100, height: 200 }\n        const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n        result.forEach((obj) => {\n          const objectBottom = obj.y + obj.height\n          expect(objectBottom).toBeGreaterThan(viewport.y)\n          expect(obj.y).toBeLessThan(viewport.y + viewport.height)\n        })\n      }\n    })\n\n    test(\"handles very wide objects in horizontal scroll\", () => {\n      const objects = [\n        createObject(\"small-1\", 0, 0, 50, 100),\n        createObject(\"wide-1\", 100, 0, 500, 100),\n        createObject(\"small-2\", 650, 0, 50, 100),\n        createObject(\"wide-2\", 750, 0, 800, 100),\n        ...Array.from({ length: 20 }, (_, i) => createObject(`filler-${i}`, i * 100 + 2000, 0, 50, 100)),\n      ]\n\n      for (let scrollX = 0; scrollX < 2000; scrollX += 100) {\n        const viewport: ViewportBounds = { x: scrollX, y: 0, width: 200, height: 100 }\n        const result = getObjectsInViewport(viewport, objects, \"row\", 0, 16)\n\n        result.forEach((obj) => {\n          const objectRight = obj.x + obj.width\n          expect(objectRight).toBeGreaterThan(viewport.x)\n          expect(obj.x).toBeLessThan(viewport.x + viewport.width)\n        })\n      }\n    })\n  })\n\n  describe(\"viewport size variations\", () => {\n    const objects = Array.from({ length: 100 }, (_, i) => createObject(`item-${i}`, 0, i * 30, 200, 30, i % 5))\n\n    test(\"very small viewport\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 500, width: 50, height: 50 }\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      expect(result.length).toBeGreaterThan(0)\n      expect(result.length).toBeLessThan(10)\n    })\n\n    test(\"very large viewport\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 500, width: 1000, height: 1000 }\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      expect(result.length).toBeGreaterThan(30)\n    })\n\n    test(\"viewport larger than all objects\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 500, height: 5000 }\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      expect(result.length).toBe(objects.length)\n    })\n  })\n\n  describe(\"negative coordinates\", () => {\n    test(\"handles negative viewport coordinates\", () => {\n      const viewport: ViewportBounds = { x: -50, y: -50, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, -100, i * 20 - 100, 200, 20))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      result.forEach((obj) => {\n        const objectBottom = obj.y + obj.height\n        expect(objectBottom).toBeGreaterThan(viewport.y)\n        expect(obj.y).toBeLessThan(viewport.y + viewport.height)\n      })\n    })\n\n    test(\"handles negative object coordinates\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 100, height: 100 }\n      const objects = [createObject(\"negative-y\", 0, -50, 100, 100), createObject(\"positive-y\", 0, 50, 100, 100)]\n      objects.push(...Array.from({ length: 20 }, (_, i) => createObject(`filler-${i}`, 0, i * 20, 100, 20)))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"negative-y\")\n      expect(visibleIds).toContain(\"positive-y\")\n    })\n  })\n\n  describe(\"sparse object distributions\", () => {\n    test(\"handles large gaps between objects\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 5000, width: 100, height: 100 }\n      const objects = Array.from({ length: 50 }, (_, i) => createObject(`obj-${i}`, 0, i * 1000, 100, 50))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"obj-5\")\n      expect(result.length).toBeLessThan(5)\n    })\n\n    test(\"handles clustered objects\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 500, width: 100, height: 100 }\n      const objects = [\n        ...Array.from({ length: 10 }, (_, i) => createObject(`cluster-${i}`, 0, 490 + i * 2, 100, 2)),\n        ...Array.from({ length: 10 }, (_, i) => createObject(`filler-${i}`, 0, i * 100, 100, 20)),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      expect(result.length).toBeGreaterThan(5)\n    })\n  })\n\n  describe(\"minTriggerSize parameter\", () => {\n    test(\"bypasses optimization when object count is below threshold\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 100, height: 100 }\n      const objects = [createObject(\"far-away\", 0, 10000, 100, 100), createObject(\"visible\", 0, 50, 100, 100)]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 100)\n      expect(result.length).toBe(2)\n      expect(result.map((o) => o.id)).toContain(\"far-away\")\n    })\n\n    test(\"applies optimization when object count meets threshold\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 0, width: 100, height: 100 }\n      const objects = [\n        ...Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20)),\n        createObject(\"far-away\", 0, 10000, 100, 100),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      expect(result.map((o) => o.id)).not.toContain(\"far-away\")\n    })\n\n    test(\"performs overlap checks when minTriggerSize is 0\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 10, width: 40, height: 1 }\n      const objects = [createObject(\"above-viewport\", 0, 0, 40, 5), createObject(\"in-viewport\", 0, 10, 40, 1)]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 0)\n      expect(result.length).toBe(1)\n      expect(result[0].id).toBe(\"in-viewport\")\n    })\n\n    test(\"filters out objects outside viewport when minTriggerSize is 0\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 10, width: 40, height: 5 }\n      const objects = [\n        createObject(\"above-1\", 0, 0, 40, 3),\n        createObject(\"above-2\", 0, 5, 40, 4),\n        createObject(\"in-viewport\", 0, 12, 40, 2),\n        createObject(\"below\", 0, 20, 40, 5),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 0)\n      expect(result.length).toBe(1)\n      expect(result[0].id).toBe(\"in-viewport\")\n    })\n\n    test(\"respects exact boundary conditions with minTriggerSize 0\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = [\n        createObject(\"ends-at-start\", 0, 50, 100, 50),\n        createObject(\"overlaps-start\", 0, 50, 100, 51),\n        createObject(\"inside\", 0, 150, 100, 20),\n        createObject(\"overlaps-end\", 0, 199, 100, 10),\n        createObject(\"starts-at-end\", 0, 200, 100, 50),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 0)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).not.toContain(\"ends-at-start\")\n      expect(visibleIds).toContain(\"overlaps-start\")\n      expect(visibleIds).toContain(\"inside\")\n      expect(visibleIds).toContain(\"overlaps-end\")\n      expect(visibleIds).not.toContain(\"starts-at-end\")\n    })\n  })\n\n  describe(\"overlapping objects\", () => {\n    test(\"handles completely overlapping objects\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = [\n        ...Array.from({ length: 10 }, (_, i) => createObject(`filler-before-${i}`, 0, i * 20, 100, 20)),\n        createObject(\"back\", 0, 100, 100, 100, 0),\n        createObject(\"middle\", 0, 100, 100, 100, 1),\n        createObject(\"front\", 0, 100, 100, 100, 2),\n        ...Array.from({ length: 10 }, (_, i) => createObject(`filler-after-${i}`, 0, (i + 10) * 20, 100, 20)),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const overlapping = result.filter(\n        (obj) => obj.id.includes(\"back\") || obj.id.includes(\"middle\") || obj.id.includes(\"front\"),\n      )\n\n      expect(overlapping[0].id).toBe(\"back\")\n      expect(overlapping[1].id).toBe(\"middle\")\n      expect(overlapping[2].id).toBe(\"front\")\n    })\n\n    test(\"handles partially overlapping objects\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = Array.from({ length: 30 }, (_, i) => createObject(`obj-${i}`, 0, i * 15, 100, 30, i % 3))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      for (let i = 1; i < result.length; i++) {\n        expect(result[i].zIndex).toBeGreaterThanOrEqual(result[i - 1].zIndex)\n      }\n    })\n  })\n\n  describe(\"extreme values\", () => {\n    test(\"zero-sized viewport returns empty array (zero width)\", () => {\n      const viewport: ViewportBounds = { x: 100, y: 100, width: 0, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      expect(result.length).toBe(0)\n    })\n\n    test(\"zero-sized viewport returns empty array (zero height)\", () => {\n      const viewport: ViewportBounds = { x: 100, y: 100, width: 100, height: 0 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      expect(result.length).toBe(0)\n    })\n\n    test(\"zero-sized viewport returns empty array (both zero)\", () => {\n      const viewport: ViewportBounds = { x: 100, y: 100, width: 0, height: 0 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 20))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      expect(result.length).toBe(0)\n    })\n\n    test(\"handles zero-sized objects\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = Array.from({ length: 20 }, (_, i) => createObject(`obj-${i}`, 0, i * 20, 100, 0))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      expect(result).toBeDefined()\n    })\n\n    test(\"handles very large coordinates\", () => {\n      const viewport: ViewportBounds = { x: 1000000, y: 1000000, width: 100, height: 100 }\n      const objects = Array.from({ length: 50 }, (_, i) => createObject(`obj-${i}`, 1000000, 1000000 + i * 20, 100, 20))\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      expect(result.length).toBeGreaterThan(0)\n    })\n  })\n\n  describe(\"performance characteristics\", () => {\n    test(\"handles 1000 objects efficiently\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 50000, width: 100, height: 100 }\n      const objects = Array.from({ length: 1000 }, (_, i) => createObject(`obj-${i}`, 0, i * 100, 100, 100, i % 10))\n\n      const start = performance.now()\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const duration = performance.now() - start\n\n      expect(result.length).toBeGreaterThan(0)\n      expect(result.length).toBeLessThan(20)\n      expect(duration).toBeLessThan(10)\n    })\n\n    test(\"handles 10000 objects efficiently\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 500000, width: 100, height: 100 }\n      const objects = Array.from({ length: 10000 }, (_, i) => createObject(`obj-${i}`, 0, i * 100, 100, 100, i % 10))\n\n      const start = performance.now()\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const duration = performance.now() - start\n\n      expect(result.length).toBeGreaterThan(0)\n      expect(result.length).toBeLessThan(20)\n      expect(duration).toBeLessThan(50)\n    })\n  })\n\n  describe(\"additional edge cases\", () => {\n    test(\"object that starts before viewport and extends through it\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 500, width: 100, height: 100 }\n      const objects = Array.from({ length: 30 }, (_, i) => {\n        if (i === 2) {\n          return createObject(\"spanning\", 0, 200, 100, 500)\n        }\n        return createObject(`obj-${i}`, 0, i * 50, 100, 40)\n      })\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      expect(visibleIds).toContain(\"spanning\")\n    })\n\n    test(\"multiple large overlapping objects during scroll\", () => {\n      const objects = [\n        createObject(\"bg-1\", 0, 0, 200, 1000, 0),\n        createObject(\"bg-2\", 0, 500, 200, 1000, 0),\n        createObject(\"bg-3\", 0, 1000, 200, 1000, 0),\n        ...Array.from({ length: 50 }, (_, i) => createObject(`small-${i}`, 0, i * 50, 200, 40, 1)),\n      ]\n\n      for (let scrollY = 0; scrollY <= 1500; scrollY += 100) {\n        const viewport: ViewportBounds = { x: 0, y: scrollY, width: 200, height: 300 }\n        const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n        result.forEach((obj) => {\n          const objectBottom = obj.y + obj.height\n          expect(objectBottom).toBeGreaterThan(viewport.y)\n          expect(obj.y).toBeLessThan(viewport.y + viewport.height)\n        })\n      }\n    })\n\n    test(\"viewport moves down through very tall object\", () => {\n      const objects = [\n        ...Array.from({ length: 5 }, (_, i) => createObject(`before-${i}`, 0, i * 50, 100, 40)),\n        createObject(\"very-tall\", 0, 300, 100, 2000),\n        ...Array.from({ length: 20 }, (_, i) => createObject(`after-${i}`, 0, 2400 + i * 50, 100, 40)),\n      ]\n\n      for (let scrollY = 0; scrollY <= 2500; scrollY += 200) {\n        const viewport: ViewportBounds = { x: 0, y: scrollY, width: 100, height: 200 }\n        const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n        if (scrollY >= 100 && scrollY <= 2100) {\n          const visibleIds = result.map((o) => o.id)\n          expect(visibleIds).toContain(\"very-tall\")\n        }\n      }\n    })\n\n    test(\"objects with zero width or height\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 100, width: 100, height: 100 }\n      const objects = [\n        createObject(\"zero-height\", 0, 150, 100, 0),\n        createObject(\"zero-width\", 0, 160, 0, 40),\n        createObject(\"point\", 0, 170, 0, 0),\n        ...Array.from({ length: 20 }, (_, i) => createObject(`normal-${i}`, 0, i * 20, 100, 15)),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      expect(result).toBeDefined()\n    })\n\n    test(\"viewport positioned between objects (should return empty)\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 1000, width: 100, height: 100 }\n      const objects = [\n        ...Array.from({ length: 10 }, (_, i) => createObject(`before-${i}`, 0, i * 50, 100, 40)),\n        ...Array.from({ length: 10 }, (_, i) => createObject(`after-${i}`, 0, 2000 + i * 50, 100, 40)),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      expect(result.length).toBe(0)\n    })\n\n    test(\"single pixel gaps between objects and viewport\", () => {\n      const viewport: ViewportBounds = { x: 0, y: 1000, width: 100, height: 100 }\n      const objects = [\n        createObject(\"one-pixel-before\", 0, 899, 100, 100), // ends at 999, gap of 1px\n        createObject(\"touching-before\", 0, 999, 100, 1), // ends exactly at 1000\n        createObject(\"inside\", 0, 1050, 100, 10), // fully inside\n        createObject(\"touching-after\", 0, 1100, 100, 1), // starts exactly at 1100\n        createObject(\"one-pixel-after\", 0, 1101, 100, 100), // starts at 1101, gap of 1px\n        ...Array.from({ length: 20 }, (_, i) => createObject(`filler-${i}`, 0, i * 100, 100, 50)),\n      ]\n\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n      const visibleIds = result.map((o) => o.id)\n\n      // Objects with even 1px gap should be excluded (no overlap)\n      expect(visibleIds).not.toContain(\"one-pixel-before\")\n      expect(visibleIds).not.toContain(\"touching-before\")\n      expect(visibleIds).toContain(\"inside\")\n      expect(visibleIds).not.toContain(\"touching-after\")\n      expect(visibleIds).not.toContain(\"one-pixel-after\")\n    })\n  })\n\n  describe(\"stress test - continuous scrolling\", () => {\n    test(\"scrolling through 1000 objects with varying heights\", () => {\n      const heights = [20, 50, 30, 100, 40, 60, 25, 80, 35, 70]\n      let currentY = 0\n      const objects = Array.from({ length: 1000 }, (_, i) => {\n        const height = heights[i % heights.length]\n        const obj = createObject(`item-${i}`, 0, currentY, 200, height, i % 5)\n        currentY += height + 2\n        return obj\n      })\n\n      const totalHeight = currentY\n      const viewportHeight = 400\n\n      for (let scrollY = 0; scrollY < totalHeight - viewportHeight; scrollY += 100) {\n        const viewport: ViewportBounds = { x: 0, y: scrollY, width: 200, height: viewportHeight }\n        const result = getObjectsInViewport(viewport, objects, \"column\", 50, 16)\n\n        result.forEach((obj) => {\n          const objectBottom = obj.y + obj.height\n          expect(objectBottom).toBeGreaterThan(viewport.y - 50)\n          expect(obj.y).toBeLessThan(viewport.y + viewport.height + 50)\n        })\n\n        expect(result.length).toBeGreaterThan(0)\n        expect(result.length).toBeLessThan(50)\n      }\n    })\n  })\n\n  describe(\"realistic scroll scenarios\", () => {\n    test(\"chat-like interface with variable height messages\", () => {\n      const heights = [30, 60, 45, 90, 120, 35, 50, 75, 40, 100]\n      let currentY = 0\n      const objects = Array.from({ length: 100 }, (_, i) => {\n        const height = heights[i % heights.length]\n        const obj = createObject(`msg-${i}`, 0, currentY, 300, height, 0)\n        currentY += height + 5\n        return obj\n      })\n\n      for (let scroll = 0; scroll < currentY - 500; scroll += 50) {\n        const viewport: ViewportBounds = { x: 0, y: scroll, width: 300, height: 500 }\n        const result = getObjectsInViewport(viewport, objects, \"column\", 20, 16)\n\n        result.forEach((obj) => {\n          const objectBottom = obj.y + obj.height\n          expect(objectBottom).toBeGreaterThan(viewport.y - 20)\n          expect(obj.y).toBeLessThan(viewport.y + viewport.height + 20)\n        })\n      }\n    })\n\n    test(\"grid layout with multiple columns\", () => {\n      const objects = Array.from({ length: 200 }, (_, i) => {\n        const col = i % 4\n        const row = Math.floor(i / 4)\n        return createObject(`item-${i}`, col * 110, row * 110, 100, 100, 0)\n      })\n\n      const viewport: ViewportBounds = { x: 0, y: 1000, width: 440, height: 400 }\n      const result = getObjectsInViewport(viewport, objects, \"column\", 0, 16)\n\n      expect(result.length).toBeGreaterThan(0)\n      expect(result.length).toBeLessThan(30)\n\n      result.forEach((obj) => {\n        expect(obj.y + obj.height).toBeGreaterThan(viewport.y)\n        expect(obj.y).toBeLessThan(viewport.y + viewport.height)\n        expect(obj.x + obj.width).toBeGreaterThan(viewport.x)\n        expect(obj.x).toBeLessThan(viewport.x + viewport.width)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/objects-in-viewport.ts",
    "content": "import type { ViewportBounds } from \"../types.js\"\n\ninterface ViewportObject {\n  x: number\n  y: number\n  width: number\n  height: number\n  zIndex: number\n}\n\n/**\n * Returns objects that overlap with the viewport bounds.\n *\n * @param viewport - The viewport bounds to check against\n * @param objects - Array of objects MUST be sorted by position (y for column, x for row direction)\n * @param direction - Primary scroll direction: \"column\" (vertical) or \"row\" (horizontal)\n * @param padding - Extra padding around viewport to include nearby objects\n * @param minTriggerSize - Minimum array size to use binary search optimization\n * @returns Array of visible objects sorted by zIndex\n *\n * @remarks\n * Objects must be pre-sorted by their start position (y for column direction, x for row direction).\n * Unsorted input will produce incorrect results.\n */\nexport function getObjectsInViewport<T extends ViewportObject>(\n  viewport: ViewportBounds,\n  objects: T[],\n  direction: \"row\" | \"column\" = \"column\",\n  padding: number = 10,\n  minTriggerSize: number = 16,\n): T[] {\n  if (viewport.width <= 0 || viewport.height <= 0) {\n    return []\n  }\n\n  if (objects.length === 0) {\n    return []\n  }\n\n  if (objects.length < minTriggerSize) {\n    return objects\n  }\n\n  const viewportTop = viewport.y - padding\n  const viewportBottom = viewport.y + viewport.height + padding\n  const viewportLeft = viewport.x - padding\n  const viewportRight = viewport.x + viewport.width + padding\n\n  const isRow = direction === \"row\"\n\n  const children = objects\n  const totalChildren = children.length\n  if (totalChildren === 0) return []\n\n  const vpStart = isRow ? viewportLeft : viewportTop\n  const vpEnd = isRow ? viewportRight : viewportBottom\n\n  // Binary search to find any child that overlaps along the primary axis\n  let lo = 0\n  let hi = totalChildren - 1\n  let candidate = -1\n  while (lo <= hi) {\n    const mid = (lo + hi) >> 1\n    const c = children[mid]\n    const start = isRow ? c.x : c.y\n    const end = isRow ? c.x + c.width : c.y + c.height\n\n    if (end < vpStart) {\n      lo = mid + 1\n    } else if (start > vpEnd) {\n      hi = mid - 1\n    } else {\n      candidate = mid\n      break\n    }\n  }\n\n  const visibleChildren: T[] = []\n\n  // If binary search found no candidate, the viewport might be in a gap between objects\n  // Start from the position where the search ended\n  if (candidate === -1) {\n    // Binary search failed to find overlap - viewport is in a gap\n    // We need to check objects before lo for any that extend into the viewport\n    candidate = lo > 0 ? lo - 1 : 0\n  }\n\n  // Expand left to find all objects that overlap the viewport\n  // To handle large objects that start early but extend far, we continue\n  // checking even after finding objects that don't overlap, up to a limit\n  // This handles cases where many small objects sit between a large object and the viewport\n  // Real-world examples: background panels, large images, or spanning containers\n  const maxLookBehind = 50\n  let left = candidate\n  let gapCount = 0\n\n  while (left - 1 >= 0) {\n    const prev = children[left - 1]\n    const prevEnd = isRow ? prev.x + prev.width : prev.y + prev.height\n\n    if (prevEnd <= vpStart) {\n      gapCount++\n      if (gapCount >= maxLookBehind) {\n        break\n      }\n    } else {\n      gapCount = 0\n    }\n\n    left--\n  }\n\n  // Expand right to find the rightmost overlapping object\n  let right = candidate + 1\n  while (right < totalChildren) {\n    const next = children[right]\n    if ((isRow ? next.x : next.y) >= vpEnd) break\n    right++\n  }\n\n  // Collect candidates that also overlap on the cross axis\n  for (let i = left; i < right; i++) {\n    const child = children[i]\n    const start = isRow ? child.x : child.y\n    const end = isRow ? child.x + child.width : child.y + child.height\n\n    // Check primary axis overlap (optimization: skip objects that don't overlap)\n    if (end <= vpStart) continue\n    if (start >= vpEnd) break\n\n    // Check cross-axis overlap\n    if (isRow) {\n      const childBottom = child.y + child.height\n      if (childBottom < viewportTop) continue\n      const childTop = child.y\n      if (childTop > viewportBottom) continue\n    } else {\n      const childRight = child.x + child.width\n      if (childRight < viewportLeft) continue\n      const childLeft = child.x\n      if (childLeft > viewportRight) continue\n    }\n\n    visibleChildren.push(child)\n  }\n\n  // Sort by zIndex\n  if (visibleChildren.length > 1) {\n    visibleChildren.sort((a, b) => (a.zIndex > b.zIndex ? 1 : a.zIndex < b.zIndex ? -1 : 0))\n  }\n\n  return visibleChildren\n}\n"
  },
  {
    "path": "packages/core/src/lib/output.capture.ts",
    "content": "import { Writable } from \"stream\"\nimport { EventEmitter } from \"events\"\n\nexport type CapturedOutput = {\n  stream: \"stdout\" | \"stderr\"\n  output: string\n}\n\nexport class Capture extends EventEmitter {\n  // TODO: Cache could rather be a buffer to avoid join()?\n  private output: CapturedOutput[] = []\n\n  constructor() {\n    super()\n  }\n\n  get size(): number {\n    return this.output.length\n  }\n\n  write(stream: \"stdout\" | \"stderr\", data: string): void {\n    this.output.push({ stream, output: data })\n    this.emit(\"write\", stream, data)\n  }\n\n  claimOutput() {\n    const output = this.output.map((o) => o.output).join(\"\")\n    this.clear()\n    return output\n  }\n\n  private clear(): void {\n    this.output = []\n  }\n}\n\nexport class CapturedWritableStream extends Writable {\n  public isTTY: boolean = true\n  public columns: number = process.stdout.columns || 80\n  public rows: number = process.stdout.rows || 24\n\n  constructor(\n    private stream: \"stdout\" | \"stderr\",\n    private capture: Capture,\n  ) {\n    super()\n  }\n\n  _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error | null) => void): void {\n    const data = chunk.toString()\n    this.capture.write(this.stream, data)\n    callback()\n  }\n\n  getColorDepth(): number {\n    return process.stdout.getColorDepth?.() || 8\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/parse.keypress-kitty.protocol.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\"\nimport { parseKeypress, type ParseKeypressOptions, type ParsedKey } from \"./parse.keypress.js\"\n\n// This mirrors the upstream protocol tables and expands them into one test\n// per case so missing mappings fail with a name.\n//\n// https://raw.githubusercontent.com/kovidgoyal/kitty/refs/heads/master/docs/keyboard-protocol.rst\n\nconst options: ParseKeypressOptions = { useKittyKeyboard: true }\n\ntype ExpectedKey = Partial<ParsedKey> & Pick<ParsedKey, \"name\">\n\ninterface KeyCase {\n  title: string\n  sequence: string\n  expected: ExpectedKey\n}\n\nfunction parse(sequence: string): ParsedKey {\n  const result = parseKeypress(sequence, options)\n  expect(result).not.toBeNull()\n  return result!\n}\n\nfunction expectKey(sequence: string, expected: ExpectedKey): void {\n  expect(parse(sequence)).toMatchObject(expected)\n}\n\nfunction defineKeyCases(cases: readonly KeyCase[]): void {\n  for (const { title, sequence, expected } of cases) {\n    test(title, () => {\n      expectKey(sequence, expected)\n    })\n  }\n}\n\nfunction rangeCases(start: number, names: readonly string[]): Array<[number, string]> {\n  return names.map((name, index) => [start + index, name] as [number, string])\n}\n\nfunction functionalCase(code: number, name: string): KeyCase {\n  return {\n    title: `CSI ${code}u -> ${name}`,\n    sequence: `\\x1b[${code}u`,\n    expected: {\n      name,\n      code: `[${code}u`,\n      eventType: \"press\",\n      source: \"kitty\",\n    },\n  }\n}\n\nfunction legacyAliasCase(sequence: string, name: string): KeyCase {\n  return {\n    title: `legacy ${JSON.stringify(sequence)} -> ${name}`,\n    sequence,\n    expected: {\n      name,\n      eventType: \"press\",\n    },\n  }\n}\n\nfunction enhancedAliasCase(sequence: string, name: string): KeyCase {\n  return {\n    title: `enhanced ${JSON.stringify(sequence)} -> ${name}`,\n    sequence,\n    expected: {\n      name,\n      eventType: \"press\",\n      source: \"kitty\",\n    },\n  }\n}\n\nfunction canonicalAliasCase(sequence: string, expected: ExpectedKey): KeyCase {\n  return {\n    title: `canonical ${JSON.stringify(sequence)} -> ${expected.name}`,\n    sequence,\n    expected: {\n      eventType: \"press\",\n      ...expected,\n    },\n  }\n}\n\nfunction modifierCase(modifier: number, expected: ExpectedKey): KeyCase {\n  return {\n    title: `modifier ${modifier} -> ${expected.name}`,\n    sequence: `\\x1b[97;${modifier}u`,\n    expected: {\n      source: \"kitty\",\n      ...expected,\n    },\n  }\n}\n\nconst numberedFunctionalEntries: Array<[number, string]> = [\n  [27, \"escape\"],\n  [13, \"return\"],\n  [9, \"tab\"],\n  [127, \"backspace\"],\n  ...rangeCases(57348, [\n    \"insert\",\n    \"delete\",\n    \"left\",\n    \"right\",\n    \"up\",\n    \"down\",\n    \"pageup\",\n    \"pagedown\",\n    \"home\",\n    \"end\",\n    \"capslock\",\n    \"scrolllock\",\n    \"numlock\",\n    \"printscreen\",\n    \"pause\",\n    \"menu\",\n  ]),\n  ...Array.from({ length: 35 }, (_, index) => [57364 + index, `f${index + 1}`] as [number, string]),\n  ...rangeCases(57399, [\n    \"kp0\",\n    \"kp1\",\n    \"kp2\",\n    \"kp3\",\n    \"kp4\",\n    \"kp5\",\n    \"kp6\",\n    \"kp7\",\n    \"kp8\",\n    \"kp9\",\n    \"kpdecimal\",\n    \"kpdivide\",\n    \"kpmultiply\",\n    \"kpminus\",\n    \"kpplus\",\n    \"kpenter\",\n    \"kpequal\",\n    \"kpseparator\",\n    \"kpleft\",\n    \"kpright\",\n    \"kpup\",\n    \"kpdown\",\n    \"kppageup\",\n    \"kppagedown\",\n    \"kphome\",\n    \"kpend\",\n    \"kpinsert\",\n    \"kpdelete\",\n    \"clear\",\n  ]),\n  ...rangeCases(57428, [\n    \"mediaplay\",\n    \"mediapause\",\n    \"mediaplaypause\",\n    \"mediareverse\",\n    \"mediastop\",\n    \"mediafastforward\",\n    \"mediarewind\",\n    \"medianext\",\n    \"mediaprev\",\n    \"mediarecord\",\n  ]),\n  ...rangeCases(57438, [\"volumedown\", \"volumeup\", \"mute\"]),\n  ...rangeCases(57441, [\n    \"leftshift\",\n    \"leftctrl\",\n    \"leftalt\",\n    \"leftsuper\",\n    \"lefthyper\",\n    \"leftmeta\",\n    \"rightshift\",\n    \"rightctrl\",\n    \"rightalt\",\n    \"rightsuper\",\n    \"righthyper\",\n    \"rightmeta\",\n  ]),\n  ...rangeCases(57453, [\"iso_level3_shift\", \"iso_level5_shift\"]),\n]\n\nconst numberedFunctionalCases: KeyCase[] = numberedFunctionalEntries.map(([code, name]) => functionalCase(code, name))\n\nconst legacyAliasCases = [\n  [\"\\x1b[A\", \"up\"],\n  [\"\\x1b[B\", \"down\"],\n  [\"\\x1b[C\", \"right\"],\n  [\"\\x1b[D\", \"left\"],\n  [\"\\x1b[E\", \"clear\"],\n  [\"\\x1b[F\", \"end\"],\n  [\"\\x1b[H\", \"home\"],\n  [\"\\x1bOP\", \"f1\"],\n  [\"\\x1bOQ\", \"f2\"],\n  [\"\\x1bOR\", \"f3\"],\n  [\"\\x1bOS\", \"f4\"],\n  [\"\\x1b[2~\", \"insert\"],\n  [\"\\x1b[3~\", \"delete\"],\n  [\"\\x1b[5~\", \"pageup\"],\n  [\"\\x1b[6~\", \"pagedown\"],\n  [\"\\x1b[7~\", \"home\"],\n  [\"\\x1b[8~\", \"end\"],\n  [\"\\x1b[11~\", \"f1\"],\n  [\"\\x1b[12~\", \"f2\"],\n  [\"\\x1b[13~\", \"f3\"],\n  [\"\\x1b[14~\", \"f4\"],\n  [\"\\x1b[15~\", \"f5\"],\n  [\"\\x1b[17~\", \"f6\"],\n  [\"\\x1b[18~\", \"f7\"],\n  [\"\\x1b[19~\", \"f8\"],\n  [\"\\x1b[20~\", \"f9\"],\n  [\"\\x1b[21~\", \"f10\"],\n  [\"\\x1b[23~\", \"f11\"],\n  [\"\\x1b[24~\", \"f12\"],\n  [\"\\x1b[29~\", \"menu\"],\n  [\"\\x1b[57427~\", \"clear\"],\n] as const satisfies ReadonlyArray<readonly [string, string]>\n\nconst enhancedLetterAliasCases = [\n  [\"\\x1b[1;1:1A\", \"up\"],\n  [\"\\x1b[1;1:1B\", \"down\"],\n  [\"\\x1b[1;1:1C\", \"right\"],\n  [\"\\x1b[1;1:1D\", \"left\"],\n  [\"\\x1b[1;1:1E\", \"clear\"],\n  [\"\\x1b[1;1:1F\", \"end\"],\n  [\"\\x1b[1;1:1H\", \"home\"],\n  [\"\\x1b[1;1:1P\", \"f1\"],\n  [\"\\x1b[1;1:1Q\", \"f2\"],\n  [\"\\x1b[1;1:1S\", \"f4\"],\n] as const satisfies ReadonlyArray<readonly [string, string]>\n\nconst canonicalLetterAliasCases: KeyCase[] = [\n  canonicalAliasCase(\"\\x1b[P\", { name: \"f1\" }),\n  canonicalAliasCase(\"\\x1b[Q\", { name: \"f2\" }),\n  canonicalAliasCase(\"\\x1b[S\", { name: \"f4\" }),\n  canonicalAliasCase(\"\\x1b[1;2P\", { name: \"f1\", shift: true }),\n  canonicalAliasCase(\"\\x1b[1;5Q\", { name: \"f2\", ctrl: true }),\n  canonicalAliasCase(\"\\x1b[1;3S\", { name: \"f4\", meta: true, option: true }),\n]\n\nconst enhancedTildeAliasCases = [\n  [\"\\x1b[2;1:1~\", \"insert\"],\n  [\"\\x1b[3;1:1~\", \"delete\"],\n  [\"\\x1b[5;1:1~\", \"pageup\"],\n  [\"\\x1b[6;1:1~\", \"pagedown\"],\n  [\"\\x1b[7;1:1~\", \"home\"],\n  [\"\\x1b[8;1:1~\", \"end\"],\n  [\"\\x1b[11;1:1~\", \"f1\"],\n  [\"\\x1b[12;1:1~\", \"f2\"],\n  [\"\\x1b[13;1:1~\", \"f3\"],\n  [\"\\x1b[14;1:1~\", \"f4\"],\n  [\"\\x1b[15;1:1~\", \"f5\"],\n  [\"\\x1b[17;1:1~\", \"f6\"],\n  [\"\\x1b[18;1:1~\", \"f7\"],\n  [\"\\x1b[19;1:1~\", \"f8\"],\n  [\"\\x1b[20;1:1~\", \"f9\"],\n  [\"\\x1b[21;1:1~\", \"f10\"],\n  [\"\\x1b[23;1:1~\", \"f11\"],\n  [\"\\x1b[24;1:1~\", \"f12\"],\n  [\"\\x1b[29;1:1~\", \"menu\"],\n  [\"\\x1b[57427;1:1~\", \"clear\"],\n] as const satisfies ReadonlyArray<readonly [string, string]>\n\nconst modifierCases: KeyCase[] = [\n  modifierCase(2, { name: \"a\", shift: true }),\n  modifierCase(3, { name: \"a\", meta: true, option: true }),\n  modifierCase(5, { name: \"a\", ctrl: true }),\n  modifierCase(9, { name: \"a\", super: true }),\n  modifierCase(17, { name: \"a\", hyper: true }),\n  modifierCase(33, { name: \"a\", meta: true, option: false }),\n  modifierCase(65, { name: \"a\", capsLock: true }),\n  modifierCase(129, { name: \"a\", numLock: true }),\n  modifierCase(256, {\n    name: \"a\",\n    shift: true,\n    ctrl: true,\n    meta: true,\n    option: true,\n    super: true,\n    hyper: true,\n    capsLock: true,\n    numLock: true,\n  }),\n]\n\nconst eventAndPayloadCases = [\n  [\"\\x1b[97;1:1u\", { name: \"a\", eventType: \"press\" }],\n  [\"\\x1b[97;1:2u\", { name: \"a\", eventType: \"press\", repeated: true }],\n  [\"\\x1b[97;1:3u\", { name: \"a\", eventType: \"release\" }],\n  [\"\\x1b[97:65u\", { name: \"a\", sequence: \"a\" }],\n  [\"\\x1b[97:65;2u\", { name: \"a\", sequence: \"A\", shift: true }],\n  [\"\\x1b[97::113u\", { name: \"a\", sequence: \"a\", baseCode: 113 }],\n  [\"\\x1b[97;1;65u\", { name: \"a\", sequence: \"A\" }],\n  [\"\\x1b[0;;229u\", { name: \"å\", sequence: \"å\" }],\n  [\"\\x1b[0;;104:105u\", { name: \"hi\", sequence: \"hi\" }],\n] as const satisfies ReadonlyArray<readonly [string, ExpectedKey]>\n\ndescribe(\"Kitty protocol: functional key definitions\", () => {\n  defineKeyCases(numberedFunctionalCases)\n})\n\ndescribe(\"Kitty protocol: legacy aliases\", () => {\n  defineKeyCases(legacyAliasCases.map(([sequence, name]) => legacyAliasCase(sequence, name)))\n})\n\ndescribe(\"Kitty protocol: enhanced letter aliases\", () => {\n  defineKeyCases(enhancedLetterAliasCases.map(([sequence, name]) => enhancedAliasCase(sequence, name)))\n\n  test(\"CSI 1;1:1R is not an enhanced F3 alias in the current spec\", () => {\n    expect(parseKeypress(\"\\x1b[1;1:1R\", options)?.name).not.toBe(\"f3\")\n  })\n})\n\ndescribe(\"Kitty protocol: canonical CSI letter forms\", () => {\n  defineKeyCases(canonicalLetterAliasCases)\n\n  test(\"CSI R is not a canonical F3 alias in the current spec\", () => {\n    expect(parseKeypress(\"\\x1b[R\", options)?.name).not.toBe(\"f3\")\n    expect(parseKeypress(\"\\x1b[1;2R\", options)?.name).not.toBe(\"f3\")\n  })\n})\n\ndescribe(\"Kitty protocol: enhanced tilde aliases\", () => {\n  defineKeyCases(enhancedTildeAliasCases.map(([sequence, name]) => enhancedAliasCase(sequence, name)))\n})\n\ndescribe(\"Kitty protocol: modifier bitfield\", () => {\n  defineKeyCases(modifierCases)\n})\n\ndescribe(\"Kitty protocol: event, alternate-key, and text fields\", () => {\n  defineKeyCases(\n    eventAndPayloadCases.map(([sequence, expected]) => ({\n      title: `payload ${JSON.stringify(sequence)}`,\n      sequence,\n      expected: { source: \"kitty\", ...expected },\n    })),\n  )\n})\n"
  },
  {
    "path": "packages/core/src/lib/parse.keypress-kitty.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { parseKeypress, type ParseKeypressOptions } from \"./parse.keypress.js\"\n\ntest(\"parseKeypress - Kitty keyboard protocol disabled by default\", () => {\n  // Kitty sequences should fall back to regular parsing when disabled\n  const result = parseKeypress(\"\\x1b[97u\")!\n  expect(result.name).toBe(\"\")\n  expect(result.code).toBeUndefined()\n})\n\ntest(\"parseKeypress - Kitty keyboard basic key\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.sequence).toBe(\"a\")\n  expect(result.ctrl).toBe(false)\n  expect(result.meta).toBe(false)\n  expect(result.shift).toBe(false)\n  expect(result.option).toBe(false)\n})\n\ntest(\"parseKeypress - Kitty keyboard shift+a\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97:65;2u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.sequence).toBe(\"A\")\n  expect(result.shift).toBe(true)\n  expect(result.ctrl).toBe(false)\n  expect(result.meta).toBe(false)\n})\n\ntest(\"parseKeypress - Kitty keyboard ctrl+a\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;5u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.ctrl).toBe(true)\n  expect(result.shift).toBe(false)\n  expect(result.meta).toBe(false)\n})\n\ntest(\"parseKeypress - Kitty keyboard alt+a\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;3u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.meta).toBe(true)\n  expect(result.option).toBe(true)\n  expect(result.ctrl).toBe(false)\n  expect(result.shift).toBe(false)\n})\n\ntest(\"parseKeypress - Kitty keyboard function key\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[57364u\", options)!\n  expect(result.name).toBe(\"f1\")\n  expect(result.code).toBe(\"[57364u\")\n})\n\ntest(\"parseKeypress - Kitty keyboard arrow key\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[57352u\", options)!\n  expect(result.name).toBe(\"up\")\n  expect(result.code).toBe(\"[57352u\")\n})\n\ntest(\"parseKeypress - Kitty keyboard shift+space\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[32;2u\", options)!\n  expect(result.name).toBe(\" \")\n  expect(result.sequence).toBe(\" \")\n  expect(result.shift).toBe(true)\n})\n\ntest(\"parseKeypress - Kitty keyboard event types\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  // Press event (explicit)\n  const pressExplicit = parseKeypress(\"\\x1b[97;1:1u\", options)!\n  expect(pressExplicit.name).toBe(\"a\")\n  expect(pressExplicit.eventType).toBe(\"press\")\n\n  // Press event (default when no event type specified)\n  const pressDefault = parseKeypress(\"\\x1b[97u\", options)!\n  expect(pressDefault.name).toBe(\"a\")\n  expect(pressDefault.eventType).toBe(\"press\")\n\n  // Press event (modifier without event type)\n  const pressWithModifier = parseKeypress(\"\\x1b[97;5u\", options)! // Ctrl+a\n  expect(pressWithModifier.name).toBe(\"a\")\n  expect(pressWithModifier.ctrl).toBe(true)\n  expect(pressWithModifier.eventType).toBe(\"press\")\n\n  // Repeat event (emitted as press with repeated=true)\n  const repeat = parseKeypress(\"\\x1b[97;1:2u\", options)!\n  expect(repeat.name).toBe(\"a\")\n  expect(repeat.eventType).toBe(\"press\")\n  expect(repeat.repeated).toBe(true)\n\n  // Release event\n  const release = parseKeypress(\"\\x1b[97;1:3u\", options)!\n  expect(release.name).toBe(\"a\")\n  expect(release.eventType).toBe(\"release\")\n\n  // Repeat event with modifier (emitted as press with repeated=true)\n  const repeatWithCtrl = parseKeypress(\"\\x1b[97;5:2u\", options)!\n  expect(repeatWithCtrl.name).toBe(\"a\")\n  expect(repeatWithCtrl.ctrl).toBe(true)\n  expect(repeatWithCtrl.eventType).toBe(\"press\")\n  expect(repeatWithCtrl.repeated).toBe(true)\n\n  // Release event with modifier\n  const releaseWithShift = parseKeypress(\"\\x1b[97;2:3u\", options)!\n  expect(releaseWithShift.name).toBe(\"a\")\n  expect(releaseWithShift.shift).toBe(true)\n  expect(releaseWithShift.eventType).toBe(\"release\")\n})\n\ntest(\"parseKeypress - Kitty keyboard with text\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;1;97u\", options)!\n  expect(result.name).toBe(\"a\")\n})\n\ntest(\"parseKeypress - Kitty keyboard ctrl+shift+a\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;6u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.ctrl).toBe(true)\n  expect(result.shift).toBe(true)\n  expect(result.meta).toBe(false)\n})\n\ntest(\"parseKeypress - Kitty keyboard alt+shift+a\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;4u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.meta).toBe(true)\n  expect(result.option).toBe(true)\n  expect(result.shift).toBe(true)\n  expect(result.ctrl).toBe(false)\n})\n\ntest(\"parseKeypress - Kitty keyboard super+a\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;9u\", options)! // modifier 9 - 1 = 8 = super\n  expect(result.name).toBe(\"a\")\n  expect(result.super).toBe(true)\n})\n\ntest(\"parseKeypress - Kitty keyboard hyper+a\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;17u\", options)! // modifier 17 - 1 = 16 = hyper\n  expect(result.name).toBe(\"a\")\n  expect(result.hyper).toBe(true)\n})\n\ntest(\"parseKeypress - Kitty keyboard with shifted codepoint\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97:65u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.sequence).toBe(\"a\") // No shift pressed, so base character\n  expect(result.shift).toBe(false)\n})\n\ntest(\"parseKeypress - Kitty keyboard with base layout codepoint\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97:65:97u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.sequence).toBe(\"a\") // No shift modifier, so base character\n  expect(result.shift).toBe(false)\n  expect(result.baseCode).toBe(97) // Base layout codepoint is 'a'\n})\n\ntest(\"parseKeypress - Kitty keyboard different layout (QWERTY A key on AZERTY)\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  // On AZERTY, Q key produces 'a', but base layout says it's Q position\n  const result = parseKeypress(\"\\x1b[97:65:113u\", options)! // 113 = 'q'\n  expect(result.name).toBe(\"a\") // Actual character produced\n  expect(result.sequence).toBe(\"a\")\n  expect(result.baseCode).toBe(113) // Physical key position is Q\n})\n\ntest(\"parseKeypress - Kitty keyboard caps lock\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;65u\", options)! // modifier 65 - 1 = 64 = caps lock\n  expect(result.name).toBe(\"a\")\n  expect(result.capsLock).toBe(true)\n})\n\ntest(\"parseKeypress - Kitty keyboard lock keys\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  const cases = [\n    [\"\\x1b[57358u\", \"capslock\", \"[57358u\"],\n    [\"\\x1b[57359u\", \"scrolllock\", \"[57359u\"],\n    [\"\\x1b[57360u\", \"numlock\", \"[57360u\"],\n  ] as const\n\n  for (const [sequence, name, code] of cases) {\n    const result = parseKeypress(sequence, options)!\n    expect(result.name).toBe(name)\n    expect(result.code).toBe(code)\n    expect(result.source).toBe(\"kitty\")\n  }\n})\n\ntest(\"parseKeypress - Kitty keyboard num lock\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[97;129u\", options)! // modifier 129 - 1 = 128 = num lock\n  expect(result.name).toBe(\"a\")\n  expect(result.numLock).toBe(true)\n})\n\ntest(\"parseKeypress - Kitty keyboard unicode character\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[233u\", options)! // é\n  expect(result.name).toBe(\"é\")\n  expect(result.sequence).toBe(\"é\")\n})\n\ntest(\"parseKeypress - Kitty keyboard emoji\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[128512u\", options)! // 😀\n  expect(result.name).toBe(\"😀\")\n  expect(result.sequence).toBe(\"😀\")\n})\n\ntest(\"parseKeypress - Kitty keyboard invalid codepoint\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const result = parseKeypress(\"\\x1b[1114112u\", options)! // Invalid codepoint > 0x10FFFF\n  // Should fall back to regular parsing when Kitty fails\n  expect(result.name).toBe(\"\")\n  expect(result.ctrl).toBe(true)\n  expect(result.meta).toBe(true)\n  expect(result.shift).toBe(true)\n  expect(result.option).toBe(true)\n})\n\ntest(\"parseKeypress - Kitty keyboard keypad keys\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  const kp0 = parseKeypress(\"\\x1b[57399u\", options)\n  expect(kp0?.name).toBe(\"kp0\")\n\n  const kpEnter = parseKeypress(\"\\x1b[57414u\", options)\n  expect(kpEnter?.name).toBe(\"kpenter\")\n})\n\ntest(\"parseKeypress - Kitty keyboard media keys\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  const play = parseKeypress(\"\\x1b[57428u\", options)\n  expect(play?.name).toBe(\"mediaplay\")\n\n  const volumeUp = parseKeypress(\"\\x1b[57439u\", options)\n  expect(volumeUp?.name).toBe(\"volumeup\")\n})\n\ntest(\"parseKeypress - Kitty keyboard modifier keys\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  const leftShift = parseKeypress(\"\\x1b[57441u\", options)\n  expect(leftShift?.name).toBe(\"leftshift\")\n  expect(leftShift?.eventType).toBe(\"press\")\n\n  const rightCtrl = parseKeypress(\"\\x1b[57448u\", options)\n  expect(rightCtrl?.name).toBe(\"rightctrl\")\n  expect(rightCtrl?.eventType).toBe(\"press\")\n})\n\ntest(\"parseKeypress - Kitty keyboard function keys with event types\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  // F1 press\n  const f1Press = parseKeypress(\"\\x1b[57364u\", options)!\n  expect(f1Press.name).toBe(\"f1\")\n  expect(f1Press.eventType).toBe(\"press\")\n\n  // F1 repeat (emitted as press with repeated=true)\n  const f1Repeat = parseKeypress(\"\\x1b[57364;1:2u\", options)!\n  expect(f1Repeat.name).toBe(\"f1\")\n  expect(f1Repeat.eventType).toBe(\"press\")\n  expect(f1Repeat.repeated).toBe(true)\n\n  // F1 release\n  const f1Release = parseKeypress(\"\\x1b[57364;1:3u\", options)!\n  expect(f1Release.name).toBe(\"f1\")\n  expect(f1Release.eventType).toBe(\"release\")\n})\n\ntest(\"parseKeypress - Kitty keyboard arrow keys with event types\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  // Up arrow press\n  const upPress = parseKeypress(\"\\x1b[57352u\", options)!\n  expect(upPress.name).toBe(\"up\")\n  expect(upPress.eventType).toBe(\"press\")\n\n  // Up arrow repeat with Ctrl (emitted as press with repeated=true)\n  const upRepeatCtrl = parseKeypress(\"\\x1b[57352;5:2u\", options)!\n  expect(upRepeatCtrl.name).toBe(\"up\")\n  expect(upRepeatCtrl.ctrl).toBe(true)\n  expect(upRepeatCtrl.eventType).toBe(\"press\")\n  expect(upRepeatCtrl.repeated).toBe(true)\n\n  // Down arrow release\n  const downRelease = parseKeypress(\"\\x1b[57353;1:3u\", options)!\n  expect(downRelease.name).toBe(\"down\")\n  expect(downRelease.eventType).toBe(\"release\")\n})\n\ntest(\"parseKeypress - Kitty functional keys with event types\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  // Legacy format: CSI 1;modifiers:event_type LETTER\n  // Up arrow press\n  const upPress = parseKeypress(\"\\x1b[1;1:1A\", options)!\n  expect(upPress.name).toBe(\"up\")\n  expect(upPress.eventType).toBe(\"press\")\n  expect(upPress.source).toBe(\"kitty\")\n\n  // Up arrow release\n  const upRelease = parseKeypress(\"\\x1b[1;1:3A\", options)!\n  expect(upRelease.name).toBe(\"up\")\n  expect(upRelease.eventType).toBe(\"release\")\n  expect(upRelease.source).toBe(\"kitty\")\n\n  // Down arrow with repeat (emitted as press with repeated=true)\n  const downRepeat = parseKeypress(\"\\x1b[1;1:2B\", options)!\n  expect(downRepeat.name).toBe(\"down\")\n  expect(downRepeat.eventType).toBe(\"press\")\n  expect(downRepeat.repeated).toBe(true)\n\n  // Left arrow press\n  const leftPress = parseKeypress(\"\\x1b[1;1:1D\", options)!\n  expect(leftPress.name).toBe(\"left\")\n  expect(leftPress.eventType).toBe(\"press\")\n\n  // Right arrow release\n  const rightRelease = parseKeypress(\"\\x1b[1;1:3C\", options)!\n  expect(rightRelease.name).toBe(\"right\")\n  expect(rightRelease.eventType).toBe(\"release\")\n\n  // Shift+up press\n  const shiftUpPress = parseKeypress(\"\\x1b[1;2:1A\", options)!\n  expect(shiftUpPress.name).toBe(\"up\")\n  expect(shiftUpPress.shift).toBe(true)\n  expect(shiftUpPress.eventType).toBe(\"press\")\n\n  // Ctrl+down release\n  const ctrlDownRelease = parseKeypress(\"\\x1b[1;5:3B\", options)!\n  expect(ctrlDownRelease.name).toBe(\"down\")\n  expect(ctrlDownRelease.ctrl).toBe(true)\n  expect(ctrlDownRelease.eventType).toBe(\"release\")\n})\n\ntest(\"parseKeypress - Kitty tilde keys with event types\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  // Page Up press\n  const pageUpPress = parseKeypress(\"\\x1b[5;1:1~\", options)!\n  expect(pageUpPress.name).toBe(\"pageup\")\n  expect(pageUpPress.eventType).toBe(\"press\")\n  expect(pageUpPress.source).toBe(\"kitty\")\n\n  // Page Up repeat\n  const pageUpRepeat = parseKeypress(\"\\x1b[5;1:2~\", options)!\n  expect(pageUpRepeat.name).toBe(\"pageup\")\n  expect(pageUpRepeat.eventType).toBe(\"press\")\n  expect(pageUpRepeat.repeated).toBe(true)\n\n  // Page Up release\n  const pageUpRelease = parseKeypress(\"\\x1b[5;1:3~\", options)!\n  expect(pageUpRelease.name).toBe(\"pageup\")\n  expect(pageUpRelease.eventType).toBe(\"release\")\n\n  // Page Down\n  const pageDownRepeat = parseKeypress(\"\\x1b[6;1:2~\", options)!\n  expect(pageDownRepeat.name).toBe(\"pagedown\")\n  expect(pageDownRepeat.repeated).toBe(true)\n\n  // Insert with shift\n  const shiftInsert = parseKeypress(\"\\x1b[2;2:1~\", options)!\n  expect(shiftInsert.name).toBe(\"insert\")\n  expect(shiftInsert.shift).toBe(true)\n  expect(shiftInsert.eventType).toBe(\"press\")\n\n  // Delete with ctrl\n  const ctrlDelete = parseKeypress(\"\\x1b[3;5:1~\", options)!\n  expect(ctrlDelete.name).toBe(\"delete\")\n  expect(ctrlDelete.ctrl).toBe(true)\n\n  // Home/End\n  const homePress = parseKeypress(\"\\x1b[1;1:1~\", options)!\n  expect(homePress.name).toBe(\"home\")\n\n  const endRelease = parseKeypress(\"\\x1b[4;1:3~\", options)!\n  expect(endRelease.name).toBe(\"end\")\n  expect(endRelease.eventType).toBe(\"release\")\n\n  // F5-F12\n  const f5Press = parseKeypress(\"\\x1b[15;1:1~\", options)!\n  expect(f5Press.name).toBe(\"f5\")\n\n  const f12Repeat = parseKeypress(\"\\x1b[24;1:2~\", options)!\n  expect(f12Repeat.name).toBe(\"f12\")\n  expect(f12Repeat.repeated).toBe(true)\n})\n\ntest(\"parseKeypress - Kitty keyboard invalid event types\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  // Unknown event type should default to press\n  const unknownEvent = parseKeypress(\"\\x1b[97;1:9u\", options)!\n  expect(unknownEvent.name).toBe(\"a\")\n  expect(unknownEvent.eventType).toBe(\"press\")\n\n  // Empty event type should default to press\n  const emptyEvent = parseKeypress(\"\\x1b[97;1:u\", options)!\n  expect(emptyEvent.name).toBe(\"a\")\n  expect(emptyEvent.eventType).toBe(\"press\")\n})\n\ntest(\"parseKeypress - Kitty repeat/release matrix\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n  const cases = [\n    [\"\\x1b[97;1:2u\", \"a\", \"press\", true, false, false, false],\n    [\"\\x1b[97;5:3u\", \"a\", \"release\", false, true, false, false],\n    [\"\\x1b[1;2:2A\", \"up\", \"press\", true, false, true, false],\n    [\"\\x1b[1;5:3B\", \"down\", \"release\", false, true, false, false],\n    [\"\\x1b[5;1:2~\", \"pageup\", \"press\", true, false, false, false],\n    [\"\\x1b[3;5:3~\", \"delete\", \"release\", false, true, false, false],\n  ] as const\n\n  for (const [sequence, name, eventType, repeated, ctrl, shift, meta] of cases) {\n    const result = parseKeypress(sequence, options)!\n    expect(result.name).toBe(name)\n    expect(result.eventType).toBe(eventType)\n    expect(result.repeated === true).toBe(repeated)\n    expect(result.ctrl).toBe(ctrl)\n    expect(result.shift).toBe(shift)\n    expect(result.meta).toBe(meta)\n    expect(result.source).toBe(\"kitty\")\n  }\n})\n\n// Test progressive enhancement (non-CSI u sequences)\n// Note: We don't implement this yet, but these should fall back to regular parsing\ntest(\"parseKeypress - Kitty progressive enhancement fallback\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  // These would normally be handled by progressive enhancement\n  // but since we don't implement it, they should fall back\n  const result = parseKeypress(\"\\x1b[1;2A\", options)! // CSI 1;2A (shift+up with modifiers)\n  expect(result.name).toBe(\"up\")\n  expect(result.shift).toBe(true)\n})\n\ntest(\"parseKeypress - Kitty sequences are NOT filtered by terminal response filters\", () => {\n  // This test ensures that ALL Kitty keyboard protocol sequences bypass\n  // the terminal response filters and reach the Kitty parser correctly.\n  // Kitty sequences all end with 'u' while filtered sequences end with other characters.\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  // Basic letters (all should have source: \"kitty\")\n  const letters = [\"a\", \"z\", \"A\", \"Z\", \"0\", \"9\"]\n  for (const letter of letters) {\n    const code = letter.charCodeAt(0)\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n  }\n\n  // All standard keys\n  const standardKeys = [\n    [27, \"escape\"],\n    [9, \"tab\"],\n    [13, \"return\"],\n    [127, \"backspace\"],\n  ] as const\n  for (const [code, expectedName] of standardKeys) {\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(expectedName)\n  }\n\n  // All arrow keys\n  const arrowKeys = [\n    [57350, \"left\"],\n    [57351, \"right\"],\n    [57352, \"up\"],\n    [57353, \"down\"],\n  ] as const\n  for (const [code, expectedName] of arrowKeys) {\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(expectedName)\n  }\n\n  // All navigation keys\n  const navKeys = [\n    [57348, \"insert\"],\n    [57349, \"delete\"],\n    [57354, \"pageup\"],\n    [57355, \"pagedown\"],\n    [57356, \"home\"],\n    [57357, \"end\"],\n  ] as const\n  for (const [code, expectedName] of navKeys) {\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(expectedName)\n  }\n\n  // All function keys (F1-F35)\n  for (let i = 1; i <= 35; i++) {\n    const code = 57363 + i\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(`f${i}`)\n  }\n\n  // All keypad keys\n  const keypadKeys = [\n    [57399, \"kp0\"],\n    [57400, \"kp1\"],\n    [57408, \"kp9\"],\n    [57409, \"kpdecimal\"],\n    [57410, \"kpdivide\"],\n    [57411, \"kpmultiply\"],\n    [57412, \"kpminus\"],\n    [57413, \"kpplus\"],\n    [57414, \"kpenter\"],\n    [57415, \"kpequal\"],\n  ] as const\n  for (const [code, expectedName] of keypadKeys) {\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(expectedName)\n  }\n\n  // All media keys\n  const mediaKeys = [\n    [57428, \"mediaplay\"],\n    [57429, \"mediapause\"],\n    [57430, \"mediaplaypause\"],\n    [57431, \"mediareverse\"],\n    [57432, \"mediastop\"],\n    [57433, \"mediafastforward\"],\n    [57434, \"mediarewind\"],\n    [57435, \"medianext\"],\n    [57436, \"mediaprev\"],\n    [57437, \"mediarecord\"],\n  ] as const\n  for (const [code, expectedName] of mediaKeys) {\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(expectedName)\n  }\n\n  // Volume keys\n  const volumeKeys = [\n    [57438, \"volumedown\"],\n    [57439, \"volumeup\"],\n    [57440, \"mute\"],\n  ] as const\n  for (const [code, expectedName] of volumeKeys) {\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(expectedName)\n  }\n\n  // All modifier keys\n  const modifierKeys = [\n    [57441, \"leftshift\"],\n    [57442, \"leftctrl\"],\n    [57443, \"leftalt\"],\n    [57444, \"leftsuper\"],\n    [57445, \"lefthyper\"],\n    [57446, \"leftmeta\"],\n    [57447, \"rightshift\"],\n    [57448, \"rightctrl\"],\n    [57449, \"rightalt\"],\n    [57450, \"rightsuper\"],\n    [57451, \"righthyper\"],\n    [57452, \"rightmeta\"],\n  ] as const\n  for (const [code, expectedName] of modifierKeys) {\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(expectedName)\n  }\n\n  // Special ISO keys\n  const isoKeys = [\n    [57453, \"iso_level3_shift\"],\n    [57454, \"iso_level5_shift\"],\n  ] as const\n  for (const [code, expectedName] of isoKeys) {\n    const result = parseKeypress(`\\x1b[${code}u`, options)\n    expect(result).not.toBeNull()\n    expect(result?.source).toBe(\"kitty\")\n    expect(result?.name).toBe(expectedName)\n  }\n\n  // Keys with modifiers\n  const withModifiers = parseKeypress(\"\\x1b[97;5u\", options) // Ctrl+a\n  expect(withModifiers).not.toBeNull()\n  expect(withModifiers?.source).toBe(\"kitty\")\n  expect(withModifiers?.ctrl).toBe(true)\n\n  // Keys with event types\n  const withEventType = parseKeypress(\"\\x1b[97;1:3u\", options) // a release\n  expect(withEventType).not.toBeNull()\n  expect(withEventType?.source).toBe(\"kitty\")\n  expect(withEventType?.eventType).toBe(\"release\")\n\n  // Keys with all fields (unicode:shifted:base; modifiers:event; text)\n  // repeat events are emitted as press with repeated=true\n  const complex = parseKeypress(\"\\x1b[97:65:113;5:2;97u\", options)\n  expect(complex).not.toBeNull()\n  expect(complex?.source).toBe(\"kitty\")\n  expect(complex?.ctrl).toBe(true)\n  expect(complex?.eventType).toBe(\"press\")\n  expect(complex?.repeated).toBe(true)\n\n  // Unicode characters\n  const unicode = parseKeypress(\"\\x1b[233u\", options) // é\n  expect(unicode).not.toBeNull()\n  expect(unicode?.source).toBe(\"kitty\")\n  expect(unicode?.name).toBe(\"é\")\n\n  // Emoji\n  const emoji = parseKeypress(\"\\x1b[128512u\", options) // 😀\n  expect(emoji).not.toBeNull()\n  expect(emoji?.source).toBe(\"kitty\")\n  expect(emoji?.name).toBe(\"😀\")\n})\n\ntest(\"parseKeypress - Kitty keyboard shift+letter without shifted codepoint\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  const result = parseKeypress(\"\\x1b[97;2u\", options)!\n  expect(result.name).toBe(\"a\")\n  expect(result.shift).toBe(true)\n  expect(result.sequence).toBe(\"A\")\n})\n\ntest(\"parseKeypress - Kitty keyboard shift+Cyrillic without shifted codepoint\", () => {\n  const options: ParseKeypressOptions = { useKittyKeyboard: true }\n\n  const result = parseKeypress(\"\\x1b[1072;2u\", options)!\n  expect(result.name).toBe(\"а\")\n  expect(result.shift).toBe(true)\n  expect(result.sequence).toBe(\"А\")\n})\n"
  },
  {
    "path": "packages/core/src/lib/parse.keypress-kitty.ts",
    "content": "// Kitty Keyboard Protocol parser\n// Based on https://sw.kovidgoyal.net/kitty/keyboard-protocol/\n\nimport type { ParsedKey } from \"./parse.keypress.js\"\n\nconst kittyKeyMap: Record<number, string> = {\n  // Standard keys\n  27: \"escape\",\n  9: \"tab\",\n  13: \"return\",\n  127: \"backspace\",\n\n  // Arrow keys\n  57344: \"escape\",\n  57345: \"return\",\n  57346: \"tab\",\n  57347: \"backspace\",\n  57348: \"insert\",\n  57349: \"delete\",\n  57350: \"left\",\n  57351: \"right\",\n  57352: \"up\",\n  57353: \"down\",\n  57354: \"pageup\",\n  57355: \"pagedown\",\n  57356: \"home\",\n  57357: \"end\",\n  57358: \"capslock\",\n  57359: \"scrolllock\",\n  57360: \"numlock\",\n  57361: \"printscreen\",\n  57362: \"pause\",\n  57363: \"menu\",\n\n  // Function keys\n  57364: \"f1\",\n  57365: \"f2\",\n  57366: \"f3\",\n  57367: \"f4\",\n  57368: \"f5\",\n  57369: \"f6\",\n  57370: \"f7\",\n  57371: \"f8\",\n  57372: \"f9\",\n  57373: \"f10\",\n  57374: \"f11\",\n  57375: \"f12\",\n  57376: \"f13\",\n  57377: \"f14\",\n  57378: \"f15\",\n  57379: \"f16\",\n  57380: \"f17\",\n  57381: \"f18\",\n  57382: \"f19\",\n  57383: \"f20\",\n  57384: \"f21\",\n  57385: \"f22\",\n  57386: \"f23\",\n  57387: \"f24\",\n  57388: \"f25\",\n  57389: \"f26\",\n  57390: \"f27\",\n  57391: \"f28\",\n  57392: \"f29\",\n  57393: \"f30\",\n  57394: \"f31\",\n  57395: \"f32\",\n  57396: \"f33\",\n  57397: \"f34\",\n  57398: \"f35\",\n\n  // Keypad\n  57399: \"kp0\",\n  57400: \"kp1\",\n  57401: \"kp2\",\n  57402: \"kp3\",\n  57403: \"kp4\",\n  57404: \"kp5\",\n  57405: \"kp6\",\n  57406: \"kp7\",\n  57407: \"kp8\",\n  57408: \"kp9\",\n  57409: \"kpdecimal\",\n  57410: \"kpdivide\",\n  57411: \"kpmultiply\",\n  57412: \"kpminus\",\n  57413: \"kpplus\",\n  57414: \"kpenter\",\n  57415: \"kpequal\",\n  57416: \"kpseparator\",\n  57417: \"kpleft\",\n  57418: \"kpright\",\n  57419: \"kpup\",\n  57420: \"kpdown\",\n  57421: \"kppageup\",\n  57422: \"kppagedown\",\n  57423: \"kphome\",\n  57424: \"kpend\",\n  57425: \"kpinsert\",\n  57426: \"kpdelete\",\n  57427: \"clear\",\n\n  // Media keys\n  57428: \"mediaplay\",\n  57429: \"mediapause\",\n  57430: \"mediaplaypause\",\n  57431: \"mediareverse\",\n  57432: \"mediastop\",\n  57433: \"mediafastforward\",\n  57434: \"mediarewind\",\n  57435: \"medianext\",\n  57436: \"mediaprev\",\n  57437: \"mediarecord\",\n\n  // Volume keys\n  57438: \"volumedown\",\n  57439: \"volumeup\",\n  57440: \"mute\",\n\n  // Modifiers\n  57441: \"leftshift\",\n  57442: \"leftctrl\",\n  57443: \"leftalt\",\n  57444: \"leftsuper\",\n  57445: \"lefthyper\",\n  57446: \"leftmeta\",\n  57447: \"rightshift\",\n  57448: \"rightctrl\",\n  57449: \"rightalt\",\n  57450: \"rightsuper\",\n  57451: \"righthyper\",\n  57452: \"rightmeta\",\n\n  // Special\n  57453: \"iso_level3_shift\",\n  57454: \"iso_level5_shift\",\n}\n\nfunction fromKittyMods(mod: number): {\n  shift: boolean\n  alt: boolean\n  ctrl: boolean\n  super: boolean\n  hyper: boolean\n  meta: boolean\n  capsLock: boolean\n  numLock: boolean\n} {\n  return {\n    shift: !!(mod & 1),\n    alt: !!(mod & 2),\n    ctrl: !!(mod & 4),\n    super: !!(mod & 8),\n    hyper: !!(mod & 16),\n    meta: !!(mod & 32),\n    capsLock: !!(mod & 64),\n    numLock: !!(mod & 128),\n  }\n}\n\n// Map functional key CSI codes to key names\nconst functionalKeyMap: Record<string, string> = {\n  A: \"up\",\n  B: \"down\",\n  C: \"right\",\n  D: \"left\",\n  H: \"home\",\n  F: \"end\",\n  E: \"clear\",\n  P: \"f1\",\n  Q: \"f2\",\n  S: \"f4\",\n}\n\n// Map tilde key numbers to key names (CSI number ~ format)\nconst tildeKeyMap: Record<string, string> = {\n  \"1\": \"home\",\n  \"2\": \"insert\",\n  \"3\": \"delete\",\n  \"4\": \"end\",\n  \"5\": \"pageup\",\n  \"6\": \"pagedown\",\n  \"7\": \"home\", // rxvt\n  \"8\": \"end\", // rxvt\n  \"11\": \"f1\",\n  \"12\": \"f2\",\n  \"13\": \"f3\",\n  \"14\": \"f4\",\n  \"15\": \"f5\",\n  \"17\": \"f6\",\n  \"18\": \"f7\",\n  \"19\": \"f8\",\n  \"20\": \"f9\",\n  \"21\": \"f10\",\n  \"23\": \"f11\",\n  \"24\": \"f12\",\n  \"29\": \"menu\",\n  \"57427\": \"clear\",\n}\n\n/**\n * Parse Kitty keyboard protocol special keys (functional and tilde) with event type\n * Formats:\n *   Functional: CSI 1;modifiers:event_type LETTER (e.g., \\x1b[1;1:1A = up arrow press)\n *   Tilde: CSI number;modifiers:event_type ~ (e.g., \\x1b[5;1:1~ = pageup press)\n */\nfunction parseKittySpecialKey(sequence: string): ParsedKey | null {\n  // Combined regex: matches both functional keys (letter) and tilde keys (~)\n  const specialKeyRe = /^\\x1b\\[(\\d+);(\\d+):(\\d+)([A-Z~])$/\n  const match = specialKeyRe.exec(sequence)\n\n  if (!match) return null\n\n  const keyNumOrOne = match[1]\n  const modifierStr = match[2]\n  const eventTypeStr = match[3]\n  const terminator = match[4]\n\n  // Determine key name based on terminator\n  let keyName: string | undefined\n  if (terminator === \"~\") {\n    // Tilde key: lookup by number\n    keyName = tildeKeyMap[keyNumOrOne]\n  } else {\n    // Functional key: must have \"1\" as first param, lookup by letter\n    if (keyNumOrOne !== \"1\") return null\n    keyName = functionalKeyMap[terminator]\n  }\n\n  if (!keyName) return null\n\n  const key: ParsedKey = {\n    name: keyName,\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence,\n    raw: sequence,\n    eventType: \"press\",\n    source: \"kitty\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  }\n\n  // Parse modifiers\n  if (modifierStr) {\n    const modifierMask = parseInt(modifierStr, 10)\n    if (!isNaN(modifierMask) && modifierMask > 1) {\n      const mods = fromKittyMods(modifierMask - 1)\n      key.shift = mods.shift\n      key.ctrl = mods.ctrl\n      key.meta = mods.alt || mods.meta\n      key.option = mods.alt\n      key.super = mods.super\n      key.hyper = mods.hyper\n      key.capsLock = mods.capsLock\n      key.numLock = mods.numLock\n    }\n  }\n\n  // Parse event type: 1 = press, 2 = repeat, 3 = release\n  if (eventTypeStr === \"1\" || !eventTypeStr) {\n    key.eventType = \"press\"\n  } else if (eventTypeStr === \"2\") {\n    key.eventType = \"press\"\n    key.repeated = true\n  } else if (eventTypeStr === \"3\") {\n    key.eventType = \"release\"\n  }\n\n  return key\n}\n\nexport function parseKittyKeyboard(sequence: string): ParsedKey | null {\n  // Try special key format (functional letters or tilde keys with event type)\n  const specialResult = parseKittySpecialKey(sequence)\n  if (specialResult) return specialResult\n\n  // Kitty keyboard protocol: CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u\n  const kittyRe = /^\\x1b\\[([^\\x1b]+)u$/\n  const match = kittyRe.exec(sequence)\n\n  if (!match) return null\n\n  const params = match[1]\n  const fields = params.split(\";\")\n\n  if (fields.length < 1) return null\n\n  const key: ParsedKey = {\n    name: \"\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence,\n    raw: sequence,\n    eventType: \"press\",\n    source: \"kitty\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  }\n\n  let text = \"\"\n\n  // Parse field 1: unicode-key-code:shifted_codepoint:base_layout_codepoint\n  const field1 = fields[0]?.split(\":\") || []\n  const codepointStr = field1[0]\n  if (!codepointStr) return null\n\n  const codepoint = parseInt(codepointStr, 10)\n  if (isNaN(codepoint)) return null\n\n  let shiftedCodepoint: number | undefined\n  let baseCodepoint: number | undefined\n\n  // Parse shifted and base codepoints\n  if (field1[1]) {\n    const shifted = parseInt(field1[1], 10)\n    if (!isNaN(shifted) && shifted > 0 && shifted <= 0x10ffff) {\n      shiftedCodepoint = shifted\n    }\n  }\n  if (field1[2]) {\n    const base = parseInt(field1[2], 10)\n    if (!isNaN(base) && base > 0 && base <= 0x10ffff) {\n      baseCodepoint = base\n    }\n  }\n\n  const knownKey = kittyKeyMap[codepoint]\n  if (knownKey) {\n    key.name = knownKey\n    key.code = `[${codepoint}u`\n  } else if (codepoint === 0) {\n    key.name = \"\"\n  } else {\n    // It's a Unicode character\n    if (codepoint > 0 && codepoint <= 0x10ffff) {\n      const char = String.fromCodePoint(codepoint)\n      key.name = char\n\n      // Store base layout codepoint for keyboard layout disambiguation\n      if (baseCodepoint) {\n        key.baseCode = baseCodepoint\n      }\n    } else {\n      return null // Invalid codepoint\n    }\n  }\n\n  // Parse field 2: modifier_mask:event_type\n  if (fields[1]) {\n    const field2 = fields[1].split(\":\")\n    const modifierStr = field2[0]\n    const eventTypeStr = field2[1]\n\n    if (modifierStr) {\n      const modifierMask = parseInt(modifierStr, 10)\n      if (!isNaN(modifierMask) && modifierMask > 1) {\n        const mods = fromKittyMods(modifierMask - 1) // Kitty modifiers start from 1\n        key.shift = mods.shift\n        key.ctrl = mods.ctrl\n        key.meta = mods.alt || mods.meta\n        key.option = mods.alt\n        key.super = mods.super\n        key.hyper = mods.hyper\n        key.capsLock = mods.capsLock\n        key.numLock = mods.numLock\n      }\n    }\n\n    // Parse event type: 1 = press (default), 2 = repeat, 3 = release\n    if (eventTypeStr === \"1\" || !eventTypeStr) {\n      key.eventType = \"press\"\n    } else if (eventTypeStr === \"2\") {\n      key.eventType = \"press\"\n      key.repeated = true\n    } else if (eventTypeStr === \"3\") {\n      key.eventType = \"release\"\n    } else {\n      key.eventType = \"press\"\n    }\n  }\n\n  // Parse field 3: text_as_codepoint[:text_as_codepoint]\n  if (fields[2]) {\n    const codepoints = fields[2].split(\":\")\n    for (const cpStr of codepoints) {\n      const cp = parseInt(cpStr, 10)\n      if (!isNaN(cp) && cp > 0 && cp <= 0x10ffff) {\n        text += String.fromCodePoint(cp)\n      }\n    }\n  }\n\n  // Handle text generation for printable characters\n  if (text === \"\") {\n    // Check if this is a printable character (not a key name like \"up\", \"f1\", etc.)\n    const isPrintable = key.name.length > 0 && !kittyKeyMap[codepoint]\n    if (isPrintable) {\n      // Use shifted codepoint if shift is active and we have one\n      if (key.shift && shiftedCodepoint) {\n        text = String.fromCodePoint(shiftedCodepoint)\n      } else if (key.shift && key.name.length === 1) {\n        // When shift is pressed but terminal didn't provide shifted codepoint,\n        // convert the character to uppercase (works for Unicode including Cyrillic)\n        text = key.name.toLocaleUpperCase()\n      } else {\n        text = key.name\n      }\n    }\n  }\n\n  // Special case: shift + space should produce a space\n  if (key.name === \" \" && key.shift && !key.ctrl && !key.meta) {\n    text = \" \"\n  }\n\n  if (text) {\n    if (codepoint === 0) {\n      key.name = text\n    }\n    key.sequence = text\n  }\n\n  if (codepoint === 0 && text === \"\") {\n    return null\n  }\n\n  return key\n}\n"
  },
  {
    "path": "packages/core/src/lib/parse.keypress.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { parseKeypress, nonAlphanumericKeys, type ParsedKey, type KeyEventType } from \"./parse.keypress.js\"\nimport { Buffer } from \"node:buffer\"\n\ntest(\"parseKeypress - basic letters\", () => {\n  expect(parseKeypress(\"a\")).toEqual({\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"a\",\n    eventType: \"press\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"A\")).toEqual({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"A\",\n    raw: \"A\",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - numbers\", () => {\n  expect(parseKeypress(\"1\")).toEqual({\n    eventType: \"press\",\n    name: \"1\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: true,\n    sequence: \"1\",\n    raw: \"1\",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - special keys\", () => {\n  expect(parseKeypress(\"\\r\")).toEqual({\n    eventType: \"press\",\n    name: \"return\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\r\",\n    raw: \"\\r\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\n\")).toEqual({\n    eventType: \"press\",\n    name: \"linefeed\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\n\",\n    raw: \"\\n\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b\\r\")).toEqual({\n    eventType: \"press\",\n    name: \"return\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b\\r\",\n    raw: \"\\x1b\\r\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b\\n\")).toEqual({\n    eventType: \"press\",\n    name: \"linefeed\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b\\n\",\n    raw: \"\\x1b\\n\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\t\")).toEqual({\n    eventType: \"press\",\n    name: \"tab\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\t\",\n    raw: \"\\t\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\b\")).toEqual({\n    eventType: \"press\",\n    name: \"backspace\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\b\",\n    raw: \"\\b\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b\")).toEqual({\n    name: \"escape\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b\",\n    raw: \"\\x1b\",\n    eventType: \"press\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\" \")).toEqual({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \" \",\n    raw: \" \",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - ctrl+letter combinations\", () => {\n  expect(parseKeypress(\"\\x01\")).toEqual({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x01\",\n    raw: \"\\x01\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1a\")).toEqual({\n    eventType: \"press\",\n    name: \"z\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1a\",\n    raw: \"\\x1a\",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - ctrl+space and alt+space\", () => {\n  // Ctrl+Space sends \\x00 (null character)\n  expect(parseKeypress(\"\\x00\")).toEqual({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x00\",\n    raw: \"\\x00\",\n    source: \"raw\",\n  })\n\n  // Also test with unicode escape notation\n  expect(parseKeypress(\"\\u0000\")).toEqual({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\u0000\",\n    raw: \"\\u0000\",\n    source: \"raw\",\n  })\n\n  // Alt+Space / Option+Space sends ESC + space (\\x1b or \\u001b followed by space)\n  // Note: meta=true indicates Alt/Option was pressed, but option=false because\n  // this is a simple ESC-prefix sequence (not an ANSI sequence with modifier bits)\n  expect(parseKeypress(\"\\x1b \")).toEqual({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b \",\n    raw: \"\\x1b \",\n    source: \"raw\",\n  })\n\n  // Test with \\u001b notation as well\n  expect(parseKeypress(\"\\u001b \")).toEqual({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\u001b \",\n    raw: \"\\u001b \",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - meta+character combinations\", () => {\n  // Simple ESC+character sequences (like ESC+a) set meta=true but option=false\n  // These sequences are typically generated by Alt/Option+key on many terminals\n  // but the simple ESC prefix doesn't distinguish between Alt/Option and Meta/Cmd\n  // so option flag is NOT set (unlike ANSI sequences with explicit modifier bits)\n  expect(parseKeypress(\"\\x1ba\")).toEqual({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false, // Note: option is NOT set for simple ESC+char sequences\n    number: false,\n    sequence: \"\\x1ba\",\n    raw: \"\\x1ba\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1bA\")).toEqual({\n    eventType: \"press\",\n    name: \"A\",\n    ctrl: false,\n    meta: true,\n    shift: true,\n    option: false, // Note: option is NOT set for simple ESC+char sequences\n    number: false,\n    sequence: \"\\x1bA\",\n    raw: \"\\x1bA\",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - function keys\", () => {\n  expect(parseKeypress(\"\\x1bOP\")).toEqual({\n    eventType: \"press\",\n    name: \"f1\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1bOP\",\n    raw: \"\\x1bOP\",\n    code: \"OP\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[11~\")).toEqual({\n    eventType: \"press\",\n    name: \"f1\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[11~\",\n    raw: \"\\x1b[11~\",\n    code: \"[11~\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[24~\")).toEqual({\n    eventType: \"press\",\n    name: \"f12\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[24~\",\n    raw: \"\\x1b[24~\",\n    code: \"[24~\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - arrow keys\", () => {\n  expect(parseKeypress(\"\\x1b[A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[A\",\n    raw: \"\\x1b[A\",\n    code: \"[A\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[B\")).toEqual({\n    eventType: \"press\",\n    name: \"down\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[B\",\n    raw: \"\\x1b[B\",\n    code: \"[B\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[C\")).toEqual({\n    eventType: \"press\",\n    name: \"right\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[C\",\n    raw: \"\\x1b[C\",\n    code: \"[C\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[D\")).toEqual({\n    eventType: \"press\",\n    name: \"left\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[D\",\n    raw: \"\\x1b[D\",\n    code: \"[D\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - navigation keys\", () => {\n  expect(parseKeypress(\"\\x1b[H\")).toEqual({\n    eventType: \"press\",\n    name: \"home\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[H\",\n    raw: \"\\x1b[H\",\n    code: \"[H\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[F\")).toEqual({\n    eventType: \"press\",\n    name: \"end\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[F\",\n    raw: \"\\x1b[F\",\n    code: \"[F\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[5~\")).toEqual({\n    eventType: \"press\",\n    name: \"pageup\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[5~\",\n    raw: \"\\x1b[5~\",\n    code: \"[5~\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[6~\")).toEqual({\n    eventType: \"press\",\n    name: \"pagedown\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[6~\",\n    raw: \"\\x1b[6~\",\n    code: \"[6~\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - modifier combinations\", () => {\n  // Shift only: modifier value 2 = bits 1 (0b0001)\n  expect(parseKeypress(\"\\x1b[1;2A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[1;2A\",\n    raw: \"\\x1b[1;2A\",\n    code: \"[A\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  // Alt/Option key: modifier value 3 = bits 2 (0b0010)\n  // Note: Alt/Option (same key) sets both meta and option flags\n  expect(parseKeypress(\"\\x1b[1;3A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: true,\n    number: false,\n    sequence: \"\\x1b[1;3A\",\n    raw: \"\\x1b[1;3A\",\n    code: \"[A\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  // Shift+Alt/Option: modifier value 4 = bits 3 (0b0011 = Shift(1) + Alt/Option(2))\n  expect(parseKeypress(\"\\x1b[1;4A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: true,\n    shift: true,\n    option: true,\n    number: false,\n    sequence: \"\\x1b[1;4A\",\n    raw: \"\\x1b[1;4A\",\n    code: \"[A\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  // Ctrl only: modifier value 5 = bits 4 (0b0100)\n  expect(parseKeypress(\"\\x1b[1;5A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[1;5A\",\n    raw: \"\\x1b[1;5A\",\n    code: \"[A\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  // Shift+Alt/Option+Ctrl: modifier value 8 = bits 7 (0b0111 = Shift(1) + Alt/Option(2) + Ctrl(4))\n  // Note: meta is true because Alt/Option is pressed, NOT because Meta/Cmd bit is set\n  expect(parseKeypress(\"\\x1b[1;8A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: true,\n    meta: true,\n    shift: true,\n    option: true,\n    number: false,\n    sequence: \"\\x1b[1;8A\",\n    raw: \"\\x1b[1;8A\",\n    code: \"[A\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  // Super modifier bit only: modifier value 9 = bits 8 (0b1000)\n  // NOTE: This is bit 8 which is the Super key\n  expect(parseKeypress(\"\\x1b[1;9A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[1;9A\",\n    raw: \"\\x1b[1;9A\",\n    code: \"[A\",\n    super: true,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  // Shift+Super: modifier value 10 = bits 9 (0b1001 = Shift(1) + Super(8))\n  expect(parseKeypress(\"\\x1b[1;10A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[1;10A\",\n    raw: \"\\x1b[1;10A\",\n    code: \"[A\",\n    super: true,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  // Alt/Option+Super: modifier value 11 = bits 10 (0b1010 = Alt/Option(2) + Super(8))\n  expect(parseKeypress(\"\\x1b[1;11A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: true,\n    number: false,\n    sequence: \"\\x1b[1;11A\",\n    raw: \"\\x1b[1;11A\",\n    code: \"[A\",\n    super: true,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  // All ANSI modifier bits: modifier value 16 = bits 15 (0b1111 = Shift(1) + Alt(2) + Ctrl(4) + Super(8))\n  expect(parseKeypress(\"\\x1b[1;16A\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: true,\n    meta: true,\n    shift: true,\n    option: true,\n    number: false,\n    sequence: \"\\x1b[1;16A\",\n    raw: \"\\x1b[1;16A\",\n    code: \"[A\",\n    super: true,\n    hyper: false,\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - delete key\", () => {\n  expect(parseKeypress(\"\\x1b[3~\")).toEqual({\n    eventType: \"press\",\n    name: \"delete\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[3~\",\n    raw: \"\\x1b[3~\",\n    code: \"[3~\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - delete key with modifiers (modifyOtherKeys format)\", () => {\n  // Delete key without modifiers: \\x1b[3~\n  const plainDelete = parseKeypress(\"\\x1b[3~\")!\n  expect(plainDelete.name).toBe(\"delete\")\n  expect(plainDelete.shift).toBe(false)\n  expect(plainDelete.ctrl).toBe(false)\n  expect(plainDelete.meta).toBe(false)\n  expect(plainDelete.option).toBe(false)\n\n  // Shift+Delete: \\x1b[3;2~\n  const shiftDelete = parseKeypress(\"\\x1b[3;2~\")!\n  expect(shiftDelete.name).toBe(\"delete\")\n  expect(shiftDelete.shift).toBe(true)\n  expect(shiftDelete.ctrl).toBe(false)\n  expect(shiftDelete.meta).toBe(false)\n  expect(shiftDelete.option).toBe(false)\n  expect(shiftDelete.sequence).toBe(\"\\x1b[3;2~\")\n  expect(shiftDelete.code).toBe(\"[3~\")\n\n  // Option/Meta+Delete: \\x1b[3;3~\n  const metaDelete = parseKeypress(\"\\x1b[3;3~\")!\n  expect(metaDelete.name).toBe(\"delete\")\n  expect(metaDelete.meta).toBe(true)\n  expect(metaDelete.option).toBe(true)\n  expect(metaDelete.ctrl).toBe(false)\n  expect(metaDelete.shift).toBe(false)\n  expect(metaDelete.sequence).toBe(\"\\x1b[3;3~\")\n  expect(metaDelete.code).toBe(\"[3~\")\n\n  // Ctrl+Delete: \\x1b[3;5~\n  const ctrlDelete = parseKeypress(\"\\x1b[3;5~\")!\n  expect(ctrlDelete.name).toBe(\"delete\")\n  expect(ctrlDelete.ctrl).toBe(true)\n  expect(ctrlDelete.shift).toBe(false)\n  expect(ctrlDelete.meta).toBe(false)\n  expect(ctrlDelete.option).toBe(false)\n  expect(ctrlDelete.sequence).toBe(\"\\x1b[3;5~\")\n  expect(ctrlDelete.code).toBe(\"[3~\")\n\n  // Shift+Option+Delete: \\x1b[3;4~\n  const shiftMetaDelete = parseKeypress(\"\\x1b[3;4~\")!\n  expect(shiftMetaDelete.name).toBe(\"delete\")\n  expect(shiftMetaDelete.shift).toBe(true)\n  expect(shiftMetaDelete.meta).toBe(true)\n  expect(shiftMetaDelete.option).toBe(true)\n  expect(shiftMetaDelete.ctrl).toBe(false)\n  expect(shiftMetaDelete.sequence).toBe(\"\\x1b[3;4~\")\n  expect(shiftMetaDelete.code).toBe(\"[3~\")\n\n  // Ctrl+Option+Delete: \\x1b[3;7~\n  const ctrlMetaDelete = parseKeypress(\"\\x1b[3;7~\")!\n  expect(ctrlMetaDelete.name).toBe(\"delete\")\n  expect(ctrlMetaDelete.ctrl).toBe(true)\n  expect(ctrlMetaDelete.meta).toBe(true)\n  expect(ctrlMetaDelete.option).toBe(true)\n  expect(ctrlMetaDelete.shift).toBe(false)\n  expect(ctrlMetaDelete.sequence).toBe(\"\\x1b[3;7~\")\n  expect(ctrlMetaDelete.code).toBe(\"[3~\")\n})\n\ntest(\"parseKeypress - delete key with modifiers (Kitty keyboard protocol)\", () => {\n  // Delete key in Kitty protocol uses code 57349\n  // Without modifiers: \\x1b[57349u\n  const plainDelete = parseKeypress(\"\\x1b[57349u\", { useKittyKeyboard: true })!\n  expect(plainDelete.name).toBe(\"delete\")\n  expect(plainDelete.shift).toBe(false)\n  expect(plainDelete.ctrl).toBe(false)\n  expect(plainDelete.meta).toBe(false)\n  expect(plainDelete.source).toBe(\"kitty\")\n\n  // Shift+Delete: \\x1b[57349;2u\n  const shiftDelete = parseKeypress(\"\\x1b[57349;2u\", { useKittyKeyboard: true })!\n  expect(shiftDelete.name).toBe(\"delete\")\n  expect(shiftDelete.shift).toBe(true)\n  expect(shiftDelete.ctrl).toBe(false)\n  expect(shiftDelete.meta).toBe(false)\n  expect(shiftDelete.source).toBe(\"kitty\")\n\n  // Option/Meta+Delete: \\x1b[57349;3u\n  const metaDelete = parseKeypress(\"\\x1b[57349;3u\", { useKittyKeyboard: true })!\n  expect(metaDelete.name).toBe(\"delete\")\n  expect(metaDelete.meta).toBe(true)\n  expect(metaDelete.ctrl).toBe(false)\n  expect(metaDelete.shift).toBe(false)\n  expect(metaDelete.source).toBe(\"kitty\")\n\n  // Ctrl+Delete: \\x1b[57349;5u\n  const ctrlDelete = parseKeypress(\"\\x1b[57349;5u\", { useKittyKeyboard: true })!\n  expect(ctrlDelete.name).toBe(\"delete\")\n  expect(ctrlDelete.ctrl).toBe(true)\n  expect(ctrlDelete.shift).toBe(false)\n  expect(ctrlDelete.meta).toBe(false)\n  expect(ctrlDelete.source).toBe(\"kitty\")\n})\n\ntest(\"parseKeypress - backspace key with modifiers (modifyOtherKeys format)\", () => {\n  // Backspace is typically \\x7f or \\b, but with modifiers uses modifyOtherKeys format\n\n  // Shift+Backspace: \\x1b[27;2;127~ (using charcode 127)\n  const shiftBackspace = parseKeypress(\"\\x1b[27;2;127~\")!\n  expect(shiftBackspace.name).toBe(\"backspace\")\n  expect(shiftBackspace.shift).toBe(true)\n  expect(shiftBackspace.ctrl).toBe(false)\n  expect(shiftBackspace.meta).toBe(false)\n  expect(shiftBackspace.option).toBe(false)\n\n  // Ctrl+Backspace: \\x1b[27;5;127~\n  const ctrlBackspace = parseKeypress(\"\\x1b[27;5;127~\")!\n  expect(ctrlBackspace.name).toBe(\"backspace\")\n  expect(ctrlBackspace.ctrl).toBe(true)\n  expect(ctrlBackspace.shift).toBe(false)\n  expect(ctrlBackspace.meta).toBe(false)\n  expect(ctrlBackspace.option).toBe(false)\n\n  // Option/Meta+Backspace: \\x1b[27;3;127~\n  const metaBackspace = parseKeypress(\"\\x1b[27;3;127~\")!\n  expect(metaBackspace.name).toBe(\"backspace\")\n  expect(metaBackspace.meta).toBe(true)\n  expect(metaBackspace.option).toBe(true)\n  expect(metaBackspace.ctrl).toBe(false)\n  expect(metaBackspace.shift).toBe(false)\n})\n\ntest(\"parseKeypress - backspace key with modifiers (Kitty keyboard protocol)\", () => {\n  // Backspace key in Kitty protocol uses code 127\n  // Ctrl+Backspace: \\x1b[127;5u\n  const ctrlBackspace = parseKeypress(\"\\x1b[127;5u\", { useKittyKeyboard: true })!\n  expect(ctrlBackspace.name).toBe(\"backspace\")\n  expect(ctrlBackspace.ctrl).toBe(true)\n  expect(ctrlBackspace.shift).toBe(false)\n  expect(ctrlBackspace.meta).toBe(false)\n  expect(ctrlBackspace.source).toBe(\"kitty\")\n\n  // Option/Meta+Backspace: \\x1b[127;3u\n  const metaBackspace = parseKeypress(\"\\x1b[127;3u\", { useKittyKeyboard: true })!\n  expect(metaBackspace.name).toBe(\"backspace\")\n  expect(metaBackspace.meta).toBe(true)\n  expect(metaBackspace.ctrl).toBe(false)\n  expect(metaBackspace.shift).toBe(false)\n  expect(metaBackspace.source).toBe(\"kitty\")\n\n  // Shift+Backspace: \\x1b[127;2u\n  const shiftBackspace = parseKeypress(\"\\x1b[127;2u\", { useKittyKeyboard: true })!\n  expect(shiftBackspace.name).toBe(\"backspace\")\n  expect(shiftBackspace.shift).toBe(true)\n  expect(shiftBackspace.ctrl).toBe(false)\n  expect(shiftBackspace.meta).toBe(false)\n  expect(shiftBackspace.source).toBe(\"kitty\")\n})\n\ntest(\"parseKeypress - Buffer input\", () => {\n  const buf = Buffer.from(\"a\")\n  expect(parseKeypress(buf)).toEqual({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"a\",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - high byte buffer handling\", () => {\n  const buf = Buffer.from([160]) // 128 + 32, should become \\x1b + \" \"\n  expect(parseKeypress(buf)).toEqual({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b \",\n    raw: \"\\x1b \",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - empty input\", () => {\n  expect(parseKeypress(\"\")).toEqual({\n    eventType: \"press\",\n    name: \"\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\",\n    raw: \"\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress()).toEqual({\n    eventType: \"press\",\n    name: \"\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\",\n    raw: \"\",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - special characters\", () => {\n  expect(parseKeypress(\"!\")).toEqual({\n    eventType: \"press\",\n    name: \"!\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"!\",\n    raw: \"!\",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"@\")).toEqual({\n    eventType: \"press\",\n    name: \"@\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"@\",\n    raw: \"@\",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - meta space and escape combinations\", () => {\n  expect(parseKeypress(\"\\x1b \")).toEqual({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b \",\n    raw: \"\\x1b \",\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b\\x1b\")).toEqual({\n    eventType: \"press\",\n    name: \"escape\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b\\x1b\",\n    raw: \"\\x1b\\x1b\",\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - rxvt style arrow keys with modifiers\", () => {\n  expect(parseKeypress(\"\\x1b[a\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[a\",\n    raw: \"\\x1b[a\",\n    code: \"[a\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[2$\")).toEqual({\n    eventType: \"press\",\n    name: \"insert\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[2$\",\n    raw: \"\\x1b[2$\",\n    code: \"[2$\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n})\n\ntest(\"parseKeypress - ctrl modifier keys\", () => {\n  expect(parseKeypress(\"\\x1bOa\")).toEqual({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1bOa\",\n    raw: \"\\x1bOa\",\n    code: \"Oa\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n\n  expect(parseKeypress(\"\\x1b[2^\")).toEqual({\n    eventType: \"press\",\n    name: \"insert\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[2^\",\n    raw: \"\\x1b[2^\",\n    code: \"[2^\",\n    super: false,\n    hyper: false,\n    source: \"raw\",\n  })\n})\n\ntest(\"nonAlphanumericKeys export\", () => {\n  expect(Array.isArray(nonAlphanumericKeys)).toBe(true)\n  expect(nonAlphanumericKeys.length).toBeGreaterThan(0)\n  expect(nonAlphanumericKeys).toContain(\"up\")\n  expect(nonAlphanumericKeys).toContain(\"down\")\n  expect(nonAlphanumericKeys).toContain(\"f1\")\n  expect(nonAlphanumericKeys).toContain(\"backspace\")\n  expect(nonAlphanumericKeys).toContain(\"tab\")\n  expect(nonAlphanumericKeys).toContain(\"left\")\n  expect(nonAlphanumericKeys).toContain(\"right\")\n})\n\n// Tests for modifier bit calculations and meta/option relationship\n// Terminal modifier bits (ANSI standard): Shift=1, Alt/Option=2, Ctrl=4, Meta=8\n//\n// IMPORTANT REALITY CHECK:\n// - Alt and Option are THE SAME PHYSICAL KEY (macOS calls it Option, others call it Alt)\n// - Cmd (Mac), Win (Windows), and many Ctrl combos DON'T reach the terminal - OS intercepts them\n// - The \"Meta\" modifier bit (8) exists in ANSI standard but is THEORETICAL\n// - In practice, only Alt/Option generates modifier sequences that reach the terminal\n//\n// The `option` flag: true when ANSI escape sequence has explicit Alt modifier bit (bit 2)\n// The `meta` flag: true when ESC prefix is detected OR ANSI Alt/Meta bits are set (legacy naming)\n//\n// Real terminal behavior on macOS (see key-results file):\n// - Alt+letter: sends ESC+char (e.g., \"\\x1ba\") → meta=true, option=false\n// - Alt+arrow: sends ANSI sequence (e.g., \"\\x1b[1;3A\") → meta=true, option=true\n// - Cmd+anything: NO EVENT reaches terminal (OS intercepts)\n// - Ctrl+arrow: NO EVENT reaches terminal on Mac (OS intercepts)\ntest(\"parseKeypress - modifier bit calculations and meta/option relationship\", () => {\n  // Individual modifiers to establish the baseline\n\n  // Shift modifier is bit 0 (value 1), so modifier value 2 = 1 + 1\n  const shiftOnly = parseKeypress(\"\\x1b[1;2A\")!\n  expect(shiftOnly.name).toBe(\"up\")\n  expect(shiftOnly.shift).toBe(true)\n  expect(shiftOnly.ctrl).toBe(false)\n  expect(shiftOnly.meta).toBe(false)\n  expect(shiftOnly.option).toBe(false)\n\n  // Alt/Option modifier is bit 1 (value 2), so modifier value 3 = 2 + 1\n  // IMPORTANT: Alt/Option (same key, different names) sets BOTH meta and option flags\n  const altOnly = parseKeypress(\"\\x1b[1;3A\")!\n  expect(altOnly.name).toBe(\"up\")\n  expect(altOnly.meta).toBe(true) // Alt/Option sets meta flag\n  expect(altOnly.option).toBe(true) // Alt/Option sets option flag\n  expect(altOnly.ctrl).toBe(false)\n  expect(altOnly.shift).toBe(false)\n\n  // Ctrl modifier is bit 2 (value 4), so modifier value 5 = 4 + 1\n  const ctrlOnly = parseKeypress(\"\\x1b[1;5A\")!\n  expect(ctrlOnly.name).toBe(\"up\")\n  expect(ctrlOnly.ctrl).toBe(true)\n  expect(ctrlOnly.meta).toBe(false)\n  expect(ctrlOnly.shift).toBe(false)\n  expect(ctrlOnly.option).toBe(false)\n\n  // Super modifier is bit 3 (value 8), so modifier value 9 = 8 + 1\n  // Super is the Command/Windows key\n  const superOnly = parseKeypress(\"\\x1b[1;9A\")!\n  expect(superOnly.name).toBe(\"up\")\n  expect(superOnly.meta).toBe(false)\n  expect(superOnly.option).toBe(false)\n  expect(superOnly.ctrl).toBe(false)\n  expect(superOnly.shift).toBe(false)\n  expect(superOnly.super).toBe(true)\n  expect(superOnly.hyper).toBe(false)\n\n  // Combined modifiers to test the relationships\n\n  // Ctrl+Super = 4 + 8 = 12, so modifier value 13 = 12 + 1\n  const ctrlSuper = parseKeypress(\"\\x1b[1;13A\")!\n  expect(ctrlSuper.name).toBe(\"up\")\n  expect(ctrlSuper.ctrl).toBe(true)\n  expect(ctrlSuper.meta).toBe(false)\n  expect(ctrlSuper.shift).toBe(false)\n  expect(ctrlSuper.option).toBe(false)\n  expect(ctrlSuper.super).toBe(true)\n  expect(ctrlSuper.hyper).toBe(false)\n\n  // Shift+Alt/Option = 1 + 2 = 3, so modifier value 4 = 3 + 1\n  // Should have meta=true, option=true (Alt/Option key is pressed)\n  const shiftAlt = parseKeypress(\"\\x1b[1;4A\")!\n  expect(shiftAlt.name).toBe(\"up\")\n  expect(shiftAlt.shift).toBe(true)\n  expect(shiftAlt.option).toBe(true) // Alt/Option sets option\n  expect(shiftAlt.meta).toBe(true) // Alt/Option also sets meta\n  expect(shiftAlt.ctrl).toBe(false)\n\n  // Alt/Option+Meta/Cmd = 2 + 8 = 10, so modifier value 11 = 10 + 1\n  // Both physical keys pressed: Alt/Option key AND Meta/Cmd key\n  const altMeta = parseKeypress(\"\\x1b[1;11A\")!\n  expect(altMeta.name).toBe(\"up\")\n  expect(altMeta.meta).toBe(true) // Both Alt/Option and Meta/Cmd set meta flag\n  expect(altMeta.option).toBe(true) // Alt/Option sets option flag\n  expect(altMeta.ctrl).toBe(false)\n  expect(altMeta.shift).toBe(false)\n\n  // Ctrl+Alt/Option = 4 + 2 = 6, so modifier value 7 = 6 + 1\n  // Should have meta=true, option=true (Alt/Option key is pressed)\n  const ctrlAlt = parseKeypress(\"\\x1b[1;7A\")!\n  expect(ctrlAlt.name).toBe(\"up\")\n  expect(ctrlAlt.ctrl).toBe(true)\n  expect(ctrlAlt.meta).toBe(true) // Alt/Option sets meta\n  expect(ctrlAlt.option).toBe(true) // Alt/Option sets option\n  expect(ctrlAlt.shift).toBe(false)\n\n  // All modifiers: Shift(1) + Alt(2) + Ctrl(4) + Meta(8) = 15, so modifier value 16 = 15 + 1\n  const allMods = parseKeypress(\"\\x1b[1;16A\")!\n  expect(allMods.name).toBe(\"up\")\n  expect(allMods.shift).toBe(true)\n  expect(allMods.option).toBe(true) // Alt is present\n  expect(allMods.ctrl).toBe(true)\n  expect(allMods.meta).toBe(true) // Both Alt and Meta are present\n})\n\ntest(\"parseKeypress - distinguishing between Alt/Option and theoretical Meta modifier\", () => {\n  // IMPORTANT REALITY:\n  // - Alt and Option are THE SAME PHYSICAL KEY (macOS calls it Option, others call it Alt)\n  // - This is the ONLY modifier key that reliably reaches the terminal\n  // - Cmd (Mac) and Win (Windows) keys are intercepted by the OS and DON'T reach the terminal\n  // - The ANSI \"Meta\" modifier bit (8) is part of the standard but rarely/never seen in practice\n  //\n  // Real terminal behavior (see key-results file):\n  // - Alt+letter: \"\\x1ba\" → meta=true, option=false (simple ESC prefix)\n  // - Alt+arrow: \"\\x1b[1;3A\" → meta=true, option=true (ANSI with Alt bit)\n  // - Cmd+anything: NO EVENT (OS intercepts)\n\n  // Alt/Option key with arrow (ANSI sequence with modifier bit 2)\n  const altArrow = parseKeypress(\"\\x1b[1;3C\")! // Real: Alt/Option+Right\n  expect(altArrow.name).toBe(\"right\")\n  expect(altArrow.meta).toBe(true)\n  expect(altArrow.option).toBe(true)\n  expect(altArrow.ctrl).toBe(false)\n  expect(altArrow.shift).toBe(false)\n\n  // Super key: bit 8\n  const superArrow = parseKeypress(\"\\x1b[1;9C\")! // Super bit only\n  expect(superArrow.name).toBe(\"right\")\n  expect(superArrow.meta).toBe(false)\n  expect(superArrow.option).toBe(false)\n  expect(superArrow.ctrl).toBe(false)\n  expect(superArrow.shift).toBe(false)\n  expect(superArrow.super).toBe(true)\n  expect(superArrow.hyper).toBe(false)\n\n  // To detect if Alt/Option was pressed in ANSI sequences: check option=true\n  expect(altArrow.option).toBe(true)\n\n  // Both Alt and Super bits set\n  const altSuperArrow = parseKeypress(\"\\x1b[1;11C\")! // Alt+Super bits\n  expect(altSuperArrow.meta).toBe(true)\n  expect(altSuperArrow.option).toBe(true)\n  expect(altSuperArrow.super).toBe(true)\n  expect(altSuperArrow.hyper).toBe(false)\n})\n\ntest(\"parseKeypress - modifier combinations with function keys\", () => {\n  // Ctrl+F1 - may work depending on OS/terminal configuration\n  const ctrlF1 = parseKeypress(\"\\x1b[11;5~\")!\n  expect(ctrlF1.name).toBe(\"f1\")\n  expect(ctrlF1.ctrl).toBe(true)\n  expect(ctrlF1.meta).toBe(false)\n  expect(ctrlF1.option).toBe(false)\n  expect(ctrlF1.eventType).toBe(\"press\")\n\n  // Alt/Option+F1 - real key combination that reaches terminal\n  const altF1 = parseKeypress(\"\\x1b[11;3~\")!\n  expect(altF1.name).toBe(\"f1\")\n  expect(altF1.meta).toBe(true)\n  expect(altF1.option).toBe(true)\n  expect(altF1.ctrl).toBe(false)\n  expect(altF1.eventType).toBe(\"press\")\n\n  // Super key (bit 8)\n  const superF1 = parseKeypress(\"\\x1b[11;9~\")!\n  expect(superF1.name).toBe(\"f1\")\n  expect(superF1.meta).toBe(false)\n  expect(superF1.option).toBe(false)\n  expect(superF1.ctrl).toBe(false)\n  expect(superF1.super).toBe(true)\n  expect(superF1.hyper).toBe(false)\n  expect(superF1.eventType).toBe(\"press\")\n\n  // Shift+Ctrl+F1 - may work depending on OS/terminal configuration\n  const shiftCtrlF1 = parseKeypress(\"\\x1b[11;6~\")!\n  expect(shiftCtrlF1.name).toBe(\"f1\")\n  expect(shiftCtrlF1.shift).toBe(true)\n  expect(shiftCtrlF1.ctrl).toBe(true)\n  expect(shiftCtrlF1.meta).toBe(false)\n  expect(shiftCtrlF1.option).toBe(false)\n  expect(shiftCtrlF1.eventType).toBe(\"press\")\n})\n\ntest(\"parseKeypress - regular parsing always defaults to press event type\", () => {\n  // Test various regular key sequences to ensure they all default to \"press\"\n  const keys = [\n    \"a\",\n    \"A\",\n    \"1\",\n    \"!\",\n    \"\\t\",\n    \"\\r\",\n    \"\\n\",\n    \" \",\n    \"\\x1b\",\n    \"\\x01\", // Ctrl+A\n    \"\\x1ba\", // Alt+A\n    \"\\x1b[A\", // Up arrow\n    \"\\x1b[11~\", // F1\n    \"\\x1b[1;2A\", // Shift+Up\n    \"\\x1b[3~\", // Delete\n  ]\n\n  for (const keySeq of keys) {\n    const result = parseKeypress(keySeq)!\n    expect(result.eventType).toBe(\"press\")\n  }\n\n  // Test with Buffer input too\n  const bufResult = parseKeypress(Buffer.from(\"x\"))\n  expect(bufResult?.eventType).toBe(\"press\")\n})\n\ntest(\"KeyEventType type validation\", () => {\n  // Test that KeyEventType only allows valid values\n  const validEventTypes: KeyEventType[] = [\"press\", \"repeat\", \"release\"]\n\n  for (const eventType of validEventTypes) {\n    // This should compile without errors\n    const mockKey: ParsedKey = {\n      name: \"test\",\n      ctrl: false,\n      meta: false,\n      shift: false,\n      option: false,\n      sequence: \"test\",\n      raw: \"test\",\n      number: false,\n      eventType: eventType,\n      source: \"raw\",\n    }\n    expect(mockKey.eventType).toBe(eventType)\n  }\n})\n\ntest(\"parseKeypress - ctrl+option+letter combinations\", () => {\n  // This is ESC (\\x1b) followed by \\x15 (which is Ctrl+U)\n  const ctrlOptionU = parseKeypress(\"\\u001b\\u0015\")!\n\n  // The sequence should be parsed as meta+ctrl+u\n  expect(ctrlOptionU?.name).toBe(\"u\")\n  expect(ctrlOptionU?.ctrl).toBe(true)\n  expect(ctrlOptionU?.meta).toBe(true) // ESC prefix indicates meta/alt/option\n  expect(ctrlOptionU?.shift).toBe(false)\n  expect(ctrlOptionU?.option).toBe(false) // Note: option flag is separate from meta\n  expect(ctrlOptionU?.sequence).toBe(\"\\u001b\\u0015\")\n  expect(ctrlOptionU?.raw).toBe(\"\\u001b\\u0015\")\n  expect(ctrlOptionU?.eventType).toBe(\"press\")\n\n  // Test other meta+ctrl combinations\n  const metaCtrlA = parseKeypress(\"\\x1b\\x01\") // ESC + Ctrl+A\n  expect(metaCtrlA?.name).toBe(\"a\")\n  expect(metaCtrlA?.ctrl).toBe(true)\n  expect(metaCtrlA?.meta).toBe(true)\n  expect(metaCtrlA?.shift).toBe(false)\n  expect(metaCtrlA?.option).toBe(false)\n\n  const metaCtrlZ = parseKeypress(\"\\x1b\\x1a\") // ESC + Ctrl+Z\n  expect(metaCtrlZ?.name).toBe(\"z\")\n  expect(metaCtrlZ?.ctrl).toBe(true)\n  expect(metaCtrlZ?.meta).toBe(true)\n  expect(metaCtrlZ?.shift).toBe(false)\n  expect(metaCtrlZ?.option).toBe(false)\n\n  // Test option+shift+u for comparison (this reportedly works)\n  // Option+Shift+U generates ESC + U (uppercase)\n  const optionShiftU = parseKeypress(\"\\x1bU\")!\n  expect(optionShiftU?.name).toBe(\"U\")\n  expect(optionShiftU?.meta).toBe(true)\n  expect(optionShiftU?.shift).toBe(true)\n  expect(optionShiftU?.ctrl).toBe(false)\n  expect(optionShiftU?.option).toBe(false)\n\n  // Edge case: ensure we don't match beyond \\x1a (26, which is Ctrl+Z)\n  const invalidCtrlSeq = parseKeypress(\"\\x1b\\x1b\") // ESC + ESC (not a ctrl char)\n  expect(invalidCtrlSeq?.name).toBe(\"escape\")\n  expect(invalidCtrlSeq?.meta).toBe(true)\n  expect(invalidCtrlSeq?.ctrl).toBe(false)\n\n  // Edge case: test boundary at \\x1a\n  const metaCtrlAtBoundary = parseKeypress(\"\\x1b\\x1a\") // ESC + Ctrl+Z\n  expect(metaCtrlAtBoundary?.name).toBe(\"z\")\n  expect(metaCtrlAtBoundary?.ctrl).toBe(true)\n  expect(metaCtrlAtBoundary?.meta).toBe(true)\n})\n\ntest(\"parseKeypress - filters out SGR mouse events\", () => {\n  const mouseDown = parseKeypress(\"\\x1b[<0;10;5M\")!\n  expect(mouseDown).toBeNull()\n\n  const mouseUp = parseKeypress(\"\\x1b[<0;10;5m\")!\n  expect(mouseUp).toBeNull()\n\n  const mouseDrag = parseKeypress(\"\\x1b[<32;15;8M\")!\n  expect(mouseDrag).toBeNull()\n\n  const mouseScroll = parseKeypress(\"\\x1b[<64;20;10M\")!\n  expect(mouseScroll).toBeNull()\n})\n\ntest(\"parseKeypress - filters out incomplete/partial SGR mouse sequences\", () => {\n  // These are flushed by the zig parser when a new ESC arrives mid-sequence\n  expect(parseKeypress(\"\\x1b[<35;\")).toBeNull()\n  expect(parseKeypress(\"\\x1b[<35;20\")).toBeNull()\n  expect(parseKeypress(\"\\x1b[<35;20;\")).toBeNull()\n  expect(parseKeypress(\"\\x1b[<35;20;5\")).toBeNull()\n  expect(parseKeypress(\"\\x1b[<\")).toBeNull()\n  expect(parseKeypress(\"\\x1b[<0\")).toBeNull()\n  expect(parseKeypress(\"\\x1b[<64;20;10\")).toBeNull()\n})\n\ntest(\"parseKeypress - filters out SGR mouse continuations without ESC\", () => {\n  // These can occur if ESC is flushed on timeout before the rest of the sequence arrives.\n  expect(parseKeypress(\"[<35;20;5m\")).toBeNull()\n  expect(parseKeypress(\"[<0;10;5M\")).toBeNull()\n  expect(parseKeypress(\"[<35;\")).toBeNull()\n  expect(parseKeypress(\"[<35;20\")).toBeNull()\n  expect(parseKeypress(\"[<35;20;\")).toBeNull()\n  expect(parseKeypress(\"[<\")).toBeNull()\n  expect(parseKeypress(\"[<64;20;10\")).toBeNull()\n})\n\ntest(\"parseKeypress - filters out basic mouse events\", () => {\n  const basicMouse = parseKeypress(\"\\x1b[M abc\")!\n  expect(basicMouse).toBeNull()\n})\n\ntest(\"parseKeypress - filters out terminal response sequences\", () => {\n  // Window/cell size reports - Format: ESC[4;height;width t or ESC[8;rows;cols t\n  // Example: resolution query response \"\\u001b[4;1782;3012t\"\n  const windowSize1 = parseKeypress(\"\\u001b[4;1782;3012t\")\n  expect(windowSize1).toBeNull()\n\n  const windowSize2 = parseKeypress(\"\\x1b[4;800;600t\")\n  expect(windowSize2).toBeNull()\n\n  const cellSize = parseKeypress(\"\\x1b[8;24;80t\")\n  expect(cellSize).toBeNull()\n\n  // Cursor position reports - Format: ESC[row;col R\n  // Response to DSR (Device Status Report) query\n  const cursorPos1 = parseKeypress(\"\\x1b[10;25R\")\n  expect(cursorPos1).toBeNull()\n\n  const cursorPos2 = parseKeypress(\"\\u001b[1;1R\")\n  expect(cursorPos2).toBeNull()\n\n  // Device Attributes (DA) responses - Format: ESC[?...c\n  // Response to terminal identification query\n  const deviceAttrs1 = parseKeypress(\"\\x1b[?1;2c\")\n  expect(deviceAttrs1).toBeNull()\n\n  const deviceAttrs2 = parseKeypress(\"\\x1b[?62;c\")\n  expect(deviceAttrs2).toBeNull()\n\n  const deviceAttrs3 = parseKeypress(\"\\x1b[?1;0;6;9;15c\")\n  expect(deviceAttrs3).toBeNull()\n\n  // Mode reports - Format: ESC[?...;...$y\n  // Response to DECRQM (Request Mode) query\n  const modeReport1 = parseKeypress(\"\\x1b[?1;2$y\")\n  expect(modeReport1).toBeNull()\n\n  const modeReport2 = parseKeypress(\"\\x1b[?25;1$y\")\n  expect(modeReport2).toBeNull()\n\n  // Focus events\n  const focusIn = parseKeypress(\"\\x1b[I\")\n  expect(focusIn).toBeNull()\n\n  const focusOut = parseKeypress(\"\\x1b[O\")\n  expect(focusOut).toBeNull()\n\n  // OSC (Operating System Command) responses - color/style queries\n  // Format: ESC]...ESC\\ or ESC]...BEL\n  // Must be complete sequences with proper terminators to be filtered\n  const oscResponse1 = parseKeypress(\"\\x1b]11;rgb:0000/0000/0000\\x1b\\\\\")\n  expect(oscResponse1).toBeNull()\n\n  const oscResponse2 = parseKeypress(\"\\x1b]10;rgb:ffff/ffff/ffff\\x07\")\n  expect(oscResponse2).toBeNull()\n\n  // Incomplete OSC sequences should NOT be filtered\n  // The stdin parser will either complete them or timeout and flush them\n  const incompleteOsc = parseKeypress(\"\\x1b]11;rgb:0000\")\n  expect(incompleteOsc).not.toBeNull()\n  expect(incompleteOsc?.name).toBe(\"\") // Unknown sequence, but not filtered\n})\n\ntest(\"parseKeypress - does not filter valid key sequences that might look similar\", () => {\n  // Make sure we don't accidentally filter out valid keys\n\n  // F1-F12 should still work (e.g., [11~, [24~)\n  const f1 = parseKeypress(\"\\x1b[11~\")\n  expect(f1).not.toBeNull()\n  expect(f1?.name).toBe(\"f1\")\n\n  const f12 = parseKeypress(\"\\x1b[24~\")\n  expect(f12).not.toBeNull()\n  expect(f12?.name).toBe(\"f12\")\n\n  // Arrow keys with O prefix should still work (SS3 sequences)\n  const arrowUp = parseKeypress(\"\\x1bOA\")\n  expect(arrowUp).not.toBeNull()\n  expect(arrowUp?.name).toBe(\"up\")\n\n  // Other SS3 sequences\n  const ss3Down = parseKeypress(\"\\x1bOB\")\n  expect(ss3Down).not.toBeNull()\n  expect(ss3Down?.name).toBe(\"down\")\n\n  // Note: ESC[O without a following character is filtered (focus out event)\n  const focusOutFiltered = parseKeypress(\"\\x1b[O\")\n  expect(focusOutFiltered).toBeNull()\n\n  // Standard arrow keys should still work\n  const arrowLeft = parseKeypress(\"\\x1b[D\")\n  expect(arrowLeft).not.toBeNull()\n  expect(arrowLeft?.name).toBe(\"left\")\n\n  // Modified keys should still work\n  const ctrlUp = parseKeypress(\"\\x1b[1;5A\")\n  expect(ctrlUp).not.toBeNull()\n  expect(ctrlUp?.name).toBe(\"up\")\n  expect(ctrlUp?.ctrl).toBe(true)\n\n  // Delete, insert, page up/down should still work\n  const deleteKey = parseKeypress(\"\\x1b[3~\")\n  expect(deleteKey).not.toBeNull()\n  expect(deleteKey?.name).toBe(\"delete\")\n\n  const insertKey = parseKeypress(\"\\x1b[2~\")\n  expect(insertKey).not.toBeNull()\n  expect(insertKey?.name).toBe(\"insert\")\n\n  const pageUp = parseKeypress(\"\\x1b[5~\")\n  expect(pageUp).not.toBeNull()\n  expect(pageUp?.name).toBe(\"pageup\")\n\n  // Kitty keyboard protocol sequences should still work\n  const kittyA = parseKeypress(\"\\x1b[97u\", { useKittyKeyboard: true })\n  expect(kittyA).not.toBeNull()\n  expect(kittyA?.name).toBe(\"a\")\n  expect(kittyA?.source).toBe(\"kitty\")\n\n  const kittyArrow = parseKeypress(\"\\x1b[57352u\", { useKittyKeyboard: true })\n  expect(kittyArrow).not.toBeNull()\n  expect(kittyArrow?.name).toBe(\"up\")\n  expect(kittyArrow?.source).toBe(\"kitty\")\n\n  // Bracketed paste markers should be filtered\n  // They're handled by KeyHandler before parseKeypress is called,\n  // but should return null for defense-in-depth\n  const pasteStart = parseKeypress(\"\\x1b[200~\")\n  expect(pasteStart).toBeNull()\n\n  const pasteEnd = parseKeypress(\"\\x1b[201~\")\n  expect(pasteEnd).toBeNull()\n\n  // Control characters should still work (including BEL which is Ctrl+G)\n  const bel = parseKeypress(\"\\x07\")\n  expect(bel).not.toBeNull()\n  expect(bel?.name).toBe(\"g\")\n  expect(bel?.ctrl).toBe(true)\n\n  const backspace = parseKeypress(\"\\b\")\n  expect(backspace).not.toBeNull()\n  expect(backspace?.name).toBe(\"backspace\")\n\n  const backspace2 = parseKeypress(\"\\x7f\")\n  expect(backspace2).not.toBeNull()\n  expect(backspace2?.name).toBe(\"backspace\")\n})\n\ntest(\"parseKeypress - source field is always 'raw' for non-Kitty parsing\", () => {\n  // Test various key types to ensure they all have source: \"raw\"\n  const letter = parseKeypress(\"a\")\n  expect(letter?.source).toBe(\"raw\")\n\n  const shiftLetter = parseKeypress(\"A\")\n  expect(shiftLetter?.source).toBe(\"raw\")\n\n  const number = parseKeypress(\"5\")\n  expect(number?.source).toBe(\"raw\")\n\n  const ctrlKey = parseKeypress(\"\\x01\") // Ctrl+A\n  expect(ctrlKey?.source).toBe(\"raw\")\n\n  const metaKey = parseKeypress(\"\\x1ba\") // Alt+A\n  expect(metaKey?.source).toBe(\"raw\")\n\n  const arrowKey = parseKeypress(\"\\x1b[A\") // Up arrow\n  expect(arrowKey?.source).toBe(\"raw\")\n\n  const functionKey = parseKeypress(\"\\x1bOP\") // F1\n  expect(functionKey?.source).toBe(\"raw\")\n\n  const modifiedArrow = parseKeypress(\"\\x1b[1;5A\") // Ctrl+Up\n  expect(modifiedArrow?.source).toBe(\"raw\")\n\n  const deleteKey = parseKeypress(\"\\x1b[3~\")\n  expect(deleteKey?.source).toBe(\"raw\")\n\n  const returnKey = parseKeypress(\"\\r\")\n  expect(returnKey?.source).toBe(\"raw\")\n\n  const tabKey = parseKeypress(\"\\t\")\n  expect(tabKey?.source).toBe(\"raw\")\n\n  const escapeKey = parseKeypress(\"\\x1b\")\n  expect(escapeKey?.source).toBe(\"raw\")\n})\n\ntest(\"parseKeypress - source field is 'kitty' when Kitty keyboard protocol is used\", () => {\n  // Test Kitty keyboard protocol sequences\n  const kittyA = parseKeypress(\"\\x1b[97u\", { useKittyKeyboard: true })\n  expect(kittyA?.source).toBe(\"kitty\")\n  expect(kittyA?.name).toBe(\"a\")\n\n  const kittyArrow = parseKeypress(\"\\x1b[57352u\", { useKittyKeyboard: true }) // Up arrow\n  expect(kittyArrow?.source).toBe(\"kitty\")\n  expect(kittyArrow?.name).toBe(\"up\")\n\n  const kittyF1 = parseKeypress(\"\\x1b[57364u\", { useKittyKeyboard: true }) // F1\n  expect(kittyF1?.source).toBe(\"kitty\")\n  expect(kittyF1?.name).toBe(\"f1\")\n\n  const kittyCtrlA = parseKeypress(\"\\x1b[97;5u\", { useKittyKeyboard: true }) // Ctrl+A\n  expect(kittyCtrlA?.source).toBe(\"kitty\")\n  expect(kittyCtrlA?.name).toBe(\"a\")\n  expect(kittyCtrlA?.ctrl).toBe(true)\n})\n\ntest(\"parseKeypress - fallback to raw parsing when Kitty option is enabled but sequence is not Kitty\", () => {\n  // Even with useKittyKeyboard enabled, non-Kitty sequences should use raw parsing\n  const normalArrow = parseKeypress(\"\\x1b[A\", { useKittyKeyboard: true })\n  expect(normalArrow?.source).toBe(\"raw\")\n  expect(normalArrow?.name).toBe(\"up\")\n\n  const normalLetter = parseKeypress(\"a\", { useKittyKeyboard: true })\n  expect(normalLetter?.source).toBe(\"raw\")\n  expect(normalLetter?.name).toBe(\"a\")\n\n  const normalCtrl = parseKeypress(\"\\x01\", { useKittyKeyboard: true })\n  expect(normalCtrl?.source).toBe(\"raw\")\n  expect(normalCtrl?.name).toBe(\"a\")\n  expect(normalCtrl?.ctrl).toBe(true)\n})\n\ntest(\"parseKeypress - modifyOtherKeys digits\", () => {\n  const shiftOne = parseKeypress(\"\\x1b[27;2;49~\")!\n  expect(shiftOne.name).toBe(\"1\")\n  expect(shiftOne.shift).toBe(true)\n  expect(shiftOne.ctrl).toBe(false)\n  expect(shiftOne.meta).toBe(false)\n  expect(shiftOne.option).toBe(false)\n  expect(shiftOne.number).toBe(true)\n  expect(shiftOne.sequence).toBe(\"1\")\n  expect(shiftOne.raw).toBe(\"\\x1b[27;2;49~\")\n  expect(shiftOne.eventType).toBe(\"press\")\n  expect(shiftOne.source).toBe(\"raw\")\n})\n\ntest(\"parseKeypress - modifyOtherKeys modified enter keys\", () => {\n  // Terminals with modifyOtherKeys mode enabled send special escape sequences for modified keys\n  // Format: CSI 27 ; modifier ; code ~ where code 13 is enter/return\n  // This is part of the CSI u protocol and is sent by xterm, iTerm2, Ghostty, etc.\n\n  // Shift+Enter: CSI 27;2;13~ (modifier 2 = shift bit 1)\n  const shiftEnter = parseKeypress(\"\\u001b[27;2;13~\")!\n  expect(shiftEnter.name).toBe(\"return\")\n  expect(shiftEnter.shift).toBe(true)\n  expect(shiftEnter.ctrl).toBe(false)\n  expect(shiftEnter.meta).toBe(false)\n  expect(shiftEnter.option).toBe(false)\n  expect(shiftEnter.sequence).toBe(\"\\u001b[27;2;13~\")\n  expect(shiftEnter.raw).toBe(\"\\u001b[27;2;13~\")\n  expect(shiftEnter.eventType).toBe(\"press\")\n  expect(shiftEnter.source).toBe(\"raw\")\n\n  // Test with \\x1b notation as well\n  const shiftEnter2 = parseKeypress(\"\\x1b[27;2;13~\")!\n  expect(shiftEnter2.name).toBe(\"return\")\n  expect(shiftEnter2.shift).toBe(true)\n  expect(shiftEnter2.ctrl).toBe(false)\n  expect(shiftEnter2.meta).toBe(false)\n  expect(shiftEnter2.option).toBe(false)\n\n  // Ctrl+Enter: CSI 27;5;13~ (modifier 5 = ctrl bit 4)\n  const ctrlEnter = parseKeypress(\"\\u001b[27;5;13~\")!\n  expect(ctrlEnter.name).toBe(\"return\")\n  expect(ctrlEnter.ctrl).toBe(true)\n  expect(ctrlEnter.shift).toBe(false)\n  expect(ctrlEnter.meta).toBe(false)\n  expect(ctrlEnter.option).toBe(false)\n  expect(ctrlEnter.sequence).toBe(\"\\u001b[27;5;13~\")\n  expect(ctrlEnter.raw).toBe(\"\\u001b[27;5;13~\")\n  expect(ctrlEnter.eventType).toBe(\"press\")\n  expect(ctrlEnter.source).toBe(\"raw\")\n\n  // Test with \\x1b notation\n  const ctrlEnter2 = parseKeypress(\"\\x1b[27;5;13~\")!\n  expect(ctrlEnter2.name).toBe(\"return\")\n  expect(ctrlEnter2.ctrl).toBe(true)\n  expect(ctrlEnter2.shift).toBe(false)\n  expect(ctrlEnter2.meta).toBe(false)\n  expect(ctrlEnter2.option).toBe(false)\n\n  // Alt/Option+Enter: CSI 27;3;13~ (modifier 3 = alt/option bit 2)\n  const altEnter = parseKeypress(\"\\u001b[27;3;13~\")!\n  expect(altEnter.name).toBe(\"return\")\n  expect(altEnter.meta).toBe(true)\n  expect(altEnter.option).toBe(true)\n  expect(altEnter.ctrl).toBe(false)\n  expect(altEnter.shift).toBe(false)\n  expect(altEnter.sequence).toBe(\"\\u001b[27;3;13~\")\n  expect(altEnter.raw).toBe(\"\\u001b[27;3;13~\")\n  expect(altEnter.eventType).toBe(\"press\")\n  expect(altEnter.source).toBe(\"raw\")\n\n  // Shift+Ctrl+Enter: CSI 27;6;13~ (modifier 6 = shift(1) + ctrl(4) = bits 5)\n  const shiftCtrlEnter = parseKeypress(\"\\u001b[27;6;13~\")!\n  expect(shiftCtrlEnter.name).toBe(\"return\")\n  expect(shiftCtrlEnter.shift).toBe(true)\n  expect(shiftCtrlEnter.ctrl).toBe(true)\n  expect(shiftCtrlEnter.meta).toBe(false)\n  expect(shiftCtrlEnter.option).toBe(false)\n  expect(shiftCtrlEnter.sequence).toBe(\"\\u001b[27;6;13~\")\n  expect(shiftCtrlEnter.raw).toBe(\"\\u001b[27;6;13~\")\n  expect(shiftCtrlEnter.eventType).toBe(\"press\")\n  expect(shiftCtrlEnter.source).toBe(\"raw\")\n\n  // Shift+Alt+Enter: CSI 27;4;13~ (modifier 4 = shift(1) + alt(2) = bits 3)\n  const shiftAltEnter = parseKeypress(\"\\u001b[27;4;13~\")!\n  expect(shiftAltEnter.name).toBe(\"return\")\n  expect(shiftAltEnter.shift).toBe(true)\n  expect(shiftAltEnter.meta).toBe(true)\n  expect(shiftAltEnter.option).toBe(true)\n  expect(shiftAltEnter.ctrl).toBe(false)\n  expect(shiftAltEnter.sequence).toBe(\"\\u001b[27;4;13~\")\n  expect(shiftAltEnter.raw).toBe(\"\\u001b[27;4;13~\")\n  expect(shiftAltEnter.eventType).toBe(\"press\")\n  expect(shiftAltEnter.source).toBe(\"raw\")\n\n  // Ctrl+Alt+Enter: CSI 27;7;13~ (modifier 7 = alt(2) + ctrl(4) = bits 6)\n  const ctrlAltEnter = parseKeypress(\"\\u001b[27;7;13~\")!\n  expect(ctrlAltEnter.name).toBe(\"return\")\n  expect(ctrlAltEnter.ctrl).toBe(true)\n  expect(ctrlAltEnter.meta).toBe(true)\n  expect(ctrlAltEnter.option).toBe(true)\n  expect(ctrlAltEnter.shift).toBe(false)\n  expect(ctrlAltEnter.sequence).toBe(\"\\u001b[27;7;13~\")\n  expect(ctrlAltEnter.raw).toBe(\"\\u001b[27;7;13~\")\n  expect(ctrlAltEnter.eventType).toBe(\"press\")\n  expect(ctrlAltEnter.source).toBe(\"raw\")\n\n  // Shift+Ctrl+Alt+Enter: CSI 27;8;13~ (modifier 8 = shift(1) + alt(2) + ctrl(4) = bits 7)\n  const allModsEnter = parseKeypress(\"\\u001b[27;8;13~\")!\n  expect(allModsEnter.name).toBe(\"return\")\n  expect(allModsEnter.shift).toBe(true)\n  expect(allModsEnter.ctrl).toBe(true)\n  expect(allModsEnter.meta).toBe(true)\n  expect(allModsEnter.option).toBe(true)\n  expect(allModsEnter.sequence).toBe(\"\\u001b[27;8;13~\")\n  expect(allModsEnter.raw).toBe(\"\\u001b[27;8;13~\")\n  expect(allModsEnter.eventType).toBe(\"press\")\n  expect(allModsEnter.source).toBe(\"raw\")\n})\n\ntest(\"parseKeypress - modifyOtherKeys modified escape keys\", () => {\n  // Terminals with modifyOtherKeys mode enabled also send modified escape key sequences\n  // Format: CSI 27 ; modifier ; 27 ~ where code 27 is escape\n\n  // Ctrl+Escape: CSI 27;5;27~ (modifier 5 = ctrl bit 4)\n  const ctrlEscape = parseKeypress(\"\\u001b[27;5;27~\")!\n  expect(ctrlEscape.name).toBe(\"escape\")\n  expect(ctrlEscape.ctrl).toBe(true)\n  expect(ctrlEscape.shift).toBe(false)\n  expect(ctrlEscape.meta).toBe(false)\n  expect(ctrlEscape.option).toBe(false)\n  expect(ctrlEscape.sequence).toBe(\"\\u001b[27;5;27~\")\n  expect(ctrlEscape.raw).toBe(\"\\u001b[27;5;27~\")\n  expect(ctrlEscape.eventType).toBe(\"press\")\n  expect(ctrlEscape.source).toBe(\"raw\")\n\n  // Test with \\x1b notation as well\n  const ctrlEscape2 = parseKeypress(\"\\x1b[27;5;27~\")!\n  expect(ctrlEscape2.name).toBe(\"escape\")\n  expect(ctrlEscape2.ctrl).toBe(true)\n  expect(ctrlEscape2.shift).toBe(false)\n  expect(ctrlEscape2.meta).toBe(false)\n  expect(ctrlEscape2.option).toBe(false)\n\n  // Shift+Escape: CSI 27;2;27~ (modifier 2 = shift bit 1)\n  const shiftEscape = parseKeypress(\"\\u001b[27;2;27~\")!\n  expect(shiftEscape.name).toBe(\"escape\")\n  expect(shiftEscape.shift).toBe(true)\n  expect(shiftEscape.ctrl).toBe(false)\n  expect(shiftEscape.meta).toBe(false)\n  expect(shiftEscape.option).toBe(false)\n  expect(shiftEscape.sequence).toBe(\"\\u001b[27;2;27~\")\n  expect(shiftEscape.raw).toBe(\"\\u001b[27;2;27~\")\n  expect(shiftEscape.eventType).toBe(\"press\")\n  expect(shiftEscape.source).toBe(\"raw\")\n\n  // Alt+Escape: CSI 27;3;27~ (modifier 3 = alt/option bit 2)\n  const altEscape = parseKeypress(\"\\u001b[27;3;27~\")!\n  expect(altEscape.name).toBe(\"escape\")\n  expect(altEscape.meta).toBe(true)\n  expect(altEscape.option).toBe(true)\n  expect(altEscape.ctrl).toBe(false)\n  expect(altEscape.shift).toBe(false)\n  expect(altEscape.sequence).toBe(\"\\u001b[27;3;27~\")\n  expect(altEscape.raw).toBe(\"\\u001b[27;3;27~\")\n  expect(altEscape.eventType).toBe(\"press\")\n  expect(altEscape.source).toBe(\"raw\")\n\n  // Shift+Ctrl+Escape: CSI 27;6;27~ (modifier 6 = shift(1) + ctrl(4) = bits 5)\n  const shiftCtrlEscape = parseKeypress(\"\\u001b[27;6;27~\")!\n  expect(shiftCtrlEscape.name).toBe(\"escape\")\n  expect(shiftCtrlEscape.shift).toBe(true)\n  expect(shiftCtrlEscape.ctrl).toBe(true)\n  expect(shiftCtrlEscape.meta).toBe(false)\n  expect(shiftCtrlEscape.option).toBe(false)\n  expect(shiftCtrlEscape.sequence).toBe(\"\\u001b[27;6;27~\")\n  expect(shiftCtrlEscape.raw).toBe(\"\\u001b[27;6;27~\")\n  expect(shiftCtrlEscape.eventType).toBe(\"press\")\n  expect(shiftCtrlEscape.source).toBe(\"raw\")\n})\n\ntest(\"parseKeypress - modifyOtherKeys modified tab, space, and backspace keys\", () => {\n  // Tab key: charcode 9\n  const ctrlTab = parseKeypress(\"\\u001b[27;5;9~\")!\n  expect(ctrlTab.name).toBe(\"tab\")\n  expect(ctrlTab.ctrl).toBe(true)\n  expect(ctrlTab.shift).toBe(false)\n  expect(ctrlTab.meta).toBe(false)\n  expect(ctrlTab.option).toBe(false)\n\n  const shiftTab = parseKeypress(\"\\u001b[27;2;9~\")!\n  expect(shiftTab.name).toBe(\"tab\")\n  expect(shiftTab.shift).toBe(true)\n  expect(shiftTab.ctrl).toBe(false)\n  expect(shiftTab.meta).toBe(false)\n  expect(shiftTab.option).toBe(false)\n\n  // Space key: charcode 32\n  const ctrlSpace = parseKeypress(\"\\u001b[27;5;32~\")!\n  expect(ctrlSpace.name).toBe(\"space\")\n  expect(ctrlSpace.ctrl).toBe(true)\n  expect(ctrlSpace.shift).toBe(false)\n  expect(ctrlSpace.meta).toBe(false)\n  expect(ctrlSpace.option).toBe(false)\n\n  const shiftSpace = parseKeypress(\"\\u001b[27;2;32~\")!\n  expect(shiftSpace.name).toBe(\"space\")\n  expect(shiftSpace.shift).toBe(true)\n  expect(shiftSpace.ctrl).toBe(false)\n  expect(shiftSpace.meta).toBe(false)\n  expect(shiftSpace.option).toBe(false)\n\n  const altSpace = parseKeypress(\"\\u001b[27;3;32~\")!\n  expect(altSpace.name).toBe(\"space\")\n  expect(altSpace.meta).toBe(true)\n  expect(altSpace.option).toBe(true)\n  expect(altSpace.ctrl).toBe(false)\n  expect(altSpace.shift).toBe(false)\n\n  // Backspace key: charcode 127 (or 8)\n  const ctrlBackspace = parseKeypress(\"\\u001b[27;5;127~\")!\n  expect(ctrlBackspace.name).toBe(\"backspace\")\n  expect(ctrlBackspace.ctrl).toBe(true)\n  expect(ctrlBackspace.shift).toBe(false)\n  expect(ctrlBackspace.meta).toBe(false)\n  expect(ctrlBackspace.option).toBe(false)\n\n  const shiftBackspace = parseKeypress(\"\\u001b[27;2;127~\")!\n  expect(shiftBackspace.name).toBe(\"backspace\")\n  expect(shiftBackspace.shift).toBe(true)\n  expect(shiftBackspace.ctrl).toBe(false)\n  expect(shiftBackspace.meta).toBe(false)\n  expect(shiftBackspace.option).toBe(false)\n\n  // Test charcode 8 variant\n  const ctrlBackspace8 = parseKeypress(\"\\u001b[27;5;8~\")!\n  expect(ctrlBackspace8.name).toBe(\"backspace\")\n  expect(ctrlBackspace8.ctrl).toBe(true)\n})\n\ntest(\"parseKeypress - meta+arrow keys with uppercase F and B (old style)\", () => {\n  // Some terminals send ESC followed by uppercase F/B for meta+arrow keys\n  // ONLY uppercase F and B map to arrow keys (not P/N which require actual shift)\n  // Lowercase f/b are just regular meta+letter combinations\n\n  // Meta+Right (uppercase F)\n  const metaRight = parseKeypress(\"\\u001BF\")!\n  expect(metaRight.name).toBe(\"right\")\n  expect(metaRight.meta).toBe(true)\n  expect(metaRight.shift).toBe(false)\n  expect(metaRight.ctrl).toBe(false)\n  expect(metaRight.option).toBe(false)\n  expect(metaRight.sequence).toBe(\"\\u001BF\")\n  expect(metaRight.raw).toBe(\"\\u001BF\")\n\n  // Meta+Left (uppercase B)\n  const metaLeft = parseKeypress(\"\\u001BB\")!\n  expect(metaLeft.name).toBe(\"left\")\n  expect(metaLeft.meta).toBe(true)\n  expect(metaLeft.shift).toBe(false)\n  expect(metaLeft.ctrl).toBe(false)\n  expect(metaLeft.option).toBe(false)\n  expect(metaLeft.sequence).toBe(\"\\u001BB\")\n  expect(metaLeft.raw).toBe(\"\\u001BB\")\n\n  // Uppercase P should be meta+shift+p (not arrow up)\n  const metaShiftP = parseKeypress(\"\\u001BP\")!\n  expect(metaShiftP.name).toBe(\"P\")\n  expect(metaShiftP.meta).toBe(true)\n  expect(metaShiftP.shift).toBe(true)\n  expect(metaShiftP.ctrl).toBe(false)\n  expect(metaShiftP.option).toBe(false)\n\n  // Uppercase N should be meta+shift+n (not arrow down)\n  const metaShiftN = parseKeypress(\"\\u001BN\")!\n  expect(metaShiftN.name).toBe(\"N\")\n  expect(metaShiftN.meta).toBe(true)\n  expect(metaShiftN.shift).toBe(true)\n  expect(metaShiftN.ctrl).toBe(false)\n  expect(metaShiftN.option).toBe(false)\n\n  // Lowercase versions should NOT map to arrow keys - they're just meta+letter\n  // Meta+f (lowercase f) should be just meta+f, NOT meta+right\n  const metaF = parseKeypress(\"\\u001Bf\")!\n  expect(metaF.name).toBe(\"f\")\n  expect(metaF.meta).toBe(true)\n  expect(metaF.shift).toBe(false)\n  expect(metaF.ctrl).toBe(false)\n\n  // Meta+b (lowercase b) should be just meta+b, NOT meta+left\n  const metaB = parseKeypress(\"\\u001Bb\")!\n  expect(metaB.name).toBe(\"b\")\n  expect(metaB.meta).toBe(true)\n  expect(metaB.shift).toBe(false)\n  expect(metaB.ctrl).toBe(false)\n})\n\ntest(\"parseKeypress - double ESC preserves meta state when fn-key modifiers are parsed\", () => {\n  const metaUp = parseKeypress(\"\\x1b\\x1b[A\")!\n  expect(metaUp.name).toBe(\"up\")\n  expect(metaUp.meta).toBe(true)\n  expect(metaUp.option).toBe(true)\n  expect(metaUp.ctrl).toBe(false)\n  expect(metaUp.shift).toBe(false)\n\n  const metaCtrlUp = parseKeypress(\"\\x1b\\x1b[1;5A\")!\n  expect(metaCtrlUp.name).toBe(\"up\")\n  expect(metaCtrlUp.meta).toBe(true)\n  expect(metaCtrlUp.option).toBe(true)\n  expect(metaCtrlUp.ctrl).toBe(true)\n  expect(metaCtrlUp.shift).toBe(false)\n})\n\ntest(\"parseKeypress - preserves printable Unicode characters including non-BMP\", () => {\n  for (const char of [\"é\", \"中\", \"👍\"]) {\n    const key = parseKeypress(char)!\n    expect(key.name).toBe(char)\n    expect(key.raw).toBe(char)\n    expect(key.sequence).toBe(char)\n    expect(key.meta).toBe(false)\n    expect(key.ctrl).toBe(false)\n    expect(key.shift).toBe(false)\n  }\n})\n"
  },
  {
    "path": "packages/core/src/lib/parse.keypress.ts",
    "content": "// Copied from https://github.com/enquirer/enquirer/blob/36785f3399a41cd61e9d28d1eb9c2fcd73d69b4c/lib/keypress.js\nimport { Buffer } from \"node:buffer\"\nimport { parseKittyKeyboard } from \"./parse.keypress-kitty.js\"\n\nconst metaKeyCodeRe = /^(?:\\x1b)([a-zA-Z0-9])$/\n\nconst fnKeyRe = /^(?:\\x1b+)(O|N|\\[|\\[\\[)(?:(\\d+)(?:;(\\d+))?([~^$])|(?:1;)?(\\d+)?([a-zA-Z]))/\n\nconst keyName: Record<string, string> = {\n  /* xterm/gnome ESC O letter */\n  OP: \"f1\",\n  OQ: \"f2\",\n  OR: \"f3\",\n  OS: \"f4\",\n  /* xterm/rxvt ESC [ number ~ */\n  \"[11~\": \"f1\",\n  \"[12~\": \"f2\",\n  \"[13~\": \"f3\",\n  \"[14~\": \"f4\",\n  /* from Cygwin and used in libuv */\n  \"[[A\": \"f1\",\n  \"[[B\": \"f2\",\n  \"[[C\": \"f3\",\n  \"[[D\": \"f4\",\n  \"[[E\": \"f5\",\n  /* common */\n  \"[15~\": \"f5\",\n  \"[17~\": \"f6\",\n  \"[18~\": \"f7\",\n  \"[19~\": \"f8\",\n  \"[20~\": \"f9\",\n  \"[21~\": \"f10\",\n  \"[23~\": \"f11\",\n  \"[24~\": \"f12\",\n  \"[29~\": \"menu\",\n  \"[57427~\": \"clear\",\n  /* xterm ESC [ letter */\n  \"[A\": \"up\",\n  \"[B\": \"down\",\n  \"[C\": \"right\",\n  \"[D\": \"left\",\n  \"[E\": \"clear\",\n  \"[F\": \"end\",\n  \"[H\": \"home\",\n  \"[P\": \"f1\",\n  \"[Q\": \"f2\",\n  \"[S\": \"f4\",\n  /* xterm/gnome ESC O letter */\n  OA: \"up\",\n  OB: \"down\",\n  OC: \"right\",\n  OD: \"left\",\n  OE: \"clear\",\n  OF: \"end\",\n  OH: \"home\",\n  /* xterm/rxvt ESC [ number ~ */\n  \"[1~\": \"home\",\n  \"[2~\": \"insert\",\n  \"[3~\": \"delete\",\n  \"[4~\": \"end\",\n  \"[5~\": \"pageup\",\n  \"[6~\": \"pagedown\",\n  /* putty */\n  \"[[5~\": \"pageup\",\n  \"[[6~\": \"pagedown\",\n  /* rxvt */\n  \"[7~\": \"home\",\n  \"[8~\": \"end\",\n  /* rxvt keys with modifiers */\n  \"[a\": \"up\",\n  \"[b\": \"down\",\n  \"[c\": \"right\",\n  \"[d\": \"left\",\n  \"[e\": \"clear\",\n  /* option + arrow keys (old style) */\n  f: \"right\",\n  b: \"left\",\n  p: \"up\",\n  n: \"down\",\n\n  \"[2$\": \"insert\",\n  \"[3$\": \"delete\",\n  \"[5$\": \"pageup\",\n  \"[6$\": \"pagedown\",\n  \"[7$\": \"home\",\n  \"[8$\": \"end\",\n\n  Oa: \"up\",\n  Ob: \"down\",\n  Oc: \"right\",\n  Od: \"left\",\n  Oe: \"clear\",\n\n  \"[2^\": \"insert\",\n  \"[3^\": \"delete\",\n  \"[5^\": \"pageup\",\n  \"[6^\": \"pagedown\",\n  \"[7^\": \"home\",\n  \"[8^\": \"end\",\n  /* misc. */\n  \"[Z\": \"tab\",\n}\n\nexport const nonAlphanumericKeys = [...Object.values(keyName), \"backspace\"]\n\nconst isShiftKey = (code: string) => {\n  return [\"[a\", \"[b\", \"[c\", \"[d\", \"[e\", \"[2$\", \"[3$\", \"[5$\", \"[6$\", \"[7$\", \"[8$\", \"[Z\"].includes(code)\n}\n\nconst isCtrlKey = (code: string) => {\n  return [\"Oa\", \"Ob\", \"Oc\", \"Od\", \"Oe\", \"[2^\", \"[3^\", \"[5^\", \"[6^\", \"[7^\", \"[8^\"].includes(code)\n}\n\nexport type KeyEventType = \"press\" | \"repeat\" | \"release\"\n\nexport interface ParsedKey {\n  name: string\n  ctrl: boolean\n  meta: boolean\n  shift: boolean\n  option: boolean\n  sequence: string\n  number: boolean\n  raw: string\n  eventType: KeyEventType\n  source: \"raw\" | \"kitty\"\n  code?: string\n  super?: boolean\n  hyper?: boolean\n  capsLock?: boolean\n  numLock?: boolean\n  baseCode?: number\n  repeated?: boolean\n}\n\nexport type ParseKeypressOptions = {\n  useKittyKeyboard?: boolean\n}\n\nconst modifyOtherKeysRe = /^\\x1b\\[27;(\\d+);(\\d+)~$/\n\nexport const parseKeypress = (s: Buffer | string = \"\", options: ParseKeypressOptions = {}): ParsedKey | null => {\n  let parts\n\n  if (Buffer.isBuffer(s)) {\n    if (s[0]! > 127 && s[1] === undefined) {\n      ;(s[0] as unknown as number) -= 128\n      s = \"\\x1b\" + String(s)\n    } else {\n      s = String(s)\n    }\n  } else if (s !== undefined && typeof s !== \"string\") {\n    s = String(s)\n  } else if (!s) {\n    s = \"\"\n  }\n\n  // Filter out mouse events (SGR and basic)\n  // Complete SGR mouse: ESC[<btn;x;yM or ESC[<btn;x;ym\n  if (/^\\x1b\\[<\\d+;\\d+;\\d+[Mm]$/.test(s)) {\n    return null\n  }\n  // Complete SGR mouse continuation without leading ESC. This can occur when\n  // ESC was flushed separately on timeout and the rest of the sequence arrived later.\n  if (/^\\[<\\d+;\\d+;\\d+[Mm]$/.test(s)) {\n    return null\n  }\n  // Incomplete/partial SGR mouse sequences (flushed by the zig parser when\n  // a new ESC arrives before the sequence is complete). These start with\n  // ESC[< followed by digits/semicolons but lack the terminal M/m.\n  if (/^\\x1b\\[<[\\d;]*$/.test(s)) {\n    return null\n  }\n  // Incomplete/partial SGR mouse continuations without ESC.\n  if (/^\\[<[\\d;]*$/.test(s)) {\n    return null\n  }\n  if (s.startsWith(\"\\x1b[M\") && s.length >= 6) {\n    return null\n  }\n\n  // Filter out terminal response sequences (not keyboard events)\n  // These are responses to terminal queries and should not be treated as key presses\n\n  // Window/cell size reports: ESC[4;height;width t or ESC[8;rows;cols t\n  if (/^\\x1b\\[\\d+;\\d+;\\d+t$/.test(s)) {\n    return null\n  }\n\n  // Cursor position reports (DSR): ESC[row;col R\n  if (/^\\x1b\\[\\d+;\\d+R$/.test(s)) {\n    return null\n  }\n\n  // Device Attributes (DA) responses: ESC[?...c\n  if (/^\\x1b\\[\\?[\\d;]+c$/.test(s)) {\n    return null\n  }\n\n  // Mode reports: ESC[?...;...$y\n  if (/^\\x1b\\[\\?[\\d;]+\\$y$/.test(s)) {\n    return null\n  }\n\n  // Focus events: ESC[I (focus in), ESC[O (focus out)\n  // Note: ESC[O is also used for SS3 sequences (like arrow keys), but those have a character after O\n  if (s === \"\\x1b[I\" || s === \"\\x1b[O\") {\n    return null\n  }\n\n  // OSC (Operating System Command) responses: ESC]...ESC\\ or ESC]...BEL\n  if (/^\\x1b\\][\\d;].*(\\x1b\\\\|\\x07)$/.test(s)) {\n    return null\n  }\n\n  // Bracketed paste mode markers: ESC[200~ (start), ESC[201~ (end)\n  if (s === \"\\x1b[200~\" || s === \"\\x1b[201~\") {\n    return null\n  }\n\n  const key: ParsedKey = {\n    name: \"\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: s,\n    raw: s,\n    eventType: \"press\",\n    source: \"raw\",\n  }\n\n  key.sequence = key.sequence || s || key.name\n\n  // Check for Kitty keyboard protocol if enabled\n  if (options.useKittyKeyboard) {\n    const kittyResult = parseKittyKeyboard(s)\n    if (kittyResult) {\n      return kittyResult\n    }\n  }\n\n  // Check for modifyOtherKeys sequences (CSI u protocol variant)\n  // Format: CSI 27 ; modifier ; code ~\n  // This is sent by terminals (xterm, iTerm2, Ghostty, etc.) with modifyOtherKeys mode enabled\n  // to encode modified versions of keys that don't normally have modifier variants\n  // Examples: CSI 27;2;13~ (shift+enter), CSI 27;5;13~ (ctrl+enter), CSI 27;5;27~ (ctrl+escape)\n  const modifyOtherKeysMatch = modifyOtherKeysRe.exec(s)\n  if (modifyOtherKeysMatch) {\n    const modifier = parseInt(modifyOtherKeysMatch[1]!, 10) - 1\n    const charCode = parseInt(modifyOtherKeysMatch[2]!, 10)\n\n    key.ctrl = !!(modifier & 4)\n    key.meta = !!(modifier & 2) // Alt/Option sets meta\n    key.shift = !!(modifier & 1)\n    key.option = !!(modifier & 2)\n    key.super = !!(modifier & 8)\n    key.hyper = !!(modifier & 16)\n\n    // Handle common keys by their ASCII codes\n    if (charCode === 13) {\n      key.name = \"return\"\n    } else if (charCode === 27) {\n      key.name = \"escape\"\n    } else if (charCode === 9) {\n      key.name = \"tab\"\n    } else if (charCode === 32) {\n      key.name = \"space\"\n    } else if (charCode === 127 || charCode === 8) {\n      key.name = \"backspace\"\n    } else {\n      // For other character codes, use the character itself\n      const char = String.fromCharCode(charCode)\n      key.name = char\n      key.sequence = char\n      if (charCode >= 48 && charCode <= 57) {\n        key.number = true\n      }\n    }\n\n    return key\n  }\n\n  if (s === \"\\r\" || s === \"\\x1b\\r\") {\n    // carriage return\n    key.name = \"return\"\n    key.meta = s.length === 2\n  } else if (s === \"\\n\" || s === \"\\x1b\\n\") {\n    // linefeed\n    key.name = \"linefeed\"\n    key.meta = s.length === 2\n  } else if (s === \"\\t\") {\n    // tab\n    key.name = \"tab\"\n  } else if (s === \"\\b\" || s === \"\\x1b\\b\" || s === \"\\x7f\" || s === \"\\x1b\\x7f\") {\n    // backspace or ctrl+h\n    // On OSX, \\x7f is also backspace\n    key.name = \"backspace\"\n    key.meta = s.charAt(0) === \"\\x1b\"\n  } else if (s === \"\\x1b\" || s === \"\\x1b\\x1b\") {\n    // escape key\n    key.name = \"escape\"\n    key.meta = s.length === 2\n  } else if (s === \" \" || s === \"\\x1b \") {\n    key.name = \"space\"\n    key.meta = s.length === 2\n  } else if (s === \"\\x00\") {\n    // ctrl+space\n    key.name = \"space\"\n    key.ctrl = true\n  } else if (s.length === 1 && s <= \"\\x1a\") {\n    // ctrl+letter\n    key.name = String.fromCharCode(s.charCodeAt(0) + \"a\".charCodeAt(0) - 1)\n    key.ctrl = true\n  } else if (s.length === 1 && s >= \"0\" && s <= \"9\") {\n    // number - keep the actual number character for vim commands\n    key.name = s\n    key.number = true\n  } else if (s.length === 1 && s >= \"a\" && s <= \"z\") {\n    // lowercase letter\n    key.name = s\n  } else if (s.length === 1 && s >= \"A\" && s <= \"Z\") {\n    // shift+letter\n    key.name = s.toLowerCase()\n    key.shift = true\n  } else if (s.length === 1 || (s.length === 2 && s.codePointAt(0)! > 0xffff)) {\n    // Single character (including emoji/surrogate pairs above BMP)\n    key.name = s\n  } else if ((parts = metaKeyCodeRe.exec(s))) {\n    // meta+character key\n    key.meta = true\n    const char = parts[1]!\n    const isUpperCase = /^[A-Z]$/.test(char)\n\n    // Check if uppercase F or B map to arrow keys (old terminal style)\n    if (char === \"F\") {\n      key.name = \"right\"\n    } else if (char === \"B\") {\n      key.name = \"left\"\n    } else if (isUpperCase) {\n      key.shift = true\n      key.name = char\n    } else {\n      key.name = char\n    }\n  } else if (s.length === 2 && s[0] === \"\\x1b\" && s[1]! <= \"\\x1a\") {\n    // meta+ctrl+letter (ESC + control character)\n    key.meta = true\n    key.ctrl = true\n    key.name = String.fromCharCode(s.charCodeAt(1) + \"a\".charCodeAt(0) - 1)\n  } else if ((parts = fnKeyRe.exec(s))) {\n    const segs = [...s]\n\n    if (segs[0] === \"\\u001b\" && segs[1] === \"\\u001b\") {\n      key.option = true\n      key.meta = true\n    }\n\n    // ansi escape sequence\n    // reassemble the key code leaving out leading \\x1b's,\n    // the modifier key bitflag and any meaningless \"1;\" sequence\n    const code = [parts[1], parts[2], parts[4], parts[6]].filter(Boolean).join(\"\")\n\n    const modifier = parseInt(parts[3] || parts[5] || \"1\", 10) - 1\n\n    // Parse the key modifier\n    // Terminal modifier bits: 1=Shift, 2=Alt/Option, 4=Ctrl, 8=Super, 16=Hyper\n    // Note: meta flag is set for Alt/Option (bit 2)\n    key.ctrl = key.ctrl || !!(modifier & 4)\n    key.meta = key.meta || !!(modifier & 2)\n    key.shift = key.shift || !!(modifier & 1)\n    key.option = key.option || !!(modifier & 2)\n    key.super = !!(modifier & 8)\n    key.hyper = !!(modifier & 16)\n    key.code = code\n\n    const keyNameResult = keyName[code]\n    if (keyNameResult) {\n      key.name = keyNameResult\n      key.shift = isShiftKey(code) || key.shift\n      key.ctrl = isCtrlKey(code) || key.ctrl\n    } else {\n      // If we matched the regex but didn't find a valid key name,\n      // reset the key to default state (unknown sequence)\n      key.name = \"\"\n      key.code = undefined\n    }\n  } else if (s === \"\\x1b[3~\") {\n    // delete key\n    key.name = \"delete\"\n    key.meta = false\n    key.code = \"[3~\"\n  }\n\n  return key\n}\n"
  },
  {
    "path": "packages/core/src/lib/parse.mouse.test.ts",
    "content": "import { describe, test, expect, beforeEach } from \"bun:test\"\nimport { MouseParser, type RawMouseEvent } from \"./parse.mouse\"\n\n// Encode a basic/X10 mouse event: ESC [ M Cb Cx Cy\n// buttonByte is the logical value (before the +32 wire offset), x/y are 0-based.\n// Returns a latin1 Buffer so charCodeAt() round-trips correctly even for high\n// coordinates (>= 95 i.e. raw byte >= 128).\nfunction encodeBasic(buttonByte: number, x: number, y: number): Buffer {\n  const cb = buttonByte + 32\n  const cx = x + 33\n  const cy = y + 33\n  return Buffer.from([0x1b, 0x5b, 0x4d, cb, cx, cy]) // ESC [ M cb cx cy\n}\n\n// Encode an SGR mouse event: ESC [ < buttonCode ; x+1 ; y+1 M/m\nfunction encodeSGR(buttonCode: number, x: number, y: number, press: boolean): Buffer {\n  const suffix = press ? \"M\" : \"m\"\n  return Buffer.from(`\\x1b[<${buttonCode};${x + 1};${y + 1}${suffix}`)\n}\n\ndescribe(\"MouseParser basic (X10) mode\", () => {\n  let parser: MouseParser\n\n  beforeEach(() => {\n    parser = new MouseParser()\n  })\n\n  describe(\"press and release\", () => {\n    test(\"left button down\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(0, 10, 5))\n      expect(e).toMatchObject({ type: \"down\", button: 0, x: 10, y: 5 })\n    })\n\n    test(\"middle button down\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(1, 10, 5))\n      expect(e).toMatchObject({ type: \"down\", button: 1, x: 10, y: 5 })\n    })\n\n    test(\"right button down\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(2, 10, 5))\n      expect(e).toMatchObject({ type: \"down\", button: 2, x: 10, y: 5 })\n    })\n\n    test(\"button release (button byte 3)\", () => {\n      // In X10, release always reports button=3 regardless of which was released\n      const e = parser.parseMouseEvent(encodeBasic(3, 10, 5))\n      expect(e).toMatchObject({ type: \"up\", x: 10, y: 5 })\n    })\n  })\n\n  describe(\"scroll\", () => {\n    test(\"scroll up (64)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(64, 10, 5))\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"up\", delta: 1 } })\n    })\n\n    test(\"scroll down (65)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(65, 10, 5))\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"down\", delta: 1 } })\n    })\n\n    test(\"scroll left (66)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(66, 10, 5))\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"left\", delta: 1 } })\n    })\n\n    test(\"scroll right (67)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(67, 10, 5))\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"right\", delta: 1 } })\n    })\n\n    test(\"scroll with shift modifier (68 = 64 + 4)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(68, 10, 5))\n      expect(e).toMatchObject({\n        type: \"scroll\",\n        scroll: { direction: \"up\", delta: 1 },\n        modifiers: { shift: true, alt: false, ctrl: false },\n      })\n    })\n  })\n\n  describe(\"modifiers\", () => {\n    test(\"shift (bit 2)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(4, 10, 5))!\n      expect(e.modifiers).toEqual({ shift: true, alt: false, ctrl: false })\n    })\n\n    test(\"alt / meta (bit 3)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(8, 10, 5))!\n      expect(e.modifiers).toEqual({ shift: false, alt: true, ctrl: false })\n    })\n\n    test(\"ctrl (bit 4)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(16, 10, 5))!\n      expect(e.modifiers).toEqual({ shift: false, alt: false, ctrl: true })\n    })\n\n    test(\"all modifiers combined (4+8+16 = 28)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(28, 10, 5))!\n      expect(e.modifiers).toEqual({ shift: true, alt: true, ctrl: true })\n    })\n\n    test(\"modifiers preserve button identity\", () => {\n      // right-click + ctrl = 2 + 16 = 18\n      const e = parser.parseMouseEvent(encodeBasic(18, 10, 5))!\n      expect(e.type).toBe(\"down\")\n      expect(e.button).toBe(2)\n      expect(e.modifiers.ctrl).toBe(true)\n    })\n  })\n\n  describe(\"motion detection\", () => {\n    test(\"move without button: byte 35 (32|3) → 'move', not 'up'\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(35, 10, 5))!\n      expect(e.type).toBe(\"move\")\n      expect(e.x).toBe(10)\n      expect(e.y).toBe(5)\n    })\n\n    test(\"drag with left button: byte 32 (32|0) → not 'down'\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(32, 10, 5))!\n      expect(e.type).toBe(\"move\") // parser says \"move\"; renderer promotes to \"drag\"\n      expect(e.type).not.toBe(\"down\")\n    })\n\n    test(\"drag with middle button: byte 33 (32|1) → not 'down'\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(33, 10, 5))!\n      expect(e.type).not.toBe(\"down\")\n    })\n\n    test(\"drag with right button: byte 34 (32|2) → not 'down'\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(34, 10, 5))!\n      expect(e.type).not.toBe(\"down\")\n    })\n\n    test(\"motion events are never classified as scroll\", () => {\n      for (const bb of [32, 33, 34, 35]) {\n        const e = parser.parseMouseEvent(encodeBasic(bb, 10, 5))!\n        expect(e.type).not.toBe(\"scroll\")\n      }\n    })\n\n    test(\"motion + shift modifier: byte 39 (32|3|4) → 'move'\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(39, 10, 5))!\n      expect(e.type).toBe(\"move\")\n      expect(e.modifiers.shift).toBe(true)\n    })\n\n    test(\"motion + ctrl: byte 51 (32|3|16) → 'move' with ctrl\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(51, 10, 5))!\n      expect(e.type).toBe(\"move\")\n      expect(e.modifiers.ctrl).toBe(true)\n    })\n\n    test(\"motion + all modifiers: byte 63 (32|3|4|8|16) → 'move'\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(63, 10, 5))!\n      expect(e.type).toBe(\"move\")\n      expect(e.modifiers).toEqual({ shift: true, alt: true, ctrl: true })\n    })\n\n    test(\"motion bit takes priority over scroll bit: byte 96 (64|32) → 'move'\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(96, 10, 5))!\n      expect(e.type).toBe(\"move\")\n    })\n\n    test(\"release without motion bit is still 'up'\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(3, 10, 5))!\n      expect(e.type).toBe(\"up\")\n    })\n  })\n\n  describe(\"coordinates\", () => {\n    test(\"origin (0,0)\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(0, 0, 0))!\n      expect(e.x).toBe(0)\n      expect(e.y).toBe(0)\n    })\n\n    test(\"typical coordinates\", () => {\n      const e = parser.parseMouseEvent(encodeBasic(0, 79, 23))!\n      expect(e.x).toBe(79)\n      expect(e.y).toBe(23)\n    })\n\n    test(\"maximum safe X10 coordinate (94) works correctly\", () => {\n      // x=94 → raw byte = 94 + 33 = 127 (0x7F), still valid single-byte in UTF-8\n      const e = parser.parseMouseEvent(encodeBasic(0, 94, 94))!\n      expect(e.x).toBe(94)\n      expect(e.y).toBe(94)\n    })\n\n    test(\"coordinates >= 95 break under utf8 toString() (known limitation)\", () => {\n      // x=95 → raw byte = 95 + 33 = 128 (0x80), invalid as a standalone UTF-8 byte.\n      //\n      // The parser calls data.toString() which defaults to utf8, so charCodeAt()\n      // on the decoded string will not equal the original byte value.\n      //\n      // This test documents the known limitation: the X10 parser is only reliable\n      // for coordinates < 95 when input is decoded as UTF-8. SGR mode (1006)\n      // avoids this entirely by using decimal numbers instead of raw bytes.\n      const buf = encodeBasic(0, 95, 0)\n      const viaUtf8 = buf.toString(\"utf8\")\n      const viaLatin1 = buf.toString(\"latin1\")\n\n      // The raw byte 0x80 is NOT valid single-byte UTF-8\n      const utf8CharCode = viaUtf8.charCodeAt(4)\n      const latin1CharCode = viaLatin1.charCodeAt(4)\n      expect(latin1CharCode).toBe(128) // latin1 preserves the byte\n      expect(utf8CharCode).not.toBe(128) // utf8 corrupts it\n    })\n  })\n\n  describe(\"framing\", () => {\n    test(\"returns null for too-short buffer\", () => {\n      // Only 5 bytes instead of required 6\n      const e = parser.parseMouseEvent(Buffer.from(\"\\x1b[M\\x20\\x21\"))\n      expect(e).toBeNull()\n    })\n\n    test(\"returns null for unrelated escape sequence\", () => {\n      expect(parser.parseMouseEvent(Buffer.from(\"\\x1b[A\"))).toBeNull() // cursor up\n      expect(parser.parseMouseEvent(Buffer.from(\"\\x1b[1;2R\"))).toBeNull() // cursor position report\n    })\n\n    test(\"returns null for empty buffer\", () => {\n      expect(parser.parseMouseEvent(Buffer.from(\"\"))).toBeNull()\n    })\n\n    test(\"accepts Uint8Array input without Buffer conversion at callsite\", () => {\n      const encoded = encodeSGR(64, 10, 5, true)\n      const view = new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength)\n      const e = parser.parseMouseEvent(view)\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"up\", delta: 1 } })\n    })\n  })\n})\n\ndescribe(\"MouseParser SGR mode\", () => {\n  let parser: MouseParser\n\n  beforeEach(() => {\n    parser = new MouseParser()\n  })\n\n  describe(\"press and release\", () => {\n    test(\"left button press\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(0, 10, 5, true))!\n      expect(e).toMatchObject({ type: \"down\", button: 0, x: 10, y: 5 })\n    })\n\n    test(\"left button release\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(0, 10, 5, false))!\n      expect(e).toMatchObject({ type: \"up\", button: 0, x: 10, y: 5 })\n    })\n\n    test(\"middle button press\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(1, 10, 5, true))!\n      expect(e).toMatchObject({ type: \"down\", button: 1 })\n    })\n\n    test(\"right button press\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(2, 10, 5, true))!\n      expect(e).toMatchObject({ type: \"down\", button: 2 })\n    })\n\n    test(\"right button release\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(2, 10, 5, false))!\n      expect(e).toMatchObject({ type: \"up\", button: 2 })\n    })\n  })\n\n  describe(\"scroll\", () => {\n    test(\"wheel up (64)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(64, 10, 5, true))!\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"up\", delta: 1 } })\n    })\n\n    test(\"wheel down (65)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(65, 10, 5, true))!\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"down\", delta: 1 } })\n    })\n\n    test(\"wheel left (66)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(66, 10, 5, true))!\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"left\", delta: 1 } })\n    })\n\n    test(\"wheel right (67)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(67, 10, 5, true))!\n      expect(e).toMatchObject({ type: \"scroll\", scroll: { direction: \"right\", delta: 1 } })\n    })\n\n    test(\"scroll+motion code 96 (64|32) is treated as move\", () => {\n      // Seen in URxvt: motion can arrive with both bits set while scrolling.\n      const e = parser.parseMouseEvent(encodeSGR(96, 80, 66, true))!\n      expect(e).toMatchObject({\n        type: \"move\",\n        x: 80,\n        y: 66,\n        scroll: undefined,\n      })\n    })\n\n    test(\"scroll+motion code 97 (65|32) is treated as move\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(97, 80, 66, true))!\n      expect(e).toMatchObject({\n        type: \"move\",\n        x: 80,\n        y: 66,\n        scroll: undefined,\n      })\n    })\n\n    test(\"scroll release (m) is not classified as scroll\", () => {\n      // Some terminals send release for scroll too; the parser should not\n      // report that as a scroll event.\n      const e = parser.parseMouseEvent(encodeSGR(64, 10, 5, false))!\n      expect(e.type).not.toBe(\"scroll\")\n    })\n  })\n\n  describe(\"motion and drag\", () => {\n    test(\"move with no button: code 35 (32|3) → 'move'\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(35, 10, 5, false))!\n      expect(e.type).toBe(\"move\")\n    })\n\n    test(\"drag with left button held: code 32 (32|0)\", () => {\n      // First press down to populate mouseButtonsPressed\n      parser.parseMouseEvent(encodeSGR(0, 10, 5, true))\n      const e = parser.parseMouseEvent(encodeSGR(32, 12, 5, false))!\n      expect(e.type).toBe(\"drag\")\n    })\n\n    test(\"motion without prior press is 'move' even when button bits != 3\", () => {\n      // No prior press → mouseButtonsPressed is empty → should be \"move\"\n      const e = parser.parseMouseEvent(encodeSGR(32, 10, 5, false))!\n      expect(e.type).toBe(\"move\")\n    })\n\n    test(\"motion + button 3 is always 'move' even with buttons pressed\", () => {\n      parser.parseMouseEvent(encodeSGR(0, 10, 5, true))\n      const e = parser.parseMouseEvent(encodeSGR(35, 12, 5, false))!\n      expect(e.type).toBe(\"move\")\n    })\n  })\n\n  describe(\"modifiers\", () => {\n    test(\"shift (bit 2)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(4, 10, 5, true))!\n      expect(e.modifiers).toEqual({ shift: true, alt: false, ctrl: false })\n    })\n\n    test(\"alt (bit 3)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(8, 10, 5, true))!\n      expect(e.modifiers).toEqual({ shift: false, alt: true, ctrl: false })\n    })\n\n    test(\"ctrl (bit 4)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(16, 10, 5, true))!\n      expect(e.modifiers).toEqual({ shift: false, alt: false, ctrl: true })\n    })\n\n    test(\"all modifiers (28 = 4+8+16)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(28, 10, 5, true))!\n      expect(e.modifiers).toEqual({ shift: true, alt: true, ctrl: true })\n    })\n  })\n\n  describe(\"button tracking state (mouseButtonsPressed)\", () => {\n    test(\"press adds to tracked set, release clears it\", () => {\n      // Press left → drag should be recognized\n      parser.parseMouseEvent(encodeSGR(0, 5, 5, true))\n      const drag = parser.parseMouseEvent(encodeSGR(32, 8, 5, false))!\n      expect(drag.type).toBe(\"drag\")\n\n      // Release → subsequent motion should be \"move\"\n      parser.parseMouseEvent(encodeSGR(0, 8, 5, false))\n      const move = parser.parseMouseEvent(encodeSGR(35, 10, 5, false))!\n      expect(move.type).toBe(\"move\")\n    })\n\n    test(\"multiple buttons pressed — any motion is drag\", () => {\n      parser.parseMouseEvent(encodeSGR(0, 5, 5, true)) // left down\n      parser.parseMouseEvent(encodeSGR(2, 5, 5, true)) // right down\n      const e = parser.parseMouseEvent(encodeSGR(32, 8, 5, false))!\n      expect(e.type).toBe(\"drag\")\n    })\n\n    test(\"release clears ALL tracked buttons\", () => {\n      parser.parseMouseEvent(encodeSGR(0, 5, 5, true)) // left down\n      parser.parseMouseEvent(encodeSGR(2, 5, 5, true)) // right down\n      parser.parseMouseEvent(encodeSGR(0, 5, 5, false)) // release (clears all)\n      const e = parser.parseMouseEvent(encodeSGR(32, 8, 5, false))!\n      expect(e.type).toBe(\"move\") // no buttons tracked → move, not drag\n    })\n\n    test(\"reset() clears button tracking state\", () => {\n      parser.parseMouseEvent(encodeSGR(0, 5, 5, true)) // left down\n      parser.reset()\n      const e = parser.parseMouseEvent(encodeSGR(32, 8, 5, false))!\n      expect(e.type).toBe(\"move\")\n    })\n  })\n\n  describe(\"coordinates\", () => {\n    test(\"origin (0,0) from 1-based wire format\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(0, 0, 0, true))!\n      expect(e.x).toBe(0)\n      expect(e.y).toBe(0)\n    })\n\n    test(\"large coordinates (SGR uses decimal, no 223 limit)\", () => {\n      const e = parser.parseMouseEvent(encodeSGR(0, 500, 300, true))!\n      expect(e.x).toBe(500)\n      expect(e.y).toBe(300)\n    })\n  })\n\n  describe(\"framing\", () => {\n    test(\"returns null for incomplete SGR sequence\", () => {\n      expect(parser.parseMouseEvent(Buffer.from(\"\\x1b[<0;1;1\"))).toBeNull()\n    })\n\n    test(\"returns null for empty buffer\", () => {\n      expect(parser.parseMouseEvent(Buffer.from(\"\"))).toBeNull()\n    })\n  })\n})\n\ndescribe(\"MouseParser parseAllMouseEvents (multi-event chunks)\", () => {\n  let parser: MouseParser\n\n  beforeEach(() => {\n    parser = new MouseParser()\n  })\n\n  test(\"single event returns array of one\", () => {\n    const events = parser.parseAllMouseEvents(encodeSGR(0, 10, 5, true))\n    expect(events).toHaveLength(1)\n    expect(events[0]).toMatchObject({ type: \"down\", button: 0, x: 10, y: 5 })\n  })\n\n  test(\"two SGR events concatenated are both parsed\", () => {\n    const buf = Buffer.concat([encodeSGR(32, 69, 49, true), encodeSGR(32, 68, 49, true)])\n    const events = parser.parseAllMouseEvents(buf)\n    expect(events).toHaveLength(2)\n    expect(events[0]).toMatchObject({ type: \"move\", x: 69, y: 49 })\n    expect(events[1]).toMatchObject({ type: \"move\", x: 68, y: 49 })\n  })\n\n  test(\"four SGR motion events (matching mouse.log line 2)\", () => {\n    const buf = Buffer.concat([\n      encodeSGR(32, 69, 49, true),\n      encodeSGR(32, 68, 49, true),\n      encodeSGR(32, 68, 48, true),\n      encodeSGR(32, 67, 48, true),\n    ])\n    const events = parser.parseAllMouseEvents(buf)\n    expect(events).toHaveLength(4)\n    expect(events[0]).toMatchObject({ x: 69, y: 49 })\n    expect(events[1]).toMatchObject({ x: 68, y: 49 })\n    expect(events[2]).toMatchObject({ x: 68, y: 48 })\n    expect(events[3]).toMatchObject({ x: 67, y: 48 })\n  })\n\n  test(\"mixed event types: press + motion + release\", () => {\n    const buf = Buffer.concat([\n      encodeSGR(0, 10, 10, true), // left down\n      encodeSGR(32, 12, 10, true), // motion (drag, since button is pressed)\n      encodeSGR(0, 12, 10, false), // left up\n    ])\n    const events = parser.parseAllMouseEvents(buf)\n    expect(events).toHaveLength(3)\n    expect(events[0]).toMatchObject({ type: \"down\", button: 0 })\n    expect(events[1]).toMatchObject({ type: \"drag\" })\n    expect(events[2]).toMatchObject({ type: \"up\", button: 0 })\n  })\n\n  test(\"scroll events in a chunk\", () => {\n    const buf = Buffer.concat([encodeSGR(64, 82, 67, true), encodeSGR(64, 82, 67, true), encodeSGR(65, 82, 67, true)])\n    const events = parser.parseAllMouseEvents(buf)\n    expect(events).toHaveLength(3)\n    expect(events[0]).toMatchObject({ type: \"scroll\", scroll: { direction: \"up\" } })\n    expect(events[1]).toMatchObject({ type: \"scroll\", scroll: { direction: \"up\" } })\n    expect(events[2]).toMatchObject({ type: \"scroll\", scroll: { direction: \"down\" } })\n  })\n\n  test(\"chunk with scroll+motion codes (96/97) keeps motion semantics\", () => {\n    const buf = Buffer.concat([encodeSGR(64, 82, 67, true), encodeSGR(96, 81, 67, true), encodeSGR(97, 80, 67, true)])\n    const events = parser.parseAllMouseEvents(buf)\n    expect(events).toHaveLength(3)\n    expect(events[0]).toMatchObject({ type: \"scroll\", scroll: { direction: \"up\" } })\n    expect(events[1]).toMatchObject({ type: \"move\", x: 81, y: 67, scroll: undefined })\n    expect(events[2]).toMatchObject({ type: \"move\", x: 80, y: 67, scroll: undefined })\n  })\n\n  test(\"returns empty array for non-mouse data\", () => {\n    const events = parser.parseAllMouseEvents(Buffer.from(\"\\x1b[A\"))\n    expect(events).toHaveLength(0)\n  })\n\n  test(\"returns empty array for empty buffer\", () => {\n    const events = parser.parseAllMouseEvents(Buffer.from(\"\"))\n    expect(events).toHaveLength(0)\n  })\n\n  test(\"two X10 basic events concatenated\", () => {\n    const buf = Buffer.concat([encodeBasic(0, 10, 5), encodeBasic(3, 10, 5)])\n    const events = parser.parseAllMouseEvents(buf)\n    expect(events).toHaveLength(2)\n    expect(events[0]).toMatchObject({ type: \"down\", button: 0, x: 10, y: 5 })\n    expect(events[1]).toMatchObject({ type: \"up\", x: 10, y: 5 })\n  })\n\n  test(\"button tracking state is maintained across events in chunk\", () => {\n    // down + motion in same chunk: second event should be classified as drag\n    const buf = Buffer.concat([\n      encodeSGR(0, 5, 5, true), // press\n      encodeSGR(32, 8, 5, true), // motion with button 0 bits → drag if button tracked\n    ])\n    const events = parser.parseAllMouseEvents(buf)\n    expect(events).toHaveLength(2)\n    expect(events[0]).toMatchObject({ type: \"down\" })\n    expect(events[1]).toMatchObject({ type: \"drag\" })\n  })\n})\n\ndescribe(\"MouseParser protocol precedence\", () => {\n  let parser: MouseParser\n\n  beforeEach(() => {\n    parser = new MouseParser()\n  })\n\n  test(\"SGR is matched before X10 when both could apply\", () => {\n    // An SGR sequence that starts with ESC[ but has < distinguishes it from X10.\n    const sgr = encodeSGR(0, 10, 5, true)\n    const e = parser.parseMouseEvent(sgr)!\n    expect(e.type).toBe(\"down\")\n    expect(e.x).toBe(10)\n    expect(e.y).toBe(5)\n  })\n\n  test(\"X10 is used as fallback when data has no < prefix\", () => {\n    const x10 = encodeBasic(0, 10, 5)\n    const e = parser.parseMouseEvent(x10)!\n    expect(e.type).toBe(\"down\")\n    expect(e.x).toBe(10)\n    expect(e.y).toBe(5)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/parse.mouse.ts",
    "content": "export type MouseEventType = \"down\" | \"up\" | \"move\" | \"drag\" | \"drag-end\" | \"drop\" | \"over\" | \"out\" | \"scroll\"\n\nexport interface ScrollInfo {\n  direction: \"up\" | \"down\" | \"left\" | \"right\"\n  delta: number\n}\n\nexport type RawMouseEvent = {\n  type: MouseEventType\n  button: number\n  x: number\n  y: number\n  modifiers: { shift: boolean; alt: boolean; ctrl: boolean }\n  scroll?: ScrollInfo\n}\n\ntype ParsedMouseSequence = {\n  event: RawMouseEvent\n  consumed: number\n}\n\nexport class MouseParser {\n  private mouseButtonsPressed = new Set<number>()\n\n  private static readonly SCROLL_DIRECTIONS: Record<number, \"up\" | \"down\" | \"left\" | \"right\"> = {\n    0: \"up\",\n    1: \"down\",\n    2: \"left\",\n    3: \"right\",\n  }\n\n  public reset(): void {\n    this.mouseButtonsPressed.clear()\n  }\n\n  // Preserve raw byte values so X10 payload bytes >= 0x80 remain intact.\n  // SGR sequences are ASCII digits + separators and are unaffected either way.\n  private decodeInput(data: Buffer | Uint8Array): string {\n    const buf = Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength)\n    return buf.toString(\"latin1\")\n  }\n\n  public parseMouseEvent(data: Buffer | Uint8Array): RawMouseEvent | null {\n    const str = this.decodeInput(data)\n    const parsed = this.parseMouseSequenceAt(str, 0)\n    return parsed?.event ?? null\n  }\n\n  public parseAllMouseEvents(data: Buffer | Uint8Array): RawMouseEvent[] {\n    const str = this.decodeInput(data)\n    const events: RawMouseEvent[] = []\n    let offset = 0\n\n    while (offset < str.length) {\n      const parsed = this.parseMouseSequenceAt(str, offset)\n      if (!parsed) {\n        // Stop at the first non-mouse sequence. Callers can decide whether to\n        // route any remaining data through keyboard/terminal input handling.\n        break\n      }\n\n      events.push(parsed.event)\n      offset += parsed.consumed\n    }\n\n    return events\n  }\n\n  private parseMouseSequenceAt(str: string, offset: number): ParsedMouseSequence | null {\n    if (!str.startsWith(\"\\x1b[\", offset)) return null\n    const introducer = str[offset + 2]\n\n    if (introducer === \"<\") {\n      return this.parseSgrSequence(str, offset)\n    }\n\n    if (introducer === \"M\") {\n      return this.parseBasicSequence(str, offset)\n    }\n\n    return null\n  }\n\n  private parseSgrSequence(str: string, offset: number): ParsedMouseSequence | null {\n    let index = offset + 3\n    const values = [0, 0, 0]\n    let part = 0\n    let hasDigit = false\n\n    while (index < str.length) {\n      const char = str[index]\n      const charCode = str.charCodeAt(index)\n\n      if (charCode >= 48 && charCode <= 57) {\n        hasDigit = true\n        values[part] = values[part]! * 10 + (charCode - 48)\n        index++\n        continue\n      }\n\n      switch (char) {\n        case \";\": {\n          if (!hasDigit || part >= 2) return null\n          part++\n          hasDigit = false\n          index++\n          break\n        }\n        case \"M\":\n        case \"m\": {\n          if (!hasDigit || part !== 2) return null\n\n          return {\n            event: this.decodeSgrEvent(values[0]!, values[1]!, values[2]!, char),\n            consumed: index - offset + 1,\n          }\n        }\n        default:\n          return null\n      }\n    }\n\n    return null\n  }\n\n  private parseBasicSequence(str: string, offset: number): ParsedMouseSequence | null {\n    // ESC [ M + 3 bytes\n    if (offset + 6 > str.length) return null\n\n    const buttonByte = str.charCodeAt(offset + 3) - 32\n    // Convert from 1-based to 0-based\n    const x = str.charCodeAt(offset + 4) - 33\n    const y = str.charCodeAt(offset + 5) - 33\n\n    return {\n      event: this.decodeBasicEvent(buttonByte, x, y),\n      consumed: 6,\n    }\n  }\n\n  private decodeSgrEvent(rawButtonCode: number, wireX: number, wireY: number, pressRelease: \"M\" | \"m\"): RawMouseEvent {\n    const button = rawButtonCode & 3\n    const isScroll = (rawButtonCode & 64) !== 0\n    const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button]\n\n    const isMotion = (rawButtonCode & 32) !== 0\n    const modifiers = {\n      shift: (rawButtonCode & 4) !== 0,\n      alt: (rawButtonCode & 8) !== 0,\n      ctrl: (rawButtonCode & 16) !== 0,\n    }\n\n    let type: MouseEventType\n    let scrollInfo: ScrollInfo | undefined\n\n    if (isMotion) {\n      const isDragging = this.mouseButtonsPressed.size > 0\n\n      if (button === 3) {\n        type = \"move\"\n      } else if (isDragging) {\n        type = \"drag\"\n      } else {\n        type = \"move\"\n      }\n    } else if (isScroll && pressRelease === \"M\") {\n      type = \"scroll\"\n      scrollInfo = {\n        direction: scrollDirection!,\n        delta: 1,\n      }\n    } else {\n      type = pressRelease === \"M\" ? \"down\" : \"up\"\n\n      if (type === \"down\" && button !== 3) {\n        this.mouseButtonsPressed.add(button)\n      } else if (type === \"up\") {\n        this.mouseButtonsPressed.clear()\n      }\n    }\n\n    return {\n      type,\n      button: button === 3 ? 0 : button,\n      x: wireX - 1,\n      y: wireY - 1,\n      modifiers,\n      scroll: scrollInfo,\n    }\n  }\n\n  private decodeBasicEvent(buttonByte: number, x: number, y: number): RawMouseEvent {\n    const button = buttonByte & 3\n    const isScroll = (buttonByte & 64) !== 0\n    const isMotion = (buttonByte & 32) !== 0\n    const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button]\n\n    const modifiers = {\n      shift: (buttonByte & 4) !== 0,\n      alt: (buttonByte & 8) !== 0,\n      ctrl: (buttonByte & 16) !== 0,\n    }\n\n    let type: MouseEventType\n    let actualButton: number\n    let scrollInfo: ScrollInfo | undefined\n\n    if (isMotion) {\n      type = \"move\"\n      actualButton = button === 3 ? -1 : button\n    } else if (isScroll) {\n      type = \"scroll\"\n      actualButton = 0\n      scrollInfo = {\n        direction: scrollDirection!,\n        delta: 1,\n      }\n    } else {\n      type = button === 3 ? \"up\" : \"down\"\n      actualButton = button === 3 ? 0 : button\n    }\n\n    return {\n      type,\n      button: actualButton,\n      x,\n      y,\n      modifiers,\n      scroll: scrollInfo,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/paste.ts",
    "content": "export type PasteKind = \"text\" | \"binary\" | \"unknown\"\n\nexport interface PasteMetadata {\n  mimeType?: string\n  kind?: PasteKind\n}\n\nconst PASTE_TEXT_DECODER = new TextDecoder()\n\nexport function decodePasteBytes(bytes: Uint8Array): string {\n  return PASTE_TEXT_DECODER.decode(bytes)\n}\n\nexport function stripAnsiSequences(text: string): string {\n  return Bun.stripANSI(text)\n}\n"
  },
  {
    "path": "packages/core/src/lib/queue.ts",
    "content": "/**\n * Generic processing queue that handles asynchronous job processing\n */\nexport class ProcessQueue<T> {\n  private queue: T[] = []\n  private processing: boolean = false\n  private autoProcess: boolean = true\n\n  constructor(\n    private processor: (item: T) => Promise<void> | void,\n    autoProcess: boolean = true,\n  ) {\n    this.autoProcess = autoProcess\n  }\n\n  enqueue(item: T): void {\n    this.queue.push(item)\n\n    if (!this.processing && this.autoProcess) {\n      this.processQueue()\n    }\n  }\n\n  private processQueue(): void {\n    if (this.queue.length === 0) {\n      return\n    }\n\n    this.processing = true\n\n    queueMicrotask(async () => {\n      if (this.queue.length === 0) {\n        this.processing = false\n        return\n      }\n\n      // Get the next item to process (FIFO)\n      const item = this.queue.shift()!\n\n      try {\n        await this.processor(item)\n      } catch (error) {\n        console.error(\"Error processing queue item:\", error)\n      }\n\n      if (this.queue.length > 0) {\n        this.processQueue()\n      } else {\n        this.processing = false\n      }\n    })\n  }\n\n  clear(): void {\n    this.queue = []\n  }\n\n  isProcessing(): boolean {\n    return this.processing\n  }\n\n  size(): number {\n    return this.queue.length\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/renderable.validations.test.ts",
    "content": "import { describe, test, expect } from \"bun:test\"\nimport {\n  validateOptions,\n  isPositionType,\n  isDimensionType,\n  isFlexBasisType,\n  isSizeType,\n  isMarginType,\n  isPaddingType,\n  isPositionTypeType,\n  isOverflowType,\n  isValidPercentage,\n} from \"./renderable.validations.js\"\n\ndescribe(\"Utility Functions\", () => {\n  test(\"validateOptions\", () => {\n    expect(validateOptions(\"test\", { width: 100, height: 100 })).toBe(undefined)\n    expect(() => validateOptions(\"test\", { width: -100, height: 100 })).toThrow(TypeError)\n    expect(() => validateOptions(\"test\", { width: 100, height: -100 })).toThrow(TypeError)\n  })\n\n  test(\"isValidPercentage\", () => {\n    expect(isValidPercentage(\"50%\")).toBe(true)\n    expect(isValidPercentage(\"0%\")).toBe(true)\n    expect(isValidPercentage(\"100.5%\")).toBe(true)\n    expect(isValidPercentage(\"abc\")).toBe(false)\n    expect(isValidPercentage(\"50\")).toBe(false)\n    expect(isValidPercentage(50)).toBe(false)\n  })\n\n  test(\"isMarginType\", () => {\n    expect(isMarginType(10)).toBe(true)\n    expect(isMarginType(\"auto\")).toBe(true)\n    expect(isMarginType(\"50%\")).toBe(true)\n    expect(isMarginType(NaN)).toBe(false)\n    expect(isMarginType(\"invalid\")).toBe(false)\n  })\n\n  test(\"isPaddingType\", () => {\n    expect(isPaddingType(10)).toBe(true)\n    expect(isPaddingType(\"50%\")).toBe(true)\n    expect(isPaddingType(\"auto\")).toBe(false)\n    expect(isPaddingType(NaN)).toBe(false)\n  })\n\n  test(\"isPositionType\", () => {\n    expect(isPositionType(10)).toBe(true)\n    expect(isPositionType(\"auto\")).toBe(true)\n    expect(isPositionType(\"50%\")).toBe(true)\n    expect(isPositionType(NaN)).toBe(false)\n  })\n\n  test(\"isDimensionType\", () => {\n    expect(isDimensionType(100)).toBe(true)\n    expect(isDimensionType(\"auto\")).toBe(true)\n    expect(isDimensionType(\"50%\")).toBe(true)\n    expect(isDimensionType(NaN)).toBe(false)\n  })\n\n  test(\"isFlexBasisType\", () => {\n    expect(isFlexBasisType(100)).toBe(true)\n    expect(isFlexBasisType(\"auto\")).toBe(true)\n    expect(isFlexBasisType(undefined)).toBe(true)\n    expect(isFlexBasisType(NaN)).toBe(false)\n  })\n\n  test(\"isSizeType\", () => {\n    expect(isSizeType(100)).toBe(true)\n    expect(isSizeType(\"50%\")).toBe(true)\n    expect(isSizeType(undefined)).toBe(true)\n    expect(isSizeType(NaN)).toBe(false)\n  })\n\n  test(\"isPositionTypeType\", () => {\n    expect(isPositionTypeType(\"relative\")).toBe(true)\n    expect(isPositionTypeType(\"absolute\")).toBe(true)\n    expect(isPositionTypeType(\"static\")).toBe(false)\n    expect(isPositionTypeType(\"fixed\")).toBe(false)\n  })\n\n  test(\"isOverflowType\", () => {\n    expect(isOverflowType(\"visible\")).toBe(true)\n    expect(isOverflowType(\"hidden\")).toBe(true)\n    expect(isOverflowType(\"scroll\")).toBe(true)\n    expect(isOverflowType(\"auto\")).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/renderable.validations.ts",
    "content": "import type { RenderableOptions, Renderable } from \"../Renderable.js\"\nimport type { PositionTypeString, OverflowString } from \"./yoga.options.js\"\n\nexport function validateOptions(id: string, options: RenderableOptions<Renderable>): void {\n  if (typeof options.width === \"number\") {\n    if (options.width < 0) {\n      throw new TypeError(`Invalid width for Renderable ${id}: ${options.width}`)\n    }\n  }\n  if (typeof options.height === \"number\") {\n    if (options.height < 0) {\n      throw new TypeError(`Invalid height for Renderable ${id}: ${options.height}`)\n    }\n  }\n}\n\nexport function isValidPercentage(value: any): value is `${number}%` {\n  if (typeof value === \"string\" && value.endsWith(\"%\")) {\n    const numPart = value.slice(0, -1)\n    const num = parseFloat(numPart)\n    return !Number.isNaN(num)\n  }\n  return false\n}\n\nexport function isMarginType(value: any): value is number | \"auto\" | `${number}%` {\n  if (typeof value === \"number\" && !Number.isNaN(value)) {\n    return true\n  }\n  if (value === \"auto\") {\n    return true\n  }\n  return isValidPercentage(value)\n}\n\nexport function isPaddingType(value: any): value is number | `${number}%` {\n  if (typeof value === \"number\" && !Number.isNaN(value)) {\n    return true\n  }\n  return isValidPercentage(value)\n}\n\nexport function isPositionType(value: any): value is number | \"auto\" | `${number}%` {\n  if (typeof value === \"number\" && !Number.isNaN(value)) {\n    return true\n  }\n  if (value === \"auto\") {\n    return true\n  }\n  return isValidPercentage(value)\n}\n\nexport function isPositionTypeType(value: any): value is PositionTypeString {\n  return value === \"relative\" || value === \"absolute\"\n}\n\nexport function isOverflowType(value: any): value is OverflowString {\n  return value === \"visible\" || value === \"hidden\" || value === \"scroll\"\n}\n\nexport function isDimensionType(value: any): value is number | \"auto\" | `${number}%` {\n  return isPositionType(value)\n}\n\nexport function isFlexBasisType(value: any): value is number | \"auto\" | undefined {\n  if (value === undefined || value === \"auto\") {\n    return true\n  }\n  if (typeof value === \"number\" && !Number.isNaN(value)) {\n    return true\n  }\n  return false\n}\n\nexport function isSizeType(value: any): value is number | `${number}%` | undefined {\n  if (value === undefined) {\n    return true\n  }\n  if (typeof value === \"number\" && !Number.isNaN(value)) {\n    return true\n  }\n  return isValidPercentage(value)\n}\n"
  },
  {
    "path": "packages/core/src/lib/scroll-acceleration.ts",
    "content": "export interface ScrollAcceleration {\n  tick(now?: number): number\n  reset(): void\n}\n\nexport class LinearScrollAccel implements ScrollAcceleration {\n  tick(_now?: number): number {\n    return 1\n  }\n\n  reset(): void {}\n}\n\n/**\n * macOS-inspired scroll acceleration.\n *\n * The class measures the time between consecutive scroll events and keeps a short\n * moving window of the latest intervals. The average interval determines which\n * multiplier to apply so that quick bursts accelerate and slower gestures stay precise.\n *\n * For intuition, treat the streak as a continuous timeline and compare it with the\n * exponential distance curve from the pointer-acceleration research post:\n *   d(t) = v₀ * ( t + A * (exp(t/τ) - 1 - t/τ) ).\n * Small t stays near the base multiplier, medium streaks settle on multiplier1, and\n * sustained bursts reach multiplier2, mirroring how the exponential curve bends up.\n *\n * Options:\n * - threshold1: upper bound (ms) of the \"medium\" band. Raise to delay the ramp.\n * - threshold2: upper bound (ms) of the \"fast\" band. Lower to demand tighter bursts.\n * - multiplier1: scale for medium speed streaks.\n * - multiplier2: scale for sustained fast streaks.\n * - baseMultiplier: scale for relaxed scrolling; set to 1 for linear behaviour.\n */\nexport class MacOSScrollAccel implements ScrollAcceleration {\n  private lastTickTime = 0\n  private velocityHistory: number[] = []\n  private readonly historySize = 3\n  private readonly streakTimeout = 150\n  // Some terminals send 2 or more ticks for each mouse wheel tick, for example Ghostty, with a small delay between each tick, about 4ms on average.\n  // We ignore these ticks otherwise they would cause faster acceleration to kick in\n  // https://github.com/ghostty-org/ghostty/discussions/7577\n  private readonly minTickInterval = 6\n\n  constructor(\n    private opts: {\n      A?: number\n      tau?: number\n      maxMultiplier?: number\n    } = {},\n  ) {}\n\n  tick(now = Date.now()): number {\n    const A = this.opts.A ?? 0.8\n    const tau = this.opts.tau ?? 3\n    const maxMultiplier = this.opts.maxMultiplier ?? 6\n\n    const dt = this.lastTickTime ? now - this.lastTickTime : Infinity\n\n    // Reset streak if too much time has passed or first tick\n    if (dt === Infinity || dt > this.streakTimeout) {\n      this.lastTickTime = now\n      this.velocityHistory = []\n      return 1\n    }\n\n    // Ignore ticks closer than minTickInterval (they're part of the same logical tick)\n    if (dt < this.minTickInterval) {\n      return 1\n    }\n\n    this.lastTickTime = now\n\n    this.velocityHistory.push(dt)\n    if (this.velocityHistory.length > this.historySize) {\n      this.velocityHistory.shift()\n    }\n\n    // Calculate average interval (lower = faster scrolling)\n    const avgInterval = this.velocityHistory.reduce((a, b) => a + b, 0) / this.velocityHistory.length\n\n    // Convert interval to velocity: faster ticks = higher velocity\n    // Normalize to a reference interval (e.g., 100ms = velocity of 1)\n    const referenceInterval = 100\n    const velocity = referenceInterval / avgInterval\n\n    // Apply exponential curve based on velocity\n    // Higher velocity (tighter ticks) = more acceleration\n    const x = velocity / tau\n    const multiplier = 1 + A * (Math.exp(x) - 1)\n\n    return Math.min(multiplier, maxMultiplier)\n  }\n\n  reset(): void {\n    this.lastTickTime = 0\n    this.velocityHistory = []\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/selection.ts",
    "content": "import { Renderable, type ViewportBounds } from \"../index.js\"\nimport { coordinateToCharacterIndex, fonts } from \"./ascii.font.js\"\n\nclass SelectionAnchor {\n  private relativeX: number\n  private relativeY: number\n\n  constructor(\n    private renderable: Renderable,\n    absoluteX: number,\n    absoluteY: number,\n  ) {\n    this.relativeX = absoluteX - this.renderable.x\n    this.relativeY = absoluteY - this.renderable.y\n  }\n\n  get x(): number {\n    return this.renderable.x + this.relativeX\n  }\n\n  get y(): number {\n    return this.renderable.y + this.relativeY\n  }\n}\n\nexport class Selection {\n  private _anchor: SelectionAnchor\n  private _focus: { x: number; y: number }\n  private _selectedRenderables: Renderable[] = []\n  private _touchedRenderables: Renderable[] = []\n  private _isActive: boolean = true\n  private _isDragging: boolean = true\n  private _isStart: boolean = false\n\n  constructor(anchorRenderable: Renderable, anchor: { x: number; y: number }, focus: { x: number; y: number }) {\n    this._anchor = new SelectionAnchor(anchorRenderable, anchor.x, anchor.y)\n    this._focus = { ...focus }\n  }\n\n  get isStart(): boolean {\n    return this._isStart\n  }\n\n  set isStart(value: boolean) {\n    this._isStart = value\n  }\n\n  get anchor(): { x: number; y: number } {\n    return { x: this._anchor.x, y: this._anchor.y }\n  }\n\n  get focus(): { x: number; y: number } {\n    return { ...this._focus }\n  }\n\n  set focus(value: { x: number; y: number }) {\n    this._focus = { ...value }\n  }\n\n  get isActive(): boolean {\n    return this._isActive\n  }\n\n  set isActive(value: boolean) {\n    this._isActive = value\n  }\n\n  get isDragging(): boolean {\n    return this._isDragging\n  }\n\n  set isDragging(value: boolean) {\n    this._isDragging = value\n  }\n\n  get bounds(): ViewportBounds {\n    const minX = Math.min(this._anchor.x, this._focus.x)\n    const maxX = Math.max(this._anchor.x, this._focus.x)\n    const minY = Math.min(this._anchor.y, this._focus.y)\n    const maxY = Math.max(this._anchor.y, this._focus.y)\n\n    // Selection bounds are inclusive of both anchor and focus\n    // A selection from (0,0) to (0,0) covers 1 cell\n    // A selection from (0,0) to (5,3) covers cells from (0,0) to (5,3) inclusive\n    const width = maxX - minX + 1\n    const height = maxY - minY + 1\n\n    return {\n      x: minX,\n      y: minY,\n      width,\n      height,\n    }\n  }\n\n  updateSelectedRenderables(selectedRenderables: Renderable[]): void {\n    this._selectedRenderables = selectedRenderables\n  }\n\n  get selectedRenderables(): Renderable[] {\n    return this._selectedRenderables\n  }\n\n  updateTouchedRenderables(touchedRenderables: Renderable[]): void {\n    this._touchedRenderables = touchedRenderables\n  }\n\n  get touchedRenderables(): Renderable[] {\n    return this._touchedRenderables\n  }\n\n  getSelectedText(): string {\n    const selectedTexts = this._selectedRenderables\n      // Sort by reading order: top-to-bottom, then left-to-right\n      .sort((a, b) => {\n        const aY = a.y\n        const bY = b.y\n        if (aY !== bY) {\n          return aY - bY\n        }\n        return a.x - b.x\n      })\n      .filter((renderable) => !renderable.isDestroyed)\n      .map((renderable) => renderable.getSelectedText())\n      .filter((text) => text)\n    return selectedTexts.join(\"\\n\")\n  }\n}\n\nexport interface LocalSelectionBounds {\n  anchorX: number\n  anchorY: number\n  focusX: number\n  focusY: number\n  isActive: boolean\n}\n\nexport function convertGlobalToLocalSelection(\n  globalSelection: Selection | null,\n  localX: number,\n  localY: number,\n): LocalSelectionBounds | null {\n  if (!globalSelection?.isActive) {\n    return null\n  }\n\n  return {\n    anchorX: globalSelection.anchor.x - localX,\n    anchorY: globalSelection.anchor.y - localY,\n    focusX: globalSelection.focus.x - localX,\n    focusY: globalSelection.focus.y - localY,\n    isActive: true,\n  }\n}\n\nexport class ASCIIFontSelectionHelper {\n  private localSelection: { start: number; end: number } | null = null\n\n  constructor(\n    private getText: () => string,\n    private getFont: () => keyof typeof fonts,\n  ) {}\n\n  hasSelection(): boolean {\n    return this.localSelection !== null\n  }\n\n  getSelection(): { start: number; end: number } | null {\n    return this.localSelection\n  }\n\n  shouldStartSelection(localX: number, localY: number, width: number, height: number): boolean {\n    if (localX < 0 || localX >= width || localY < 0 || localY >= height) {\n      return false\n    }\n\n    const text = this.getText()\n    const font = this.getFont()\n    const charIndex = coordinateToCharacterIndex(localX, text, font)\n\n    return charIndex >= 0 && charIndex <= text.length\n  }\n\n  onLocalSelectionChanged(localSelection: LocalSelectionBounds | null, width: number, height: number): boolean {\n    const previousSelection = this.localSelection\n\n    if (!localSelection?.isActive) {\n      this.localSelection = null\n      return previousSelection !== null\n    }\n\n    const text = this.getText()\n    const font = this.getFont()\n\n    const selStart = { x: localSelection.anchorX, y: localSelection.anchorY }\n    const selEnd = { x: localSelection.focusX, y: localSelection.focusY }\n\n    if (height - 1 < selStart.y || 0 > selEnd.y) {\n      this.localSelection = null\n      return previousSelection !== null\n    }\n\n    let startCharIndex = 0\n    let endCharIndex = text.length\n\n    if (selStart.y > height - 1) {\n      // Selection starts below us - we're not selected\n      this.localSelection = null\n      return previousSelection !== null\n    } else if (selStart.y >= 0 && selStart.y <= height - 1) {\n      // Selection starts within our Y range - use the actual start X coordinate\n      if (selStart.x > 0) {\n        startCharIndex = coordinateToCharacterIndex(selStart.x, text, font)\n      }\n    }\n\n    if (selEnd.y < 0) {\n      // Selection ends above us - we're not selected\n      this.localSelection = null\n      return previousSelection !== null\n    } else if (selEnd.y >= 0 && selEnd.y <= height - 1) {\n      // Selection ends within our Y range - use the actual end X coordinate\n      if (selEnd.x >= 0) {\n        endCharIndex = coordinateToCharacterIndex(selEnd.x, text, font)\n      } else {\n        endCharIndex = 0\n      }\n    }\n\n    if (startCharIndex < endCharIndex && startCharIndex >= 0 && endCharIndex <= text.length) {\n      this.localSelection = { start: startCharIndex, end: endCharIndex }\n    } else {\n      this.localSelection = null\n    }\n\n    return (\n      previousSelection?.start !== this.localSelection?.start || previousSelection?.end !== this.localSelection?.end\n    )\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/singleton.ts",
    "content": "const singletonCacheSymbol = Symbol.for(\"@opentui/core/singleton\")\n\n/**\n * Ensures a value is initialized once per process,\n * persists across Bun hot reloads, and is type-safe.\n */\nexport function singleton<T>(key: string, factory: () => T): T {\n  // @ts-expect-error this symbol is only used in this file and is not part of the public API\n  const bag = (globalThis[singletonCacheSymbol] ??= {})\n  if (!(key in bag)) {\n    bag[key] = factory()\n  }\n  return bag[key] as T\n}\n\nexport function destroySingleton(key: string): void {\n  // @ts-expect-error this symbol is only used in this file and is not part of the public API\n  const bag = globalThis[singletonCacheSymbol]\n  if (bag && key in bag) {\n    delete bag[key]\n  }\n}\n\nexport function hasSingleton(key: string): boolean {\n  // @ts-expect-error this symbol is only used in this file and is not part of the public API\n  const bag = globalThis[singletonCacheSymbol]\n  return bag && key in bag\n}\n"
  },
  {
    "path": "packages/core/src/lib/stdin-parser.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\"\nimport { Buffer } from \"node:buffer\"\nimport { ManualClock } from \"../testing/manual-clock\"\nimport type { ScrollInfo } from \"./parse.mouse\"\nimport { StdinParser, type StdinEvent, type StdinParserOptions } from \"./stdin-parser\"\n\ntype KeySnap = {\n  type: \"key\"\n  raw: string\n  name: string\n  ctrl: boolean\n  meta: boolean\n  shift: boolean\n  eventType: string\n}\ntype MouseSnap = { type: \"mouse\"; raw: string; encoding: \"sgr\" | \"x10\"; event: Record<string, unknown> }\ntype PasteSnap = { type: \"paste\"; bytes: Uint8Array }\ntype RespSnap = { type: \"response\"; protocol: string; sequence: string }\ntype Snap = KeySnap | MouseSnap | PasteSnap | RespSnap\n\nconst K_DEFAULTS = { ctrl: false, meta: false, shift: false, eventType: \"press\" }\ntype KOpts = { raw?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; eventType?: string }\n\nfunction k(name: string, opts: KOpts = {}): KeySnap {\n  return { type: \"key\", raw: opts.raw ?? name, name, ...K_DEFAULTS, ...opts }\n}\n\nfunction resp(protocol: string, sequence: string): RespSnap {\n  return { type: \"response\", protocol, sequence }\n}\n\nfunction paste(text: string): PasteSnap {\n  return { type: \"paste\", bytes: Uint8Array.from(Buffer.from(text)) }\n}\n\nconst NO_MODS = { shift: false, alt: false, ctrl: false }\n\nfunction sgr(\n  raw: string,\n  evType: string,\n  x: number,\n  y: number,\n  opts: { button?: number; mods?: Partial<typeof NO_MODS>; scroll?: ScrollInfo } = {},\n): MouseSnap {\n  const event: Record<string, unknown> = {\n    type: evType,\n    button: opts.button ?? 0,\n    x,\n    y,\n    modifiers: { ...NO_MODS, ...opts.mods },\n  }\n  if (opts.scroll) event.scroll = opts.scroll\n  return { type: \"mouse\", raw, encoding: \"sgr\", event }\n}\n\nfunction x10m(\n  raw: string,\n  evType: string,\n  x: number,\n  y: number,\n  opts: { button?: number; mods?: Partial<typeof NO_MODS>; scroll?: ScrollInfo } = {},\n): MouseSnap {\n  const event: Record<string, unknown> = {\n    type: evType,\n    button: opts.button ?? 0,\n    x,\n    y,\n    modifiers: { ...NO_MODS, ...opts.mods },\n  }\n  if (opts.scroll) event.scroll = opts.scroll\n  return { type: \"mouse\", raw, encoding: \"x10\", event }\n}\n\nfunction createParser(options: StdinParserOptions = {}): StdinParser {\n  return new StdinParser({ armTimeouts: false, clock: new ManualClock(), ...options })\n}\n\nfunction createTimedParser(options: StdinParserOptions = {}): { parser: StdinParser; clock: ManualClock } {\n  const clock = new ManualClock()\n  return { parser: new StdinParser({ armTimeouts: true, clock, ...options }), clock }\n}\n\nfunction snapshotEvent(event: StdinEvent): Snap {\n  switch (event.type) {\n    case \"key\":\n      return {\n        type: \"key\",\n        raw: event.raw,\n        name: event.key.name,\n        ctrl: event.key.ctrl,\n        meta: event.key.meta,\n        shift: event.key.shift,\n        eventType: event.key.eventType,\n      }\n    case \"mouse\": {\n      const ev: Record<string, unknown> = { ...event.event }\n      if (!ev.scroll) delete ev.scroll\n      return { type: \"mouse\", raw: event.raw, encoding: event.encoding, event: ev }\n    }\n    case \"paste\":\n      return { type: \"paste\", bytes: event.bytes }\n    case \"response\":\n      return { type: \"response\", protocol: event.protocol, sequence: event.sequence }\n  }\n}\n\nfunction snap(parser: StdinParser): Snap[] {\n  const events: StdinEvent[] = []\n  parser.drain((e) => events.push(e))\n  return events.map(snapshotEvent)\n}\n\ntype ChunkInput = string | number[] | Uint8Array\n\nfunction buf(input: ChunkInput): Uint8Array {\n  if (typeof input === \"string\") return Buffer.from(input)\n  return input instanceof Uint8Array ? input : Uint8Array.from(input)\n}\n\nfunction latin1(input: number[] | Uint8Array): string {\n  return Buffer.from(buf(input)).toString(\"latin1\")\n}\n\nfunction snapChunks(chunks: ChunkInput[], opts?: StdinParserOptions): Snap[] {\n  const p = createParser(opts)\n  try {\n    for (const chunk of chunks) p.push(buf(chunk))\n    return snap(p)\n  } finally {\n    p.destroy()\n  }\n}\n\nfunction concatChunks(chunks: ChunkInput[]): Uint8Array {\n  return Buffer.concat(chunks.map((chunk) => Buffer.from(buf(chunk))))\n}\n\nfunction x10bytes(rawButton: number, x: number, y: number): number[] {\n  return [0x1b, 0x5b, 0x4d, rawButton + 32, x + 33, y + 33]\n}\n\ntype Case = [label: string, input: ChunkInput, expected: Snap[]]\n\nfunction table(cases: Case[], opts?: StdinParserOptions) {\n  for (const [label, input, expected] of cases) {\n    test(label, () => {\n      const p = createParser(opts)\n      try {\n        p.push(buf(input))\n        expect(snap(p)).toEqual(expected)\n      } finally {\n        p.destroy()\n      }\n    })\n  }\n}\n\n/** push each byte individually, assert same result as whole-chunk push */\nfunction assertChunkInvariant(input: Uint8Array, opts?: StdinParserOptions) {\n  const whole = createParser(opts)\n  const split = createParser(opts)\n  try {\n    whole.push(input)\n    const expected = snap(whole)\n    for (let i = 0; i < input.length; i++) split.push(input.subarray(i, i + 1))\n    expect(snap(split)).toEqual(expected)\n  } finally {\n    whole.destroy()\n    split.destroy()\n  }\n}\n\ndescribe(\"StdinParser\", () => {\n  describe(\"printable ASCII\", () => {\n    test(\"lowercase a-z\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"abcdefghijklmnopqrstuvwxyz\"))\n        expect(snap(p)).toEqual(\"abcdefghijklmnopqrstuvwxyz\".split(\"\").map((c) => k(c)))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"uppercase A-Z produce shifted keys\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"))\n        expect(snap(p)).toEqual(\n          \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\".split(\"\").map((c) => k(c.toLowerCase(), { raw: c, shift: true })),\n        )\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"digits 0-9\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"0123456789\"))\n        expect(snap(p)).toEqual(\"0123456789\".split(\"\").map((c) => k(c)))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"common symbols\", () => {\n      const p = createParser()\n      try {\n        const syms = \"!@#$%^&*()-_=+[]{}|;':,./<>?`~\"\n        p.push(Buffer.from(syms))\n        expect(snap(p)).toEqual(syms.split(\"\").map((c) => k(c)))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"space produces key named space\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\" \"))\n        expect(snap(p)).toEqual([k(\"space\", { raw: \" \" })])\n      } finally {\n        p.destroy()\n      }\n    })\n  })\n\n  describe(\"control characters\", () => {\n    // Map of special control bytes that get their own key name instead of ctrl+letter\n    const special: Record<number, [string, KOpts]> = {\n      0x00: [\"space\", { ctrl: true }],\n      0x08: [\"backspace\", {}],\n      0x09: [\"tab\", {}],\n      0x0a: [\"linefeed\", {}],\n      0x0d: [\"return\", {}],\n    }\n\n    const cases: Case[] = []\n    for (let byte = 0; byte <= 0x1a; byte++) {\n      if (byte === 0x1b) continue // ESC tested separately\n      const raw = String.fromCharCode(byte)\n      const sp = special[byte]\n      if (sp) {\n        cases.push([`0x${byte.toString(16).padStart(2, \"0\")} → ${sp[0]}`, [byte], [k(sp[0], { raw, ...sp[1] })]])\n      } else {\n        const letter = String.fromCharCode(byte + 96)\n        cases.push([\n          `ctrl+${letter} (0x${byte.toString(16).padStart(2, \"0\")})`,\n          [byte],\n          [k(letter, { raw, ctrl: true })],\n        ])\n      }\n    }\n    cases.push([\"0x7f → backspace\", [0x7f], [k(\"backspace\", { raw: \"\\x7f\" })]])\n\n    table(cases)\n  })\n\n  describe(\"special keys\", () => {\n    table([\n      [\"return\", \"\\r\", [k(\"return\", { raw: \"\\r\" })]],\n      [\"linefeed\", \"\\n\", [k(\"linefeed\", { raw: \"\\n\" })]],\n      [\"tab\", \"\\t\", [k(\"tab\", { raw: \"\\t\" })]],\n      [\"backspace (0x08)\", \"\\b\", [k(\"backspace\", { raw: \"\\b\" })]],\n      [\"backspace (0x7f)\", \"\\x7f\", [k(\"backspace\", { raw: \"\\x7f\" })]],\n      [\"escape (lone, no timeout)\", \"\\x1b\", []], // stays pending without timeout\n      [\"shift-tab\", \"\\x1b[Z\", [k(\"tab\", { raw: \"\\x1b[Z\", shift: true })]],\n      [\"ctrl+space\", \"\\x00\", [k(\"space\", { raw: \"\\x00\", ctrl: true })]],\n    ])\n\n    test(\"lone ESC with timeout produces escape key\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n  })\n\n  describe(\"arrows and navigation\", () => {\n    table([\n      // CSI arrows\n      [\"up\", \"\\x1b[A\", [k(\"up\", { raw: \"\\x1b[A\" })]],\n      [\"down\", \"\\x1b[B\", [k(\"down\", { raw: \"\\x1b[B\" })]],\n      [\"right\", \"\\x1b[C\", [k(\"right\", { raw: \"\\x1b[C\" })]],\n      [\"left\", \"\\x1b[D\", [k(\"left\", { raw: \"\\x1b[D\" })]],\n      [\"home\", \"\\x1b[H\", [k(\"home\", { raw: \"\\x1b[H\" })]],\n      [\"end\", \"\\x1b[F\", [k(\"end\", { raw: \"\\x1b[F\" })]],\n      [\"clear\", \"\\x1b[E\", [k(\"clear\", { raw: \"\\x1b[E\" })]],\n      // tilde navigation\n      [\"home ~\", \"\\x1b[1~\", [k(\"home\", { raw: \"\\x1b[1~\" })]],\n      [\"insert ~\", \"\\x1b[2~\", [k(\"insert\", { raw: \"\\x1b[2~\" })]],\n      [\"delete ~\", \"\\x1b[3~\", [k(\"delete\", { raw: \"\\x1b[3~\" })]],\n      [\"end ~\", \"\\x1b[4~\", [k(\"end\", { raw: \"\\x1b[4~\" })]],\n      [\"pgup ~\", \"\\x1b[5~\", [k(\"pageup\", { raw: \"\\x1b[5~\" })]],\n      [\"pgdn ~\", \"\\x1b[6~\", [k(\"pagedown\", { raw: \"\\x1b[6~\" })]],\n      // rxvt\n      [\"home rxvt\", \"\\x1b[7~\", [k(\"home\", { raw: \"\\x1b[7~\" })]],\n      [\"end rxvt\", \"\\x1b[8~\", [k(\"end\", { raw: \"\\x1b[8~\" })]],\n    ])\n  })\n\n  describe(\"function keys\", () => {\n    // ESC [ n ~ form\n    const tildeF: [string, string][] = [\n      [\"f1\", \"11\"],\n      [\"f2\", \"12\"],\n      [\"f3\", \"13\"],\n      [\"f4\", \"14\"],\n      [\"f5\", \"15\"],\n      [\"f6\", \"17\"],\n      [\"f7\", \"18\"],\n      [\"f8\", \"19\"],\n      [\"f9\", \"20\"],\n      [\"f10\", \"21\"],\n      [\"f11\", \"23\"],\n      [\"f12\", \"24\"],\n    ]\n    table(tildeF.map(([name, num]) => [`${name} (CSI ${num}~)`, `\\x1b[${num}~`, [k(name, { raw: `\\x1b[${num}~` })]]))\n\n    // ESC O letter (SS3) form — F1-F4\n    table([\n      [\"f1 (SS3)\", \"\\x1bOP\", [k(\"f1\", { raw: \"\\x1bOP\" })]],\n      [\"f2 (SS3)\", \"\\x1bOQ\", [k(\"f2\", { raw: \"\\x1bOQ\" })]],\n      [\"f3 (SS3)\", \"\\x1bOR\", [k(\"f3\", { raw: \"\\x1bOR\" })]],\n      [\"f4 (SS3)\", \"\\x1bOS\", [k(\"f4\", { raw: \"\\x1bOS\" })]],\n    ])\n  })\n\n  describe(\"double-bracket CSI variants\", () => {\n    table([\n      [\"f1 ([[A)\", \"\\x1b[[A\", [k(\"f1\", { raw: \"\\x1b[[A\" })]],\n      [\"f2 ([[B)\", \"\\x1b[[B\", [k(\"f2\", { raw: \"\\x1b[[B\" })]],\n      [\"f3 ([[C)\", \"\\x1b[[C\", [k(\"f3\", { raw: \"\\x1b[[C\" })]],\n      [\"f4 ([[D)\", \"\\x1b[[D\", [k(\"f4\", { raw: \"\\x1b[[D\" })]],\n      [\"f5 ([[E)\", \"\\x1b[[E\", [k(\"f5\", { raw: \"\\x1b[[E\" })]],\n      [\"pageup ([[5~)\", \"\\x1b[[5~\", [k(\"pageup\", { raw: \"\\x1b[[5~\" })]],\n      [\"pagedown ([[6~)\", \"\\x1b[[6~\", [k(\"pagedown\", { raw: \"\\x1b[[6~\" })]],\n    ])\n  })\n\n  describe(\"SS3 sequences\", () => {\n    table([\n      [\"up\", \"\\x1bOA\", [k(\"up\", { raw: \"\\x1bOA\" })]],\n      [\"down\", \"\\x1bOB\", [k(\"down\", { raw: \"\\x1bOB\" })]],\n      [\"right\", \"\\x1bOC\", [k(\"right\", { raw: \"\\x1bOC\" })]],\n      [\"left\", \"\\x1bOD\", [k(\"left\", { raw: \"\\x1bOD\" })]],\n      [\"home\", \"\\x1bOH\", [k(\"home\", { raw: \"\\x1bOH\" })]],\n      [\"end\", \"\\x1bOF\", [k(\"end\", { raw: \"\\x1bOF\" })]],\n      [\"clear\", \"\\x1bOE\", [k(\"clear\", { raw: \"\\x1bOE\" })]],\n    ])\n\n    test(\"SS3 interrupted by embedded ESC flushes partial then restarts\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1bO\\x1bOA\"))\n        const s = snap(p)\n        expect(s).toHaveLength(2)\n        expect(s[0]).toEqual(resp(\"unknown\", \"\\x1bO\"))\n        expect(s[1]).toEqual(k(\"up\", { raw: \"\\x1bOA\" }))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"SS3 timeout-flushed as unknown response\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1bO\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1bO\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"SS3 timeout flush does not swallow later text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1bO\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1bO\")])\n\n        parser.push(Buffer.from(\"a\"))\n        expect(snap(parser)).toEqual([k(\"a\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n  })\n\n  describe(\"modifier combinations\", () => {\n    // CSI 1;modifier letter format\n    const modTable: [string, number, KOpts][] = [\n      [\"shift\", 2, { shift: true }],\n      [\"alt\", 3, { meta: true }],\n      [\"shift+alt\", 4, { shift: true, meta: true }],\n      [\"ctrl\", 5, { ctrl: true }],\n      [\"shift+ctrl\", 6, { shift: true, ctrl: true }],\n      [\"alt+ctrl\", 7, { meta: true, ctrl: true }],\n    ]\n\n    const arrows: [string, string][] = [\n      [\"up\", \"A\"],\n      [\"down\", \"B\"],\n      [\"right\", \"C\"],\n      [\"left\", \"D\"],\n    ]\n\n    const cases: Case[] = []\n    for (const [modName, modNum, modOpts] of modTable) {\n      for (const [keyName, letter] of arrows) {\n        const seq = `\\x1b[1;${modNum}${letter}`\n        cases.push([`${modName}+${keyName}`, seq, [k(keyName, { raw: seq, ...modOpts })]])\n      }\n    }\n    table(cases)\n\n    // rxvt shift variants\n    table([\n      [\"shift+up (rxvt)\", \"\\x1b[a\", [k(\"up\", { raw: \"\\x1b[a\", shift: true })]],\n      [\"shift+down (rxvt)\", \"\\x1b[b\", [k(\"down\", { raw: \"\\x1b[b\", shift: true })]],\n      [\"shift+right (rxvt)\", \"\\x1b[c\", [k(\"right\", { raw: \"\\x1b[c\", shift: true })]],\n      [\"shift+left (rxvt)\", \"\\x1b[d\", [k(\"left\", { raw: \"\\x1b[d\", shift: true })]],\n    ])\n\n    // rxvt ctrl variants\n    table([\n      [\"ctrl+up (rxvt)\", \"\\x1bOa\", [k(\"up\", { raw: \"\\x1bOa\", ctrl: true })]],\n      [\"ctrl+down (rxvt)\", \"\\x1bOb\", [k(\"down\", { raw: \"\\x1bOb\", ctrl: true })]],\n      [\"ctrl+right (rxvt)\", \"\\x1bOc\", [k(\"right\", { raw: \"\\x1bOc\", ctrl: true })]],\n      [\"ctrl+left (rxvt)\", \"\\x1bOd\", [k(\"left\", { raw: \"\\x1bOd\", ctrl: true })]],\n    ])\n\n    // rxvt $ (shift) and ^ (ctrl) on tilde keys\n    table([\n      [\"shift+insert (rxvt $)\", \"\\x1b[2$\", [k(\"insert\", { raw: \"\\x1b[2$\", shift: true })]],\n      [\"shift+delete (rxvt $)\", \"\\x1b[3$\", [k(\"delete\", { raw: \"\\x1b[3$\", shift: true })]],\n      [\"shift+pgup (rxvt $)\", \"\\x1b[5$\", [k(\"pageup\", { raw: \"\\x1b[5$\", shift: true })]],\n      [\"shift+pgdn (rxvt $)\", \"\\x1b[6$\", [k(\"pagedown\", { raw: \"\\x1b[6$\", shift: true })]],\n      [\"ctrl+insert (rxvt ^)\", \"\\x1b[2^\", [k(\"insert\", { raw: \"\\x1b[2^\", ctrl: true })]],\n      [\"ctrl+delete (rxvt ^)\", \"\\x1b[3^\", [k(\"delete\", { raw: \"\\x1b[3^\", ctrl: true })]],\n      [\"ctrl+pgup (rxvt ^)\", \"\\x1b[5^\", [k(\"pageup\", { raw: \"\\x1b[5^\", ctrl: true })]],\n      [\"ctrl+pgdn (rxvt ^)\", \"\\x1b[6^\", [k(\"pagedown\", { raw: \"\\x1b[6^\", ctrl: true })]],\n    ])\n  })\n\n  describe(\"meta key combinations\", () => {\n    test(\"meta+lowercase letters\", () => {\n      const p = createParser()\n      try {\n        // Push all ESC+letter pairs at once — each should produce meta+key\n        for (const ch of \"acdeghijklmoqrstuvwxyz\".split(\"\")) {\n          p.push(Buffer.from(`\\x1b${ch}`))\n        }\n        const s = snap(p)\n        for (let i = 0; i < s.length; i++) {\n          const ch = \"acdeghijklmoqrstuvwxyz\"[i]!\n          expect(s[i]).toEqual(k(ch, { raw: `\\x1b${ch}`, meta: true }))\n        }\n      } finally {\n        p.destroy()\n      }\n    })\n\n    // Lowercase ESC+b / ESC+f stay literal meta chords, while uppercase ESC+B / ESC+F\n    // preserve the old-style meta+arrow behavior from `main`.\n    table([\n      [\"meta+b (literal chord)\", \"\\x1bb\", [k(\"b\", { raw: \"\\x1bb\", meta: true })]],\n      [\"meta+f (literal chord)\", \"\\x1bf\", [k(\"f\", { raw: \"\\x1bf\", meta: true })]],\n      [\"meta+B (old-style left)\", \"\\x1bB\", [k(\"left\", { raw: \"\\x1bB\", meta: true })]],\n      [\"meta+F (old-style right)\", \"\\x1bF\", [k(\"right\", { raw: \"\\x1bF\", meta: true })]],\n      [\"meta+n (plain letter)\", \"\\x1bn\", [k(\"n\", { raw: \"\\x1bn\", meta: true })]],\n      [\"meta+p (plain letter)\", \"\\x1bp\", [k(\"p\", { raw: \"\\x1bp\", meta: true })]],\n    ])\n\n    table([\n      [\"meta+return\", \"\\x1b\\r\", [k(\"return\", { raw: \"\\x1b\\r\", meta: true })]],\n      [\"meta+linefeed\", \"\\x1b\\n\", [k(\"linefeed\", { raw: \"\\x1b\\n\", meta: true })]],\n      [\"meta+backspace\", \"\\x1b\\x7f\", [k(\"backspace\", { raw: \"\\x1b\\x7f\", meta: true })]],\n      [\"meta+backspace (0x08)\", \"\\x1b\\b\", [k(\"backspace\", { raw: \"\\x1b\\b\", meta: true })]],\n      [\"meta+space\", \"\\x1b \", [k(\"space\", { raw: \"\\x1b \", meta: true })]],\n    ])\n\n    test(\"meta+escape (requires timeout for \\\\x1b\\\\x1b)\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\\x1b\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\\x1b\", meta: true })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    table([[\"double-ESC + [A → meta+up\", \"\\x1b\\x1b[A\", [k(\"up\", { raw: \"\\x1b\\x1b[A\", meta: true })]]])\n\n    test(\"meta+uppercase sets shift\", () => {\n      const p = createParser()\n      try {\n        // ESC + uppercase letter → meta + shift + name (uppercase preserved in parseKeypress)\n        // Excluding B and F which map to arrow keys\n        p.push(Buffer.from(\"\\x1bA\"))\n        const s = snap(p)\n        expect(s).toEqual([k(\"A\", { raw: \"\\x1bA\", meta: true, shift: true })])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"meta+ctrl+letter\", () => {\n      const p = createParser()\n      try {\n        // ESC + ctrl char (e.g. ESC + 0x01 = meta+ctrl+a)\n        p.push(Uint8Array.from([0x1b, 0x01]))\n        expect(snap(p)).toEqual([k(\"a\", { raw: \"\\x1b\\x01\", meta: true, ctrl: true })])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"meta+digit\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b5\"))\n        expect(snap(p)).toEqual([k(\"5\", { raw: \"\\x1b5\", meta: true })])\n      } finally {\n        p.destroy()\n      }\n    })\n  })\n\n  describe(\"kitty keyboard protocol\", () => {\n    // CSI codepoint u format\n    table([\n      [\"a key\", \"\\x1b[97u\", [k(\"a\", { raw: \"\\x1b[97u\" })]],\n      [\"shift+a\", \"\\x1b[97;2u\", [k(\"a\", { raw: \"\\x1b[97;2u\", shift: true })]],\n      [\"ctrl+a\", \"\\x1b[97;5u\", [k(\"a\", { raw: \"\\x1b[97;5u\", ctrl: true })]],\n      [\"alt+a\", \"\\x1b[97;3u\", [k(\"a\", { raw: \"\\x1b[97;3u\", meta: true })]],\n      [\"ctrl+shift+a\", \"\\x1b[97;6u\", [k(\"a\", { raw: \"\\x1b[97;6u\", ctrl: true, shift: true })]],\n      [\"a release\", \"\\x1b[97;1:3u\", [k(\"a\", { raw: \"\\x1b[97;1:3u\", eventType: \"release\" })]],\n      [\"escape\", \"\\x1b[27u\", [k(\"escape\", { raw: \"\\x1b[27u\" })]],\n      [\"return\", \"\\x1b[13u\", [k(\"return\", { raw: \"\\x1b[13u\" })]],\n      [\"tab\", \"\\x1b[9u\", [k(\"tab\", { raw: \"\\x1b[9u\" })]],\n      [\"backspace\", \"\\x1b[127u\", [k(\"backspace\", { raw: \"\\x1b[127u\" })]],\n      [\"delete\", \"\\x1b[57349u\", [k(\"delete\", { raw: \"\\x1b[57349u\" })]],\n      [\"insert\", \"\\x1b[57348u\", [k(\"insert\", { raw: \"\\x1b[57348u\" })]],\n      [\"f1\", \"\\x1b[57364u\", [k(\"f1\", { raw: \"\\x1b[57364u\" })]],\n      [\"f12\", \"\\x1b[57375u\", [k(\"f12\", { raw: \"\\x1b[57375u\" })]],\n    ])\n\n    // CSI 1;modifier:event letter format (kitty functional keys)\n    table([\n      [\"up press\", \"\\x1b[1;1:1A\", [k(\"up\", { raw: \"\\x1b[1;1:1A\" })]],\n      [\"up release\", \"\\x1b[1;1:3A\", [k(\"up\", { raw: \"\\x1b[1;1:3A\", eventType: \"release\" })]],\n      [\"ctrl+right\", \"\\x1b[1;5:1C\", [k(\"right\", { raw: \"\\x1b[1;5:1C\", ctrl: true })]],\n      [\"shift+left\", \"\\x1b[1;2:1D\", [k(\"left\", { raw: \"\\x1b[1;2:1D\", shift: true })]],\n      [\"home\", \"\\x1b[1;1:1H\", [k(\"home\", { raw: \"\\x1b[1;1:1H\" })]],\n      [\"end release\", \"\\x1b[1;1:3F\", [k(\"end\", { raw: \"\\x1b[1;1:3F\", eventType: \"release\" })]],\n      [\"f1 press\", \"\\x1b[1;1:1P\", [k(\"f1\", { raw: \"\\x1b[1;1:1P\" })]],\n    ])\n\n    // CSI number;modifier:event ~ format (kitty tilde keys)\n    table([\n      [\"pageup press\", \"\\x1b[5;1:1~\", [k(\"pageup\", { raw: \"\\x1b[5;1:1~\" })]],\n      [\"ctrl+delete\", \"\\x1b[3;5:1~\", [k(\"delete\", { raw: \"\\x1b[3;5:1~\", ctrl: true })]],\n      [\"insert release\", \"\\x1b[2;1:3~\", [k(\"insert\", { raw: \"\\x1b[2;1:3~\", eventType: \"release\" })]],\n    ])\n  })\n\n  describe(\"modifyOtherKeys\", () => {\n    table([\n      [\"shift+return\", \"\\x1b[27;2;13~\", [k(\"return\", { raw: \"\\x1b[27;2;13~\", shift: true })]],\n      [\"ctrl+return\", \"\\x1b[27;5;13~\", [k(\"return\", { raw: \"\\x1b[27;5;13~\", ctrl: true })]],\n      [\"ctrl+escape\", \"\\x1b[27;5;27~\", [k(\"escape\", { raw: \"\\x1b[27;5;27~\", ctrl: true })]],\n      [\"alt+tab\", \"\\x1b[27;3;9~\", [k(\"tab\", { raw: \"\\x1b[27;3;9~\", meta: true })]],\n      [\"shift+space\", \"\\x1b[27;2;32~\", [k(\"space\", { raw: \"\\x1b[27;2;32~\", shift: true })]],\n      [\"ctrl+backspace\", \"\\x1b[27;5;127~\", [k(\"backspace\", { raw: \"\\x1b[27;5;127~\", ctrl: true })]],\n      [\"shift+digit 5\", \"\\x1b[27;2;53~\", [k(\"5\", { raw: \"\\x1b[27;2;53~\", shift: true })]],\n    ])\n  })\n\n  describe(\"mouse: SGR protocol\", () => {\n    table([\n      // Button press/release\n      [\"left down\", \"\\x1b[<0;1;1M\", [sgr(\"\\x1b[<0;1;1M\", \"down\", 0, 0)]],\n      [\"left up\", \"\\x1b[<0;1;1m\", [sgr(\"\\x1b[<0;1;1m\", \"up\", 0, 0)]],\n      [\"middle down\", \"\\x1b[<1;1;1M\", [sgr(\"\\x1b[<1;1;1M\", \"down\", 0, 0, { button: 1 })]],\n      [\"middle up\", \"\\x1b[<1;1;1m\", [sgr(\"\\x1b[<1;1;1m\", \"up\", 0, 0, { button: 1 })]],\n      [\"right down\", \"\\x1b[<2;1;1M\", [sgr(\"\\x1b[<2;1;1M\", \"down\", 0, 0, { button: 2 })]],\n      [\"right up\", \"\\x1b[<2;1;1m\", [sgr(\"\\x1b[<2;1;1m\", \"up\", 0, 0, { button: 2 })]],\n      // Scroll\n      [\n        \"scroll up\",\n        \"\\x1b[<64;10;5M\",\n        [sgr(\"\\x1b[<64;10;5M\", \"scroll\", 9, 4, { scroll: { direction: \"up\", delta: 1 } })],\n      ],\n      [\n        \"scroll down\",\n        \"\\x1b[<65;10;5M\",\n        [sgr(\"\\x1b[<65;10;5M\", \"scroll\", 9, 4, { button: 1, scroll: { direction: \"down\", delta: 1 } })],\n      ],\n      [\n        \"scroll left\",\n        \"\\x1b[<66;10;5M\",\n        [sgr(\"\\x1b[<66;10;5M\", \"scroll\", 9, 4, { button: 2, scroll: { direction: \"left\", delta: 1 } })],\n      ],\n      [\n        \"scroll right\",\n        \"\\x1b[<67;10;5M\",\n        [sgr(\"\\x1b[<67;10;5M\", \"scroll\", 9, 4, { button: 0, scroll: { direction: \"right\", delta: 1 } })],\n      ],\n      // Motion (no button)\n      [\"move\", \"\\x1b[<35;20;10M\", [sgr(\"\\x1b[<35;20;10M\", \"move\", 19, 9)]],\n      // Large coordinates\n      [\"large coords\", \"\\x1b[<0;300;200M\", [sgr(\"\\x1b[<0;300;200M\", \"down\", 299, 199)]],\n      // Modifiers\n      [\"shift+left down\", \"\\x1b[<4;1;1M\", [sgr(\"\\x1b[<4;1;1M\", \"down\", 0, 0, { mods: { shift: true } })]],\n      [\"alt+left down\", \"\\x1b[<8;1;1M\", [sgr(\"\\x1b[<8;1;1M\", \"down\", 0, 0, { mods: { alt: true } })]],\n      [\"ctrl+left down\", \"\\x1b[<16;1;1M\", [sgr(\"\\x1b[<16;1;1M\", \"down\", 0, 0, { mods: { ctrl: true } })]],\n    ])\n\n    test(\"drag detection after button down\", () => {\n      const p = createParser()\n      try {\n        // Button 0 down, then motion with button 0 flag\n        p.push(Buffer.from(\"\\x1b[<0;5;5M\\x1b[<32;6;5M\"))\n        const s = snap(p)\n        expect(s).toHaveLength(2)\n        expect(s[0]).toEqual(sgr(\"\\x1b[<0;5;5M\", \"down\", 4, 4))\n        expect(s[1]).toEqual(sgr(\"\\x1b[<32;6;5M\", \"drag\", 5, 4))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"split SGR across two pushes\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[<64;10;\"))\n        expect(snap(p)).toEqual([])\n        p.push(Buffer.from(\"5M\"))\n        expect(snap(p)).toEqual([sgr(\"\\x1b[<64;10;5M\", \"scroll\", 9, 4, { scroll: { direction: \"up\", delta: 1 } })])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"multiple mouse events in one push\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[<0;1;1M\\x1b[<0;2;1M\\x1b[<0;2;1m\"))\n        const s = snap(p)\n        expect(s).toHaveLength(3)\n        expect(s[0]).toEqual(sgr(\"\\x1b[<0;1;1M\", \"down\", 0, 0))\n        expect(s[1]).toEqual(sgr(\"\\x1b[<0;2;1M\", \"down\", 1, 0))\n        expect(s[2]).toEqual(sgr(\"\\x1b[<0;2;1m\", \"up\", 1, 0))\n      } finally {\n        p.destroy()\n      }\n    })\n  })\n\n  describe(\"mouse: X10 protocol\", () => {\n    // X10: ESC [ M <button+32> <x+33> <y+33>\n    const leftDown = x10bytes(0, 0, 0)\n    const middleDown = x10bytes(1, 0, 0)\n    const rightDown = x10bytes(2, 0, 0)\n    const release = x10bytes(3, 0, 0)\n    const at1020 = x10bytes(0, 10, 20)\n    const move = x10bytes(35, 4, 5)\n    const scrollUp = x10bytes(64, 2, 3)\n    const shiftLeftDown = x10bytes(4, 0, 0)\n    const ctrlScrollUp = x10bytes(80, 7, 8)\n\n    table([\n      [\"left down (0,0)\", leftDown, [x10m(latin1(leftDown), \"down\", 0, 0)]],\n      [\"middle down\", middleDown, [x10m(latin1(middleDown), \"down\", 0, 0, { button: 1 })]],\n      [\"right down\", rightDown, [x10m(latin1(rightDown), \"down\", 0, 0, { button: 2 })]],\n      [\"release\", release, [x10m(latin1(release), \"up\", 0, 0)]],\n      [\"at position 10,20\", at1020, [x10m(latin1(at1020), \"down\", 10, 20)]],\n      [\"move with no button\", move, [x10m(latin1(move), \"move\", 4, 5, { button: -1 })]],\n      [\"scroll up\", scrollUp, [x10m(latin1(scrollUp), \"scroll\", 2, 3, { scroll: { direction: \"up\", delta: 1 } })]],\n      [\"shift+left down\", shiftLeftDown, [x10m(latin1(shiftLeftDown), \"down\", 0, 0, { mods: { shift: true } })]],\n      [\n        \"ctrl+scroll up\",\n        ctrlScrollUp,\n        [x10m(latin1(ctrlScrollUp), \"scroll\", 7, 8, { mods: { ctrl: true }, scroll: { direction: \"up\", delta: 1 } })],\n      ],\n    ])\n\n    test(\"X10 mouse followed by key\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[M !!x\"))\n        const s = snap(p)\n        expect(s).toHaveLength(2)\n        expect(s[0]).toEqual(x10m(\"\\x1b[M !!\", \"down\", 0, 0))\n        expect(s[1]).toEqual(k(\"x\"))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"split X10 across pushes waits for payload\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[M\"))\n        expect(snap(p)).toEqual([])\n        p.push(Buffer.from(\" !!\"))\n        expect(snap(p)).toEqual([x10m(\"\\x1b[M !!\", \"down\", 0, 0)])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"delayed X10 continuation after timed-out escape stays opaque\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n\n        parser.push(Buffer.from(\"[M\"))\n        expect(snap(parser)).toEqual([])\n        parser.push(Buffer.from(\" !!\"))\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"[M !!\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n  })\n\n  describe(\"UTF-8 handling\", () => {\n    table([\n      [\"2-byte (é)\", \"\\u00e9\", [k(\"\\u00e9\")]],\n      [\"3-byte (中)\", \"\\u4e2d\", [k(\"\\u4e2d\")]],\n      [\"4-byte (👍)\", \"👍\", [k(\"👍\")]],\n      [\"multiple utf-8 chars\", \"日本語\", [k(\"日\"), k(\"本\"), k(\"語\")]],\n    ])\n\n    test(\"2-byte split at byte boundary\", () => {\n      const bytes = Buffer.from(\"é\")\n      expect(bytes.length).toBe(2)\n      const p = createParser()\n      try {\n        p.push(bytes.subarray(0, 1))\n        expect(snap(p)).toEqual([])\n        p.push(bytes.subarray(1))\n        expect(snap(p)).toEqual([k(\"é\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"3-byte split at every boundary\", () => {\n      const bytes = Buffer.from(\"中\")\n      expect(bytes.length).toBe(3)\n      for (let split = 1; split < bytes.length; split++) {\n        const p = createParser()\n        try {\n          p.push(bytes.subarray(0, split))\n          expect(snap(p)).toEqual([])\n          p.push(bytes.subarray(split))\n          expect(snap(p)).toEqual([k(\"中\")])\n        } finally {\n          p.destroy()\n        }\n      }\n    })\n\n    test(\"4-byte split at every boundary\", () => {\n      const bytes = Buffer.from(\"👍\")\n      expect(bytes.length).toBe(4)\n      for (let split = 1; split < bytes.length; split++) {\n        const p = createParser()\n        try {\n          p.push(bytes.subarray(0, split))\n          expect(snap(p)).toEqual([])\n          p.push(bytes.subarray(split))\n          expect(snap(p)).toEqual([k(\"👍\")])\n        } finally {\n          p.destroy()\n        }\n      }\n    })\n\n    test(\"invalid UTF-8 lead (0xC0) followed by ASCII falls back to legacy high-byte\", () => {\n      const p = createParser()\n      try {\n        p.push(Uint8Array.from([0xc0, 0x41]))\n        const s = snap(p)\n        expect(s).toHaveLength(2)\n        // 0xC0 - 128 = 0x40 = '@', treated as ESC + '@' → legacy path\n        expect(s[0]!.type).toBe(\"key\")\n        expect(s[1]).toEqual(k(\"a\", { raw: \"A\", shift: true }))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"invalid continuation byte after valid lead falls back to legacy\", () => {\n      const p = createParser()\n      try {\n        p.push(Uint8Array.from([0xe9])) // valid 3-byte lead\n        expect(snap(p)).toEqual([]) // waits for continuation\n        p.push(Buffer.from(\"x\")) // not a continuation byte\n        const s = snap(p)\n        expect(s).toEqual([\n          k(\"i\", { raw: \"\\x1bi\", meta: true }), // 0xe9 → legacy: 0xe9-128=0x69='i', ESC prefix\n          k(\"x\"),\n        ])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"legacy single high-byte on timeout\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Uint8Array.from([0xe9]))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"i\", { raw: \"\\x1bi\", meta: true })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"high byte 0xFF on timeout → meta+backspace\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Uint8Array.from([0xff]))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        // 0xFF - 128 = 0x7F = DEL, so ESC + DEL = meta+backspace\n        expect(snap(parser)).toEqual([k(\"backspace\", { raw: \"\\x1b\\x7f\", meta: true })])\n      } finally {\n        parser.destroy()\n      }\n    })\n  })\n\n  describe(\"protocol responses\", () => {\n    table([\n      // OSC (BEL-terminated)\n      [\"OSC (BEL)\", \"\\x1b]4;0;#ffffff\\x07\", [resp(\"osc\", \"\\x1b]4;0;#ffffff\\x07\")]],\n      // OSC (ESC \\\\ terminated)\n      [\"OSC (ST)\", \"\\x1b]4;0;rgb:ff/ff/ff\\x1b\\\\\", [resp(\"osc\", \"\\x1b]4;0;rgb:ff/ff/ff\\x1b\\\\\")]],\n      // DCS\n      [\"DCS\", \"\\x1bP>|kitty(0.40.1)\\x1b\\\\\", [resp(\"dcs\", \"\\x1bP>|kitty(0.40.1)\\x1b\\\\\")]],\n      // APC\n      [\"APC\", \"\\x1b_Gi=1;OK\\x1b\\\\\", [resp(\"apc\", \"\\x1b_Gi=1;OK\\x1b\\\\\")]],\n      // Focus\n      [\"focus in\", \"\\x1b[I\", [resp(\"csi\", \"\\x1b[I\")]],\n      [\"focus out\", \"\\x1b[O\", [resp(\"csi\", \"\\x1b[O\")]],\n      // DA (Device Attributes)\n      [\"DA1\", \"\\x1b[?62;1;2;6;7;8;9;15;22c\", [resp(\"csi\", \"\\x1b[?62;1;2;6;7;8;9;15;22c\")]],\n      // CPR (Cursor Position Report)\n      [\"CPR\", \"\\x1b[24;80R\", [resp(\"csi\", \"\\x1b[24;80R\")]],\n      // Window/cell size\n      [\"window size\", \"\\x1b[4;600;800t\", [resp(\"csi\", \"\\x1b[4;600;800t\")]],\n      // Mode report\n      [\"mode report\", \"\\x1b[?2004;1$y\", [resp(\"csi\", \"\\x1b[?2004;1$y\")]],\n    ])\n\n    test(\"all three protocol responses in one push\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b]4;0;#fff\\x07\\x1bP>|test\\x1b\\\\\\x1b_OK\\x1b\\\\\"))\n        expect(snap(p)).toEqual([\n          resp(\"osc\", \"\\x1b]4;0;#fff\\x07\"),\n          resp(\"dcs\", \"\\x1bP>|test\\x1b\\\\\"),\n          resp(\"apc\", \"\\x1b_OK\\x1b\\\\\"),\n        ])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"split OSC across pushes\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b]4;0;\"))\n        expect(snap(p)).toEqual([])\n        p.push(Buffer.from(\"#ffffff\\x07\"))\n        expect(snap(p)).toEqual([resp(\"osc\", \"\\x1b]4;0;#ffffff\\x07\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"split DCS terminator ESC \\\\ across pushes\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1bPtest\\x1b\"))\n        expect(snap(p)).toEqual([])\n        p.push(Buffer.from(\"\\\\\"))\n        expect(snap(p)).toEqual([resp(\"dcs\", \"\\x1bPtest\\x1b\\\\\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"focus events interleaved with keys\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"a\\x1b[Ib\\x1b[Oc\"))\n        expect(snap(p)).toEqual([k(\"a\"), resp(\"csi\", \"\\x1b[I\"), k(\"b\"), resp(\"csi\", \"\\x1b[O\"), k(\"c\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"partial OSC flushes on timeout as unknown\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b]incomplete\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b]incomplete\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial DCS flushes on timeout as unknown\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1bPpartial\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1bPpartial\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial APC flushes on timeout as unknown\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b_partial\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b_partial\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial generic CSI flushes on timeout as unknown\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[123\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[123\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial kitty CSI stays pending after timeout\", () => {\n      const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })\n      try {\n        parser.push(Buffer.from(\"\\x1b[118;5\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial kitty CSI stays pending after timeout when split after first semicolon\", () => {\n      const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })\n      try {\n        parser.push(Buffer.from(\"\\x1b[97;\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"2u\"))\n        expect(snap(parser)).toEqual([k(\"a\", { shift: true, raw: \"\\x1b[97;2u\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial kitty CSI stays pending after timeout for higher modifier bits\", () => {\n      const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })\n      try {\n        parser.push(Buffer.from(\"\\x1b[97;9\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"u\"))\n        const event = parser.read()\n        expect(event?.type).toBe(\"key\")\n        if (!event || event.type !== \"key\") throw new Error(\"expected key event\")\n        expect(event.raw).toBe(\"\\x1b[97;9u\")\n        expect(event.key.name).toBe(\"a\")\n        expect(event.key.super).toBe(true)\n        expect(parser.read()).toBeNull()\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial kitty special-key CSI stays pending after timeout\", () => {\n      const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })\n      try {\n        parser.push(Buffer.from(\"\\x1b[1;1:\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"3A\"))\n        expect(snap(parser)).toEqual([k(\"up\", { raw: \"\\x1b[1;1:3A\", eventType: \"release\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial SGR mouse CSI stays pending after timeout\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[<35;20\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\";5m\"))\n        expect(snap(parser)).toEqual([sgr(\"\\x1b[<35;20;5m\", \"move\", 19, 4)])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"split CSI across reads reassembles after timeout\", () => {\n      const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })\n      try {\n        // Kitty Ctrl+V release split across two reads\n        parser.push(Buffer.from(\"\\x1b[118;5\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        // Stays pending — not flushed\n        expect(snap(parser)).toEqual([])\n        parser.push(Buffer.from(\";3u\"))\n        expect(snap(parser)).toEqual([k(\"v\", { ctrl: true, raw: \"\\x1b[118;5;3u\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"split kitty escape CSI across reads reassembles after timeout\", () => {\n      const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })\n      try {\n        parser.push(Buffer.from(\"\\x1b[27;5\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n        parser.push(Buffer.from(\"u\"))\n        expect(snap(parser)).toEqual([k(\"escape\", { ctrl: true, raw: \"\\x1b[27;5u\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out standard one-semicolon CSI key flushes before later text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[1;5\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[1;5\")])\n\n        parser.push(Buffer.from(\"A\"))\n        expect(snap(parser)).toEqual([k(\"a\", { raw: \"A\", shift: true })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out one-semicolon CSI response flushes before later text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[24;80\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[24;80\")])\n\n        parser.push(Buffer.from(\"R\"))\n        expect(snap(parser)).toEqual([k(\"r\", { raw: \"R\", shift: true })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out partial kitty CSI resyncs on a later ESC\", () => {\n      const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })\n      try {\n        parser.push(Buffer.from(\"\\x1b[118;5\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"\\x1b[A\"))\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[118;5\"), k(\"up\", { raw: \"\\x1b[A\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out partial kitty CSI flushes before unrelated later text\", () => {\n      const { parser, clock } = createTimedParser({ protocolContext: { kittyKeyboardEnabled: true } })\n      try {\n        parser.push(Buffer.from(\"\\x1b[118;5\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"a\"))\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[118;5\"), k(\"a\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial generic CSI timeout flush does not swallow later text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[123\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[123\")])\n\n        parser.push(Buffer.from(\"a\"))\n        expect(snap(parser)).toEqual([k(\"a\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial large-parameter CSI flushes on timeout before later text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[80;120\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[80;120\")])\n\n        parser.push(Buffer.from(\"a\"))\n        expect(snap(parser)).toEqual([k(\"a\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial OSC timeout flush does not swallow later text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b]52;c;\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b]52;c;\")])\n\n        parser.push(Buffer.from(\"abc\"))\n        expect(snap(parser)).toEqual([k(\"a\"), k(\"b\"), k(\"c\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial OSC timeout flush does not swallow later escape sequences\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b]52;c;\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b]52;c;\")])\n\n        parser.push(Buffer.from(\"\\x1b[A\"))\n        expect(snap(parser)).toEqual([k(\"up\", { raw: \"\\x1b[A\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n  })\n\n  describe(\"protocol context\", () => {\n    test(\"partial explicit-width CPR stays pending after timeout when probe is active\", () => {\n      const { parser, clock } = createTimedParser({\n        protocolContext: { explicitWidthCprActive: true },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[1;2\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"R\"))\n        expect(snap(parser)).toEqual([resp(\"csi\", \"\\x1b[1;2R\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial explicit-width CPR flushes before later text when probe is inactive\", () => {\n      const { parser, clock } = createTimedParser()\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[1;2\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[1;2\")])\n\n        parser.push(Buffer.from(\"R\"))\n        expect(snap(parser)).toEqual([k(\"r\", { raw: \"R\", shift: true })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial pixel resolution response stays pending after timeout while query is active\", () => {\n      const { parser, clock } = createTimedParser({\n        protocolContext: { pixelResolutionQueryActive: true },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[4;1080;192\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"0t\"))\n        expect(snap(parser)).toEqual([resp(\"csi\", \"\\x1b[4;1080;1920t\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial DECRPM stays pending after timeout while capability probe is active\", () => {\n      const { parser, clock } = createTimedParser({\n        protocolContext: { privateCapabilityRepliesActive: true },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[?1016;2$\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"y\"))\n        expect(snap(parser)).toEqual([resp(\"csi\", \"\\x1b[?1016;2$y\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"partial DA1 stays pending after timeout while capability probe is active\", () => {\n      const { parser, clock } = createTimedParser({\n        protocolContext: { privateCapabilityRepliesActive: true },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[?62;\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"c\"))\n        expect(snap(parser)).toEqual([resp(\"csi\", \"\\x1b[?62;c\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out modified CSI key still flushes before later final byte\", () => {\n      const { parser, clock } = createTimedParser({\n        protocolContext: { explicitWidthCprActive: true },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[1;5\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"A\"))\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[1;5\"), k(\"a\", { raw: \"A\", shift: true })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"generic row/col CPR does not reassemble during explicit-width probe window\", () => {\n      const { parser, clock } = createTimedParser({\n        protocolContext: { explicitWidthCprActive: true },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[24;80\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[24;80\")])\n\n        parser.push(Buffer.from(\"R\"))\n        expect(snap(parser)).toEqual([k(\"r\", { raw: \"R\", shift: true })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"deferred explicit-width CPR flushes when probe context is cleared\", () => {\n      const { parser, clock } = createTimedParser({\n        protocolContext: { explicitWidthCprActive: true },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[1;2\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([])\n\n        parser.updateProtocolContext({ explicitWidthCprActive: false })\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[1;2\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out pending explicit-width CPR does not rearm until more bytes arrive\", () => {\n      const clock = new ManualClock()\n      let timeoutFlushes = 0\n      let parser!: StdinParser\n      parser = new StdinParser({\n        armTimeouts: true,\n        clock,\n        protocolContext: { explicitWidthCprActive: true },\n        onTimeoutFlush: () => {\n          timeoutFlushes += 1\n          parser.drain(() => {})\n        },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[1;2\"))\n        clock.advance(10)\n        expect(timeoutFlushes).toBe(1)\n\n        clock.advance(50)\n        expect(timeoutFlushes).toBe(1)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\";\"))\n        clock.advance(10)\n        expect(timeoutFlushes).toBe(2)\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out pending private reply does not rearm until more bytes arrive\", () => {\n      const clock = new ManualClock()\n      let timeoutFlushes = 0\n      let parser!: StdinParser\n      parser = new StdinParser({\n        armTimeouts: true,\n        clock,\n        protocolContext: { privateCapabilityRepliesActive: true },\n        onTimeoutFlush: () => {\n          timeoutFlushes += 1\n          parser.drain(() => {})\n        },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[?1016;2$\"))\n        clock.advance(10)\n        expect(timeoutFlushes).toBe(1)\n\n        clock.advance(50)\n        expect(timeoutFlushes).toBe(1)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\";\"))\n        clock.advance(10)\n        expect(timeoutFlushes).toBe(2)\n      } finally {\n        parser.destroy()\n      }\n    })\n  })\n\n  describe(\"bracketed paste\", () => {\n    table([\n      [\"simple paste\", \"\\x1b[200~hello\\x1b[201~\", [paste(\"hello\")]],\n      [\"empty paste\", \"\\x1b[200~\\x1b[201~\", [paste(\"\")]],\n      [\"paste with newlines\", \"\\x1b[200~line1\\nline2\\x1b[201~\", [paste(\"line1\\nline2\")]],\n      [\"paste with tabs\", \"\\x1b[200~a\\tb\\x1b[201~\", [paste(\"a\\tb\")]],\n      [\"paste with ESC in body\", \"\\x1b[200~abc\\x1bdef\\x1b[201~\", [paste(\"abc\\x1bdef\")]],\n    ])\n\n    test(\"split paste start marker across pushes\", () => {\n      const start = \"\\x1b[200~\"\n      for (let split = 1; split < start.length; split++) {\n        const p = createParser()\n        try {\n          p.push(Buffer.from(start.slice(0, split)))\n          p.push(Buffer.from(start.slice(split) + \"hi\\x1b[201~\"))\n          expect(snap(p)).toEqual([paste(\"hi\")])\n        } finally {\n          p.destroy()\n        }\n      }\n    })\n\n    test(\"split paste end marker at every boundary\", () => {\n      const end = \"\\x1b[201~\"\n      for (let split = 1; split < end.length; split++) {\n        const p = createParser()\n        try {\n          p.push(Buffer.from(\"\\x1b[200~hello\"))\n          p.push(Buffer.from(end.slice(0, split)))\n          expect(snap(p)).toEqual([])\n          p.push(Buffer.from(end.slice(split)))\n          expect(snap(p)).toEqual([paste(\"hello\")])\n        } finally {\n          p.destroy()\n        }\n      }\n    })\n\n    test(\"paste body bytes do not alias caller buffers across pushes\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[200~\"))\n\n        const chunk = Buffer.from(\"hello\")\n        p.push(chunk)\n        chunk.fill(0x78)\n\n        p.push(Buffer.from(\"\\x1b[201~\"))\n        expect(snap(p)).toEqual([paste(\"hello\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"near-match end markers are part of paste body\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[200~abc\\x1b[202~def\\x1b[201~\"))\n        expect(snap(p)).toEqual([paste(\"abc\\x1b[202~def\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"doubled ESC before paste end marker\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[200~abc\\x1b\"))\n        expect(snap(p)).toEqual([])\n        p.push(Buffer.from(\"\\x1b[201~\"))\n        expect(snap(p)).toEqual([paste(\"abc\\x1b\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"large paste does not grow parser buffer\", () => {\n      const p = createParser({ maxPendingBytes: 32 })\n      const payload = \"x\".repeat(100_000)\n      try {\n        p.push(Buffer.from(`\\x1b[200~${payload}\\x1b[201~z`))\n        expect(snap(p)).toEqual([paste(payload), k(\"z\")])\n        expect(p.bufferCapacity).toBeLessThanOrEqual(512)\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"large paste across many small chunks\", () => {\n      const p = createParser({ maxPendingBytes: 32 })\n      try {\n        p.push(Buffer.from(\"\\x1b[200~\"))\n        for (let i = 0; i < 1000; i++) p.push(Buffer.from(\"chunk \"))\n        p.push(Buffer.from(\"\\x1b[201~\"))\n        const s = snap(p)\n        expect(s).toHaveLength(1)\n        expect(s[0]!.type).toBe(\"paste\")\n        expect((s[0] as PasteSnap).bytes).toHaveLength(6000)\n        expect(p.bufferCapacity).toBeLessThanOrEqual(512)\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"trailing bytes after paste end are parsed normally\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[200~hello\\x1b[201~\\x1b[A\"))\n        expect(snap(p)).toEqual([paste(\"hello\"), k(\"up\", { raw: \"\\x1b[A\" })])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"back-to-back pastes\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[200~first\\x1b[201~\\x1b[200~second\\x1b[201~\"))\n        expect(snap(p)).toEqual([paste(\"first\"), paste(\"second\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"paste with UTF-8 content\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[200~日本語👍\\x1b[201~\"))\n        expect(snap(p)).toEqual([paste(\"日本語👍\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"paste with UTF-8 split across chunks\", () => {\n      const p = createParser()\n      const emoji = Buffer.from(\"👍\")\n      try {\n        p.push(Buffer.from(\"\\x1b[200~\"))\n        p.push(emoji.subarray(0, 2))\n        p.push(emoji.subarray(2))\n        p.push(Buffer.from(\"\\x1b[201~\"))\n        expect(snap(p)).toEqual([paste(\"👍\")])\n      } finally {\n        p.destroy()\n      }\n    })\n  })\n\n  describe(\"ESC-less SGR continuation recovery\", () => {\n    test(\"after timed-out ESC, continuation is not split into text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n\n        parser.push(Buffer.from(\"[<35;20;5m\"))\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"[<35;20;5m\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"after timed-out ESC, split continuation across pushes is not split into text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n\n        parser.push(Buffer.from(\"[\"))\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\"<35;20;5m\"))\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"[<35;20;5m\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"after timed-out ESC, partial [< waits, then timeout flushes as one response\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n\n        parser.push(Buffer.from(\"[<35;20\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"[<35;20\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"after timed-out ESC, [< followed by non-digit aborts immediately\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n\n        parser.push(Buffer.from(\"[<x\"))\n        const s = snap(parser)\n        expect(s).toHaveLength(2)\n        expect(s[0]).toEqual(resp(\"unknown\", \"[<\"))\n        expect(s[1]).toEqual(k(\"x\"))\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"without prior flushed ESC, [< stays literal text\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"[<35;20;5m\"))\n        expect(snap(p)).toEqual(\"[<35;20;5m\".split(\"\").map((char) => k(char)))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"without prior flushed ESC, standalone [ then < stay as individual keys\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"[\"))\n        expect(snap(p)).toEqual([k(\"[\")])\n        p.push(Buffer.from(\"<\"))\n        expect(snap(p)).toEqual([k(\"<\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"after timed-out ESC, bare [ waits for more and then flushes as text\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n\n        parser.push(Buffer.from(\"[\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"[\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n  })\n\n  describe(\"timeout behavior\", () => {\n    test(\"timeout at exact boundary (9ms no fire, 10ms fires)\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(9)\n        expect(snap(parser)).toEqual([])\n        clock.advance(1)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timeout resets when more bytes arrive\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[<35;20;\"))\n        clock.advance(9) // almost timeout\n        parser.push(Buffer.from(\"5\")) // new byte resets timer\n        expect(snap(parser)).toEqual([])\n        clock.advance(9) // almost timeout again\n        expect(snap(parser)).toEqual([])\n        parser.push(Buffer.from(\"m\")) // complete\n        expect(snap(parser)).toEqual([sgr(\"\\x1b[<35;20;5m\", \"move\", 19, 4)])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out pending kitty CSI does not rearm until more bytes arrive\", () => {\n      const clock = new ManualClock()\n      let timeoutFlushes = 0\n      let parser!: StdinParser\n      parser = new StdinParser({\n        armTimeouts: true,\n        clock,\n        protocolContext: { kittyKeyboardEnabled: true },\n        onTimeoutFlush: () => {\n          timeoutFlushes += 1\n          parser.drain(() => {})\n        },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[118;5\"))\n        clock.advance(10)\n        expect(timeoutFlushes).toBe(1)\n\n        clock.advance(50)\n        expect(timeoutFlushes).toBe(1)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\";\"))\n        clock.advance(10)\n        expect(timeoutFlushes).toBe(2)\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timed-out pending SGR mouse CSI does not rearm until more bytes arrive\", () => {\n      const clock = new ManualClock()\n      let timeoutFlushes = 0\n      let parser!: StdinParser\n      parser = new StdinParser({\n        armTimeouts: true,\n        clock,\n        onTimeoutFlush: () => {\n          timeoutFlushes += 1\n          parser.drain(() => {})\n        },\n      })\n\n      try {\n        parser.push(Buffer.from(\"\\x1b[<35;20\"))\n        clock.advance(10)\n        expect(timeoutFlushes).toBe(1)\n\n        clock.advance(50)\n        expect(timeoutFlushes).toBe(1)\n        expect(snap(parser)).toEqual([])\n\n        parser.push(Buffer.from(\";\"))\n        clock.advance(10)\n        expect(timeoutFlushes).toBe(2)\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"timeout does not fire during paste mode\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[200~partial\"))\n        clock.advance(100) // way past timeout\n        expect(snap(parser)).toEqual([]) // still collecting paste\n        parser.push(Buffer.from(\"\\x1b[201~\"))\n        expect(snap(parser)).toEqual([paste(\"partial\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"multiple sequential timeouts\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"custom timeout delay\", () => {\n      const { parser, clock } = createTimedParser({ timeoutMs: 50 })\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(49)\n        expect(snap(parser)).toEqual([])\n        clock.advance(1)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"data completing sequence before timeout cancels flush\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b\"))\n        clock.advance(5) // halfway to timeout\n        parser.push(Buffer.from(\"[A\")) // completes arrow sequence\n        expect(snap(parser)).toEqual([k(\"up\", { raw: \"\\x1b[A\" })])\n        clock.advance(100) // timeout would have fired, but sequence is done\n        expect(snap(parser)).toEqual([])\n      } finally {\n        parser.destroy()\n      }\n    })\n  })\n\n  describe(\"embedded ESC abort\", () => {\n    test(\"ESC inside partial CSI flushes as unknown, restarts\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[<35;\\x1b[<35;20;5m\"))\n        expect(snap(p)).toEqual([resp(\"unknown\", \"\\x1b[<35;\"), sgr(\"\\x1b[<35;20;5m\", \"move\", 19, 4)])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"ESC inside partial CSI with no following data\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[123\\x1b\"))\n        const s = snap(parser)\n        // first part flushed as unknown response, ESC starts new escape\n        expect(s).toEqual([resp(\"unknown\", \"\\x1b[123\")])\n        // the trailing ESC is pending\n        clock.advance(10)\n        expect(snap(parser)).toEqual([k(\"escape\", { raw: \"\\x1b\" })])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"ESC inside OSC restarts parsing\", () => {\n      const p = createParser()\n      try {\n        // ESC ] ... ESC ESC [ A — the first ESC after OSC body starts ST check,\n        // but second ESC byte is not \\, so sawEsc resets. Then ESC starts escape.\n        // Actually: \\x1b]foo has sawEsc=false. Then \\x1b sets sawEsc=true.\n        // Then [ is not \\, so sawEsc resets to false and [ is consumed as content.\n        // Then \\x1b sets sawEsc=true. Then \\ (0x5c = \\\\) terminates OSC.\n        p.push(Buffer.from(\"\\x1b]foo\\x1b\\\\\"))\n        expect(snap(p)).toEqual([resp(\"osc\", \"\\x1b]foo\\x1b\\\\\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"ESC in SS3 flushes partial as unknown\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1bO\\x1b[A\"))\n        expect(snap(p)).toEqual([resp(\"unknown\", \"\\x1bO\"), k(\"up\", { raw: \"\\x1b[A\" })])\n      } finally {\n        p.destroy()\n      }\n    })\n  })\n\n  describe(\"chunk-shape invariance\", () => {\n    const sequences = [\n      \"abc\", // multiple ASCII\n      \"\\x1b[A\", // arrow\n      \"\\x1bOP\", // SS3 F1\n      \"\\x1b[[A\", // Cygwin F1\n      \"\\x1b[[5~\", // putty pageup\n      \"\\x1b[<0;10;20M\", // SGR mouse\n      \"\\x1b[M !!\", // X10 mouse\n      \"\\x1b]4;0;#ffffff\\x07\", // OSC\n      \"\\x1bP>|test\\x1b\\\\\", // DCS\n      \"\\x1b_OK\\x1b\\\\\", // APC\n      \"\\x1b[200~hello\\x1b[201~\", // paste\n      \"\\x1b[I\", // focus in\n      \"\\x1b[1;5A\", // ctrl+up\n      \"\\x1b[97u\", // kitty key\n      \"\\x1b[27;2;13~\", // modifyOtherKeys\n    ]\n\n    for (const seq of sequences) {\n      test(`byte-at-a-time: ${JSON.stringify(seq).slice(1, -1).slice(0, 30)}`, () => {\n        assertChunkInvariant(Buffer.from(seq))\n      })\n    }\n\n    test(\"mixed stream byte-at-a-time\", () => {\n      const stream = Buffer.concat([\n        Buffer.from(\"x\"),\n        Buffer.from(\"\\x1b[<64;10;5M\"),\n        Buffer.from(\"\\x1b[I\"),\n        Buffer.from(\"\\x1b]4;0;#fff\\x07\"),\n        Buffer.from(\"\\x1b[200~paste\\x1b[201~\"),\n        Buffer.from(\"👍\"),\n      ])\n      assertChunkInvariant(stream)\n    })\n\n    test(\"random two-chunk splits\", () => {\n      const stream = Buffer.from(\"x\\x1b[<64;10;5M\\x1b[I\\x1b]4;0;#fff\\x07\\x1b[200~p\\x1b[201~y\")\n      const whole = createParser()\n      try {\n        whole.push(stream)\n        const expected = snap(whole)\n        // Try splitting at every possible position\n        for (let split = 1; split < stream.length - 1; split++) {\n          const p = createParser()\n          try {\n            p.push(stream.subarray(0, split))\n            p.push(stream.subarray(split))\n            expect(snap(p)).toEqual(expected)\n          } finally {\n            p.destroy()\n          }\n        }\n      } finally {\n        whole.destroy()\n      }\n    })\n\n    const comboAtoms: Array<[label: string, input: ChunkInput]> = [\n      [\"ascii\", \"xy\"],\n      [\"utf8\", \"👍\"],\n      [\"arrow\", \"\\x1b[A\"],\n      [\"sgr\", \"\\x1b[<64;10;5M\"],\n      [\"x10\", x10bytes(0, 0, 0)],\n      [\"osc\", \"\\x1b]4;0;#fff\\x07\"],\n      [\"paste\", \"\\x1b[200~p\\x1b[201~\"],\n      [\"kitty\", \"\\x1b[97u\"],\n    ]\n\n    for (const [firstLabel, first] of comboAtoms) {\n      for (const [secondLabel, second] of comboAtoms) {\n        test(`${firstLabel} + ${secondLabel} across every two-chunk split`, () => {\n          const stream = concatChunks([first, second])\n          const expected = snapChunks([stream])\n\n          expect(snapChunks([first, second])).toEqual(expected)\n          for (let split = 1; split < stream.length; split++) {\n            expect(snapChunks([stream.subarray(0, split), stream.subarray(split)])).toEqual(expected)\n          }\n        })\n      }\n    }\n  })\n\n  describe(\"state management\", () => {\n    test(\"reset clears pending bytes and releases capacity\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[\"))\n        expect(snap(p)).toEqual([])\n        p.push(Buffer.alloc(4096, 0x78)) // 'x' bytes to grow buffer\n        p.reset()\n        expect(snap(p)).toEqual([])\n        expect(p.bufferCapacity).toBeLessThanOrEqual(256)\n        // parser works normally after reset\n        p.push(Buffer.from(\"a\"))\n        expect(snap(p)).toEqual([k(\"a\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"reset during paste mode clears paste state\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[200~partial paste\"))\n        expect(snap(p)).toEqual([])\n        p.reset()\n        expect(snap(p)).toEqual([])\n        // parser works normally after reset\n        p.push(Buffer.from(\"a\"))\n        expect(snap(p)).toEqual([k(\"a\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"reset during escape sequence clears state\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[\"))\n        expect(snap(p)).toEqual([])\n        p.reset()\n        // After reset, the partial CSI is gone; new input starts fresh\n        p.push(Buffer.from(\"A\"))\n        expect(snap(p)).toEqual([k(\"a\", { raw: \"A\", shift: true })]) // 'A' = shift+a\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"double reset is safe\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[\"))\n        p.reset()\n        p.reset()\n        p.push(Buffer.from(\"x\"))\n        expect(snap(p)).toEqual([k(\"x\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"double destroy is safe\", () => {\n      const p = createParser()\n      p.destroy()\n      expect(() => p.destroy()).not.toThrow()\n    })\n\n    test(\"push after destroy throws\", () => {\n      const p = createParser()\n      p.destroy()\n      expect(() => p.push(Buffer.from(\"a\"))).toThrow(\"destroyed\")\n    })\n\n    test(\"read after destroy throws\", () => {\n      const p = createParser()\n      p.destroy()\n      expect(() => p.read()).toThrow(\"destroyed\")\n    })\n\n    test(\"drain after destroy throws\", () => {\n      const p = createParser()\n      p.destroy()\n      expect(() => p.drain(() => {})).toThrow(\"destroyed\")\n    })\n\n    test(\"destroy during drain stops iteration\", () => {\n      const p = createParser()\n      p.push(Buffer.from(\"abc\"))\n      let count = 0\n      expect(() => {\n        p.drain(() => {\n          count++\n          if (count === 1) p.destroy()\n        })\n      }).not.toThrow()\n      expect(count).toBe(1)\n    })\n\n    test(\"read returns null when queue is empty\", () => {\n      const p = createParser()\n      try {\n        expect(p.read()).toBeNull()\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"read pops events one at a time\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"abc\"))\n        const e1 = p.read()\n        const e2 = p.read()\n        const e3 = p.read()\n        const e4 = p.read()\n        expect(e1).not.toBeNull()\n        expect(e2).not.toBeNull()\n        expect(e3).not.toBeNull()\n        expect(e4).toBeNull()\n        expect(snapshotEvent(e1!)).toEqual(k(\"a\"))\n        expect(snapshotEvent(e2!)).toEqual(k(\"b\"))\n        expect(snapshotEvent(e3!)).toEqual(k(\"c\"))\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"overflow flushes incomplete protocols as one unknown response and recovers\", () => {\n      const longDigits = \"1\".repeat(40)\n      const longOsc = \"a\".repeat(40)\n      const longDcs = \"x\".repeat(40)\n      const cases: Array<[label: string, chunks: ChunkInput[], expected: Snap[]]> = [\n        [\"CSI\", [`\\x1b[${longDigits}`], [resp(\"unknown\", `\\x1b[${longDigits}`)]],\n        [\"OSC\", [`\\x1b]${longOsc}`], [resp(\"unknown\", `\\x1b]${longOsc}`)]],\n        [\"DCS + recovery\", [`\\x1bP${longDcs}`, \"z\"], [resp(\"unknown\", `\\x1bP${longDcs}`), k(\"z\")]],\n      ]\n\n      for (const [label, chunks, expected] of cases) {\n        expect(snapChunks(chunks, { maxPendingBytes: 16 })).toEqual(expected)\n      }\n    })\n  })\n\n  describe(\"multi-event interleaving\", () => {\n    table([\n      [\n        \"key + mouse\",\n        \"x\\x1b[<64;10;5M\",\n        [k(\"x\"), sgr(\"\\x1b[<64;10;5M\", \"scroll\", 9, 4, { scroll: { direction: \"up\", delta: 1 } })],\n      ],\n      [\n        \"mouse + key\",\n        \"\\x1b[<64;10;5Mx\",\n        [sgr(\"\\x1b[<64;10;5M\", \"scroll\", 9, 4, { scroll: { direction: \"up\", delta: 1 } }), k(\"x\")],\n      ],\n      [\"key + focus + key\", \"a\\x1b[Ib\", [k(\"a\"), resp(\"csi\", \"\\x1b[I\"), k(\"b\")]],\n      [\"paste + key\", \"\\x1b[200~hi\\x1b[201~z\", [paste(\"hi\"), k(\"z\")]],\n      [\"multiple keys\", \"abc\", [k(\"a\"), k(\"b\"), k(\"c\")]],\n      [\n        \"arrow + text + mouse\",\n        \"\\x1b[Ax\\x1b[<0;1;1M\",\n        [k(\"up\", { raw: \"\\x1b[A\" }), k(\"x\"), sgr(\"\\x1b[<0;1;1M\", \"down\", 0, 0)],\n      ],\n    ])\n\n    test(\"OSC + key + mouse + paste in one push\", () => {\n      const p = createParser()\n      try {\n        const input = \"\\x1b]4;0;#fff\\x07a\\x1b[<0;1;1M\\x1b[200~p\\x1b[201~\"\n        p.push(Buffer.from(input))\n        expect(snap(p)).toEqual([\n          resp(\"osc\", \"\\x1b]4;0;#fff\\x07\"),\n          k(\"a\"),\n          sgr(\"\\x1b[<0;1;1M\", \"down\", 0, 0),\n          paste(\"p\"),\n        ])\n      } finally {\n        p.destroy()\n      }\n    })\n  })\n\n  describe(\"negative and edge cases\", () => {\n    test(\"push with empty buffer emits an empty key event\", () => {\n      const p = createParser()\n      try {\n        p.push(new Uint8Array(0))\n        expect(snap(p)).toEqual([k(\"\")])\n      } finally {\n        p.destroy()\n      }\n    })\n\n    test(\"drain with no events does not call callback\", () => {\n      const p = createParser()\n      try {\n        let called = false\n        p.drain(() => {\n          called = true\n        })\n        expect(called).toBe(false)\n      } finally {\n        p.destroy()\n      }\n    })\n\n    table([\n      [\"CSI with unknown final byte produces empty-name key\", \"\\x1b[h\", [k(\"\", { raw: \"\\x1b[h\" })]],\n      [\"ESC followed by punctuation stays one empty-name key\", \"\\x1b!\", [k(\"\", { raw: \"\\x1b!\" })]],\n      [\"ESC followed by N becomes meta+shift+N\", \"\\x1bN\", [k(\"N\", { raw: \"\\x1bN\", meta: true, shift: true })]],\n      [\"malformed SGR mouse falls through as empty-name CSI key\", \"\\x1b[<0M\", [k(\"\", { raw: \"\\x1b[<0M\" })]],\n      [\"bracketed paste end outside paste mode is a CSI response\", \"\\x1b[201~\", [resp(\"csi\", \"\\x1b[201~\")]],\n    ])\n\n    test(\"partial X10 times out as one unknown response\", () => {\n      const { parser, clock } = createTimedParser()\n      try {\n        parser.push(Buffer.from(\"\\x1b[M !\"))\n        expect(snap(parser)).toEqual([])\n        clock.advance(10)\n        expect(snap(parser)).toEqual([resp(\"unknown\", \"\\x1b[M !\")])\n      } finally {\n        parser.destroy()\n      }\n    })\n\n    test(\"very long paste with partial end marker in every chunk\", () => {\n      const p = createParser()\n      try {\n        p.push(Buffer.from(\"\\x1b[200~\"))\n        for (let i = 0; i < 100; i++) p.push(Buffer.from(\"\\x1b[20\"))\n        p.push(Buffer.from(\"\\x1b[201~\"))\n        expect(snap(p)).toEqual([paste(\"\\x1b[20\".repeat(100))])\n      } finally {\n        p.destroy()\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/stdin-parser.ts",
    "content": "// Byte-level stdin parser that turns raw terminal input into typed StdinEvents.\n//\n// This replaces a two-phase token -> decode pipeline with a single state machine\n// that produces fully typed events (key, mouse, paste, response) directly from\n// bytes. The parser owns all byte framing and protocol recognition. It does NOT\n// own event dispatch — that belongs to KeyHandler and the renderer.\n\nimport { Buffer } from \"node:buffer\"\nimport { SystemClock, type Clock, type TimerHandle } from \"./clock\"\nimport { parseKeypress, type ParsedKey } from \"./parse.keypress\"\nimport { MouseParser, type RawMouseEvent } from \"./parse.mouse\"\nimport type { PasteMetadata } from \"./paste\"\n\nexport { SystemClock, type Clock, type TimerHandle } from \"./clock\"\n\nexport type StdinResponseProtocol = \"csi\" | \"osc\" | \"dcs\" | \"apc\" | \"unknown\"\n\n// The four event types the parser produces. Everything stdin sends becomes\n// exactly one of these.\nexport type StdinEvent =\n  | {\n      type: \"key\"\n      raw: string\n      key: ParsedKey\n    }\n  | {\n      type: \"mouse\"\n      raw: string\n      encoding: \"sgr\" | \"x10\"\n      event: RawMouseEvent\n    }\n  | {\n      type: \"paste\"\n      bytes: Uint8Array\n      metadata?: PasteMetadata\n    }\n  | {\n      type: \"response\"\n      protocol: StdinResponseProtocol\n      sequence: string\n    }\n\nexport interface StdinParserProtocolContext {\n  kittyKeyboardEnabled: boolean\n  privateCapabilityRepliesActive: boolean\n  pixelResolutionQueryActive: boolean\n  explicitWidthCprActive: boolean\n}\n\nexport interface StdinParserOptions {\n  timeoutMs?: number\n  maxPendingBytes?: number\n  armTimeouts?: boolean\n  onTimeoutFlush?: () => void\n  useKittyKeyboard?: boolean\n  protocolContext?: Partial<StdinParserProtocolContext>\n  clock?: Clock\n}\n\n// State machine tags for the byte scanner. Each tag represents which protocol\n// framing mode the parser is currently inside. The sawEsc flag in osc/dcs/apc\n// tracks whether the previous byte was ESC, since the two-byte ST terminator\n// (ESC \\) can split across push() calls.\ntype ParserState =\n  | { tag: \"ground\" }\n  | { tag: \"utf8\"; expected: number; seen: number }\n  | { tag: \"esc\" }\n  | { tag: \"ss3\" }\n  | { tag: \"csi\" }\n  | { tag: \"csi_sgr_mouse\"; part: number; hasDigit: boolean }\n  | { tag: \"csi_sgr_mouse_deferred\"; part: number; hasDigit: boolean }\n  | { tag: \"csi_parametric\"; semicolons: number; segments: number; hasDigit: boolean; firstParamValue: number | null }\n  | {\n      tag: \"csi_parametric_deferred\"\n      semicolons: number\n      segments: number\n      hasDigit: boolean\n      firstParamValue: number | null\n    }\n  | { tag: \"csi_private_reply\"; semicolons: number; hasDigit: boolean; sawDollar: boolean }\n  | { tag: \"csi_private_reply_deferred\"; semicolons: number; hasDigit: boolean; sawDollar: boolean }\n  | { tag: \"osc\"; sawEsc: boolean }\n  | { tag: \"dcs\"; sawEsc: boolean }\n  | { tag: \"apc\"; sawEsc: boolean }\n  | { tag: \"esc_recovery\" }\n  | { tag: \"esc_less_mouse\" }\n  | { tag: \"esc_less_x10_mouse\" }\n\n// Collects paste body incrementally, bypassing the main ByteQueue so large\n// pastes don't grow the parser buffer. Keeps only a small tail for end-marker\n// detection across chunk boundaries.\ninterface PasteCollector {\n  tail: Uint8Array\n  parts: Uint8Array[]\n  totalLength: number\n}\n\n// 10ms is enough to distinguish a lone ESC keypress from the start of an\n// escape sequence on all but the slowest connections.\nconst DEFAULT_TIMEOUT_MS = 10\nconst DEFAULT_MAX_PENDING_BYTES = 64 * 1024\nconst INITIAL_PENDING_CAPACITY = 256\nconst ESC = 0x1b\nconst BEL = 0x07\nconst BRACKETED_PASTE_START = Buffer.from(\"\\x1b[200~\")\nconst BRACKETED_PASTE_END = Buffer.from(\"\\x1b[201~\")\nconst EMPTY_BYTES = new Uint8Array(0)\nconst KEY_DECODER = new TextDecoder()\nconst DEFAULT_PROTOCOL_CONTEXT: StdinParserProtocolContext = {\n  kittyKeyboardEnabled: false,\n  privateCapabilityRepliesActive: false,\n  pixelResolutionQueryActive: false,\n  explicitWidthCprActive: false,\n}\n// rxvt uses $-terminated CSI sequences for shifted function keys (e.g. ESC[2$).\n// Standard CSI treats $ as an intermediate byte, not a final, so we match these\n// explicitly to avoid waiting for a \"real\" final byte that never arrives.\nconst RXVT_DOLLAR_CSI_RE = /^\\x1b\\[\\d+\\$$/\n\nconst SYSTEM_CLOCK = new SystemClock()\n\n// Byte buffer for pending input. Uses start/end offsets so consume() just\n// advances the start pointer without copying. Compacts (via copyWithin) only\n// when the consumed prefix exceeds half the buffer, keeping amortized cost low.\nclass ByteQueue {\n  private buf: Uint8Array\n  private start = 0\n  private end = 0\n\n  constructor(capacity = INITIAL_PENDING_CAPACITY) {\n    this.buf = new Uint8Array(capacity)\n  }\n\n  get length(): number {\n    return this.end - this.start\n  }\n\n  get capacity(): number {\n    return this.buf.length\n  }\n\n  view(): Uint8Array {\n    return this.buf.subarray(this.start, this.end)\n  }\n\n  // Returns a view of the contents and resets the queue. The view shares\n  // the underlying buffer, so it becomes invalid on the next append().\n  take(): Uint8Array {\n    const chunk = this.view()\n    this.start = 0\n    this.end = 0\n    return chunk\n  }\n\n  append(chunk: Uint8Array): void {\n    if (chunk.length === 0) {\n      return\n    }\n\n    this.ensureCapacity(this.length + chunk.length)\n    this.buf.set(chunk, this.end)\n    this.end += chunk.length\n  }\n\n  // Drops the first `count` bytes. Compacts when the consumed prefix\n  // exceeds half the buffer to reclaim wasted space at the front.\n  consume(count: number): void {\n    if (count <= 0) {\n      return\n    }\n\n    if (count >= this.length) {\n      this.start = 0\n      this.end = 0\n      return\n    }\n\n    this.start += count\n    if (this.start >= this.buf.length / 2) {\n      this.buf.copyWithin(0, this.start, this.end)\n      this.end -= this.start\n      this.start = 0\n    }\n  }\n\n  clear(): void {\n    this.start = 0\n    this.end = 0\n  }\n\n  reset(capacity = INITIAL_PENDING_CAPACITY): void {\n    this.buf = new Uint8Array(capacity)\n    this.start = 0\n    this.end = 0\n  }\n\n  // Tries reclaiming space by compacting data to the front first.\n  // Doubles the allocation if that still isn't enough.\n  private ensureCapacity(requiredLength: number): void {\n    const currentLength = this.length\n    if (requiredLength <= this.buf.length) {\n      const availableAtEnd = this.buf.length - this.end\n      if (availableAtEnd >= requiredLength - currentLength) {\n        return\n      }\n\n      this.buf.copyWithin(0, this.start, this.end)\n      this.end = currentLength\n      this.start = 0\n      if (requiredLength <= this.buf.length) {\n        return\n      }\n    }\n\n    let nextCapacity = this.buf.length\n    while (nextCapacity < requiredLength) {\n      nextCapacity *= 2\n    }\n\n    const next = new Uint8Array(nextCapacity)\n    next.set(this.view(), 0)\n    this.buf = next\n    this.start = 0\n    this.end = currentLength\n  }\n}\n\nfunction normalizePositiveOption(value: number | undefined, fallback: number): number {\n  if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) {\n    return fallback\n  }\n\n  return Math.floor(value)\n}\n\n// Returns the expected byte count for a UTF-8 sequence given its lead byte,\n// or 0 for bytes that aren't valid UTF-8 leads. Returning 0 tells the parser\n// this is a legacy high-byte character (0x80–0xBF, 0xC0–0xC1, 0xF5+) that\n// goes through the parseKeypress() meta-key path instead.\nfunction utf8SequenceLength(first: number): number {\n  if (first < 0x80) return 1\n  if (first >= 0xc2 && first <= 0xdf) return 2\n  if (first >= 0xe0 && first <= 0xef) return 3\n  if (first >= 0xf0 && first <= 0xf4) return 4\n  return 0\n}\n\nfunction bytesEqual(left: Uint8Array, right: Uint8Array): boolean {\n  if (left.length !== right.length) {\n    return false\n  }\n\n  for (let index = 0; index < left.length; index += 1) {\n    if (left[index] !== right[index]) {\n      return false\n    }\n  }\n\n  return true\n}\n\n// Checks whether a byte sequence is a complete SGR mouse report:\n// ESC [ < Ps ; Ps ; Ps M/m  (three semicolon-separated digit groups).\nfunction isMouseSgrSequence(sequence: Uint8Array): boolean {\n  if (sequence.length < 7) {\n    return false\n  }\n\n  if (sequence[0] !== ESC || sequence[1] !== 0x5b || sequence[2] !== 0x3c) {\n    return false\n  }\n\n  const final = sequence[sequence.length - 1]\n  if (final !== 0x4d && final !== 0x6d) {\n    return false\n  }\n\n  let part = 0\n  let hasDigit = false\n  for (let index = 3; index < sequence.length - 1; index += 1) {\n    const byte = sequence[index]!\n    if (byte >= 0x30 && byte <= 0x39) {\n      hasDigit = true\n      continue\n    }\n\n    if (byte === 0x3b && hasDigit && part < 2) {\n      part += 1\n      hasDigit = false\n      continue\n    }\n\n    return false\n  }\n\n  return part === 2 && hasDigit\n}\n\nfunction isAsciiDigit(byte: number): boolean {\n  return byte >= 0x30 && byte <= 0x39\n}\n\ninterface ParametricCsiLike {\n  semicolons: number\n  segments: number\n  hasDigit: boolean\n  firstParamValue: number | null\n}\n\ninterface PrivateReplyCsiLike {\n  semicolons: number\n  hasDigit: boolean\n  sawDollar: boolean\n}\n\nfunction parsePositiveDecimalPrefix(sequence: Uint8Array, start: number, endExclusive: number): number | null {\n  if (start >= endExclusive) return null\n\n  let value = 0\n  let sawDigit = false\n  for (let index = start; index < endExclusive; index += 1) {\n    const byte = sequence[index]!\n    if (!isAsciiDigit(byte)) return null\n    sawDigit = true\n    value = value * 10 + (byte - 0x30)\n  }\n\n  return sawDigit ? value : null\n}\n\nfunction canStillBeKittyU(state: ParametricCsiLike): boolean {\n  return state.semicolons >= 1\n}\n\nfunction canStillBeKittySpecial(state: ParametricCsiLike): boolean {\n  return state.semicolons === 1 && state.segments > 1\n}\n\nfunction canStillBeExplicitWidthCpr(state: ParametricCsiLike): boolean {\n  return state.firstParamValue === 1 && state.semicolons === 1\n}\n\nfunction canStillBePixelResolution(state: ParametricCsiLike): boolean {\n  return state.firstParamValue === 4 && state.semicolons === 2\n}\n\nfunction canDeferParametricCsi(state: ParametricCsiLike, context: StdinParserProtocolContext): boolean {\n  return (\n    (context.kittyKeyboardEnabled && (canStillBeKittyU(state) || canStillBeKittySpecial(state))) ||\n    (context.explicitWidthCprActive && canStillBeExplicitWidthCpr(state)) ||\n    (context.pixelResolutionQueryActive && canStillBePixelResolution(state))\n  )\n}\n\nfunction canCompleteDeferredParametricCsi(\n  state: ParametricCsiLike,\n  byte: number,\n  context: StdinParserProtocolContext,\n): boolean {\n  if (context.kittyKeyboardEnabled) {\n    if (state.hasDigit && byte === 0x75) return true\n    if (\n      state.hasDigit &&\n      state.semicolons === 1 &&\n      state.segments > 1 &&\n      (byte === 0x7e || (byte >= 0x41 && byte <= 0x5a))\n    ) {\n      return true\n    }\n  }\n\n  if (\n    context.explicitWidthCprActive &&\n    state.hasDigit &&\n    state.firstParamValue === 1 &&\n    state.semicolons === 1 &&\n    byte === 0x52\n  ) {\n    return true\n  }\n\n  if (\n    context.pixelResolutionQueryActive &&\n    state.hasDigit &&\n    state.firstParamValue === 4 &&\n    state.semicolons === 2 &&\n    byte === 0x74\n  ) {\n    return true\n  }\n\n  return false\n}\n\nfunction canDeferPrivateReplyCsi(context: StdinParserProtocolContext): boolean {\n  return context.privateCapabilityRepliesActive\n}\n\nfunction canCompleteDeferredPrivateReplyCsi(\n  state: PrivateReplyCsiLike,\n  byte: number,\n  context: StdinParserProtocolContext,\n): boolean {\n  if (!context.privateCapabilityRepliesActive) return false\n  if (state.sawDollar) return state.hasDigit && byte === 0x79\n  if (byte === 0x63) return state.hasDigit || state.semicolons > 0\n  return state.hasDigit && byte === 0x75\n}\n\nfunction concatBytes(left: Uint8Array, right: Uint8Array): Uint8Array {\n  if (left.length === 0) {\n    return right\n  }\n\n  if (right.length === 0) {\n    return left\n  }\n\n  const combined = new Uint8Array(left.length + right.length)\n  combined.set(left, 0)\n  combined.set(right, left.length)\n  return combined\n}\n\nfunction indexOfBytes(haystack: Uint8Array, needle: Uint8Array): number {\n  if (needle.length === 0) {\n    return 0\n  }\n\n  const limit = haystack.length - needle.length\n  for (let offset = 0; offset <= limit; offset += 1) {\n    let matched = true\n    for (let index = 0; index < needle.length; index += 1) {\n      if (haystack[offset + index] !== needle[index]) {\n        matched = false\n        break\n      }\n    }\n\n    if (matched) {\n      return offset\n    }\n  }\n\n  return -1\n}\n\n// Decodes raw protocol bytes as latin1. Used for mouse and response events\n// where the wire bytes may not be valid UTF-8 but need a lossless string\n// form for downstream sequence handlers.\nfunction decodeLatin1(bytes: Uint8Array): string {\n  return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString(\"latin1\")\n}\n\nfunction decodeUtf8(bytes: Uint8Array): string {\n  return KEY_DECODER.decode(bytes)\n}\n\nfunction createPasteCollector(): PasteCollector {\n  return {\n    tail: EMPTY_BYTES,\n    parts: [],\n    totalLength: 0,\n  }\n}\n\nfunction joinPasteBytes(parts: Uint8Array[], totalLength: number): Uint8Array {\n  if (totalLength === 0) {\n    return EMPTY_BYTES\n  }\n\n  if (parts.length === 1) {\n    return parts[0]!\n  }\n\n  const bytes = new Uint8Array(totalLength)\n  let offset = 0\n  for (const part of parts) {\n    bytes.set(part, offset)\n    offset += part.length\n  }\n\n  return bytes\n}\n\n// Push-driven stdin parser. Callers feed raw bytes via push(), then read\n// typed events via read() or drain(). At most one incomplete protocol unit\n// is buffered at a time; everything else is immediately converted to events.\n//\n// The parser guarantees chunk-shape invariance: the same bytes always produce\n// the same events, regardless of chunk boundaries. A lone ESC resolves via\n// timeout, split UTF-8 codepoints reassemble correctly, and bracketed paste\n// markers may split across any chunk boundary.\nexport class StdinParser {\n  private readonly pending = new ByteQueue(INITIAL_PENDING_CAPACITY)\n  private readonly events: StdinEvent[] = []\n  private readonly timeoutMs: number\n  private readonly maxPendingBytes: number\n  private readonly armTimeouts: boolean\n  private readonly onTimeoutFlush: (() => void) | null\n  private readonly useKittyKeyboard: boolean\n  private readonly mouseParser = new MouseParser()\n  private readonly clock: Clock\n  private protocolContext: StdinParserProtocolContext\n  private timeoutId: TimerHandle | null = null\n  private destroyed = false\n  // When the current incomplete unit first appeared. Null when nothing is pending.\n  private pendingSinceMs: number | null = null\n  // When true, the state machine treats the current incomplete prefix as\n  // final and emits it as one atomic event (e.g. a lone ESC becomes an\n  // Escape key). Set by the timeout, consumed by the next read() or drain().\n  private forceFlush = false\n  // True only immediately after a timeout flush emits a lone ESC key. The next\n  // `[` may begin a delayed `[<...M/m` mouse continuation recovery path.\n  private justFlushedEsc = false\n  private state: ParserState = { tag: \"ground\" }\n  // Scan position within pending.view() during scanPending().\n  private cursor = 0\n  // Start of the protocol unit currently being parsed. The bytes from\n  // unitStart through cursor all belong to one atomic unit.\n  private unitStart = 0\n  // When non-null, the parser is inside a bracketed paste. All incoming\n  // bytes flow through consumePasteBytes() instead of the normal state machine.\n  private paste: PasteCollector | null = null\n\n  constructor(options: StdinParserOptions = {}) {\n    this.timeoutMs = normalizePositiveOption(options.timeoutMs, DEFAULT_TIMEOUT_MS)\n    this.maxPendingBytes = normalizePositiveOption(options.maxPendingBytes, DEFAULT_MAX_PENDING_BYTES)\n    this.armTimeouts = options.armTimeouts ?? true\n    this.onTimeoutFlush = options.onTimeoutFlush ?? null\n    this.useKittyKeyboard = options.useKittyKeyboard ?? true\n    this.clock = options.clock ?? SYSTEM_CLOCK\n    this.protocolContext = {\n      ...DEFAULT_PROTOCOL_CONTEXT,\n      kittyKeyboardEnabled: options.protocolContext?.kittyKeyboardEnabled ?? false,\n      privateCapabilityRepliesActive: options.protocolContext?.privateCapabilityRepliesActive ?? false,\n      pixelResolutionQueryActive: options.protocolContext?.pixelResolutionQueryActive ?? false,\n      explicitWidthCprActive: options.protocolContext?.explicitWidthCprActive ?? false,\n    }\n  }\n\n  public get bufferCapacity(): number {\n    return this.pending.capacity\n  }\n\n  public updateProtocolContext(patch: Partial<StdinParserProtocolContext>): void {\n    this.ensureAlive()\n    this.protocolContext = { ...this.protocolContext, ...patch }\n    this.reconcileDeferredStateWithProtocolContext()\n    this.reconcileTimeoutState()\n  }\n\n  // Feeds raw stdin bytes into the parser. Converts as much as possible into\n  // queued events and leaves at most one incomplete unit behind in pending.\n  //\n  // When a chunk contains a paste start marker, bytes before the marker go\n  // through normal parsing, then paste mode takes over for the rest. This\n  // prevents large pastes from growing the main buffer.\n  public push(data: Uint8Array): void {\n    this.ensureAlive()\n    if (data.length === 0) {\n      // Preserve the existing empty-chunk -> empty-keypress behavior.\n      this.emitKeyOrResponse(\"unknown\", \"\")\n      return\n    }\n\n    let remainder = data\n    while (remainder.length > 0) {\n      if (this.paste) {\n        remainder = this.consumePasteBytes(remainder)\n        continue\n      }\n\n      // If we're in ground state with nothing pending, scan the incoming\n      // chunk for a paste start marker. Only append through the marker so\n      // scanPending() enters paste mode without buffering the full paste.\n      const immediatePasteStartIndex =\n        this.state.tag === \"ground\" && this.pending.length === 0 ? indexOfBytes(remainder, BRACKETED_PASTE_START) : -1\n      const appendEnd =\n        immediatePasteStartIndex === -1 ? remainder.length : immediatePasteStartIndex + BRACKETED_PASTE_START.length\n\n      this.pending.append(remainder.subarray(0, appendEnd))\n      remainder = remainder.subarray(appendEnd)\n      this.scanPending()\n\n      if (this.paste && this.pending.length > 0) {\n        remainder = this.consumePasteBytes(this.takePendingBytes())\n        continue\n      }\n\n      if (!this.paste && this.pending.length > this.maxPendingBytes) {\n        this.flushPendingOverflow()\n        this.scanPending()\n\n        if (this.paste && this.pending.length > 0) {\n          remainder = this.consumePasteBytes(this.takePendingBytes())\n        }\n      }\n    }\n\n    this.reconcileTimeoutState()\n  }\n\n  // Pops one event from the queue. If the queue is empty and a timeout has\n  // set forceFlush, re-scans pending to convert the timed-out incomplete\n  // unit into one final event before returning it.\n  public read(): StdinEvent | null {\n    this.ensureAlive()\n\n    if (this.events.length === 0 && this.forceFlush) {\n      this.scanPending()\n      this.reconcileTimeoutState()\n    }\n\n    return this.events.shift() ?? null\n  }\n\n  // Delivers all queued events. Stops early if the parser is destroyed\n  // during a callback (e.g. an event handler triggers teardown).\n  public drain(onEvent: (event: StdinEvent) => void): void {\n    this.ensureAlive()\n\n    while (true) {\n      if (this.destroyed) {\n        return\n      }\n\n      const event = this.read()\n      if (!event) {\n        return\n      }\n\n      onEvent(event)\n    }\n  }\n\n  // Marks the parser for forced flush if enough time has passed since\n  // incomplete data arrived. Does not immediately emit events — the next\n  // read() or drain() does the actual flush. This separation keeps the\n  // timer callback from emitting events mid-flight in user code.\n  public flushTimeout(nowMsValue: number = this.clock.now()): void {\n    this.ensureAlive()\n\n    if (this.paste || this.pendingSinceMs === null || this.pending.length === 0) {\n      return\n    }\n\n    if (nowMsValue < this.pendingSinceMs || nowMsValue - this.pendingSinceMs < this.timeoutMs) {\n      return\n    }\n\n    this.forceFlush = true\n  }\n\n  public reset(): void {\n    if (this.destroyed) {\n      return\n    }\n\n    this.clearTimeout()\n    this.resetState()\n  }\n\n  public resetMouseState(): void {\n    this.ensureAlive()\n    this.mouseParser.reset()\n  }\n\n  public destroy(): void {\n    if (this.destroyed) {\n      return\n    }\n\n    this.clearTimeout()\n    this.destroyed = true\n    this.resetState()\n  }\n\n  private ensureAlive(): void {\n    if (this.destroyed) {\n      throw new Error(\"StdinParser has been destroyed\")\n    }\n  }\n\n  // Scans the pending byte buffer one byte at a time, dispatching on the\n  // current parser state. All protocol framing lives in this single switch\n  // — intentionally not split into per-mode scan helpers.\n  //\n  // Exits when: all bytes consumed (ground), more bytes needed (incomplete\n  // unit), or paste mode entered (body handled by consumePasteBytes).\n  private scanPending(): void {\n    while (!this.paste) {\n      const bytes = this.pending.view()\n      if (this.state.tag === \"ground\" && this.cursor >= bytes.length) {\n        this.pending.clear()\n        this.cursor = 0\n        this.unitStart = 0\n        this.pendingSinceMs = null\n        this.forceFlush = false\n        return\n      }\n\n      const byte = this.cursor < bytes.length ? bytes[this.cursor]! : -1\n      switch (this.state.tag) {\n        case \"ground\": {\n          this.unitStart = this.cursor\n\n          // After a timeout-flushed lone ESC, a following `[` may be the start\n          // of a delayed `[<...M/m` mouse continuation. Recover only this narrow\n          // case; otherwise clear the recovery flag and parse bytes normally.\n          if (this.justFlushedEsc) {\n            if (byte === 0x5b) {\n              this.justFlushedEsc = false\n              this.cursor += 1\n              this.state = { tag: \"esc_recovery\" }\n              continue\n            }\n\n            this.justFlushedEsc = false\n          }\n\n          if (byte === ESC) {\n            this.cursor += 1\n            this.state = { tag: \"esc\" }\n            continue\n          }\n\n          if (byte < 0x80) {\n            this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.cursor, this.cursor + 1)))\n            this.consumePrefix(this.cursor + 1)\n            continue\n          }\n\n          // Invalid UTF-8 lead byte. Could be a legacy high-byte from an\n          // older terminal. If it's the last byte in the buffer, wait for\n          // more data or a timeout before committing. On timeout, emit\n          // through parseKeypress() which handles meta-key behavior.\n          const expected = utf8SequenceLength(byte)\n          if (expected === 0) {\n            if (!this.forceFlush && this.cursor + 1 === bytes.length) {\n              this.markPending()\n              return\n            }\n\n            this.emitLegacyHighByte(byte)\n            this.consumePrefix(this.cursor + 1)\n            continue\n          }\n\n          this.cursor += 1\n          this.state = { tag: \"utf8\", expected, seen: 1 }\n          continue\n        }\n\n        case \"utf8\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitLegacyHighByte(bytes[this.unitStart]!)\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.unitStart + 1)\n            continue\n          }\n\n          // Not a valid continuation byte. Treat the lead byte as a legacy\n          // high-byte character and restart parsing from this position.\n          if ((byte & 0xc0) !== 0x80) {\n            this.emitLegacyHighByte(bytes[this.unitStart]!)\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.unitStart + 1)\n            continue\n          }\n\n          const nextSeen = this.state.seen + 1\n          this.cursor += 1\n          if (nextSeen < this.state.expected) {\n            this.state = { tag: \"utf8\", expected: this.state.expected, seen: nextSeen }\n            continue\n          }\n\n          this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.cursor)\n          continue\n        }\n\n        case \"esc\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            const flushedLoneEsc = this.cursor === this.unitStart + 1 && bytes[this.unitStart] === ESC\n            this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n            this.justFlushedEsc = flushedLoneEsc\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          // The byte after ESC determines the sub-protocol:\n          // [  ->  CSI, O  ->  SS3, ]  ->  OSC, P  ->  DCS, _  ->  APC.\n          switch (byte) {\n            case 0x5b:\n              this.cursor += 1\n              this.state = { tag: \"csi\" }\n              continue\n            case 0x4f:\n              this.cursor += 1\n              this.state = { tag: \"ss3\" }\n              continue\n            case 0x5d:\n              this.cursor += 1\n              this.state = { tag: \"osc\", sawEsc: false }\n              continue\n            case 0x50:\n              this.cursor += 1\n              this.state = { tag: \"dcs\", sawEsc: false }\n              continue\n            case 0x5f:\n              this.cursor += 1\n              this.state = { tag: \"apc\", sawEsc: false }\n              continue\n            // ESC ESC: stay in esc state. Terminals encode Alt+ESC and\n            // similar sequences as ESC ESC [...], so we keep scanning.\n            case ESC:\n              this.cursor += 1\n              continue\n            default:\n              this.cursor += 1\n              this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n              this.state = { tag: \"ground\" }\n              this.consumePrefix(this.cursor)\n              continue\n          }\n        }\n\n        case \"ss3\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (byte === ESC) {\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          this.cursor += 1\n          this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.cursor)\n          continue\n        }\n\n        // Narrow recovery path for delayed mouse continuations after a\n        // timeout-flushed lone ESC. Wait for either `<` (SGR) or `M` (X10); if\n        // neither arrives, flush `[` as a normal key.\n        case \"esc_recovery\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (byte === 0x3c) {\n            this.cursor += 1\n            this.state = { tag: \"esc_less_mouse\" }\n            continue\n          }\n\n          if (byte === 0x4d) {\n            this.cursor += 1\n            this.state = { tag: \"esc_less_x10_mouse\" }\n            continue\n          }\n\n          this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.unitStart + 1)))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.unitStart + 1)\n          continue\n        }\n\n        case \"csi\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          // A new ESC inside an incomplete CSI means the previous sequence\n          // was interrupted. Flush everything before the new ESC as one\n          // opaque response, then restart parsing at the new ESC.\n          if (byte === ESC) {\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          // X10 mouse: ESC [ M plus 3 raw payload bytes (button, x, y).\n          // cursor === unitStart + 2 confirms M comes right after ESC[,\n          // not as a later final byte in a different CSI sequence.\n          if (byte === 0x4d && this.cursor === this.unitStart + 2) {\n            const end = this.cursor + 4\n            if (bytes.length < end) {\n              if (!this.forceFlush) {\n                this.markPending()\n                return\n              }\n\n              this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, bytes.length))\n              this.state = { tag: \"ground\" }\n              this.consumePrefix(bytes.length)\n              continue\n            }\n\n            this.emitMouse(bytes.subarray(this.unitStart, end), \"x10\")\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(end)\n            continue\n          }\n\n          if (byte === 0x24) {\n            const candidateEnd = this.cursor + 1\n            const candidate = decodeUtf8(bytes.subarray(this.unitStart, candidateEnd))\n            if (RXVT_DOLLAR_CSI_RE.test(candidate)) {\n              this.emitKeyOrResponse(\"csi\", candidate)\n              this.state = { tag: \"ground\" }\n              this.consumePrefix(candidateEnd)\n              continue\n            }\n\n            if (!this.forceFlush && candidateEnd >= bytes.length) {\n              this.markPending()\n              return\n            }\n          }\n\n          if (byte === 0x3c && this.cursor === this.unitStart + 2) {\n            this.cursor += 1\n            this.state = { tag: \"csi_sgr_mouse\", part: 0, hasDigit: false }\n            continue\n          }\n\n          // Some terminals use ESC [[A..E / ESC [[5~ / ESC [[6~ variants.\n          // Treat the second `[` immediately after ESC[ as part of the CSI\n          // payload instead of as a final byte so parseKeypress() can match\n          // `[[A`, `[[B`, `[[5~`, etc.\n          if (byte === 0x5b && this.cursor === this.unitStart + 2) {\n            this.cursor += 1\n            continue\n          }\n\n          if (byte === 0x3f && this.cursor === this.unitStart + 2) {\n            this.cursor += 1\n            this.state = { tag: \"csi_private_reply\", semicolons: 0, hasDigit: false, sawDollar: false }\n            continue\n          }\n\n          if (byte === 0x3b) {\n            const firstParamValue = parsePositiveDecimalPrefix(bytes, this.unitStart + 2, this.cursor)\n            if (firstParamValue !== null) {\n              this.cursor += 1\n              this.state = {\n                tag: \"csi_parametric\",\n                semicolons: 1,\n                segments: 1,\n                hasDigit: false,\n                firstParamValue,\n              }\n              continue\n            }\n          }\n\n          // Standard CSI final byte (0x40–0x7E). Check for bracketed paste\n          // start, SGR mouse, or a regular CSI key/response.\n          if (byte >= 0x40 && byte <= 0x7e) {\n            const end = this.cursor + 1\n            const rawBytes = bytes.subarray(this.unitStart, end)\n\n            if (bytesEqual(rawBytes, BRACKETED_PASTE_START)) {\n              this.state = { tag: \"ground\" }\n              this.consumePrefix(end)\n              this.paste = createPasteCollector()\n              continue\n            }\n\n            if (isMouseSgrSequence(rawBytes)) {\n              this.emitMouse(rawBytes, \"sgr\")\n              this.state = { tag: \"ground\" }\n              this.consumePrefix(end)\n              continue\n            }\n\n            this.emitKeyOrResponse(\"csi\", decodeUtf8(rawBytes))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(end)\n            continue\n          }\n\n          this.cursor += 1\n          continue\n        }\n\n        case \"csi_sgr_mouse\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.state = { tag: \"csi_sgr_mouse_deferred\", part: this.state.part, hasDigit: this.state.hasDigit }\n            this.pendingSinceMs = null\n            this.forceFlush = false\n            return\n          }\n\n          if (byte === ESC) {\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (isAsciiDigit(byte)) {\n            this.cursor += 1\n            this.state = { tag: \"csi_sgr_mouse\", part: this.state.part, hasDigit: true }\n            continue\n          }\n\n          if (byte === 0x3b && this.state.hasDigit && this.state.part < 2) {\n            this.cursor += 1\n            this.state = { tag: \"csi_sgr_mouse\", part: this.state.part + 1, hasDigit: false }\n            continue\n          }\n\n          if (byte >= 0x40 && byte <= 0x7e) {\n            const end = this.cursor + 1\n            const rawBytes = bytes.subarray(this.unitStart, end)\n            if (isMouseSgrSequence(rawBytes)) {\n              this.emitMouse(rawBytes, \"sgr\")\n            } else {\n              this.emitKeyOrResponse(\"csi\", decodeUtf8(rawBytes))\n            }\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(end)\n            continue\n          }\n\n          this.state = { tag: \"csi\" }\n          continue\n        }\n\n        case \"csi_sgr_mouse_deferred\": {\n          if (this.cursor >= bytes.length) {\n            this.pendingSinceMs = null\n            this.forceFlush = false\n            return\n          }\n\n          if (byte === ESC) {\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (isAsciiDigit(byte) || byte === 0x3b || byte === 0x4d || byte === 0x6d) {\n            this.state = { tag: \"csi_sgr_mouse\", part: this.state.part, hasDigit: this.state.hasDigit }\n            continue\n          }\n\n          this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.cursor)\n          continue\n        }\n\n        case \"csi_parametric\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            if (canDeferParametricCsi(this.state, this.protocolContext)) {\n              this.state = {\n                tag: \"csi_parametric_deferred\",\n                semicolons: this.state.semicolons,\n                segments: this.state.segments,\n                hasDigit: this.state.hasDigit,\n                firstParamValue: this.state.firstParamValue,\n              }\n              this.pendingSinceMs = null\n              this.forceFlush = false\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (byte === ESC) {\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (isAsciiDigit(byte)) {\n            this.cursor += 1\n            this.state = {\n              tag: \"csi_parametric\",\n              semicolons: this.state.semicolons,\n              segments: this.state.segments,\n              hasDigit: true,\n              firstParamValue: this.state.firstParamValue,\n            }\n            continue\n          }\n\n          if (byte === 0x3a && this.state.hasDigit && this.state.segments < 3) {\n            this.cursor += 1\n            this.state = {\n              tag: \"csi_parametric\",\n              semicolons: this.state.semicolons,\n              segments: this.state.segments + 1,\n              hasDigit: false,\n              firstParamValue: this.state.firstParamValue,\n            }\n            continue\n          }\n\n          if (byte === 0x3b && this.state.semicolons < 2) {\n            this.cursor += 1\n            this.state = {\n              tag: \"csi_parametric\",\n              semicolons: this.state.semicolons + 1,\n              segments: 1,\n              hasDigit: false,\n              firstParamValue: this.state.firstParamValue,\n            }\n            continue\n          }\n\n          if (byte >= 0x40 && byte <= 0x7e) {\n            const end = this.cursor + 1\n            this.emitKeyOrResponse(\"csi\", decodeUtf8(bytes.subarray(this.unitStart, end)))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(end)\n            continue\n          }\n\n          this.state = { tag: \"csi\" }\n          continue\n        }\n\n        case \"csi_parametric_deferred\": {\n          if (this.cursor >= bytes.length) {\n            this.pendingSinceMs = null\n            this.forceFlush = false\n            return\n          }\n\n          if (byte === ESC) {\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (isAsciiDigit(byte) || byte === 0x3a || byte === 0x3b) {\n            this.state = {\n              tag: \"csi_parametric\",\n              semicolons: this.state.semicolons,\n              segments: this.state.segments,\n              hasDigit: this.state.hasDigit,\n              firstParamValue: this.state.firstParamValue,\n            }\n            continue\n          }\n\n          if (canCompleteDeferredParametricCsi(this.state, byte, this.protocolContext)) {\n            this.state = {\n              tag: \"csi_parametric\",\n              semicolons: this.state.semicolons,\n              segments: this.state.segments,\n              hasDigit: this.state.hasDigit,\n              firstParamValue: this.state.firstParamValue,\n            }\n            continue\n          }\n\n          this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.cursor)\n          continue\n        }\n\n        case \"csi_private_reply\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            if (canDeferPrivateReplyCsi(this.protocolContext)) {\n              this.state = {\n                tag: \"csi_private_reply_deferred\",\n                semicolons: this.state.semicolons,\n                hasDigit: this.state.hasDigit,\n                sawDollar: this.state.sawDollar,\n              }\n              this.pendingSinceMs = null\n              this.forceFlush = false\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (byte === ESC) {\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (isAsciiDigit(byte)) {\n            this.cursor += 1\n            this.state = {\n              tag: \"csi_private_reply\",\n              semicolons: this.state.semicolons,\n              hasDigit: true,\n              sawDollar: this.state.sawDollar,\n            }\n            continue\n          }\n\n          if (byte === 0x3b) {\n            this.cursor += 1\n            this.state = {\n              tag: \"csi_private_reply\",\n              semicolons: this.state.semicolons + 1,\n              hasDigit: false,\n              sawDollar: false,\n            }\n            continue\n          }\n\n          if (byte === 0x24 && this.state.hasDigit && !this.state.sawDollar) {\n            this.cursor += 1\n            this.state = {\n              tag: \"csi_private_reply\",\n              semicolons: this.state.semicolons,\n              hasDigit: true,\n              sawDollar: true,\n            }\n            continue\n          }\n\n          if (byte >= 0x40 && byte <= 0x7e) {\n            const end = this.cursor + 1\n            this.emitKeyOrResponse(\"csi\", decodeUtf8(bytes.subarray(this.unitStart, end)))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(end)\n            continue\n          }\n\n          this.state = { tag: \"csi\" }\n          continue\n        }\n\n        case \"csi_private_reply_deferred\": {\n          if (this.cursor >= bytes.length) {\n            this.pendingSinceMs = null\n            this.forceFlush = false\n            return\n          }\n\n          if (byte === ESC) {\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (isAsciiDigit(byte) || byte === 0x3b || byte === 0x24) {\n            this.state = {\n              tag: \"csi_private_reply\",\n              semicolons: this.state.semicolons,\n              hasDigit: this.state.hasDigit,\n              sawDollar: this.state.sawDollar,\n            }\n            continue\n          }\n\n          if (canCompleteDeferredPrivateReplyCsi(this.state, byte, this.protocolContext)) {\n            this.state = {\n              tag: \"csi_private_reply\",\n              semicolons: this.state.semicolons,\n              hasDigit: this.state.hasDigit,\n              sawDollar: this.state.sawDollar,\n            }\n            continue\n          }\n\n          this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.cursor)\n          continue\n        }\n\n        // OSC sequences end at BEL or ESC \\. DCS and APC end at ESC \\\n        // only. The sawEsc flag tracks whether the previous byte was ESC,\n        // since the two-byte ESC \\ can split across push() calls.\n        case \"osc\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (this.state.sawEsc) {\n            if (byte === 0x5c) {\n              const end = this.cursor + 1\n              this.emitOpaqueResponse(\"osc\", bytes.subarray(this.unitStart, end))\n              this.state = { tag: \"ground\" }\n              this.consumePrefix(end)\n              continue\n            }\n\n            this.state = { tag: \"osc\", sawEsc: false }\n            continue\n          }\n\n          if (byte === BEL) {\n            const end = this.cursor + 1\n            this.emitOpaqueResponse(\"osc\", bytes.subarray(this.unitStart, end))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(end)\n            continue\n          }\n\n          if (byte === ESC) {\n            this.cursor += 1\n            this.state = { tag: \"osc\", sawEsc: true }\n            continue\n          }\n\n          this.cursor += 1\n          continue\n        }\n\n        case \"dcs\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (this.state.sawEsc) {\n            if (byte === 0x5c) {\n              const end = this.cursor + 1\n              this.emitOpaqueResponse(\"dcs\", bytes.subarray(this.unitStart, end))\n              this.state = { tag: \"ground\" }\n              this.consumePrefix(end)\n              continue\n            }\n\n            this.state = { tag: \"dcs\", sawEsc: false }\n            continue\n          }\n\n          if (byte === ESC) {\n            this.cursor += 1\n            this.state = { tag: \"dcs\", sawEsc: true }\n            continue\n          }\n\n          this.cursor += 1\n          continue\n        }\n\n        case \"apc\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if (this.state.sawEsc) {\n            if (byte === 0x5c) {\n              const end = this.cursor + 1\n              this.emitOpaqueResponse(\"apc\", bytes.subarray(this.unitStart, end))\n              this.state = { tag: \"ground\" }\n              this.consumePrefix(end)\n              continue\n            }\n\n            this.state = { tag: \"apc\", sawEsc: false }\n            continue\n          }\n\n          if (byte === ESC) {\n            this.cursor += 1\n            this.state = { tag: \"apc\", sawEsc: true }\n            continue\n          }\n\n          this.cursor += 1\n          continue\n        }\n\n        // Delayed SGR mouse continuation after `esc_recovery` has consumed the\n        // leading `[`. Consume the rest of `<digits;digits;digitsM/m` as one\n        // opaque response so split mouse bytes never leak into text.\n        case \"esc_less_mouse\": {\n          if (this.cursor >= bytes.length) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(this.cursor)\n            continue\n          }\n\n          if ((byte >= 0x30 && byte <= 0x39) || byte === 0x3b) {\n            this.cursor += 1\n            continue\n          }\n\n          if (byte === 0x4d || byte === 0x6d) {\n            const end = this.cursor + 1\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, end))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(end)\n            continue\n          }\n\n          this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.cursor)\n          continue\n        }\n\n        // Delayed X10 mouse continuation after `esc_recovery` has consumed the\n        // leading `[`. Consume `[M` plus its three raw payload bytes as one\n        // opaque response so split mouse bytes never leak into text.\n        case \"esc_less_x10_mouse\": {\n          const end = this.unitStart + 5\n\n          if (bytes.length < end) {\n            if (!this.forceFlush) {\n              this.markPending()\n              return\n            }\n\n            this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, bytes.length))\n            this.state = { tag: \"ground\" }\n            this.consumePrefix(bytes.length)\n            continue\n          }\n\n          this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, end))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(end)\n          continue\n        }\n      }\n    }\n  }\n\n  // Tries to parse the raw string as a key via parseKeypress(). If it\n  // recognizes the sequence (printable char, arrow, function key, etc.),\n  // emits a key event. Otherwise emits a response event — this is how\n  // capability responses, focus sequences, and other non-key CSI traffic\n  // avoids becoming text.\n  private emitKeyOrResponse(protocol: StdinResponseProtocol, raw: string): void {\n    const parsed = parseKeypress(raw, { useKittyKeyboard: this.useKittyKeyboard })\n    if (parsed) {\n      this.events.push({\n        type: \"key\",\n        raw: parsed.raw,\n        key: parsed,\n      })\n      return\n    }\n\n    this.events.push({\n      type: \"response\",\n      protocol,\n      sequence: raw,\n    })\n  }\n\n  private emitMouse(rawBytes: Uint8Array, encoding: \"sgr\" | \"x10\"): void {\n    const event = this.mouseParser.parseMouseEvent(rawBytes)\n    if (!event) {\n      this.emitOpaqueResponse(\"unknown\", rawBytes)\n      return\n    }\n\n    this.events.push({\n      type: \"mouse\",\n      raw: decodeLatin1(rawBytes),\n      encoding,\n      event,\n    })\n  }\n\n  // Handles single bytes in the 0x80–0xFF range that aren't valid UTF-8\n  // leads. Passes them through parseKeypress() which maps them to the\n  // existing meta-key behavior (e.g. Alt+letter in terminals that send\n  // high bytes instead of ESC-prefixed sequences).\n  private emitLegacyHighByte(byte: number): void {\n    const parsed = parseKeypress(Buffer.from([byte]), { useKittyKeyboard: this.useKittyKeyboard })\n    if (parsed) {\n      this.events.push({\n        type: \"key\",\n        raw: parsed.raw,\n        key: parsed,\n      })\n      return\n    }\n\n    this.events.push({\n      type: \"response\",\n      protocol: \"unknown\",\n      sequence: String.fromCharCode(byte),\n    })\n  }\n\n  private emitOpaqueResponse(protocol: StdinResponseProtocol, rawBytes: Uint8Array): void {\n    this.events.push({\n      type: \"response\",\n      protocol,\n      sequence: decodeLatin1(rawBytes),\n    })\n  }\n\n  // Advances past a completed protocol unit. Resets cursor, unitStart,\n  // and timeout state so the next scan iteration starts clean.\n  private consumePrefix(endExclusive: number): void {\n    this.pending.consume(endExclusive)\n    this.cursor = 0\n    this.unitStart = 0\n    this.pendingSinceMs = null\n    this.forceFlush = false\n  }\n\n  // Removes all bytes from the pending queue and returns them. Used when\n  // entering paste mode — leftover bytes after the paste start marker\n  // need to flow through consumePasteBytes() instead.\n  private takePendingBytes(): Uint8Array {\n    const buffered = this.pending.take()\n    this.cursor = 0\n    this.unitStart = 0\n    this.pendingSinceMs = null\n    this.forceFlush = false\n    return buffered\n  }\n\n  // Emits all pending bytes as one opaque response and clears the buffer.\n  // This keeps the parser buffer bounded at maxPendingBytes without\n  // dropping data or splitting it into per-character events.\n  private flushPendingOverflow(): void {\n    if (this.pending.length === 0) {\n      return\n    }\n\n    this.emitOpaqueResponse(\"unknown\", this.pending.view())\n    this.pending.clear()\n    this.cursor = 0\n    this.unitStart = 0\n    this.pendingSinceMs = null\n    this.forceFlush = false\n    this.state = { tag: \"ground\" }\n  }\n\n  // Records when incomplete data first appeared so flushTimeout() can\n  // decide whether enough time has elapsed to force-flush it.\n  private markPending(): void {\n    this.pendingSinceMs = this.clock.now()\n  }\n\n  // Processes bytes during an active bracketed paste. Searches for the end\n  // marker (ESC[201~) using a sliding tail window so the marker can split\n  // across chunk boundaries. Bytes that can't be part of the end marker are\n  // appended to the paste collector without decoding.\n  //\n  // Returns any bytes that follow the end marker — those go back through\n  // normal parsing in the push() loop.\n  private consumePasteBytes(chunk: Uint8Array): Uint8Array {\n    const paste = this.paste!\n    const combined = concatBytes(paste.tail, chunk)\n    const endIndex = indexOfBytes(combined, BRACKETED_PASTE_END)\n\n    if (endIndex !== -1) {\n      this.pushPasteBytes(combined.subarray(0, endIndex))\n\n      this.events.push({\n        type: \"paste\",\n        bytes: joinPasteBytes(paste.parts, paste.totalLength),\n      })\n\n      this.paste = null\n      return combined.subarray(endIndex + BRACKETED_PASTE_END.length)\n    }\n\n    // Keep enough trailing bytes to detect an end marker split across chunks.\n    // Everything before that point is safe to retain immediately.\n    const keep = Math.min(BRACKETED_PASTE_END.length - 1, combined.length)\n    const stableLength = combined.length - keep\n    if (stableLength > 0) {\n      this.pushPasteBytes(combined.subarray(0, stableLength))\n    }\n\n    paste.tail = Uint8Array.from(combined.subarray(stableLength))\n    return EMPTY_BYTES\n  }\n\n  private pushPasteBytes(bytes: Uint8Array): void {\n    if (bytes.length === 0) {\n      return\n    }\n\n    // Copy here because subarray() inputs may alias the caller's chunk or the\n    // parser's pending buffer across pushes. The emitted paste event must keep\n    // the original bytes even if those backing buffers are later reused.\n    this.paste!.parts.push(Uint8Array.from(bytes))\n    this.paste!.totalLength += bytes.length\n  }\n\n  private reconcileDeferredStateWithProtocolContext(): void {\n    switch (this.state.tag) {\n      case \"csi_parametric_deferred\":\n        if (!canDeferParametricCsi(this.state, this.protocolContext)) {\n          this.emitOpaqueResponse(\"unknown\", this.pending.view().subarray(this.unitStart, this.cursor))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.cursor)\n        }\n        return\n\n      case \"csi_private_reply_deferred\":\n        if (!canDeferPrivateReplyCsi(this.protocolContext)) {\n          this.emitOpaqueResponse(\"unknown\", this.pending.view().subarray(this.unitStart, this.cursor))\n          this.state = { tag: \"ground\" }\n          this.consumePrefix(this.cursor)\n        }\n        return\n    }\n  }\n\n  // Arms or disarms the timeout after every push(). If there's an incomplete\n  // unit in the buffer, starts a timer. When the timer fires, it sets\n  // forceFlush so the next read() converts the incomplete unit into one\n  // atomic event (e.g. a lone ESC becoming an Escape key).\n  private reconcileTimeoutState(): void {\n    if (!this.armTimeouts) {\n      return\n    }\n\n    if (this.paste || this.pendingSinceMs === null || this.pending.length === 0) {\n      this.clearTimeout()\n      return\n    }\n\n    this.clearTimeout()\n    this.timeoutId = this.clock.setTimeout(() => {\n      this.timeoutId = null\n      if (this.destroyed) {\n        return\n      }\n\n      try {\n        this.flushTimeout(this.clock.now())\n        this.onTimeoutFlush?.()\n      } catch (error) {\n        console.error(\"stdin parser timeout flush failed\", error)\n      }\n    }, this.timeoutMs)\n  }\n\n  private clearTimeout(): void {\n    if (!this.timeoutId) {\n      return\n    }\n\n    this.clock.clearTimeout(this.timeoutId)\n    this.timeoutId = null\n  }\n\n  // Clears all parser state: pending bytes, queued events, timeout tracking,\n  // and any active paste collector. Called by both reset() (suspend/resume)\n  // and destroy() to ensure no stale state survives.\n  private resetState(): void {\n    this.pending.reset(INITIAL_PENDING_CAPACITY)\n    this.events.length = 0\n    this.pendingSinceMs = null\n    this.forceFlush = false\n    this.justFlushedEsc = false\n    this.state = { tag: \"ground\" }\n    this.cursor = 0\n    this.unitStart = 0\n    this.paste = null\n    this.mouseParser.reset()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/styled-text.ts",
    "content": "import type { TextRenderable } from \"../renderables/Text.js\"\nimport type { TextBuffer, TextChunk } from \"../text-buffer.js\"\nimport { createTextAttributes } from \"../utils.js\"\nimport { parseColor, type ColorInput } from \"./RGBA.js\"\n\nconst BrandedStyledText: unique symbol = Symbol.for(\"@opentui/core/StyledText\")\n\nexport type Color = ColorInput\n\nexport interface StyleAttrs {\n  fg?: Color\n  bg?: Color\n  bold?: boolean\n  italic?: boolean\n  underline?: boolean\n  strikethrough?: boolean\n  dim?: boolean\n  reverse?: boolean\n  blink?: boolean\n}\n\nexport function isStyledText(obj: any): obj is StyledText {\n  return obj && obj[BrandedStyledText]\n}\n\nexport class StyledText {\n  [BrandedStyledText] = true\n\n  public chunks: TextChunk[]\n\n  constructor(chunks: TextChunk[]) {\n    this.chunks = chunks\n  }\n}\n\nexport function stringToStyledText(content: string): StyledText {\n  const chunk = {\n    __isChunk: true as const,\n    text: content,\n  }\n  return new StyledText([chunk])\n}\n\nexport type StylableInput = string | number | boolean | TextChunk\n\nfunction applyStyle(input: StylableInput, style: StyleAttrs): TextChunk {\n  if (typeof input === \"object\" && \"__isChunk\" in input) {\n    const existingChunk = input as TextChunk\n\n    const fg = style.fg ? parseColor(style.fg) : existingChunk.fg\n    const bg = style.bg ? parseColor(style.bg) : existingChunk.bg\n\n    const newAttrs = createTextAttributes(style)\n    const mergedAttrs = existingChunk.attributes ? existingChunk.attributes | newAttrs : newAttrs\n\n    return {\n      __isChunk: true,\n      text: existingChunk.text,\n      fg,\n      bg,\n      attributes: mergedAttrs,\n      link: existingChunk.link,\n    }\n  } else {\n    const plainTextStr = String(input)\n    const fg = style.fg ? parseColor(style.fg) : undefined\n    const bg = style.bg ? parseColor(style.bg) : undefined\n    const attributes = createTextAttributes(style)\n\n    return {\n      __isChunk: true,\n      text: plainTextStr,\n      fg,\n      bg,\n      attributes,\n    }\n  }\n}\n\n// Color functions\nexport const black = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"black\" })\nexport const red = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"red\" })\nexport const green = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"green\" })\nexport const yellow = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"yellow\" })\nexport const blue = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"blue\" })\nexport const magenta = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"magenta\" })\nexport const cyan = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"cyan\" })\nexport const white = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"white\" })\n\n// Bright color functions\nexport const brightBlack = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightBlack\" })\nexport const brightRed = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightRed\" })\nexport const brightGreen = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightGreen\" })\nexport const brightYellow = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightYellow\" })\nexport const brightBlue = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightBlue\" })\nexport const brightMagenta = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightMagenta\" })\nexport const brightCyan = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightCyan\" })\nexport const brightWhite = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightWhite\" })\n\n// Background color functions\nexport const bgBlack = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"black\" })\nexport const bgRed = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"red\" })\nexport const bgGreen = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"green\" })\nexport const bgYellow = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"yellow\" })\nexport const bgBlue = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"blue\" })\nexport const bgMagenta = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"magenta\" })\nexport const bgCyan = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"cyan\" })\nexport const bgWhite = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"white\" })\n\n// Style functions\nexport const bold = (input: StylableInput): TextChunk => applyStyle(input, { bold: true })\nexport const italic = (input: StylableInput): TextChunk => applyStyle(input, { italic: true })\nexport const underline = (input: StylableInput): TextChunk => applyStyle(input, { underline: true })\nexport const strikethrough = (input: StylableInput): TextChunk => applyStyle(input, { strikethrough: true })\nexport const dim = (input: StylableInput): TextChunk => applyStyle(input, { dim: true })\nexport const reverse = (input: StylableInput): TextChunk => applyStyle(input, { reverse: true })\nexport const blink = (input: StylableInput): TextChunk => applyStyle(input, { blink: true })\n\n// Custom color functions\nexport const fg =\n  (color: Color) =>\n  (input: StylableInput): TextChunk =>\n    applyStyle(input, { fg: color })\nexport const bg =\n  (color: Color) =>\n  (input: StylableInput): TextChunk =>\n    applyStyle(input, { bg: color })\n\nexport const link =\n  (url: string) =>\n  (input: StylableInput): TextChunk => {\n    const chunk =\n      typeof input === \"object\" && \"__isChunk\" in input\n        ? (input as TextChunk)\n        : {\n            __isChunk: true as const,\n            text: String(input),\n          }\n\n    return {\n      ...chunk,\n      link: { url },\n    }\n  }\n\n/**\n * Template literal handler for styled text (non-cached version).\n * Returns a StyledText object containing chunks of text with optional styles.\n */\nexport function t(strings: TemplateStringsArray, ...values: StylableInput[]): StyledText {\n  const chunks: TextChunk[] = []\n\n  for (let i = 0; i < strings.length; i++) {\n    const raw = strings[i]\n\n    if (raw) {\n      chunks.push({\n        __isChunk: true,\n        text: raw,\n        attributes: 0,\n      })\n    }\n\n    const val = values[i]\n    if (typeof val === \"object\" && \"__isChunk\" in val) {\n      chunks.push(val as TextChunk)\n    } else if (val !== undefined) {\n      const plainTextStr = String(val)\n      chunks.push({\n        __isChunk: true,\n        text: plainTextStr,\n        attributes: 0,\n      })\n    }\n  }\n\n  return new StyledText(chunks)\n}\n"
  },
  {
    "path": "packages/core/src/lib/terminal-capability-detection.test.ts",
    "content": "import { test, expect, describe } from \"bun:test\"\nimport {\n  isCapabilityResponse,\n  isPixelResolutionResponse,\n  parsePixelResolution,\n} from \"./terminal-capability-detection.js\"\n\ndescribe(\"isCapabilityResponse\", () => {\n  test(\"detects DECRPM responses\", () => {\n    expect(isCapabilityResponse(\"\\x1b[?1016;2$y\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?2027;0$y\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?2031;2$y\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?1004;1$y\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?2026;2$y\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?2004;2$y\")).toBe(true)\n  })\n\n  test(\"detects CPR responses for width detection\", () => {\n    expect(isCapabilityResponse(\"\\x1b[1;2R\")).toBe(true) // explicit width\n    expect(isCapabilityResponse(\"\\x1b[1;3R\")).toBe(true) // scaled text\n  })\n\n  test(\"does not detect regular CPR responses as capabilities\", () => {\n    // Regular cursor position reports are NOT capabilities\n    expect(isCapabilityResponse(\"\\x1b[10;5R\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b[20;30R\")).toBe(false)\n  })\n\n  test(\"detects XTVersion responses\", () => {\n    expect(isCapabilityResponse(\"\\x1bP>|kitty(0.40.1)\\x1b\\\\\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1bP>|ghostty 1.1.3\\x1b\\\\\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1bP>|tmux 3.5a\\x1b\\\\\")).toBe(true)\n  })\n\n  test(\"detects Kitty graphics responses\", () => {\n    expect(isCapabilityResponse(\"\\x1b_Gi=1;OK\\x1b\\\\\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b_Gi=1;EINVAL:Zero width/height not allowed\\x1b\\\\\")).toBe(true)\n  })\n\n  test(\"detects DA1 (Device Attributes) responses\", () => {\n    expect(isCapabilityResponse(\"\\x1b[?62;c\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?62;22c\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?1;2;4c\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?6c\")).toBe(true)\n  })\n\n  test(\"detects Kitty keyboard query responses\", () => {\n    expect(isCapabilityResponse(\"\\x1b[?0u\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?1u\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?31u\")).toBe(true)\n  })\n\n  test(\"does not detect regular keypresses\", () => {\n    expect(isCapabilityResponse(\"a\")).toBe(false)\n    expect(isCapabilityResponse(\"A\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1ba\")).toBe(false)\n  })\n\n  test(\"does not detect arrow keys\", () => {\n    expect(isCapabilityResponse(\"\\x1b[A\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b[B\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b[C\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b[D\")).toBe(false)\n  })\n\n  test(\"does not detect function keys\", () => {\n    expect(isCapabilityResponse(\"\\x1bOP\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b[11~\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b[24~\")).toBe(false)\n  })\n\n  test(\"does not detect modified arrow keys\", () => {\n    expect(isCapabilityResponse(\"\\x1b[1;2A\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b[1;5C\")).toBe(false)\n  })\n\n  test(\"does not detect mouse sequences\", () => {\n    expect(isCapabilityResponse(\"\\x1b[<35;20;5m\")).toBe(false)\n    expect(isCapabilityResponse(\"\\x1b[<0;10;10M\")).toBe(false)\n  })\n})\n\ndescribe(\"isPixelResolutionResponse\", () => {\n  test(\"detects pixel resolution responses\", () => {\n    expect(isPixelResolutionResponse(\"\\x1b[4;720;1280t\")).toBe(true)\n    expect(isPixelResolutionResponse(\"\\x1b[4;1080;1920t\")).toBe(true)\n    expect(isPixelResolutionResponse(\"\\x1b[4;0;0t\")).toBe(true)\n  })\n\n  test(\"does not detect other sequences\", () => {\n    expect(isPixelResolutionResponse(\"a\")).toBe(false)\n    expect(isPixelResolutionResponse(\"\\x1b[A\")).toBe(false)\n    expect(isPixelResolutionResponse(\"\\x1b[?1016;2$y\")).toBe(false)\n  })\n})\n\ndescribe(\"parsePixelResolution\", () => {\n  test(\"parses valid pixel resolution responses\", () => {\n    expect(parsePixelResolution(\"\\x1b[4;720;1280t\")).toEqual({ width: 1280, height: 720 })\n    expect(parsePixelResolution(\"\\x1b[4;1080;1920t\")).toEqual({ width: 1920, height: 1080 })\n    expect(parsePixelResolution(\"\\x1b[4;0;0t\")).toEqual({ width: 0, height: 0 })\n  })\n\n  test(\"returns null for invalid sequences\", () => {\n    expect(parsePixelResolution(\"a\")).toBeNull()\n    expect(parsePixelResolution(\"\\x1b[A\")).toBeNull()\n    expect(parsePixelResolution(\"\\x1b[?1016;2$y\")).toBeNull()\n  })\n})\n\ndescribe(\"real-world terminal capability sequences\", () => {\n  test(\"kitty terminal full response - individual sequences\", () => {\n    // Should detect multiple capability sequences\n    expect(isCapabilityResponse(\"\\x1b[?1016;2$y\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?2027;0$y\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[1;2R\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[1;3R\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1bP>|kitty(0.40.1)\\x1b\\\\\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b_Gi=1;EINVAL:Zero width/height not allowed\\x1b\\\\\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?62;c\")).toBe(true)\n  })\n\n  test(\"ghostty terminal response - individual sequences\", () => {\n    expect(isCapabilityResponse(\"\\x1bP>|ghostty 1.1.3\\x1b\\\\\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b_Gi=1;OK\\x1b\\\\\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?62;22c\")).toBe(true)\n  })\n\n  test(\"alacritty terminal response - individual sequences\", () => {\n    expect(isCapabilityResponse(\"\\x1b[?1016;0$y\")).toBe(true)\n    expect(isCapabilityResponse(\"\\x1b[?6c\")).toBe(true)\n  })\n\n  test(\"vscode terminal minimal response\", () => {\n    expect(isCapabilityResponse(\"\\x1b[?1016;2$y\")).toBe(true)\n  })\n})\n\ndescribe(\"renderer capabilities event\", () => {\n  /**\n   * The renderer emits \"capabilities\" event each time a capability response is processed.\n   * This happens multiple times at startup because the terminal responds to multiple queries:\n   * - DECRPM queries (sgr_pixels, unicode, color_scheme, focus, bracketed_paste, sync)\n   * - CPR queries for width detection (explicit_width, scaled_text)\n   * - XTVersion (terminal name/version, kitty detection)\n   * - Kitty keyboard query response\n   *\n   * Each response arrives async and triggers the event, so consumers should expect\n   * multiple emissions and handle them reactively.\n   */\n  test(\"kitty terminal emits capabilities event for each response\", async () => {\n    const { createTestRenderer } = await import(\"../testing/test-renderer\")\n    const { renderer } = await createTestRenderer({})\n\n    const events: any[] = []\n    renderer.on(\"capabilities\", (caps) => events.push({ ...caps }))\n\n    // Simulate all 10 Kitty capability responses (as they arrive separately)\n    const kittyResponses = [\n      \"\\x1b[?1016;2$y\", // 1. sgr_pixels\n      \"\\x1b[?2027;0$y\", // 2. unicode query\n      \"\\x1b[?2031;2$y\", // 3. color_scheme_updates\n      \"\\x1b[?1004;2$y\", // 4. focus_tracking\n      \"\\x1b[?2004;2$y\", // 5. bracketed_paste\n      \"\\x1b[?2026;2$y\", // 6. sync\n      \"\\x1b[1;2R\", // 7. explicit_width (CPR)\n      \"\\x1b[1;3R\", // 8. scaled_text (CPR)\n      \"\\x1bP>|kitty(0.42.2)\\x1b\\\\\", // 9. xtversion (triggers kitty detection)\n      \"\\x1b[?0u\", // 10. kitty keyboard query\n    ]\n\n    for (const response of kittyResponses) {\n      renderer.stdin.emit(\"data\", Buffer.from(response))\n      await new Promise((resolve) => setTimeout(resolve, 10))\n    }\n\n    // Should have received 10 capability events\n    expect(events.length).toBe(10)\n\n    // First event: sgr_pixels detected\n    expect(events[0].sgr_pixels).toBe(true)\n\n    // After xtversion (event 9): kitty_keyboard should be true\n    expect(events[8].kitty_keyboard).toBe(true)\n    expect(events[8].kitty_graphics).toBe(true)\n    expect(events[8].terminal.name).toBe(\"kitty\")\n    expect(events[8].terminal.version).toBe(\"0.42.2\")\n\n    // Final state should have all kitty capabilities\n    const finalCaps = events[9]\n    expect(finalCaps.kitty_keyboard).toBe(true)\n    expect(finalCaps.sgr_pixels).toBe(true)\n    expect(finalCaps.color_scheme_updates).toBe(true)\n    expect(finalCaps.focus_tracking).toBe(true)\n    expect(finalCaps.sync).toBe(true)\n    expect(finalCaps.explicit_width).toBe(true)\n    expect(finalCaps.scaled_text).toBe(true)\n\n    renderer.destroy()\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/terminal-capability-detection.ts",
    "content": "/**\n * Terminal capability response detection utilities.\n *\n * Detects various terminal capability response sequences:\n * - DECRPM (DEC Request Mode): ESC[?...;N$y where N is 0,1,2,3,4\n * - CPR (Cursor Position Report): ESC[row;colR (used for width detection)\n * - XTVersion: ESC P >| ... ESC \\\n * - Kitty Graphics: ESC _ G ... ESC \\\n * - Kitty Keyboard Query: ESC[?Nu where N is 0,1,2,etc\n * - DA1 (Device Attributes): ESC[?...c\n * - Pixel Resolution: ESC[4;height;widtht\n */\n\n/**\n * Check if a sequence is a terminal capability response.\n * Returns true if the sequence matches any known capability response pattern.\n */\nexport function isCapabilityResponse(sequence: string): boolean {\n  // DECRPM: ESC[?digits;digits$y\n  if (/\\x1b\\[\\?\\d+(?:;\\d+)*\\$y/.test(sequence)) {\n    return true\n  }\n\n  // CPR for explicit width/scaled text detection: ESC[1;NR where N >= 2\n  // The column number tells us how many characters were rendered with width annotations\n  // ESC[1;1R means no width support (cursor didn't move)\n  // ESC[1;2R or higher means width support (cursor moved after rendering)\n  // We accept any column >= 2 to handle cases where cursor wasn't at exact home position\n  if (/\\x1b\\[1;(?!1R)\\d+R/.test(sequence)) {\n    return true\n  }\n\n  // XTVersion: ESC P >| ... ESC \\\n  if (/\\x1bP>\\|[\\s\\S]*?\\x1b\\\\/.test(sequence)) {\n    return true\n  }\n\n  // Kitty graphics response: ESC _ G ... ESC \\\n  // Matches any graphics response including OK, errors, etc.\n  // This is for filtering capability responses from user input\n  if (/\\x1b_G[\\s\\S]*?\\x1b\\\\/.test(sequence)) {\n    return true\n  }\n\n  // Kitty keyboard query response: ESC[?Nu or ESC[?N;Mu (progressive enhancement)\n  if (/\\x1b\\[\\?\\d+(?:;\\d+)?u/.test(sequence)) {\n    return true\n  }\n\n  // DA1 (Device Attributes): ESC[?...c\n  if (/\\x1b\\[\\?[0-9;]*c/.test(sequence)) {\n    return true\n  }\n\n  return false\n}\n\n/**\n * Check if a sequence is a pixel resolution response.\n * Format: ESC[4;height;widtht\n */\nexport function isPixelResolutionResponse(sequence: string): boolean {\n  return /\\x1b\\[4;\\d+;\\d+t/.test(sequence)\n}\n\n/**\n * Parse pixel resolution from response sequence.\n * Returns { width, height } or null if not a valid resolution response.\n */\nexport function parsePixelResolution(sequence: string): { width: number; height: number } | null {\n  const match = sequence.match(/\\x1b\\[4;(\\d+);(\\d+)t/)\n  if (match) {\n    return {\n      width: parseInt(match[2]),\n      height: parseInt(match[1]),\n    }\n  }\n  return null\n}\n"
  },
  {
    "path": "packages/core/src/lib/terminal-palette.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { TerminalPalette } from \"./terminal-palette.js\"\nimport { EventEmitter } from \"events\"\nimport { Buffer } from \"node:buffer\"\nimport { ManualClock } from \"../testing/manual-clock\"\n\nclass MockStream extends EventEmitter {\n  isTTY = true\n  isRaw = false\n  isPaused() {\n    return false\n  }\n  write(_data: string) {\n    return true\n  }\n}\n\nfunction createPaletteHarness(\n  options: {\n    writeFn?: (data: string | Buffer) => boolean\n    oscSource?: {\n      subscribeOsc(handler: (sequence: string) => void): () => void\n    }\n  } = {},\n) {\n  const stdin = new MockStream() as any\n  const stdout = new MockStream() as any\n  const clock = new ManualClock()\n  const palette = new TerminalPalette(stdin, stdout, options.writeFn, false, options.oscSource, clock)\n\n  return { stdin, stdout, clock, palette }\n}\n\nasync function flushAsync(): Promise<void> {\n  await Promise.resolve()\n  await Promise.resolve()\n}\n\nasync function startPaletteDetection(\n  options: {\n    timeout?: number\n    size?: number\n    writeFn?: (data: string | Buffer) => boolean\n  } = {},\n) {\n  const timeout = options.timeout ?? 2000\n  const harness = createPaletteHarness({ writeFn: options.writeFn })\n  const detectPromise = harness.palette.detect({\n    timeout,\n    size: options.size ?? 256,\n  })\n\n  harness.stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#000000\\x07\"))\n  await flushAsync()\n\n  return { ...harness, detectPromise, timeout }\n}\n\nasync function advanceClock(clock: ManualClock, ms: number): Promise<void> {\n  await flushAsync()\n  // Flush queued 0ms mock terminal responses before advancing the real timeout window.\n  clock.advance(0)\n  await flushAsync()\n  clock.advance(ms)\n  await flushAsync()\n}\n\ntest(\"TerminalPalette detectOSCSupport returns true on response\", async () => {\n  const { stdin, clock, palette } = createPaletteHarness()\n\n  const detectPromise = palette.detectOSCSupport(500)\n\n  stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff0000\\x07\"))\n\n  await advanceClock(clock, 500)\n\n  const result = await detectPromise\n\n  expect(result).toBe(true)\n})\n\ntest(\"TerminalPalette detectOSCSupport returns false on timeout\", async () => {\n  const { clock, palette } = createPaletteHarness()\n\n  const detectPromise = palette.detectOSCSupport(100)\n\n  await advanceClock(clock, 300)\n\n  const result = await detectPromise\n\n  expect(result).toBe(false)\n})\n\ntest(\"TerminalPalette parses OSC 4 hex format correctly\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    for (let i = 0; i < 256; i++) {\n      const color = i === 0 ? \"#ff00aa\" : i === 1 ? \"#00ff00\" : i === 2 ? \"#0000ff\" : \"#000000\"\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};${color}\\x07`))\n    }\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]10;#aabbcc\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]11;#ddeeff\\x07\"))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n  expect(result.palette[2]).toBe(\"#0000ff\")\n  expect(result.defaultForeground).toBe(\"#aabbcc\")\n  expect(result.defaultBackground).toBe(\"#ddeeff\")\n})\n\ntest(\"TerminalPalette parses OSC 4 rgb format with 4 hex digits\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;rgb:ffff/0000/aaaa\\x07\"))\n    for (let i = 1; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toMatch(/^#[0-9a-f]{6}$/)\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n})\n\ntest(\"TerminalPalette parses OSC 4 rgb format with 2 hex digits\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;rgb:ff/00/aa\\x07\"))\n    for (let i = 1; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toMatch(/^#[0-9a-f]{6}$/)\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n})\n\ntest(\"TerminalPalette handles multiple color responses in single buffer\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\n      \"data\",\n      Buffer.from(\n        \"\\x1b]4;0;rgb:0000/0000/0000\\x07\" +\n          \"\\x1b]4;1;rgb:aa00/0000/0000\\x07\" +\n          \"\\x1b]4;2;rgb:0000/aa00/0000\\x07\" +\n          \"\\x1b]4;3;rgb:aa00/aa00/0000\\x07\",\n      ),\n    )\n\n    for (let i = 4; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#000000\")\n  expect(result.palette[1]).toBe(\"#a90000\")\n  expect(result.palette[2]).toBe(\"#00a900\")\n  expect(result.palette[3]).toBe(\"#a9a900\")\n})\n\ntest(\"TerminalPalette handles BEL terminator\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff0000\\x07\"))\n    for (let i = 1; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff0000\")\n})\n\ntest(\"TerminalPalette handles ST terminator\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#00ff00\\x1b\\\\\"))\n    for (let i = 1; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#00ff00\")\n})\n\ntest(\"TerminalPalette scales color components correctly\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;rgb:ffff/0000/0000\\x07\"))\n    for (let i = 1; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff0000\")\n})\n\ntest(\"TerminalPalette returns null for colors that don't respond\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection({ timeout: 1000 })\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff0000\\x07\"))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff0000\")\n  expect(result.palette.some((color: string | null) => color === null)).toBe(true)\n})\n\ntest(\"TerminalPalette handles response split across chunks\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff\"))\n    stdin.emit(\"data\", Buffer.from(\"00aa\\x07\"))\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;rgb:0000/\"))\n    stdin.emit(\"data\", Buffer.from(\"ffff/\"))\n    stdin.emit(\"data\", Buffer.from(\"0000\\x07\"))\n\n    for (let i = 2; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n})\n\ntest(\"TerminalPalette handles OSC response mixed with mouse events\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff00aa\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[<0;10;5M\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;#00ff00\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[<0;11;5M\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;2;#0000ff\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[<0;12;5m\"))\n\n    for (let i = 3; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n  expect(result.palette[2]).toBe(\"#0000ff\")\n})\n\ntest(\"TerminalPalette handles OSC response mixed with key events\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff00aa\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"hello\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;#00ff00\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[A\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;2;#0000ff\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[B\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;3;#ffff00\\x07\"))\n\n    for (let i = 4; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n  expect(result.palette[2]).toBe(\"#0000ff\")\n  expect(result.palette[3]).toBe(\"#ffff00\")\n})\n\ntest(\"TerminalPalette handles response split mid-escape sequence\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b\"))\n    stdin.emit(\"data\", Buffer.from(\"]4;0;#ff00aa\\x07\"))\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]\"))\n    stdin.emit(\"data\", Buffer.from(\"4;1;#00ff00\\x07\"))\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4\"))\n    stdin.emit(\"data\", Buffer.from(\";2;#0000ff\\x07\"))\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;\"))\n    stdin.emit(\"data\", Buffer.from(\"3;#ffff00\\x07\"))\n\n    for (let i = 4; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n  expect(result.palette[2]).toBe(\"#0000ff\")\n  expect(result.palette[3]).toBe(\"#ffff00\")\n})\n\ntest(\"TerminalPalette handles mixed ANSI sequences and OSC responses\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[2J\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff00aa\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[H\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;#00ff00\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[31m\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;2;#0000ff\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[0m\"))\n\n    for (let i = 3; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n  expect(result.palette[2]).toBe(\"#0000ff\")\n})\n\ntest(\"TerminalPalette handles complex chunking with partial responses\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    const response0 = \"\\x1b]4;0;rgb:ffff/0000/aaaa\\x07\"\n    for (let i = 0; i < response0.length; i += 3) {\n      stdin.emit(\"data\", Buffer.from(response0.slice(i, i + 3)))\n    }\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1\"))\n    stdin.emit(\"data\", Buffer.from(\";#00\"))\n    stdin.emit(\"data\", Buffer.from(\"some junk data\"))\n    stdin.emit(\"data\", Buffer.from(\"ff00\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[D\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x07\"))\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;#00ff00\\x07\"))\n\n    for (let i = 2; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n})\n\ntest(\"TerminalPalette ignores malformed responses and waits for valid ones\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff00\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;rgb:gg00/0000/0000\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;2;#zzzzzz\\x07\"))\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff00aa\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;#00ff00\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;2;#0000ff\\x07\"))\n\n    for (let i = 3; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n  expect(result.palette[2]).toBe(\"#0000ff\")\n})\n\ntest(\"TerminalPalette handles buffer overflow gracefully\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    const junkData = \"x\".repeat(10000)\n    stdin.emit(\"data\", Buffer.from(junkData))\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff00aa\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;#00ff00\\x07\"))\n\n    for (let i = 2; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff00aa\")\n  expect(result.palette[1]).toBe(\"#00ff00\")\n})\n\ntest(\"TerminalPalette handles all 256 colors in a single blob\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    let blob = \"\"\n    for (let i = 0; i < 256; i++) {\n      const color = i === 0 ? \"#ff0011\" : i === 1 ? \"#00ff22\" : i === 255 ? \"#aabbcc\" : \"#000000\"\n      blob += `\\x1b]4;${i};${color}\\x07`\n    }\n\n    stdin.emit(\"data\", Buffer.from(blob))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff0011\")\n  expect(result.palette[1]).toBe(\"#00ff22\")\n  expect(result.palette[255]).toBe(\"#aabbcc\")\n  expect(result.palette.length).toBe(256)\n})\n\ntest(\"TerminalPalette handles blob split across multiple chunks\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    let blob = \"\"\n    for (let i = 0; i < 256; i++) {\n      const color = i === 5 ? \"#112233\" : i === 100 ? \"#445566\" : i === 200 ? \"#778899\" : \"#000000\"\n      blob += `\\x1b]4;${i};${color}\\x07`\n    }\n\n    const chunkSize = 500\n    for (let i = 0; i < blob.length; i += chunkSize) {\n      stdin.emit(\"data\", Buffer.from(blob.slice(i, i + chunkSize)))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[5]).toBe(\"#112233\")\n  expect(result.palette[100]).toBe(\"#445566\")\n  expect(result.palette[200]).toBe(\"#778899\")\n  expect(result.palette.length).toBe(256)\n})\n\ntest(\"TerminalPalette handles blob with mixed junk data\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    let blob = \"\"\n    for (let i = 0; i < 256; i++) {\n      const color = i === 10 ? \"#abcdef\" : i === 50 ? \"#fedcba\" : \"#000000\"\n      blob += `\\x1b]4;${i};${color}\\x07`\n\n      if (i % 20 === 0) {\n        blob += \"JUNK_DATA_HERE\"\n      }\n      if (i % 30 === 0) {\n        blob += \"\\x1b[2J\\x1b[H\"\n      }\n      if (i % 40 === 0) {\n        blob += \"\\x1b[<0;10;5M\"\n      }\n    }\n\n    stdin.emit(\"data\", Buffer.from(blob))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[10]).toBe(\"#abcdef\")\n  expect(result.palette[50]).toBe(\"#fedcba\")\n  expect(result.palette.length).toBe(256)\n})\n\ntest(\"TerminalPalette handles realistic terminal response pattern\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    let chunk1 = \"\"\n    for (let i = 0; i <= 5; i++) {\n      chunk1 += `\\x1b]4;${i};#ff0000\\x07`\n    }\n    stdin.emit(\"data\", Buffer.from(chunk1))\n\n    let chunk2 = \"\"\n    for (let i = 6; i <= 50; i++) {\n      chunk2 += `\\x1b]4;${i};#00ff00\\x07`\n    }\n    stdin.emit(\"data\", Buffer.from(chunk2.slice(0, 200)))\n    stdin.emit(\"data\", Buffer.from(chunk2.slice(200)))\n\n    stdin.emit(\"data\", Buffer.from(\"\\x1b[<35;20;10M\"))\n    let chunk3 = \"\"\n    for (let i = 51; i <= 150; i++) {\n      chunk3 += `\\x1b]4;${i};#0000ff\\x07`\n    }\n    stdin.emit(\"data\", Buffer.from(chunk3))\n\n    let chunk4 = \"\"\n    for (let i = 151; i <= 255; i++) {\n      chunk4 += `\\x1b]4;${i};#ffffff\\x07`\n    }\n    stdin.emit(\"data\", Buffer.from(chunk4))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#ff0000\")\n  expect(result.palette[5]).toBe(\"#ff0000\")\n  expect(result.palette[6]).toBe(\"#00ff00\")\n  expect(result.palette[50]).toBe(\"#00ff00\")\n  expect(result.palette[51]).toBe(\"#0000ff\")\n  expect(result.palette[150]).toBe(\"#0000ff\")\n  expect(result.palette[151]).toBe(\"#ffffff\")\n  expect(result.palette[255]).toBe(\"#ffffff\")\n  expect(result.palette.length).toBe(256)\n})\n\ntest(\"TerminalPalette uses custom write function when provided\", async () => {\n  const writtenData: string[] = []\n\n  const customWrite = (data: string | Buffer) => {\n    writtenData.push(data.toString())\n    return true\n  }\n\n  const { stdin, clock, palette } = createPaletteHarness({ writeFn: customWrite })\n\n  const detectPromise = palette.detectOSCSupport(500)\n\n  stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff0000\\x07\"))\n\n  await advanceClock(clock, 500)\n\n  const result = await detectPromise\n\n  expect(result).toBe(true)\n  expect(writtenData.length).toBe(1)\n  expect(writtenData[0]).toBe(\"\\x1b]4;0;?\\x07\")\n})\n\ntest(\"TerminalPalette uses custom write function for palette detection\", async () => {\n  const writtenData: string[] = []\n\n  const customWrite = (data: string | Buffer) => {\n    writtenData.push(data.toString())\n    return true\n  }\n\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection({ writeFn: customWrite })\n\n  clock.setTimeout(() => {\n    for (let i = 0; i < 256; i++) {\n      const color = \"#aabbcc\"\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};${color}\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  await detectPromise\n\n  expect(writtenData.length).toBe(3)\n  expect(writtenData[0]).toBe(\"\\x1b]4;0;?\\x07\")\n\n  const paletteQuery = writtenData[1]\n  for (let i = 0; i < 256; i++) {\n    expect(paletteQuery).toContain(`\\x1b]4;${i};?\\x07`)\n  }\n\n  const specialQuery = writtenData[2]\n  expect(specialQuery).toContain(\"\\x1b]10;?\\x07\")\n  expect(specialQuery).toContain(\"\\x1b]11;?\\x07\")\n})\n\ntest(\"TerminalPalette falls back to stdout.write when no custom write function provided\", async () => {\n  const clock = new ManualClock()\n  const stdin = new MockStream() as any\n  const writtenData: string[] = []\n\n  const stdout = new MockStream() as any\n  stdout.write = (data: string) => {\n    writtenData.push(data)\n    return true\n  }\n\n  const palette = new TerminalPalette(stdin, stdout, undefined, false, undefined, clock)\n\n  const detectPromise = palette.detectOSCSupport(500)\n\n  stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff0000\\x07\"))\n\n  await advanceClock(clock, 500)\n\n  const result = await detectPromise\n\n  expect(result).toBe(true)\n  expect(writtenData.length).toBe(1)\n  expect(writtenData[0]).toBe(\"\\x1b]4;0;?\\x07\")\n})\n\ntest(\"TerminalPalette custom write function can intercept and modify output\", async () => {\n  const interceptedWrites: string[] = []\n  let actualWrites = 0\n\n  const customWrite = (data: string | Buffer) => {\n    interceptedWrites.push(data.toString())\n    actualWrites++\n    return true\n  }\n\n  const { stdin, clock, palette } = createPaletteHarness({ writeFn: customWrite })\n\n  const detectPromise = palette.detectOSCSupport(500)\n\n  stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#ff0000\\x07\"))\n\n  await advanceClock(clock, 500)\n\n  await detectPromise\n\n  expect(actualWrites).toBe(1)\n  expect(interceptedWrites.length).toBe(1)\n  expect(interceptedWrites[0]).toBe(\"\\x1b]4;0;?\\x07\")\n})\n\ntest(\"TerminalPalette detects all special OSC colors (10-19)\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    for (let i = 0; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]10;#ff0001\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]11;#ff0002\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]12;#ff0003\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]13;#ff0004\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]14;#ff0005\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]15;#ff0006\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]16;#ff0007\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]17;#ff0008\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]19;#ff0009\\x07\"))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.defaultForeground).toBe(\"#ff0001\")\n  expect(result.defaultBackground).toBe(\"#ff0002\")\n  expect(result.cursorColor).toBe(\"#ff0003\")\n  expect(result.mouseForeground).toBe(\"#ff0004\")\n  expect(result.mouseBackground).toBe(\"#ff0005\")\n  expect(result.tekForeground).toBe(\"#ff0006\")\n  expect(result.tekBackground).toBe(\"#ff0007\")\n  expect(result.highlightBackground).toBe(\"#ff0008\")\n  expect(result.highlightForeground).toBe(\"#ff0009\")\n})\n\ntest(\"TerminalPalette handles special colors in rgb format\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    for (let i = 0; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]10;rgb:ffff/0000/0000\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]11;rgb:0000/ffff/0000\\x07\"))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.defaultForeground).toBe(\"#ff0000\")\n  expect(result.defaultBackground).toBe(\"#00ff00\")\n})\n\ntest(\"TerminalPalette handles missing special colors gracefully\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    for (let i = 0; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]10;#ff0001\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]11;#ff0002\\x07\"))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.defaultForeground).toBe(\"#ff0001\")\n  expect(result.defaultBackground).toBe(\"#ff0002\")\n  expect(result.cursorColor).toBe(null)\n  expect(result.mouseForeground).toBe(null)\n  expect(result.mouseBackground).toBe(null)\n})\n\ntest(\"TerminalPalette special colors with ST terminator\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    for (let i = 0; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]10;#aabbcc\\x1b\\\\\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]11;#ddeeff\\x1b\\\\\"))\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.defaultForeground).toBe(\"#aabbcc\")\n  expect(result.defaultBackground).toBe(\"#ddeeff\")\n})\n\ntest(\"TerminalPalette handles mixed palette and special color responses\", async () => {\n  const { stdin, clock, detectPromise, timeout } = await startPaletteDetection()\n\n  clock.setTimeout(() => {\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#010203\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]10;#aabbcc\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;#040506\\x07\"))\n    stdin.emit(\"data\", Buffer.from(\"\\x1b]11;#ddeeff\\x07\"))\n    for (let i = 2; i < 256; i++) {\n      stdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#000000\\x07`))\n    }\n  }, 0)\n\n  await advanceClock(clock, timeout)\n\n  const result = await detectPromise\n\n  expect(result.palette[0]).toBe(\"#010203\")\n  expect(result.palette[1]).toBe(\"#040506\")\n  expect(result.defaultForeground).toBe(\"#aabbcc\")\n  expect(result.defaultBackground).toBe(\"#ddeeff\")\n})\n\ntest(\"TerminalPalette returns null special colors on non-TTY\", async () => {\n  const { stdin, stdout, clock, palette } = createPaletteHarness()\n  stdin.isTTY = false\n  stdout.isTTY = false\n\n  const detectPromise = palette.detect({ timeout: 100 })\n\n  await advanceClock(clock, 100)\n\n  const result = await detectPromise\n\n  expect(result.defaultForeground).toBe(null)\n  expect(result.defaultBackground).toBe(null)\n  expect(result.cursorColor).toBe(null)\n  expect(result.palette.every((c: string | null) => c === null)).toBe(true)\n})\n\ntest(\"TerminalPalette returns null special colors on OSC not supported\", async () => {\n  const { clock, palette } = createPaletteHarness()\n\n  const detectPromise = palette.detect({ timeout: 100 })\n\n  await advanceClock(clock, 300)\n\n  const result = await detectPromise\n\n  expect(result.defaultForeground).toBe(null)\n  expect(result.defaultBackground).toBe(null)\n  expect(result.cursorColor).toBe(null)\n  expect(result.palette.every((c: string | null) => c === null)).toBe(true)\n})\n\ntest(\"TerminalPalette can read OSC from router subscription source\", async () => {\n  const stdin = new MockStream() as any\n\n  const handlers = new Set<(sequence: string) => void>()\n  let subscribeCount = 0\n  let unsubscribeCount = 0\n\n  const oscSource = {\n    subscribeOsc(handler: (sequence: string) => void) {\n      subscribeCount++\n      handlers.add(handler)\n      return () => {\n        unsubscribeCount++\n        handlers.delete(handler)\n      }\n    },\n  }\n\n  const clock = new ManualClock()\n  const stdout = new MockStream() as any\n  const palette = new TerminalPalette(stdin, stdout, undefined, false, oscSource, clock)\n\n  const detectPromise = palette.detectOSCSupport(500)\n  for (const handler of handlers) {\n    handler(\"\\x1b]4;0;#ff0000\\x07\")\n  }\n\n  await advanceClock(clock, 500)\n\n  const supported = await detectPromise\n  expect(supported).toBe(true)\n  expect(subscribeCount).toBe(1)\n  expect(unsubscribeCount).toBe(1)\n  expect(stdin.listenerCount(\"data\")).toBe(0)\n})\n"
  },
  {
    "path": "packages/core/src/lib/terminal-palette.ts",
    "content": "import { SystemClock, type Clock, type TimerHandle } from \"./clock\"\n\ntype Hex = string | null\n\nconst SYSTEM_CLOCK = new SystemClock()\n\nconst OSC4_RESPONSE =\n  /\\x1b]4;(\\d+);(?:(?:rgb:)([0-9a-fA-F]+)\\/([0-9a-fA-F]+)\\/([0-9a-fA-F]+)|#([0-9a-fA-F]{6}))(?:\\x07|\\x1b\\\\)/g\n\nconst OSC_SPECIAL_RESPONSE =\n  /\\x1b](\\d+);(?:(?:rgb:)([0-9a-fA-F]+)\\/([0-9a-fA-F]+)\\/([0-9a-fA-F]+)|#([0-9a-fA-F]{6}))(?:\\x07|\\x1b\\\\)/g\n\nexport type WriteFunction = (data: string | Buffer) => boolean\n\nexport interface TerminalColors {\n  palette: Hex[]\n  defaultForeground: Hex\n  defaultBackground: Hex\n  cursorColor: Hex\n  mouseForeground: Hex\n  mouseBackground: Hex\n  tekForeground: Hex\n  tekBackground: Hex\n  highlightBackground: Hex\n  highlightForeground: Hex\n}\n\nexport interface GetPaletteOptions {\n  timeout?: number\n  size?: number\n}\n\nexport interface TerminalPaletteDetector {\n  detect(options?: GetPaletteOptions): Promise<TerminalColors>\n  detectOSCSupport(timeoutMs?: number): Promise<boolean>\n  cleanup(): void\n}\n\nexport type OscSubscriptionSource = {\n  subscribeOsc(handler: (sequence: string) => void): () => void\n}\n\nfunction scaleComponent(comp: string): string {\n  const val = parseInt(comp, 16)\n  const maxIn = (1 << (4 * comp.length)) - 1\n  return Math.round((val / maxIn) * 255)\n    .toString(16)\n    .padStart(2, \"0\")\n}\n\nfunction toHex(r?: string, g?: string, b?: string, hex6?: string): string {\n  if (hex6) return `#${hex6.toLowerCase()}`\n  if (r && g && b) return `#${scaleComponent(r)}${scaleComponent(g)}${scaleComponent(b)}`\n  return \"#000000\"\n}\n\n/**\n * Wrap OSC sequence for tmux passthrough\n * tmux requires DCS sequences to pass OSC to the underlying terminal\n * Format: ESC P tmux; ESC <OSC_SEQUENCE> ESC \\\n */\nfunction wrapForTmux(osc: string): string {\n  // Replace ESC with ESC ESC for tmux (escape the escape)\n  const escaped = osc.replace(/\\x1b/g, \"\\x1b\\x1b\")\n  return `\\x1bPtmux;${escaped}\\x1b\\\\`\n}\n\nexport class TerminalPalette implements TerminalPaletteDetector {\n  private stdin: NodeJS.ReadStream\n  private stdout: NodeJS.WriteStream\n  private writeFn: WriteFunction\n  private activeQuerySessions: Array<() => void> = []\n  private inLegacyTmux: boolean\n  private oscSource?: OscSubscriptionSource\n  private readonly clock: Clock\n\n  constructor(\n    stdin: NodeJS.ReadStream,\n    stdout: NodeJS.WriteStream,\n    writeFn?: WriteFunction,\n    isLegacyTmux?: boolean,\n    oscSource?: OscSubscriptionSource,\n    clock?: Clock,\n  ) {\n    this.stdin = stdin\n    this.stdout = stdout\n    this.writeFn = writeFn || ((data: string | Buffer) => stdout.write(data))\n    this.inLegacyTmux = isLegacyTmux ?? false\n    this.oscSource = oscSource\n    this.clock = clock ?? SYSTEM_CLOCK\n  }\n\n  /**\n   * Write an OSC sequence, wrapping for tmux if needed\n   */\n  private writeOsc(osc: string): boolean {\n    const data = this.inLegacyTmux ? wrapForTmux(osc) : osc\n    return this.writeFn(data)\n  }\n\n  cleanup(): void {\n    for (const cleanupSession of [...this.activeQuerySessions]) {\n      cleanupSession()\n    }\n    this.activeQuerySessions = []\n  }\n\n  private subscribeInput(handler: (chunk: string | Buffer) => void): () => void {\n    if (this.oscSource) {\n      return this.oscSource.subscribeOsc((sequence) => {\n        handler(sequence)\n      })\n    }\n\n    this.stdin.on(\"data\", handler)\n    return () => {\n      this.stdin.removeListener(\"data\", handler)\n    }\n  }\n\n  private createQuerySession() {\n    const timers = new Set<TimerHandle>()\n    const subscriptions = new Set<() => void>()\n    let closed = false\n\n    const cleanup = () => {\n      if (closed) return\n      closed = true\n\n      for (const timer of timers) {\n        this.clock.clearTimeout(timer)\n      }\n      timers.clear()\n\n      for (const unsubscribe of subscriptions) {\n        unsubscribe()\n      }\n      subscriptions.clear()\n\n      const idx = this.activeQuerySessions.indexOf(cleanup)\n      if (idx !== -1) this.activeQuerySessions.splice(idx, 1)\n    }\n\n    this.activeQuerySessions.push(cleanup)\n\n    return {\n      setTimer: (fn: () => void, ms: number): TimerHandle => {\n        const timer = this.clock.setTimeout(fn, ms)\n        timers.add(timer)\n        return timer\n      },\n      resetTimer: (existing: TimerHandle | null, fn: () => void, ms: number): TimerHandle => {\n        if (existing) {\n          this.clock.clearTimeout(existing)\n          timers.delete(existing)\n        }\n\n        const timer = this.clock.setTimeout(fn, ms)\n        timers.add(timer)\n        return timer\n      },\n      subscribeInput: (handler: (chunk: string | Buffer) => void): (() => void) => {\n        const unsubscribe = this.subscribeInput(handler)\n        subscriptions.add(unsubscribe)\n        return () => {\n          if (!subscriptions.has(unsubscribe)) return\n          subscriptions.delete(unsubscribe)\n          unsubscribe()\n        }\n      },\n      cleanup,\n    }\n  }\n\n  async detectOSCSupport(timeoutMs = 300): Promise<boolean> {\n    const out = this.stdout\n\n    if (!out.isTTY || !this.stdin.isTTY) return false\n\n    return new Promise<boolean>((resolve) => {\n      const session = this.createQuerySession()\n      let buffer = \"\"\n      let settled = false\n\n      const finish = (supported: boolean) => {\n        if (settled) return\n        settled = true\n        session.cleanup()\n        resolve(supported)\n      }\n\n      const onData = (chunk: string | Buffer) => {\n        buffer += chunk.toString()\n        // Reset regex lastIndex before testing due to global flag\n        OSC4_RESPONSE.lastIndex = 0\n        if (OSC4_RESPONSE.test(buffer)) {\n          finish(true)\n        }\n      }\n\n      session.setTimer(() => {\n        finish(false)\n      }, timeoutMs)\n      session.subscribeInput(onData)\n      this.writeOsc(\"\\x1b]4;0;?\\x07\")\n    })\n  }\n\n  private async queryPalette(indices: number[], timeoutMs = 1200): Promise<Map<number, Hex>> {\n    const out = this.stdout\n    const results = new Map<number, Hex>()\n    indices.forEach((i) => results.set(i, null))\n\n    if (!out.isTTY || !this.stdin.isTTY) {\n      return results\n    }\n\n    return new Promise<Map<number, Hex>>((resolve) => {\n      const session = this.createQuerySession()\n      let buffer = \"\"\n      let idleTimer: TimerHandle | null = null\n      let settled = false\n\n      const finish = () => {\n        if (settled) return\n        settled = true\n        session.cleanup()\n        resolve(results)\n      }\n\n      const onData = (chunk: string | Buffer) => {\n        buffer += chunk.toString()\n\n        let m: RegExpExecArray | null\n        OSC4_RESPONSE.lastIndex = 0\n        while ((m = OSC4_RESPONSE.exec(buffer))) {\n          const idx = parseInt(m[1], 10)\n          if (results.has(idx)) results.set(idx, toHex(m[2], m[3], m[4], m[5]))\n        }\n\n        if (buffer.length > 8192) buffer = buffer.slice(-4096)\n\n        const done = [...results.values()].filter((v) => v !== null).length\n        if (done === results.size) {\n          finish()\n          return\n        }\n\n        idleTimer = session.resetTimer(idleTimer, finish, 150)\n      }\n\n      session.setTimer(finish, timeoutMs)\n      session.subscribeInput(onData)\n      this.writeOsc(indices.map((i) => `\\x1b]4;${i};?\\x07`).join(\"\"))\n    })\n  }\n\n  private async querySpecialColors(timeoutMs = 1200): Promise<Record<number, Hex>> {\n    const out = this.stdout\n    const results: Record<number, Hex> = {\n      10: null,\n      11: null,\n      12: null,\n      13: null,\n      14: null,\n      15: null,\n      16: null,\n      17: null,\n      19: null,\n    }\n\n    if (!out.isTTY || !this.stdin.isTTY) {\n      return results\n    }\n\n    return new Promise<Record<number, Hex>>((resolve) => {\n      const session = this.createQuerySession()\n      let buffer = \"\"\n      let idleTimer: TimerHandle | null = null\n      let settled = false\n\n      const finish = () => {\n        if (settled) return\n        settled = true\n        session.cleanup()\n        resolve(results)\n      }\n\n      const onData = (chunk: string | Buffer) => {\n        buffer += chunk.toString()\n        let updated = false\n\n        let m: RegExpExecArray | null\n        OSC_SPECIAL_RESPONSE.lastIndex = 0\n        while ((m = OSC_SPECIAL_RESPONSE.exec(buffer))) {\n          const idx = parseInt(m[1], 10)\n          if (idx in results) {\n            results[idx] = toHex(m[2], m[3], m[4], m[5])\n            updated = true\n          }\n        }\n\n        if (buffer.length > 8192) buffer = buffer.slice(-4096)\n\n        const done = Object.values(results).filter((v) => v !== null).length\n        if (done === Object.keys(results).length) {\n          finish()\n          return\n        }\n\n        if (!updated) return\n\n        idleTimer = session.resetTimer(idleTimer, finish, 150)\n      }\n\n      session.setTimer(finish, timeoutMs)\n      session.subscribeInput(onData)\n      this.writeOsc(\n        [\n          \"\\x1b]10;?\\x07\",\n          \"\\x1b]11;?\\x07\",\n          \"\\x1b]12;?\\x07\",\n          \"\\x1b]13;?\\x07\",\n          \"\\x1b]14;?\\x07\",\n          \"\\x1b]15;?\\x07\",\n          \"\\x1b]16;?\\x07\",\n          \"\\x1b]17;?\\x07\",\n          \"\\x1b]19;?\\x07\",\n        ].join(\"\"),\n      )\n    })\n  }\n\n  async detect(options?: GetPaletteOptions): Promise<TerminalColors> {\n    const { timeout = 5000, size = 16 } = options || {}\n    const supported = await this.detectOSCSupport()\n\n    if (!supported) {\n      return {\n        palette: Array(size).fill(null),\n        defaultForeground: null,\n        defaultBackground: null,\n        cursorColor: null,\n        mouseForeground: null,\n        mouseBackground: null,\n        tekForeground: null,\n        tekBackground: null,\n        highlightBackground: null,\n        highlightForeground: null,\n      }\n    }\n\n    const indicesToQuery = [...Array(size).keys()]\n    const [paletteResults, specialColors] = await Promise.all([\n      this.queryPalette(indicesToQuery, timeout),\n      this.querySpecialColors(timeout),\n    ])\n\n    return {\n      palette: [...Array(size).keys()].map((i) => paletteResults.get(i) ?? null),\n      defaultForeground: specialColors[10],\n      defaultBackground: specialColors[11],\n      cursorColor: specialColors[12],\n      mouseForeground: specialColors[13],\n      mouseBackground: specialColors[14],\n      tekForeground: specialColors[15],\n      tekBackground: specialColors[16],\n      highlightBackground: specialColors[17],\n      highlightForeground: specialColors[19],\n    }\n  }\n}\n\nexport function createTerminalPalette(\n  stdin: NodeJS.ReadStream,\n  stdout: NodeJS.WriteStream,\n  writeFn?: WriteFunction,\n  isLegacyTmux?: boolean,\n  oscSource?: OscSubscriptionSource,\n  clock?: Clock,\n): TerminalPaletteDetector {\n  return new TerminalPalette(stdin, stdout, writeFn, isLegacyTmux, oscSource, clock)\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets/README.md",
    "content": "# Tree-sitter Assets\n\nThis directory contains downloaded tree-sitter language parsers and highlight queries for the default parsers used by OpenTUI.\nThey are included in the repository to avoid downloading them every time the project is built or tests are run.\n\n## Asset Management\n\nParser definitions are configured in `../parsers-config.json`:\n\n### Update Script\n\nThe `update.ts` script downloads parsers and queries from the configured URLs and generates the `../default-parsers.ts` file.\n\n#### Usage\n\n**For OpenTUI Core Development (using default paths):**\n\n```bash\n# Run from this directory\nbun update.ts\n\n# Or from the project root\nbun packages/core/src/lib/tree-sitter/assets/update.ts\n```\n\n**For Application Developers (using custom paths):**\n\n```bash\n# CLI usage with custom paths\nbun update.ts \\\n  --config ./my-parsers-config.json \\\n  --assets ./src/tree-sitter/assets \\\n  --output ./src/tree-sitter/parsers.ts\n\n# Show help\nbun update.ts --help\n```\n\n**Programmatic Usage:**\n\n```typescript\nimport { updateAssets } from \"@opentui/core/lib/tree-sitter/assets/update\"\n\nawait updateAssets({\n  configPath: \"./my-parsers-config.json\",\n  assetsDir: \"./src/tree-sitter/assets\",\n  outputPath: \"./src/tree-sitter/parsers.ts\",\n})\n```\n\n#### What it does\n\n1. **Downloads Language Parsers**: Downloads `.wasm` files from GitHub releases\n2. **Downloads Query Files**: Downloads `.scm` highlight query files from repositories\n3. **Combines Queries**: For languages like TypeScript, combines multiple query files (JS base + TS extensions)\n4. **Generates Imports**: Creates a TypeScript file with proper file imports using Bun's `with { type: \"file\" }` syntax\n\n## Adding New Parsers\n\n### For Application Developers\n\nIf you're using OpenTUI in your application and want to add support for additional languages:\n\n#### 1. Create a parsers configuration file\n\nCreate a `parsers-config.json` file in your project with the structure:\n\n```json\n{\n  \"parsers\": [\n    {\n      \"filetype\": \"python\",\n      \"wasm\": \"https://github.com/tree-sitter/tree-sitter-python/releases/download/v0.20.4/tree-sitter-python.wasm\",\n      \"queries\": {\n        \"highlights\": [\n          \"https://raw.githubusercontent.com/tree-sitter/tree-sitter-python/refs/heads/master/queries/highlights.scm\"\n        ]\n      }\n    }\n  ]\n}\n```\n\n#### 2. Add to your build pipeline\n\nAdd the update script to your `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"prebuild\": \"bun node_modules/@opentui/core/lib/tree-sitter/assets/update.ts --config ./parsers-config.json --assets ./src/parsers --output ./src/parsers.ts\",\n    \"build\": \"bun build ./src/index.ts\"\n  }\n}\n```\n\n#### 3. Use the generated parsers\n\n```typescript\nimport { getTreeSitterClient } from \"@opentui/core\"\nimport { getParsers } from \"./parsers\"\n\nconst client = getTreeSitterClient()\n\n// Register your custom parsers\nfor (const parser of getParsers()) {\n  client.addFiletypeParser(parser)\n}\n```\n\nFor more information about using Tree-Sitter in your application, see the [Tree-Sitter guide](../../../docs/tree-sitter.md).\n\n### For OpenTUI Core Developers\n\nTo add a new default parser to OpenTUI Core:\n\n1. **Update Configuration**: Add the new parser to `../parsers-config.json`\n2. **Run Update Script**: Execute `bun update.ts` to download assets and regenerate imports\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets/javascript/highlights.scm",
    "content": "; Query from: https://raw.githubusercontent.com/tree-sitter/tree-sitter-javascript/refs/heads/master/queries/highlights.scm\n; Variables\n;----------\n\n(identifier) @variable\n\n; Properties\n;-----------\n\n(property_identifier) @property\n\n; Function and method definitions\n;--------------------------------\n\n(function_expression\n  name: (identifier) @function)\n(function_declaration\n  name: (identifier) @function)\n(method_definition\n  name: (property_identifier) @function.method)\n\n(pair\n  key: (property_identifier) @function.method\n  value: [(function_expression) (arrow_function)])\n\n(assignment_expression\n  left: (member_expression\n    property: (property_identifier) @function.method)\n  right: [(function_expression) (arrow_function)])\n\n(variable_declarator\n  name: (identifier) @function\n  value: [(function_expression) (arrow_function)])\n\n(assignment_expression\n  left: (identifier) @function\n  right: [(function_expression) (arrow_function)])\n\n; Function and method calls\n;--------------------------\n\n(call_expression\n  function: (identifier) @function)\n\n(call_expression\n  function: (member_expression\n    property: (property_identifier) @function.method))\n\n; Special identifiers\n;--------------------\n\n((identifier) @constructor\n (#match? @constructor \"^[A-Z]\"))\n\n([\n    (identifier)\n    (shorthand_property_identifier)\n    (shorthand_property_identifier_pattern)\n ] @constant\n (#match? @constant \"^[A-Z_][A-Z\\\\d_]+$\"))\n\n((identifier) @variable.builtin\n (#match? @variable.builtin \"^(arguments|module|console|window|document)$\")\n (#is-not? local))\n\n((identifier) @function.builtin\n (#eq? @function.builtin \"require\")\n (#is-not? local))\n\n; Literals\n;---------\n\n(this) @variable.builtin\n(super) @variable.builtin\n\n[\n  (true)\n  (false)\n  (null)\n  (undefined)\n] @constant.builtin\n\n(comment) @comment\n\n[\n  (string)\n  (template_string)\n] @string\n\n(regex) @string.special\n(number) @number\n\n; Tokens\n;-------\n\n[\n  \";\"\n  (optional_chain)\n  \".\"\n  \",\"\n] @punctuation.delimiter\n\n[\n  \"-\"\n  \"--\"\n  \"-=\"\n  \"+\"\n  \"++\"\n  \"+=\"\n  \"*\"\n  \"*=\"\n  \"**\"\n  \"**=\"\n  \"/\"\n  \"/=\"\n  \"%\"\n  \"%=\"\n  \"<\"\n  \"<=\"\n  \"<<\"\n  \"<<=\"\n  \"=\"\n  \"==\"\n  \"===\"\n  \"!\"\n  \"!=\"\n  \"!==\"\n  \"=>\"\n  \">\"\n  \">=\"\n  \">>\"\n  \">>=\"\n  \">>>\"\n  \">>>=\"\n  \"~\"\n  \"^\"\n  \"&\"\n  \"|\"\n  \"^=\"\n  \"&=\"\n  \"|=\"\n  \"&&\"\n  \"||\"\n  \"??\"\n  \"&&=\"\n  \"||=\"\n  \"??=\"\n] @operator\n\n[\n  \"(\"\n  \")\"\n  \"[\"\n  \"]\"\n  \"{\"\n  \"}\"\n]  @punctuation.bracket\n\n(template_substitution\n  \"${\" @punctuation.special\n  \"}\" @punctuation.special) @embedded\n\n[\n  \"as\"\n  \"async\"\n  \"await\"\n  \"break\"\n  \"case\"\n  \"catch\"\n  \"class\"\n  \"const\"\n  \"continue\"\n  \"debugger\"\n  \"default\"\n  \"delete\"\n  \"do\"\n  \"else\"\n  \"export\"\n  \"extends\"\n  \"finally\"\n  \"for\"\n  \"from\"\n  \"function\"\n  \"get\"\n  \"if\"\n  \"import\"\n  \"in\"\n  \"instanceof\"\n  \"let\"\n  \"new\"\n  \"of\"\n  \"return\"\n  \"set\"\n  \"static\"\n  \"switch\"\n  \"target\"\n  \"throw\"\n  \"try\"\n  \"typeof\"\n  \"var\"\n  \"void\"\n  \"while\"\n  \"with\"\n  \"yield\"\n] @keyword\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets/markdown/highlights.scm",
    "content": "; Query from: https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/markdown/highlights.scm\n;From MDeiml/tree-sitter-markdown & Helix\n(setext_heading\n  (paragraph) @markup.heading.1\n  (setext_h1_underline) @markup.heading.1)\n\n(setext_heading\n  (paragraph) @markup.heading.2\n  (setext_h2_underline) @markup.heading.2)\n\n; Capture the entire heading node first based on the marker it contains\n((atx_heading (atx_h1_marker)) @markup.heading.1\n  (#set! priority 90))\n\n((atx_heading (atx_h2_marker)) @markup.heading.2\n  (#set! priority 90))\n\n((atx_heading (atx_h3_marker)) @markup.heading.3\n  (#set! priority 90))\n\n((atx_heading (atx_h4_marker)) @markup.heading.4\n  (#set! priority 90))\n\n((atx_heading (atx_h5_marker)) @markup.heading.5\n  (#set! priority 90))\n\n((atx_heading (atx_h6_marker)) @markup.heading.6\n  (#set! priority 90))\n\n; Then capture and conceal just the markers (they don't need special styling)\n(atx_heading\n  (atx_h1_marker) @conceal\n  (#set! conceal \"\"))\n\n(atx_heading\n  (atx_h2_marker) @conceal\n  (#set! conceal \"\"))\n\n(atx_heading\n  (atx_h3_marker) @conceal\n  (#set! conceal \"\"))\n\n(atx_heading\n  (atx_h4_marker) @conceal\n  (#set! conceal \"\"))\n\n(atx_heading\n  (atx_h5_marker) @conceal\n  (#set! conceal \"\"))\n\n(atx_heading\n  (atx_h6_marker) @conceal\n  (#set! conceal \"\"))\n\n(info_string) @label\n\n(pipe_table_header\n  (pipe_table_cell) @markup.heading)\n\n(pipe_table_header\n  \"|\" @punctuation.special)\n\n(pipe_table_row\n  \"|\" @punctuation.special)\n\n(pipe_table_delimiter_row\n  \"|\" @punctuation.special)\n\n(pipe_table_delimiter_cell) @punctuation.special\n\n; Code blocks (conceal backticks and language annotation)\n(indented_code_block) @markup.raw.block\n\n((fenced_code_block) @markup.raw.block\n  (#set! priority 90))\n\n(fenced_code_block\n  (fenced_code_block_delimiter) @markup.raw.block\n  (#set! conceal \"\")\n  (#set! conceal_lines \"\"))\n\n(fenced_code_block\n  (info_string\n    (language) @label\n    (#set! conceal \"\")\n    (#set! conceal_lines \"\")))\n\n(link_destination) @markup.link.url\n\n[\n  (link_title)\n  (link_label)\n] @markup.link.label\n\n((link_label)\n  .\n  \":\" @punctuation.delimiter)\n\n[\n  (list_marker_plus)\n  (list_marker_minus)\n  (list_marker_star)\n  (list_marker_dot)\n  (list_marker_parenthesis)\n] @markup.list\n\n; NOTE: The following has been commented out due to issues with spaces in the\n; list marker nodes generated by the parser. If those spaces ever get captured\n; by a different node (e.g. block_continuation) we can safely re-add these\n; conceals.\n; ;; Conceal bullet points\n; ([(list_marker_plus) (list_marker_star)]\n;   @punctuation.special\n;   (#offset! @punctuation.special 0 0 0 -1)\n;   (#set! conceal \"•\"))\n; ([(list_marker_plus) (list_marker_star)]\n;   @punctuation.special\n;   (#any-of? @punctuation.special \"+\" \"*\")\n;   (#set! conceal \"•\"))\n; ((list_marker_minus)\n;   @punctuation.special\n;   (#offset! @punctuation.special 0 0 0 -1)\n;   (#set! conceal \"—\"))\n; ((list_marker_minus)\n;   @punctuation.special\n;   (#eq? @punctuation.special \"-\")\n;   (#set! conceal \"—\"))\n(thematic_break) @punctuation.special\n\n(task_list_marker_unchecked) @markup.list.unchecked\n\n(task_list_marker_checked) @markup.list.checked\n\n((block_quote) @markup.quote\n  (#set! priority 90))\n\n([\n  (plus_metadata)\n  (minus_metadata)\n] @keyword.directive\n  (#set! priority 90))\n\n[\n  (block_continuation)\n  (block_quote_marker)\n] @punctuation.special\n\n(backslash_escape) @string.escape\n\n(inline) @spell\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets/markdown/injections.scm",
    "content": "; Query from: https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/markdown/injections.scm\n(fenced_code_block\n  (info_string\n    (language) @_lang)\n  (code_fence_content) @injection.content\n  (#set-lang-from-info-string! @_lang))\n\n((html_block) @injection.content\n  (#set! injection.language \"html\")\n  (#set! injection.combined)\n  (#set! injection.include-children))\n\n((minus_metadata) @injection.content\n  (#set! injection.language \"yaml\")\n  (#offset! @injection.content 1 0 -1 0)\n  (#set! injection.include-children))\n\n((plus_metadata) @injection.content\n  (#set! injection.language \"toml\")\n  (#offset! @injection.content 1 0 -1 0)\n  (#set! injection.include-children))\n\n([\n  (inline)\n  (pipe_table_cell)\n] @injection.content\n  (#set! injection.language \"markdown_inline\"))\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets/markdown_inline/highlights.scm",
    "content": "; Query from: https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/99ddf573531c4dbe53f743ecbc1595af5eb1d32f/queries/markdown_inline/highlights.scm\n; From MDeiml/tree-sitter-markdown\n(code_span) @markup.raw @nospell\n\n(emphasis) @markup.italic\n\n(strong_emphasis) @markup.strong\n\n(strikethrough) @markup.strikethrough\n\n(shortcut_link\n  (link_text) @nospell)\n\n[\n  (backslash_escape)\n  (hard_line_break)\n] @string.escape\n\n; Conceal codeblock and text style markers\n([\n  (code_span_delimiter)\n  (emphasis_delimiter)\n] @conceal\n  (#set! conceal \"\"))\n\n; Inline links - style all parts\n(inline_link\n  [\"[\" \"(\" \")\"] @markup.link)\n\n(inline_link\n  \"]\" @markup.link.bracket.close)\n\n; Conceal opening bracket\n((inline_link\n  \"[\" @conceal)\n  (#set! conceal \"\"))\n\n; Conceal closing bracket with space replacement\n((inline_link\n  \"]\" @conceal)\n  (#set! conceal \" \"))\n\n; Conceal image links\n(image\n  [\n    \"!\"\n    \"[\"\n    \"]\"\n    \"(\"\n    (link_destination)\n    \")\"\n  ] @markup.link\n  (#set! conceal \"\"))\n\n; Conceal full reference links\n(full_reference_link\n  [\n    \"[\"\n    \"]\"\n    (link_label)\n  ] @markup.link\n  (#set! conceal \"\"))\n\n; Conceal collapsed reference links\n(collapsed_reference_link\n  [\n    \"[\"\n    \"]\"\n  ] @markup.link\n  (#set! conceal \"\"))\n\n; Conceal shortcut links\n(shortcut_link\n  [\n    \"[\"\n    \"]\"\n  ] @markup.link\n  (#set! conceal \"\"))\n\n[\n  (link_destination)\n  (uri_autolink)\n] @markup.link.url @nospell\n\n[\n  (link_label)\n  (link_text)\n  (link_title)\n  (image_description)\n] @markup.link.label\n\n; Replace common HTML entities.\n((entity_reference) @character.special\n  (#eq? @character.special \"&nbsp;\")\n  (#set! conceal \"\"))\n\n((entity_reference) @character.special\n  (#eq? @character.special \"&lt;\")\n  (#set! conceal \"<\"))\n\n((entity_reference) @character.special\n  (#eq? @character.special \"&gt;\")\n  (#set! conceal \">\"))\n\n((entity_reference) @character.special\n  (#eq? @character.special \"&amp;\")\n  (#set! conceal \"&\"))\n\n((entity_reference) @character.special\n  (#eq? @character.special \"&quot;\")\n  (#set! conceal \"\\\"\"))\n\n((entity_reference) @character.special\n  (#any-of? @character.special \"&ensp;\" \"&emsp;\")\n  (#set! conceal \" \"))\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets/typescript/highlights.scm",
    "content": "; Query from: https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/ecma/highlights.scm\n; Types\n; Javascript\n; Variables\n;-----------\n(identifier) @variable\n\n; Properties\n;-----------\n(property_identifier) @variable.member\n\n(shorthand_property_identifier) @variable.member\n\n(private_property_identifier) @variable.member\n\n(object_pattern\n  (shorthand_property_identifier_pattern) @variable)\n\n(object_pattern\n  (object_assignment_pattern\n    (shorthand_property_identifier_pattern) @variable))\n\n; Special identifiers\n;--------------------\n((identifier) @type\n  (#lua-match? @type \"^[A-Z]\"))\n\n((identifier) @constant\n  (#lua-match? @constant \"^_*[A-Z][A-Z%d_]*$\"))\n\n((shorthand_property_identifier) @constant\n  (#lua-match? @constant \"^_*[A-Z][A-Z%d_]*$\"))\n\n((identifier) @variable.builtin\n  (#any-of? @variable.builtin \"arguments\" \"module\" \"console\" \"window\" \"document\"))\n\n((identifier) @type.builtin\n  (#any-of? @type.builtin\n    \"Object\" \"Function\" \"Boolean\" \"Symbol\" \"Number\" \"Math\" \"Date\" \"String\" \"RegExp\" \"Map\" \"Set\"\n    \"WeakMap\" \"WeakSet\" \"Promise\" \"Array\" \"Int8Array\" \"Uint8Array\" \"Uint8ClampedArray\" \"Int16Array\"\n    \"Uint16Array\" \"Int32Array\" \"Uint32Array\" \"Float32Array\" \"Float64Array\" \"ArrayBuffer\" \"DataView\"\n    \"Error\" \"EvalError\" \"InternalError\" \"RangeError\" \"ReferenceError\" \"SyntaxError\" \"TypeError\"\n    \"URIError\"))\n\n(statement_identifier) @label\n\n; Function and method definitions\n;--------------------------------\n(function_expression\n  name: (identifier) @function)\n\n(function_declaration\n  name: (identifier) @function)\n\n(generator_function\n  name: (identifier) @function)\n\n(generator_function_declaration\n  name: (identifier) @function)\n\n(method_definition\n  name: [\n    (property_identifier)\n    (private_property_identifier)\n  ] @function.method)\n\n(method_definition\n  name: (property_identifier) @constructor\n  (#eq? @constructor \"constructor\"))\n\n(pair\n  key: (property_identifier) @function.method\n  value: (function_expression))\n\n(pair\n  key: (property_identifier) @function.method\n  value: (arrow_function))\n\n(assignment_expression\n  left: (member_expression\n    property: (property_identifier) @function.method)\n  right: (arrow_function))\n\n(assignment_expression\n  left: (member_expression\n    property: (property_identifier) @function.method)\n  right: (function_expression))\n\n(variable_declarator\n  name: (identifier) @function\n  value: (arrow_function))\n\n(variable_declarator\n  name: (identifier) @function\n  value: (function_expression))\n\n(assignment_expression\n  left: (identifier) @function\n  right: (arrow_function))\n\n(assignment_expression\n  left: (identifier) @function\n  right: (function_expression))\n\n; Function and method calls\n;--------------------------\n(call_expression\n  function: (identifier) @function.call)\n\n(call_expression\n  function: (member_expression\n    property: [\n      (property_identifier)\n      (private_property_identifier)\n    ] @function.method.call))\n\n(call_expression\n  function: (await_expression\n    (identifier) @function.call))\n\n(call_expression\n  function: (await_expression\n    (member_expression\n      property: [\n        (property_identifier)\n        (private_property_identifier)\n      ] @function.method.call)))\n\n; Builtins\n;---------\n((identifier) @module.builtin\n  (#eq? @module.builtin \"Intl\"))\n\n((identifier) @function.builtin\n  (#any-of? @function.builtin\n    \"eval\" \"isFinite\" \"isNaN\" \"parseFloat\" \"parseInt\" \"decodeURI\" \"decodeURIComponent\" \"encodeURI\"\n    \"encodeURIComponent\" \"require\"))\n\n; Constructor\n;------------\n(new_expression\n  constructor: (identifier) @constructor)\n\n; Decorators\n;----------\n(decorator\n  \"@\" @attribute\n  (identifier) @attribute)\n\n(decorator\n  \"@\" @attribute\n  (call_expression\n    (identifier) @attribute))\n\n(decorator\n  \"@\" @attribute\n  (member_expression\n    (property_identifier) @attribute))\n\n(decorator\n  \"@\" @attribute\n  (call_expression\n    (member_expression\n      (property_identifier) @attribute)))\n\n; Literals\n;---------\n[\n  (this)\n  (super)\n] @variable.builtin\n\n((identifier) @variable.builtin\n  (#eq? @variable.builtin \"self\"))\n\n[\n  (true)\n  (false)\n] @boolean\n\n[\n  (null)\n  (undefined)\n] @constant.builtin\n\n[\n  (comment)\n  (html_comment)\n] @comment @spell\n\n((comment) @comment.documentation\n  (#lua-match? @comment.documentation \"^/[*][*][^*].*[*]/$\"))\n\n(hash_bang_line) @keyword.directive\n\n((string_fragment) @keyword.directive\n  (#eq? @keyword.directive \"use strict\"))\n\n(string) @string\n\n(template_string) @string\n\n(escape_sequence) @string.escape\n\n(regex_pattern) @string.regexp\n\n(regex_flags) @character.special\n\n(regex\n  \"/\" @punctuation.bracket) ; Regex delimiters\n\n(number) @number\n\n((identifier) @number\n  (#any-of? @number \"NaN\" \"Infinity\"))\n\n; Punctuation\n;------------\n[\n  \";\"\n  \".\"\n  \",\"\n  \":\"\n] @punctuation.delimiter\n\n[\n  \"--\"\n  \"-\"\n  \"-=\"\n  \"&&\"\n  \"+\"\n  \"++\"\n  \"+=\"\n  \"&=\"\n  \"/=\"\n  \"**=\"\n  \"<<=\"\n  \"<\"\n  \"<=\"\n  \"<<\"\n  \"=\"\n  \"==\"\n  \"===\"\n  \"!=\"\n  \"!==\"\n  \"=>\"\n  \">\"\n  \">=\"\n  \">>\"\n  \"||\"\n  \"%\"\n  \"%=\"\n  \"*\"\n  \"**\"\n  \">>>\"\n  \"&\"\n  \"|\"\n  \"^\"\n  \"??\"\n  \"*=\"\n  \">>=\"\n  \">>>=\"\n  \"^=\"\n  \"|=\"\n  \"&&=\"\n  \"||=\"\n  \"??=\"\n  \"...\"\n] @operator\n\n(binary_expression\n  \"/\" @operator)\n\n(ternary_expression\n  [\n    \"?\"\n    \":\"\n  ] @keyword.conditional.ternary)\n\n(unary_expression\n  [\n    \"!\"\n    \"~\"\n    \"-\"\n    \"+\"\n  ] @operator)\n\n(unary_expression\n  [\n    \"delete\"\n    \"void\"\n  ] @keyword.operator)\n\n[\n  \"(\"\n  \")\"\n  \"[\"\n  \"]\"\n  \"{\"\n  \"}\"\n] @punctuation.bracket\n\n(template_substitution\n  [\n    \"${\"\n    \"}\"\n  ] @punctuation.special) @none\n\n; Imports\n;----------\n(namespace_import\n  \"*\" @character.special\n  (identifier) @module)\n\n(namespace_export\n  \"*\" @character.special\n  (identifier) @module)\n\n(export_statement\n  \"*\" @character.special)\n\n; Keywords\n;----------\n[\n  \"if\"\n  \"else\"\n  \"switch\"\n  \"case\"\n] @keyword.conditional\n\n[\n  \"import\"\n  \"from\"\n  \"as\"\n  \"export\"\n] @keyword.import\n\n[\n  \"for\"\n  \"of\"\n  \"do\"\n  \"while\"\n  \"continue\"\n] @keyword.repeat\n\n[\n  \"break\"\n  \"const\"\n  \"debugger\"\n  \"extends\"\n  \"get\"\n  \"let\"\n  \"set\"\n  \"static\"\n  \"target\"\n  \"var\"\n  \"with\"\n] @keyword\n\n\"class\" @keyword.type\n\n[\n  \"async\"\n  \"await\"\n] @keyword.coroutine\n\n[\n  \"return\"\n  \"yield\"\n] @keyword.return\n\n\"function\" @keyword.function\n\n[\n  \"new\"\n  \"delete\"\n  \"in\"\n  \"instanceof\"\n  \"typeof\"\n] @keyword.operator\n\n[\n  \"throw\"\n  \"try\"\n  \"catch\"\n  \"finally\"\n] @keyword.exception\n\n(export_statement\n  \"default\" @keyword)\n\n(switch_default\n  \"default\" @keyword.conditional)\n\n\n; Query from: https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/typescript/highlights.scm\n; inherits: ecma\n\n\"require\" @keyword.import\n\n(import_require_clause\n  source: (string) @string.special.url)\n\n[\n  \"declare\"\n  \"implements\"\n  \"type\"\n  \"override\"\n  \"module\"\n  \"asserts\"\n  \"infer\"\n  \"is\"\n  \"using\"\n] @keyword\n\n[\n  \"namespace\"\n  \"interface\"\n  \"enum\"\n] @keyword.type\n\n[\n  \"keyof\"\n  \"satisfies\"\n] @keyword.operator\n\n(as_expression\n  \"as\" @keyword.operator)\n\n(mapped_type_clause\n  \"as\" @keyword.operator)\n\n[\n  \"abstract\"\n  \"private\"\n  \"protected\"\n  \"public\"\n  \"readonly\"\n] @keyword.modifier\n\n; types\n(type_identifier) @type\n\n(predefined_type) @type.builtin\n\n(import_statement\n  \"type\"\n  (import_clause\n    (named_imports\n      (import_specifier\n        name: (identifier) @type))))\n\n(template_literal_type) @string\n\n(non_null_expression\n  \"!\" @operator)\n\n; punctuation\n(type_arguments\n  [\n    \"<\"\n    \">\"\n  ] @punctuation.bracket)\n\n(type_parameters\n  [\n    \"<\"\n    \">\"\n  ] @punctuation.bracket)\n\n(object_type\n  [\n    \"{|\"\n    \"|}\"\n  ] @punctuation.bracket)\n\n(union_type\n  \"|\" @punctuation.delimiter)\n\n(intersection_type\n  \"&\" @punctuation.delimiter)\n\n(type_annotation\n  \":\" @punctuation.delimiter)\n\n(type_predicate_annotation\n  \":\" @punctuation.delimiter)\n\n(index_signature\n  \":\" @punctuation.delimiter)\n\n(omitting_type_annotation\n  \"-?:\" @punctuation.delimiter)\n\n(adding_type_annotation\n  \"+?:\" @punctuation.delimiter)\n\n(opting_type_annotation\n  \"?:\" @punctuation.delimiter)\n\n\"?.\" @punctuation.delimiter\n\n(abstract_method_signature\n  \"?\" @punctuation.special)\n\n(method_signature\n  \"?\" @punctuation.special)\n\n(method_definition\n  \"?\" @punctuation.special)\n\n(property_signature\n  \"?\" @punctuation.special)\n\n(optional_parameter\n  \"?\" @punctuation.special)\n\n(optional_type\n  \"?\" @punctuation.special)\n\n(public_field_definition\n  [\n    \"?\"\n    \"!\"\n  ] @punctuation.special)\n\n(flow_maybe_type\n  \"?\" @punctuation.special)\n\n(template_type\n  [\n    \"${\"\n    \"}\"\n  ] @punctuation.special)\n\n(conditional_type\n  [\n    \"?\"\n    \":\"\n  ] @keyword.conditional.ternary)\n\n; Parameters\n(required_parameter\n  pattern: (identifier) @variable.parameter)\n\n(optional_parameter\n  pattern: (identifier) @variable.parameter)\n\n(required_parameter\n  (rest_pattern\n    (identifier) @variable.parameter))\n\n; ({ a }) => null\n(required_parameter\n  (object_pattern\n    (shorthand_property_identifier_pattern) @variable.parameter))\n\n; ({ a = b }) => null\n(required_parameter\n  (object_pattern\n    (object_assignment_pattern\n      (shorthand_property_identifier_pattern) @variable.parameter)))\n\n; ({ a: b }) => null\n(required_parameter\n  (object_pattern\n    (pair_pattern\n      value: (identifier) @variable.parameter)))\n\n; ([ a ]) => null\n(required_parameter\n  (array_pattern\n    (identifier) @variable.parameter))\n\n; a => null\n(arrow_function\n  parameter: (identifier) @variable.parameter)\n\n; global declaration\n(ambient_declaration\n  \"global\" @module)\n\n; function signatures\n(ambient_declaration\n  (function_signature\n    name: (identifier) @function))\n\n; method signatures\n(method_signature\n  name: (_) @function.method)\n\n(abstract_method_signature\n  name: (property_identifier) @function.method)\n\n; property signatures\n(property_signature\n  name: (property_identifier) @function.method\n  type: (type_annotation\n    [\n      (union_type\n        (parenthesized_type\n          (function_type)))\n      (function_type)\n    ]))\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets/update.ts",
    "content": "#!/usr/bin/env bun\n\nimport { readFile, writeFile, mkdir } from \"fs/promises\"\nimport * as path from \"path\"\nimport { DownloadUtils } from \"../download-utils.js\"\nimport { parseArgs } from \"util\"\nimport type { FiletypeParserOptions } from \"../types.js\"\nimport { readdir } from \"fs/promises\"\n\ninterface ParsersConfig {\n  parsers: FiletypeParserOptions[]\n}\n\ninterface GeneratedParser {\n  filetype: string\n  aliases?: string[]\n  languagePath: string\n  highlightsPath: string\n  injectionsPath?: string\n  injectionMapping?: any\n}\n\nexport interface UpdateOptions {\n  /** Path to parsers-config.json */\n  configPath: string\n  /** Directory where .wasm and .scm files will be downloaded */\n  assetsDir: string\n  /** Path where the generated TypeScript file will be written */\n  outputPath: string\n}\n\nfunction getDefaultOptions(): UpdateOptions {\n  return {\n    configPath: path.resolve(__dirname, \"../parsers-config\"),\n    assetsDir: path.resolve(__dirname),\n    outputPath: path.resolve(__dirname, \"../default-parsers.ts\"),\n  }\n}\n\nasync function loadConfig(configPath: string): Promise<ParsersConfig> {\n  let ext = path.extname(configPath)\n  let resolvedConfigPath = configPath\n\n  if (ext === \"\") {\n    const files = await readdir(path.dirname(configPath))\n    const file = files.find(\n      (file) =>\n        file.startsWith(path.basename(configPath)) &&\n        (file.endsWith(\".json\") || file.endsWith(\".ts\") || file.endsWith(\".js\")),\n    )\n    if (!file) {\n      throw new Error(`No config file found for ${configPath}`)\n    }\n    resolvedConfigPath = path.join(path.dirname(configPath), file)\n    ext = path.extname(resolvedConfigPath)\n  }\n\n  if (ext === \".json\") {\n    const configContent = await readFile(resolvedConfigPath, \"utf-8\")\n    return JSON.parse(configContent)\n  } else if (ext === \".ts\" || ext === \".js\") {\n    const { default: configContent } = await import(resolvedConfigPath)\n    return configContent\n  }\n  throw new Error(`Unsupported config file extension: ${ext}`)\n}\n\nasync function downloadLanguage(\n  filetype: string,\n  languageUrl: string,\n  assetsDir: string,\n  outputPath: string,\n): Promise<string> {\n  const languageDir = path.join(assetsDir, filetype)\n  const languageFilename = path.basename(languageUrl)\n  const languagePath = path.join(languageDir, languageFilename)\n\n  const result = await DownloadUtils.downloadToPath(languageUrl, languagePath)\n\n  if (result.error) {\n    throw new Error(`Failed to download language for ${filetype}: ${result.error}`)\n  }\n\n  return \"./\" + path.relative(path.dirname(outputPath), languagePath)\n}\n\nasync function downloadAndCombineQueries(\n  filetype: string,\n  queryUrls: string[],\n  assetsDir: string,\n  outputPath: string,\n  queryType: \"highlights\" | \"injections\",\n  configPath: string,\n): Promise<string> {\n  const queriesDir = path.join(assetsDir, filetype)\n  const queryPath = path.join(queriesDir, `${queryType}.scm`)\n\n  const queryContents: string[] = []\n\n  for (let i = 0; i < queryUrls.length; i++) {\n    const queryUrl = queryUrls[i]\n\n    if (queryUrl.startsWith(\"./\")) {\n      console.log(`    Using local query ${i + 1}/${queryUrls.length}: ${queryUrl}`)\n\n      try {\n        const localPath = path.resolve(path.dirname(configPath), queryUrl)\n        const content = await readFile(localPath, \"utf-8\")\n\n        if (content.trim()) {\n          queryContents.push(content)\n          console.log(`    ✓ Loaded ${content.split(\"\\n\").length} lines from local file`)\n        }\n      } catch (error) {\n        console.warn(`Failed to read local query from ${queryUrl}: ${error}`)\n        continue\n      }\n    } else {\n      console.log(`    Downloading query ${i + 1}/${queryUrls.length}: ${queryUrl}`)\n\n      try {\n        const response = await fetch(queryUrl)\n        if (!response.ok) {\n          console.warn(`Failed to download query from ${queryUrl}: ${response.statusText}`)\n          continue\n        }\n\n        const content = await response.text()\n        if (content.trim()) {\n          queryContents.push(`; Query from: ${queryUrl}\\n${content}`)\n          console.log(`    ✓ Downloaded ${content.split(\"\\n\").length} lines`)\n        }\n      } catch (error) {\n        console.warn(`Failed to download query from ${queryUrl}: ${error}`)\n        continue\n      }\n    }\n  }\n\n  const combinedContent = queryContents.join(\"\\n\\n\")\n  await writeFile(queryPath, combinedContent, \"utf-8\")\n\n  console.log(`  Combined ${queryContents.length} queries into ${queryPath}`)\n\n  return \"./\" + path.relative(path.dirname(outputPath), queryPath)\n}\n\nasync function generateDefaultParsersFile(parsers: GeneratedParser[], outputPath: string): Promise<void> {\n  const imports = parsers\n    .map((parser) => {\n      const safeFiletype = parser.filetype.replace(/[^a-zA-Z0-9]/g, \"_\")\n      const lines = [\n        `import ${safeFiletype}_highlights from \"${parser.highlightsPath}\" with { type: \"file\" }`,\n        `import ${safeFiletype}_language from \"${parser.languagePath}\" with { type: \"file\" }`,\n      ]\n      if (parser.injectionsPath) {\n        lines.push(`import ${safeFiletype}_injections from \"${parser.injectionsPath}\" with { type: \"file\" }`)\n      }\n      return lines.join(\"\\n\")\n    })\n    .join(\"\\n\")\n\n  const parserDefinitions = parsers\n    .map((parser) => {\n      const safeFiletype = parser.filetype.replace(/[^a-zA-Z0-9]/g, \"_\")\n      const queriesLines = [\n        `          highlights: [resolve(dirname(fileURLToPath(import.meta.url)), ${safeFiletype}_highlights)],`,\n      ]\n      if (parser.injectionsPath) {\n        queriesLines.push(\n          `          injections: [resolve(dirname(fileURLToPath(import.meta.url)), ${safeFiletype}_injections)],`,\n        )\n      }\n\n      const injectionMappingLine = parser.injectionMapping\n        ? `        injectionMapping: ${JSON.stringify(parser.injectionMapping, null, 10)},`\n        : \"\"\n      const aliasesLine = parser.aliases?.length ? `        aliases: ${JSON.stringify(parser.aliases)},` : \"\"\n\n      return `      {\n        filetype: \"${parser.filetype}\",\n${aliasesLine ? aliasesLine + \"\\n\" : \"\"}        queries: {\n${queriesLines.join(\"\\n\")}\n        },\n        wasm: resolve(dirname(fileURLToPath(import.meta.url)), ${safeFiletype}_language),${injectionMappingLine ? \"\\n\" + injectionMappingLine : \"\"}\n      }`\n    })\n    .join(\",\\n\")\n\n  const fileContent = `// This file is generated by assets/update.ts - DO NOT EDIT MANUALLY\n// Run 'bun assets/update.ts' to regenerate this file\n// Last generated: ${new Date().toISOString()}\n\nimport type { FiletypeParserOptions } from \"./types\"\nimport { resolve, dirname } from \"path\"\nimport { fileURLToPath } from \"url\"\n\n${imports}\n\n// Cached parsers to avoid re-resolving paths on every call\nlet _cachedParsers: FiletypeParserOptions[] | undefined\n\nexport function getParsers(): FiletypeParserOptions[] {\n  if (!_cachedParsers) {\n    _cachedParsers = [\n${parserDefinitions},\n    ]\n  }\n  return _cachedParsers\n}\n`\n\n  await mkdir(path.dirname(outputPath), { recursive: true })\n  await writeFile(outputPath, fileContent, \"utf-8\")\n  console.log(`Generated ${path.basename(outputPath)} with ${parsers.length} parsers`)\n}\n\nasync function main(options?: Partial<UpdateOptions>): Promise<void> {\n  const opts = { ...getDefaultOptions(), ...options }\n\n  try {\n    console.log(\"Loading parsers configuration...\")\n    console.log(`  Config: ${opts.configPath}`)\n    console.log(`  Assets Dir: ${opts.assetsDir}`)\n    console.log(`  Output: ${opts.outputPath}`)\n\n    const config = await loadConfig(opts.configPath)\n\n    console.log(`Found ${config.parsers.length} parsers to process`)\n\n    const generatedParsers: GeneratedParser[] = []\n\n    for (const parser of config.parsers) {\n      console.log(`Processing ${parser.filetype}...`)\n\n      console.log(`  Downloading language...`)\n      const languagePath = await downloadLanguage(parser.filetype, parser.wasm, opts.assetsDir, opts.outputPath)\n\n      console.log(`  Downloading ${parser.queries.highlights.length} highlight queries...`)\n      const highlightsPath = await downloadAndCombineQueries(\n        parser.filetype,\n        parser.queries.highlights,\n        opts.assetsDir,\n        opts.outputPath,\n        \"highlights\",\n        opts.configPath,\n      )\n\n      let injectionsPath: string | undefined\n      if (parser.queries.injections && parser.queries.injections.length > 0) {\n        console.log(`  Downloading ${parser.queries.injections.length} injection queries...`)\n        injectionsPath = await downloadAndCombineQueries(\n          parser.filetype,\n          parser.queries.injections,\n          opts.assetsDir,\n          opts.outputPath,\n          \"injections\",\n          opts.configPath,\n        )\n      }\n\n      generatedParsers.push({\n        filetype: parser.filetype,\n        aliases: parser.aliases,\n        languagePath,\n        highlightsPath,\n        injectionsPath,\n        injectionMapping: parser.injectionMapping,\n      })\n\n      console.log(`  ✓ Completed ${parser.filetype}`)\n    }\n\n    console.log(\"Generating output file...\")\n    await generateDefaultParsersFile(generatedParsers, opts.outputPath)\n\n    console.log(\"✅ Update completed successfully!\")\n  } catch (error) {\n    console.error(\"❌ Update failed:\", error)\n    process.exit(1)\n  }\n}\n\nfunction parseCLIArgs(): Partial<UpdateOptions> | null {\n  try {\n    const { values } = parseArgs({\n      args: Bun.argv.slice(2),\n      options: {\n        config: { type: \"string\" },\n        assets: { type: \"string\" },\n        output: { type: \"string\" },\n        help: { type: \"boolean\" },\n      },\n      strict: true,\n    })\n\n    if (values.help) {\n      console.log(`Usage: bun update.ts [options]\n\nOptions:\n  --config <path>  Path to parsers-config.json\n  --assets <path>  Directory where .wasm and .scm files will be downloaded\n  --output <path>  Path where the generated TypeScript file will be written\n  --help           Show this help message\n\nExamples:\n  # Use default paths (for OpenTUI core development)\n  bun update.ts\n\n  # Use custom paths (for application integration)\n  bun update.ts --config ./my-parsers.json --assets ./src/parsers --output ./src/parsers.ts\n`)\n      process.exit(0)\n    }\n\n    const options: Partial<UpdateOptions> = {}\n    if (values.config) options.configPath = path.resolve(values.config)\n    if (values.assets) options.assetsDir = path.resolve(values.assets)\n    if (values.output) options.outputPath = path.resolve(values.output)\n\n    return Object.keys(options).length > 0 ? options : null\n  } catch (error) {\n    console.error(`Error parsing arguments: ${error}`)\n    console.log(\"Run with --help for usage information\")\n    process.exit(1)\n  }\n}\n\nif (import.meta.main) {\n  const cliOptions = parseCLIArgs()\n  main(cliOptions || undefined)\n}\n\nexport { main as updateAssets }\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets/zig/highlights.scm",
    "content": "; Query from: https://github.com/nvim-treesitter/nvim-treesitter/raw/refs/heads/master/queries/zig/highlights.scm\n; Variables\n(identifier) @variable\n\n; Parameters\n(parameter\n  name: (identifier) @variable.parameter)\n\n(payload\n  (identifier) @variable.parameter)\n\n; Types\n(parameter\n  type: (identifier) @type)\n\n((identifier) @type\n  (#lua-match? @type \"^[A-Z_][a-zA-Z0-9_]*\"))\n\n(variable_declaration\n  (identifier) @type\n  \"=\"\n  [\n    (struct_declaration)\n    (enum_declaration)\n    (union_declaration)\n    (opaque_declaration)\n  ])\n\n[\n  (builtin_type)\n  \"anyframe\"\n] @type.builtin\n\n; Constants\n((identifier) @constant\n  (#lua-match? @constant \"^[A-Z][A-Z_0-9]+$\"))\n\n[\n  \"null\"\n  \"unreachable\"\n  \"undefined\"\n] @constant.builtin\n\n(field_expression\n  .\n  member: (identifier) @constant)\n\n(enum_declaration\n  (container_field\n    type: (identifier) @constant))\n\n; Labels\n(block_label\n  (identifier) @label)\n\n(break_label\n  (identifier) @label)\n\n; Fields\n(field_initializer\n  .\n  (identifier) @variable.member)\n\n(field_expression\n  (_)\n  member: (identifier) @variable.member)\n\n(container_field\n  name: (identifier) @variable.member)\n\n(initializer_list\n  (assignment_expression\n    left: (field_expression\n      .\n      member: (identifier) @variable.member)))\n\n; Functions\n(builtin_identifier) @function.builtin\n\n(call_expression\n  function: (identifier) @function.call)\n\n(call_expression\n  function: (field_expression\n    member: (identifier) @function.call))\n\n(function_declaration\n  name: (identifier) @function)\n\n; Modules\n(variable_declaration\n  (identifier) @module\n  (builtin_function\n    (builtin_identifier) @keyword.import\n    (#any-of? @keyword.import \"@import\" \"@cImport\")))\n\n; Builtins\n[\n  \"c\"\n  \"...\"\n] @variable.builtin\n\n((identifier) @variable.builtin\n  (#eq? @variable.builtin \"_\"))\n\n(calling_convention\n  (identifier) @variable.builtin)\n\n; Keywords\n[\n  \"asm\"\n  \"defer\"\n  \"errdefer\"\n  \"test\"\n  \"error\"\n  \"const\"\n  \"var\"\n] @keyword\n\n[\n  \"struct\"\n  \"union\"\n  \"enum\"\n  \"opaque\"\n] @keyword.type\n\n[\n  \"async\"\n  \"await\"\n  \"suspend\"\n  \"nosuspend\"\n  \"resume\"\n] @keyword.coroutine\n\n\"fn\" @keyword.function\n\n[\n  \"and\"\n  \"or\"\n  \"orelse\"\n] @keyword.operator\n\n\"return\" @keyword.return\n\n[\n  \"if\"\n  \"else\"\n  \"switch\"\n] @keyword.conditional\n\n[\n  \"for\"\n  \"while\"\n  \"break\"\n  \"continue\"\n] @keyword.repeat\n\n[\n  \"usingnamespace\"\n  \"export\"\n] @keyword.import\n\n[\n  \"try\"\n  \"catch\"\n] @keyword.exception\n\n[\n  \"volatile\"\n  \"allowzero\"\n  \"noalias\"\n  \"addrspace\"\n  \"align\"\n  \"callconv\"\n  \"linksection\"\n  \"pub\"\n  \"inline\"\n  \"noinline\"\n  \"extern\"\n  \"comptime\"\n  \"packed\"\n  \"threadlocal\"\n] @keyword.modifier\n\n; Operator\n[\n  \"=\"\n  \"*=\"\n  \"*%=\"\n  \"*|=\"\n  \"/=\"\n  \"%=\"\n  \"+=\"\n  \"+%=\"\n  \"+|=\"\n  \"-=\"\n  \"-%=\"\n  \"-|=\"\n  \"<<=\"\n  \"<<|=\"\n  \">>=\"\n  \"&=\"\n  \"^=\"\n  \"|=\"\n  \"!\"\n  \"~\"\n  \"-\"\n  \"-%\"\n  \"&\"\n  \"==\"\n  \"!=\"\n  \">\"\n  \">=\"\n  \"<=\"\n  \"<\"\n  \"&\"\n  \"^\"\n  \"|\"\n  \"<<\"\n  \">>\"\n  \"<<|\"\n  \"+\"\n  \"++\"\n  \"+%\"\n  \"-%\"\n  \"+|\"\n  \"-|\"\n  \"*\"\n  \"/\"\n  \"%\"\n  \"**\"\n  \"*%\"\n  \"*|\"\n  \"||\"\n  \".*\"\n  \".?\"\n  \"?\"\n  \"..\"\n] @operator\n\n; Literals\n(character) @character\n\n([\n  (string)\n  (multiline_string)\n] @string\n  (#set! \"priority\" 95))\n\n(integer) @number\n\n(float) @number.float\n\n(boolean) @boolean\n\n(escape_sequence) @string.escape\n\n; Punctuation\n[\n  \"[\"\n  \"]\"\n  \"(\"\n  \")\"\n  \"{\"\n  \"}\"\n] @punctuation.bracket\n\n[\n  \";\"\n  \".\"\n  \",\"\n  \":\"\n  \"=>\"\n  \"->\"\n] @punctuation.delimiter\n\n(payload\n  \"|\" @punctuation.bracket)\n\n; Comments\n(comment) @comment @spell\n\n((comment) @comment.documentation\n  (#lua-match? @comment.documentation \"^//!\"))\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/assets.d.ts",
    "content": "declare module \"*.scm\" {\n  const value: string\n  export default value\n}\n\ndeclare module \"*.wasm\" {\n  const value: string\n  export default value\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/cache.test.ts",
    "content": "import { test, expect, beforeEach, beforeAll, afterAll, describe } from \"bun:test\"\nimport { TreeSitterClient, addDefaultParsers } from \"./client.js\"\nimport { tmpdir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\nimport { mkdir, readdir, stat, writeFile } from \"node:fs/promises\"\nimport { readFileSync } from \"node:fs\"\nimport type { FiletypeParserOptions } from \"./types.js\"\n\ndescribe(\"TreeSitterClient Caching\", () => {\n  let dataPath: string\n  let testServer: any\n  const TEST_PORT = 55231\n  const BASE_URL = `http://localhost:${TEST_PORT}`\n\n  beforeAll(async () => {\n    const assetsDir = resolve(__dirname, \"assets\")\n    testServer = Bun.serve({\n      port: TEST_PORT,\n      fetch(req) {\n        const url = new URL(req.url)\n        const filePath = join(assetsDir, url.pathname)\n        return new Response(readFileSync(filePath))\n      },\n    })\n  })\n\n  afterAll(async () => {\n    if (testServer) {\n      testServer.stop()\n    }\n  })\n\n  beforeEach(async () => {\n    dataPath = join(tmpdir(), \"tree-sitter-cache-test-\" + Math.random().toString(36).slice(2))\n    await mkdir(dataPath, { recursive: true })\n  })\n\n  test(\"should create storage directories on initialization\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n    await client.initialize()\n\n    const languagesDir = join(dataPath, \"tree-sitter\", \"languages\")\n    const queriesDir = join(dataPath, \"tree-sitter\", \"queries\")\n\n    const languagesStat = await stat(languagesDir)\n    const queriesStat = await stat(queriesDir)\n\n    expect(languagesStat.isDirectory()).toBe(true)\n    expect(queriesStat.isDirectory()).toBe(true)\n\n    await client.destroy()\n  })\n\n  test(\"should cache downloaded language files\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n    await client.initialize()\n\n    // Add URL-based parser for this test\n    client.addFiletypeParser({\n      filetype: \"javascript\",\n      queries: {\n        highlights: [`${BASE_URL}/javascript/highlights.scm`],\n      },\n      wasm: `${BASE_URL}/javascript/tree-sitter-javascript.wasm`,\n    })\n\n    const hasParser = await client.preloadParser(\"javascript\")\n    expect(hasParser).toBe(true)\n\n    const languagesDir = join(dataPath, \"tree-sitter\", \"languages\")\n    const cachedFiles = await readdir(languagesDir)\n\n    expect(cachedFiles).toContain(\"tree-sitter-javascript.wasm\")\n\n    await client.destroy()\n  })\n\n  test(\"should cache downloaded highlight queries\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n    await client.initialize()\n\n    // Add URL-based parser for this test\n    client.addFiletypeParser({\n      filetype: \"javascript\",\n      queries: {\n        highlights: [`${BASE_URL}/javascript/highlights.scm`],\n      },\n      wasm: `${BASE_URL}/javascript/tree-sitter-javascript.wasm`,\n    })\n\n    const hasParser = await client.preloadParser(\"javascript\")\n    expect(hasParser).toBe(true)\n\n    const queriesDir = join(dataPath, \"tree-sitter\", \"queries\")\n    const cachedQueries = await readdir(queriesDir)\n\n    const scmFiles = cachedQueries.filter((file) => file.endsWith(\".scm\"))\n    expect(scmFiles.length).toBeGreaterThan(0)\n\n    await client.destroy()\n  })\n\n  // TODO: This is flaky, there must be a more reliable way to test this\n  test.skip(\"should reuse cached files across client instances\", async () => {\n    const jsParser: FiletypeParserOptions = {\n      filetype: \"javascript\",\n      queries: {\n        highlights: [`${BASE_URL}/javascript/highlights.scm`],\n      },\n      wasm: `${BASE_URL}/javascript/tree-sitter-javascript.wasm`,\n    }\n\n    let client1 = new TreeSitterClient({ dataPath })\n    await client1.initialize()\n    client1.addFiletypeParser(jsParser)\n\n    console.log(\"=== First client (should download) ===\")\n    const start1 = Date.now()\n    const hasParser1 = await client1.preloadParser(\"javascript\")\n    const duration1 = Date.now() - start1\n    expect(hasParser1).toBe(true)\n\n    await client1.destroy()\n\n    let client2 = new TreeSitterClient({ dataPath })\n    await client2.initialize()\n    client2.addFiletypeParser(jsParser)\n\n    console.log(\"=== Second client (should use cache) ===\")\n    const start2 = Date.now()\n    const hasParser2 = await client2.preloadParser(\"javascript\")\n    const duration2 = Date.now() - start2\n    expect(hasParser2).toBe(true)\n\n    console.log(`First client: ${duration1}ms, Second client: ${duration2}ms`)\n\n    expect(duration2).toBeLessThanOrEqual(duration1)\n    expect(duration2).toBeLessThan(100) // Should be very fast with cache\n\n    await client2.destroy()\n  })\n\n  test(\"should handle multiple parsers with independent caching\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n    await client.initialize()\n\n    // Add URL-based parsers for this test\n    client.addFiletypeParser({\n      filetype: \"javascript\",\n      queries: {\n        highlights: [`${BASE_URL}/javascript/highlights.scm`],\n      },\n      wasm: `${BASE_URL}/javascript/tree-sitter-javascript.wasm`,\n    })\n    client.addFiletypeParser({\n      filetype: \"typescript\",\n      queries: {\n        highlights: [`${BASE_URL}/typescript/highlights.scm`],\n      },\n      wasm: `${BASE_URL}/typescript/tree-sitter-typescript.wasm`,\n    })\n\n    const hasJS = await client.preloadParser(\"javascript\")\n    const hasTS = await client.preloadParser(\"typescript\")\n\n    expect(hasJS).toBe(true)\n    expect(hasTS).toBe(true)\n\n    const languagesDir = join(dataPath, \"tree-sitter\", \"languages\")\n    const cachedFiles = await readdir(languagesDir)\n\n    expect(cachedFiles).toContain(\"tree-sitter-javascript.wasm\")\n    expect(cachedFiles).toContain(\"tree-sitter-typescript.wasm\")\n\n    const queriesDir = join(dataPath, \"tree-sitter\", \"queries\")\n    const cachedQueries = await readdir(queriesDir)\n    const scmFiles = cachedQueries.filter((file) => file.endsWith(\".scm\"))\n\n    expect(scmFiles.length).toBe(2)\n\n    await client.destroy()\n  })\n\n  test(\"should store files in dataPath subdirectories\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n    await client.initialize()\n\n    // Add URL-based parser for this test\n    client.addFiletypeParser({\n      filetype: \"javascript\",\n      queries: {\n        highlights: [`${BASE_URL}/javascript/highlights.scm`],\n      },\n      wasm: `${BASE_URL}/javascript/tree-sitter-javascript.wasm`,\n    })\n\n    const hasParser = await client.preloadParser(\"javascript\")\n    expect(hasParser).toBe(true)\n\n    const languagesDir = join(dataPath, \"tree-sitter\", \"languages\")\n    const queriesDir = join(dataPath, \"tree-sitter\", \"queries\")\n\n    const languagesStat = await stat(languagesDir)\n    const queriesStat = await stat(queriesDir)\n\n    expect(languagesStat.isDirectory()).toBe(true)\n    expect(queriesStat.isDirectory()).toBe(true)\n\n    const cachedFiles = await readdir(languagesDir)\n    expect(cachedFiles).toContain(\"tree-sitter-javascript.wasm\")\n\n    await client.destroy()\n  })\n\n  test(\"should reject when tree-sitter cache directories cannot be created\", async () => {\n    const blockedDataPath = join(tmpdir(), \"tree-sitter-blocked-\" + Math.random().toString(36).slice(2))\n    await mkdir(blockedDataPath, { recursive: true })\n    await writeFile(join(blockedDataPath, \"tree-sitter\"), \"blocked\")\n\n    const client = new TreeSitterClient({ dataPath: blockedDataPath })\n\n    await expect(client.initialize()).rejects.toThrow()\n\n    await client.destroy()\n  })\n\n  test(\"should handle data path changes\", async () => {\n    const initialDataPath = join(tmpdir(), \"tree-sitter-initial-\" + Math.random().toString(36).slice(2))\n    const newDataPath = join(tmpdir(), \"tree-sitter-new-\" + Math.random().toString(36).slice(2))\n\n    await mkdir(initialDataPath, { recursive: true })\n    await mkdir(newDataPath, { recursive: true })\n\n    const client = new TreeSitterClient({ dataPath: initialDataPath })\n    await client.initialize()\n\n    // Add URL-based parsers for this test\n    client.addFiletypeParser({\n      filetype: \"javascript\",\n      queries: {\n        highlights: [`${BASE_URL}/javascript/highlights.scm`],\n      },\n      wasm: `${BASE_URL}/javascript/tree-sitter-javascript.wasm`,\n    })\n\n    const hasParser1 = await client.preloadParser(\"javascript\")\n    expect(hasParser1).toBe(true)\n\n    const initialLanguagesDir = join(initialDataPath, \"tree-sitter\", \"languages\")\n    const initialFiles = await readdir(initialLanguagesDir)\n    expect(initialFiles).toContain(\"tree-sitter-javascript.wasm\")\n\n    await client.setDataPath(newDataPath)\n\n    // Add typescript parser for the new data path\n    client.addFiletypeParser({\n      filetype: \"typescript\",\n      queries: {\n        highlights: [`${BASE_URL}/typescript/highlights.scm`],\n      },\n      wasm: `${BASE_URL}/typescript/tree-sitter-typescript.wasm`,\n    })\n\n    const hasParser2 = await client.preloadParser(\"typescript\")\n    expect(hasParser2).toBe(true)\n\n    const newLanguagesDir = join(newDataPath, \"tree-sitter\", \"languages\")\n    const newFiles = await readdir(newLanguagesDir)\n    expect(newFiles).toContain(\"tree-sitter-typescript.wasm\")\n\n    await client.destroy()\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/client.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, beforeAll, describe } from \"bun:test\"\nimport { TreeSitterClient } from \"./client.js\"\nimport { tmpdir } from \"os\"\nimport { join } from \"path\"\nimport { mkdir, writeFile, unlink } from \"fs/promises\"\nimport { getDataPaths } from \"../data-paths.js\"\nimport { getTreeSitterClient } from \"./index.js\"\n\ndescribe(\"TreeSitterClient\", () => {\n  let client: TreeSitterClient\n  let dataPath: string\n\n  const sharedDataPath = join(tmpdir(), \"tree-sitter-shared-test-data\")\n\n  beforeAll(async () => {\n    await mkdir(sharedDataPath, { recursive: true })\n  })\n\n  beforeEach(async () => {\n    dataPath = sharedDataPath\n    client = new TreeSitterClient({\n      dataPath,\n    })\n  })\n\n  afterEach(async () => {\n    if (client) {\n      await client.destroy()\n    }\n  })\n\n  test(\"should initialize successfully\", async () => {\n    await client.initialize()\n    expect(client.isInitialized()).toBe(true)\n  })\n\n  test(\"should preload parsers for supported filetypes\", async () => {\n    await client.initialize()\n\n    const hasJavaScript = await client.preloadParser(\"javascript\")\n    expect(hasJavaScript).toBe(true)\n\n    const hasJavaScriptReact = await client.preloadParser(\"javascriptreact\")\n    expect(hasJavaScriptReact).toBe(true)\n\n    const hasTypeScript = await client.preloadParser(\"typescript\")\n    expect(hasTypeScript).toBe(true)\n\n    const hasTypeScriptReact = await client.preloadParser(\"typescriptreact\")\n    expect(hasTypeScriptReact).toBe(true)\n  })\n\n  test(\"should return false for unsupported filetypes\", async () => {\n    await client.initialize()\n\n    const hasUnsupported = await client.preloadParser(\"unsupported-language\")\n    expect(hasUnsupported).toBe(false)\n  })\n\n  test(\"should create buffer with supported filetype\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";'\n    const hasParser = await client.createBuffer(1, jsCode, \"javascript\")\n\n    expect(hasParser).toBe(true)\n\n    const buffer = client.getBuffer(1)\n    expect(buffer).toBeDefined()\n    expect(buffer?.hasParser).toBe(true)\n    expect(buffer?.content).toBe(jsCode)\n    expect(buffer?.filetype).toBe(\"javascript\")\n  })\n\n  test(\"should create buffer without parser for unsupported filetype\", async () => {\n    await client.initialize()\n\n    const content = \"some random content\"\n    const hasParser = await client.createBuffer(1, content, \"unsupported\")\n\n    expect(hasParser).toBe(false)\n\n    const buffer = client.getBuffer(1)\n    expect(buffer).toBeDefined()\n    expect(buffer?.hasParser).toBe(false)\n  })\n\n  test(\"should emit highlights:response event when buffer is updated\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";'\n    await client.createBuffer(1, jsCode, \"javascript\")\n\n    let highlightReceived = false\n    let receivedBufferId: number | undefined\n    let receivedVersion: number | undefined\n\n    client.on(\"highlights:response\", (bufferId, version, highlights) => {\n      highlightReceived = true\n      receivedBufferId = bufferId\n      receivedVersion = version\n    })\n\n    await new Promise((resolve) => setTimeout(resolve, 100))\n\n    const newCode = 'const hello = \"world\";\\nconst foo = 42;'\n    const edits = [\n      {\n        startIndex: jsCode.length,\n        oldEndIndex: jsCode.length,\n        newEndIndex: newCode.length,\n        startPosition: { row: 0, column: jsCode.length },\n        oldEndPosition: { row: 0, column: jsCode.length },\n        newEndPosition: { row: 1, column: 14 },\n      },\n    ]\n\n    await client.updateBuffer(1, edits, newCode, 2)\n\n    await new Promise((resolve) => setTimeout(resolve, 200))\n\n    expect(highlightReceived).toBe(true)\n    expect(receivedBufferId).toBe(1)\n    expect(receivedVersion).toBe(2)\n  })\n\n  test(\"should handle buffer removal\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";'\n    await client.createBuffer(1, jsCode, \"javascript\")\n\n    let bufferDisposed = false\n    client.on(\"buffer:disposed\", (bufferId) => {\n      if (bufferId === 1) {\n        bufferDisposed = true\n      }\n    })\n\n    await client.removeBuffer(1)\n\n    expect(bufferDisposed).toBe(true)\n    expect(client.getBuffer(1)).toBeUndefined()\n  })\n\n  test(\"should handle multiple buffers\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";'\n    const tsCode = \"interface Test { value: string }\"\n\n    await client.createBuffer(1, jsCode, \"javascript\")\n    await client.createBuffer(2, tsCode, \"typescript\")\n\n    const buffers = client.getAllBuffers()\n    expect(buffers).toHaveLength(2)\n\n    const jsBuffer = client.getBuffer(1)\n    const tsBuffer = client.getBuffer(2)\n\n    expect(jsBuffer?.filetype).toBe(\"javascript\")\n    expect(tsBuffer?.filetype).toBe(\"typescript\")\n    expect(jsBuffer?.hasParser).toBe(true)\n    expect(tsBuffer?.hasParser).toBe(true)\n  })\n\n  test(\"should handle buffer reset\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";'\n    await client.createBuffer(1, jsCode, \"javascript\")\n\n    const newContent = \"function test() { return 42; }\"\n    await client.resetBuffer(1, 2, newContent)\n\n    const buffer = client.getBuffer(1)\n    expect(buffer?.content).toBe(newContent)\n    expect(buffer?.version).toBe(2)\n  })\n\n  test(\"should emit error events for invalid operations\", async () => {\n    await client.initialize()\n\n    let errorReceived = false\n    let errorMessage = \"\"\n\n    client.on(\"error\", (error, bufferId) => {\n      errorReceived = true\n      errorMessage = error\n    })\n\n    await client.resetBuffer(999, 1, \"test\")\n\n    expect(errorReceived).toBe(true)\n    expect(errorMessage).toContain(\"Cannot reset buffer with no parser\")\n  })\n\n  test(\"should prevent duplicate buffer creation\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";'\n    await client.createBuffer(1, jsCode, \"javascript\")\n\n    await expect(client.createBuffer(1, \"other code\", \"javascript\")).rejects.toThrow(\"Buffer with id 1 already exists\")\n  })\n\n  test(\"should handle performance metrics\", async () => {\n    await client.initialize()\n\n    const performance = await client.getPerformance()\n    expect(performance).toBeDefined()\n    expect(typeof performance.averageParseTime).toBe(\"number\")\n    expect(typeof performance.averageQueryTime).toBe(\"number\")\n    expect(Array.isArray(performance.parseTimes)).toBe(true)\n    expect(Array.isArray(performance.queryTimes)).toBe(true)\n  })\n\n  test(\"should handle concurrent buffer operations\", async () => {\n    await client.initialize()\n\n    const promises = []\n\n    for (let i = 0; i < 5; i++) {\n      const code = `const var${i} = ${i};`\n      promises.push(client.createBuffer(i, code, \"javascript\"))\n    }\n\n    const results = await Promise.all(promises)\n    expect(results.every((result) => result === true)).toBe(true)\n\n    const buffers = client.getAllBuffers()\n    expect(buffers).toHaveLength(5)\n  })\n\n  test(\"should clean up resources on destroy\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";'\n    await client.createBuffer(1, jsCode, \"javascript\")\n\n    expect(client.getAllBuffers()).toHaveLength(1)\n\n    await client.destroy()\n\n    expect(client.isInitialized()).toBe(false)\n    expect(client.getAllBuffers()).toHaveLength(0)\n  })\n\n  test(\"should perform one-shot highlighting\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";\\nfunction test() { return 42; }'\n    const result = await client.highlightOnce(jsCode, \"javascript\")\n\n    expect(result.highlights).toBeDefined()\n    expect(result.highlights!.length).toBeGreaterThan(0)\n\n    const firstHighlight = result.highlights![0]\n    expect(Array.isArray(firstHighlight)).toBe(true)\n    expect(firstHighlight).toHaveLength(3)\n    expect(typeof firstHighlight[0]).toBe(\"number\")\n    expect(typeof firstHighlight[1]).toBe(\"number\")\n    expect(typeof firstHighlight[2]).toBe(\"string\")\n\n    const groups = result.highlights!.map((hl) => hl[2])\n    expect(groups.length).toBeGreaterThan(0)\n    expect(groups).toContain(\"keyword\")\n  })\n\n  test(\"should handle one-shot highlighting for unsupported filetype\", async () => {\n    await client.initialize()\n\n    const result = await client.highlightOnce(\"some content\", \"unsupported-lang\")\n\n    expect(result.highlights).toBeUndefined()\n    expect(result.warning).toContain(\"No parser available for filetype unsupported-lang\")\n  }, 5000)\n\n  test(\"should perform multiple one-shot highlights independently\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const hello = \"world\";'\n    const tsCode = \"interface Test { value: string }\"\n\n    const [jsResult, tsResult] = await Promise.all([\n      client.highlightOnce(jsCode, \"javascript\"),\n      client.highlightOnce(tsCode, \"typescript\"),\n    ])\n\n    expect(jsResult.highlights).toBeDefined()\n    expect(tsResult.highlights).toBeDefined()\n    expect(jsResult.highlights!.length).toBeGreaterThan(0)\n    expect(tsResult.highlights!.length).toBeGreaterThan(0)\n\n    jsResult.highlights!.forEach((hl) => {\n      expect(Array.isArray(hl)).toBe(true)\n      expect(hl).toHaveLength(3)\n    })\n\n    tsResult.highlights!.forEach((hl) => {\n      expect(Array.isArray(hl)).toBe(true)\n      expect(hl).toHaveLength(3)\n    })\n\n    expect(client.getAllBuffers()).toHaveLength(0)\n  })\n\n  test(\"should perform one-shot highlighting for react parser aliases\", async () => {\n    await client.initialize()\n\n    const jsxCode = 'const view = <div className=\"card\">hello</div>'\n    const tsxCode = 'const view: JSX.Element = <div className=\"card\">hello</div>'\n\n    const [jsxResult, tsxResult] = await Promise.all([\n      client.highlightOnce(jsxCode, \"javascriptreact\"),\n      client.highlightOnce(tsxCode, \"typescriptreact\"),\n    ])\n\n    expect(jsxResult.highlights).toBeDefined()\n    expect(tsxResult.highlights).toBeDefined()\n    expect(jsxResult.highlights!.length).toBeGreaterThan(0)\n    expect(tsxResult.highlights!.length).toBeGreaterThan(0)\n\n    const jsxGroups = jsxResult.highlights!.map((hl) => hl[2])\n    const tsxGroups = tsxResult.highlights!.map((hl) => hl[2])\n\n    expect(jsxGroups).toContain(\"keyword\")\n    expect(tsxGroups).toContain(\"keyword\")\n  })\n\n  test(\"should handle Devanagari characters and highlight ranges after them correctly\", async () => {\n    await client.initialize()\n\n    const jsCode = 'const greeting = \"नमस्ते\";\\nconst x = 42;'\n    const result = await client.highlightOnce(jsCode, \"javascript\")\n\n    expect(result.highlights).toBeDefined()\n    expect(result.highlights!.length).toBeGreaterThan(0)\n\n    const keywordHighlights = result.highlights!.filter((hl) => hl[2] === \"keyword\")\n    expect(keywordHighlights.length).toBeGreaterThanOrEqual(2)\n\n    const constHighlights = keywordHighlights.filter((hl) => {\n      const text = jsCode.substring(hl[0], hl[1])\n      return text === \"const\"\n    })\n\n    expect(constHighlights).toHaveLength(2)\n\n    const firstConst = constHighlights[0]\n    const secondConst = constHighlights[1]\n\n    expect(jsCode.substring(firstConst[0], firstConst[1])).toBe(\"const\")\n    expect(jsCode.substring(secondConst[0], secondConst[1])).toBe(\"const\")\n\n    expect(firstConst[0]).toBe(0)\n    expect(firstConst[1]).toBe(5)\n\n    expect(secondConst[0]).toBeGreaterThan(firstConst[1])\n    const textBetween = jsCode.substring(firstConst[1], secondConst[0])\n    expect(textBetween).toContain(\"नमस्ते\")\n\n    const numberHighlight = result.highlights!.find((hl) => {\n      const text = jsCode.substring(hl[0], hl[1])\n      return text === \"42\" && hl[2] === \"number\"\n    })\n\n    expect(numberHighlight).toBeDefined()\n    if (numberHighlight) {\n      const [start, end] = numberHighlight\n      const actualText = jsCode.substring(start, end)\n      expect(actualText).toBe(\"42\")\n\n      const secondLine = jsCode.split(\"\\n\")[1]\n      const secondLineStart = jsCode.indexOf(secondLine)\n      const expectedStart = secondLineStart + secondLine.indexOf(\"42\")\n      expect(start).toBe(expectedStart)\n    }\n  })\n\n  test(\"should support local file paths for parser configuration\", async () => {\n    const testQueryPath = join(dataPath, `test-highlights-${Date.now()}.scm`)\n    const simpleQuery = \"(identifier) @variable\"\n    await writeFile(testQueryPath, simpleQuery, \"utf8\")\n\n    try {\n      client.addFiletypeParser({\n        filetype: \"test-lang\",\n        aliases: [\"test-lang-react\"],\n        queries: {\n          highlights: [testQueryPath],\n        },\n        wasm: \"https://github.com/tree-sitter/tree-sitter-javascript/releases/download/v0.23.1/tree-sitter-javascript.wasm\",\n      })\n\n      await client.initialize()\n\n      const hasParser = await client.preloadParser(\"test-lang\")\n      expect(hasParser).toBe(true)\n\n      const hasAliasParser = await client.preloadParser(\"test-lang-react\")\n      expect(hasAliasParser).toBe(true)\n\n      const testCode = \"const myVariable = 42;\"\n      const result = await client.highlightOnce(testCode, \"test-lang\")\n      const aliasResult = await client.highlightOnce(testCode, \"test-lang-react\")\n\n      expect(result.highlights).toBeDefined()\n      expect(aliasResult.highlights).toBeDefined()\n      expect(result.error).toBeUndefined()\n      expect(aliasResult.error).toBeUndefined()\n      expect(result.warning).toBeUndefined()\n      expect(aliasResult.warning).toBeUndefined()\n    } finally {\n      try {\n        await unlink(testQueryPath)\n      } catch (e) {\n        // Ignore cleanup errors\n      }\n    }\n  })\n\n  test(\"should handle concurrent highlightOnce calls efficiently (no duplicate parser loading)\", async () => {\n    const freshClient = new TreeSitterClient({ dataPath })\n    const workerLogs: string[] = []\n\n    freshClient.on(\"worker:log\", (logType, message) => {\n      if (message.includes(\"Loading from local path:\")) {\n        workerLogs.push(message)\n      }\n    })\n\n    try {\n      await freshClient.initialize()\n\n      const jsCode = 'const hello = \"world\"; function test() { return 42; }'\n      const promises = Array.from({ length: 5 }, () => freshClient.highlightOnce(jsCode, \"javascript\"))\n\n      const results = await Promise.all(promises)\n\n      for (const result of results) {\n        expect(result.highlights).toBeDefined()\n        expect(result.highlights!.length).toBeGreaterThan(0)\n        expect(result.error).toBeUndefined()\n      }\n\n      const firstResult = results[0]\n      for (let i = 1; i < results.length; i++) {\n        expect(results[i].highlights).toEqual(firstResult.highlights)\n      }\n\n      await new Promise((resolve) => setTimeout(resolve, 100))\n\n      const languageLoadLogs = workerLogs.filter((log) => log.includes(\"tree-sitter-javascript.wasm\"))\n      const queryLoadLogs = workerLogs.filter((log) => log.includes(\"highlights.scm\"))\n\n      expect(languageLoadLogs.length).toBeLessThanOrEqual(1)\n      expect(queryLoadLogs.length).toBeLessThanOrEqual(1)\n    } finally {\n      await freshClient.destroy()\n    }\n  })\n\n  test(\"should reuse canonical parser assets for aliased filetypes\", async () => {\n    const freshClient = new TreeSitterClient({ dataPath })\n    const workerLogs: string[] = []\n\n    freshClient.on(\"worker:log\", (_logType, message) => {\n      if (message.includes(\"Loading from local path:\")) {\n        workerLogs.push(message)\n      }\n    })\n\n    try {\n      await freshClient.initialize()\n\n      const jsxCode = 'const view = <div className=\"card\">hello</div>'\n      const [canonicalResult, aliasResult] = await Promise.all([\n        freshClient.highlightOnce(jsxCode, \"javascript\"),\n        freshClient.highlightOnce(jsxCode, \"javascriptreact\"),\n      ])\n\n      expect(canonicalResult.highlights).toBeDefined()\n      expect(aliasResult.highlights).toBeDefined()\n      expect(canonicalResult.error).toBeUndefined()\n      expect(aliasResult.error).toBeUndefined()\n\n      await new Promise((resolve) => setTimeout(resolve, 100))\n\n      const languageLoadLogs = workerLogs.filter((log) => log.includes(\"tree-sitter-javascript.wasm\"))\n      const queryLoadLogs = workerLogs.filter((log) => log.includes(\"/assets/javascript/highlights.scm\"))\n\n      expect(languageLoadLogs.length).toBeLessThanOrEqual(1)\n      expect(queryLoadLogs.length).toBeLessThanOrEqual(1)\n      expect(workerLogs.some((log) => log.includes(\"javascriptreact\"))).toBe(false)\n    } finally {\n      await freshClient.destroy()\n    }\n  })\n})\n\ndescribe(\"TreeSitterClient Injections\", () => {\n  let dataPath: string\n\n  const injectionsDataPath = join(tmpdir(), \"tree-sitter-injections-test-data\")\n\n  beforeAll(async () => {\n    await mkdir(injectionsDataPath, { recursive: true })\n  })\n\n  beforeEach(async () => {\n    dataPath = injectionsDataPath\n  })\n\n  test(\"should highlight inline code in markdown using markdown_inline injection\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `# Hello World\n\nThe \\`CodeRenderable\\` component provides syntax highlighting.\n\nYou can use \\`const x = 42\\` in your code.`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.highlights!.length).toBeGreaterThan(0)\n\n      const groups = result.highlights!.map((hl) => hl[2])\n      const hasInlineCodeHighlights = groups.some((g) => g.includes(\"markup.raw\"))\n\n      expect(hasInlineCodeHighlights).toBe(true)\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should highlight code blocks in markdown using language-specific injection\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `# Code Example\n\n\\`\\`\\`typescript\nconst hello: string = \"world\";\nfunction test() { return 42; }\n\\`\\`\\`\n\nSome text here.`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.highlights!.length).toBeGreaterThan(0)\n\n      const groups = result.highlights!.map((hl) => hl[2])\n      const hasTypeScriptHighlights = groups.some((g) => g === \"keyword\" || g === \"type\" || g === \"function\")\n\n      expect(hasTypeScriptHighlights).toBe(true)\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should highlight tsx code blocks in markdown using language-specific injection\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `# Code Example\n\n\\`\\`\\`tsx\nconst view: JSX.Element = <div>Hello</div>;\n\\`\\`\\`\n\nSome text here.`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.highlights!.length).toBeGreaterThan(0)\n\n      const constHighlight = result.highlights!.find((hl) => {\n        const text = markdownCode.substring(hl[0], hl[1])\n        return text === \"const\" && hl[2] === \"keyword\"\n      })\n\n      expect(constHighlight).toBeDefined()\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should return correct offsets for injected code in markdown code blocks\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `# Title\\n\\n\\`\\`\\`typescript\\nconst x = 42;\\n\\`\\`\\``\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.highlights!.length).toBeGreaterThan(0)\n\n      const constHighlight = result.highlights!.find((hl) => {\n        const text = markdownCode.substring(hl[0], hl[1])\n        return text === \"const\" && hl[2] === \"keyword\"\n      })\n\n      expect(constHighlight).toBeDefined()\n      if (constHighlight) {\n        const [start, end, group] = constHighlight\n        const text = markdownCode.substring(start, end)\n\n        expect(text).toBe(\"const\")\n        expect(group).toBe(\"keyword\")\n        expect(start).toBe(23)\n        expect(end).toBe(28)\n      }\n\n      const numberHighlight = result.highlights!.find((hl) => {\n        const text = markdownCode.substring(hl[0], hl[1])\n        return text === \"42\" && hl[2] === \"number\"\n      })\n\n      expect(numberHighlight).toBeDefined()\n      if (numberHighlight) {\n        const [start, end, group] = numberHighlight\n        const text = markdownCode.substring(start, end)\n\n        expect(text).toBe(\"42\")\n        expect(group).toBe(\"number\")\n        expect(start).toBe(33)\n        expect(end).toBe(35)\n      }\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should return highlights sorted by start offset for injected code\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `# Documentation\n\nSome text with \\`inline code\\` here.\n\n\\`\\`\\`typescript\nconst first = 1;\nconst second = 2;\n\\`\\`\\`\n\nMore text with \\`another inline\\` code.\n\n\\`\\`\\`javascript\nfunction test() {\n  return 42;\n}\n\\`\\`\\``\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.highlights!.length).toBeGreaterThan(0)\n\n      for (let i = 1; i < result.highlights!.length; i++) {\n        const prevStart = result.highlights![i - 1][0]\n        const currStart = result.highlights![i][0]\n        expect(currStart).toBeGreaterThanOrEqual(prevStart)\n      }\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should handle markdown with injections and return valid highlights\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `# Heading\n\nSome **bold** text with \\`inline code\\`.\n\n\\`\\`\\`typescript\nconst x: string = \"hello\";\n\\`\\`\\`\n\n[Link text](https://example.com)`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.highlights!.length).toBeGreaterThan(0)\n\n      const overlaps: Array<[number, number]> = []\n      for (let i = 0; i < result.highlights!.length; i++) {\n        for (let j = i + 1; j < result.highlights!.length; j++) {\n          const [start1, end1] = result.highlights![i]\n          const [start2, end2] = result.highlights![j]\n\n          if (start2 < end1) {\n            overlaps.push([i, j])\n          }\n        }\n      }\n\n      expect(overlaps.length).toBeGreaterThanOrEqual(0)\n\n      const injectionHighlights = result.highlights!.filter((hl) => hl[2].includes(\"injection\"))\n      expect(injectionHighlights).toBeDefined()\n\n      const concealHighlights = result.highlights!.filter((hl) => hl[2] === \"conceal\")\n      expect(concealHighlights).toBeDefined()\n\n      const blockHighlights = result.highlights!.filter((hl) => hl[2] === \"markup.raw.block\")\n      expect(blockHighlights).toBeDefined()\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should handle fast concurrent markdown highlighting requests with injections\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    const errors: string[] = []\n    client.on(\"error\", (error) => {\n      errors.push(error)\n    })\n\n    client.on(\"worker:log\", (logType, message) => {\n      if (logType === \"error\") {\n        errors.push(message)\n      }\n    })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `# OpenTUI Documentation\n\n## Getting Started\n\nOpenTUI is a modern terminal UI framework built on **tree-sitter** and WebGPU.\n\n### Installation\n\n\\`\\`\\`bash\nbun install opentui\n\\`\\`\\`\n\n### Quick Example\n\n\\`\\`\\`typescript\nimport { createCliRenderer, BoxRenderable } from 'opentui';\n\nconst renderer = await createCliRenderer();\nconst box = new BoxRenderable(renderer, {\n  border: true,\n  title: \"Hello World\"\n});\nrenderer.root.add(box);\n\\`\\`\\`\n\nThe \\`CodeRenderable\\` component provides syntax highlighting.\n\n| Property | Type | Description |\n|----------|------|-------------|\n| content | string | Code to display |\n| filetype | string | Language type |`\n\n      const jsCode = `function test() {\n  const hello = \"world\";\n  return hello;\n}`\n\n      const tsCode = `interface User {\n  name: string;\n  age: number;\n}\n\nconst user: User = { name: \"Alice\", age: 25 };`\n\n      const promises = []\n      for (let i = 0; i < 5; i++) {\n        promises.push(client.highlightOnce(markdownCode, \"markdown\"))\n      }\n\n      const results = await Promise.allSettled(promises)\n\n      for (let i = 0; i < results.length; i++) {\n        const result = results[i]\n        if (result.status === \"fulfilled\") {\n          expect(result.value.error).toBeUndefined()\n          expect(result.value.highlights).toBeDefined()\n        } else {\n          throw new Error(`Request ${i} was rejected: ${result.reason}`)\n        }\n      }\n\n      await new Promise((resolve) => setTimeout(resolve, 500))\n\n      const hasMemoryErrors = errors.some((err) => err.includes(\"Out of bounds memory access\"))\n      expect(hasMemoryErrors).toBe(false)\n    } finally {\n      await client.destroy()\n    }\n  }, 15000)\n})\n\ndescribe(\"TreeSitterClient Conceal Values\", () => {\n  let dataPath: string\n\n  const concealDataPath = join(tmpdir(), \"tree-sitter-conceal-test-data\")\n\n  beforeAll(async () => {\n    await mkdir(concealDataPath, { recursive: true })\n  })\n\n  beforeEach(async () => {\n    dataPath = concealDataPath\n  })\n\n  test(\"should return conceal values from normal (non-injected) queries\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `![Image Alt Text](https://example.com/image.png)`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.error).toBeUndefined()\n\n      const concealedHighlights = result.highlights!.filter((hl) => {\n        const meta = (hl as any)[3]\n        return meta && meta.conceal !== undefined\n      })\n\n      expect(concealedHighlights.length).toBeGreaterThan(0)\n\n      concealedHighlights.forEach((hl) => {\n        const meta = (hl as any)[3]\n        expect(meta.conceal).toBeDefined()\n      })\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should return conceal values from injected queries (markdown_inline)\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `Here is a [link](https://example.com) in text.`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.error).toBeUndefined()\n\n      const concealedHighlights = result.highlights!.filter((hl) => {\n        const meta = (hl as any)[3]\n        return meta && meta.conceal !== undefined\n      })\n\n      expect(concealedHighlights.length).toBeGreaterThan(0)\n\n      concealedHighlights.forEach((hl) => {\n        const meta = (hl as any)[3]\n        expect(meta.conceal).toBeDefined()\n        expect(meta.isInjection).toBeDefined()\n      })\n\n      const closingBracketHighlight = concealedHighlights.find((hl) => {\n        const text = markdownCode.substring(hl[0], hl[1])\n        const meta = (hl as any)[3]\n        return text === \"]\" && meta.conceal !== \"\"\n      })\n\n      if (closingBracketHighlight) {\n        const meta = (closingBracketHighlight as any)[3]\n        expect(meta.conceal).toBeDefined()\n      }\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should distinguish conceal values between normal and injected queries\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `Here is a [link](https://example.com) and ![image](https://example.com/img.png).`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.error).toBeUndefined()\n\n      const concealedHighlights = result.highlights!.filter((hl) => {\n        const meta = (hl as any)[3]\n        return meta && meta.conceal !== undefined\n      })\n\n      expect(concealedHighlights.length).toBeGreaterThan(0)\n\n      const normalConceal = concealedHighlights.filter((hl) => {\n        const meta = (hl as any)[3]\n        return !meta.isInjection\n      })\n\n      const injectedConceal = concealedHighlights.filter((hl) => {\n        const meta = (hl as any)[3]\n        return meta.isInjection\n      })\n\n      expect(injectedConceal.length).toBeGreaterThan(0)\n\n      injectedConceal.forEach((hl) => {\n        const meta = (hl as any)[3]\n        expect(meta.conceal).toBeDefined()\n        expect(meta.isInjection).toBe(true)\n      })\n\n      concealedHighlights.forEach((hl) => {\n        const meta = (hl as any)[3]\n        expect(meta.conceal).toBeDefined()\n        expect(typeof meta.isInjection).toBe(\"boolean\")\n      })\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should handle pattern index lookups correctly for injections\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `A [link](url) here.`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.error).toBeUndefined()\n\n      const concealedHighlights = result.highlights!.filter((hl) => {\n        const meta = (hl as any)[3]\n        return meta && meta.conceal !== undefined\n      })\n\n      expect(concealedHighlights.length).toBeGreaterThan(0)\n\n      concealedHighlights.forEach((hl) => {\n        const meta = (hl as any)[3]\n        expect(meta.conceal).toBeDefined()\n      })\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should handle multiple injected languages with different conceal patterns\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `# Title\n\nInline \\`code\\` and a [link](url) here.\n\n\\`\\`\\`typescript\nconst x = 42;\n\\`\\`\\`\n\nMore text with ![image](img.png) and **bold**.`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.error).toBeUndefined()\n\n      const concealedHighlights = result.highlights!.filter((hl) => {\n        const meta = (hl as any)[3]\n        return meta && meta.conceal !== undefined\n      })\n\n      expect(concealedHighlights.length).toBeGreaterThan(0)\n\n      const byLang = new Map<string, any[]>()\n      concealedHighlights.forEach((hl) => {\n        const meta = (hl as any)[3]\n        const lang = meta.isInjection ? meta.injectionLang || \"injected\" : \"normal\"\n        if (!byLang.has(lang)) {\n          byLang.set(lang, [])\n        }\n        byLang.get(lang)!.push(hl)\n      })\n\n      expect(byLang.size).toBeGreaterThan(0)\n\n      byLang.forEach((highlights) => {\n        expect(highlights.length).toBeGreaterThan(0)\n        highlights.forEach((hl: any) => {\n          const meta = hl[3]\n          expect(meta.conceal).toBeDefined()\n        })\n      })\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n\n  test(\"should preserve non-empty conceal replacements like space character\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    try {\n      await client.initialize()\n\n      const markdownCode = `Check [this link](https://example.com) out!`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n      expect(result.highlights).toBeDefined()\n      expect(result.error).toBeUndefined()\n\n      const closingBracket = result.highlights!.find((hl) => {\n        const text = markdownCode.substring(hl[0], hl[1])\n        const meta = (hl as any)[3]\n        return text === \"]\" && hl[2] === \"conceal\" && meta?.conceal !== undefined\n      })\n\n      if (closingBracket) {\n        const meta = (closingBracket as any)[3]\n        expect(meta).toBeDefined()\n        expect(meta.conceal).toBeDefined()\n        expect(meta.conceal).toBe(\" \")\n        expect(meta.conceal.length).toBeGreaterThan(0)\n      }\n    } finally {\n      await client.destroy()\n    }\n  }, 10000)\n})\n\ndescribe(\"TreeSitterClient Edge Cases\", () => {\n  let dataPath: string\n\n  const edgeCaseDataPath = join(tmpdir(), \"tree-sitter-edge-case-test-data\")\n\n  beforeAll(async () => {\n    await mkdir(edgeCaseDataPath, { recursive: true })\n  })\n\n  beforeEach(async () => {\n    dataPath = edgeCaseDataPath\n  })\n\n  test(\"should handle initialization timeout\", async () => {\n    const client = new TreeSitterClient({\n      dataPath,\n      workerPath: \"invalid-path\",\n      initTimeout: 500,\n    })\n\n    await expect(client.initialize()).rejects.toThrow(/Worker error|Worker initialization timed out/)\n\n    await client.destroy()\n  })\n\n  test(\"should handle operations before initialization\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    expect(client.isInitialized()).toBe(false)\n    expect(client.getAllBuffers()).toHaveLength(0)\n    expect(client.getBuffer(1)).toBeUndefined()\n\n    await client.destroy()\n  })\n\n  test(\"should handle destroy() during pending initialization\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    // Start init but don't await\n    const initPromise = client.initialize()\n\n    // Immediately destroy\n    await client.destroy()\n\n    // Init promise should reject with specific error\n    await expect(initPromise).rejects.toThrow(\"Client destroyed during initialization\")\n\n    expect(client.isInitialized()).toBe(false)\n  })\n\n  test(\"should handle worker errors gracefully\", async () => {\n    const client = new TreeSitterClient({ dataPath })\n\n    let errorReceived = false\n    client.on(\"error\", () => {\n      errorReceived = true\n    })\n\n    const hasParser = await client.createBuffer(1, \"test\", \"javascript\", 1, false)\n    expect(hasParser).toBe(false)\n    expect(errorReceived).toBe(true)\n\n    await client.destroy()\n  })\n\n  test(\"should handle data path changes with reactive getTreeSitterClient\", async () => {\n    const dataPathsManager = getDataPaths()\n    const originalAppName = dataPathsManager.appName\n    let client: any\n\n    try {\n      client = getTreeSitterClient()\n      await client.initialize()\n\n      const initialDataPath = dataPathsManager.globalDataPath\n\n      dataPathsManager.appName = \"test-app-changed\"\n\n      await new Promise((resolve) => setTimeout(resolve, 100))\n\n      const newDataPath = dataPathsManager.globalDataPath\n      expect(newDataPath).not.toBe(initialDataPath)\n      expect(newDataPath).toContain(\"test-app-changed\")\n\n      if (!client.isInitialized()) {\n        await client.initialize()\n      }\n\n      const hasParser = await client.preloadParser(\"javascript\")\n      expect(hasParser).toBe(true)\n    } finally {\n      if (client) {\n        await client.destroy()\n      }\n\n      dataPathsManager.appName = originalAppName\n    }\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/client.ts",
    "content": "import { EventEmitter } from \"events\"\nimport { createDebounce, clearDebounceScope, DebounceController } from \"../debounce.js\"\nimport { ProcessQueue } from \"../queue.js\"\nimport type {\n  TreeSitterClientOptions,\n  TreeSitterClientEvents,\n  BufferState,\n  ParsedBuffer,\n  FiletypeParserOptions,\n  Edit,\n  PerformanceStats,\n  SimpleHighlight,\n} from \"./types.js\"\nimport { getParsers } from \"./default-parsers.js\"\nimport { resolve, isAbsolute, parse } from \"path\"\nimport { existsSync } from \"fs\"\nimport { registerEnvVar, env } from \"../env.js\"\nimport { isBunfsPath, normalizeBunfsPath } from \"../bunfs.js\"\n\nregisterEnvVar({\n  name: \"OTUI_TREE_SITTER_WORKER_PATH\",\n  description: \"Path to the TreeSitter worker\",\n  type: \"string\",\n  default: \"\",\n})\n\ndeclare global {\n  const OTUI_TREE_SITTER_WORKER_PATH: string\n}\n\ninterface EditQueueItem {\n  edits: Edit[]\n  newContent: string\n  version: number\n  isReset?: boolean\n}\n\nlet DEFAULT_PARSERS: FiletypeParserOptions[] = getParsers()\n\nexport function addDefaultParsers(parsers: FiletypeParserOptions[]): void {\n  for (const parser of parsers) {\n    DEFAULT_PARSERS = [\n      ...DEFAULT_PARSERS.filter((existingParser) => existingParser.filetype !== parser.filetype),\n      parser,\n    ]\n  }\n}\n\nconst isUrl = (path: string) => path.startsWith(\"http://\") || path.startsWith(\"https://\")\n\n// Parser options now support both URLs and local file paths\n// TODO: TreeSitterClient should have a setOptions method, passing it on to the worker etc.\nexport class TreeSitterClient extends EventEmitter<TreeSitterClientEvents> {\n  private initialized = false\n  private worker: Worker | undefined\n  private buffers: Map<number, BufferState> = new Map()\n  private initializePromise: Promise<void> | undefined\n  private initializeResolvers:\n    | { resolve: () => void; reject: (error: Error) => void; timeoutId: ReturnType<typeof setTimeout> }\n    | undefined\n  private messageCallbacks: Map<string, (response: any) => void> = new Map()\n  private messageIdCounter: number = 0\n  private editQueues: Map<number, ProcessQueue<EditQueueItem>> = new Map()\n  private debouncer: DebounceController\n  private options: TreeSitterClientOptions\n\n  constructor(options: TreeSitterClientOptions) {\n    super()\n    this.options = options\n    this.debouncer = createDebounce(\"tree-sitter-client\")\n    this.startWorker()\n  }\n\n  private emitError(error: string, bufferId?: number): void {\n    if (this.listenerCount(\"error\") > 0) {\n      this.emit(\"error\", error, bufferId)\n    }\n  }\n\n  private emitWarning(warning: string, bufferId?: number): void {\n    if (this.listenerCount(\"warning\") > 0) {\n      this.emit(\"warning\", warning, bufferId)\n    }\n  }\n\n  private startWorker() {\n    if (this.worker) {\n      return\n    }\n\n    let worker_path: string | URL\n\n    if (env.OTUI_TREE_SITTER_WORKER_PATH) {\n      worker_path = env.OTUI_TREE_SITTER_WORKER_PATH\n    } else if (typeof OTUI_TREE_SITTER_WORKER_PATH !== \"undefined\") {\n      worker_path = OTUI_TREE_SITTER_WORKER_PATH\n    } else if (this.options.workerPath) {\n      worker_path = this.options.workerPath\n    } else {\n      worker_path = new URL(\"./parser.worker.js\", import.meta.url).href\n      if (!existsSync(resolve(import.meta.dirname, \"parser.worker.js\"))) {\n        worker_path = new URL(\"./parser.worker.ts\", import.meta.url).href\n      }\n    }\n\n    this.worker = new Worker(worker_path)\n\n    // @ts-ignore - onmessage exists\n    this.worker.onmessage = this.handleWorkerMessage.bind(this)\n\n    // @ts-ignore - onerror exists\n    this.worker.onerror = (error: ErrorEvent) => {\n      console.error(\"TreeSitter worker error:\", error.message)\n\n      // If we're still initializing, reject the init promise\n      if (this.initializeResolvers) {\n        clearTimeout(this.initializeResolvers.timeoutId)\n        this.initializeResolvers.reject(new Error(`Worker error: ${error.message}`))\n        this.initializeResolvers = undefined\n      }\n\n      this.emitError(`Worker error: ${error.message}`)\n    }\n  }\n\n  private stopWorker() {\n    if (!this.worker) {\n      return\n    }\n\n    this.worker.terminate()\n    this.worker = undefined\n  }\n\n  // NOTE: Unused, but useful for debugging and testing\n  private handleReset() {\n    this.buffers.clear()\n    this.stopWorker()\n    this.startWorker()\n    this.initializePromise = undefined\n    this.initializeResolvers = undefined\n    return this.initialize()\n  }\n\n  async initialize(): Promise<void> {\n    if (this.initializePromise) {\n      return this.initializePromise\n    }\n\n    this.initializePromise = new Promise((resolve, reject) => {\n      const timeoutMs = this.options.initTimeout ?? 10000 // Default to 10 seconds\n      const timeoutId = setTimeout(() => {\n        const error = new Error(\"Worker initialization timed out\")\n        console.error(\"TreeSitter client:\", error.message)\n        this.initializeResolvers = undefined\n        reject(error)\n      }, timeoutMs)\n\n      this.initializeResolvers = { resolve, reject, timeoutId }\n      this.worker?.postMessage({\n        type: \"INIT\",\n        dataPath: this.options.dataPath,\n      })\n    })\n\n    await this.initializePromise\n    await this.registerDefaultParsers()\n\n    return this.initializePromise\n  }\n\n  private async registerDefaultParsers(): Promise<void> {\n    for (const parser of DEFAULT_PARSERS) {\n      this.addFiletypeParser(parser)\n    }\n  }\n\n  private resolvePath(path: string): string {\n    if (isUrl(path)) {\n      return path\n    }\n    if (isBunfsPath(path)) {\n      return normalizeBunfsPath(parse(path).base)\n    }\n    if (!isAbsolute(path)) {\n      return resolve(path)\n    }\n    return path\n  }\n\n  public addFiletypeParser(filetypeParser: FiletypeParserOptions): void {\n    const resolvedParser: FiletypeParserOptions = {\n      ...filetypeParser,\n      aliases: filetypeParser.aliases\n        ? [...new Set(filetypeParser.aliases.filter((alias) => alias !== filetypeParser.filetype))]\n        : undefined,\n      wasm: this.resolvePath(filetypeParser.wasm),\n      queries: {\n        highlights: filetypeParser.queries.highlights.map((path) => this.resolvePath(path)),\n        injections: filetypeParser.queries.injections?.map((path) => this.resolvePath(path)),\n      },\n    }\n    this.worker?.postMessage({ type: \"ADD_FILETYPE_PARSER\", filetypeParser: resolvedParser })\n  }\n\n  public async getPerformance(): Promise<PerformanceStats> {\n    const messageId = `performance_${this.messageIdCounter++}`\n    return new Promise<PerformanceStats>((resolve) => {\n      this.messageCallbacks.set(messageId, resolve)\n      this.worker?.postMessage({ type: \"GET_PERFORMANCE\", messageId })\n    })\n  }\n\n  public async highlightOnce(\n    content: string,\n    filetype: string,\n  ): Promise<{ highlights?: SimpleHighlight[]; warning?: string; error?: string }> {\n    if (!this.initialized) {\n      try {\n        await this.initialize()\n      } catch (error) {\n        return { error: \"Could not highlight because of initialization error\" }\n      }\n    }\n\n    const messageId = `oneshot_${this.messageIdCounter++}`\n    return new Promise((resolve) => {\n      this.messageCallbacks.set(messageId, resolve)\n      this.worker?.postMessage({\n        type: \"ONESHOT_HIGHLIGHT\",\n        content,\n        filetype,\n        messageId,\n      })\n    })\n  }\n\n  private handleWorkerMessage(event: MessageEvent) {\n    const { type, bufferId, error, highlights, warning, messageId, hasParser, performance, version } = event.data\n\n    if (type === \"HIGHLIGHT_RESPONSE\") {\n      const buffer = this.buffers.get(bufferId)\n      if (!buffer || !buffer.hasParser) return\n      if (buffer.version !== version) {\n        this.resetBuffer(bufferId, buffer.version, buffer.content)\n        return\n      }\n      this.emit(\"highlights:response\", bufferId, version, highlights)\n    }\n\n    if (type === \"INIT_RESPONSE\") {\n      if (this.initializeResolvers) {\n        clearTimeout(this.initializeResolvers.timeoutId)\n        if (error) {\n          console.error(\"TreeSitter client initialization failed:\", error)\n          this.initializeResolvers.reject(new Error(error))\n        } else {\n          this.initialized = true\n          this.initializeResolvers.resolve()\n        }\n        this.initializeResolvers = undefined\n        return\n      }\n    }\n\n    if (type === \"PARSER_INIT_RESPONSE\") {\n      const callback = this.messageCallbacks.get(messageId)\n      if (callback) {\n        this.messageCallbacks.delete(messageId)\n        callback({ hasParser, warning, error })\n      }\n      return\n    }\n\n    if (type === \"PRELOAD_PARSER_RESPONSE\") {\n      const callback = this.messageCallbacks.get(messageId)\n      if (callback) {\n        this.messageCallbacks.delete(messageId)\n        callback({ hasParser })\n      }\n      return\n    }\n\n    if (type === \"BUFFER_DISPOSED\") {\n      const callback = this.messageCallbacks.get(`dispose_${bufferId}`)\n      if (callback) {\n        this.messageCallbacks.delete(`dispose_${bufferId}`)\n        callback(true)\n      }\n      this.emit(\"buffer:disposed\", bufferId)\n      return\n    }\n\n    if (type === \"PERFORMANCE_RESPONSE\") {\n      const callback = this.messageCallbacks.get(messageId)\n      if (callback) {\n        this.messageCallbacks.delete(messageId)\n        callback(performance)\n      }\n      return\n    }\n\n    if (type === \"ONESHOT_HIGHLIGHT_RESPONSE\") {\n      const callback = this.messageCallbacks.get(messageId)\n      if (callback) {\n        this.messageCallbacks.delete(messageId)\n        callback({ highlights, warning, error })\n      }\n      return\n    }\n\n    if (type === \"UPDATE_DATA_PATH_RESPONSE\") {\n      const callback = this.messageCallbacks.get(messageId)\n      if (callback) {\n        this.messageCallbacks.delete(messageId)\n        callback({ error })\n      }\n      return\n    }\n\n    if (type === \"CLEAR_CACHE_RESPONSE\") {\n      const callback = this.messageCallbacks.get(messageId)\n      if (callback) {\n        this.messageCallbacks.delete(messageId)\n        callback({ error })\n      }\n      return\n    }\n\n    if (warning) {\n      this.emitWarning(warning, bufferId)\n      return\n    }\n\n    if (error) {\n      this.emitError(error, bufferId)\n      return\n    }\n\n    if (type === \"WORKER_LOG\") {\n      const { logType, data } = event.data\n      const message = data.join(\" \")\n\n      this.emit(\"worker:log\", logType, message)\n\n      if (logType === \"log\") {\n        console.log(\"TSWorker:\", ...data)\n      } else if (logType === \"error\") {\n        console.error(\"TSWorker:\", ...data)\n      } else if (logType === \"warn\") {\n        console.warn(\"TSWorker:\", ...data)\n      }\n      return\n    }\n  }\n\n  public async preloadParser(filetype: string): Promise<boolean> {\n    const messageId = `has_parser_${this.messageIdCounter++}`\n    const response = await new Promise<{ hasParser: boolean; warning?: string; error?: string }>((resolve) => {\n      this.messageCallbacks.set(messageId, resolve)\n      this.worker?.postMessage({\n        type: \"PRELOAD_PARSER\",\n        filetype,\n        messageId,\n      })\n    })\n    return response.hasParser\n  }\n\n  public async createBuffer(\n    id: number,\n    content: string,\n    filetype: string,\n    version: number = 1,\n    autoInitialize: boolean = true,\n  ): Promise<boolean> {\n    if (!this.initialized) {\n      if (!autoInitialize) {\n        this.emitError(\"Could not create buffer because client is not initialized\")\n        return false\n      }\n      try {\n        await this.initialize()\n      } catch (error) {\n        this.emitError(\"Could not create buffer because of initialization error\")\n        return false\n      }\n    }\n\n    if (this.buffers.has(id)) {\n      throw new Error(`Buffer with id ${id} already exists`)\n    }\n\n    // Set buffer state immediately to avoid race conditions\n    this.buffers.set(id, { id, content, filetype, version, hasParser: false })\n\n    const messageId = `init_${this.messageIdCounter++}`\n    const response = await new Promise<{ hasParser: boolean; warning?: string; error?: string }>((resolve) => {\n      this.messageCallbacks.set(messageId, resolve)\n      this.worker?.postMessage({\n        type: \"INITIALIZE_PARSER\",\n        bufferId: id,\n        version,\n        content,\n        filetype,\n        messageId,\n      })\n    })\n\n    if (!response.hasParser) {\n      this.emit(\"buffer:initialized\", id, false)\n      if (filetype !== \"plaintext\") {\n        this.emitWarning(response.warning || response.error || \"Buffer has no parser\", id)\n      }\n      return false\n    }\n\n    // Update buffer state to indicate it has a parser\n    const bufferState: ParsedBuffer = { id, content, filetype, version, hasParser: true }\n    this.buffers.set(id, bufferState)\n\n    this.emit(\"buffer:initialized\", id, true)\n    return true\n  }\n\n  public async updateBuffer(id: number, edits: Edit[], newContent: string, version: number): Promise<void> {\n    if (!this.initialized) {\n      return\n    }\n\n    const buffer = this.buffers.get(id)\n    if (!buffer || !buffer.hasParser) {\n      return\n    }\n\n    // Update buffer state\n    this.buffers.set(id, { ...buffer, content: newContent, version })\n\n    if (!this.editQueues.has(id)) {\n      this.editQueues.set(\n        id,\n        new ProcessQueue<EditQueueItem>((item) =>\n          this.processEdit(id, item.edits, item.newContent, item.version, item.isReset),\n        ),\n      )\n    }\n\n    const bufferQueue = this.editQueues.get(id)!\n    bufferQueue.enqueue({ edits, newContent, version })\n  }\n\n  private async processEdit(\n    bufferId: number,\n    edits: Edit[],\n    newContent: string,\n    version: number,\n    isReset = false,\n  ): Promise<void> {\n    this.worker?.postMessage({\n      type: isReset ? \"RESET_BUFFER\" : \"HANDLE_EDITS\",\n      bufferId,\n      version,\n      content: newContent,\n      edits,\n    })\n  }\n\n  public async removeBuffer(bufferId: number): Promise<void> {\n    if (!this.initialized) {\n      return\n    }\n\n    this.buffers.delete(bufferId)\n\n    if (this.editQueues.has(bufferId)) {\n      this.editQueues.get(bufferId)?.clear()\n      this.editQueues.delete(bufferId)\n    }\n\n    if (this.worker) {\n      await new Promise<boolean>((resolve) => {\n        const messageId = `dispose_${bufferId}`\n        this.messageCallbacks.set(messageId, resolve)\n        try {\n          this.worker!.postMessage({\n            type: \"DISPOSE_BUFFER\",\n            bufferId,\n          })\n        } catch (error) {\n          console.error(\"Error disposing buffer\", error)\n          resolve(false)\n        }\n\n        // Add a timeout in case the worker doesn't respond\n        setTimeout(() => {\n          if (this.messageCallbacks.has(messageId)) {\n            this.messageCallbacks.delete(messageId)\n            console.warn({ bufferId }, \"Timed out waiting for buffer to be disposed\")\n            resolve(false)\n          }\n        }, 3000)\n      })\n    }\n\n    this.debouncer.clearDebounce(`reset-${bufferId}`)\n  }\n\n  public async destroy(): Promise<void> {\n    if (this.initializeResolvers) {\n      clearTimeout(this.initializeResolvers.timeoutId)\n      // Reject pending initialization promise to prevent hanging awaits\n      this.initializeResolvers.reject(new Error(\"Client destroyed during initialization\"))\n      this.initializeResolvers = undefined\n    }\n\n    for (const [messageId, callback] of this.messageCallbacks.entries()) {\n      if (typeof callback === \"function\") {\n        try {\n          callback({ error: \"Client destroyed\" })\n        } catch (e) {\n          // Ignore errors during cleanup\n        }\n      }\n    }\n    this.messageCallbacks.clear()\n\n    clearDebounceScope(\"tree-sitter-client\")\n    this.debouncer.clear()\n\n    this.editQueues.clear()\n    this.buffers.clear()\n\n    this.stopWorker()\n\n    this.initialized = false\n    this.initializePromise = undefined\n  }\n\n  public async resetBuffer(bufferId: number, version: number, content: string): Promise<void> {\n    if (!this.initialized) {\n      return\n    }\n\n    const buffer = this.buffers.get(bufferId)\n    if (!buffer || !buffer.hasParser) {\n      this.emitError(\"Cannot reset buffer with no parser\", bufferId)\n      return\n    }\n\n    // Update buffer state\n    this.buffers.set(bufferId, { ...buffer, content, version })\n\n    // Use debouncer to avoid excessive resets\n    this.debouncer.debounce(`reset-${bufferId}`, 10, () => this.processEdit(bufferId, [], content, version, true))\n  }\n\n  public getBuffer(bufferId: number): BufferState | undefined {\n    return this.buffers.get(bufferId)\n  }\n\n  public getAllBuffers(): BufferState[] {\n    return Array.from(this.buffers.values())\n  }\n\n  public isInitialized(): boolean {\n    return this.initialized\n  }\n\n  public async setDataPath(dataPath: string): Promise<void> {\n    if (this.options.dataPath === dataPath) {\n      return\n    }\n\n    this.options.dataPath = dataPath\n\n    if (this.initialized && this.worker) {\n      const messageId = `update_datapath_${this.messageIdCounter++}`\n      return new Promise<void>((resolve, reject) => {\n        this.messageCallbacks.set(messageId, (response: any) => {\n          if (response.error) {\n            reject(new Error(response.error))\n          } else {\n            resolve()\n          }\n        })\n        this.worker!.postMessage({\n          type: \"UPDATE_DATA_PATH\",\n          dataPath,\n          messageId,\n        })\n      })\n    }\n  }\n\n  public async clearCache(): Promise<void> {\n    if (!this.initialized || !this.worker) {\n      throw new Error(\"Cannot clear cache: client is not initialized\")\n    }\n\n    const messageId = `clear_cache_${this.messageIdCounter++}`\n    return new Promise<void>((resolve, reject) => {\n      this.messageCallbacks.set(messageId, (response: any) => {\n        if (response.error) {\n          reject(new Error(response.error))\n        } else {\n          resolve()\n        }\n      })\n      this.worker!.postMessage({\n        type: \"CLEAR_CACHE\",\n        messageId,\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/default-parsers.ts",
    "content": "// This file is generated by assets/update.ts - DO NOT EDIT MANUALLY\n// Run 'bun assets/update.ts' to regenerate this file\n// Last generated: 2026-03-20T21:07:24.696Z\n\nimport type { FiletypeParserOptions } from \"./types\"\nimport { resolve, dirname } from \"path\"\nimport { fileURLToPath } from \"url\"\n\nimport javascript_highlights from \"./assets/javascript/highlights.scm\" with { type: \"file\" }\nimport javascript_language from \"./assets/javascript/tree-sitter-javascript.wasm\" with { type: \"file\" }\nimport typescript_highlights from \"./assets/typescript/highlights.scm\" with { type: \"file\" }\nimport typescript_language from \"./assets/typescript/tree-sitter-typescript.wasm\" with { type: \"file\" }\nimport markdown_highlights from \"./assets/markdown/highlights.scm\" with { type: \"file\" }\nimport markdown_language from \"./assets/markdown/tree-sitter-markdown.wasm\" with { type: \"file\" }\nimport markdown_injections from \"./assets/markdown/injections.scm\" with { type: \"file\" }\nimport markdown_inline_highlights from \"./assets/markdown_inline/highlights.scm\" with { type: \"file\" }\nimport markdown_inline_language from \"./assets/markdown_inline/tree-sitter-markdown_inline.wasm\" with { type: \"file\" }\nimport zig_highlights from \"./assets/zig/highlights.scm\" with { type: \"file\" }\nimport zig_language from \"./assets/zig/tree-sitter-zig.wasm\" with { type: \"file\" }\n\n// Cached parsers to avoid re-resolving paths on every call\nlet _cachedParsers: FiletypeParserOptions[] | undefined\n\nexport function getParsers(): FiletypeParserOptions[] {\n  if (!_cachedParsers) {\n    _cachedParsers = [\n      {\n        filetype: \"javascript\",\n        aliases: [\"javascriptreact\"],\n        queries: {\n          highlights: [resolve(dirname(fileURLToPath(import.meta.url)), javascript_highlights)],\n        },\n        wasm: resolve(dirname(fileURLToPath(import.meta.url)), javascript_language),\n      },\n      {\n        filetype: \"typescript\",\n        aliases: [\"typescriptreact\"],\n        queries: {\n          highlights: [resolve(dirname(fileURLToPath(import.meta.url)), typescript_highlights)],\n        },\n        wasm: resolve(dirname(fileURLToPath(import.meta.url)), typescript_language),\n      },\n      {\n        filetype: \"markdown\",\n        queries: {\n          highlights: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_highlights)],\n          injections: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_injections)],\n        },\n        wasm: resolve(dirname(fileURLToPath(import.meta.url)), markdown_language),\n        injectionMapping: {\n          \"nodeTypes\": {\n                    \"inline\": \"markdown_inline\",\n                    \"pipe_table_cell\": \"markdown_inline\"\n          },\n          \"infoStringMap\": {\n                    \"javascript\": \"javascript\",\n                    \"js\": \"javascript\",\n                    \"jsx\": \"javascriptreact\",\n                    \"javascriptreact\": \"javascriptreact\",\n                    \"typescript\": \"typescript\",\n                    \"ts\": \"typescript\",\n                    \"tsx\": \"typescriptreact\",\n                    \"typescriptreact\": \"typescriptreact\",\n                    \"markdown\": \"markdown\",\n                    \"md\": \"markdown\"\n          }\n},\n      },\n      {\n        filetype: \"markdown_inline\",\n        queries: {\n          highlights: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_inline_highlights)],\n        },\n        wasm: resolve(dirname(fileURLToPath(import.meta.url)), markdown_inline_language),\n      },\n      {\n        filetype: \"zig\",\n        queries: {\n          highlights: [resolve(dirname(fileURLToPath(import.meta.url)), zig_highlights)],\n        },\n        wasm: resolve(dirname(fileURLToPath(import.meta.url)), zig_language),\n      },\n    ]\n  }\n  return _cachedParsers\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/download-utils.ts",
    "content": "import { mkdir, readFile, writeFile } from \"fs/promises\"\nimport * as path from \"path\"\n\nexport interface DownloadResult {\n  content?: Buffer\n  filePath?: string\n  error?: string\n}\n\nexport class DownloadUtils {\n  private static hashUrl(url: string): string {\n    let hash = 0\n    for (let i = 0; i < url.length; i++) {\n      const char = url.charCodeAt(i)\n      hash = (hash << 5) - hash + char\n      hash = hash & hash\n    }\n    return Math.abs(hash).toString(16)\n  }\n\n  /**\n   * Download a file from URL or load from local path, with caching support\n   */\n  static async downloadOrLoad(\n    source: string,\n    cacheDir: string,\n    cacheSubdir: string,\n    fileExtension: string,\n    useHashForCache: boolean = true,\n    filetype?: string,\n  ): Promise<DownloadResult> {\n    const isUrl = source.startsWith(\"http://\") || source.startsWith(\"https://\")\n\n    if (isUrl) {\n      let cacheFileName: string\n      if (useHashForCache) {\n        const hash = this.hashUrl(source)\n        cacheFileName = filetype ? `${filetype}-${hash}${fileExtension}` : `${hash}${fileExtension}`\n      } else {\n        cacheFileName = path.basename(source)\n      }\n      const cacheFile = path.join(cacheDir, cacheSubdir, cacheFileName)\n\n      // Ensure cache directory exists\n      await mkdir(path.dirname(cacheFile), { recursive: true })\n\n      try {\n        const cachedContent = await readFile(cacheFile)\n        if (cachedContent.byteLength > 0) {\n          console.log(`Loaded from cache: ${cacheFile} (${source})`)\n          return { content: cachedContent, filePath: cacheFile }\n        }\n      } catch (error) {\n        // Cache miss, continue to fetch\n      }\n\n      try {\n        console.log(`Downloading from URL: ${source}`)\n        const response = await fetch(source)\n        if (!response.ok) {\n          return { error: `Failed to fetch from ${source}: ${response.statusText}` }\n        }\n        const content = Buffer.from(await response.arrayBuffer())\n\n        try {\n          await writeFile(cacheFile, Buffer.from(content))\n          console.log(`Cached: ${source}`)\n        } catch (cacheError) {\n          console.warn(`Failed to cache: ${cacheError}`)\n        }\n\n        return { content, filePath: cacheFile }\n      } catch (error) {\n        return { error: `Error downloading from ${source}: ${error}` }\n      }\n    } else {\n      try {\n        console.log(`Loading from local path: ${source}`)\n        const content = await readFile(source)\n        return { content, filePath: source }\n      } catch (error) {\n        return { error: `Error loading from local path ${source}: ${error}` }\n      }\n    }\n  }\n\n  /**\n   * Download and save a file to a specific target path\n   */\n  static async downloadToPath(source: string, targetPath: string): Promise<DownloadResult> {\n    const isUrl = source.startsWith(\"http://\") || source.startsWith(\"https://\")\n\n    await mkdir(path.dirname(targetPath), { recursive: true })\n\n    if (isUrl) {\n      try {\n        console.log(`Downloading from URL: ${source}`)\n        const response = await fetch(source)\n        if (!response.ok) {\n          return { error: `Failed to fetch from ${source}: ${response.statusText}` }\n        }\n        const content = Buffer.from(await response.arrayBuffer())\n\n        await writeFile(targetPath, Buffer.from(content))\n        console.log(`Downloaded: ${source} -> ${targetPath}`)\n\n        return { content, filePath: targetPath }\n      } catch (error) {\n        return { error: `Error downloading from ${source}: ${error}` }\n      }\n    } else {\n      try {\n        console.log(`Copying from local path: ${source}`)\n        const content = await readFile(source)\n        await writeFile(targetPath, Buffer.from(content))\n        return { content, filePath: targetPath }\n      } catch (error) {\n        return { error: `Error copying from local path ${source}: ${error}` }\n      }\n    }\n  }\n\n  /**\n   * Fetch multiple highlight queries and concatenate them\n   */\n  static async fetchHighlightQueries(sources: string[], cacheDir: string, filetype: string): Promise<string> {\n    const queryPromises = sources.map((source) => this.fetchHighlightQuery(source, cacheDir, filetype))\n    const queryResults = await Promise.all(queryPromises)\n\n    const validQueries = queryResults.filter((query) => query.trim().length > 0)\n    return validQueries.join(\"\\n\")\n  }\n\n  private static async fetchHighlightQuery(source: string, cacheDir: string, filetype: string): Promise<string> {\n    const result = await this.downloadOrLoad(source, cacheDir, \"queries\", \".scm\", true, filetype)\n\n    if (result.error) {\n      console.error(`Error fetching highlight query from ${source}:`, result.error)\n      return \"\"\n    }\n\n    if (result.content) {\n      return new TextDecoder().decode(result.content)\n    }\n\n    return \"\"\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/index.ts",
    "content": "import { singleton } from \"../singleton.js\"\nimport { TreeSitterClient } from \"./client.js\"\nimport type { TreeSitterClientOptions } from \"./types.js\"\nimport { getDataPaths } from \"../data-paths.js\"\n\nexport * from \"./client.js\"\nexport * from \"../tree-sitter-styled-text.js\"\nexport * from \"./types.js\"\nexport * from \"./resolve-ft.js\"\nexport type { UpdateOptions } from \"./assets/update.js\"\nexport { updateAssets } from \"./assets/update.js\"\n\nexport function getTreeSitterClient(): TreeSitterClient {\n  const dataPathsManager = getDataPaths()\n  const defaultOptions: TreeSitterClientOptions = {\n    dataPath: dataPathsManager.globalDataPath,\n  }\n\n  return singleton(\"tree-sitter-client\", () => {\n    const client = new TreeSitterClient(defaultOptions)\n\n    dataPathsManager.on(\"paths:changed\", (paths) => {\n      client.setDataPath(paths.globalDataPath)\n    })\n\n    return client\n  })\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/parser.worker.ts",
    "content": "import { Parser, Query, Tree, Language } from \"web-tree-sitter\"\nimport type { Edit, QueryCapture, Range } from \"web-tree-sitter\"\nimport { mkdir } from \"fs/promises\"\nimport * as path from \"path\"\nimport type {\n  HighlightRange,\n  HighlightResponse,\n  SimpleHighlight,\n  FiletypeParserOptions,\n  PerformanceStats,\n  InjectionMapping,\n} from \"./types.js\"\nimport { DownloadUtils } from \"./download-utils.js\"\nimport { isMainThread } from \"worker_threads\"\nimport { isBunfsPath, normalizeBunfsPath } from \"../bunfs.js\"\n\nconst self = globalThis\n\ntype ParserState = {\n  parser: Parser\n  tree: Tree\n  queries: {\n    highlights: Query\n    injections?: Query\n  }\n  filetype: string\n  content: string\n  injectionMapping?: InjectionMapping\n}\n\ninterface FiletypeParser {\n  filetype: string\n  queries: {\n    highlights: Query\n    injections?: Query\n  }\n  language: Language\n  injectionMapping?: InjectionMapping\n}\n\ninterface ReusableParserState {\n  parser: Parser\n  filetypeParser: FiletypeParser\n  queries: {\n    highlights: Query\n    injections?: Query\n  }\n}\n\nclass ParserWorker {\n  private bufferParsers: Map<number, ParserState> = new Map()\n  private filetypeParserOptions: Map<string, FiletypeParserOptions> = new Map()\n  private filetypeAliases: Map<string, string> = new Map()\n  private filetypeParsers: Map<string, FiletypeParser> = new Map()\n  private filetypeParserPromises: Map<string, Promise<FiletypeParser | undefined>> = new Map()\n  private reusableParsers: Map<string, ReusableParserState> = new Map()\n  private reusableParserPromises: Map<string, Promise<ReusableParserState | undefined>> = new Map()\n  private initializePromise: Promise<void> | undefined\n  public performance: PerformanceStats\n  private dataPath: string | undefined\n  private tsDataPath: string | undefined\n  private initialized: boolean = false\n\n  constructor() {\n    this.performance = {\n      averageParseTime: 0,\n      parseTimes: [],\n      averageQueryTime: 0,\n      queryTimes: [],\n    }\n  }\n\n  private async fetchQueries(sources: string[], filetype: string): Promise<string> {\n    if (!this.tsDataPath) {\n      return \"\"\n    }\n    return DownloadUtils.fetchHighlightQueries(sources, this.tsDataPath, filetype)\n  }\n\n  async initialize({ dataPath }: { dataPath: string }) {\n    if (this.initializePromise) {\n      return this.initializePromise\n    }\n    this.initializePromise = new Promise(async (resolve, reject) => {\n      this.dataPath = dataPath\n      this.tsDataPath = path.join(dataPath, \"tree-sitter\")\n\n      try {\n        await mkdir(path.join(this.tsDataPath, \"languages\"), { recursive: true })\n        await mkdir(path.join(this.tsDataPath, \"queries\"), { recursive: true })\n\n        let { default: treeWasm } = await import(\"web-tree-sitter/tree-sitter.wasm\" as string, {\n          with: { type: \"wasm\" },\n        })\n\n        if (isBunfsPath(treeWasm)) {\n          treeWasm = normalizeBunfsPath(path.parse(treeWasm).base)\n        }\n\n        await Parser.init({\n          locateFile() {\n            return treeWasm\n          },\n        })\n\n        this.initialized = true\n        resolve()\n      } catch (error) {\n        reject(error)\n      }\n    })\n    return this.initializePromise\n  }\n\n  public addFiletypeParser(filetypeParser: FiletypeParserOptions) {\n    const previousAliases = this.filetypeParserOptions.get(filetypeParser.filetype)?.aliases ?? []\n    for (const alias of previousAliases) {\n      if (this.filetypeAliases.get(alias) === filetypeParser.filetype) {\n        this.filetypeAliases.delete(alias)\n      }\n    }\n\n    const aliases = [...new Set((filetypeParser.aliases ?? []).filter((alias) => alias !== filetypeParser.filetype))]\n\n    this.filetypeAliases.delete(filetypeParser.filetype)\n    this.filetypeParserOptions.set(filetypeParser.filetype, {\n      ...filetypeParser,\n      aliases,\n    })\n\n    for (const alias of aliases) {\n      this.filetypeAliases.set(alias, filetypeParser.filetype)\n    }\n\n    this.invalidateParserCaches(filetypeParser.filetype)\n  }\n\n  private resolveCanonicalFiletype(filetype: string): string {\n    if (this.filetypeParserOptions.has(filetype)) {\n      return filetype\n    }\n\n    return this.filetypeAliases.get(filetype) ?? filetype\n  }\n\n  private invalidateParserCaches(filetype: string): void {\n    this.filetypeParsers.delete(filetype)\n    this.filetypeParserPromises.delete(filetype)\n\n    const reusableParser = this.reusableParsers.get(filetype)\n    if (reusableParser) {\n      reusableParser.parser.delete()\n      this.reusableParsers.delete(filetype)\n    }\n\n    this.reusableParserPromises.delete(filetype)\n  }\n\n  private async createQueries(\n    filetypeParser: FiletypeParserOptions,\n    language: Language,\n  ): Promise<\n    | {\n        highlights: Query\n        injections?: Query\n      }\n    | undefined\n  > {\n    try {\n      const highlightQueryContent = await this.fetchQueries(filetypeParser.queries.highlights, filetypeParser.filetype)\n      if (!highlightQueryContent) {\n        console.error(\"Failed to fetch highlight queries for:\", filetypeParser.filetype)\n        return undefined\n      }\n\n      const highlightsQuery = new Query(language, highlightQueryContent)\n      const result: { highlights: Query; injections?: Query } = {\n        highlights: highlightsQuery,\n      }\n\n      if (filetypeParser.queries.injections && filetypeParser.queries.injections.length > 0) {\n        const injectionQueryContent = await this.fetchQueries(\n          filetypeParser.queries.injections,\n          filetypeParser.filetype,\n        )\n        if (injectionQueryContent) {\n          result.injections = new Query(language, injectionQueryContent)\n        }\n      }\n\n      return result\n    } catch (error) {\n      console.error(\"Error creating queries for\", filetypeParser.filetype, filetypeParser.queries)\n      console.error(error)\n      return undefined\n    }\n  }\n\n  private async loadLanguage(languageSource: string): Promise<Language | undefined> {\n    if (!this.initialized || !this.tsDataPath) {\n      return undefined\n    }\n\n    const result = await DownloadUtils.downloadOrLoad(languageSource, this.tsDataPath, \"languages\", \".wasm\", false)\n\n    if (result.error) {\n      console.error(`Error loading language ${languageSource}:`, result.error)\n      return undefined\n    }\n\n    if (!result.filePath) {\n      return undefined\n    }\n\n    // Normalize path for Windows compatibility - tree-sitter expects forward slashes\n    const normalizedPath = result.filePath.replaceAll(\"\\\\\", \"/\")\n\n    try {\n      const language = await Language.load(normalizedPath)\n      return language\n    } catch (error) {\n      console.error(`Error loading language from ${normalizedPath}:`, error)\n      return undefined\n    }\n  }\n\n  private async resolveFiletypeParser(filetype: string): Promise<FiletypeParser | undefined> {\n    const canonicalFiletype = this.resolveCanonicalFiletype(filetype)\n\n    if (this.filetypeParsers.has(canonicalFiletype)) {\n      return this.filetypeParsers.get(canonicalFiletype)\n    }\n\n    if (this.filetypeParserPromises.has(canonicalFiletype)) {\n      return this.filetypeParserPromises.get(canonicalFiletype)\n    }\n\n    const loadingPromise = this.loadFiletypeParser(canonicalFiletype)\n    this.filetypeParserPromises.set(canonicalFiletype, loadingPromise)\n\n    try {\n      const result = await loadingPromise\n      if (result) {\n        this.filetypeParsers.set(canonicalFiletype, result)\n      }\n      return result\n    } finally {\n      this.filetypeParserPromises.delete(canonicalFiletype)\n    }\n  }\n\n  private async loadFiletypeParser(filetype: string): Promise<FiletypeParser | undefined> {\n    const filetypeParserOptions = this.filetypeParserOptions.get(filetype)\n    if (!filetypeParserOptions) {\n      return undefined\n    }\n    const language = await this.loadLanguage(filetypeParserOptions.wasm)\n    if (!language) {\n      return undefined\n    }\n    const queries = await this.createQueries(filetypeParserOptions, language)\n    if (!queries) {\n      console.error(\"Failed to create queries for:\", filetype)\n      return undefined\n    }\n    const filetypeParser: FiletypeParser = {\n      ...filetypeParserOptions,\n      queries,\n      language,\n    }\n    return filetypeParser\n  }\n\n  public async preloadParser(filetype: string) {\n    return this.resolveFiletypeParser(filetype)\n  }\n\n  private async getReusableParser(filetype: string): Promise<ReusableParserState | undefined> {\n    const canonicalFiletype = this.resolveCanonicalFiletype(filetype)\n\n    if (this.reusableParsers.has(canonicalFiletype)) {\n      return this.reusableParsers.get(canonicalFiletype)\n    }\n\n    if (this.reusableParserPromises.has(canonicalFiletype)) {\n      return this.reusableParserPromises.get(canonicalFiletype)\n    }\n\n    const creationPromise = this.createReusableParser(canonicalFiletype)\n    this.reusableParserPromises.set(canonicalFiletype, creationPromise)\n\n    try {\n      const result = await creationPromise\n      if (result) {\n        this.reusableParsers.set(canonicalFiletype, result)\n      }\n      return result\n    } finally {\n      this.reusableParserPromises.delete(canonicalFiletype)\n    }\n  }\n\n  private async createReusableParser(filetype: string): Promise<ReusableParserState | undefined> {\n    const filetypeParser = await this.resolveFiletypeParser(filetype)\n    if (!filetypeParser) {\n      return undefined\n    }\n\n    const parser = new Parser()\n    parser.setLanguage(filetypeParser.language)\n\n    const reusableState: ReusableParserState = {\n      parser,\n      filetypeParser,\n      queries: filetypeParser.queries,\n    }\n\n    return reusableState\n  }\n\n  async handleInitializeParser(\n    bufferId: number,\n    version: number,\n    content: string,\n    filetype: string,\n    messageId: string,\n  ) {\n    const filetypeParser = await this.resolveFiletypeParser(filetype)\n\n    if (!filetypeParser) {\n      self.postMessage({\n        type: \"PARSER_INIT_RESPONSE\",\n        bufferId,\n        messageId,\n        hasParser: false,\n        warning: `No parser available for filetype ${filetype}`,\n      })\n      return\n    }\n\n    const parser = new Parser()\n    parser.setLanguage(filetypeParser.language)\n    const tree = parser.parse(content)\n    if (!tree) {\n      self.postMessage({\n        type: \"PARSER_INIT_RESPONSE\",\n        bufferId,\n        messageId,\n        hasParser: false,\n        error: \"Failed to parse buffer\",\n      })\n      return\n    }\n\n    const parserState: ParserState = {\n      parser,\n      tree,\n      queries: filetypeParser.queries,\n      filetype,\n      content,\n      injectionMapping: filetypeParser.injectionMapping,\n    }\n    this.bufferParsers.set(bufferId, parserState)\n\n    self.postMessage({\n      type: \"PARSER_INIT_RESPONSE\",\n      bufferId,\n      messageId,\n      hasParser: true,\n    })\n    const highlights = await this.initialQuery(parserState)\n    self.postMessage({\n      type: \"HIGHLIGHT_RESPONSE\",\n      bufferId,\n      version,\n      ...highlights,\n    })\n  }\n\n  private async initialQuery(parserState: ParserState) {\n    const query = parserState.queries.highlights\n    const matches: QueryCapture[] = query.captures(parserState.tree.rootNode)\n    let injectionRanges = new Map<string, Array<{ start: number; end: number }>>()\n\n    if (parserState.queries.injections) {\n      const injectionResult = await this.processInjections(parserState)\n      matches.push(...injectionResult.captures)\n      injectionRanges = injectionResult.injectionRanges\n    }\n\n    return this.getHighlights(parserState, matches, injectionRanges)\n  }\n\n  private getNodeText(node: any, content: string): string {\n    return content.substring(node.startIndex, node.endIndex)\n  }\n\n  private async processInjections(\n    parserState: ParserState,\n  ): Promise<{ captures: QueryCapture[]; injectionRanges: Map<string, Array<{ start: number; end: number }>> }> {\n    const injectionMatches: QueryCapture[] = []\n    const injectionRanges = new Map<string, Array<{ start: number; end: number }>>()\n\n    if (!parserState.queries.injections) {\n      return { captures: injectionMatches, injectionRanges }\n    }\n\n    const content = parserState.content\n    const injectionCaptures = parserState.queries.injections.captures(parserState.tree.rootNode)\n    const languageGroups = new Map<string, Array<{ node: any; name: string }>>()\n\n    // Use the injection mapping stored in the parser state\n    const injectionMapping = parserState.injectionMapping\n\n    for (const capture of injectionCaptures) {\n      const captureName = capture.name\n\n      if (captureName === \"injection.content\" || captureName.includes(\"injection\")) {\n        const nodeType = capture.node.type\n        let targetLanguage: string | undefined\n\n        // First, check if there's a direct node type mapping\n        if (injectionMapping?.nodeTypes && injectionMapping.nodeTypes[nodeType]) {\n          targetLanguage = injectionMapping.nodeTypes[nodeType]\n        } else if (nodeType === \"code_fence_content\") {\n          // For code fence content, try to extract language from info_string\n          const parent = capture.node.parent\n          if (parent) {\n            const infoString = parent.children.find((child: any) => child.type === \"info_string\")\n            if (infoString) {\n              const languageNode = infoString.children.find((child: any) => child.type === \"language\")\n              if (languageNode) {\n                const languageName = this.getNodeText(languageNode, content)\n\n                if (injectionMapping?.infoStringMap && injectionMapping.infoStringMap[languageName]) {\n                  targetLanguage = injectionMapping.infoStringMap[languageName]\n                } else {\n                  targetLanguage = languageName\n                }\n              }\n            }\n          }\n        }\n\n        if (targetLanguage) {\n          if (!languageGroups.has(targetLanguage)) {\n            languageGroups.set(targetLanguage, [])\n          }\n          languageGroups.get(targetLanguage)!.push({ node: capture.node, name: capture.name })\n        }\n      }\n    }\n\n    // Process each language group\n    for (const [language, captures] of languageGroups.entries()) {\n      const injectedParser = await this.getReusableParser(language)\n\n      if (!injectedParser) {\n        console.warn(`No parser found for injection language: ${language}`)\n        continue\n      }\n\n      // Track injection ranges for this language\n      if (!injectionRanges.has(language)) {\n        injectionRanges.set(language, [])\n      }\n\n      const parser = injectedParser.parser\n      for (const { node: injectionNode } of captures) {\n        try {\n          // Record the injection range\n          injectionRanges.get(language)!.push({\n            start: injectionNode.startIndex,\n            end: injectionNode.endIndex,\n          })\n\n          const injectionContent = this.getNodeText(injectionNode, content)\n          const tree = parser.parse(injectionContent)\n\n          if (tree) {\n            const matches = injectedParser.queries.highlights.captures(tree.rootNode)\n\n            // Create new QueryCapture objects with offset positions\n            for (const match of matches) {\n              // Calculate offset positions by creating a new capture with adjusted node properties\n              // Store the injected query reference so we can look up properties correctly\n              const offsetCapture: QueryCapture & { _injectedQuery?: Query } = {\n                name: match.name,\n                patternIndex: match.patternIndex,\n                _injectedQuery: injectedParser.queries.highlights, // Store the correct query reference\n                node: {\n                  ...match.node,\n                  startPosition: {\n                    row: match.node.startPosition.row + injectionNode.startPosition.row,\n                    column:\n                      match.node.startPosition.row === 0\n                        ? match.node.startPosition.column + injectionNode.startPosition.column\n                        : match.node.startPosition.column,\n                  },\n                  endPosition: {\n                    row: match.node.endPosition.row + injectionNode.startPosition.row,\n                    column:\n                      match.node.endPosition.row === 0\n                        ? match.node.endPosition.column + injectionNode.startPosition.column\n                        : match.node.endPosition.column,\n                  },\n                  startIndex: match.node.startIndex + injectionNode.startIndex,\n                  endIndex: match.node.endIndex + injectionNode.startIndex,\n                } as any, // Cast to any since we're creating a pseudo-node\n              }\n\n              injectionMatches.push(offsetCapture)\n            }\n\n            tree.delete()\n          }\n        } catch (error) {\n          console.error(`Error processing injection for language ${language}:`, error)\n        }\n      }\n\n      // NOTE: Do NOT call parser.delete() here - this is a reusable parser!\n    }\n\n    return { captures: injectionMatches, injectionRanges }\n  }\n\n  private editToRange(edit: Edit): Range {\n    return {\n      startPosition: {\n        column: edit.startPosition.column,\n        row: edit.startPosition.row,\n      },\n      endPosition: {\n        column: edit.newEndPosition.column,\n        row: edit.newEndPosition.row,\n      },\n      startIndex: edit.startIndex,\n      endIndex: edit.newEndIndex,\n    }\n  }\n\n  async handleEdits(\n    bufferId: number,\n    content: string,\n    edits: Edit[],\n  ): Promise<{ highlights?: HighlightResponse[]; warning?: string; error?: string }> {\n    const parserState = this.bufferParsers.get(bufferId)\n    if (!parserState) {\n      return { warning: \"No parser state found for buffer\" }\n    }\n\n    parserState.content = content\n\n    for (const edit of edits) {\n      parserState.tree.edit(edit)\n    }\n\n    const startParse = performance.now()\n\n    const newTree = parserState.parser.parse(content, parserState.tree)\n\n    const endParse = performance.now()\n    const parseTime = endParse - startParse\n    this.performance.parseTimes.push(parseTime)\n    if (this.performance.parseTimes.length > 10) {\n      this.performance.parseTimes.shift()\n    }\n    this.performance.averageParseTime =\n      this.performance.parseTimes.reduce((acc, time) => acc + time, 0) / this.performance.parseTimes.length\n\n    if (!newTree) {\n      return { error: \"Failed to parse buffer\" }\n    }\n\n    const changedRanges = parserState.tree.getChangedRanges(newTree)\n    parserState.tree = newTree\n\n    const startQuery = performance.now()\n    const matches: QueryCapture[] = []\n\n    if (changedRanges.length === 0) {\n      edits.forEach((edit) => {\n        const range = this.editToRange(edit)\n        changedRanges.push(range)\n      })\n    }\n\n    for (const range of changedRanges) {\n      let node = parserState.tree.rootNode.descendantForPosition(range.startPosition, range.endPosition)\n\n      if (!node) {\n        continue\n      }\n\n      // If we got the root node, query with range to limit scope\n      if (node.equals(parserState.tree.rootNode)) {\n        // WHY ARE RANGES NOT WORKING!?\n        // The changed ranges are not returning anything in some cases\n        // Even this shit somehow returns many lines before the actual range,\n        // and even though expanded by 1000 bytes it does not capture much beyond the actual range.\n        // So freaking weird.\n        const rangeCaptures = parserState.queries.highlights.captures(\n          node,\n          // WTF!?\n          {\n            startIndex: range.startIndex - 100,\n            endIndex: range.endIndex + 1000,\n          },\n        )\n        matches.push(...rangeCaptures)\n        continue\n      }\n\n      while (node && !this.nodeContainsRange(node, range)) {\n        node = node.parent\n      }\n\n      if (!node) {\n        node = parserState.tree.rootNode\n      }\n\n      const nodeCaptures = parserState.queries.highlights.captures(node)\n      matches.push(...nodeCaptures)\n    }\n\n    let injectionRanges = new Map<string, Array<{ start: number; end: number }>>()\n    if (parserState.queries.injections) {\n      const injectionResult = await this.processInjections(parserState)\n      // Only add injection matches that are in the changed ranges\n      // This is a simplification - ideally we'd only process injections in changed ranges\n      matches.push(...injectionResult.captures)\n      injectionRanges = injectionResult.injectionRanges\n    }\n\n    const endQuery = performance.now()\n    const queryTime = endQuery - startQuery\n    this.performance.queryTimes.push(queryTime)\n    if (this.performance.queryTimes.length > 10) {\n      this.performance.queryTimes.shift()\n    }\n    this.performance.averageQueryTime =\n      this.performance.queryTimes.reduce((acc, time) => acc + time, 0) / this.performance.queryTimes.length\n\n    return this.getHighlights(parserState, matches, injectionRanges)\n  }\n\n  private nodeContainsRange(node: any, range: any): boolean {\n    return (\n      node.startPosition.row <= range.startPosition.row &&\n      node.endPosition.row >= range.endPosition.row &&\n      (node.startPosition.row < range.startPosition.row || node.startPosition.column <= range.startPosition.column) &&\n      (node.endPosition.row > range.endPosition.row || node.endPosition.column >= range.endPosition.column)\n    )\n  }\n\n  private getHighlights(\n    parserState: ParserState,\n    matches: QueryCapture[],\n    injectionRanges?: Map<string, Array<{ start: number; end: number }>>,\n  ): { highlights: HighlightResponse[] } {\n    const lineHighlights: Map<number, Map<number, HighlightRange>> = new Map()\n    const droppedHighlights: Map<number, Map<number, HighlightRange>> = new Map()\n\n    for (const match of matches) {\n      const node = match.node\n      const startLine = node.startPosition.row\n      const endLine = node.endPosition.row\n\n      const highlight = {\n        startCol: node.startPosition.column,\n        endCol: node.endPosition.column,\n        group: match.name,\n      }\n\n      if (!lineHighlights.has(startLine)) {\n        lineHighlights.set(startLine, new Map())\n        droppedHighlights.set(startLine, new Map())\n      }\n      if (lineHighlights.get(startLine)?.has(node.id)) {\n        droppedHighlights.get(startLine)?.set(node.id, lineHighlights.get(startLine)?.get(node.id)!)\n      }\n      lineHighlights.get(startLine)?.set(node.id, highlight)\n\n      if (startLine !== endLine) {\n        for (let line = startLine + 1; line <= endLine; line++) {\n          if (!lineHighlights.has(line)) {\n            lineHighlights.set(line, new Map())\n          }\n          const hl: HighlightRange = {\n            startCol: 0,\n            endCol: node.endPosition.column,\n            group: match.name,\n          }\n          lineHighlights.get(line)?.set(node.id, hl)\n        }\n      }\n    }\n\n    return {\n      highlights: Array.from(lineHighlights.entries()).map(([line, lineHighlights]) => ({\n        line,\n        highlights: Array.from(lineHighlights.values()),\n        droppedHighlights: droppedHighlights.get(line) ? Array.from(droppedHighlights.get(line)!.values()) : [],\n      })),\n    }\n  }\n\n  private getSimpleHighlights(\n    matches: QueryCapture[],\n    injectionRanges: Map<string, Array<{ start: number; end: number }>>,\n  ): SimpleHighlight[] {\n    const highlights: SimpleHighlight[] = []\n\n    const flatInjectionRanges: Array<{ start: number; end: number; lang: string }> = []\n    for (const [lang, ranges] of injectionRanges.entries()) {\n      for (const range of ranges) {\n        flatInjectionRanges.push({ ...range, lang })\n      }\n    }\n\n    for (const match of matches) {\n      const node = match.node\n\n      let isInjection = false\n      let injectionLang: string | undefined\n      let containsInjection = false\n      for (const injRange of flatInjectionRanges) {\n        if (node.startIndex >= injRange.start && node.endIndex <= injRange.end) {\n          isInjection = true\n          injectionLang = injRange.lang\n          break\n        } else if (node.startIndex <= injRange.start && node.endIndex >= injRange.end) {\n          containsInjection = true\n          break\n        }\n      }\n\n      const matchQuery = (match as any)._injectedQuery\n      const patternProperties = matchQuery?.setProperties?.[match.patternIndex]\n\n      const concealValue = patternProperties?.conceal ?? match.setProperties?.conceal\n      const concealLines = patternProperties?.conceal_lines ?? match.setProperties?.conceal_lines\n\n      const meta: any = {}\n      if (isInjection && injectionLang) {\n        meta.isInjection = true\n        meta.injectionLang = injectionLang\n      }\n      if (containsInjection) {\n        meta.containsInjection = true\n      }\n      if (concealValue !== undefined) {\n        meta.conceal = concealValue\n      }\n      if (concealLines !== undefined) {\n        meta.concealLines = concealLines\n      }\n\n      if (Object.keys(meta).length > 0) {\n        highlights.push([node.startIndex, node.endIndex, match.name, meta])\n      } else {\n        highlights.push([node.startIndex, node.endIndex, match.name])\n      }\n    }\n\n    highlights.sort((a, b) => a[0] - b[0])\n\n    return highlights\n  }\n\n  async handleResetBuffer(\n    bufferId: number,\n    version: number,\n    content: string,\n  ): Promise<{ highlights?: HighlightResponse[]; warning?: string; error?: string }> {\n    const parserState = this.bufferParsers.get(bufferId)\n    if (!parserState) {\n      return { warning: \"No parser state found for buffer\" }\n    }\n\n    parserState.content = content\n\n    const newTree = parserState.parser.parse(content)\n\n    if (!newTree) {\n      return { error: \"Failed to parse buffer during reset\" }\n    }\n\n    parserState.tree = newTree\n    const matches = parserState.queries.highlights.captures(parserState.tree.rootNode)\n\n    let injectionRanges = new Map<string, Array<{ start: number; end: number }>>()\n    if (parserState.queries.injections) {\n      const injectionResult = await this.processInjections(parserState)\n      matches.push(...injectionResult.captures)\n      injectionRanges = injectionResult.injectionRanges\n    }\n\n    return this.getHighlights(parserState, matches, injectionRanges)\n  }\n\n  disposeBuffer(bufferId: number): void {\n    const parserState = this.bufferParsers.get(bufferId)\n    if (!parserState) {\n      return\n    }\n\n    parserState.tree.delete()\n    parserState.parser.delete()\n\n    this.bufferParsers.delete(bufferId)\n  }\n\n  async handleOneShotHighlight(content: string, filetype: string, messageId: string): Promise<void> {\n    const reusableState = await this.getReusableParser(filetype)\n\n    if (!reusableState) {\n      self.postMessage({\n        type: \"ONESHOT_HIGHLIGHT_RESPONSE\",\n        messageId,\n        hasParser: false,\n        warning: `No parser available for filetype ${filetype}`,\n      })\n      return\n    }\n\n    // Markdown Parser BUG: For markdown, ensure content ends with newline so closing delimiters are parsed correctly\n    // The tree-sitter markdown parser only creates closing delimiter nodes when followed by newline\n    const parseContent = filetype === \"markdown\" && content.endsWith(\"```\") ? content + \"\\n\" : content\n\n    const tree = reusableState.parser.parse(parseContent)\n\n    if (!tree) {\n      self.postMessage({\n        type: \"ONESHOT_HIGHLIGHT_RESPONSE\",\n        messageId,\n        hasParser: false,\n        error: \"Failed to parse content\",\n      })\n      return\n    }\n\n    try {\n      const matches = reusableState.filetypeParser.queries.highlights.captures(tree.rootNode)\n\n      let injectionRanges = new Map<string, Array<{ start: number; end: number }>>()\n      if (reusableState.filetypeParser.queries.injections) {\n        const parserState: ParserState = {\n          parser: reusableState.parser,\n          tree,\n          queries: reusableState.filetypeParser.queries,\n          filetype,\n          content,\n          injectionMapping: reusableState.filetypeParser.injectionMapping,\n        }\n        const injectionResult = await this.processInjections(parserState)\n\n        matches.push(...injectionResult.captures)\n        injectionRanges = injectionResult.injectionRanges\n      }\n\n      const highlights = this.getSimpleHighlights(matches, injectionRanges)\n\n      self.postMessage({\n        type: \"ONESHOT_HIGHLIGHT_RESPONSE\",\n        messageId,\n        hasParser: true,\n        highlights,\n      })\n    } finally {\n      tree.delete()\n    }\n  }\n\n  async updateDataPath(dataPath: string): Promise<void> {\n    this.dataPath = dataPath\n    this.tsDataPath = path.join(dataPath, \"tree-sitter\")\n\n    try {\n      await mkdir(path.join(this.tsDataPath, \"languages\"), { recursive: true })\n      await mkdir(path.join(this.tsDataPath, \"queries\"), { recursive: true })\n    } catch (error) {\n      throw new Error(`Failed to update data path: ${error}`)\n    }\n  }\n\n  async clearCache(): Promise<void> {\n    if (!this.dataPath || !this.tsDataPath) {\n      throw new Error(\"No data path configured\")\n    }\n\n    const { rm } = await import(\"fs/promises\")\n\n    try {\n      const treeSitterPath = path.join(this.dataPath, \"tree-sitter\")\n\n      await rm(treeSitterPath, { recursive: true, force: true })\n\n      await mkdir(path.join(treeSitterPath, \"languages\"), { recursive: true })\n      await mkdir(path.join(treeSitterPath, \"queries\"), { recursive: true })\n\n      this.filetypeParsers.clear()\n      this.filetypeParserPromises.clear()\n      this.reusableParsers.clear()\n      this.reusableParserPromises.clear()\n    } catch (error) {\n      throw new Error(`Failed to clear cache: ${error}`)\n    }\n  }\n}\nif (!isMainThread) {\n  const worker = new ParserWorker()\n\n  function logMessage(type: \"log\" | \"error\" | \"warn\", ...args: any[]) {\n    self.postMessage({\n      type: \"WORKER_LOG\",\n      logType: type,\n      data: args,\n    })\n  }\n  console.log = (...args) => logMessage(\"log\", ...args)\n  console.error = (...args) => logMessage(\"error\", ...args)\n  console.warn = (...args) => logMessage(\"warn\", ...args)\n\n  // @ts-ignore - we'll fix this in the future for sure\n  self.onmessage = async (e: MessageEvent) => {\n    const { type, bufferId, version, content, filetype, edits, filetypeParser, messageId, dataPath } = e.data\n\n    try {\n      switch (type) {\n        case \"INIT\":\n          try {\n            await worker.initialize({ dataPath })\n            self.postMessage({ type: \"INIT_RESPONSE\" })\n          } catch (error) {\n            self.postMessage({\n              type: \"INIT_RESPONSE\",\n              error: error instanceof Error ? error.stack || error.message : String(error),\n            })\n          }\n          break\n\n        case \"ADD_FILETYPE_PARSER\":\n          worker.addFiletypeParser(filetypeParser)\n          break\n\n        case \"PRELOAD_PARSER\":\n          const maybeParser = await worker.preloadParser(filetype)\n          self.postMessage({ type: \"PRELOAD_PARSER_RESPONSE\", messageId, hasParser: !!maybeParser })\n          break\n\n        case \"INITIALIZE_PARSER\":\n          await worker.handleInitializeParser(bufferId, version, content, filetype, messageId)\n          break\n\n        case \"HANDLE_EDITS\":\n          const response = await worker.handleEdits(bufferId, content, edits)\n          if (response.highlights && response.highlights.length > 0) {\n            self.postMessage({ type: \"HIGHLIGHT_RESPONSE\", bufferId, version, ...response })\n          } else if (response.warning) {\n            self.postMessage({ type: \"WARNING\", bufferId, warning: response.warning })\n          } else if (response.error) {\n            self.postMessage({ type: \"ERROR\", bufferId, error: response.error })\n          }\n          break\n\n        case \"GET_PERFORMANCE\":\n          self.postMessage({ type: \"PERFORMANCE_RESPONSE\", performance: worker.performance, messageId })\n          break\n\n        case \"RESET_BUFFER\":\n          const resetResponse = await worker.handleResetBuffer(bufferId, version, content)\n          if (resetResponse.highlights && resetResponse.highlights.length > 0) {\n            self.postMessage({ type: \"HIGHLIGHT_RESPONSE\", bufferId, version, ...resetResponse })\n          } else if (resetResponse.warning) {\n            self.postMessage({ type: \"WARNING\", bufferId, warning: resetResponse.warning })\n          } else if (resetResponse.error) {\n            self.postMessage({ type: \"ERROR\", bufferId, error: resetResponse.error })\n          }\n          break\n\n        case \"DISPOSE_BUFFER\":\n          worker.disposeBuffer(bufferId)\n          self.postMessage({ type: \"BUFFER_DISPOSED\", bufferId })\n          break\n\n        case \"ONESHOT_HIGHLIGHT\":\n          await worker.handleOneShotHighlight(content, filetype, messageId)\n          break\n\n        case \"UPDATE_DATA_PATH\":\n          try {\n            await worker.updateDataPath(dataPath)\n            self.postMessage({ type: \"UPDATE_DATA_PATH_RESPONSE\", messageId })\n          } catch (error) {\n            self.postMessage({\n              type: \"UPDATE_DATA_PATH_RESPONSE\",\n              messageId,\n              error: error instanceof Error ? error.message : String(error),\n            })\n          }\n          break\n\n        case \"CLEAR_CACHE\":\n          try {\n            await worker.clearCache()\n            self.postMessage({ type: \"CLEAR_CACHE_RESPONSE\", messageId })\n          } catch (error) {\n            self.postMessage({\n              type: \"CLEAR_CACHE_RESPONSE\",\n              messageId,\n              error: error instanceof Error ? error.message : String(error),\n            })\n          }\n          break\n\n        default:\n          self.postMessage({\n            type: \"ERROR\",\n            bufferId,\n            error: `Unknown message type: ${type}`,\n          })\n      }\n    } catch (error) {\n      self.postMessage({\n        type: \"ERROR\",\n        bufferId,\n        error: error instanceof Error ? error.stack || error.message : String(error),\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/parsers-config.ts",
    "content": "/**\n * This file contains the configuration for the defaulttree-sitter parsers.\n * It is used by ./assets/update.ts to generate the default-parsers.ts file.\n * For changes here to be reflected in the default-parsers.ts file, you need to run `bun run ./assets/update.ts`\n */\nexport default {\n  parsers: [\n    {\n      filetype: \"javascript\",\n      aliases: [\"javascriptreact\"],\n      wasm: \"https://github.com/tree-sitter/tree-sitter-javascript/releases/download/v0.25.0/tree-sitter-javascript.wasm\",\n      queries: {\n        highlights: [\n          \"https://raw.githubusercontent.com/tree-sitter/tree-sitter-javascript/refs/heads/master/queries/highlights.scm\",\n        ],\n      },\n    },\n    {\n      filetype: \"typescript\",\n      aliases: [\"typescriptreact\"],\n      wasm: \"https://github.com/tree-sitter/tree-sitter-typescript/releases/download/v0.23.2/tree-sitter-typescript.wasm\",\n      queries: {\n        highlights: [\n          \"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/ecma/highlights.scm\",\n          \"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/typescript/highlights.scm\",\n        ],\n      },\n    },\n    {\n      filetype: \"markdown\",\n      wasm: \"https://github.com/tree-sitter-grammars/tree-sitter-markdown/releases/download/v0.5.1/tree-sitter-markdown.wasm\",\n      queries: {\n        highlights: [\n          // Using local file to preserve custom modifications\n          \"./assets/markdown/highlights.scm\",\n        ],\n        injections: [\n          \"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/markdown/injections.scm\",\n        ],\n      },\n      injectionMapping: {\n        nodeTypes: {\n          inline: \"markdown_inline\",\n          pipe_table_cell: \"markdown_inline\",\n        },\n        infoStringMap: {\n          javascript: \"javascript\",\n          js: \"javascript\",\n          jsx: \"javascriptreact\",\n          javascriptreact: \"javascriptreact\",\n          typescript: \"typescript\",\n          ts: \"typescript\",\n          tsx: \"typescriptreact\",\n          typescriptreact: \"typescriptreact\",\n          markdown: \"markdown\",\n          md: \"markdown\",\n        },\n      },\n    },\n    {\n      filetype: \"markdown_inline\",\n      wasm: \"https://github.com/tree-sitter-grammars/tree-sitter-markdown/releases/download/v0.5.1/tree-sitter-markdown_inline.wasm\",\n      queries: {\n        highlights: [\n          // NOTE: Based on the last working version of the query, newer versions are adapted to neovim breaking changes\n          // \"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/99ddf573531c4dbe53f743ecbc1595af5eb1d32f/queries/markdown_inline/highlights.scm\",\n          \"./assets/markdown_inline/highlights.scm\",\n        ],\n      },\n    },\n    {\n      filetype: \"zig\",\n      wasm: \"https://github.com/tree-sitter-grammars/tree-sitter-zig/releases/download/v1.1.2/tree-sitter-zig.wasm\",\n      queries: {\n        highlights: [\n          \"https://github.com/nvim-treesitter/nvim-treesitter/raw/refs/heads/master/queries/zig/highlights.scm\",\n        ],\n      },\n    },\n  ],\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/resolve-ft.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { extensionToFiletype, infoStringToFiletype, pathToFiletype } from \"./resolve-ft.js\"\n\ntest(\"pathToFiletype only resolves actual paths\", () => {\n  expect(pathToFiletype(\"tsx\")).toBeUndefined()\n  expect(pathToFiletype(\"components/Button.tsx\")).toBe(\"typescriptreact\")\n})\n\ntest(\"pathToFiletype resolves common extension aliases to parser ids\", () => {\n  expect(pathToFiletype(\"src/index.mjs\")).toBe(\"javascript\")\n  expect(pathToFiletype(\"src/index.cts\")).toBe(\"typescript\")\n  expect(pathToFiletype(\"src/index.mtsx\")).toBe(\"typescriptreact\")\n  expect(pathToFiletype(\"src/module.cc\")).toBe(\"cpp\")\n  expect(pathToFiletype(\"src/module.hxx\")).toBe(\"cpp\")\n  expect(pathToFiletype(\"src/config.hrl\")).toBe(\"erlang\")\n  expect(pathToFiletype(\"src/main.hs\")).toBe(\"haskell\")\n  expect(pathToFiletype(\"src/main.ml\")).toBe(\"ocaml\")\n  expect(pathToFiletype(\"src/main.scala\")).toBe(\"scala\")\n  expect(pathToFiletype(\"src/config.zon\")).toBe(\"zig\")\n  expect(pathToFiletype(\"src/script.sh\")).toBe(\"bash\")\n})\n\ntest(\"pathToFiletype resolves common basenames\", () => {\n  expect(pathToFiletype(\"Dockerfile\")).toBe(\"dockerfile\")\n  expect(pathToFiletype(\"Containerfile\")).toBe(\"dockerfile\")\n  expect(pathToFiletype(\"Makefile\")).toBe(\"make\")\n  expect(pathToFiletype(\"Rakefile\")).toBe(\"ruby\")\n  expect(pathToFiletype(\".bashrc\")).toBe(\"bash\")\n  expect(pathToFiletype(\".vimrc\")).toBe(\"vim\")\n})\n\ntest(\"infoStringToFiletype normalizes markdown fence labels\", () => {\n  expect(infoStringToFiletype(\"tsx\")).toBe(\"typescriptreact\")\n  expect(infoStringToFiletype(\"TSX title=Button.tsx\")).toBe(\"typescriptreact\")\n  expect(infoStringToFiletype(\".jsx\")).toBe(\"javascriptreact\")\n  expect(infoStringToFiletype(\"Button.tsx\")).toBe(\"typescriptreact\")\n  expect(infoStringToFiletype(\"Dockerfile\")).toBe(\"dockerfile\")\n  expect(infoStringToFiletype(\"bash\")).toBe(\"bash\")\n})\n\ntest(\"extensionToFiletype can be extended by consumers\", () => {\n  const previous = extensionToFiletype.get(\"foo\")\n\n  try {\n    extensionToFiletype.set(\"foo\", \"custom\")\n    expect(infoStringToFiletype(\"foo\")).toBe(\"custom\")\n    expect(pathToFiletype(\"example.foo\")).toBe(\"custom\")\n  } finally {\n    if (previous === undefined) {\n      extensionToFiletype.delete(\"foo\")\n    } else {\n      extensionToFiletype.set(\"foo\", previous)\n    }\n  }\n})\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/resolve-ft.ts",
    "content": "import path from \"node:path\"\n\nexport const extensionToFiletype: Map<string, string> = new Map([\n  [\"astro\", \"astro\"],\n  [\"bash\", \"bash\"],\n  [\"c\", \"c\"],\n  [\"cc\", \"cpp\"],\n  [\"cjs\", \"javascript\"],\n  [\"clj\", \"clojure\"],\n  [\"cljs\", \"clojure\"],\n  [\"cljc\", \"clojure\"],\n  [\"cpp\", \"cpp\"],\n  [\"cxx\", \"cpp\"],\n  [\"cs\", \"csharp\"],\n  [\"cts\", \"typescript\"],\n  [\"ctsx\", \"typescriptreact\"],\n  [\"dart\", \"dart\"],\n  [\"diff\", \"diff\"],\n  [\"edn\", \"clojure\"],\n  [\"go\", \"go\"],\n  [\"gemspec\", \"ruby\"],\n  [\"groovy\", \"groovy\"],\n  [\"h\", \"c\"],\n  [\"handlebars\", \"handlebars\"],\n  [\"hbs\", \"handlebars\"],\n  [\"hpp\", \"cpp\"],\n  [\"hxx\", \"cpp\"],\n  [\"h++\", \"cpp\"],\n  [\"hh\", \"cpp\"],\n  [\"hrl\", \"erlang\"],\n  [\"hs\", \"haskell\"],\n  [\"htm\", \"html\"],\n  [\"html\", \"html\"],\n  [\"ini\", \"ini\"],\n  [\"js\", \"javascript\"],\n  [\"jsx\", \"javascriptreact\"],\n  [\"jl\", \"julia\"],\n  [\"json\", \"json\"],\n  [\"ksh\", \"bash\"],\n  [\"kt\", \"kotlin\"],\n  [\"kts\", \"kotlin\"],\n  [\"latex\", \"latex\"],\n  [\"less\", \"less\"],\n  [\"lua\", \"lua\"],\n  [\"markdown\", \"markdown\"],\n  [\"md\", \"markdown\"],\n  [\"mdown\", \"markdown\"],\n  [\"mkd\", \"markdown\"],\n  [\"mjs\", \"javascript\"],\n  [\"ml\", \"ocaml\"],\n  [\"mli\", \"ocaml\"],\n  [\"mts\", \"typescript\"],\n  [\"mtsx\", \"typescriptreact\"],\n  [\"patch\", \"diff\"],\n  [\"php\", \"php\"],\n  [\"pl\", \"perl\"],\n  [\"pm\", \"perl\"],\n  [\"ps1\", \"powershell\"],\n  [\"psm1\", \"powershell\"],\n  [\"py\", \"python\"],\n  [\"pyi\", \"python\"],\n  [\"r\", \"r\"],\n  [\"rb\", \"ruby\"],\n  [\"rake\", \"ruby\"],\n  [\"rs\", \"rust\"],\n  [\"ru\", \"ruby\"],\n  [\"sass\", \"sass\"],\n  [\"sc\", \"scala\"],\n  [\"scala\", \"scala\"],\n  [\"scss\", \"scss\"],\n  [\"sh\", \"bash\"],\n  [\"sql\", \"sql\"],\n  [\"svelte\", \"svelte\"],\n  [\"swift\", \"swift\"],\n  [\"ts\", \"typescript\"],\n  [\"tsx\", \"typescriptreact\"],\n  [\"tex\", \"latex\"],\n  [\"toml\", \"toml\"],\n  [\"vue\", \"vue\"],\n  [\"vim\", \"vim\"],\n  [\"xml\", \"xml\"],\n  [\"xsl\", \"xsl\"],\n  [\"yaml\", \"yaml\"],\n  [\"yml\", \"yaml\"],\n  [\"zig\", \"zig\"],\n  [\"zon\", \"zig\"],\n  [\"zsh\", \"bash\"],\n  [\"c++\", \"cpp\"],\n  [\"erl\", \"erlang\"],\n  [\"exs\", \"elixir\"],\n  [\"ex\", \"elixir\"],\n  [\"elm\", \"elm\"],\n  [\"fsharp\", \"fsharp\"],\n  [\"fs\", \"fsharp\"],\n  [\"fsx\", \"fsharp\"],\n  [\"fsscript\", \"fsharp\"],\n  [\"fsi\", \"fsharp\"],\n  [\"java\", \"java\"],\n  [\"css\", \"css\"],\n])\n\nexport const basenameToFiletype: Map<string, string> = new Map([\n  [\".bash_aliases\", \"bash\"],\n  [\".bash_logout\", \"bash\"],\n  [\".bash_profile\", \"bash\"],\n  [\".bashrc\", \"bash\"],\n  [\".kshrc\", \"bash\"],\n  [\".profile\", \"bash\"],\n  [\".vimrc\", \"vim\"],\n  [\".zlogin\", \"bash\"],\n  [\".zlogout\", \"bash\"],\n  [\".zprofile\", \"bash\"],\n  [\".zshenv\", \"bash\"],\n  [\".zshrc\", \"bash\"],\n  [\"appfile\", \"ruby\"],\n  [\"berksfile\", \"ruby\"],\n  [\"brewfile\", \"ruby\"],\n  [\"cheffile\", \"ruby\"],\n  [\"containerfile\", \"dockerfile\"],\n  [\"dockerfile\", \"dockerfile\"],\n  [\"fastfile\", \"ruby\"],\n  [\"gemfile\", \"ruby\"],\n  [\"gnumakefile\", \"make\"],\n  [\"gvimrc\", \"vim\"],\n  [\"guardfile\", \"ruby\"],\n  [\"makefile\", \"make\"],\n  [\"podfile\", \"ruby\"],\n  [\"rakefile\", \"ruby\"],\n  [\"thorfile\", \"ruby\"],\n  [\"vagrantfile\", \"ruby\"],\n])\n\nfunction normalizeFiletypeToken(value: string): string | undefined {\n  const normalizedValue = value.trim().replace(/^\\./, \"\").toLowerCase()\n  return normalizedValue || undefined\n}\n\nfunction getBasename(value: string): string | undefined {\n  const normalizedValue = value.trim().replaceAll(\"\\\\\", \"/\")\n  if (!normalizedValue) return undefined\n\n  const basename = path.posix.basename(normalizedValue).toLowerCase()\n  return basename || undefined\n}\n\nexport function extToFiletype(extension: string): string | undefined {\n  const normalizedExtension = normalizeFiletypeToken(extension)\n  if (!normalizedExtension) return undefined\n\n  return extensionToFiletype.get(normalizedExtension)\n}\n\nexport function pathToFiletype(path: string): string | undefined {\n  if (typeof path !== \"string\") return undefined\n\n  const basename = getBasename(path)\n  if (!basename) return undefined\n\n  const basenameFiletype = basenameToFiletype.get(basename)\n  if (basenameFiletype) {\n    return basenameFiletype\n  }\n\n  const lastDot = basename.lastIndexOf(\".\")\n  if (lastDot === -1 || lastDot === basename.length - 1) {\n    return undefined\n  }\n\n  const extension = basename.substring(lastDot + 1)\n  return extToFiletype(extension)\n}\n\nexport function infoStringToFiletype(infoString: string): string | undefined {\n  if (typeof infoString !== \"string\") return undefined\n\n  const token = infoString.trim().split(/\\s+/, 1)[0]\n  const directBasenameMatch = basenameToFiletype.get(token.toLowerCase())\n  if (directBasenameMatch) return directBasenameMatch\n\n  const normalizedToken = normalizeFiletypeToken(token)\n  if (!normalizedToken) return undefined\n\n  return (\n    basenameToFiletype.get(normalizedToken) ??\n    pathToFiletype(normalizedToken) ??\n    extToFiletype(normalizedToken) ??\n    normalizedToken\n  )\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter/types.ts",
    "content": "export interface HighlightRange {\n  startCol: number\n  endCol: number\n  group: string\n}\n\nexport interface HighlightResponse {\n  line: number\n  highlights: HighlightRange[]\n  droppedHighlights: HighlightRange[]\n}\n\nexport interface HighlightMeta {\n  isInjection?: boolean\n  injectionLang?: string\n  containsInjection?: boolean\n  conceal?: string | null // Value from (#set! conceal \"...\") predicate\n  concealLines?: string | null // Value from (#set! conceal_lines \"...\") predicate - indicates the whole line should be concealed\n}\n\nexport type SimpleHighlight = [number, number, string, HighlightMeta?]\n\nexport interface InjectionMapping {\n  // Maps tree-sitter node types to target filetypes\n  nodeTypes?: { [nodeType: string]: string }\n  // Maps info string content (e.g., from code blocks) to target filetypes\n  infoStringMap?: { [infoString: string]: string }\n}\n\nexport interface FiletypeParserOptions {\n  filetype: string\n  aliases?: string[]\n  queries: {\n    highlights: string[] // Array of URLs or local file paths to fetch highlight queries from\n    injections?: string[] // Array of URLs or local file paths to fetch injection queries from\n  }\n  wasm: string // URL or local file path to the language parser WASM file\n  injectionMapping?: InjectionMapping // Optional mapping for injection handling\n}\n\nexport interface BufferState {\n  id: number\n  version: number\n  content: string\n  filetype: string\n  hasParser: boolean\n}\n\nexport interface ParsedBuffer extends BufferState {\n  hasParser: true\n}\n\nexport interface TreeSitterClientEvents {\n  \"highlights:response\": [bufferId: number, version: number, highlights: HighlightResponse[]]\n  \"buffer:initialized\": [bufferId: number, hasParser: boolean]\n  \"buffer:disposed\": [bufferId: number]\n  \"worker:log\": [logType: \"log\" | \"error\", message: string]\n  error: [error: string, bufferId?: number]\n  warning: [warning: string, bufferId?: number]\n}\n\nexport interface TreeSitterClientOptions {\n  dataPath: string // Directory for storing downloaded parsers and queries\n  workerPath?: string | URL\n  initTimeout?: number // Timeout in milliseconds for worker initialization, defaults to 10000\n}\n\nexport interface Edit {\n  startIndex: number\n  oldEndIndex: number\n  newEndIndex: number\n  startPosition: { row: number; column: number }\n  oldEndPosition: { row: number; column: number }\n  newEndPosition: { row: number; column: number }\n}\n\nexport interface PerformanceStats {\n  averageParseTime: number\n  parseTimes: number[]\n  averageQueryTime: number\n  queryTimes: number[]\n}\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter-styled-text.test.ts",
    "content": "import { test, expect, beforeAll, afterAll, describe } from \"bun:test\"\nimport { TreeSitterClient } from \"./tree-sitter/client.js\"\nimport { treeSitterToStyledText, treeSitterToTextChunks } from \"./tree-sitter-styled-text.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { RGBA } from \"./RGBA.js\"\nimport { createTextAttributes } from \"../utils.js\"\nimport { tmpdir } from \"os\"\nimport { join } from \"path\"\nimport { mkdir } from \"fs/promises\"\nimport type { SimpleHighlight } from \"./tree-sitter/types.js\"\n\ndescribe(\"TreeSitter Styled Text\", () => {\n  let client: TreeSitterClient\n  let syntaxStyle: SyntaxStyle\n  const dataPath = join(tmpdir(), \"tree-sitter-styled-text-test\")\n\n  beforeAll(async () => {\n    await mkdir(dataPath, { recursive: true })\n    client = new TreeSitterClient({ dataPath })\n    await client.initialize()\n\n    // Create a syntax style similar to common themes\n    syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromInts(255, 255, 255, 255) }, // white\n      keyword: { fg: RGBA.fromInts(255, 100, 100, 255), bold: true }, // red bold\n      string: { fg: RGBA.fromInts(100, 255, 100, 255) }, // green\n      number: { fg: RGBA.fromInts(100, 100, 255, 255) }, // blue\n      function: { fg: RGBA.fromInts(255, 255, 100, 255), italic: true }, // yellow italic\n      comment: { fg: RGBA.fromInts(128, 128, 128, 255), italic: true }, // gray italic\n      variable: { fg: RGBA.fromInts(200, 200, 255, 255) }, // light blue\n      type: { fg: RGBA.fromInts(255, 200, 100, 255) }, // orange\n      \"markup.heading\": { fg: RGBA.fromInts(255, 200, 200, 255), bold: true }, // light red bold\n      \"markup.strong\": { bold: true }, // bold\n      \"markup.italic\": { italic: true }, // italic\n      \"markup.raw\": { fg: RGBA.fromInts(200, 255, 200, 255) }, // light green\n      \"markup.quote\": { fg: RGBA.fromInts(180, 180, 180, 255), italic: true }, // gray italic\n      \"markup.list\": { fg: RGBA.fromInts(255, 200, 100, 255) }, // orange\n    })\n  })\n\n  afterAll(async () => {\n    await client.destroy()\n    syntaxStyle.destroy()\n  })\n\n  test(\"should convert JavaScript code to styled text\", async () => {\n    const jsCode = 'const greeting = \"Hello, world!\";\\nfunction test() { return 42; }'\n\n    const styledText = await treeSitterToStyledText(jsCode, \"javascript\", syntaxStyle, client)\n\n    expect(styledText).toBeDefined()\n\n    const chunks = styledText.chunks\n    expect(chunks.length).toBeGreaterThan(1) // Should have multiple styled chunks\n\n    const chunksWithColor = chunks.filter((chunk) => chunk.fg)\n    expect(chunksWithColor.length).toBeGreaterThan(0) // Some chunks should have colors\n  })\n\n  test(\"should convert TypeScript code to styled text\", async () => {\n    const tsCode = \"interface User {\\n  name: string;\\n  age: number;\\n}\"\n\n    const styledText = await treeSitterToStyledText(tsCode, \"typescript\", syntaxStyle, client)\n\n    expect(styledText).toBeDefined()\n\n    const chunks = styledText.chunks\n    expect(chunks.length).toBeGreaterThan(1)\n\n    const styledChunks = chunks.filter((chunk) => chunk.fg)\n    expect(styledChunks.length).toBeGreaterThan(0)\n  })\n\n  test(\"should handle unsupported filetype gracefully\", async () => {\n    const content = \"some random content\"\n\n    const styledText = await treeSitterToStyledText(content, \"unsupported\", syntaxStyle, client)\n\n    expect(styledText).toBeDefined()\n\n    const chunks = styledText.chunks\n    expect(chunks).toHaveLength(1)\n    expect(chunks[0].text).toBe(content)\n\n    expect(chunks[0].fg).toBeDefined()\n  })\n\n  test(\"should handle empty content\", async () => {\n    const styledText = await treeSitterToStyledText(\"\", \"javascript\", syntaxStyle, client)\n\n    expect(styledText).toBeDefined()\n\n    const chunks = styledText.chunks\n    expect(chunks).toHaveLength(1)\n    expect(chunks[0].text).toBe(\"\")\n  })\n\n  test(\"should handle multiline content correctly\", async () => {\n    const multilineCode = `// This is a comment\nconst value = 123;\nconst text = \"hello\";\nfunction add(a, b) {\n  return a + b;\n}`\n\n    const styledText = await treeSitterToStyledText(multilineCode, \"javascript\", syntaxStyle, client)\n\n    expect(styledText).toBeDefined()\n\n    const chunks = styledText.chunks\n    expect(chunks.length).toBeGreaterThan(5) // Multiple chunks for different elements\n\n    // Should contain newlines\n    const newlineChunks = chunks.filter((chunk) => chunk.text.includes(\"\\n\"))\n    expect(newlineChunks.length).toBeGreaterThan(0)\n  })\n\n  test(\"should preserve original text content\", async () => {\n    const originalCode = 'const test = \"preserve this exact text\";'\n\n    const styledText = await treeSitterToStyledText(originalCode, \"javascript\", syntaxStyle, client)\n\n    const reconstructed = styledText.chunks.map((chunk) => chunk.text).join(\"\")\n    expect(reconstructed).toBe(originalCode)\n  })\n\n  test(\"should apply different styles to different syntax elements\", async () => {\n    const jsCode = \"const number = 42; // comment\"\n\n    const styledText = await treeSitterToStyledText(jsCode, \"javascript\", syntaxStyle, client)\n    const chunks = styledText.chunks\n\n    // Should have some chunks with colors\n    const chunksWithColors = chunks.filter((chunk) => chunk.fg)\n    expect(chunksWithColors.length).toBeGreaterThan(0)\n\n    // Should have some chunks with attributes (bold, italic, etc.)\n    const chunksWithAttributes = chunks.filter((chunk) => chunk.attributes && chunk.attributes > 0)\n    expect(chunksWithAttributes.length).toBeGreaterThan(0)\n  })\n\n  test(\"should handle template literals correctly without duplication\", async () => {\n    const templateLiteralCode = \"console.log(`Total users: ${manager.getUserCount()}`);\"\n\n    const styledText = await treeSitterToStyledText(templateLiteralCode, \"javascript\", syntaxStyle, client)\n    const chunks = styledText.chunks\n\n    // Reconstruct the text from chunks to check for duplication\n    const reconstructed = chunks.map((chunk) => chunk.text).join(\"\")\n\n    expect(reconstructed).toBe(templateLiteralCode)\n\n    expect(chunks.length).toBeGreaterThan(1)\n\n    const styledChunks = chunks.filter((chunk) => chunk.fg)\n    expect(styledChunks.length).toBeGreaterThan(0)\n  })\n\n  test(\"should handle complex template literals with multiple expressions\", async () => {\n    const complexTemplateCode =\n      'console.log(`User: ${user.name}, Age: ${user.age}, Status: ${user.active ? \"active\" : \"inactive\"}`);'\n\n    const styledText = await treeSitterToStyledText(complexTemplateCode, \"javascript\", syntaxStyle, client)\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((chunk) => chunk.text).join(\"\")\n\n    expect(reconstructed).toBe(complexTemplateCode)\n  })\n\n  test(\"should correctly highlight template literal with embedded expressions\", async () => {\n    const templateLiteralCode = \"console.log(`Total users: ${manager.getUserCount()}`);\"\n\n    const result = await client.highlightOnce(templateLiteralCode, \"javascript\")\n\n    expect(result.highlights).toBeDefined()\n    expect(result.highlights!.length).toBeGreaterThan(0)\n\n    const groups = result.highlights!.map(([, , group]) => group)\n    expect(groups).toContain(\"variable\") // console, manager\n    expect(groups).toContain(\"property\") // log, getUserCount\n    expect(groups).toContain(\"string\") // template literal\n    expect(groups).toContain(\"embedded\") // ${...} expression\n    expect(groups).toContain(\"punctuation.bracket\") // (), {}\n\n    const styledText = await treeSitterToStyledText(templateLiteralCode, \"javascript\", syntaxStyle, client)\n    const chunks = styledText.chunks\n\n    expect(chunks.length).toBeGreaterThan(5)\n\n    const reconstructed = chunks.map((chunk) => chunk.text).join(\"\")\n    expect(reconstructed).toBe(templateLiteralCode)\n\n    const styledChunks = chunks.filter((chunk) => chunk.fg !== syntaxStyle.mergeStyles(\"default\").fg)\n    expect(styledChunks.length).toBeGreaterThan(0) // Some chunks should be styled differently\n  })\n\n  test(\"should work with real tree-sitter output containing dot-delimited groups\", async () => {\n    const tsCode = \"interface User { name: string; age?: number; }\"\n\n    const result = await client.highlightOnce(tsCode, \"typescript\")\n    expect(result.highlights).toBeDefined()\n\n    const groups = result.highlights!.map(([, , group]) => group)\n    const dotDelimitedGroups = groups.filter((group) => group.includes(\".\"))\n    expect(dotDelimitedGroups.length).toBeGreaterThan(0)\n\n    const styledText = await treeSitterToStyledText(tsCode, \"typescript\", syntaxStyle, client)\n    const chunks = styledText.chunks\n\n    expect(chunks.length).toBeGreaterThan(1)\n\n    const styledChunks = chunks.filter((chunk) => chunk.fg !== syntaxStyle.mergeStyles(\"default\").fg)\n    expect(styledChunks.length).toBeGreaterThan(0)\n\n    const reconstructed = chunks.map((chunk) => chunk.text).join(\"\")\n    expect(reconstructed).toBe(tsCode)\n  })\n\n  test(\"should resolve styles correctly for dot-delimited groups and multiple overlapping groups\", async () => {\n    // Test the getStyle method directly\n    expect(syntaxStyle.getStyle(\"function.method\")).toEqual(syntaxStyle.getStyle(\"function\"))\n    expect(syntaxStyle.getStyle(\"variable.member\")).toEqual(syntaxStyle.getStyle(\"variable\"))\n    expect(syntaxStyle.getStyle(\"nonexistent.fallback\")).toBeUndefined()\n    expect(syntaxStyle.getStyle(\"function\")).toBeDefined()\n    expect(syntaxStyle.getStyle(\"constructor\")).toBeUndefined() // Should not return Object constructor\n\n    // Test with mock highlights that have multiple groups for same range\n    const mockHighlights: Array<[number, number, string]> = [\n      [0, 4, \"variable.member\"], // should resolve to 'variable' style\n      [0, 4, \"function.method\"], // should resolve to 'function' style (last valid)\n      [0, 4, \"nonexistent\"], // undefined, should not override\n      [4, 8, \"keyword\"], // should resolve to 'keyword' style\n    ]\n\n    const content = \"testfunc\"\n    const chunks = treeSitterToTextChunks(content, mockHighlights, syntaxStyle)\n\n    expect(chunks.length).toBe(2) // Two highlight ranges, no gaps\n\n    // First chunk [0,4] should have function style (last valid style)\n    const functionStyle = syntaxStyle.getStyle(\"function\")!\n    expect(chunks[0].text).toBe(\"test\")\n    expect(chunks[0].fg).toEqual(functionStyle.fg)\n    expect(chunks[0].attributes).toBe(\n      createTextAttributes({\n        bold: functionStyle.bold,\n        italic: functionStyle.italic,\n        underline: functionStyle.underline,\n        dim: functionStyle.dim,\n      }),\n    )\n\n    // Second chunk [4,8] should have keyword style\n    const keywordStyle = syntaxStyle.getStyle(\"keyword\")!\n    expect(chunks[1].text).toBe(\"func\")\n    expect(chunks[1].fg).toEqual(keywordStyle.fg)\n    expect(chunks[1].attributes).toBe(\n      createTextAttributes({\n        bold: keywordStyle.bold,\n        italic: keywordStyle.italic,\n        underline: keywordStyle.underline,\n        dim: keywordStyle.dim,\n      }),\n    )\n  })\n\n  test(\"should handle constructor group correctly\", async () => {\n    expect(syntaxStyle.getStyle(\"constructor\")).toBeUndefined()\n\n    const mockHighlights: Array<[number, number, string]> = [\n      [0, 11, \"variable.member\"], // should resolve to 'variable' style\n      [0, 11, \"constructor\"], // should resolve to undefined\n      [0, 11, \"function.method\"], // should resolve to 'function' style (last valid)\n    ]\n\n    const content = \"constructor\"\n    const chunks = treeSitterToTextChunks(content, mockHighlights, syntaxStyle)\n\n    expect(chunks.length).toBe(1)\n\n    const functionStyle = syntaxStyle.getStyle(\"function\")!\n    expect(chunks[0].text).toBe(\"constructor\")\n    expect(chunks[0].fg).toEqual(functionStyle.fg)\n    expect(chunks[0].attributes).toBe(\n      createTextAttributes({\n        bold: functionStyle.bold,\n        italic: functionStyle.italic,\n        underline: functionStyle.underline,\n        dim: functionStyle.dim,\n      }),\n    )\n  })\n\n  test(\"should handle markdown with TypeScript injection - suppress parent block styles\", async () => {\n    const markdownCode = `\\`\\`\\`typescript\nconst x: string = \"hello\";\n\\`\\`\\``\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: false }, // Disable concealing to test text preservation\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n    expect(reconstructed).toBe(markdownCode)\n\n    const tsStart = markdownCode.indexOf(\"const\")\n    const tsEnd = markdownCode.lastIndexOf(\";\") + 1\n\n    let currentPos = 0\n    const tsChunks: typeof chunks = []\n    for (const chunk of chunks) {\n      const chunkStart = currentPos\n      const chunkEnd = currentPos + chunk.text.length\n      if (chunkStart >= tsStart && chunkEnd <= tsEnd) {\n        tsChunks.push(chunk)\n      }\n      currentPos = chunkEnd\n    }\n\n    // and NOT the parent markup.raw.block background\n    expect(tsChunks.length).toBeGreaterThan(0)\n\n    const hasKeywordStyle = tsChunks.some((chunk) => {\n      const keywordStyle = syntaxStyle.getStyle(\"keyword\")\n      return (\n        keywordStyle &&\n        chunk.fg &&\n        keywordStyle.fg &&\n        chunk.fg.r === keywordStyle.fg.r &&\n        chunk.fg.g === keywordStyle.fg.g &&\n        chunk.fg.b === keywordStyle.fg.b\n      )\n    })\n    expect(hasKeywordStyle).toBe(true)\n  })\n\n  test(\"should conceal backticks in inline code\", async () => {\n    const markdownCode = \"Some text with `inline code` here.\"\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n    expect(reconstructed).not.toContain(\"`\")\n    expect(reconstructed).toContain(\"inline code\")\n    expect(reconstructed).toContain(\"Some text with \")\n    expect(reconstructed).toContain(\" here.\")\n  })\n\n  test(\"should conceal bold markers\", async () => {\n    const markdownCode = \"Some **bold** text\"\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n    expect(reconstructed).not.toContain(\"**\")\n    expect(reconstructed).not.toContain(\"*\")\n    expect(reconstructed).toContain(\"bold\")\n    expect(reconstructed).toContain(\"Some \")\n    expect(reconstructed).toContain(\" text\")\n  })\n\n  test(\"should conceal link syntax but keep text and URL\", async () => {\n    const markdownCode = \"[Link text](https://example.com)\"\n\n    const result = await client.highlightOnce(markdownCode, \"markdown\")\n    expect(result.highlights).toBeDefined()\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n\n    expect(reconstructed).not.toContain(\"[\")\n    expect(reconstructed).not.toContain(\"]\")\n    expect(reconstructed).toContain(\"(\")\n    expect(reconstructed).toContain(\")\")\n\n    expect(reconstructed).toContain(\"Link text\")\n    expect(reconstructed).toContain(\"https://example.com\")\n\n    expect(reconstructed).toBe(\"Link text (https://example.com)\")\n  })\n\n  test(\"should conceal code block delimiters and language info\", async () => {\n    const markdownCode = `\\`\\`\\`typescript\nconst x: string = \"hello\";\n\\`\\`\\``\n\n    const result = await client.highlightOnce(markdownCode, \"markdown\")\n    expect(result.highlights).toBeDefined()\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n\n    expect(reconstructed).toContain(\"const x\")\n    expect(reconstructed).toContain(\"hello\")\n\n    expect(reconstructed).not.toContain(\"typescript\")\n\n    expect(reconstructed.startsWith(\"const\")).toBe(true)\n\n    expect(reconstructed.split(\"\\n\").filter((l) => l.trim() === \"\").length).toBeLessThanOrEqual(1)\n  })\n\n  test(\"should handle overlapping highlights with specificity resolution\", async () => {\n    const mockHighlights: SimpleHighlight[] = [\n      [0, 10, \"variable\"],\n      [0, 10, \"variable.member\"], // More specific, should win\n      [0, 10, \"type\"],\n      [11, 16, \"keyword\"],\n      [11, 16, \"keyword.coroutine\"], // More specific, should win\n    ]\n\n    const content = \"identifier const\"\n    // \"identifier\" = indices 0-9 (10 chars)\n    // \" \" = index 10 (1 char)\n    // \"const\" = indices 11-15 (5 chars)\n    const chunks = treeSitterToTextChunks(content, mockHighlights, syntaxStyle)\n\n    expect(chunks.length).toBe(3) // \"identifier\", \" \", \"const\"\n\n    const variableStyle = syntaxStyle.getStyle(\"variable\")!\n    expect(chunks[0].text).toBe(\"identifier\")\n    expect(chunks[0].fg).toEqual(variableStyle.fg)\n\n    expect(chunks[1].text).toBe(\" \")\n\n    const keywordStyle = syntaxStyle.getStyle(\"keyword\")!\n    expect(chunks[2].text).toBe(\"const\")\n    expect(chunks[2].fg).toEqual(keywordStyle.fg)\n  })\n\n  test(\"should not conceal when conceal option is disabled\", async () => {\n    const markdownCode = \"Some text with `inline code` here.\"\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: false },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n    expect(reconstructed).toContain(\"`\")\n    expect(reconstructed).toBe(markdownCode)\n  })\n\n  test(\"should handle complex markdown with multiple features\", async () => {\n    const markdownCode = `# Heading\n\nSome **bold** text and \\`code\\`.\n\n\\`\\`\\`typescript\nconst hello: string = \"world\";\n\\`\\`\\`\n\n[Link](https://example.com)`\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n\n    expect(reconstructed).toContain(\"Heading\")\n    expect(reconstructed).toContain(\"bold\")\n    expect(reconstructed).toContain(\"code\")\n    expect(reconstructed).toContain(\"const hello\")\n    expect(reconstructed).toContain(\"Link\")\n\n    expect(reconstructed).not.toContain(\"**\")\n  })\n\n  test(\"should correctly handle ranges after concealed text\", async () => {\n    const markdownCode = \"Text with **bold** and *italic* markers.\"\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n\n    expect(reconstructed).toContain(\"Text with \")\n    expect(reconstructed).toContain(\"bold\")\n    expect(reconstructed).toContain(\" and \")\n    expect(reconstructed).toContain(\"italic\")\n    expect(reconstructed).toContain(\" markers.\")\n\n    expect(reconstructed).not.toContain(\"**\")\n    expect(reconstructed).not.toContain(\"*\")\n\n    expect(reconstructed).toMatch(/Text with \\w+ and \\w+ markers\\./)\n  })\n\n  test(\"should conceal heading markers and preserve heading styling\", async () => {\n    const markdownCode = \"## Heading 2\"\n\n    const result = await client.highlightOnce(markdownCode, \"markdown\")\n\n    const hasAnyConceals = result.highlights!.some(([, , , meta]) => meta?.conceal !== undefined)\n    expect(hasAnyConceals).toBe(true) // Should have conceal on the ## marker\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n\n    expect(reconstructed).toContain(\"Heading 2\")\n\n    expect(reconstructed).not.toContain(\"##\")\n    expect(reconstructed).not.toContain(\"#\")\n\n    expect(reconstructed).toBe(\"Heading 2\")\n\n    expect(reconstructed.startsWith(\" \")).toBe(false)\n    expect(reconstructed.startsWith(\"Heading\")).toBe(true)\n\n    // Note: Heading styling depends on having the parent markup.heading style\n    // properly cascade to child text. In a real application with proper theme setup,\n    // the heading text will be styled correctly as shown in other tests.\n  })\n\n  test(\"should not create empty lines when concealing code block delimiters\", async () => {\n    const markdownCode = `\\`\\`\\`typescript\nconst x = 1;\nconst y = 2;\n\\`\\`\\``\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n\n    const reconstructed = styledText.chunks.map((c) => c.text).join(\"\")\n\n    const originalLines = markdownCode.split(\"\\n\")\n    expect(originalLines.length).toBe(4)\n\n    // (The ```typescript line is completely removed including its newline)\n    const reconstructedLines = reconstructed.split(\"\\n\")\n    expect(reconstructedLines.length).toBe(3)\n\n    expect(reconstructedLines[0]).toBe(\"const x = 1;\")\n\n    expect(reconstructed.startsWith(\"\\n\")).toBe(false)\n    expect(reconstructed.startsWith(\"const\")).toBe(true)\n  })\n\n  test(\"should conceal closing triple backticks in plain code block (no injection)\", async () => {\n    const markdownCode = `\\`\\`\\`\nconst msg = \"hello\";\n\\`\\`\\``\n\n    const result = await client.highlightOnce(markdownCode, \"markdown\")\n    expect(result.highlights).toBeDefined()\n\n    const closingBackticksHighlight = result.highlights!.find(([start, end, , meta]) => {\n      const text = markdownCode.slice(start, end)\n      return text === \"```\" && start > 10 && meta?.conceal !== undefined\n    })\n\n    expect(closingBackticksHighlight).toBeDefined()\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n\n    expect(reconstructed).not.toContain(\"```\")\n    expect(reconstructed).toContain(\"const msg\")\n  })\n\n  test(\"should conceal closing triple backticks when they are the last content (with TypeScript injection)\", async () => {\n    const markdownCode = `\\`\\`\\`typescript\nconst msg = \"hello\";\n\\`\\`\\``\n\n    const result = await client.highlightOnce(markdownCode, \"markdown\")\n    expect(result.highlights).toBeDefined()\n\n    const closingBackticksHighlights = result.highlights!.filter(([start, end]) => {\n      const text = markdownCode.slice(start, end)\n      return start > 30 && text.includes(\"`\")\n    })\n\n    const hasClosingConceal = closingBackticksHighlights.some(([, , , meta]) => meta?.conceal !== undefined)\n    expect(hasClosingConceal).toBe(true)\n\n    const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n      conceal: { enabled: true },\n    })\n    const chunks = styledText.chunks\n\n    const reconstructed = chunks.map((c) => c.text).join(\"\")\n\n    expect(reconstructed).not.toContain(\"```\")\n    expect(reconstructed).toContain(\"const msg\")\n    expect(reconstructed).toContain(\"hello\")\n\n    expect(reconstructed.endsWith(\"```\")).toBe(false)\n    expect(reconstructed.endsWith(\"`\")).toBe(false)\n  })\n\n  describe(\"Markdown highlighting comprehensive coverage\", () => {\n    test(\"headings should have full styling applied\", async () => {\n      const markdownCode = `# Heading 1\n## Heading 2\n### Heading 3`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const groups = result.highlights!.map(([, , group]) => group)\n      expect(groups).toContain(\"markup.heading.1\")\n      expect(groups).toContain(\"markup.heading.2\")\n      expect(groups).toContain(\"markup.heading.3\")\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: false }, // Disable concealing to test text preservation\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).toBe(markdownCode)\n\n      const hashOrHeadingChunks = chunks.filter((chunk) => chunk.text.includes(\"#\") || /heading/i.test(chunk.text))\n      expect(hashOrHeadingChunks.length).toBeGreaterThan(0)\n\n      const headingGroups = groups.filter((g) => g.includes(\"markup.heading\"))\n      expect(headingGroups.length).toBeGreaterThan(0)\n    })\n\n    test(\"inline raw blocks (code) should be styled\", async () => {\n      const markdownCode = \"Some text with `inline code` here.\"\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const groups = result.highlights!.map(([, , group]) => group)\n      const hasCodeGroup = groups.some((g) => g.includes(\"markup.raw\") || g.includes(\"code\"))\n      expect(hasCodeGroup).toBe(true)\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: false },\n      })\n      const chunks = styledText.chunks\n\n      const codeChunks = chunks.filter((c) => c.text.includes(\"inline\") || c.text.includes(\"code\"))\n      expect(codeChunks.length).toBeGreaterThan(0)\n\n      const defaultStyle = syntaxStyle.mergeStyles(\"default\")\n      const styledCodeChunks = codeChunks.filter((c) => c.fg !== defaultStyle.fg || c.attributes !== 0)\n      expect(styledCodeChunks.length).toBeGreaterThan(0)\n    })\n\n    test(\"quotes should be styled correctly\", async () => {\n      const markdownCode = `> This is a quote\n> Another line`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const groups = result.highlights!.map(([, , group]) => group)\n      const hasQuoteGroup = groups.some((g) => g.includes(\"quote\"))\n      expect(hasQuoteGroup).toBe(true)\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client)\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).toBe(markdownCode)\n    })\n\n    test(\"italic text should be styled in all places\", async () => {\n      const markdownCode = `*italic* text in paragraph\n\n# *italic in heading*\n\n- *italic in list*`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const groups = result.highlights!.map(([, , group]) => group)\n      const hasItalicGroup = groups.some((g) => g.includes(\"italic\") || g.includes(\"emphasis\"))\n      expect(hasItalicGroup).toBe(true)\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: true },\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      const asteriskCount = (reconstructed.match(/\\*/g) || []).length\n      const originalAsteriskCount = (markdownCode.match(/\\*/g) || []).length\n      expect(asteriskCount).toBeLessThan(originalAsteriskCount)\n    })\n\n    test(\"bold text should work in all contexts\", async () => {\n      const markdownCode = `**bold** text in paragraph\n\n# **bold in heading**\n\n- **bold in list**\n\n> **bold in quote**`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const groups = result.highlights!.map(([, , group]) => group)\n      const hasBoldGroup = groups.some((g) => g.includes(\"strong\") || g.includes(\"bold\"))\n      expect(hasBoldGroup).toBe(true)\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: true },\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).not.toContain(\"**\")\n      expect(reconstructed).toContain(\"bold\")\n    })\n\n    test(\"TypeScript code block should not contain parent markup.raw.block fragments between syntax ranges\", async () => {\n      const markdownCode = `\\`\\`\\`typescript\nconst greeting: string = \"hello\";\nfunction test() { return 42; }\n\\`\\`\\``\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const hasInjection = result.highlights!.some(([, , , meta]) => meta?.injectionLang === \"typescript\")\n      expect(hasInjection).toBe(true)\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: false }, // Disable concealing to test text preservation\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).toBe(markdownCode)\n\n      const tsCodeStart = markdownCode.indexOf(\"\\n\") + 1 // After first ```typescript\\n\n      const tsCodeEnd = markdownCode.lastIndexOf(\"\\n```\") // Before last \\n```\n\n      let currentPos = 0\n      const tsChunks: typeof chunks = []\n      for (const chunk of chunks) {\n        const chunkStart = currentPos\n        const chunkEnd = currentPos + chunk.text.length\n        if (chunkEnd > tsCodeStart && chunkStart < tsCodeEnd) {\n          tsChunks.push(chunk)\n        }\n        currentPos = chunkEnd\n      }\n\n      expect(tsChunks.length).toBeGreaterThan(0)\n\n      // (keyword, type, string, etc.) and NOT markup.raw.block background\n      const keywordStyle = syntaxStyle.getStyle(\"keyword\")\n      const stringStyle = syntaxStyle.getStyle(\"string\")\n      const typeStyle = syntaxStyle.getStyle(\"type\")\n\n      const hasKeywordStyle = tsChunks.some((chunk) => {\n        return (\n          keywordStyle &&\n          chunk.fg &&\n          keywordStyle.fg &&\n          chunk.fg.r === keywordStyle.fg.r &&\n          chunk.fg.g === keywordStyle.fg.g &&\n          chunk.fg.b === keywordStyle.fg.b\n        )\n      })\n\n      const hasStringStyle = tsChunks.some((chunk) => {\n        return (\n          stringStyle &&\n          chunk.fg &&\n          stringStyle.fg &&\n          chunk.fg.r === stringStyle.fg.r &&\n          chunk.fg.g === stringStyle.fg.g &&\n          chunk.fg.b === stringStyle.fg.b\n        )\n      })\n\n      expect(hasKeywordStyle || hasStringStyle).toBe(true)\n\n      const defaultStyle = syntaxStyle.mergeStyles(\"default\")\n\n      for (const chunk of tsChunks) {\n        // 1. TypeScript-specific styling (keyword, string, type, etc.)\n        // 2. Default styling (for whitespace, punctuation)\n        // 3. NOT markup.raw.block background (which would be wrong)\n\n        // we verify that chunks are either styled or default\n        const isStyled = chunk.fg !== defaultStyle.fg || chunk.attributes !== 0\n        const isDefault = chunk.fg === defaultStyle.fg\n\n        expect(isStyled || isDefault).toBe(true)\n      }\n    })\n\n    test(\"mixed formatting (bold + italic) should work\", async () => {\n      const markdownCode = \"***bold and italic*** text\"\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: true },\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).not.toContain(\"***\")\n      expect(reconstructed).toContain(\"bold and italic\")\n    })\n\n    test(\"inline code in headings should be styled\", async () => {\n      const markdownCode = \"# Heading with `code` inside\"\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: false },\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).toBe(markdownCode)\n\n      const groups = result.highlights!.map(([, , group]) => group)\n      expect(groups.some((g) => g.includes(\"heading\"))).toBe(true)\n      expect(groups.some((g) => g.includes(\"markup.raw\") || g.includes(\"code\"))).toBe(true)\n    })\n\n    test(\"bold and italic in lists should work\", async () => {\n      const markdownCode = `- **bold item**\n- *italic item*\n- normal item`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: true },\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).toContain(\"bold item\")\n      expect(reconstructed).toContain(\"italic item\")\n      expect(reconstructed).not.toContain(\"**\")\n    })\n\n    test(\"code blocks with different languages should suppress parent styles\", async () => {\n      const markdownCode = `\\`\\`\\`javascript\nconst x = 42;\n\\`\\`\\`\n\n\\`\\`\\`typescript\nconst y: number = 42;\n\\`\\`\\``\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: false }, // Disable concealing to test text preservation\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).toBe(markdownCode)\n\n      const jsInjection = result.highlights!.some(([, , , meta]) => meta?.injectionLang === \"javascript\")\n      const tsInjection = result.highlights!.some(([, , , meta]) => meta?.injectionLang === \"typescript\")\n\n      expect(jsInjection || tsInjection).toBe(true)\n    })\n\n    test(\"complex nested markdown structures\", async () => {\n      const markdownCode = `# Main Heading\n\n> This is a quote with **bold** and *italic* and \\`code\\`.\n\n## Sub Heading\n\n- List item with **bold**\n- Another item with \\`inline code\\`\n\n\\`\\`\\`typescript\n// Comment in code\nconst value = \"string\";\n\\`\\`\\`\n\nNormal paragraph with [link](https://example.com).`\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n      expect(result.highlights!.length).toBeGreaterThan(10)\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", syntaxStyle, client, {\n        conceal: { enabled: true },\n      })\n      const chunks = styledText.chunks\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n\n      expect(reconstructed).toContain(\"Main Heading\")\n      expect(reconstructed).toContain(\"Sub Heading\")\n      expect(reconstructed).toContain(\"quote\")\n      expect(reconstructed).toContain(\"bold\")\n      expect(reconstructed).toContain(\"italic\")\n      expect(reconstructed).toContain(\"code\")\n      expect(reconstructed).toContain(\"const value\")\n      expect(reconstructed).toContain(\"link\")\n\n      expect(reconstructed).not.toContain(\"**\")\n\n      const defaultStyle = syntaxStyle.mergeStyles(\"default\")\n      const styledChunks = chunks.filter((c) => c.fg !== defaultStyle.fg || c.attributes !== 0)\n      expect(styledChunks.length).toBeGreaterThan(5)\n    })\n  })\n\n  describe(\"Style Inheritance\", () => {\n    test(\"should merge styles from nested highlights with child overriding parent\", () => {\n      const mockHighlights: SimpleHighlight[] = [\n        [0, 20, \"markup.link\"], // Parent: entire link with underline\n        [1, 11, \"markup.link.label\"], // Child: label with different color\n        [13, 19, \"markup.link.url\"], // Child: url with different color\n      ]\n\n      const testStyle = SyntaxStyle.fromStyles({\n        default: { fg: RGBA.fromInts(255, 255, 255, 255) },\n        \"markup.link\": { fg: RGBA.fromInts(100, 100, 255, 255), underline: true }, // Blue underlined\n        \"markup.link.label\": { fg: RGBA.fromInts(165, 214, 255, 255) }, // Light blue (no underline specified)\n        \"markup.link.url\": { fg: RGBA.fromInts(88, 166, 255, 255) }, // Different blue (no underline specified)\n      })\n\n      const content = \"[Link text](url)\"\n\n      const labelStyle = testStyle.getStyle(\"markup.link.label\")!\n      const urlStyle = testStyle.getStyle(\"markup.link.url\")!\n\n      const chunks = treeSitterToTextChunks(content, mockHighlights, testStyle)\n\n      testStyle.destroy()\n\n      expect(chunks.length).toBeGreaterThan(0)\n\n      let currentPos = 0\n      const labelChunks: typeof chunks = []\n      const urlChunks: typeof chunks = []\n\n      for (const chunk of chunks) {\n        const chunkStart = currentPos\n        const chunkEnd = currentPos + chunk.text.length\n\n        // Label is at [1, 11] - \"Link text\"\n        if (chunkStart >= 1 && chunkStart < 11 && chunk.text.length > 0) {\n          labelChunks.push(chunk)\n        }\n\n        // URL is at [13, 19] - \"url\"\n        if (chunkStart >= 13 && chunkStart < 19 && chunk.text.length > 0) {\n          urlChunks.push(chunk)\n        }\n\n        currentPos = chunkEnd\n      }\n\n      expect(labelChunks.length).toBeGreaterThan(0)\n      expect(urlChunks.length).toBeGreaterThan(0)\n\n      const underlineAttr = createTextAttributes({ underline: true })\n      for (const chunk of [...labelChunks, ...urlChunks]) {\n        expect(chunk.attributes).toBe(underlineAttr)\n      }\n\n      for (const chunk of labelChunks) {\n        expect(chunk.fg?.r).toBeCloseTo(labelStyle.fg!.r, 2)\n        expect(chunk.fg?.g).toBeCloseTo(labelStyle.fg!.g, 2)\n        expect(chunk.fg?.b).toBeCloseTo(labelStyle.fg!.b, 2)\n      }\n\n      for (const chunk of urlChunks) {\n        expect(chunk.fg?.r).toBeCloseTo(urlStyle.fg!.r, 2)\n        expect(chunk.fg?.g).toBeCloseTo(urlStyle.fg!.g, 2)\n        expect(chunk.fg?.b).toBeCloseTo(urlStyle.fg!.b, 2)\n      }\n    })\n\n    test(\"should merge multiple overlapping styles with correct priority\", () => {\n      const mockHighlights: SimpleHighlight[] = [\n        [0, 10, \"text\"], // Base style\n        [0, 10, \"text.special\"], // More specific: adds bold\n        [0, 10, \"text.special.highlighted\"], // Most specific: adds underline\n      ]\n\n      const testStyle = SyntaxStyle.fromStyles({\n        default: { fg: RGBA.fromInts(255, 255, 255, 255) },\n        text: { fg: RGBA.fromInts(200, 200, 200, 255) }, // Gray\n        \"text.special\": { bold: true }, // Add bold, no color change\n        \"text.special.highlighted\": { underline: true, fg: RGBA.fromInts(255, 255, 100, 255) }, // Add underline and yellow\n      })\n\n      const content = \"test text \"\n      const chunks = treeSitterToTextChunks(content, mockHighlights, testStyle)\n\n      testStyle.destroy()\n\n      expect(chunks.length).toBeGreaterThan(0)\n\n      const chunk = chunks[0]\n\n      expect(chunk.fg?.r).toBeCloseTo(1.0, 2)\n      expect(chunk.fg?.g).toBeCloseTo(1.0, 2)\n      expect(chunk.fg?.b).toBeCloseTo(100 / 255, 2)\n\n      const expectedAttributes = createTextAttributes({ bold: true, underline: true })\n      expect(chunk.attributes).toBe(expectedAttributes)\n    })\n\n    test(\"should handle style inheritance when parent only sets attributes\", () => {\n      const mockHighlights: SimpleHighlight[] = [\n        [0, 15, \"container\"], // Parent: only underline\n        [0, 5, \"container.part1\"], // Child: only color\n        [5, 10, \"container.part2\"], // Child: different color\n        [10, 15, \"container.part3\"], // Child: yet another color\n      ]\n\n      const testStyle = SyntaxStyle.fromStyles({\n        default: { fg: RGBA.fromInts(255, 255, 255, 255) },\n        container: { underline: true }, // Only underline, no color\n        \"container.part1\": { fg: RGBA.fromInts(255, 100, 100, 255) }, // Red\n        \"container.part2\": { fg: RGBA.fromInts(100, 255, 100, 255) }, // Green\n        \"container.part3\": { fg: RGBA.fromInts(100, 100, 255, 255) }, // Blue\n      })\n\n      const content = \"part1part2part3\"\n      const chunks = treeSitterToTextChunks(content, mockHighlights, testStyle)\n\n      testStyle.destroy()\n\n      expect(chunks.length).toBe(3)\n\n      const underlineAttr = createTextAttributes({ underline: true })\n      for (const chunk of chunks) {\n        expect(chunk.attributes).toBe(underlineAttr)\n      }\n\n      expect(chunks[0].fg?.r).toBeCloseTo(1.0, 2) // 255 / 255\n      expect(chunks[0].fg?.g).toBeCloseTo(100 / 255, 2)\n      expect(chunks[0].fg?.b).toBeCloseTo(100 / 255, 2)\n\n      expect(chunks[1].fg?.r).toBeCloseTo(100 / 255, 2)\n      expect(chunks[1].fg?.g).toBeCloseTo(1.0, 2) // 255 / 255\n      expect(chunks[1].fg?.b).toBeCloseTo(100 / 255, 2)\n\n      expect(chunks[2].fg?.r).toBeCloseTo(100 / 255, 2)\n      expect(chunks[2].fg?.g).toBeCloseTo(100 / 255, 2)\n      expect(chunks[2].fg?.b).toBeCloseTo(1.0, 2) // 255 / 255\n    })\n\n    test(\"should handle markdown link with realistic tree-sitter output\", async () => {\n      const markdownCode = \"[Label](url)\"\n\n      const result = await client.highlightOnce(markdownCode, \"markdown\")\n      expect(result.highlights).toBeDefined()\n\n      // IMPORTANT: Tree-sitter markdown parser emits:\n      // - markup.link ONLY for brackets/parens: \"[\", \"]\", \"(\", \")\"\n      // - markup.link.label ONLY for the label text: \"Label\" (not nested under markup.link!)\n      // - markup.link.url for the URL text: \"url\" (ALONG WITH markup.link as sibling)\n      //\n      // This means label does NOT inherit from markup.link because it's not a child range!\n      // Therefore, if you want label underlined, you must specify it explicitly.\n\n      const labelHighlights = result.highlights!.filter(\n        ([start, end, group]) => group === \"markup.link.label\" && markdownCode.slice(start, end) === \"Label\",\n      )\n      expect(labelHighlights.length).toBe(1)\n\n      const labelStart = labelHighlights[0][0]\n      const labelEnd = labelHighlights[0][1]\n      const labelHasParentLink = result.highlights!.some(\n        ([start, end, group]) => group === \"markup.link\" && start === labelStart && end === labelEnd,\n      )\n      expect(labelHasParentLink).toBe(false) // Confirms label is NOT nested\n\n      const linkStyle = SyntaxStyle.fromStyles({\n        default: { fg: RGBA.fromInts(255, 255, 255, 255) },\n        \"markup.link\": { underline: true }, // Brackets and parens\n        \"markup.link.label\": { fg: RGBA.fromInts(165, 214, 255, 255), underline: true }, // Must set underline!\n        \"markup.link.url\": { fg: RGBA.fromInts(88, 166, 255, 255), underline: true }, // Must set underline!\n      })\n\n      const styledText = await treeSitterToStyledText(markdownCode, \"markdown\", linkStyle, client, {\n        conceal: { enabled: false },\n      })\n      const chunks = styledText.chunks\n\n      linkStyle.destroy()\n\n      const reconstructed = chunks.map((c) => c.text).join(\"\")\n      expect(reconstructed).toBe(markdownCode)\n\n      const labelChunk = chunks.find((c) => c.text === \"Label\")\n      const urlChunk = chunks.find((c) => c.text === \"url\")\n\n      expect(labelChunk).toBeDefined()\n      expect(urlChunk).toBeDefined()\n\n      const underlineAttr = createTextAttributes({ underline: true })\n      expect(labelChunk!.attributes).toBe(underlineAttr)\n      expect(urlChunk!.attributes).toBe(underlineAttr)\n\n      expect(labelChunk!.fg?.r).toBeCloseTo(165 / 255, 2)\n      expect(urlChunk!.fg?.r).toBeCloseTo(88 / 255, 2)\n    })\n\n    test(\"should preserve original behavior for non-overlapping highlights\", () => {\n      const mockHighlights: SimpleHighlight[] = [\n        [0, 5, \"keyword\"], // \"const\"\n        [6, 11, \"string\"], // \"'str'\"\n        [12, 15, \"number\"], // \"123\"\n      ]\n\n      const testStyle = SyntaxStyle.fromStyles({\n        default: { fg: RGBA.fromInts(255, 255, 255, 255) },\n        keyword: { fg: RGBA.fromInts(255, 100, 100, 255), bold: true },\n        string: { fg: RGBA.fromInts(100, 255, 100, 255) },\n        number: { fg: RGBA.fromInts(100, 100, 255, 255) },\n      })\n\n      const content = \"const 'str' 123\"\n      const chunks = treeSitterToTextChunks(content, mockHighlights, testStyle)\n\n      testStyle.destroy()\n\n      expect(chunks.length).toBe(5)\n\n      expect(chunks[0].text).toBe(\"const\")\n      expect(chunks[0].fg?.r).toBeCloseTo(1.0, 2) // 255 / 255\n      expect(chunks[0].attributes).toBe(createTextAttributes({ bold: true }))\n\n      expect(chunks[1].text).toBe(\" \")\n\n      expect(chunks[2].text).toBe(\"'str'\")\n      expect(chunks[2].fg?.g).toBeCloseTo(1.0, 2) // 255 / 255\n\n      expect(chunks[3].text).toBe(\" \")\n\n      expect(chunks[4].text).toBe(\"123\")\n      expect(chunks[4].fg?.b).toBeCloseTo(1.0, 2) // 255 / 255\n    })\n\n    test(\"should demonstrate when inheritance works vs when it does not\", () => {\n      const nestedHighlights: SimpleHighlight[] = [\n        [0, 10, \"parent\"], // Parent covers entire range\n        [2, 8, \"parent.child\"], // Child is INSIDE parent\n      ]\n\n      const nestedStyle = SyntaxStyle.fromStyles({\n        default: { fg: RGBA.fromInts(255, 255, 255, 255) },\n        parent: { underline: true },\n        \"parent.child\": { fg: RGBA.fromInts(200, 100, 100, 255) }, // No underline specified\n      })\n\n      const nestedContent = \"0123456789\"\n      const nestedChunks = treeSitterToTextChunks(nestedContent, nestedHighlights, nestedStyle)\n\n      nestedStyle.destroy()\n\n      const childChunk = nestedChunks.find((c) => c.text.includes(\"234567\"))\n      expect(childChunk).toBeDefined()\n      expect(childChunk!.attributes).toBe(createTextAttributes({ underline: true }))\n      expect(childChunk!.fg?.r).toBeCloseTo(200 / 255, 2)\n\n      const siblingHighlights: SimpleHighlight[] = [\n        [0, 5, \"typeA\"], // First range\n        [5, 10, \"typeB\"], // Second range (NOT nested)\n      ]\n\n      const siblingStyle = SyntaxStyle.fromStyles({\n        default: { fg: RGBA.fromInts(255, 255, 255, 255) },\n        typeA: { underline: true, fg: RGBA.fromInts(100, 100, 255, 255) },\n        typeB: { fg: RGBA.fromInts(255, 100, 100, 255) }, // No underline\n      })\n\n      const siblingContent = \"0123456789\"\n      const siblingChunks = treeSitterToTextChunks(siblingContent, siblingHighlights, siblingStyle)\n\n      siblingStyle.destroy()\n\n      expect(siblingChunks.length).toBe(2)\n\n      expect(siblingChunks[0].attributes).toBe(createTextAttributes({ underline: true }))\n\n      expect(siblingChunks[1].attributes).toBe(0) // No attributes\n      expect(siblingChunks[1].fg?.r).toBeCloseTo(255 / 255, 2)\n    })\n\n    test(\"should handle child style completely overriding parent attributes\", () => {\n      const mockHighlights: SimpleHighlight[] = [\n        [0, 10, \"parent\"],\n        [0, 10, \"parent.child\"],\n      ]\n\n      const testStyle = SyntaxStyle.fromStyles({\n        default: { fg: RGBA.fromInts(255, 255, 255, 255) },\n        parent: { bold: true, italic: true, underline: true },\n        \"parent.child\": { bold: false, fg: RGBA.fromInts(200, 200, 200, 255) }, // Override bold, set color\n      })\n\n      const content = \"test text \"\n      const chunks = treeSitterToTextChunks(content, mockHighlights, testStyle)\n\n      testStyle.destroy()\n\n      expect(chunks.length).toBeGreaterThan(0)\n\n      const chunk = chunks[0]\n\n      expect(chunk.fg?.r).toBeCloseTo(200 / 255, 2)\n\n      const expectedAttributes = createTextAttributes({ bold: false, italic: true, underline: true })\n      expect(chunk.attributes).toBe(expectedAttributes)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/tree-sitter-styled-text.ts",
    "content": "import type { TextChunk } from \"../text-buffer.js\"\nimport { StyledText } from \"./styled-text.js\"\nimport { SyntaxStyle, type StyleDefinition } from \"../syntax-style.js\"\nimport { TreeSitterClient } from \"./tree-sitter/client.js\"\nimport type { SimpleHighlight } from \"./tree-sitter/types.js\"\nimport { createTextAttributes } from \"../utils.js\"\nimport { registerEnvVar, env } from \"./env.js\"\n\nregisterEnvVar({ name: \"OTUI_TS_STYLE_WARN\", default: false, description: \"Enable warnings for missing syntax styles\" })\n\ninterface ConcealOptions {\n  enabled: boolean\n}\n\ninterface Boundary {\n  offset: number\n  type: \"start\" | \"end\"\n  highlightIndex: number\n}\n\nfunction getSpecificity(group: string): number {\n  return group.split(\".\").length\n}\n\nfunction shouldSuppressInInjection(group: string, meta: any): boolean {\n  if (meta?.isInjection) {\n    return false\n  }\n\n  // Check if this is a parent block that should be suppressed\n  // TODO: This is language/highlight specific,\n  // not generic enough. Needs a more generic solution.\n  // The styles need to be more like a stack that gets merged\n  // and for a container with injections we just don't push that container style\n  return group === \"markup.raw.block\"\n}\n\nexport function treeSitterToTextChunks(\n  content: string,\n  highlights: SimpleHighlight[],\n  syntaxStyle: SyntaxStyle,\n  options?: ConcealOptions,\n): TextChunk[] {\n  const chunks: TextChunk[] = []\n  const defaultStyle = syntaxStyle.getStyle(\"default\")\n  const concealEnabled = options?.enabled ?? true\n\n  const injectionContainerRanges: Array<{ start: number; end: number }> = []\n  const boundaries: Boundary[] = []\n\n  for (let i = 0; i < highlights.length; i++) {\n    const [start, end, , meta] = highlights[i]\n    if (start === end) continue // Skip zero-length ranges\n    if (meta?.containsInjection) {\n      injectionContainerRanges.push({ start, end })\n    }\n    boundaries.push({ offset: start, type: \"start\", highlightIndex: i })\n    boundaries.push({ offset: end, type: \"end\", highlightIndex: i })\n  }\n\n  // Sort boundaries by offset, with ends before starts at same offset\n  // This ensures we close old ranges before opening new ones at the same position\n  boundaries.sort((a, b) => {\n    if (a.offset !== b.offset) return a.offset - b.offset\n    if (a.type === \"end\" && b.type === \"start\") return -1\n    if (a.type === \"start\" && b.type === \"end\") return 1\n    return 0\n  })\n\n  const activeHighlights = new Set<number>()\n  let currentOffset = 0\n\n  for (let i = 0; i < boundaries.length; i++) {\n    const boundary = boundaries[i]\n\n    if (currentOffset < boundary.offset && activeHighlights.size > 0) {\n      const segmentText = content.slice(currentOffset, boundary.offset)\n\n      const activeGroups: Array<{ group: string; meta: any; index: number }> = []\n      for (const idx of activeHighlights) {\n        const [, , group, meta] = highlights[idx]\n        activeGroups.push({ group, meta, index: idx })\n      }\n\n      // Check if any active highlight has a conceal property\n      // Priority: 1. Check meta.conceal first 2. Check group === \"conceal\" or starts with \"conceal.\"\n      const concealHighlight = concealEnabled\n        ? activeGroups.find(\n            (h) => h.meta?.conceal !== undefined || h.group === \"conceal\" || h.group.startsWith(\"conceal.\"),\n          )\n        : undefined\n\n      if (concealHighlight) {\n        let replacementText = \"\"\n\n        if (concealHighlight.meta?.conceal !== undefined) {\n          // If meta.conceal is set, use it (this would come from (#set! conceal \"...\") if supported)\n          replacementText = concealHighlight.meta.conceal\n        } else if (concealHighlight.group === \"conceal.with.space\") {\n          // Special group name means replace with space\n          replacementText = \" \"\n        }\n\n        if (replacementText) {\n          chunks.push({\n            __isChunk: true,\n            text: replacementText,\n            fg: defaultStyle?.fg,\n            bg: defaultStyle?.bg,\n            attributes: defaultStyle\n              ? createTextAttributes({\n                  bold: defaultStyle.bold,\n                  italic: defaultStyle.italic,\n                  underline: defaultStyle.underline,\n                  dim: defaultStyle.dim,\n                })\n              : 0,\n          })\n        }\n      } else {\n        const insideInjectionContainer = injectionContainerRanges.some(\n          (range) => currentOffset >= range.start && currentOffset < range.end,\n        )\n\n        // Filter out highlights that should be suppressed\n        // Suppress highlights when we're inside an injection container\n        const validGroups = activeGroups.filter((h) => {\n          // If we're inside an injection container, suppress all markup.raw.block highlights\n          // This includes both the container itself and any nested markup.raw.block\n          if (insideInjectionContainer && shouldSuppressInInjection(h.group, h.meta)) {\n            return false\n          }\n          return true\n        })\n\n        // Sort groups by specificity (least to most), then by index (earlier to later)\n        // This ensures we merge styles in the correct order: parent styles first, then child overrides\n        const sortedGroups = validGroups.sort((a, b) => {\n          const aSpec = getSpecificity(a.group)\n          const bSpec = getSpecificity(b.group)\n          if (aSpec !== bSpec) return aSpec - bSpec // Lower specificity first\n          return a.index - b.index // Earlier index first\n        })\n\n        // Merge all active styles in order (like CSS cascade)\n        // Later/more specific styles override earlier/less specific ones\n        const mergedStyle: StyleDefinition = {}\n\n        for (const { group } of sortedGroups) {\n          let styleForGroup = syntaxStyle.getStyle(group)\n\n          if (!styleForGroup && group.includes(\".\")) {\n            // Fallback to base scope\n            const baseName = group.split(\".\")[0]\n            styleForGroup = syntaxStyle.getStyle(baseName)\n          }\n\n          if (styleForGroup) {\n            // Merge properties - later styles override earlier ones\n            if (styleForGroup.fg !== undefined) mergedStyle.fg = styleForGroup.fg\n            if (styleForGroup.bg !== undefined) mergedStyle.bg = styleForGroup.bg\n            if (styleForGroup.bold !== undefined) mergedStyle.bold = styleForGroup.bold\n            if (styleForGroup.italic !== undefined) mergedStyle.italic = styleForGroup.italic\n            if (styleForGroup.underline !== undefined) mergedStyle.underline = styleForGroup.underline\n            if (styleForGroup.dim !== undefined) mergedStyle.dim = styleForGroup.dim\n          } else {\n            if (group.includes(\".\")) {\n              const baseName = group.split(\".\")[0]\n              if (env.OTUI_TS_STYLE_WARN) {\n                console.warn(\n                  `Syntax style not found for group \"${group}\" or base scope \"${baseName}\", using default style`,\n                )\n              }\n            } else {\n              if (env.OTUI_TS_STYLE_WARN) {\n                console.warn(`Syntax style not found for group \"${group}\", using default style`)\n              }\n            }\n          }\n        }\n\n        // Use merged style, falling back to default if nothing was merged\n        const finalStyle = Object.keys(mergedStyle).length > 0 ? mergedStyle : defaultStyle\n\n        chunks.push({\n          __isChunk: true,\n          text: segmentText,\n          fg: finalStyle?.fg,\n          bg: finalStyle?.bg,\n          attributes: finalStyle\n            ? createTextAttributes({\n                bold: finalStyle.bold,\n                italic: finalStyle.italic,\n                underline: finalStyle.underline,\n                dim: finalStyle.dim,\n              })\n            : 0,\n        })\n      }\n    } else if (currentOffset < boundary.offset) {\n      const text = content.slice(currentOffset, boundary.offset)\n      chunks.push({\n        __isChunk: true,\n        text,\n        fg: defaultStyle?.fg,\n        bg: defaultStyle?.bg,\n        attributes: defaultStyle\n          ? createTextAttributes({\n              bold: defaultStyle.bold,\n              italic: defaultStyle.italic,\n              underline: defaultStyle.underline,\n              dim: defaultStyle.dim,\n            })\n          : 0,\n      })\n    }\n\n    if (boundary.type === \"start\") {\n      activeHighlights.add(boundary.highlightIndex)\n    } else {\n      activeHighlights.delete(boundary.highlightIndex)\n\n      if (concealEnabled) {\n        const [, , group, meta] = highlights[boundary.highlightIndex]\n        if (meta?.concealLines !== undefined) {\n          if (boundary.offset < content.length && content[boundary.offset] === \"\\n\") {\n            currentOffset = boundary.offset + 1\n            continue\n          }\n        }\n\n        // TODO: This is also a query specific workaround, needs improvement\n        if (meta?.conceal !== undefined) {\n          // Skip the next space if we replaced with a space (prevents double spaces like \"text] (url)\")\n          if (meta.conceal === \" \") {\n            if (boundary.offset < content.length && content[boundary.offset] === \" \") {\n              currentOffset = boundary.offset + 1\n              continue\n            }\n          }\n          // For heading markers specifically, also skip the trailing space\n          // The group is just \"conceal\" for heading markers from the markdown query\n          // We need to check if this conceal is NOT from an injection (markdown_inline)\n          else if (meta.conceal === \"\" && group === \"conceal\" && !meta.isInjection) {\n            if (boundary.offset < content.length && content[boundary.offset] === \" \") {\n              currentOffset = boundary.offset + 1\n              continue\n            }\n          }\n        }\n      }\n    }\n\n    currentOffset = boundary.offset\n  }\n\n  if (currentOffset < content.length) {\n    const text = content.slice(currentOffset)\n    chunks.push({\n      __isChunk: true,\n      text,\n      fg: defaultStyle?.fg,\n      bg: defaultStyle?.bg,\n      attributes: defaultStyle\n        ? createTextAttributes({\n            bold: defaultStyle.bold,\n            italic: defaultStyle.italic,\n            underline: defaultStyle.underline,\n            dim: defaultStyle.dim,\n          })\n        : 0,\n    })\n  }\n\n  return chunks\n}\n\nexport interface TreeSitterToStyledTextOptions {\n  conceal?: ConcealOptions\n}\n\nexport async function treeSitterToStyledText(\n  content: string,\n  filetype: string,\n  syntaxStyle: SyntaxStyle,\n  client: TreeSitterClient,\n  options?: TreeSitterToStyledTextOptions,\n): Promise<StyledText> {\n  const result = await client.highlightOnce(content, filetype)\n  if (result.highlights && result.highlights.length > 0) {\n    const chunks = treeSitterToTextChunks(content, result.highlights, syntaxStyle, options?.conceal)\n    return new StyledText(chunks)\n  } else {\n    const defaultStyle = syntaxStyle.mergeStyles(\"default\")\n    const chunks: TextChunk[] = [\n      {\n        __isChunk: true,\n        text: content,\n        fg: defaultStyle.fg,\n        bg: defaultStyle.bg,\n        attributes: defaultStyle.attributes,\n      },\n    ]\n    return new StyledText(chunks)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/lib/validate-dir-name.ts",
    "content": "export function isValidDirectoryName(name: string): boolean {\n  if (!name || typeof name !== \"string\") {\n    return false\n  }\n\n  if (name.trim().length === 0) {\n    return false\n  }\n\n  const reservedNames = [\n    \"CON\",\n    \"PRN\",\n    \"AUX\",\n    \"NUL\",\n    \"COM1\",\n    \"COM2\",\n    \"COM3\",\n    \"COM4\",\n    \"COM5\",\n    \"COM6\",\n    \"COM7\",\n    \"COM8\",\n    \"COM9\",\n    \"LPT1\",\n    \"LPT2\",\n    \"LPT3\",\n    \"LPT4\",\n    \"LPT5\",\n    \"LPT6\",\n    \"LPT7\",\n    \"LPT8\",\n    \"LPT9\",\n  ]\n  if (reservedNames.includes(name.toUpperCase())) {\n    return false\n  }\n\n  // Check for invalid characters\n  // Windows: < > : \" | ? * \\ and control characters (0-31)\n  // Unix: null character and forward slash\n  const invalidChars = /[<>:\"|?*\\/\\\\\\x00-\\x1f]/\n  if (invalidChars.test(name)) {\n    return false\n  }\n\n  if (name.endsWith(\".\") || name.endsWith(\" \")) {\n    return false\n  }\n\n  if (name === \".\" || name === \"..\") {\n    return false\n  }\n\n  return true\n}\n"
  },
  {
    "path": "packages/core/src/lib/yoga.options.test.ts",
    "content": "import { test, expect, describe } from \"bun:test\"\nimport {\n  parseBoxSizing,\n  parseDimension,\n  parseDirection,\n  parseDisplay,\n  parseEdge,\n  parseGutter,\n  parseLogLevel,\n  parseMeasureMode,\n  parseUnit,\n  parseAlign,\n  parseAlignItems,\n  parseFlexDirection,\n  parseJustify,\n  parseOverflow,\n  parsePositionType,\n  parseWrap,\n} from \"./yoga.options.js\"\nimport {\n  BoxSizing,\n  Align,\n  Dimension,\n  Direction,\n  Display,\n  Edge,\n  FlexDirection,\n  Gutter,\n  Justify,\n  LogLevel,\n  MeasureMode,\n  Overflow,\n  PositionType,\n  Unit,\n  Wrap,\n} from \"yoga-layout\"\n\ndescribe(\"parseBoxSizing\", () => {\n  test(\"parses border-box\", () => {\n    expect(parseBoxSizing(\"border-box\")).toBe(BoxSizing.BorderBox)\n  })\n\n  test(\"parses content-box\", () => {\n    expect(parseBoxSizing(\"content-box\")).toBe(BoxSizing.ContentBox)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseBoxSizing(\"BORDER-BOX\")).toBe(BoxSizing.BorderBox)\n    expect(parseBoxSizing(\"CONTENT-BOX\")).toBe(BoxSizing.ContentBox)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseBoxSizing(\"invalid\")).toBe(BoxSizing.BorderBox)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseBoxSizing(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    expect(() => parseBoxSizing(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseDimension\", () => {\n  test(\"parses width\", () => {\n    expect(parseDimension(\"width\")).toBe(Dimension.Width)\n  })\n\n  test(\"parses height\", () => {\n    expect(parseDimension(\"height\")).toBe(Dimension.Height)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseDimension(\"WIDTH\")).toBe(Dimension.Width)\n    expect(parseDimension(\"HEIGHT\")).toBe(Dimension.Height)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseDimension(\"invalid\")).toBe(Dimension.Width)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseDimension(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    expect(() => parseDimension(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseDirection\", () => {\n  test(\"parses inherit\", () => {\n    expect(parseDirection(\"inherit\")).toBe(Direction.Inherit)\n  })\n\n  test(\"parses ltr\", () => {\n    expect(parseDirection(\"ltr\")).toBe(Direction.LTR)\n  })\n\n  test(\"parses rtl\", () => {\n    expect(parseDirection(\"rtl\")).toBe(Direction.RTL)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseDirection(\"INHERIT\")).toBe(Direction.Inherit)\n    expect(parseDirection(\"LTR\")).toBe(Direction.LTR)\n    expect(parseDirection(\"RTL\")).toBe(Direction.RTL)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseDirection(\"invalid\")).toBe(Direction.LTR)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseDirection(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    expect(() => parseDirection(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseDisplay\", () => {\n  test(\"parses flex\", () => {\n    expect(parseDisplay(\"flex\")).toBe(Display.Flex)\n  })\n\n  test(\"parses none\", () => {\n    expect(parseDisplay(\"none\")).toBe(Display.None)\n  })\n\n  test(\"parses contents\", () => {\n    expect(parseDisplay(\"contents\")).toBe(Display.Contents)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseDisplay(\"FLEX\")).toBe(Display.Flex)\n    expect(parseDisplay(\"NONE\")).toBe(Display.None)\n    expect(parseDisplay(\"CONTENTS\")).toBe(Display.Contents)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseDisplay(\"invalid\")).toBe(Display.Flex)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseDisplay(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    expect(() => parseDisplay(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseEdge\", () => {\n  test(\"parses left\", () => {\n    expect(parseEdge(\"left\")).toBe(Edge.Left)\n  })\n\n  test(\"parses top\", () => {\n    expect(parseEdge(\"top\")).toBe(Edge.Top)\n  })\n\n  test(\"parses right\", () => {\n    expect(parseEdge(\"right\")).toBe(Edge.Right)\n  })\n\n  test(\"parses bottom\", () => {\n    expect(parseEdge(\"bottom\")).toBe(Edge.Bottom)\n  })\n\n  test(\"parses start\", () => {\n    expect(parseEdge(\"start\")).toBe(Edge.Start)\n  })\n\n  test(\"parses end\", () => {\n    expect(parseEdge(\"end\")).toBe(Edge.End)\n  })\n\n  test(\"parses horizontal\", () => {\n    expect(parseEdge(\"horizontal\")).toBe(Edge.Horizontal)\n  })\n\n  test(\"parses vertical\", () => {\n    expect(parseEdge(\"vertical\")).toBe(Edge.Vertical)\n  })\n\n  test(\"parses all\", () => {\n    expect(parseEdge(\"all\")).toBe(Edge.All)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseEdge(\"LEFT\")).toBe(Edge.Left)\n    expect(parseEdge(\"TOP\")).toBe(Edge.Top)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseEdge(\"invalid\")).toBe(Edge.All)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseEdge(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    expect(() => parseEdge(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseGutter\", () => {\n  test(\"parses column\", () => {\n    expect(parseGutter(\"column\")).toBe(Gutter.Column)\n  })\n\n  test(\"parses row\", () => {\n    expect(parseGutter(\"row\")).toBe(Gutter.Row)\n  })\n\n  test(\"parses all\", () => {\n    expect(parseGutter(\"all\")).toBe(Gutter.All)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseGutter(\"COLUMN\")).toBe(Gutter.Column)\n    expect(parseGutter(\"ROW\")).toBe(Gutter.Row)\n    expect(parseGutter(\"ALL\")).toBe(Gutter.All)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseGutter(\"invalid\")).toBe(Gutter.All)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseGutter(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    expect(() => parseGutter(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseLogLevel\", () => {\n  test(\"parses error\", () => {\n    expect(parseLogLevel(\"error\")).toBe(LogLevel.Error)\n  })\n\n  test(\"parses warn\", () => {\n    expect(parseLogLevel(\"warn\")).toBe(LogLevel.Warn)\n  })\n\n  test(\"parses info\", () => {\n    expect(parseLogLevel(\"info\")).toBe(LogLevel.Info)\n  })\n\n  test(\"parses debug\", () => {\n    expect(parseLogLevel(\"debug\")).toBe(LogLevel.Debug)\n  })\n\n  test(\"parses verbose\", () => {\n    expect(parseLogLevel(\"verbose\")).toBe(LogLevel.Verbose)\n  })\n\n  test(\"parses fatal\", () => {\n    expect(parseLogLevel(\"fatal\")).toBe(LogLevel.Fatal)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseLogLevel(\"ERROR\")).toBe(LogLevel.Error)\n    expect(parseLogLevel(\"WARN\")).toBe(LogLevel.Warn)\n    expect(parseLogLevel(\"INFO\")).toBe(LogLevel.Info)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseLogLevel(\"invalid\")).toBe(LogLevel.Info)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseLogLevel(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    expect(() => parseLogLevel(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseMeasureMode\", () => {\n  test(\"parses undefined\", () => {\n    expect(parseMeasureMode(\"undefined\")).toBe(MeasureMode.Undefined)\n  })\n\n  test(\"parses exactly\", () => {\n    expect(parseMeasureMode(\"exactly\")).toBe(MeasureMode.Exactly)\n  })\n\n  test(\"parses at-most\", () => {\n    expect(parseMeasureMode(\"at-most\")).toBe(MeasureMode.AtMost)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseMeasureMode(\"UNDEFINED\")).toBe(MeasureMode.Undefined)\n    expect(parseMeasureMode(\"EXACTLY\")).toBe(MeasureMode.Exactly)\n    expect(parseMeasureMode(\"AT-MOST\")).toBe(MeasureMode.AtMost)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseMeasureMode(\"invalid\")).toBe(MeasureMode.Undefined)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseMeasureMode(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined value\", () => {\n    expect(() => parseMeasureMode(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseUnit\", () => {\n  test(\"parses undefined\", () => {\n    expect(parseUnit(\"undefined\")).toBe(Unit.Undefined)\n  })\n\n  test(\"parses point\", () => {\n    expect(parseUnit(\"point\")).toBe(Unit.Point)\n  })\n\n  test(\"parses percent\", () => {\n    expect(parseUnit(\"percent\")).toBe(Unit.Percent)\n  })\n\n  test(\"parses auto\", () => {\n    expect(parseUnit(\"auto\")).toBe(Unit.Auto)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseUnit(\"UNDEFINED\")).toBe(Unit.Undefined)\n    expect(parseUnit(\"POINT\")).toBe(Unit.Point)\n    expect(parseUnit(\"PERCENT\")).toBe(Unit.Percent)\n    expect(parseUnit(\"AUTO\")).toBe(Unit.Auto)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseUnit(\"invalid\")).toBe(Unit.Point)\n  })\n\n  test(\"handles null\", () => {\n    expect(() => parseUnit(null as any)).not.toThrow()\n  })\n\n  test(\"handles undefined value\", () => {\n    expect(() => parseUnit(undefined as any)).not.toThrow()\n  })\n})\n\ndescribe(\"parseAlign\", () => {\n  test(\"parses auto\", () => {\n    expect(parseAlign(\"auto\")).toBe(Align.Auto)\n  })\n\n  test(\"parses flex-start\", () => {\n    expect(parseAlign(\"flex-start\")).toBe(Align.FlexStart)\n  })\n\n  test(\"parses center\", () => {\n    expect(parseAlign(\"center\")).toBe(Align.Center)\n  })\n\n  test(\"parses flex-end\", () => {\n    expect(parseAlign(\"flex-end\")).toBe(Align.FlexEnd)\n  })\n\n  test(\"parses stretch\", () => {\n    expect(parseAlign(\"stretch\")).toBe(Align.Stretch)\n  })\n\n  test(\"parses baseline\", () => {\n    expect(parseAlign(\"baseline\")).toBe(Align.Baseline)\n  })\n\n  test(\"parses space-between\", () => {\n    expect(parseAlign(\"space-between\")).toBe(Align.SpaceBetween)\n  })\n\n  test(\"parses space-around\", () => {\n    expect(parseAlign(\"space-around\")).toBe(Align.SpaceAround)\n  })\n\n  test(\"parses space-evenly\", () => {\n    expect(parseAlign(\"space-evenly\")).toBe(Align.SpaceEvenly)\n  })\n\n  test(\"handles null\", () => {\n    expect(parseAlign(null)).toBe(Align.Auto)\n  })\n\n  test(\"handles undefined\", () => {\n    expect(parseAlign(undefined)).toBe(Align.Auto)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseAlign(\"CENTER\")).toBe(Align.Center)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseAlign(\"invalid\")).toBe(Align.Auto)\n  })\n})\n\ndescribe(\"parseAlignItems\", () => {\n  test(\"parses auto\", () => {\n    expect(parseAlignItems(\"auto\")).toBe(Align.Auto)\n  })\n\n  test(\"parses flex-start\", () => {\n    expect(parseAlignItems(\"flex-start\")).toBe(Align.FlexStart)\n  })\n\n  test(\"parses center\", () => {\n    expect(parseAlignItems(\"center\")).toBe(Align.Center)\n  })\n\n  test(\"parses flex-end\", () => {\n    expect(parseAlignItems(\"flex-end\")).toBe(Align.FlexEnd)\n  })\n\n  test(\"parses stretch\", () => {\n    expect(parseAlignItems(\"stretch\")).toBe(Align.Stretch)\n  })\n\n  test(\"parses baseline\", () => {\n    expect(parseAlignItems(\"baseline\")).toBe(Align.Baseline)\n  })\n\n  test(\"parses space-between\", () => {\n    expect(parseAlignItems(\"space-between\")).toBe(Align.SpaceBetween)\n  })\n\n  test(\"parses space-around\", () => {\n    expect(parseAlignItems(\"space-around\")).toBe(Align.SpaceAround)\n  })\n\n  test(\"parses space-evenly\", () => {\n    expect(parseAlignItems(\"space-evenly\")).toBe(Align.SpaceEvenly)\n  })\n\n  test(\"returns Stretch for null\", () => {\n    expect(parseAlignItems(null)).toBe(Align.Stretch)\n  })\n\n  test(\"returns Stretch for undefined\", () => {\n    expect(parseAlignItems(undefined)).toBe(Align.Stretch)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseAlignItems(\"CENTER\")).toBe(Align.Center)\n  })\n\n  test(\"returns Stretch for invalid value\", () => {\n    expect(parseAlignItems(\"invalid\")).toBe(Align.Stretch)\n  })\n})\n\ndescribe(\"parseFlexDirection\", () => {\n  test(\"parses column\", () => {\n    expect(parseFlexDirection(\"column\")).toBe(FlexDirection.Column)\n  })\n\n  test(\"parses column-reverse\", () => {\n    expect(parseFlexDirection(\"column-reverse\")).toBe(FlexDirection.ColumnReverse)\n  })\n\n  test(\"parses row\", () => {\n    expect(parseFlexDirection(\"row\")).toBe(FlexDirection.Row)\n  })\n\n  test(\"parses row-reverse\", () => {\n    expect(parseFlexDirection(\"row-reverse\")).toBe(FlexDirection.RowReverse)\n  })\n\n  test(\"handles null\", () => {\n    expect(parseFlexDirection(null)).toBe(FlexDirection.Column)\n  })\n\n  test(\"handles undefined\", () => {\n    expect(parseFlexDirection(undefined)).toBe(FlexDirection.Column)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseFlexDirection(\"ROW\")).toBe(FlexDirection.Row)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseFlexDirection(\"invalid\")).toBe(FlexDirection.Column)\n  })\n})\n\ndescribe(\"parseJustify\", () => {\n  test(\"parses flex-start\", () => {\n    expect(parseJustify(\"flex-start\")).toBe(Justify.FlexStart)\n  })\n\n  test(\"parses center\", () => {\n    expect(parseJustify(\"center\")).toBe(Justify.Center)\n  })\n\n  test(\"parses flex-end\", () => {\n    expect(parseJustify(\"flex-end\")).toBe(Justify.FlexEnd)\n  })\n\n  test(\"parses space-between\", () => {\n    expect(parseJustify(\"space-between\")).toBe(Justify.SpaceBetween)\n  })\n\n  test(\"parses space-around\", () => {\n    expect(parseJustify(\"space-around\")).toBe(Justify.SpaceAround)\n  })\n\n  test(\"parses space-evenly\", () => {\n    expect(parseJustify(\"space-evenly\")).toBe(Justify.SpaceEvenly)\n  })\n\n  test(\"handles null\", () => {\n    expect(parseJustify(null)).toBe(Justify.FlexStart)\n  })\n\n  test(\"handles undefined\", () => {\n    expect(parseJustify(undefined)).toBe(Justify.FlexStart)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseJustify(\"CENTER\")).toBe(Justify.Center)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseJustify(\"invalid\")).toBe(Justify.FlexStart)\n  })\n})\n\ndescribe(\"parseOverflow\", () => {\n  test(\"parses visible\", () => {\n    expect(parseOverflow(\"visible\")).toBe(Overflow.Visible)\n  })\n\n  test(\"parses hidden\", () => {\n    expect(parseOverflow(\"hidden\")).toBe(Overflow.Hidden)\n  })\n\n  test(\"parses scroll\", () => {\n    expect(parseOverflow(\"scroll\")).toBe(Overflow.Scroll)\n  })\n\n  test(\"handles null\", () => {\n    expect(parseOverflow(null)).toBe(Overflow.Visible)\n  })\n\n  test(\"handles undefined\", () => {\n    expect(parseOverflow(undefined)).toBe(Overflow.Visible)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseOverflow(\"HIDDEN\")).toBe(Overflow.Hidden)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseOverflow(\"invalid\")).toBe(Overflow.Visible)\n  })\n})\n\ndescribe(\"parsePositionType\", () => {\n  test(\"parses static\", () => {\n    expect(parsePositionType(\"static\")).toBe(PositionType.Static)\n  })\n\n  test(\"parses relative\", () => {\n    expect(parsePositionType(\"relative\")).toBe(PositionType.Relative)\n  })\n\n  test(\"parses absolute\", () => {\n    expect(parsePositionType(\"absolute\")).toBe(PositionType.Absolute)\n  })\n\n  test(\"handles null\", () => {\n    expect(parsePositionType(null)).toBe(PositionType.Relative)\n  })\n\n  test(\"handles undefined\", () => {\n    expect(parsePositionType(undefined)).toBe(PositionType.Relative)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parsePositionType(\"ABSOLUTE\")).toBe(PositionType.Absolute)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parsePositionType(\"invalid\")).toBe(PositionType.Static)\n  })\n})\n\ndescribe(\"parseWrap\", () => {\n  test(\"parses no-wrap\", () => {\n    expect(parseWrap(\"no-wrap\")).toBe(Wrap.NoWrap)\n  })\n\n  test(\"parses wrap\", () => {\n    expect(parseWrap(\"wrap\")).toBe(Wrap.Wrap)\n  })\n\n  test(\"parses wrap-reverse\", () => {\n    expect(parseWrap(\"wrap-reverse\")).toBe(Wrap.WrapReverse)\n  })\n\n  test(\"handles null\", () => {\n    expect(parseWrap(null)).toBe(Wrap.NoWrap)\n  })\n\n  test(\"handles undefined\", () => {\n    expect(parseWrap(undefined)).toBe(Wrap.NoWrap)\n  })\n\n  test(\"handles uppercase\", () => {\n    expect(parseWrap(\"WRAP\")).toBe(Wrap.Wrap)\n  })\n\n  test(\"returns default for invalid value\", () => {\n    expect(parseWrap(\"invalid\")).toBe(Wrap.NoWrap)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/lib/yoga.options.ts",
    "content": "import {\n  BoxSizing,\n  Align,\n  Dimension,\n  Direction,\n  Display,\n  Edge,\n  FlexDirection,\n  Gutter,\n  Justify,\n  LogLevel,\n  MeasureMode,\n  Overflow,\n  PositionType,\n  Unit,\n  Wrap,\n} from \"yoga-layout\"\n\nexport type AlignString =\n  | \"auto\"\n  | \"flex-start\"\n  | \"center\"\n  | \"flex-end\"\n  | \"stretch\"\n  | \"baseline\"\n  | \"space-between\"\n  | \"space-around\"\n  | \"space-evenly\"\nexport type BoxSizingString = \"border-box\" | \"content-box\"\nexport type DimensionString = \"width\" | \"height\"\nexport type DirectionString = \"inherit\" | \"ltr\" | \"rtl\"\nexport type DisplayString = \"flex\" | \"none\" | \"contents\"\nexport type EdgeString = \"left\" | \"top\" | \"right\" | \"bottom\" | \"start\" | \"end\" | \"horizontal\" | \"vertical\" | \"all\"\nexport type FlexDirectionString = \"column\" | \"column-reverse\" | \"row\" | \"row-reverse\"\nexport type GutterString = \"column\" | \"row\" | \"all\"\nexport type JustifyString = \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | \"space-evenly\"\nexport type LogLevelString = \"error\" | \"warn\" | \"info\" | \"debug\" | \"verbose\" | \"fatal\"\nexport type MeasureModeString = \"undefined\" | \"exactly\" | \"at-most\"\nexport type OverflowString = \"visible\" | \"hidden\" | \"scroll\"\nexport type PositionTypeString = \"static\" | \"relative\" | \"absolute\"\nexport type UnitString = \"undefined\" | \"point\" | \"percent\" | \"auto\"\nexport type WrapString = \"no-wrap\" | \"wrap\" | \"wrap-reverse\"\n\nexport function parseAlign(value: string | null | undefined): Align {\n  if (value == null) {\n    return Align.Auto\n  }\n  switch (value.toLowerCase()) {\n    case \"auto\":\n      return Align.Auto\n    case \"flex-start\":\n      return Align.FlexStart\n    case \"center\":\n      return Align.Center\n    case \"flex-end\":\n      return Align.FlexEnd\n    case \"stretch\":\n      return Align.Stretch\n    case \"baseline\":\n      return Align.Baseline\n    case \"space-between\":\n      return Align.SpaceBetween\n    case \"space-around\":\n      return Align.SpaceAround\n    case \"space-evenly\":\n      return Align.SpaceEvenly\n    default:\n      return Align.Auto\n  }\n}\n\nexport function parseAlignItems(value: string | null | undefined): Align {\n  if (value == null) {\n    return Align.Stretch\n  }\n  switch (value.toLowerCase()) {\n    case \"auto\":\n      return Align.Auto\n    case \"flex-start\":\n      return Align.FlexStart\n    case \"center\":\n      return Align.Center\n    case \"flex-end\":\n      return Align.FlexEnd\n    case \"stretch\":\n      return Align.Stretch\n    case \"baseline\":\n      return Align.Baseline\n    case \"space-between\":\n      return Align.SpaceBetween\n    case \"space-around\":\n      return Align.SpaceAround\n    case \"space-evenly\":\n      return Align.SpaceEvenly\n    default:\n      return Align.Stretch\n  }\n}\n\nexport function parseBoxSizing(value: string): BoxSizing {\n  if (value == null) {\n    return BoxSizing.BorderBox\n  }\n  switch (value.toLowerCase()) {\n    case \"border-box\":\n      return BoxSizing.BorderBox\n    case \"content-box\":\n      return BoxSizing.ContentBox\n    default:\n      return BoxSizing.BorderBox\n  }\n}\n\nexport function parseDimension(value: string): Dimension {\n  if (value == null) {\n    return Dimension.Width\n  }\n  switch (value.toLowerCase()) {\n    case \"width\":\n      return Dimension.Width\n    case \"height\":\n      return Dimension.Height\n    default:\n      return Dimension.Width\n  }\n}\n\nexport function parseDirection(value: string): Direction {\n  if (value == null) {\n    return Direction.LTR\n  }\n  switch (value.toLowerCase()) {\n    case \"inherit\":\n      return Direction.Inherit\n    case \"ltr\":\n      return Direction.LTR\n    case \"rtl\":\n      return Direction.RTL\n    default:\n      return Direction.LTR\n  }\n}\n\nexport function parseDisplay(value: string): Display {\n  if (value == null) {\n    return Display.Flex\n  }\n  switch (value.toLowerCase()) {\n    case \"flex\":\n      return Display.Flex\n    case \"none\":\n      return Display.None\n    case \"contents\":\n      return Display.Contents\n    default:\n      return Display.Flex\n  }\n}\n\nexport function parseEdge(value: string): Edge {\n  if (value == null) {\n    return Edge.All\n  }\n  switch (value.toLowerCase()) {\n    case \"left\":\n      return Edge.Left\n    case \"top\":\n      return Edge.Top\n    case \"right\":\n      return Edge.Right\n    case \"bottom\":\n      return Edge.Bottom\n    case \"start\":\n      return Edge.Start\n    case \"end\":\n      return Edge.End\n    case \"horizontal\":\n      return Edge.Horizontal\n    case \"vertical\":\n      return Edge.Vertical\n    case \"all\":\n      return Edge.All\n    default:\n      return Edge.All\n  }\n}\n\nexport function parseFlexDirection(value: string | null | undefined): FlexDirection {\n  if (value == null) {\n    return FlexDirection.Column\n  }\n  switch (value.toLowerCase()) {\n    case \"column\":\n      return FlexDirection.Column\n    case \"column-reverse\":\n      return FlexDirection.ColumnReverse\n    case \"row\":\n      return FlexDirection.Row\n    case \"row-reverse\":\n      return FlexDirection.RowReverse\n    default:\n      return FlexDirection.Column\n  }\n}\n\nexport function parseGutter(value: string): Gutter {\n  if (value == null) {\n    return Gutter.All\n  }\n  switch (value.toLowerCase()) {\n    case \"column\":\n      return Gutter.Column\n    case \"row\":\n      return Gutter.Row\n    case \"all\":\n      return Gutter.All\n    default:\n      return Gutter.All\n  }\n}\n\nexport function parseJustify(value: string | null | undefined): Justify {\n  if (value == null) {\n    return Justify.FlexStart\n  }\n  switch (value.toLowerCase()) {\n    case \"flex-start\":\n      return Justify.FlexStart\n    case \"center\":\n      return Justify.Center\n    case \"flex-end\":\n      return Justify.FlexEnd\n    case \"space-between\":\n      return Justify.SpaceBetween\n    case \"space-around\":\n      return Justify.SpaceAround\n    case \"space-evenly\":\n      return Justify.SpaceEvenly\n    default:\n      return Justify.FlexStart\n  }\n}\n\nexport function parseLogLevel(value: string): LogLevel {\n  if (value == null) {\n    return LogLevel.Info\n  }\n  switch (value.toLowerCase()) {\n    case \"error\":\n      return LogLevel.Error\n    case \"warn\":\n      return LogLevel.Warn\n    case \"info\":\n      return LogLevel.Info\n    case \"debug\":\n      return LogLevel.Debug\n    case \"verbose\":\n      return LogLevel.Verbose\n    case \"fatal\":\n      return LogLevel.Fatal\n    default:\n      return LogLevel.Info\n  }\n}\n\nexport function parseMeasureMode(value: string): MeasureMode {\n  if (value == null) {\n    return MeasureMode.Undefined\n  }\n  switch (value.toLowerCase()) {\n    case \"undefined\":\n      return MeasureMode.Undefined\n    case \"exactly\":\n      return MeasureMode.Exactly\n    case \"at-most\":\n      return MeasureMode.AtMost\n    default:\n      return MeasureMode.Undefined\n  }\n}\n\nexport function parseOverflow(value: string | null | undefined): Overflow {\n  if (value == null) {\n    return Overflow.Visible\n  }\n  switch (value.toLowerCase()) {\n    case \"visible\":\n      return Overflow.Visible\n    case \"hidden\":\n      return Overflow.Hidden\n    case \"scroll\":\n      return Overflow.Scroll\n    default:\n      return Overflow.Visible\n  }\n}\n\nexport function parsePositionType(value: string | null | undefined): PositionType {\n  if (value == null) {\n    return PositionType.Relative\n  }\n  switch (value.toLowerCase()) {\n    case \"static\":\n      return PositionType.Static\n    case \"relative\":\n      return PositionType.Relative\n    case \"absolute\":\n      return PositionType.Absolute\n    default:\n      return PositionType.Static\n  }\n}\n\nexport function parseUnit(value: string): Unit {\n  if (value == null) {\n    return Unit.Point\n  }\n  switch (value.toLowerCase()) {\n    case \"undefined\":\n      return Unit.Undefined\n    case \"point\":\n      return Unit.Point\n    case \"percent\":\n      return Unit.Percent\n    case \"auto\":\n      return Unit.Auto\n    default:\n      return Unit.Point\n  }\n}\n\nexport function parseWrap(value: string | null | undefined): Wrap {\n  if (value == null) {\n    return Wrap.NoWrap\n  }\n  switch (value.toLowerCase()) {\n    case \"no-wrap\":\n      return Wrap.NoWrap\n    case \"wrap\":\n      return Wrap.Wrap\n    case \"wrap-reverse\":\n      return Wrap.WrapReverse\n    default:\n      return Wrap.NoWrap\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/core-slot.ts",
    "content": "import { BaseRenderable, Renderable, type RenderableOptions } from \"../Renderable\"\nimport type { CliRenderer } from \"../renderer\"\nimport type { RenderContext } from \"../types\"\nimport { createSlotRegistry, SlotRegistry, type SlotRegistryOptions } from \"./registry\"\nimport type { Plugin, PluginContext, PluginErrorEvent, SlotMode } from \"./types\"\n\nexport type CoreSlotMode = SlotMode\n\ntype CoreSlotProps<TSlotName extends string, TData extends object> = {\n  [K in TSlotName]: TData\n}\n\nexport type CoreSlotRegistry<\n  TSlotName extends string,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> = SlotRegistry<BaseRenderable, CoreSlotProps<TSlotName, TData>, TContext>\n\nexport type CoreSlotRenderer<\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> = (ctx: Readonly<TContext>, data: TData) => BaseRenderable\n\nexport interface CoreManagedSlot<\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> {\n  render: CoreSlotRenderer<TContext, TData>\n  onActivate?: (ctx: Readonly<TContext>) => void\n  onDeactivate?: (ctx: Readonly<TContext>) => void\n  onDispose?: (ctx: Readonly<TContext>) => void\n}\n\nexport type CoreSlotContribution<\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> = CoreSlotRenderer<TContext, TData> | CoreManagedSlot<TContext, TData>\n\nexport interface CorePlugin<\n  TSlotName extends string,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> {\n  id: string\n  order?: number\n  setup?: (ctx: Readonly<TContext>, renderer: CliRenderer) => void\n  dispose?: () => void\n  slots: Partial<Record<TSlotName, CoreSlotContribution<TContext, TData>>>\n}\n\nexport interface CoreResolvedSlotRenderer<\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> {\n  id: string\n  renderer: CoreSlotRenderer<TContext, TData>\n}\n\ntype FallbackNodes = BaseRenderable | BaseRenderable[] | undefined\n\nexport type CoreSlotFailurePlaceholder<TContext extends PluginContext = PluginContext> = (\n  failure: PluginErrorEvent,\n  ctx: Readonly<TContext>,\n) => FallbackNodes\n\ntype CoreSlotOwnership = \"host\" | \"plugin\"\n\ntype WrappedCoreSlotRenderer<\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> = ((ctx: Readonly<TContext>, props: TData) => BaseRenderable) & {\n  __coreSlotOwnership?: CoreSlotOwnership\n  __coreManagedSlot?: CoreManagedSlot<TContext, TData>\n}\n\ninterface ResolvedCoreSlotEntry<\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> extends CoreResolvedSlotRenderer<TContext, TData> {\n  ownership: CoreSlotOwnership\n  managedSlot?: CoreManagedSlot<TContext, TData>\n}\n\ninterface SlotNodeState<\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> {\n  nodes: BaseRenderable[]\n  ownership: CoreSlotOwnership\n  managedSlot?: CoreManagedSlot<TContext, TData>\n  dataRef: TData\n}\n\nfunction isCoreManagedSlot<\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n>(contribution: CoreSlotContribution<TContext, TData>): contribution is CoreManagedSlot<TContext, TData> {\n  return typeof contribution === \"object\" && contribution !== null && \"render\" in contribution\n}\n\nfunction toCorePlugin<\n  TSlotName extends string,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n>(plugin: CorePlugin<TSlotName, TContext, TData>): Plugin<BaseRenderable, CoreSlotProps<TSlotName, TData>, TContext> {\n  const slots: Partial<Record<TSlotName, WrappedCoreSlotRenderer<TContext, TData>>> = {}\n\n  for (const [slotName, contribution] of Object.entries(plugin.slots) as Array<\n    [TSlotName, CoreSlotContribution<TContext, TData>]\n  >) {\n    const wrappedRenderer: WrappedCoreSlotRenderer<TContext, TData> = (ctx: Readonly<TContext>, data: TData) => {\n      if (isCoreManagedSlot(contribution)) {\n        return contribution.render(ctx, data)\n      }\n\n      return contribution(ctx, data)\n    }\n\n    if (isCoreManagedSlot(contribution)) {\n      wrappedRenderer.__coreSlotOwnership = \"plugin\"\n      wrappedRenderer.__coreManagedSlot = contribution\n    } else {\n      wrappedRenderer.__coreSlotOwnership = \"host\"\n    }\n\n    slots[slotName] = wrappedRenderer\n  }\n\n  return {\n    id: plugin.id,\n    order: plugin.order,\n    setup: plugin.setup,\n    dispose: plugin.dispose,\n    slots,\n  }\n}\n\nfunction asArray(value: FallbackNodes): BaseRenderable[] {\n  if (!value) {\n    return []\n  }\n\n  return Array.isArray(value) ? [...value] : [value]\n}\n\nfunction ensureValidNode(node: unknown, pluginId: string, mount: BaseRenderable): asserts node is BaseRenderable {\n  if (!node) {\n    throw new Error(`Plugin \"${pluginId}\" did not return a renderable node`)\n  }\n\n  if (typeof (node as { then?: unknown }).then === \"function\") {\n    throw new Error(`Plugin \"${pluginId}\" returned an async value. Core slots require synchronous renderers.`)\n  }\n\n  if (!(node instanceof BaseRenderable)) {\n    throw new Error(`Plugin \"${pluginId}\" must return a BaseRenderable`)\n  }\n\n  if (node === mount) {\n    throw new Error(`Plugin \"${pluginId}\" returned the slot mount container as its node`)\n  }\n\n  if (node.parent && node.parent !== mount) {\n    throw new Error(`Plugin \"${pluginId}\" returned a renderable already attached to another parent`)\n  }\n}\n\nexport function createCoreSlotRegistry<\n  TSlotName extends string,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n>(\n  renderer: CliRenderer,\n  context: TContext,\n  options: SlotRegistryOptions = {},\n): CoreSlotRegistry<TSlotName, TContext, TData> {\n  return createSlotRegistry<BaseRenderable, CoreSlotProps<TSlotName, TData>, TContext>(\n    renderer,\n    \"core:slot-registry\",\n    context,\n    options,\n  )\n}\n\nexport function registerCorePlugin<\n  TSlotName extends string,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n>(registry: CoreSlotRegistry<TSlotName, TContext, TData>, plugin: CorePlugin<TSlotName, TContext, TData>): () => void {\n  return registry.register(toCorePlugin(plugin))\n}\n\nexport function resolveCoreSlot<\n  TSlotName extends string,\n  K extends TSlotName,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n>(registry: CoreSlotRegistry<TSlotName, TContext, TData>, slot: K): Array<CoreResolvedSlotRenderer<TContext, TData>> {\n  return resolveCoreSlotEntries(registry, slot).map((entry) => {\n    return {\n      id: entry.id,\n      renderer: entry.renderer,\n    }\n  })\n}\n\nfunction resolveCoreSlotEntries<\n  TSlotName extends string,\n  K extends TSlotName,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n>(registry: CoreSlotRegistry<TSlotName, TContext, TData>, slot: K): Array<ResolvedCoreSlotEntry<TContext, TData>> {\n  return registry.resolveEntries(slot).map((entry) => {\n    const wrappedRenderer = entry.renderer as WrappedCoreSlotRenderer<TContext, TData>\n\n    return {\n      id: entry.id,\n      renderer: (ctx: Readonly<TContext>, data: TData) => wrappedRenderer(ctx, data),\n      ownership: wrappedRenderer.__coreSlotOwnership ?? \"host\",\n      managedSlot: wrappedRenderer.__coreManagedSlot,\n    }\n  })\n}\n\n// -- SlotRenderable ---------------------------------------------------------\n\nexport interface SlotRenderableOptions<\n  TSlotName extends string,\n  K extends TSlotName,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> extends RenderableOptions {\n  registry: CoreSlotRegistry<TSlotName, TContext, TData>\n  name: K\n  data?: TData\n  mode?: CoreSlotMode\n  fallback?: FallbackNodes | (() => FallbackNodes)\n  pluginFailurePlaceholder?: CoreSlotFailurePlaceholder<TContext>\n}\n\nexport class SlotRenderable<\n  TSlotName extends string = string,\n  TContext extends PluginContext = PluginContext,\n  TData extends object = Record<string, unknown>,\n> extends Renderable {\n  private _mode: CoreSlotMode\n  private _slotRegistry: CoreSlotRegistry<TSlotName, TContext, TData>\n  private _slotName: TSlotName\n  private _data: TData\n  private _fallbackOption: FallbackNodes | (() => FallbackNodes)\n  private _pluginFailurePlaceholder?: CoreSlotFailurePlaceholder<TContext>\n  private _disposed = false\n  private _mountedNodes: BaseRenderable[] = []\n  private _pluginNodes = new Map<string, SlotNodeState<TContext, TData>>()\n  private _activePluginIds = new Set<string>()\n  private _fallbackNodes: BaseRenderable[] | null = null\n  private _unsubscribe: (() => void) | null = null\n\n  constructor(ctx: RenderContext, options: SlotRenderableOptions<TSlotName, TSlotName, TContext, TData>) {\n    super(ctx, options)\n\n    this._slotRegistry = options.registry\n    this._slotName = options.name\n    this._data = options.data ?? ({} as TData)\n    this._mode = options.mode ?? \"append\"\n    this._fallbackOption = options.fallback\n    this._pluginFailurePlaceholder = options.pluginFailurePlaceholder\n\n    this._unsubscribe = this._slotRegistry.subscribe(() => this.refresh())\n\n    try {\n      this.refresh()\n    } catch (error) {\n      this._cleanupAll()\n      throw error\n    }\n  }\n\n  public get mode(): CoreSlotMode {\n    return this._mode\n  }\n\n  public set mode(value: CoreSlotMode) {\n    this._mode = value\n    this.refresh()\n  }\n\n  public get data(): TData {\n    return this._data\n  }\n\n  public set data(value: TData) {\n    this._data = value\n    this.refresh()\n  }\n\n  public refresh(): void {\n    if (this._disposed) {\n      return\n    }\n\n    const allEntries = resolveCoreSlotEntries(this._slotRegistry, this._slotName)\n    const activeEntries = this._mode === \"single_winner\" && allEntries.length > 0 ? [allEntries[0]] : allEntries\n    const nextActivePluginIds = new Set(activeEntries.map((entry) => entry.id))\n    const registeredPluginIds = new Set(allEntries.map((entry) => entry.id))\n\n    this._cleanupInactivePluginNodes(nextActivePluginIds, registeredPluginIds)\n\n    for (const entry of activeEntries) {\n      let state = this._pluginNodes.get(entry.id)\n      const shouldRender =\n        !state || (state.ownership === \"plugin\" && state.nodes.length === 0) || state.dataRef !== this._data\n\n      if (shouldRender) {\n        const previousState = state\n\n        try {\n          const node = entry.renderer(this._slotRegistry.context, this._data)\n          ensureValidNode(node, entry.id, this)\n          state = {\n            nodes: [node],\n            ownership: entry.ownership,\n            managedSlot: entry.managedSlot ?? state?.managedSlot,\n            dataRef: this._data,\n          }\n        } catch (error) {\n          const failure = this._slotRegistry.reportPluginError({\n            pluginId: entry.id,\n            slot: String(this._slotName),\n            phase: \"render\",\n            source: \"core\",\n            error,\n          })\n\n          state = {\n            nodes: this._resolvePluginFailurePlaceholder(failure),\n            ownership: \"host\",\n            managedSlot: entry.managedSlot ?? state?.managedSlot,\n            dataRef: this._data,\n          }\n        }\n\n        if (previousState) {\n          this._cleanupReplacedPluginNodes(previousState, state.nodes)\n        }\n\n        this._pluginNodes.set(entry.id, state)\n      }\n\n      if (!this._activePluginIds.has(entry.id)) {\n        const activeState = this._pluginNodes.get(entry.id)\n        if (activeState) {\n          this._callManagedHook(entry.id, activeState.managedSlot, \"onActivate\", \"setup\")\n        }\n      }\n    }\n\n    const desiredNodes: BaseRenderable[] = []\n\n    if (this._mode === \"append\" || activeEntries.length === 0) {\n      desiredNodes.push(...this._ensureFallbackNodes())\n    }\n\n    for (const entry of activeEntries) {\n      const state = this._pluginNodes.get(entry.id)\n      if (state) {\n        desiredNodes.push(...state.nodes)\n      }\n    }\n\n    if (this._mode !== \"append\" && desiredNodes.length === 0) {\n      desiredNodes.push(...this._ensureFallbackNodes())\n    }\n\n    this._reconcileMountedNodes(desiredNodes)\n    this._activePluginIds = nextActivePluginIds\n  }\n\n  protected override destroySelf(): void {\n    this._cleanupAll()\n  }\n\n  private _cleanupAll(): void {\n    if (this._disposed) {\n      return\n    }\n    this._disposed = true\n\n    this._unsubscribe?.()\n    this._unsubscribe = null\n\n    for (const [pluginId, state] of this._pluginNodes) {\n      if (this._activePluginIds.has(pluginId)) {\n        this._callManagedHook(pluginId, state.managedSlot, \"onDeactivate\", \"dispose\")\n      }\n\n      this._callManagedHook(pluginId, state.managedSlot, \"onDispose\", \"dispose\")\n\n      for (const node of state.nodes) {\n        this._detachNodeFromMount(node)\n      }\n\n      if (state.ownership === \"host\") {\n        for (const node of state.nodes) {\n          node.destroyRecursively()\n        }\n      }\n    }\n\n    this._pluginNodes.clear()\n    this._activePluginIds = new Set()\n\n    if (this._fallbackNodes) {\n      for (const node of this._fallbackNodes) {\n        node.destroyRecursively()\n      }\n      this._fallbackNodes = null\n    }\n\n    this._mountedNodes = []\n  }\n\n  private _ensureFallbackNodes(): BaseRenderable[] {\n    if (this._fallbackNodes !== null) {\n      return this._fallbackNodes\n    }\n\n    const source = typeof this._fallbackOption === \"function\" ? this._fallbackOption() : this._fallbackOption\n    const nodes = asArray(source)\n    for (const node of nodes) {\n      ensureValidNode(node, \"fallback\", this)\n    }\n\n    this._fallbackNodes = nodes\n    return this._fallbackNodes\n  }\n\n  private _callManagedHook(\n    pluginId: string,\n    managedSlot: CoreManagedSlot<TContext, TData> | undefined,\n    hook: \"onActivate\" | \"onDeactivate\" | \"onDispose\",\n    phase: \"setup\" | \"dispose\",\n  ): void {\n    const callback = managedSlot?.[hook]\n    if (!callback) {\n      return\n    }\n\n    try {\n      callback(this._slotRegistry.context)\n    } catch (error) {\n      this._slotRegistry.reportPluginError({\n        pluginId,\n        slot: String(this._slotName),\n        phase,\n        source: \"core\",\n        error,\n      })\n    }\n  }\n\n  private _detachNodeFromMount(node: BaseRenderable): void {\n    if (node.parent === this) {\n      this.remove(node.id)\n    }\n  }\n\n  private _cleanupInactivePluginNodes(nextActivePluginIds: Set<string>, registeredPluginIds: Set<string>): void {\n    for (const [pluginId, state] of this._pluginNodes) {\n      if (nextActivePluginIds.has(pluginId)) {\n        continue\n      }\n\n      if (this._activePluginIds.has(pluginId)) {\n        this._callManagedHook(pluginId, state.managedSlot, \"onDeactivate\", \"dispose\")\n      }\n\n      for (const node of state.nodes) {\n        this._detachNodeFromMount(node)\n      }\n\n      if (!registeredPluginIds.has(pluginId)) {\n        this._callManagedHook(pluginId, state.managedSlot, \"onDispose\", \"dispose\")\n\n        if (state.ownership === \"host\") {\n          for (const node of state.nodes) {\n            node.destroyRecursively()\n          }\n        }\n\n        this._pluginNodes.delete(pluginId)\n        continue\n      }\n\n      if (state.ownership === \"host\") {\n        for (const node of state.nodes) {\n          node.destroyRecursively()\n        }\n        this._pluginNodes.delete(pluginId)\n        continue\n      }\n\n      state.nodes = []\n    }\n  }\n\n  private _cleanupReplacedPluginNodes(\n    previousState: SlotNodeState<TContext, TData>,\n    nextNodes: BaseRenderable[],\n  ): void {\n    const retainedNodes = new Set(nextNodes)\n\n    for (const node of previousState.nodes) {\n      if (retainedNodes.has(node)) {\n        continue\n      }\n\n      this._detachNodeFromMount(node)\n\n      if (previousState.ownership === \"host\") {\n        node.destroyRecursively()\n      }\n    }\n  }\n\n  private _resolvePluginFailurePlaceholder(failure: PluginErrorEvent): BaseRenderable[] {\n    if (!this._pluginFailurePlaceholder) {\n      return []\n    }\n\n    try {\n      const placeholderSource = this._pluginFailurePlaceholder(failure, this._slotRegistry.context)\n      const placeholderNodes = asArray(placeholderSource)\n\n      for (const node of placeholderNodes) {\n        ensureValidNode(node, `${failure.pluginId}:error-placeholder`, this)\n      }\n\n      return placeholderNodes\n    } catch (placeholderError) {\n      this._slotRegistry.reportPluginError({\n        pluginId: failure.pluginId,\n        slot: String(this._slotName),\n        phase: \"error_placeholder\",\n        source: \"core\",\n        error: placeholderError,\n      })\n      return []\n    }\n  }\n\n  private _reconcileMountedNodes(desiredNodes: BaseRenderable[]): void {\n    const desiredNodeSet = new Set(desiredNodes)\n\n    for (const node of this._mountedNodes) {\n      if (!desiredNodeSet.has(node)) {\n        if (node.parent === this) {\n          this.remove(node.id)\n        }\n      }\n    }\n\n    for (let index = 0; index < desiredNodes.length; index++) {\n      const node = desiredNodes[index]\n\n      if (node.parent !== this) {\n        this.add(node, index)\n        continue\n      }\n\n      const childAtIndex = this.getChildren()[index]\n      if (childAtIndex?.id !== node.id) {\n        this.add(node, index)\n      }\n    }\n\n    this._mountedNodes = [...desiredNodes]\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/registry.ts",
    "content": "import type { CliRenderer } from \"../renderer\"\nimport type {\n  Plugin,\n  PluginContext,\n  PluginErrorEvent,\n  PluginErrorReport,\n  ResolvedSlotRenderer,\n  SlotRenderer,\n} from \"./types\"\n\nconst noop = () => {}\nconst DEFAULT_DEBUG_PLUGIN_ERRORS = false\nconst DEFAULT_MAX_PLUGIN_ERRORS = 100\n\nfunction normalizeError(error: unknown): Error {\n  if (error instanceof Error) {\n    return error\n  }\n\n  if (typeof error === \"string\") {\n    return new Error(error)\n  }\n\n  return new Error(`Unknown plugin error: ${String(error)}`)\n}\n\nexport interface SlotRegistryOptions {\n  onPluginError?: (event: PluginErrorEvent) => void\n  debugPluginErrors?: boolean\n  maxPluginErrors?: number\n}\n\ninterface RegisteredPlugin<TNode, TSlots extends object, TContext extends PluginContext = PluginContext> {\n  plugin: Plugin<TNode, TSlots, TContext>\n  registrationOrder: number\n  cachedOrder: number\n  cachedId: string\n}\n\nexport class SlotRegistry<TNode, TSlots extends object, TContext extends PluginContext = PluginContext> {\n  private plugins: RegisteredPlugin<TNode, TSlots, TContext>[] = []\n  private sortedPluginsCache: RegisteredPlugin<TNode, TSlots, TContext>[] | null = null\n  private listeners: Set<() => void> = new Set()\n  private errorListeners: Set<(event: PluginErrorEvent) => void> = new Set()\n  private pluginErrors: PluginErrorEvent[] = []\n  private registrationOrder = 0\n  private rendererInstance: CliRenderer\n  private hostContext: Readonly<TContext>\n  private options: Required<Pick<SlotRegistryOptions, \"debugPluginErrors\" | \"maxPluginErrors\">> &\n    Pick<SlotRegistryOptions, \"onPluginError\">\n\n  constructor(renderer: CliRenderer, context: TContext, options: SlotRegistryOptions = {}) {\n    this.rendererInstance = renderer\n    this.hostContext = context\n    this.options = {\n      debugPluginErrors: options.debugPluginErrors ?? DEFAULT_DEBUG_PLUGIN_ERRORS,\n      maxPluginErrors: options.maxPluginErrors ?? DEFAULT_MAX_PLUGIN_ERRORS,\n      onPluginError: options.onPluginError,\n    }\n  }\n\n  public get renderer(): CliRenderer {\n    return this.rendererInstance\n  }\n\n  public get context(): Readonly<TContext> {\n    return this.hostContext\n  }\n\n  public configure(options: SlotRegistryOptions): void {\n    if (\"debugPluginErrors\" in options) {\n      this.options.debugPluginErrors = options.debugPluginErrors ?? DEFAULT_DEBUG_PLUGIN_ERRORS\n    }\n\n    if (\"maxPluginErrors\" in options) {\n      this.options.maxPluginErrors = options.maxPluginErrors ?? DEFAULT_MAX_PLUGIN_ERRORS\n    }\n\n    if (\"onPluginError\" in options) {\n      this.options.onPluginError = options.onPluginError\n    }\n  }\n\n  public register(plugin: Plugin<TNode, TSlots, TContext>): () => void {\n    if (this.plugins.some((entry) => entry.plugin.id === plugin.id)) {\n      throw new Error(`Plugin with id \\\"${plugin.id}\\\" is already registered`)\n    }\n\n    try {\n      plugin.setup?.(this.hostContext, this.rendererInstance)\n    } catch (error) {\n      this.reportPluginError({\n        pluginId: plugin.id,\n        phase: \"setup\",\n        source: \"registry\",\n        error,\n      })\n\n      return noop\n    }\n\n    this.plugins.push({\n      plugin,\n      registrationOrder: this.registrationOrder++,\n      cachedOrder: plugin.order ?? 0,\n      cachedId: plugin.id,\n    })\n\n    this.invalidateSortedPluginsCache()\n    this.notifyListeners()\n\n    return () => {\n      this.unregister(plugin.id)\n    }\n  }\n\n  public unregister(id: string): boolean {\n    const index = this.plugins.findIndex((entry) => entry.plugin.id === id)\n    if (index === -1) {\n      return false\n    }\n\n    const [entry] = this.plugins.splice(index, 1)\n\n    this.invalidateSortedPluginsCache()\n\n    try {\n      entry?.plugin.dispose?.()\n    } catch (error) {\n      this.reportPluginError({\n        pluginId: id,\n        phase: \"dispose\",\n        source: \"registry\",\n        error,\n      })\n    }\n\n    this.notifyListeners()\n\n    return true\n  }\n\n  public updateOrder(id: string, order: number): boolean {\n    const entry = this.plugins.find((pluginEntry) => pluginEntry.plugin.id === id)\n    if (!entry) {\n      return false\n    }\n\n    if ((entry.plugin.order ?? 0) === order) {\n      return true\n    }\n\n    entry.plugin.order = order\n    entry.cachedOrder = order\n    this.invalidateSortedPluginsCache()\n    this.notifyListeners()\n    return true\n  }\n\n  public clear(): void {\n    if (this.plugins.length === 0) {\n      return\n    }\n\n    const plugins = [...this.plugins]\n    this.plugins = []\n    this.invalidateSortedPluginsCache()\n\n    for (const entry of plugins) {\n      try {\n        entry.plugin.dispose?.()\n      } catch (error) {\n        this.reportPluginError({\n          pluginId: entry.plugin.id,\n          phase: \"dispose\",\n          source: \"registry\",\n          error,\n        })\n      }\n    }\n\n    this.notifyListeners()\n  }\n\n  public subscribe(listener: () => void): () => void {\n    this.listeners.add(listener)\n    return () => {\n      this.listeners.delete(listener)\n    }\n  }\n\n  public onPluginError(listener: (event: PluginErrorEvent) => void): () => void {\n    this.errorListeners.add(listener)\n    return () => {\n      this.errorListeners.delete(listener)\n    }\n  }\n\n  public getPluginErrors(): readonly PluginErrorEvent[] {\n    return this.pluginErrors\n  }\n\n  public clearPluginErrors(): void {\n    this.pluginErrors = []\n  }\n\n  public reportPluginError(report: PluginErrorReport): PluginErrorEvent {\n    const event: PluginErrorEvent = {\n      pluginId: report.pluginId,\n      slot: report.slot,\n      phase: report.phase,\n      source: report.source ?? \"registry\",\n      error: normalizeError(report.error),\n      timestamp: Date.now(),\n    }\n\n    this.pluginErrors.push(event)\n    if (this.pluginErrors.length > this.options.maxPluginErrors) {\n      this.pluginErrors.splice(0, this.pluginErrors.length - this.options.maxPluginErrors)\n    }\n\n    if (this.options.debugPluginErrors) {\n      const slotLabel = event.slot ? ` slot=\\\"${event.slot}\\\"` : \"\"\n      console.debug(\n        `[SlotRegistry][PluginError] plugin=\\\"${event.pluginId}\\\" phase=\\\"${event.phase}\\\" source=\\\"${event.source}\\\"${slotLabel}`,\n      )\n      console.debug(event.error)\n    }\n\n    for (const listener of this.errorListeners) {\n      try {\n        listener(event)\n      } catch (error) {\n        console.error(\"Error in plugin error listener:\", error)\n      }\n    }\n\n    try {\n      this.options.onPluginError?.(event)\n    } catch (error) {\n      console.error(\"Error in plugin error callback:\", error)\n    }\n\n    return event\n  }\n\n  public resolve<K extends keyof TSlots>(slot: K): Array<SlotRenderer<TNode, TSlots[K], TContext>> {\n    return this.resolveEntries(slot).map((entry) => entry.renderer)\n  }\n\n  public resolveEntries<K extends keyof TSlots>(slot: K): Array<ResolvedSlotRenderer<TNode, TSlots[K], TContext>> {\n    const slotRenderers: Array<ResolvedSlotRenderer<TNode, TSlots[K], TContext>> = []\n\n    for (const entry of this.getSortedPlugins()) {\n      const renderer = entry.plugin.slots[slot]\n      if (renderer) {\n        slotRenderers.push({\n          id: entry.plugin.id,\n          renderer: renderer as SlotRenderer<TNode, TSlots[K], TContext>,\n        })\n      }\n    }\n\n    return slotRenderers\n  }\n\n  private getSortedPlugins(): RegisteredPlugin<TNode, TSlots, TContext>[] {\n    this.syncPluginSortMetadata()\n\n    if (this.sortedPluginsCache) {\n      return this.sortedPluginsCache\n    }\n\n    this.sortedPluginsCache = [...this.plugins].sort((left, right) => {\n      const leftOrder = left.cachedOrder\n      const rightOrder = right.cachedOrder\n\n      if (leftOrder !== rightOrder) {\n        return leftOrder - rightOrder\n      }\n\n      if (left.registrationOrder !== right.registrationOrder) {\n        return left.registrationOrder - right.registrationOrder\n      }\n\n      return left.cachedId.localeCompare(right.cachedId)\n    })\n\n    return this.sortedPluginsCache\n  }\n\n  private syncPluginSortMetadata(): void {\n    let hasChanges = false\n\n    for (const entry of this.plugins) {\n      const nextOrder = entry.plugin.order ?? 0\n      const nextId = entry.plugin.id\n\n      if (entry.cachedOrder !== nextOrder || entry.cachedId !== nextId) {\n        entry.cachedOrder = nextOrder\n        entry.cachedId = nextId\n        hasChanges = true\n      }\n    }\n\n    if (hasChanges) {\n      this.invalidateSortedPluginsCache()\n    }\n  }\n\n  private invalidateSortedPluginsCache(): void {\n    this.sortedPluginsCache = null\n  }\n\n  private notifyListeners(): void {\n    for (const listener of this.listeners) {\n      try {\n        listener()\n      } catch (error) {\n        console.error(\"Error in slot registry listener:\", error)\n      }\n    }\n  }\n}\n\nconst slotRegistriesByRenderer = new WeakMap<CliRenderer, Map<string, SlotRegistry<any, any, any>>>()\n\nfunction getSlotRegistryStore(renderer: CliRenderer): Map<string, SlotRegistry<any, any, any>> {\n  const existingStore = slotRegistriesByRenderer.get(renderer)\n  if (existingStore) {\n    return existingStore\n  }\n\n  const createdStore = new Map<string, SlotRegistry<any, any, any>>()\n  slotRegistriesByRenderer.set(renderer, createdStore)\n\n  renderer.once(\"destroy\", () => {\n    for (const registry of createdStore.values()) {\n      try {\n        registry.clear()\n      } catch (error) {\n        console.error(\"Error disposing slot registry:\", error)\n      }\n    }\n\n    createdStore.clear()\n    slotRegistriesByRenderer.delete(renderer)\n  })\n\n  return createdStore\n}\n\nexport function createSlotRegistry<TNode, TSlots extends object, TContext extends PluginContext = PluginContext>(\n  renderer: CliRenderer,\n  key: string,\n  context: TContext,\n  options: SlotRegistryOptions = {},\n): SlotRegistry<TNode, TSlots, TContext> {\n  const store = getSlotRegistryStore(renderer)\n  const existing = store.get(key)\n\n  if (existing) {\n    if (existing.context !== context) {\n      throw new Error(\n        `createSlotRegistry called with a different context for renderer key \"${key}\". Reuse the original context object.`,\n      )\n    }\n\n    const typedExisting = existing as SlotRegistry<TNode, TSlots, TContext>\n    typedExisting.configure(options)\n    return typedExisting\n  }\n\n  const created = new SlotRegistry<TNode, TSlots, TContext>(renderer, context, options)\n  store.set(key, created as SlotRegistry<any, any, any>)\n  return created\n}\n"
  },
  {
    "path": "packages/core/src/plugins/types.ts",
    "content": "import type { CliRenderer } from \"../renderer\"\n\nexport type PluginContext = object\n\nexport type SlotMode = \"append\" | \"replace\" | \"single_winner\"\n\nexport type PluginErrorPhase = \"setup\" | \"render\" | \"dispose\" | \"error_placeholder\"\n\nexport type PluginErrorSource = \"registry\" | \"core\" | (string & {})\n\nexport interface PluginErrorEvent {\n  pluginId: string\n  slot?: string\n  phase: PluginErrorPhase\n  source: PluginErrorSource\n  error: Error\n  timestamp: number\n}\n\nexport interface PluginErrorReport {\n  pluginId: string\n  slot?: string\n  phase: PluginErrorPhase\n  source?: PluginErrorSource\n  error: unknown\n}\n\nexport type SlotRenderer<TNode, TProps, TContext extends PluginContext = PluginContext> = (\n  ctx: Readonly<TContext>,\n  props: TProps,\n) => TNode\n\nexport interface Plugin<TNode, TSlots extends object, TContext extends PluginContext = PluginContext> {\n  id: string\n  order?: number\n  setup?: (ctx: Readonly<TContext>, renderer: CliRenderer) => void\n  dispose?: () => void\n  slots: {\n    [K in keyof TSlots]?: SlotRenderer<TNode, TSlots[K], TContext>\n  }\n}\n\nexport interface ResolvedSlotRenderer<TNode, TProps, TContext extends PluginContext = PluginContext> {\n  id: string\n  renderer: SlotRenderer<TNode, TProps, TContext>\n}\n"
  },
  {
    "path": "packages/core/src/post/effects.ts",
    "content": "import type { OptimizedBuffer } from \"../buffer\"\n\ninterface ActiveGlitch {\n  y: number\n  type: \"shift\" | \"flip\" | \"color\"\n  amount: number\n}\n\nexport class DistortionEffect {\n  // --- Configurable Parameters ---\n  public glitchChancePerSecond: number = 0.5\n  public maxGlitchLines: number = 3\n  public minGlitchDuration: number = 0.05\n  public maxGlitchDuration: number = 0.2\n  public maxShiftAmount: number = 10\n  public shiftFlipRatio: number = 0.6\n  public colorGlitchChance: number = 0.2\n\n  // --- Internal State ---\n  private lastGlitchTime: number = 0\n  private glitchDuration: number = 0\n  private activeGlitches: ActiveGlitch[] = []\n\n  constructor(options?: Partial<DistortionEffect>) {\n    if (options) {\n      Object.assign(this, options)\n    }\n  }\n\n  /**\n   * Applies the animated distortion/glitch effect to the buffer.\n   */\n  public apply(buffer: OptimizedBuffer, deltaTime: number): void {\n    const width = buffer.width\n    const height = buffer.height\n    const buf = buffer.buffers\n    // Note: Using internal timer based on deltaTime is more reliable than Date.now()\n\n    // Update glitch timer\n    this.lastGlitchTime += deltaTime\n\n    // End current glitch if duration is over\n    if (this.activeGlitches.length > 0 && this.lastGlitchTime >= this.glitchDuration) {\n      this.activeGlitches = []\n      this.glitchDuration = 0\n    }\n\n    // Chance to start a new glitch\n    if (this.activeGlitches.length === 0 && Math.random() < this.glitchChancePerSecond * deltaTime) {\n      this.lastGlitchTime = 0\n      this.glitchDuration = this.minGlitchDuration + Math.random() * (this.maxGlitchDuration - this.minGlitchDuration)\n      const numGlitches = 1 + Math.floor(Math.random() * this.maxGlitchLines)\n\n      for (let i = 0; i < numGlitches; i++) {\n        const y = Math.floor(Math.random() * height)\n        let type: ActiveGlitch[\"type\"]\n        let amount = 0\n\n        const typeRoll = Math.random()\n        if (typeRoll < this.colorGlitchChance) {\n          type = \"color\"\n        } else {\n          // Determine shift or flip based on remaining probability\n          const shiftRoll = (typeRoll - this.colorGlitchChance) / (1 - this.colorGlitchChance)\n          if (shiftRoll < this.shiftFlipRatio) {\n            type = \"shift\"\n            amount = Math.floor((Math.random() - 0.5) * 2 * this.maxShiftAmount)\n          } else {\n            type = \"flip\"\n          }\n        }\n\n        // Avoid glitching the same line twice in one burst\n        if (!this.activeGlitches.some((g) => g.y === y)) {\n          this.activeGlitches.push({ y, type, amount })\n        }\n      }\n    }\n\n    // Apply active glitches\n    if (this.activeGlitches.length > 0) {\n      // Create temporary arrays lazily if needed (minor optimization for shift/flip)\n      let tempChar: Uint32Array | null = null\n      let tempFg: Float32Array | null = null\n      let tempBg: Float32Array | null = null\n      let tempAttr: Uint8Array | null = null\n\n      for (const glitch of this.activeGlitches) {\n        const y = glitch.y\n        // Ensure y is within bounds (safer)\n        if (y < 0 || y >= height) continue\n        const baseIndex = y * width\n\n        if (glitch.type === \"shift\" || glitch.type === \"flip\") {\n          // Lazily create temp buffers only when needed for shift/flip\n          if (!tempChar) {\n            tempChar = new Uint32Array(width)\n            tempFg = new Float32Array(width * 4)\n            tempBg = new Float32Array(width * 4)\n            tempAttr = new Uint8Array(width)\n          }\n\n          // 1. Copy original row data to temp buffers\n          try {\n            tempChar.set(buf.char.subarray(baseIndex, baseIndex + width))\n            tempFg!.set(buf.fg.subarray(baseIndex * 4, (baseIndex + width) * 4))\n            tempBg!.set(buf.bg.subarray(baseIndex * 4, (baseIndex + width) * 4))\n            tempAttr!.set(buf.attributes.subarray(baseIndex, baseIndex + width))\n          } catch (e) {\n            // Handle potential range errors if buffer size changes unexpectedly\n            console.error(`Error copying row ${y} for distortion:`, e)\n            continue\n          }\n\n          if (glitch.type === \"shift\") {\n            const shift = glitch.amount\n            for (let x = 0; x < width; x++) {\n              const srcX = (x - shift + width) % width // Wrap around shift\n              const destIndex = baseIndex + x\n              const srcTempIndex = srcX\n\n              buf.char[destIndex] = tempChar[srcTempIndex]\n              buf.attributes[destIndex] = tempAttr![srcTempIndex]\n\n              const destColorIndex = destIndex * 4\n              const srcTempColorIndex = srcTempIndex * 4\n\n              buf.fg.set(tempFg!.subarray(srcTempColorIndex, srcTempColorIndex + 4), destColorIndex)\n              buf.bg.set(tempBg!.subarray(srcTempColorIndex, srcTempColorIndex + 4), destColorIndex)\n            }\n          } else {\n            // type === 'flip'\n            for (let x = 0; x < width; x++) {\n              const srcX = width - 1 - x // Flipped index\n              const destIndex = baseIndex + x\n              const srcTempIndex = srcX\n\n              buf.char[destIndex] = tempChar[srcTempIndex]\n              buf.attributes[destIndex] = tempAttr![srcTempIndex]\n\n              const destColorIndex = destIndex * 4\n              const srcTempColorIndex = srcTempIndex * 4\n\n              buf.fg.set(tempFg!.subarray(srcTempColorIndex, srcTempColorIndex + 4), destColorIndex)\n              buf.bg.set(tempBg!.subarray(srcTempColorIndex, srcTempColorIndex + 4), destColorIndex)\n            }\n          }\n        } else if (glitch.type === \"color\") {\n          const glitchStart = Math.floor(Math.random() * width)\n          // Make glitch length at least 1 pixel, up to the rest of the line\n          const maxPossibleLength = width - glitchStart\n          // Introduce more variability: sometimes short, sometimes long, but not always full width\n          let glitchLength = Math.floor(Math.random() * maxPossibleLength) + 1\n          if (Math.random() < 0.2) {\n            // 20% chance of a shorter, more intense glitch segment\n            glitchLength = Math.floor(Math.random() * (width / 4)) + 1\n          }\n          glitchLength = Math.min(glitchLength, maxPossibleLength)\n\n          for (let x = glitchStart; x < glitchStart + glitchLength; x++) {\n            if (x >= width) break // Boundary check\n\n            const destIndex = baseIndex + x\n            const destColorIndex = destIndex * 4\n\n            let rFg, gFg, bFg, rBg, gBg, bBg\n\n            // More varied and \"glitchy\" colors\n            const colorMode = Math.random()\n            if (colorMode < 0.33) {\n              // Pure random\n              rFg = Math.random()\n              gFg = Math.random()\n              bFg = Math.random()\n              rBg = Math.random()\n              gBg = Math.random()\n              bBg = Math.random()\n            } else if (colorMode < 0.66) {\n              // Single channel emphasis or block color\n              const emphasis = Math.random()\n              if (emphasis < 0.25) {\n                rFg = Math.random()\n                gFg = 0\n                bFg = 0\n              } // Red\n              else if (emphasis < 0.5) {\n                rFg = 0\n                gFg = Math.random()\n                bFg = 0\n              } // Green\n              else if (emphasis < 0.75) {\n                rFg = 0\n                gFg = 0\n                bFg = Math.random()\n              } // Blue\n              else {\n                // Bright glitch color\n                const glitchColorRoll = Math.random()\n                if (glitchColorRoll < 0.33) {\n                  rFg = 1\n                  gFg = 0\n                  bFg = 1\n                } // Magenta\n                else if (glitchColorRoll < 0.66) {\n                  rFg = 0\n                  gFg = 1\n                  bFg = 1\n                } // Cyan\n                else {\n                  rFg = 1\n                  gFg = 1\n                  bFg = 0\n                } // Yellow\n              }\n              // Background can be inverted or similar to FG\n              if (Math.random() < 0.5) {\n                rBg = 1 - rFg\n                gBg = 1 - gFg\n                bBg = 1 - bFg\n              } else {\n                rBg = rFg * (Math.random() * 0.5 + 0.2) // Darker shade of fg\n                gBg = gFg * (Math.random() * 0.5 + 0.2)\n                bBg = bFg * (Math.random() * 0.5 + 0.2)\n              }\n            } else {\n              // Inverted or high contrast\n              rFg = Math.random() > 0.5 ? 1 : 0\n              gFg = Math.random() > 0.5 ? 1 : 0\n              bFg = Math.random() > 0.5 ? 1 : 0\n              rBg = 1 - rFg\n              gBg = 1 - gFg\n              bBg = 1 - bFg\n            }\n\n            buf.fg[destColorIndex] = rFg\n            buf.fg[destColorIndex + 1] = gFg\n            buf.fg[destColorIndex + 2] = bFg\n            // Keep alpha buf.fg[destColorIndex + 3]\n\n            buf.bg[destColorIndex] = rBg\n            buf.bg[destColorIndex + 1] = gBg\n            buf.bg[destColorIndex + 2] = bBg\n            // Keep alpha buf.bg[destColorIndex + 3]\n          }\n        }\n      }\n    }\n  }\n}\n\n/**\n * Applies a vignette effect by darkening the corners, optimized with precomputation.\n * Uses native colorMatrix with a zero matrix for attenuation.\n */\nexport class VignetteEffect {\n  private _strength: number\n  // Stores packed cell masks [x, y, attenuation] per pixel\n  private precomputedAttenuationCellMask: Float32Array | null = null\n  private cachedWidth: number = -1\n  private cachedHeight: number = -1\n  // Zero matrix for attenuation (maps everything toward black based on strength)\n  private static zeroMatrix = new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])\n\n  constructor(strength: number = 0.5) {\n    this._strength = strength\n  }\n\n  public set strength(newStrength: number) {\n    this._strength = Math.max(0, newStrength) // Ensure strength is non-negative\n    // Invalidate cached cell masks when strength changes\n    this.cachedWidth = -1\n    this.cachedHeight = -1\n    this.precomputedAttenuationCellMask = null\n  }\n\n  public get strength(): number {\n    return this._strength\n  }\n\n  private _computeFactors(width: number, height: number): void {\n    this.precomputedAttenuationCellMask = new Float32Array(width * height * 3)\n    const centerX = width / 2\n    const centerY = height / 2\n    const maxDistSq = centerX * centerX + centerY * centerY\n    const safeMaxDistSq = maxDistSq === 0 ? 1 : maxDistSq // Avoid division by zero\n    const strength = this._strength\n    let i = 0\n\n    for (let y = 0; y < height; y++) {\n      const dy = y - centerY\n      const dySq = dy * dy\n      for (let x = 0; x < width; x++) {\n        const dx = x - centerX\n        const distSq = dx * dx + dySq\n        // Calculate base attenuation (0 to 1 based on distance)\n        const baseAttenuation = Math.min(1, distSq / safeMaxDistSq)\n        // Precompute final attenuation value including strength\n        const attenuation = baseAttenuation * strength\n        this.precomputedAttenuationCellMask[i++] = x\n        this.precomputedAttenuationCellMask[i++] = y\n        this.precomputedAttenuationCellMask[i++] = attenuation\n      }\n    }\n    this.cachedWidth = width\n    this.cachedHeight = height\n  }\n\n  /**\n   * Applies the vignette effect using native colorMatrix with a zero matrix.\n   * The zero matrix maps all colors to black, and the attenuation cell masks\n   * control how much of the effect is applied (strength-based blending).\n   */\n  public apply(buffer: OptimizedBuffer): void {\n    const width = buffer.width\n    const height = buffer.height\n\n    // Recompute attenuation cell masks if dimensions changed, strength changed,\n    // or factors haven't been computed yet\n    if (width !== this.cachedWidth || height !== this.cachedHeight || !this.precomputedAttenuationCellMask) {\n      this._computeFactors(width, height)\n    }\n\n    // Use colorMatrix with zero matrix to apply attenuation\n    // colorMatrix blends: result = original + (transformed - original) × strength\n    // With zero matrix: transformed = 0\n    // Result = original + (0 - original) × attenuation = original × (1 - attenuation)\n    buffer.colorMatrix(VignetteEffect.zeroMatrix, this.precomputedAttenuationCellMask!, 1.0, 3)\n  }\n}\n\n/**\n * Simple Perlin noise implementation for procedural cloud generation.\n * Uses gradient vectors and smooth interpolation.\n */\nclass PerlinNoise {\n  private perm: Uint8Array\n  private grad3: number[][] = [\n    [1, 1, 0],\n    [-1, 1, 0],\n    [1, -1, 0],\n    [-1, -1, 0],\n    [1, 0, 1],\n    [-1, 0, 1],\n    [1, 0, -1],\n    [-1, 0, -1],\n    [0, 1, 1],\n    [0, -1, 1],\n    [0, 1, -1],\n    [0, -1, -1],\n  ]\n\n  constructor() {\n    // Permutation table for pseudo-random gradient selection\n    this.perm = new Uint8Array(512)\n    const p = new Uint8Array(256)\n    for (let i = 0; i < 256; i++) {\n      p[i] = i\n    }\n    // Shuffle\n    for (let i = 255; i > 0; i--) {\n      const r = Math.floor(Math.random() * (i + 1))\n      ;[p[i], p[r]] = [p[r], p[i]]\n    }\n    // Duplicate for overflow handling\n    for (let i = 0; i < 512; i++) {\n      this.perm[i] = p[i & 255]\n    }\n  }\n\n  private dot(g: number[], x: number, y: number, z: number): number {\n    return g[0] * x + g[1] * y + g[2] * z\n  }\n\n  private mix(a: number, b: number, t: number): number {\n    return a + (b - a) * t\n  }\n\n  private fade(t: number): number {\n    return t * t * t * (t * (t * 6 - 15) + 10)\n  }\n\n  /**\n   * 3D Perlin noise at coordinates (x, y, z)\n   * Returns value in range [-1, 1]\n   */\n  noise3d(x: number, y: number, z: number): number {\n    // Find unit grid cell containing point\n    const X = Math.floor(x) & 255\n    const Y = Math.floor(y) & 255\n    const Z = Math.floor(z) & 255\n\n    // Get relative xyz coordinates of point within that cell\n    const xf = x - Math.floor(x)\n    const yf = y - Math.floor(y)\n    const zf = z - Math.floor(z)\n\n    // Compute fade curves for each of x, y, z\n    const u = this.fade(xf)\n    const v = this.fade(yf)\n    const w = this.fade(zf)\n\n    // Hash coordinates of the 8 cube corners\n    const A = this.perm[X] + Y\n    const AA = this.perm[A] + Z\n    const AB = this.perm[A + 1] + Z\n    const B = this.perm[X + 1] + Y\n    const BA = this.perm[B] + Z\n    const BB = this.perm[B + 1] + Z\n\n    // Add blended results from 8 corners of the cube\n    let res = this.mix(\n      this.mix(\n        this.mix(\n          this.dot(this.grad3[this.perm[AA] % 12], xf, yf, zf),\n          this.dot(this.grad3[this.perm[BA] % 12], xf - 1, yf, zf),\n          u,\n        ),\n        this.mix(\n          this.dot(this.grad3[this.perm[AB] % 12], xf, yf - 1, zf),\n          this.dot(this.grad3[this.perm[BB] % 12], xf - 1, yf - 1, zf),\n          u,\n        ),\n        v,\n      ),\n      this.mix(\n        this.mix(\n          this.dot(this.grad3[this.perm[AA + 1] % 12], xf, yf, zf - 1),\n          this.dot(this.grad3[this.perm[BA + 1] % 12], xf - 1, yf, zf - 1),\n          u,\n        ),\n        this.mix(\n          this.dot(this.grad3[this.perm[AB + 1] % 12], xf, yf - 1, zf - 1),\n          this.dot(this.grad3[this.perm[BB + 1] % 12], xf - 1, yf - 1, zf - 1),\n          u,\n        ),\n        v,\n      ),\n      w,\n    )\n\n    return res\n  }\n}\n\n/**\n * Applies animated cloud shadows using Perlin noise.\n * Darkens the background buffer based on procedural cloud density.\n */\nexport class CloudsEffect {\n  private noise: PerlinNoise\n  private _scale: number\n  private _speed: number\n  private _density: number\n  private _darkness: number\n  private time: number = 0\n\n  constructor(scale: number = 0.02, speed: number = 0.5, density: number = 0.6, darkness: number = 0.7) {\n    this.noise = new PerlinNoise()\n    this._scale = scale\n    this._speed = speed\n    this._density = density\n    this._darkness = darkness\n  }\n\n  public set scale(newScale: number) {\n    this._scale = Math.max(0.001, newScale)\n  }\n  public get scale(): number {\n    return this._scale\n  }\n\n  public set speed(newSpeed: number) {\n    this._speed = Math.max(0, newSpeed)\n  }\n  public get speed(): number {\n    return this._speed\n  }\n\n  public set density(newDensity: number) {\n    this._density = Math.max(0, Math.min(1, newDensity))\n  }\n  public get density(): number {\n    return this._density\n  }\n\n  public set darkness(newDarkness: number) {\n    this._darkness = Math.max(0, Math.min(1, newDarkness))\n  }\n  public get darkness(): number {\n    return this._darkness\n  }\n\n  /**\n   * Applies cloud shadow effect using Perlin noise mask with native colorMatrix.\n   * Uses FBM (Fractal Brownian Motion) for detailed clouds, offloaded to native code.\n   */\n  public apply(buffer: OptimizedBuffer, deltaTime: number): void {\n    const width = buffer.width\n    const height = buffer.height\n\n    // Update time for animation\n    this.time += deltaTime * this._speed\n\n    const scale = this._scale\n    const timeOffset = this.time\n\n    // Build cell mask with per-pixel attenuation values\n    // Format: [x, y, attenuation] for each pixel\n    const cellMask = new Float32Array(width * height * 3)\n    let maskIdx = 0\n\n    for (let y = 0; y < height; y++) {\n      for (let x = 0; x < width; x++) {\n        // FBM - combine multiple octaves for detail\n        let noiseValue = 0\n        let amplitude = 1\n        let frequency = 1\n        let maxValue = 0\n\n        // 4 octaves of noise\n        for (let i = 0; i < 4; i++) {\n          const nx = (x * scale * frequency + timeOffset) * 0.5\n          const ny = y * scale * frequency * 0.5\n          const nz = timeOffset * 0.3 // Use time as z for evolution\n\n          noiseValue += this.noise.noise3d(nx, ny, nz) * amplitude\n          maxValue += amplitude\n          amplitude *= 0.5\n          frequency *= 2\n        }\n\n        // Normalize to [0, 1]\n        noiseValue = (noiseValue / maxValue + 1) * 0.5\n\n        // Apply density threshold - lower density = more clouds\n        const cloudDensity = Math.max(0, noiseValue - (1 - this._density))\n\n        // Scale to actual darkness (0 = no change, 1 = full darkness)\n        const attenuation = cloudDensity * this._darkness\n\n        // Add to cell mask\n        cellMask[maskIdx++] = x\n        cellMask[maskIdx++] = y\n        cellMask[maskIdx++] = attenuation\n      }\n    }\n\n    // Use native colorMatrix with zero matrix to apply attenuation\n    // Zero matrix: transformed = 0 (black)\n    // Result = original + (0 - original) × attenuation = original × (1 - attenuation)\n    const zeroMatrix = new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])\n    buffer.colorMatrix(zeroMatrix, cellMask, 1.0, 2) // target = 2 (background only)\n  }\n}\n\n/**\n * Applies animated flames rising from the bottom using Perlin noise.\n * Creates warm fire effect that fades as it rises.\n */\nexport class FlamesEffect {\n  private noise: PerlinNoise\n  private _scale: number\n  private _speed: number\n  private _intensity: number\n  private time: number = 0\n\n  constructor(scale: number = 0.03, speed: number = 0.02, intensity: number = 0.8) {\n    this.noise = new PerlinNoise()\n    this._scale = scale\n    this._speed = speed\n    this._intensity = intensity\n  }\n\n  public set scale(newScale: number) {\n    this._scale = Math.max(0.001, newScale)\n  }\n  public get scale(): number {\n    return this._scale\n  }\n\n  public set speed(newSpeed: number) {\n    this._speed = Math.max(0, newSpeed)\n  }\n  public get speed(): number {\n    return this._speed\n  }\n\n  public set intensity(newIntensity: number) {\n    this._intensity = Math.max(0, Math.min(1, newIntensity))\n  }\n  public get intensity(): number {\n    return this._intensity\n  }\n\n  /**\n   * Applies flame effect rising from bottom using Perlin noise.\n   * Flames get cooler (redder) and fade as they rise.\n   */\n  public apply(buffer: OptimizedBuffer, deltaTime: number): void {\n    const width = buffer.width\n    const height = buffer.height\n    const bg = buffer.buffers.bg\n\n    // Update time for animation\n    this.time += deltaTime * this._speed\n\n    const scale = this._scale\n    const timeOffset = this.time\n\n    for (let y = 0; y < height; y++) {\n      // Calculate height factor - stronger at bottom (1.0), fading toward top (0.0)\n      const heightFactor = 1.0 - y / height\n\n      for (let x = 0; x < width; x++) {\n        // FBM - combine multiple octaves for organic flame detail\n        let noiseValue = 0\n        let amplitude = 1\n        let frequency = 1\n        let maxValue = 0\n\n        // 3 octaves of noise for flames\n        for (let i = 0; i < 3; i++) {\n          // Flip y coordinate so flames rise from bottom\n          const nx = (x * scale * frequency + timeOffset) * 0.5\n          const ny = (height - y) * scale * frequency * 2 * 0.5\n          const nz = timeOffset * 2.0 // Faster z evolution for more flicker\n\n          noiseValue += this.noise.noise3d(nx, ny, nz) * amplitude\n          maxValue += amplitude\n          amplitude *= 0.5\n          frequency *= 2\n        }\n\n        // Normalize to [0, 1]\n        noiseValue = (noiseValue / maxValue + 1) * 0.5\n\n        // Combine with height factor for rising fade effect\n        const flameIntensity = noiseValue * heightFactor * this._intensity\n\n        if (flameIntensity > 0) {\n          const colorIndex = (y * width + x) * 4\n\n          // Fire color gradient based on intensity\n          // High intensity = white/yellow (hottest), low = red (coolest)\n          let r: number, g: number, b: number\n\n          if (flameIntensity > 0.7) {\n            // White/yellow hot core\n            r = 1.0\n            g = 1.0\n            b = 0.3 + (flameIntensity - 0.7) * 2.3 // 0.3 to 1.0\n          } else if (flameIntensity > 0.4) {\n            // Orange\n            r = 1.0\n            g = 0.5 + (flameIntensity - 0.4) * 1.67 // 0.5 to 1.0\n            b = 0.0\n          } else {\n            // Red fading to dark\n            r = 0.3 + flameIntensity * 1.75 // 0.3 to 1.0\n            g = flameIntensity * 0.5 // 0.0 to 0.2\n            b = 0.0\n          }\n\n          // Blend with existing background\n          bg[colorIndex] = Math.max(bg[colorIndex], r * flameIntensity)\n          bg[colorIndex + 1] = Math.max(bg[colorIndex + 1], g * flameIntensity)\n          bg[colorIndex + 2] = Math.max(bg[colorIndex + 2], b * flameIntensity)\n        }\n      }\n    }\n  }\n}\n\n/**\n * Applies a CRT rolling bar effect - a horizontal bar that slowly scans down the screen.\n * Simulates the classic CRT monitor rolling bar artifact.\n */\nexport class CRTRollingBarEffect {\n  private _speed: number\n  private _height: number\n  private _intensity: number\n  private _fadeDistance: number\n  private position: number = 0\n\n  constructor(speed: number = 0.5, height: number = 0.15, intensity: number = 0.3, fadeDistance: number = 0.3) {\n    this._speed = speed\n    this._height = Math.max(0.01, Math.min(0.5, height))\n    this._intensity = Math.max(0, Math.min(1, intensity))\n    this._fadeDistance = Math.max(0, Math.min(1, fadeDistance))\n  }\n\n  public set speed(newSpeed: number) {\n    this._speed = newSpeed\n  }\n  public get speed(): number {\n    return this._speed\n  }\n\n  public set height(newHeight: number) {\n    this._height = Math.max(0.01, Math.min(0.5, newHeight))\n  }\n  public get height(): number {\n    return this._height\n  }\n\n  public set intensity(newIntensity: number) {\n    this._intensity = Math.max(0, Math.min(1, newIntensity))\n  }\n  public get intensity(): number {\n    return this._intensity\n  }\n\n  public set fadeDistance(newFadeDistance: number) {\n    this._fadeDistance = Math.max(0, Math.min(1, newFadeDistance))\n  }\n  public get fadeDistance(): number {\n    return this._fadeDistance\n  }\n\n  /**\n   * Applies the rolling bar effect to the buffer.\n   * Creates a smooth horizontal bar that scans down the screen with a bell-curve gradient.\n   * The bar has a bright center that smoothly fades to the edges.\n   */\n  public apply(buffer: OptimizedBuffer, deltaTime: number): void {\n    const width = buffer.width\n    const height = buffer.height\n    const fg = buffer.buffers.fg\n    const bg = buffer.buffers.bg\n\n    // Update bar position (convert deltaTime from ms to seconds)\n    this.position += (deltaTime / 1000) * this._speed\n    // Wrap position to keep it rolling continuously\n    const cycleHeight = height + this._height * height * 2\n    this.position = this.position % cycleHeight\n\n    const barPixelHeight = this._height * height\n    const fadePixelDistance = this._fadeDistance * barPixelHeight\n    const totalEffectHeight = barPixelHeight + fadePixelDistance * 2\n    const effectCenter = this.position - totalEffectHeight / 2 + barPixelHeight / 2\n\n    for (let y = 0; y < height; y++) {\n      // Calculate distance from the bar center\n      const distFromCenter = Math.abs(y - effectCenter)\n\n      // Create a smooth bell-curve effect\n      // Full intensity at center, smooth falloff using Gaussian-like curve\n      let barFactor = 0\n\n      if (distFromCenter <= totalEffectHeight / 2) {\n        // Normalize distance to 0-1 range across the whole effect\n        const normalizedDist = distFromCenter / (totalEffectHeight / 2)\n        // Cosine falloff: 1 at center, 0 at edge\n        // cos(0) = 1, cos(π/2) = 0\n        barFactor = Math.cos((normalizedDist * Math.PI) / 2)\n      }\n\n      if (barFactor > 0.001) {\n        const rowMultiplier = 1 + this._intensity * barFactor\n\n        for (let x = 0; x < width; x++) {\n          const colorIndex = (y * width + x) * 4\n\n          // Brighten foreground\n          fg[colorIndex] = Math.min(1, fg[colorIndex] * rowMultiplier)\n          fg[colorIndex + 1] = Math.min(1, fg[colorIndex + 1] * rowMultiplier)\n          fg[colorIndex + 2] = Math.min(1, fg[colorIndex + 2] * rowMultiplier)\n\n          // Brighten background\n          bg[colorIndex] = Math.min(1, bg[colorIndex] * rowMultiplier)\n          bg[colorIndex + 1] = Math.min(1, bg[colorIndex + 1] * rowMultiplier)\n          bg[colorIndex + 2] = Math.min(1, bg[colorIndex + 2] * rowMultiplier)\n        }\n      }\n    }\n  }\n}\n\n/**\n * Applies animated rainbow colors to cells with white foreground.\n * Cycles through HSV hue spectrum over time.\n */\nexport class RainbowTextEffect {\n  private _speed: number\n  private _saturation: number\n  private _value: number\n  private _repeats: number\n  private time: number = 0\n\n  constructor(speed: number = 0.01, saturation: number = 1.0, value: number = 1.0, repeats: number = 3.0) {\n    this._speed = speed\n    this._saturation = saturation\n    this._value = value\n    this._repeats = repeats\n  }\n\n  public set speed(newSpeed: number) {\n    this._speed = Math.max(0, newSpeed)\n  }\n  public get speed(): number {\n    return this._speed\n  }\n\n  public set saturation(newSaturation: number) {\n    this._saturation = Math.max(0, Math.min(1, newSaturation))\n  }\n  public get saturation(): number {\n    return this._saturation\n  }\n\n  public set value(newValue: number) {\n    this._value = Math.max(0, Math.min(1, newValue))\n  }\n  public get value(): number {\n    return this._value\n  }\n\n  public set repeats(newRepeats: number) {\n    this._repeats = Math.max(0.1, newRepeats)\n  }\n  public get repeats(): number {\n    return this._repeats\n  }\n\n  /**\n   * Converts HSV color to RGB\n   * @param h - Hue [0, 1]\n   * @param s - Saturation [0, 1]\n   * @param v - Value [0, 1]\n   * @returns [r, g, b] each in [0, 1]\n   */\n  private hsvToRgb(h: number, s: number, v: number): [number, number, number] {\n    let r = 0,\n      g = 0,\n      b = 0\n\n    const i = Math.floor(h * 6)\n    const f = h * 6 - i\n    const p = v * (1 - s)\n    const q = v * (1 - f * s)\n    const t = v * (1 - (1 - f) * s)\n\n    switch (i % 6) {\n      case 0:\n        r = v\n        g = t\n        b = p\n        break\n      case 1:\n        r = q\n        g = v\n        b = p\n        break\n      case 2:\n        r = p\n        g = v\n        b = t\n        break\n      case 3:\n        r = p\n        g = q\n        b = v\n        break\n      case 4:\n        r = t\n        g = p\n        b = v\n        break\n      case 5:\n        r = v\n        g = p\n        b = q\n        break\n    }\n\n    return [r, g, b]\n  }\n\n  /**\n   * Applies rainbow colors to cells with white foreground.\n   * White is defined as R, G, B all >= 0.9\n   */\n  public apply(buffer: OptimizedBuffer, deltaTime: number): void {\n    const width = buffer.width\n    const height = buffer.height\n    const fg = buffer.buffers.fg\n\n    // Update time for animation\n    this.time += deltaTime * this._speed\n\n    const saturation = this._saturation\n    const value = this._value\n    const repeats = this._repeats\n\n    // 25 degree angle for diagonal rainbow\n    const angleRad = (25 * Math.PI) / 180\n    const cosAngle = Math.cos(angleRad)\n    const sinAngle = Math.sin(angleRad)\n\n    // Define white threshold\n    const whiteThreshold = 0.9\n\n    for (let y = 0; y < height; y++) {\n      for (let x = 0; x < width; x++) {\n        const colorIndex = (y * width + x) * 4\n\n        const r = fg[colorIndex]\n        const g = fg[colorIndex + 1]\n        const b = fg[colorIndex + 2]\n\n        // Check if foreground is white-ish (all components >= threshold)\n        if (r >= whiteThreshold && g >= whiteThreshold && b >= whiteThreshold) {\n          // Calculate hue based on position projected at 25-degree angle\n          // Creates a diagonal moving rainbow wave effect\n          const projection = x * cosAngle + y * sinAngle\n          const maxProjection = width * cosAngle + height * sinAngle\n          const hue = ((projection / maxProjection) * repeats + this.time * 0.1) % 1.0\n\n          // Convert HSV to RGB\n          const [newR, newG, newB] = this.hsvToRgb(hue, saturation, value)\n\n          fg[colorIndex] = newR\n          fg[colorIndex + 1] = newG\n          fg[colorIndex + 2] = newB\n          // Keep alpha unchanged\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/post/filters.ts",
    "content": "import type { OptimizedBuffer } from \"../buffer.js\"\n\n/**\n * Applies a scanline effect by darkening every nth row using native color matrix.\n * Only affects the background buffer to maintain text readability.\n */\nexport function applyScanlines(buffer: OptimizedBuffer, strength: number = 0.8, step: number = 2): void {\n  if (strength === 1.0 || step < 1) return\n\n  const width = buffer.width\n  const height = buffer.height\n\n  // Calculate number of affected rows\n  const affectedRows = Math.ceil(height / step)\n  const cellCount = width * affectedRows\n  const cellMask = new Float32Array(cellCount * 3)\n\n  let maskIdx = 0\n  for (let y = 0; y < height; y += step) {\n    for (let x = 0; x < width; x++) {\n      cellMask[maskIdx++] = x\n      cellMask[maskIdx++] = y\n      cellMask[maskIdx++] = 1.0 // full strength\n    }\n  }\n\n  // Gain matrix to scale down background colors\n  const s = strength\n  const matrix = new Float32Array([\n    s,\n    0,\n    0,\n    0, // Row 0: Red output\n    0,\n    s,\n    0,\n    0, // Row 1: Green output\n    0,\n    0,\n    s,\n    0, // Row 2: Blue output\n    0,\n    0,\n    0,\n    1, // Row 3: Alpha output (identity)\n  ])\n\n  // Apply only to background buffer (target = 2)\n  buffer.colorMatrix(matrix, cellMask, 1.0, 2)\n}\n\n/**\n * Inverts the colors in the buffer using native color matrix.\n * Uses negative matrix with alpha offset: output = 1.0 - input for each RGB channel.\n */\nexport function applyInvert(buffer: OptimizedBuffer, strength: number = 1.0): void {\n  if (strength === 0.0) return\n\n  // Invert matrix: output = -1*input + 1*alpha = 1.0 - input (assuming alpha=1.0)\n  // Row format: [R_coeff, G_coeff, B_coeff, A_coeff]\n  const matrix = new Float32Array([\n    -1,\n    0,\n    0,\n    1, // Row 0: Red output = -1*R + 0*G + 0*B + 1*A = 1 - R\n    0,\n    -1,\n    0,\n    1, // Row 1: Green output = 1 - G\n    0,\n    0,\n    -1,\n    1, // Row 2: Blue output = 1 - B\n    0,\n    0,\n    0,\n    1, // Row 3: Alpha output = A\n  ])\n\n  buffer.colorMatrixUniform(matrix, strength, 3)\n}\n\n/**\n * Adds random noise to the buffer colors using colorMatrix with brightness matrix.\n * Uses per-pixel random strength values to dim/brighten each cell.\n */\nexport function applyNoise(buffer: OptimizedBuffer, strength: number = 0.1): void {\n  const width = buffer.width\n  const height = buffer.height\n  const size = width * height\n\n  // Skip if no effect\n  if (strength === 0) return\n\n  // Generate random cellMask with per-pixel strength values\n  // Each pixel gets [x, y, random_strength] where random_strength ranges from -1 to 1\n  const cellMask = new Float32Array(size * 3)\n  let cellMaskIndex = 0\n\n  for (let y = 0; y < height; y++) {\n    for (let x = 0; x < width; x++) {\n      cellMask[cellMaskIndex++] = x\n      cellMask[cellMaskIndex++] = y\n      // Random strength from -1 to 1\n      cellMask[cellMaskIndex++] = (Math.random() - 0.5) * 2\n    }\n  }\n\n  // Brightness matrix: scales all channels by (1 + strength)\n  // With cellMask strength S, result = original * (1 + (B - 1) * S)\n  // where B = 1 + strength\n  // So: S=1 → original * (1 + strength), S=-1 → original * (1 - strength)\n  const b = 1.0 + strength\n  const matrix = new Float32Array([\n    b,\n    0,\n    0,\n    0, // Row 0 (Red output)\n    0,\n    b,\n    0,\n    0, // Row 1 (Green output)\n    0,\n    0,\n    b,\n    0, // Row 2 (Blue output)\n    0,\n    0,\n    0,\n    1, // Row 3 (Alpha output - identity)\n  ])\n\n  buffer.colorMatrix(matrix, cellMask, 1.0, 3)\n}\n\n/**\n * Applies a simplified chromatic aberration effect.\n */\nexport function applyChromaticAberration(buffer: OptimizedBuffer, strength: number = 1): void {\n  const width = buffer.width\n  const height = buffer.height\n  const srcFg = Float32Array.from(buffer.buffers.fg) // Copy original fg data\n  const destFg = buffer.buffers.fg\n  const centerX = width / 2\n  const centerY = height / 2\n\n  for (let y = 0; y < height; y++) {\n    for (let x = 0; x < width; x++) {\n      const dx = x - centerX\n      const dy = y - centerY\n      const offset = Math.round((Math.sqrt(dx * dx + dy * dy) / Math.max(centerX, centerY)) * strength)\n\n      const rX = Math.max(0, Math.min(width - 1, x - offset))\n      const bX = Math.max(0, Math.min(width - 1, x + offset))\n\n      const rIndex = (y * width + rX) * 4\n      const gIndex = (y * width + x) * 4 // Green from original position\n      const bIndex = (y * width + bX) * 4\n      const destIndex = (y * width + x) * 4\n\n      destFg[destIndex] = srcFg[rIndex] // Red from left offset\n      destFg[destIndex + 1] = srcFg[gIndex + 1] // Green from center\n      destFg[destIndex + 2] = srcFg[bIndex + 2] // Blue from right offset\n      // Keep original Alpha\n    }\n  }\n}\n\n/**\n * Converts the buffer to ASCII art based on background brightness.\n * Uses native colorMatrix for efficient color corrections.\n */\nexport function applyAsciiArt(\n  buffer: OptimizedBuffer,\n  ramp: string = ' .\\'`^\"\",:;Il!i><~+_-?][}{1)(|\\\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$',\n  fgColor: { r: number; g: number; b: number } = { r: 1.0, g: 1.0, b: 1.0 },\n  bgColor: { r: number; g: number; b: number } = { r: 0.0, g: 0.0, b: 0.0 },\n): void {\n  const width = buffer.width\n  const height = buffer.height\n  const chars = buffer.buffers.char\n  const bg = buffer.buffers.bg\n  const rampLength = ramp.length\n\n  // Set ASCII characters based on background luminance\n  for (let y = 0; y < height; y++) {\n    for (let x = 0; x < width; x++) {\n      const index = y * width + x\n      const colorIndex = index * 4\n      const bgR = bg[colorIndex]\n      const bgG = bg[colorIndex + 1]\n      const bgB = bg[colorIndex + 2]\n      const lum = 0.299 * bgR + 0.587 * bgG + 0.114 * bgB // Luminance\n      const rampIndex = Math.min(rampLength - 1, Math.floor(lum * rampLength))\n      chars[index] = ramp[rampIndex].charCodeAt(0)\n    }\n  }\n\n  // Create color matrix that sets all pixels to the target color\n  // Matrix: output = target_color * input (where input is 1.0 from alpha)\n  const fgMatrix = new Float32Array([\n    0,\n    0,\n    0,\n    fgColor.r, // Red output\n    0,\n    0,\n    0,\n    fgColor.g, // Green output\n    0,\n    0,\n    0,\n    fgColor.b, // Blue output\n    0,\n    0,\n    0,\n    1, // Alpha output (identity)\n  ])\n\n  const bgMatrix = new Float32Array([\n    0,\n    0,\n    0,\n    bgColor.r, // Red output\n    0,\n    0,\n    0,\n    bgColor.g, // Green output\n    0,\n    0,\n    0,\n    bgColor.b, // Blue output\n    0,\n    0,\n    0,\n    1, // Alpha output (identity)\n  ])\n\n  // Apply uniform color transformation to foreground (target = 1)\n  buffer.colorMatrixUniform(fgMatrix, 1.0, 1)\n  // Apply uniform color transformation to background (target = 2)\n  buffer.colorMatrixUniform(bgMatrix, 1.0, 2)\n}\n\n/**\n * Adjusts the brightness of the buffer using color matrix transformation.\n * Brightness adds the brightness value to all RGB channels (additive brightness).\n *                   If not provided, applies uniform brightness to entire buffer.\n */\nexport function applyBrightness(buffer: OptimizedBuffer, brightness: number = 0.0, cellMask?: Float32Array): void {\n  // No need to process if brightness is 0 (no change)\n  if (brightness === 0.0) return\n\n  const b = brightness\n  // Additive brightness matrix: adds brightness to all channels via alpha column\n  const matrix = new Float32Array([\n    1,\n    0,\n    0,\n    b, // Row 0 (Red output = R + brightness*A)\n    0,\n    1,\n    0,\n    b, // Row 1 (Green output = G + brightness*A)\n    0,\n    0,\n    1,\n    b, // Row 2 (Blue output = B + brightness*A)\n    0,\n    0,\n    0,\n    1, // Row 3 (Alpha output = A)\n  ])\n\n  if (!cellMask || cellMask.length === 0) {\n    buffer.colorMatrixUniform(matrix, 1.0, 3)\n  } else {\n    buffer.colorMatrix(matrix, cellMask, 1.0, 3)\n  }\n}\n\n/**\n * Adjusts the gain of the buffer using color matrix transformation.\n * Gain multiplies all RGB channels by the gain factor (no clamping).\n *                   If not provided, applies uniform gain to entire buffer.\n */\nexport function applyGain(buffer: OptimizedBuffer, gain: number = 1.0, cellMask?: Float32Array): void {\n  // No need to process if gain is 1 (no change)\n  if (gain === 1.0) return\n\n  const g = Math.max(0, gain)\n  const matrix = new Float32Array([\n    g,\n    0,\n    0,\n    0, // Row 0 (Red output)\n    0,\n    g,\n    0,\n    0, // Row 1 (Green output)\n    0,\n    0,\n    g,\n    0, // Row 2 (Blue output)\n    0,\n    0,\n    0,\n    1, // Row 3 (Alpha output - identity)\n  ])\n\n  if (!cellMask || cellMask.length === 0) {\n    buffer.colorMatrixUniform(matrix, 1.0, 3)\n  } else {\n    buffer.colorMatrix(matrix, cellMask, 1.0, 3)\n  }\n}\n\n/**\n * Generates a saturation color matrix (4x4 RGBA with alpha identity).\n */\nfunction createSaturationMatrix(saturation: number): Float32Array {\n  const s = Math.max(0, saturation)\n  const sr = 0.299 * (1 - s)\n  const sg = 0.587 * (1 - s)\n  const sb = 0.114 * (1 - s)\n\n  // Row 0 (Red output)\n  const m00 = sr + s // 0.299 + 0.701*s\n  const m01 = sg // 0.587 * (1 - s)\n  const m02 = sb // 0.114 * (1 - s)\n\n  // Row 1 (Green output)\n  const m10 = sr // 0.299 * (1 - s)\n  const m11 = sg + s // 0.587 + 0.413*s\n  const m12 = sb // 0.114 * (1 - s)\n\n  // Row 2 (Blue output)\n  const m20 = sr // 0.299 * (1 - s)\n  const m21 = sg // 0.587 * (1 - s)\n  const m22 = sb + s // 0.114 + 0.886*s\n\n  // 4x4 matrix with alpha identity\n  return new Float32Array([\n    m00,\n    m01,\n    m02,\n    0, // Red output row\n    m10,\n    m11,\n    m12,\n    0, // Green output row\n    m20,\n    m21,\n    m22,\n    0, // Blue output row\n    0,\n    0,\n    0,\n    1, // Alpha output row (identity)\n  ])\n}\n\n/**\n * Applies a saturation adjustment to the buffer.\n */\nexport function applySaturation(buffer: OptimizedBuffer, cellMask?: Float32Array, strength: number = 1.0): void {\n  // No need to process if saturation is 1 (no change) or strength is 0\n  if (strength === 1.0 || strength === 0) {\n    return\n  }\n\n  const matrix = createSaturationMatrix(strength)\n\n  // If no cellMask provided, use uniform saturation (much faster)\n  if (!cellMask || cellMask.length === 0) {\n    buffer.colorMatrixUniform(matrix, 1.0, 3)\n  } else {\n    buffer.colorMatrix(matrix, cellMask, 1.0, 3)\n  }\n}\n\n/**\n * Applies a bloom effect based on bright areas (Simplified).\n */\nexport class BloomEffect {\n  private _threshold: number\n  private _strength: number\n  private _radius: number\n\n  constructor(threshold: number = 0.8, strength: number = 0.2, radius: number = 2) {\n    this._threshold = Math.max(0, Math.min(1, threshold))\n    this._strength = Math.max(0, strength)\n    this._radius = Math.max(0, Math.round(radius))\n  }\n\n  public set threshold(newThreshold: number) {\n    this._threshold = Math.max(0, Math.min(1, newThreshold))\n  }\n  public get threshold(): number {\n    return this._threshold\n  }\n\n  public set strength(newStrength: number) {\n    this._strength = Math.max(0, newStrength)\n  }\n  public get strength(): number {\n    return this._strength\n  }\n\n  public set radius(newRadius: number) {\n    this._radius = Math.max(0, Math.round(newRadius))\n  }\n  public get radius(): number {\n    return this._radius\n  }\n\n  public apply(buffer: OptimizedBuffer): void {\n    const threshold = this._threshold\n    const strength = this._strength\n    const radius = this._radius\n\n    if (strength <= 0 || radius <= 0) return // No bloom if strength or radius is non-positive\n\n    const width = buffer.width\n    const height = buffer.height\n    // Operate directly on the buffer's data for bloom, but need a source copy temporarily\n    const srcFg = Float32Array.from(buffer.buffers.fg)\n    const srcBg = Float32Array.from(buffer.buffers.bg)\n    const destFg = buffer.buffers.fg\n    const destBg = buffer.buffers.bg\n\n    const brightPixels: { x: number; y: number; intensity: number }[] = []\n\n    // 1. Find bright pixels based on original data\n    for (let y = 0; y < height; y++) {\n      for (let x = 0; x < width; x++) {\n        const index = (y * width + x) * 4\n        // Consider max component brightness, or luminance? Using luminance.\n        const fgLum = 0.299 * srcFg[index] + 0.587 * srcFg[index + 1] + 0.114 * srcFg[index + 2]\n        const bgLum = 0.299 * srcBg[index] + 0.587 * srcBg[index + 1] + 0.114 * srcBg[index + 2]\n        const lum = Math.max(fgLum, bgLum)\n        if (lum > threshold) {\n          const intensity = (lum - threshold) / (1 - threshold + 1e-6) // Add epsilon to avoid div by zero\n          brightPixels.push({ x, y, intensity: Math.max(0, intensity) })\n        }\n      }\n    }\n\n    // If no bright pixels found, exit early\n    if (brightPixels.length === 0) return\n\n    // Initialize destination buffers by copying original state before applying bloom\n    // This prevents bloom from compounding on itself within one frame pass\n    destFg.set(srcFg)\n    destBg.set(srcBg)\n\n    // 2. Apply bloom spread from bright pixels onto the destination buffers\n    for (const bright of brightPixels) {\n      for (let ky = -radius; ky <= radius; ky++) {\n        for (let kx = -radius; kx <= radius; kx++) {\n          if (kx === 0 && ky === 0) continue // Don't bloom self\n\n          const sampleX = bright.x + kx\n          const sampleY = bright.y + ky\n\n          if (sampleX >= 0 && sampleX < width && sampleY >= 0 && sampleY < height) {\n            const distSq = kx * kx + ky * ky // Use squared distance for falloff calculation\n            const radiusSq = radius * radius\n            if (distSq <= radiusSq) {\n              // Simple linear falloff based on squared distance\n              const falloff = 1 - distSq / radiusSq\n              const bloomAmount = bright.intensity * strength * falloff\n              const destIndex = (sampleY * width + sampleX) * 4\n\n              // Add bloom to both fg and bg, clamping at 1.0\n              destFg[destIndex] = Math.min(1.0, destFg[destIndex] + bloomAmount)\n              destFg[destIndex + 1] = Math.min(1.0, destFg[destIndex + 1] + bloomAmount)\n              destFg[destIndex + 2] = Math.min(1.0, destFg[destIndex + 2] + bloomAmount)\n\n              destBg[destIndex] = Math.min(1.0, destBg[destIndex] + bloomAmount)\n              destBg[destIndex + 1] = Math.min(1.0, destBg[destIndex + 1] + bloomAmount)\n              destBg[destIndex + 2] = Math.min(1.0, destBg[destIndex + 2] + bloomAmount)\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/post/matrices.ts",
    "content": "// Standard sepia transformation matrix (4x4 RGBA with alpha identity)\nexport const SEPIA_MATRIX = new Float32Array([\n  0.393,\n  0.769,\n  0.189,\n  0, // Red output (r->r, g->r, b->r, a->r)\n  0.349,\n  0.686,\n  0.168,\n  0, // Green output (r->g, g->g, b->g, a->g)\n  0.272,\n  0.534,\n  0.131,\n  0, // Blue output (r->b, g->b, b->b, a->b)\n  0,\n  0,\n  0,\n  1, // Alpha output (r->a, g->a, b->a, a->a) - identity\n])\n\n/**\n * Colorblindness simulation and compensation filters using color matrix transformations.\n */\n\n// Protanopia (Red-blind) simulation matrix - shows how colors appear to someone with red-blindness\nexport const PROTANOPIA_SIM_MATRIX = new Float32Array([\n  0.567,\n  0.433,\n  0.0,\n  0, // Red output\n  0.558,\n  0.442,\n  0.0,\n  0, // Green output\n  0.0,\n  0.242,\n  0.758,\n  0, // Blue output\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Deuteranopia (Green-blind) simulation matrix - shows how colors appear to someone with green-blindness\nexport const DEUTERANOPIA_SIM_MATRIX = new Float32Array([\n  0.625,\n  0.375,\n  0.0,\n  0, // Red output\n  0.7,\n  0.3,\n  0.0,\n  0, // Green output\n  0.0,\n  0.3,\n  0.7,\n  0, // Blue output\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Tritanopia (Blue-blind) simulation matrix - shows how colors appear to someone with blue-blindness\nexport const TRITANOPIA_SIM_MATRIX = new Float32Array([\n  0.95,\n  0.05,\n  0.0,\n  0, // Red output\n  0.0,\n  0.433,\n  0.567,\n  0, // Green output\n  0.0,\n  0.475,\n  0.525,\n  0, // Blue output\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Achromatopsia (Complete color blindness) - grayscale\nexport const ACHROMATOPSIA_MATRIX = new Float32Array([\n  0.299,\n  0.587,\n  0.114,\n  0, // Red output (luminance)\n  0.299,\n  0.587,\n  0.114,\n  0, // Green output (luminance)\n  0.299,\n  0.587,\n  0.114,\n  0, // Blue output (luminance)\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Protanopia compensation matrix - shifts colors to make them more distinguishable\nexport const PROTANOPIA_COMP_MATRIX = new Float32Array([\n  1.0,\n  0.2,\n  0.0,\n  0, // Boost red channel\n  0.0,\n  0.9,\n  0.1,\n  0, // Adjust green\n  0.0,\n  0.1,\n  0.9,\n  0, // Enhance blue\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Deuteranopia compensation matrix - shifts colors to make them more distinguishable\nexport const DEUTERANOPIA_COMP_MATRIX = new Float32Array([\n  0.9,\n  0.1,\n  0.0,\n  0, // Adjust red\n  0.2,\n  0.8,\n  0.0,\n  0, // Boost green channel\n  0.0,\n  0.0,\n  1.0,\n  0, // Keep blue\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Tritanopia compensation matrix - shifts colors to make them more distinguishable\nexport const TRITANOPIA_COMP_MATRIX = new Float32Array([\n  1.0,\n  0.0,\n  0.0,\n  0, // Keep red\n  0.0,\n  0.9,\n  0.1,\n  0, // Adjust green\n  0.1,\n  0.0,\n  0.9,\n  0, // Boost blue channel\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n/**\n * Creative color effect matrices.\n */\n\n// Technicolor effect - enhances reds and greens for a vintage Hollywood look\nexport const TECHNICOLOR_MATRIX = new Float32Array([\n  1.5,\n  -0.2,\n  -0.3,\n  0, // Red output - boosted with reduced green/blue influence\n  -0.3,\n  1.4,\n  -0.1,\n  0, // Green output - boosted with reduced red/blue influence\n  -0.2,\n  -0.2,\n  1.4,\n  0, // Blue output - slightly boosted\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Solarization effect - partial negative that creates a surreal look\n// Inverts blue channel strongly, partially inverts others\nexport const SOLARIZATION_MATRIX = new Float32Array([\n  -0.5,\n  0.5,\n  0.5,\n  0, // Red output - partial negative\n  0.5,\n  -0.5,\n  0.5,\n  0, // Green output - partial negative\n  0.5,\n  0.5,\n  -0.5,\n  0, // Blue output - partial negative\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Synthwave effect - eliminates green and shifts red toward magenta for that retro neon aesthetic\nexport const SYNTHWAVE_MATRIX = new Float32Array([\n  1.0,\n  0.0,\n  0.25,\n  0, // Red output - full red + some blue = magenta when bright\n  0.1,\n  0.1,\n  0.1,\n  0, // Green output - heavily suppressed, minimal contribution\n  0.25,\n  0.0,\n  1.0,\n  0, // Blue output - full blue + some red = enhances magenta tones\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Greenscale effect - converts image to monochrome green by mapping luminance to green channel only\nexport const GREENSCALE_MATRIX = new Float32Array([\n  0,\n  0,\n  0,\n  0, // Red output - zeroed out\n  0.299,\n  0.587,\n  0.114,\n  0, // Green output - full luminance from all channels\n  0,\n  0,\n  0,\n  0, // Blue output - zeroed out\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Grayscale effect - converts image to monochrome gray using luminance weights\nexport const GRAYSCALE_MATRIX = new Float32Array([\n  0.299,\n  0.587,\n  0.114,\n  0, // Red output - luminance from all channels\n  0.299,\n  0.587,\n  0.114,\n  0, // Green output - luminance from all channels\n  0.299,\n  0.587,\n  0.114,\n  0, // Blue output - luminance from all channels\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n\n// Invert effect - inverts all color channels (photographic negative)\nexport const INVERT_MATRIX = new Float32Array([\n  -1,\n  0,\n  0,\n  1, // Red output = 1 - R\n  0,\n  -1,\n  0,\n  1, // Green output = 1 - G\n  0,\n  0,\n  -1,\n  1, // Blue output = 1 - B\n  0,\n  0,\n  0,\n  1, // Alpha output - identity\n])\n"
  },
  {
    "path": "packages/core/src/renderables/ASCIIFont.ts",
    "content": "import {\n  getCharacterPositions,\n  measureText,\n  renderFontToFrameBuffer,\n  type ASCIIFontName,\n  type fonts,\n} from \"../lib/ascii.font.js\"\nimport { parseColor, type ColorInput } from \"../lib/RGBA.js\"\nimport {\n  ASCIIFontSelectionHelper,\n  convertGlobalToLocalSelection,\n  Selection,\n  type LocalSelectionBounds,\n} from \"../lib/selection.js\"\nimport type { RenderableOptions } from \"../Renderable.js\"\nimport type { RenderContext } from \"../types.js\"\nimport { FrameBufferRenderable, type FrameBufferOptions } from \"./FrameBuffer.js\"\n\nexport interface ASCIIFontOptions extends Omit<RenderableOptions<ASCIIFontRenderable>, \"width\" | \"height\"> {\n  text?: string\n  font?: ASCIIFontName\n  color?: ColorInput | ColorInput[]\n  backgroundColor?: ColorInput\n  selectionBg?: ColorInput\n  selectionFg?: ColorInput\n  selectable?: boolean\n}\n\nexport class ASCIIFontRenderable extends FrameBufferRenderable {\n  public selectable: boolean = true\n\n  protected static readonly _defaultOptions = {\n    text: \"\",\n    font: \"tiny\",\n    color: \"#FFFFFF\",\n    backgroundColor: \"transparent\",\n    selectionBg: undefined,\n    selectionFg: undefined,\n    selectable: true,\n  } satisfies Partial<ASCIIFontOptions>\n\n  protected _text: string\n  protected _font: keyof typeof fonts\n  protected _color: ColorInput | ColorInput[]\n  protected _backgroundColor: ColorInput\n  protected _selectionBg: ColorInput | undefined\n  protected _selectionFg: ColorInput | undefined\n  protected lastLocalSelection: LocalSelectionBounds | null = null\n\n  private selectionHelper: ASCIIFontSelectionHelper\n\n  constructor(ctx: RenderContext, options: ASCIIFontOptions) {\n    const defaultOptions = ASCIIFontRenderable._defaultOptions\n    const font = options.font || defaultOptions.font\n    const text = options.text || defaultOptions.text\n    const measurements = measureText({ text: text, font })\n\n    super(ctx, {\n      flexShrink: 0,\n      ...options,\n      width: measurements.width || 1,\n      height: measurements.height || 1,\n      respectAlpha: true,\n    } as FrameBufferOptions)\n\n    this._text = text\n    this._font = font\n    this._color = options.color || defaultOptions.color\n    this._backgroundColor = options.backgroundColor || defaultOptions.backgroundColor\n    this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined\n    this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined\n    this.selectable = options.selectable ?? true\n\n    this.selectionHelper = new ASCIIFontSelectionHelper(\n      () => this._text,\n      () => this._font,\n    )\n\n    this.renderFontToBuffer()\n  }\n\n  get text(): string {\n    return this._text\n  }\n\n  set text(value: string) {\n    this._text = value\n    this.updateDimensions()\n\n    if (this.lastLocalSelection) {\n      this.selectionHelper.onLocalSelectionChanged(this.lastLocalSelection, this.width, this.height)\n    }\n\n    this.renderFontToBuffer()\n    this.requestRender()\n  }\n\n  get font(): keyof typeof fonts {\n    return this._font\n  }\n\n  set font(value: keyof typeof fonts) {\n    this._font = value\n    this.updateDimensions()\n\n    if (this.lastLocalSelection) {\n      this.selectionHelper.onLocalSelectionChanged(this.lastLocalSelection, this.width, this.height)\n    }\n\n    this.renderFontToBuffer()\n    this.requestRender()\n  }\n\n  get color(): ColorInput | ColorInput[] {\n    return this._color\n  }\n\n  set color(value: ColorInput | ColorInput[]) {\n    this._color = value\n    this.renderFontToBuffer()\n    this.requestRender()\n  }\n\n  get backgroundColor(): ColorInput {\n    return this._backgroundColor\n  }\n\n  set backgroundColor(value: ColorInput) {\n    this._backgroundColor = value\n    this.renderFontToBuffer()\n    this.requestRender()\n  }\n\n  private updateDimensions(): void {\n    const measurements = measureText({ text: this._text, font: this._font })\n    this.width = measurements.width\n    this.height = measurements.height\n  }\n\n  shouldStartSelection(x: number, y: number): boolean {\n    const localX = x - this.x\n    const localY = y - this.y\n    return this.selectionHelper.shouldStartSelection(localX, localY, this.width, this.height)\n  }\n\n  onSelectionChanged(selection: Selection | null): boolean {\n    const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y)\n    this.lastLocalSelection = localSelection\n    const changed = this.selectionHelper.onLocalSelectionChanged(localSelection, this.width, this.height)\n    if (changed) {\n      this.renderFontToBuffer()\n      this.requestRender()\n    }\n    return changed\n  }\n\n  getSelectedText(): string {\n    const selection = this.selectionHelper.getSelection()\n    if (!selection) return \"\"\n    return this._text.slice(selection.start, selection.end)\n  }\n\n  hasSelection(): boolean {\n    return this.selectionHelper.hasSelection()\n  }\n\n  protected onResize(width: number, height: number): void {\n    super.onResize(width, height)\n    this.renderFontToBuffer()\n  }\n\n  private renderFontToBuffer(): void {\n    if (this.isDestroyed) return\n    this.frameBuffer.clear(parseColor(this._backgroundColor))\n\n    renderFontToFrameBuffer(this.frameBuffer, {\n      text: this._text,\n      x: 0,\n      y: 0,\n      color: this.color,\n      backgroundColor: this._backgroundColor,\n      font: this._font,\n    })\n\n    const selection = this.selectionHelper.getSelection()\n    if (selection && (this._selectionBg || this._selectionFg)) {\n      this.renderSelectionHighlight(selection)\n    }\n  }\n\n  private renderSelectionHighlight(selection: { start: number; end: number }): void {\n    if (!this._selectionBg && !this._selectionFg) return\n\n    const selectedText = this._text.slice(selection.start, selection.end)\n    if (!selectedText) return\n\n    const positions = getCharacterPositions(this._text, this._font)\n    const startX = positions[selection.start] || 0\n    const endX =\n      selection.end < positions.length\n        ? positions[selection.end]\n        : measureText({ text: this._text, font: this._font }).width\n\n    if (this._selectionBg) {\n      this.frameBuffer.fillRect(startX, 0, endX - startX, this.height, parseColor(this._selectionBg))\n    }\n\n    if (this._selectionFg || this._selectionBg) {\n      renderFontToFrameBuffer(this.frameBuffer, {\n        text: selectedText,\n        x: startX,\n        y: 0,\n        color: this._selectionFg ? this._selectionFg : this._color,\n        backgroundColor: this._selectionBg ? this._selectionBg : this._backgroundColor,\n        font: this._font,\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Box.test.ts",
    "content": "import { test, expect, describe, beforeEach, afterEach, spyOn } from \"bun:test\"\nimport { BoxRenderable, type BoxOptions } from \"./Box.js\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport type { BorderStyle } from \"../lib/border.js\"\n\nlet testRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet warnSpy: ReturnType<typeof spyOn>\n\nbeforeEach(async () => {\n  ;({ renderer: testRenderer, renderOnce } = await createTestRenderer({}))\n  warnSpy = spyOn(console, \"warn\").mockImplementation(() => {})\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n  warnSpy.mockRestore()\n})\n\ndescribe(\"BoxRenderable - focusable option\", () => {\n  test(\"is not focusable by default\", async () => {\n    const box = new BoxRenderable(testRenderer, {\n      id: \"test-box\",\n      width: 10,\n      height: 5,\n    })\n\n    expect(box.focusable).toBe(false)\n    box.focus()\n    expect(box.focused).toBe(false)\n  })\n\n  test(\"can be made focusable via option\", async () => {\n    const box = new BoxRenderable(testRenderer, {\n      id: \"test-box\",\n      focusable: true,\n      width: 10,\n      height: 5,\n    })\n\n    expect(box.focusable).toBe(true)\n    box.focus()\n    expect(box.focused).toBe(true)\n  })\n})\n\ndescribe(\"BoxRenderable - borderStyle validation\", () => {\n  describe(\"regression: invalid borderStyle via constructor does not crash\", () => {\n    test(\"handles invalid string borderStyle in constructor\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        id: \"test-box\",\n        borderStyle: \"invalid-style\" as BorderStyle,\n        border: true,\n        width: 10,\n        height: 5,\n      })\n\n      testRenderer.root.add(box)\n      await renderOnce()\n\n      expect(box.borderStyle).toBe(\"single\")\n      expect(box.isDestroyed).toBe(false)\n    })\n\n    test(\"handles undefined borderStyle in constructor\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        id: \"test-box\",\n        borderStyle: undefined,\n        border: true,\n        width: 10,\n        height: 5,\n      })\n\n      testRenderer.root.add(box)\n      await renderOnce()\n\n      expect(box.borderStyle).toBe(\"single\")\n      expect(box.isDestroyed).toBe(false)\n    })\n  })\n\n  describe(\"regression: invalid borderStyle via setter does not crash\", () => {\n    test(\"handles invalid string borderStyle via setter\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        id: \"test-box\",\n        borderStyle: \"double\",\n        border: true,\n        width: 10,\n        height: 5,\n      })\n\n      testRenderer.root.add(box)\n      await renderOnce()\n\n      expect(box.borderStyle).toBe(\"double\")\n\n      box.borderStyle = \"invalid-style\" as BorderStyle\n      await renderOnce()\n\n      expect(box.borderStyle).toBe(\"single\")\n      expect(box.isDestroyed).toBe(false)\n    })\n\n    test(\"renders correctly after fallback from invalid borderStyle\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        id: \"test-box\",\n        borderStyle: \"invalid\" as BorderStyle,\n        border: true,\n        width: 10,\n        height: 5,\n      })\n\n      testRenderer.root.add(box)\n\n      // Should not throw during render\n      await expect(renderOnce()).resolves.toBeUndefined()\n      expect(box.isDestroyed).toBe(false)\n    })\n  })\n\n  describe(\"valid borderStyle values work correctly\", () => {\n    test.each([\"single\", \"double\", \"rounded\", \"heavy\"] as BorderStyle[])(\n      \"accepts valid borderStyle '%s' in constructor\",\n      async (style) => {\n        const box = new BoxRenderable(testRenderer, {\n          id: \"test-box\",\n          borderStyle: style,\n          border: true,\n          width: 10,\n          height: 5,\n        })\n\n        testRenderer.root.add(box)\n        await renderOnce()\n\n        expect(box.borderStyle).toBe(style)\n      },\n    )\n\n    test.each([\"single\", \"double\", \"rounded\", \"heavy\"] as BorderStyle[])(\n      \"accepts valid borderStyle '%s' via setter\",\n      async (style) => {\n        const box = new BoxRenderable(testRenderer, {\n          id: \"test-box\",\n          border: true,\n          width: 10,\n          height: 5,\n        })\n\n        testRenderer.root.add(box)\n        await renderOnce()\n\n        box.borderStyle = style\n        await renderOnce()\n\n        expect(box.borderStyle).toBe(style)\n      },\n    )\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Box.ts",
    "content": "import { Edge, Gutter } from \"yoga-layout\"\nimport { type RenderableOptions, Renderable } from \"../Renderable.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport {\n  type BorderCharacters,\n  type BorderSides,\n  type BorderSidesConfig,\n  type BorderStyle,\n  borderCharsToArray,\n  getBorderSides,\n  parseBorderStyle,\n} from \"../lib/index.js\"\nimport { type ColorInput, RGBA, parseColor } from \"../lib/RGBA.js\"\nimport { isValidPercentage } from \"../lib/renderable.validations.js\"\nimport type { RenderContext } from \"../types.js\"\n\nexport interface BoxOptions<TRenderable extends Renderable = BoxRenderable> extends RenderableOptions<TRenderable> {\n  backgroundColor?: string | RGBA\n  borderStyle?: BorderStyle\n  border?: boolean | BorderSides[]\n  borderColor?: string | RGBA\n  customBorderChars?: BorderCharacters\n  shouldFill?: boolean\n  title?: string\n  titleAlignment?: \"left\" | \"center\" | \"right\"\n  focusedBorderColor?: ColorInput\n  focusable?: boolean\n  gap?: number | `${number}%`\n  rowGap?: number | `${number}%`\n  columnGap?: number | `${number}%`\n}\n\nfunction isGapType(value: any): value is number | undefined {\n  if (value === undefined) {\n    return true\n  }\n  if (typeof value === \"number\" && !Number.isNaN(value)) {\n    return true\n  }\n  return isValidPercentage(value)\n}\n\nexport class BoxRenderable extends Renderable {\n  protected _backgroundColor: RGBA\n  protected _border: boolean | BorderSides[]\n  protected _borderStyle: BorderStyle\n  protected _borderColor: RGBA\n  protected _focusedBorderColor: RGBA\n  private _customBorderCharsObj: BorderCharacters | undefined\n  protected _customBorderChars?: Uint32Array\n  protected borderSides: BorderSidesConfig\n  public shouldFill: boolean\n  protected _title?: string\n  protected _titleAlignment: \"left\" | \"center\" | \"right\"\n\n  protected _defaultOptions = {\n    backgroundColor: \"transparent\",\n    borderStyle: \"single\",\n    border: false,\n    borderColor: \"#FFFFFF\",\n    shouldFill: true,\n    titleAlignment: \"left\",\n    focusedBorderColor: \"#00AAFF\",\n  } satisfies Partial<BoxOptions>\n\n  constructor(ctx: RenderContext, options: BoxOptions) {\n    super(ctx, options)\n\n    if (options.focusable === true) {\n      this._focusable = true\n    }\n\n    this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor)\n    this._border = options.border ?? this._defaultOptions.border\n    if (\n      !options.border &&\n      (options.borderStyle || options.borderColor || options.focusedBorderColor || options.customBorderChars)\n    ) {\n      this._border = true\n    }\n    this._borderStyle = parseBorderStyle(options.borderStyle, this._defaultOptions.borderStyle)\n    this._borderColor = parseColor(options.borderColor || this._defaultOptions.borderColor)\n    this._focusedBorderColor = parseColor(options.focusedBorderColor || this._defaultOptions.focusedBorderColor)\n    this._customBorderCharsObj = options.customBorderChars\n    this._customBorderChars = this._customBorderCharsObj ? borderCharsToArray(this._customBorderCharsObj) : undefined\n    this.borderSides = getBorderSides(this._border)\n    this.shouldFill = options.shouldFill ?? this._defaultOptions.shouldFill\n    this._title = options.title\n    this._titleAlignment = options.titleAlignment || this._defaultOptions.titleAlignment\n\n    this.applyYogaBorders()\n\n    const hasInitialGapProps =\n      options.gap !== undefined || options.rowGap !== undefined || options.columnGap !== undefined\n    if (hasInitialGapProps) {\n      this.applyYogaGap(options)\n    }\n  }\n\n  private initializeBorder(): void {\n    // https://github.com/anomalyco/opentui/issues/186\n    // Solid-js reconciler does not pass props to constructor on init,\n    // so we need to initialize the border when supporting properties are set.\n    // borderStyle, borderColor, focusedBorderColor\n    if (this._border === false) {\n      this._border = true\n      this.borderSides = getBorderSides(this._border)\n      this.applyYogaBorders()\n    }\n  }\n\n  public get customBorderChars(): BorderCharacters | undefined {\n    return this._customBorderCharsObj\n  }\n\n  public set customBorderChars(value: BorderCharacters | undefined) {\n    this._customBorderCharsObj = value\n    this._customBorderChars = value ? borderCharsToArray(value) : undefined\n    this.requestRender()\n  }\n\n  public get backgroundColor(): RGBA {\n    return this._backgroundColor\n  }\n\n  public set backgroundColor(value: RGBA | string | undefined) {\n    const newColor = parseColor(value ?? this._defaultOptions.backgroundColor)\n    if (this._backgroundColor !== newColor) {\n      this._backgroundColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public get border(): boolean | BorderSides[] {\n    return this._border\n  }\n\n  public set border(value: boolean | BorderSides[]) {\n    if (this._border !== value) {\n      this._border = value\n      this.borderSides = getBorderSides(value)\n      this.applyYogaBorders()\n      this.requestRender()\n    }\n  }\n\n  public get borderStyle(): BorderStyle {\n    return this._borderStyle\n  }\n\n  public set borderStyle(value: BorderStyle) {\n    const _value = parseBorderStyle(value, this._defaultOptions.borderStyle)\n    if (this._borderStyle !== _value || !this._border) {\n      this._borderStyle = _value\n      this._customBorderChars = undefined\n      this.initializeBorder()\n      this.requestRender()\n    }\n  }\n\n  public get borderColor(): RGBA {\n    return this._borderColor\n  }\n\n  public set borderColor(value: RGBA | string) {\n    const newColor = parseColor(value ?? this._defaultOptions.borderColor)\n    if (this._borderColor !== newColor) {\n      this._borderColor = newColor\n      this.initializeBorder()\n      this.requestRender()\n    }\n  }\n\n  public get focusedBorderColor(): RGBA {\n    return this._focusedBorderColor\n  }\n\n  public set focusedBorderColor(value: RGBA | string) {\n    const newColor = parseColor(value ?? this._defaultOptions.focusedBorderColor)\n    if (this._focusedBorderColor !== newColor) {\n      this._focusedBorderColor = newColor\n      this.initializeBorder()\n      if (this._focused) {\n        this.requestRender()\n      }\n    }\n  }\n\n  public get title(): string | undefined {\n    return this._title\n  }\n\n  public set title(value: string | undefined) {\n    if (this._title !== value) {\n      this._title = value\n      this.requestRender()\n    }\n  }\n\n  public get titleAlignment(): \"left\" | \"center\" | \"right\" {\n    return this._titleAlignment\n  }\n\n  public set titleAlignment(value: \"left\" | \"center\" | \"right\") {\n    if (this._titleAlignment !== value) {\n      this._titleAlignment = value\n      this.requestRender()\n    }\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    const currentBorderColor = this._focused ? this._focusedBorderColor : this._borderColor\n\n    buffer.drawBox({\n      x: this.x,\n      y: this.y,\n      width: this.width,\n      height: this.height,\n      borderStyle: this._borderStyle,\n      customBorderChars: this._customBorderChars,\n      border: this._border,\n      borderColor: currentBorderColor,\n      backgroundColor: this._backgroundColor,\n      shouldFill: this.shouldFill,\n      title: this._title,\n      titleAlignment: this._titleAlignment,\n    })\n  }\n\n  protected getScissorRect(): { x: number; y: number; width: number; height: number } {\n    const baseRect = super.getScissorRect()\n\n    if (!this.borderSides.top && !this.borderSides.right && !this.borderSides.bottom && !this.borderSides.left) {\n      return baseRect\n    }\n\n    const leftInset = this.borderSides.left ? 1 : 0\n    const rightInset = this.borderSides.right ? 1 : 0\n    const topInset = this.borderSides.top ? 1 : 0\n    const bottomInset = this.borderSides.bottom ? 1 : 0\n\n    return {\n      x: baseRect.x + leftInset,\n      y: baseRect.y + topInset,\n      width: Math.max(0, baseRect.width - leftInset - rightInset),\n      height: Math.max(0, baseRect.height - topInset - bottomInset),\n    }\n  }\n\n  private applyYogaBorders(): void {\n    const node = this.yogaNode\n    node.setBorder(Edge.Left, this.borderSides.left ? 1 : 0)\n    node.setBorder(Edge.Right, this.borderSides.right ? 1 : 0)\n    node.setBorder(Edge.Top, this.borderSides.top ? 1 : 0)\n    node.setBorder(Edge.Bottom, this.borderSides.bottom ? 1 : 0)\n    this.requestRender()\n  }\n\n  private applyYogaGap(options: BoxOptions): void {\n    const node = this.yogaNode\n\n    if (isGapType(options.gap)) {\n      node.setGap(Gutter.All, options.gap)\n    }\n\n    if (isGapType(options.rowGap)) {\n      node.setGap(Gutter.Row, options.rowGap)\n    }\n\n    if (isGapType(options.columnGap)) {\n      node.setGap(Gutter.Column, options.columnGap)\n    }\n  }\n\n  public set gap(gap: number | `${number}%` | undefined) {\n    if (isGapType(gap)) {\n      this.yogaNode.setGap(Gutter.All, gap)\n      this.requestRender()\n    }\n  }\n\n  public set rowGap(rowGap: number | `${number}%` | undefined) {\n    if (isGapType(rowGap)) {\n      this.yogaNode.setGap(Gutter.Row, rowGap)\n      this.requestRender()\n    }\n  }\n\n  public set columnGap(columnGap: number | `${number}%` | undefined) {\n    if (isGapType(columnGap)) {\n      this.yogaNode.setGap(Gutter.Column, columnGap)\n      this.requestRender()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Code.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { CodeRenderable } from \"./Code.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { createTestRenderer, type TestRenderer, MockTreeSitterClient, type MockMouse } from \"../testing.js\"\nimport { TreeSitterClient } from \"../lib/tree-sitter/index.js\"\nimport type { SimpleHighlight } from \"../lib/tree-sitter/types.js\"\nimport { BoxRenderable } from \"./Box.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet captureFrame: () => string\nlet mockMouse: MockMouse\nlet resize: (width: number, height: number) => void\n\nbeforeEach(async () => {\n  const testRenderer = await createTestRenderer({ width: 80, height: 24 })\n  currentRenderer = testRenderer.renderer\n  renderOnce = testRenderer.renderOnce\n  captureFrame = testRenderer.captureCharFrame\n  mockMouse = testRenderer.mockMouse\n  resize = testRenderer.resize\n})\n\nafterEach(async () => {\n  if (currentRenderer) {\n    currentRenderer.destroy()\n  }\n})\n\ntest(\"CodeRenderable - basic construction\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n    string: { fg: RGBA.fromValues(0, 1, 0, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: 'const message = \"Hello, world!\";',\n    filetype: \"javascript\",\n    syntaxStyle,\n    conceal: false,\n  })\n\n  expect(codeRenderable.content).toBe('const message = \"Hello, world!\";')\n  expect(codeRenderable.filetype).toBe(\"javascript\")\n  expect(codeRenderable.syntaxStyle).toBe(syntaxStyle)\n})\n\ntest(\"CodeRenderable - content updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"original content\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    conceal: false,\n  })\n\n  expect(codeRenderable.content).toBe(\"original content\")\n\n  codeRenderable.content = \"updated content\"\n  expect(codeRenderable.content).toBe(\"updated content\")\n})\n\ntest(\"CodeRenderable - filetype updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"console.log('test');\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    conceal: false,\n  })\n\n  expect(codeRenderable.filetype).toBe(\"javascript\")\n\n  codeRenderable.filetype = \"typescript\"\n  expect(codeRenderable.filetype).toBe(\"typescript\")\n})\n\ntest(\"CodeRenderable - re-highlights when content changes during active highlighting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [\n      [0, 5, \"keyword\"],\n      [6, 13, \"identifier\"],\n    ] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  codeRenderable.content = \"let newMessage = 'world';\"\n\n  expect(codeRenderable.content).toBe(\"let newMessage = 'world';\")\n\n  await renderOnce()\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  expect(mockClient.isHighlighting()).toBe(false)\n})\n\ntest(\"CodeRenderable - multiple content changes during highlighting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"original content\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  codeRenderable.content = \"first change\"\n  codeRenderable.content = \"second change\"\n  codeRenderable.content = \"final content\"\n\n  expect(codeRenderable.content).toBe(\"final content\")\n\n  await renderOnce()\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  expect(mockClient.isHighlighting()).toBe(false)\n})\n\ntest(\"CodeRenderable - uses fallback rendering when no filetype provided\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello world';\",\n    syntaxStyle,\n    conceal: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(codeRenderable.content).toBe(\"const message = 'hello world';\")\n  expect(codeRenderable.filetype).toBeUndefined()\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello world';\")\n})\n\ntest(\"CodeRenderable - uses fallback rendering when highlighting throws error\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n\n  mockClient.highlightOnce = async () => {\n    throw new Error(\"Highlighting failed\")\n  }\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello world';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  await new Promise((resolve) => setTimeout(resolve, 20))\n  await renderOnce()\n\n  expect(codeRenderable.content).toBe(\"const message = 'hello world';\")\n  expect(codeRenderable.filetype).toBe(\"javascript\")\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello world';\")\n})\n\ntest(\"CodeRenderable - handles empty content\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    conceal: false,\n  })\n\n  await renderOnce()\n\n  expect(codeRenderable.content).toBe(\"\")\n  expect(codeRenderable.filetype).toBe(\"javascript\")\n  expect(codeRenderable.plainText).toBe(\"\")\n})\n\ntest(\"CodeRenderable - empty content does not trigger highlighting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.content).toBe(\"const message = 'hello';\")\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n\n  codeRenderable.content = \"\"\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(false)\n  expect(codeRenderable.content).toBe(\"\")\n})\n\ntest(\"CodeRenderable - text renders immediately before highlighting completes\", async () => {\n  resize(32, 2)\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [\n      [0, 5, \"keyword\"],\n      [6, 13, \"identifier\"],\n    ] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello world';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  const frameBeforeHighlighting = captureFrame()\n  expect(frameBeforeHighlighting).toMatchSnapshot(\"text visible before highlighting completes\")\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  const frameAfterHighlighting = captureFrame()\n  expect(frameAfterHighlighting).toMatchSnapshot(\"text visible after highlighting completes\")\n})\n\ntest(\"CodeRenderable - batches concurrent content and filetype updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  let highlightCount = 0\n  const mockClient = new MockTreeSitterClient()\n  const originalHighlightOnce = mockClient.highlightOnce.bind(mockClient)\n\n  mockClient.highlightOnce = async (content: string, filetype: string) => {\n    highlightCount++\n    return originalHighlightOnce(content, filetype)\n  }\n\n  mockClient.setMockResult({\n    highlights: [[0, 3, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  highlightCount = 0\n\n  codeRenderable.content = \"let newMessage = 'world';\"\n  codeRenderable.filetype = \"typescript\"\n\n  await renderOnce()\n\n  mockClient.resolveAllHighlightOnce()\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  expect(highlightCount).toBe(1)\n  expect(codeRenderable.content).toBe(\"let newMessage = 'world';\")\n  expect(codeRenderable.filetype).toBe(\"typescript\")\n})\n\ntest(\"CodeRenderable - batches multiple updates in same tick into single highlight\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  let highlightCount = 0\n  const highlightCalls: Array<{ content: string; filetype: string }> = []\n  const mockClient = new MockTreeSitterClient()\n  const originalHighlightOnce = mockClient.highlightOnce.bind(mockClient)\n\n  mockClient.highlightOnce = async (content: string, filetype: string) => {\n    highlightCount++\n    highlightCalls.push({ content, filetype })\n    return originalHighlightOnce(content, filetype)\n  }\n\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"initial\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  highlightCount = 0\n  highlightCalls.length = 0\n\n  codeRenderable.content = \"first content change\"\n  codeRenderable.filetype = \"typescript\"\n  codeRenderable.content = \"second content change\"\n\n  await renderOnce()\n\n  mockClient.resolveAllHighlightOnce()\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  expect(highlightCount).toBe(1)\n  expect(highlightCalls[0]?.content).toBe(\"second content change\")\n  expect(highlightCalls[0]?.filetype).toBe(\"typescript\")\n})\n\ntest(\"CodeRenderable - renders markdown with TypeScript injection correctly\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(1, 0, 0, 1) }, // Red\n    string: { fg: RGBA.fromValues(0, 1, 0, 1) }, // Green\n    \"markup.heading.1\": { fg: RGBA.fromValues(0, 0, 1, 1) }, // Blue\n  })\n\n  const markdownCode = `# Hello\\n\\n\\`\\`\\`typescript\\nconst msg: string = \"hi\";\\n\\`\\`\\``\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-markdown\",\n    content: markdownCode,\n    filetype: \"markdown\",\n    syntaxStyle,\n    conceal: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  await new Promise((resolve) => setTimeout(resolve, 100))\n  await renderOnce()\n\n  expect(codeRenderable.plainText).toContain(\"# Hello\")\n  expect(codeRenderable.plainText).toContain(\"const msg\")\n  expect(codeRenderable.plainText).toContain(\"typescript\")\n})\n\ntest(\"CodeRenderable - continues highlighting after unresolved promise\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  let highlightCount = 0\n  const pendingPromises: Array<{ content: string; filetype: string; never: boolean }> = []\n\n  class HangingMockClient extends TreeSitterClient {\n    constructor() {\n      super({ dataPath: \"/tmp/mock\" })\n    }\n\n    async highlightOnce(\n      content: string,\n      filetype: string,\n    ): Promise<{ highlights?: SimpleHighlight[]; warning?: string; error?: string }> {\n      highlightCount++\n\n      const shouldHang = highlightCount === 4 && filetype === \"typescript\"\n\n      pendingPromises.push({ content, filetype, never: shouldHang })\n\n      if (shouldHang) {\n        return new Promise(() => {})\n      }\n\n      return Promise.resolve({ highlights: [] })\n    }\n  }\n\n  const mockClient = new HangingMockClient()\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"interface User { name: string; }\",\n    filetype: \"typescript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n  await new Promise((resolve) => setTimeout(resolve, 20))\n\n  highlightCount = 0\n  pendingPromises.length = 0\n\n  codeRenderable.content = \"const message = 'hello';\"\n  codeRenderable.filetype = \"javascript\"\n  await renderOnce()\n  await new Promise((resolve) => setTimeout(resolve, 20))\n\n  codeRenderable.content = \"# Documentation\"\n  codeRenderable.filetype = \"markdown\"\n  await renderOnce()\n  await new Promise((resolve) => setTimeout(resolve, 20))\n\n  codeRenderable.content = \"const message = 'world';\"\n  codeRenderable.filetype = \"javascript\"\n  await renderOnce()\n  await new Promise((resolve) => setTimeout(resolve, 20))\n\n  codeRenderable.content = \"interface User { name: string; }\"\n  codeRenderable.filetype = \"typescript\"\n  await renderOnce()\n  await new Promise((resolve) => setTimeout(resolve, 20))\n\n  codeRenderable.content = \"# New Documentation\"\n  codeRenderable.filetype = \"markdown\"\n  await renderOnce()\n  await new Promise((resolve) => setTimeout(resolve, 20))\n\n  const markdownHighlightHappened = pendingPromises.some(\n    (p) => p.content === \"# New Documentation\" && p.filetype === \"markdown\",\n  )\n\n  expect(codeRenderable.content).toBe(\"# New Documentation\")\n  expect(codeRenderable.filetype).toBe(\"markdown\")\n  expect(markdownHighlightHappened).toBe(true)\n  expect(highlightCount).toBe(5)\n})\n\ntest(\"CodeRenderable - concealment is enabled by default\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n  })\n\n  expect(codeRenderable.conceal).toBe(true)\n})\n\ntest(\"CodeRenderable - concealment can be disabled explicitly\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    conceal: false,\n  })\n\n  expect(codeRenderable.conceal).toBe(false)\n})\n\ntest(\"CodeRenderable - applies concealment to styled text\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: true,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  expect(codeRenderable.conceal).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.content).toBe(\"const message = 'hello';\")\n})\n\ntest(\"CodeRenderable - updating conceal triggers re-highlighting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    conceal: true,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(codeRenderable.conceal).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  codeRenderable.conceal = false\n  expect(codeRenderable.conceal).toBe(false)\n\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n})\n\ntest(\"CodeRenderable - drawUnstyledText is true by default\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n  })\n\n  expect(codeRenderable.drawUnstyledText).toBe(true)\n})\n\ntest(\"CodeRenderable - drawUnstyledText can be set to false\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    drawUnstyledText: false,\n  })\n\n  expect(codeRenderable.drawUnstyledText).toBe(false)\n})\n\ntest(\"CodeRenderable - with drawUnstyledText=true, text renders before highlighting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: true,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n})\n\ntest(\"CodeRenderable - with drawUnstyledText=false, text does not render before highlighting but lineCount is correct\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  // Text buffer has content (for lineCount), but nothing renders yet\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n  expect(codeRenderable.lineCount).toBe(1)\n  const frameBeforeHighlighting = captureFrame()\n  expect(frameBeforeHighlighting.trim()).toBe(\"\")\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n  const frameAfterHighlighting = captureFrame()\n  expect(frameAfterHighlighting).toContain(\"const message\")\n})\n\ntest(\"CodeRenderable - updating drawUnstyledText from false to true triggers re-highlighting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  expect(codeRenderable.drawUnstyledText).toBe(false)\n\n  await renderOnce()\n  // Text buffer has content for lineCount, but we can verify nothing renders\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n  expect(codeRenderable.lineCount).toBe(1)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  codeRenderable.drawUnstyledText = true\n  expect(codeRenderable.drawUnstyledText).toBe(true)\n\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(false)\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n})\n\ntest(\"CodeRenderable - updating drawUnstyledText from true to false triggers re-highlighting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: true,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(codeRenderable.drawUnstyledText).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  codeRenderable.drawUnstyledText = false\n  expect(codeRenderable.drawUnstyledText).toBe(false)\n\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n})\n\ntest(\"CodeRenderable - uses fallback rendering on error even with drawUnstyledText=false\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n\n  mockClient.highlightOnce = async () => {\n    throw new Error(\"Highlighting failed\")\n  }\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello world';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  await new Promise((resolve) => setTimeout(resolve, 20))\n  await renderOnce()\n\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello world';\")\n})\n\ntest(\"CodeRenderable - with drawUnstyledText=false and no filetype, fallback is used\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello world';\",\n    syntaxStyle,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  await renderOnce()\n\n  expect(codeRenderable.filetype).toBeUndefined()\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello world';\")\n})\n\ntest(\"CodeRenderable - with drawUnstyledText=false, multiple updates only render final highlighted text\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 3, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  // Text buffer has content (for lineCount), but nothing renders yet\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n  expect(codeRenderable.lineCount).toBe(1)\n  const frameBeforeHighlighting = captureFrame()\n  expect(frameBeforeHighlighting.trim()).toBe(\"\")\n\n  codeRenderable.content = \"let newMessage = 'world';\"\n  await renderOnce()\n\n  // Text buffer updated immediately, but still no rendering\n  expect(codeRenderable.plainText).toBe(\"let newMessage = 'world';\")\n  expect(codeRenderable.lineCount).toBe(1)\n  const frameAfterUpdate = captureFrame()\n  expect(frameAfterUpdate.trim()).toBe(\"\")\n\n  mockClient.resolveAllHighlightOnce()\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  expect(mockClient.isHighlighting()).toBe(false)\n  expect(codeRenderable.plainText).toBe(\"let newMessage = 'world';\")\n  const frameAfterHighlighting = captureFrame()\n  expect(frameAfterHighlighting).toContain(\"let newMessage\")\n})\n\n// TODO: flaky in CI because it needs to finish in time\n// lib/tree-sitter/client.ts needs a way to check if the queue is empty\n// then this can wait for all tree-sitter operations to complete\n// instead of the arbitrary 500ms wait\n// it worked before because text was set anyway for drawUnstyledText=false\ntest.skip(\"CodeRenderable - simulates markdown stream from LLM with async updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n    string: { fg: RGBA.fromValues(0, 1, 0, 1) },\n    \"markup.heading.1\": { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  // Base markdown content that we'll repeat to grow to ~1MB\n  const baseMarkdownContent = `# Code Example\n\nHere's a simple TypeScript function:\n\n\\`\\`\\`typescript\nfunction greet(name: string): string {\n  return \\`Hello, \\${name}!\\`;\n}\n\nconst message = greet(\"World\");\nconsole.log(message);\n\\`\\`\\`\n`\n\n  const targetSize = 64 * 128\n  let fullMarkdownContent = \"\"\n  let iteration = 0\n  while (fullMarkdownContent.length < targetSize) {\n    fullMarkdownContent += `\\n--- Iteration ${iteration} ---\\n\\n` + baseMarkdownContent\n    iteration++\n  }\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-markdown-stream\",\n    content: \"\",\n    filetype: \"markdown\",\n    syntaxStyle,\n    conceal: false,\n    left: 0,\n    top: 0,\n    drawUnstyledText: false,\n  })\n  await codeRenderable.treeSitterClient.initialize()\n  await codeRenderable.treeSitterClient.preloadParser(\"markdown\")\n\n  currentRenderer.root.add(codeRenderable)\n  currentRenderer.start()\n\n  let currentContent = \"\"\n\n  const chunkSize = 64\n  const chunks: string[] = []\n  for (let i = 0; i < fullMarkdownContent.length; i += chunkSize) {\n    chunks.push(fullMarkdownContent.slice(i, Math.min(i + chunkSize, fullMarkdownContent.length)))\n  }\n\n  for (let i = 0; i < chunks.length; i++) {\n    const chunk = chunks[i]\n    currentContent += chunk\n    codeRenderable.content = currentContent\n    await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 25) + 1))\n  }\n\n  // wait for highlighting to complete (long for slow machines/CI)\n  await new Promise((resolve) => setTimeout(resolve, 500))\n\n  expect(codeRenderable.content).toBe(fullMarkdownContent)\n  expect(codeRenderable.content.length).toBeGreaterThanOrEqual(targetSize)\n  expect(codeRenderable.plainText).toContain(\"# Code Example\")\n  expect(codeRenderable.plainText).toContain(\"function greet\")\n  expect(codeRenderable.plainText).toContain(\"typescript\")\n  expect(codeRenderable.plainText).toContain(\"Hello\")\n\n  const plainText = codeRenderable.plainText\n  expect(plainText.length).toBeGreaterThan(targetSize * 0.9)\n  expect(plainText).toContain(\"Code Example\")\n  expect(plainText).toContain(\"const message = greet\")\n})\n\ntest(\"CodeRenderable - streaming option is false by default\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n  })\n\n  expect(codeRenderable.streaming).toBe(false)\n})\n\ntest(\"CodeRenderable - streaming can be enabled\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  expect(codeRenderable.streaming).toBe(true)\n})\n\ntest(\"CodeRenderable - streaming mode respects drawUnstyledText only for initial content\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const initial = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n    drawUnstyledText: true,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  await renderOnce()\n  expect(codeRenderable.plainText).toBe(\"const initial = 'hello';\")\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  codeRenderable.content = \"const updated = 'world';\"\n  await new Promise((resolve) => queueMicrotask(resolve))\n\n  expect(codeRenderable.content).toBe(\"const updated = 'world';\")\n})\n\ntest(\"CodeRenderable - streaming mode with drawUnstyledText=false waits for new highlights\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient({ autoResolveTimeout: 10 })\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const initial = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  currentRenderer.start()\n\n  await Bun.sleep(30)\n\n  expect(codeRenderable.plainText).toBe(\"const initial = 'hello';\")\n\n  codeRenderable.content = \"const updated = 'world';\"\n  expect(codeRenderable.plainText).toBe(\"const initial = 'hello';\")\n\n  await Bun.sleep(30)\n\n  expect(codeRenderable.plainText).toBe(\"const updated = 'world';\")\n\n  currentRenderer.stop()\n})\n\ntest(\"CodeRenderable - onChunks callback can transform chunks when highlights are empty\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  let callbackInvoked = false\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"hello\",\n    filetype: \"plaintext\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    onChunks: (chunks) => {\n      callbackInvoked = true\n      return chunks.map((chunk) => ({\n        ...chunk,\n        text: chunk.text.toUpperCase(),\n      }))\n    },\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(callbackInvoked).toBe(true)\n  expect(codeRenderable.plainText).toBe(\"HELLO\")\n})\n\ntest(\"CodeRenderable - onHighlight callback receives highlights and context\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  let callbackInvoked = false\n  let receivedHighlights: SimpleHighlight[] | null = null\n  let receivedContext: { content: string; filetype: string | undefined; syntaxStyle: SyntaxStyle } | null = null\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    onHighlight: (highlights, context) => {\n      callbackInvoked = true\n      receivedHighlights = [...highlights]\n      receivedContext = { ...context }\n      return highlights\n    },\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(callbackInvoked).toBe(true)\n  expect(receivedHighlights).not.toBeNull()\n  expect(receivedHighlights?.length).toBe(1)\n  expect(receivedHighlights?.[0]).toEqual([0, 5, \"keyword\"])\n  expect(receivedContext?.content).toBe(\"const message = 'hello';\")\n  expect(receivedContext?.filetype).toBe(\"javascript\")\n  expect(receivedContext?.syntaxStyle).toBe(syntaxStyle)\n})\n\ntest(\"CodeRenderable - onHighlight callback can add custom highlights\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n    \"custom.highlight\": { fg: RGBA.fromValues(1, 0, 0, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    onHighlight: (highlights) => {\n      highlights.push([6, 13, \"custom.highlight\", {}])\n      return highlights\n    },\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n\n  // Verify both the original keyword highlight and the custom highlight are applied\n  const lineHighlights = codeRenderable.getLineHighlights(0)\n  expect(lineHighlights.length).toBeGreaterThanOrEqual(2)\n\n  // Check keyword highlight exists with the correct styleId\n  const keywordStyleId = syntaxStyle.getStyleId(\"keyword\")\n  const keywordHighlight = lineHighlights.find((h) => h.styleId === keywordStyleId)\n  expect(keywordHighlight).toBeDefined()\n\n  // Check custom highlight exists with the correct styleId\n  const customStyleId = syntaxStyle.getStyleId(\"custom.highlight\")\n  const customHighlight = lineHighlights.find((h) => h.styleId === customStyleId)\n  expect(customHighlight).toBeDefined()\n})\n\ntest(\"CodeRenderable - onHighlight callback returning undefined uses original highlights\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  let callbackInvoked = false\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    onHighlight: (highlights) => {\n      callbackInvoked = true\n      return undefined as unknown as SimpleHighlight[]\n    },\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(callbackInvoked).toBe(true)\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n})\n\ntest(\"CodeRenderable - onHighlight callback is called on re-highlighting when content changes\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  let callbackCount = 0\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    onHighlight: (highlights) => {\n      callbackCount++\n      return highlights\n    },\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(callbackCount).toBe(1)\n\n  codeRenderable.content = \"let newMessage = 'world';\"\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(callbackCount).toBe(2)\n})\n\ntest(\"CodeRenderable - onHighlight callback supports async functions\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n    \"async.highlight\": { fg: RGBA.fromValues(0, 1, 0, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  let asyncCallbackCompleted = false\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const message = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    onHighlight: async (highlights) => {\n      // Simulate async operation (e.g., fetching additional highlight data)\n      await new Promise((resolve) => setTimeout(resolve, 5))\n      highlights.push([6, 13, \"async.highlight\", {}])\n      asyncCallbackCompleted = true\n      return highlights\n    },\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 20))\n  await renderOnce()\n\n  expect(asyncCallbackCompleted).toBe(true)\n  expect(codeRenderable.plainText).toBe(\"const message = 'hello';\")\n\n  // Verify the async highlight was applied\n  const lineHighlights = codeRenderable.getLineHighlights(0)\n  expect(lineHighlights.length).toBeGreaterThanOrEqual(2)\n\n  const asyncStyleId = syntaxStyle.getStyleId(\"async.highlight\")\n  const asyncHighlight = lineHighlights.find((h) => h.styleId === asyncStyleId && h.start === 6 && h.end === 13)\n  expect(asyncHighlight).toBeDefined()\n})\n\ntest(\"CodeRenderable - streaming mode caches highlights between updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const initial = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  codeRenderable.content = \"const updated = 'world';\"\n  await new Promise((resolve) => queueMicrotask(resolve))\n\n  codeRenderable.content = \"const updated2 = 'test';\"\n  await new Promise((resolve) => queueMicrotask(resolve))\n\n  codeRenderable.content = \"const final = 'done';\"\n  await new Promise((resolve) => queueMicrotask(resolve))\n\n  await renderOnce()\n\n  expect(codeRenderable.content).toBe(\"const final = 'done';\")\n  expect(codeRenderable.plainText).toBe(\"const final = 'done';\")\n})\n\ntest(\"CodeRenderable - streaming mode works with large content updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const x = 1;\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n    drawUnstyledText: true,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  // Wait for initial highlighting\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  // Simulate streaming with progressively larger content\n  let content = \"const x = 1;\"\n  for (let i = 0; i < 10; i++) {\n    content += `\\nconst var${i} = ${i};`\n    codeRenderable.content = content\n    await new Promise((resolve) => setTimeout(resolve, 5))\n  }\n\n  await renderOnce()\n  mockClient.resolveAllHighlightOnce()\n  await new Promise((resolve) => setTimeout(resolve, 20))\n  await renderOnce()\n\n  expect(codeRenderable.content).toContain(\"const var9 = 9;\")\n  expect(codeRenderable.plainText).toContain(\"const var9 = 9;\")\n})\n\ntest(\"CodeRenderable - disabling streaming clears cached highlights\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const initial = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(codeRenderable.streaming).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  codeRenderable.streaming = false\n  expect(codeRenderable.streaming).toBe(false)\n\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n})\n\ntest(\"CodeRenderable - streaming mode with drawUnstyledText=false shows nothing initially\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const initial = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  await renderOnce()\n  const frameBeforeHighlighting = captureFrame()\n  expect(frameBeforeHighlighting.trim()).toBe(\"\")\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  const frameAfterHighlighting = captureFrame()\n  expect(frameAfterHighlighting).toContain(\"const initial\")\n})\n\ntest(\"CodeRenderable - streaming mode handles empty cached highlights gracefully\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"plain text\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n    drawUnstyledText: true,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  codeRenderable.content = \"more plain text\"\n  await renderOnce()\n\n  expect(codeRenderable.content).toBe(\"more plain text\")\n  expect(codeRenderable.plainText).toBe(\"more plain text\")\n})\n\ntest(\"CodeRenderable - selection across two Code renderables in flex row\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const container = new BoxRenderable(currentRenderer, {\n    id: \"container\",\n    width: 80,\n    height: 10,\n    flexDirection: \"row\",\n    left: 0,\n    top: 0,\n  })\n  currentRenderer.root.add(container)\n\n  const leftCode = new CodeRenderable(currentRenderer, {\n    id: \"left-code\",\n    content: \"line1\\nline2\\nline3\\nline4\\nline5\",\n    syntaxStyle,\n    selectable: true,\n    wrapMode: \"none\",\n    width: 20,\n    height: 5,\n  })\n\n  const rightCode = new CodeRenderable(currentRenderer, {\n    id: \"right-code\",\n    content: \"lineA\\nlineB\\nlineC\\nlineD\\nlineE\",\n    syntaxStyle,\n    selectable: true,\n    wrapMode: \"none\",\n    width: 20,\n    height: 5,\n  })\n\n  container.add(leftCode)\n  container.add(rightCode)\n\n  await renderOnce()\n\n  expect(leftCode.x).toBe(0)\n  expect(rightCode.x).toBeGreaterThan(leftCode.x)\n\n  const startX = leftCode.x + 2\n  const startY = leftCode.y + 2\n  const endX = rightCode.x + 3\n  const endY = rightCode.y + rightCode.height + 2\n\n  await mockMouse.drag(startX, startY, endX, endY)\n  await renderOnce()\n\n  expect(leftCode.hasSelection()).toBe(true)\n  expect(rightCode.hasSelection()).toBe(true)\n\n  const leftSelection = leftCode.getSelectedText()\n  const rightSelection = rightCode.getSelectedText()\n  const leftSelectionObj = leftCode.getSelection()\n  const rightSelectionObj = rightCode.getSelection()\n\n  expect(leftSelectionObj).not.toBeNull()\n  expect(rightSelectionObj).not.toBeNull()\n\n  if (leftSelectionObj && rightSelectionObj) {\n    expect(leftSelectionObj.start).toBeGreaterThan(0)\n    expect(leftSelectionObj.end).toBe(29)\n    expect(rightSelectionObj.start).toBe(0)\n    expect(rightSelectionObj.end).toBe(29)\n    expect(leftSelection).toBe(\"ne3\\nline4\\nline5\")\n    expect(rightSelection).toBe(\"lineA\\nlineB\\nlineC\\nlineD\\nlineE\")\n  }\n})\n\ntest(\"CodeRenderable - content update during async highlighting does not get overwritten by stale highlight result\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({\n    highlights: [[0, 5, \"keyword\"]] as SimpleHighlight[],\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"line1\\nline2\\nline3\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: true,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n  expect(codeRenderable.lineCount).toBe(3)\n\n  codeRenderable.content = \"line1\\nline2\\nline3\\nline4\\nline5\"\n  expect(codeRenderable.lineCount).toBe(5)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n\n  expect(codeRenderable.content).toBe(\"line1\\nline2\\nline3\\nline4\\nline5\")\n  expect(codeRenderable.lineCount).toBe(5)\n\n  await renderOnce()\n  expect(codeRenderable.lineCount).toBe(5)\n\n  expect(mockClient.isHighlighting()).toBe(true)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.content).toBe(\"line1\\nline2\\nline3\\nline4\\nline5\")\n  expect(codeRenderable.lineCount).toBe(5)\n  expect(codeRenderable.plainText).toBe(\"line1\\nline2\\nline3\\nline4\\nline5\")\n})\n\ntest(\"CodeRenderable - lineCount is correct immediately with drawUnstyledText=false\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"line1\\nline2\\nline3\\nline4\\nline5\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n  })\n\n  expect(codeRenderable.lineCount).toBe(5)\n  expect(codeRenderable.content).toBe(\"line1\\nline2\\nline3\\nline4\\nline5\")\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n  expect(codeRenderable.lineCount).toBe(5)\n\n  const frameBeforeHighlighting = captureFrame()\n  expect(frameBeforeHighlighting.trim()).toBe(\"\")\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.lineCount).toBe(5)\n  const frameAfterHighlighting = captureFrame()\n  expect(frameAfterHighlighting).toContain(\"line1\")\n})\n\ntest(\"CodeRenderable - lineCount updates correctly when content changes with drawUnstyledText=false\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"line1\\nline2\\nline3\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n  })\n\n  expect(codeRenderable.lineCount).toBe(3)\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  codeRenderable.content = \"line1\\nline2\\nline3\\nline4\\nline5\\nline6\\nline7\"\n  expect(codeRenderable.lineCount).toBe(7)\n\n  await renderOnce()\n  expect(codeRenderable.lineCount).toBe(7)\n\n  codeRenderable.content = \"line1\\nline2\"\n  expect(codeRenderable.lineCount).toBe(2)\n\n  await renderOnce()\n  expect(codeRenderable.lineCount).toBe(2)\n})\n\ntest(\"CodeRenderable - lineInfo is accessible with drawUnstyledText=false before highlighting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"short\\nlonger line here\\nmed\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  expect(codeRenderable.lineCount).toBe(3)\n  expect(codeRenderable.lineInfo.lineStartCols.length).toBe(3)\n\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n  expect(codeRenderable.lineInfo.lineStartCols.length).toBe(3)\n  expect(codeRenderable.lineInfo.lineSources.length).toBe(3)\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.lineInfo.lineStartCols.length).toBe(3)\n  expect(codeRenderable.lineInfo.lineSources.length).toBe(3)\n})\n\ntest(\"CodeRenderable - plainText reflects content immediately with drawUnstyledText=false\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"initial content\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n  })\n\n  expect(codeRenderable.plainText).toBe(\"initial content\")\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(mockClient.isHighlighting()).toBe(true)\n  expect(codeRenderable.plainText).toBe(\"initial content\")\n\n  codeRenderable.content = \"updated content\"\n  expect(codeRenderable.plainText).toBe(\"updated content\")\n\n  await renderOnce()\n  const frame = captureFrame()\n  expect(frame.trim()).toBe(\"\")\n\n  mockClient.resolveAllHighlightOnce()\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.plainText).toBe(\"updated content\")\n  const finalFrame = captureFrame()\n  expect(finalFrame).toContain(\"updated content\")\n})\n\ntest(\"CodeRenderable - textLength is correct with drawUnstyledText=false\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const content = \"hello world test\"\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content,\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    drawUnstyledText: false,\n  })\n\n  expect(codeRenderable.textLength).toBe(content.length)\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  expect(codeRenderable.textLength).toBe(content.length)\n\n  const newContent = \"longer content here\"\n  codeRenderable.content = newContent\n  expect(codeRenderable.textLength).toBe(newContent.length)\n})\n\ntest(\"CodeRenderable - streaming mode with drawUnstyledText=false has correct lineCount\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n  mockClient.setMockResult({ highlights: [] })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"line1\\nline2\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n    drawUnstyledText: false,\n  })\n\n  expect(codeRenderable.lineCount).toBe(2)\n\n  currentRenderer.root.add(codeRenderable)\n  await renderOnce()\n\n  const frameBeforeHighlighting = captureFrame()\n  expect(frameBeforeHighlighting.trim()).toBe(\"\")\n\n  mockClient.resolveHighlightOnce(0)\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.lineCount).toBe(2)\n\n  codeRenderable.content = \"line1\\nline2\\nline3\\nline4\"\n  expect(codeRenderable.lineCount).toBe(2)\n\n  codeRenderable.content = \"line1\\nline2\\nline3\\nline4\\nline5\\nline6\"\n  expect(codeRenderable.lineCount).toBe(2)\n\n  await renderOnce()\n  mockClient.resolveAllHighlightOnce()\n  await new Promise((resolve) => setTimeout(resolve, 10))\n  await renderOnce()\n\n  expect(codeRenderable.lineCount).toBe(6)\n  const finalFrame = captureFrame()\n  expect(finalFrame).toContain(\"line1\")\n})\n\ntest(\"CodeRenderable - streaming with conceal and drawUnstyledText=false should not jump when fenced code blocks are concealed\", async () => {\n  resize(80, 20)\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(0, 0, 1, 1) },\n    string: { fg: RGBA.fromValues(0, 1, 0, 1) },\n    \"markup.heading.1\": { fg: RGBA.fromValues(0, 0, 1, 1) },\n    \"markup.raw.block\": { fg: RGBA.fromValues(0.5, 0.5, 0.5, 1) },\n  })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-markdown\",\n    content: \"# Example\",\n    filetype: \"markdown\",\n    syntaxStyle,\n    streaming: true,\n    conceal: true,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n\n  const waitForHighlightingCycle = async (timeout = 2000) => {\n    const start = Date.now()\n    await renderOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    while (codeRenderable.isHighlighting && Date.now() - start < timeout) {\n      await new Promise((resolve) => setTimeout(resolve, 10))\n    }\n    await renderOnce()\n  }\n\n  // Use TestRecorder to capture frames\n  const { TestRecorder } = await import(\"../testing/test-recorder\")\n  const recorder = new TestRecorder(currentRenderer)\n\n  // Start renderer and recorder\n  currentRenderer.start()\n  recorder.rec()\n\n  // Wait for initial highlighting to complete\n  await waitForHighlightingCycle()\n\n  // Now simulate streaming: add more content including fenced code block\n  codeRenderable.content = `# Example\\n\\nHere's some code:\\n\\n\\`\\`\\`typescript\\nconst x = 1;\\n\\`\\`\\``\n\n  // Wait for highlighting to process the update\n  await waitForHighlightingCycle()\n\n  // Stop everything\n  currentRenderer.stop()\n  recorder.stop()\n\n  const frames = recorder.recordedFrames\n\n  // Analyze frames to detect the presence of backticks\n  const frameAnalysis: Array<{ hasBackticks: boolean; lineCount: number; isEmpty: boolean }> = []\n\n  for (const recordedFrame of frames) {\n    const frame = recordedFrame.frame\n    const hasBackticks = frame.includes(\"```\")\n    const lines = frame.split(\"\\n\").filter((line) => line.trim().length > 0)\n    const isEmpty = frame.trim().length === 0\n\n    frameAnalysis.push({\n      hasBackticks,\n      lineCount: lines.length,\n      isEmpty,\n    })\n  }\n\n  let hasFlickering = false\n  for (let i = 2; i < frameAnalysis.length; i++) {\n    const prev = frameAnalysis[i - 1]\n    const curr = frameAnalysis[i]\n    if (!prev.isEmpty && curr.isEmpty) {\n      hasFlickering = true\n    }\n  }\n\n  const framesWithBackticks = frameAnalysis.filter((f) => f.hasBackticks && !f.isEmpty)\n\n  expect(framesWithBackticks.length).toBe(0)\n  expect(hasFlickering).toBe(false)\n\n  const finalFrame = frameAnalysis[frameAnalysis.length - 1]\n  expect(finalFrame.isEmpty).toBe(false)\n  expect(finalFrame.hasBackticks).toBe(false)\n  expect(finalFrame.lineCount).toBe(3)\n\n  const finalFrameText = frames[frames.length - 1].frame\n  expect(finalFrameText).toContain(\"Example\")\n  expect(finalFrameText).toContain(\"Here's some code\")\n  expect(finalFrameText).toContain(\"const x = 1\")\n  expect(finalFrameText).not.toContain(\"```\")\n})\n\ntest(\"CodeRenderable - streaming with drawUnstyledText=false falls back to unstyled text when highlights fail\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient({ autoResolveTimeout: 10 })\n\n  const codeRenderable = new CodeRenderable(currentRenderer, {\n    id: \"test-code\",\n    content: \"const initial = 'hello';\",\n    filetype: \"javascript\",\n    syntaxStyle,\n    treeSitterClient: mockClient,\n    streaming: true,\n    drawUnstyledText: false,\n    left: 0,\n    top: 0,\n  })\n\n  currentRenderer.root.add(codeRenderable)\n  currentRenderer.start()\n\n  await Bun.sleep(30)\n\n  mockClient.highlightOnce = async () => {\n    throw new Error(\"Highlighting failed\")\n  }\n\n  codeRenderable.content = \"const updated = 'world';\"\n\n  await Bun.sleep(30)\n\n  expect(codeRenderable.plainText).toBe(\"const updated = 'world';\")\n\n  currentRenderer.stop()\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Code.ts",
    "content": "import { type RenderContext } from \"../types.js\"\nimport { StyledText } from \"../lib/styled-text.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { getTreeSitterClient, treeSitterToStyledText, TreeSitterClient } from \"../lib/tree-sitter/index.js\"\nimport { TextBufferRenderable, type TextBufferOptions } from \"./TextBufferRenderable.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport type { SimpleHighlight } from \"../lib/tree-sitter/types.js\"\nimport type { TextChunk } from \"../text-buffer.js\"\nimport { treeSitterToTextChunks } from \"../lib/tree-sitter-styled-text.js\"\n\nexport interface HighlightContext {\n  content: string\n  filetype: string\n  syntaxStyle: SyntaxStyle\n}\n\nexport type OnHighlightCallback = (\n  highlights: SimpleHighlight[],\n  context: HighlightContext,\n) => SimpleHighlight[] | undefined | Promise<SimpleHighlight[] | undefined>\n\nexport interface ChunkRenderContext extends HighlightContext {\n  highlights: SimpleHighlight[]\n}\n\nexport type OnChunksCallback = (\n  chunks: TextChunk[],\n  context: ChunkRenderContext,\n) => TextChunk[] | undefined | Promise<TextChunk[] | undefined>\n\nexport interface CodeOptions extends TextBufferOptions {\n  content?: string\n  filetype?: string\n  syntaxStyle: SyntaxStyle\n  treeSitterClient?: TreeSitterClient\n  conceal?: boolean\n  drawUnstyledText?: boolean\n  streaming?: boolean\n  onHighlight?: OnHighlightCallback\n  onChunks?: OnChunksCallback\n}\n\nexport class CodeRenderable extends TextBufferRenderable {\n  private _content: string\n  private _filetype?: string\n  private _syntaxStyle: SyntaxStyle\n  private _isHighlighting: boolean = false\n  private _treeSitterClient: TreeSitterClient\n  private _highlightsDirty: boolean = false\n  private _highlightSnapshotId: number = 0\n  private _conceal: boolean\n  private _drawUnstyledText: boolean\n  private _shouldRenderTextBuffer: boolean = true\n  private _streaming: boolean\n  private _hadInitialContent: boolean = false\n  private _lastHighlights: SimpleHighlight[] = []\n  private _onHighlight?: OnHighlightCallback\n  private _onChunks?: OnChunksCallback\n  private _highlightingPromise: Promise<void> = Promise.resolve()\n\n  protected _contentDefaultOptions = {\n    content: \"\",\n    conceal: true,\n    drawUnstyledText: true,\n    streaming: false,\n  } satisfies Partial<CodeOptions>\n\n  constructor(ctx: RenderContext, options: CodeOptions) {\n    super(ctx, options)\n\n    this._content = options.content ?? this._contentDefaultOptions.content\n    this._filetype = options.filetype\n    this._syntaxStyle = options.syntaxStyle\n    this._treeSitterClient = options.treeSitterClient ?? getTreeSitterClient()\n    this._conceal = options.conceal ?? this._contentDefaultOptions.conceal\n    this._drawUnstyledText = options.drawUnstyledText ?? this._contentDefaultOptions.drawUnstyledText\n    this._streaming = options.streaming ?? this._contentDefaultOptions.streaming\n    this._onHighlight = options.onHighlight\n    this._onChunks = options.onChunks\n\n    if (this._content.length > 0) {\n      this.textBuffer.setText(this._content)\n      this.updateTextInfo()\n      this._shouldRenderTextBuffer = this._drawUnstyledText || !this._filetype\n    }\n\n    this._highlightsDirty = this._content.length > 0\n  }\n\n  get content(): string {\n    return this._content\n  }\n\n  set content(value: string) {\n    if (this._content !== value) {\n      this._content = value\n      this._highlightsDirty = true\n      this._highlightSnapshotId++\n\n      if (this._streaming && !this._drawUnstyledText && this._filetype) {\n        return\n      }\n\n      this.textBuffer.setText(value)\n      this.updateTextInfo()\n    }\n  }\n\n  get filetype(): string | undefined {\n    return this._filetype\n  }\n\n  set filetype(value: string | undefined) {\n    if (this._filetype !== value) {\n      this._filetype = value\n      this._highlightsDirty = true\n    }\n  }\n\n  get syntaxStyle(): SyntaxStyle {\n    return this._syntaxStyle\n  }\n\n  set syntaxStyle(value: SyntaxStyle) {\n    if (this._syntaxStyle !== value) {\n      this._syntaxStyle = value\n      this._highlightsDirty = true\n    }\n  }\n\n  get conceal(): boolean {\n    return this._conceal\n  }\n\n  set conceal(value: boolean) {\n    if (this._conceal !== value) {\n      this._conceal = value\n      this._highlightsDirty = true\n    }\n  }\n\n  get drawUnstyledText(): boolean {\n    return this._drawUnstyledText\n  }\n\n  set drawUnstyledText(value: boolean) {\n    if (this._drawUnstyledText !== value) {\n      this._drawUnstyledText = value\n      this._highlightsDirty = true\n    }\n  }\n\n  get streaming(): boolean {\n    return this._streaming\n  }\n\n  set streaming(value: boolean) {\n    if (this._streaming !== value) {\n      this._streaming = value\n      this._hadInitialContent = false\n      this._lastHighlights = []\n      this._highlightsDirty = true\n    }\n  }\n\n  get treeSitterClient(): TreeSitterClient {\n    return this._treeSitterClient\n  }\n\n  set treeSitterClient(value: TreeSitterClient) {\n    if (this._treeSitterClient !== value) {\n      this._treeSitterClient = value\n      this._highlightsDirty = true\n    }\n  }\n\n  get onHighlight(): OnHighlightCallback | undefined {\n    return this._onHighlight\n  }\n\n  set onHighlight(value: OnHighlightCallback | undefined) {\n    if (this._onHighlight !== value) {\n      this._onHighlight = value\n      this._highlightsDirty = true\n    }\n  }\n\n  get onChunks(): OnChunksCallback | undefined {\n    return this._onChunks\n  }\n\n  set onChunks(value: OnChunksCallback | undefined) {\n    if (this._onChunks !== value) {\n      this._onChunks = value\n      this._highlightsDirty = true\n    }\n  }\n\n  get isHighlighting(): boolean {\n    return this._isHighlighting\n  }\n\n  get highlightingDone(): Promise<void> {\n    return this._highlightingPromise\n  }\n\n  protected async transformChunks(chunks: TextChunk[], context: ChunkRenderContext): Promise<TextChunk[]> {\n    if (!this._onChunks) return chunks\n\n    const modified = await this._onChunks(chunks, context)\n    return modified ?? chunks\n  }\n\n  private ensureVisibleTextBeforeHighlight(): void {\n    if (this.isDestroyed) return\n\n    const content = this._content\n\n    if (!this._filetype) {\n      this._shouldRenderTextBuffer = true\n      return\n    }\n\n    const isInitialContent = this._streaming && !this._hadInitialContent\n    const shouldDrawUnstyledNow = this._streaming ? isInitialContent && this._drawUnstyledText : this._drawUnstyledText\n\n    if (this._streaming && !isInitialContent) {\n      this._shouldRenderTextBuffer = true\n    } else if (shouldDrawUnstyledNow) {\n      this.textBuffer.setText(content)\n      this._shouldRenderTextBuffer = true\n    } else {\n      this._shouldRenderTextBuffer = false\n    }\n  }\n\n  private async startHighlight(): Promise<void> {\n    const content = this._content\n    const filetype = this._filetype\n    const snapshotId = ++this._highlightSnapshotId\n\n    if (!filetype) return\n\n    const isInitialContent = this._streaming && !this._hadInitialContent\n    if (isInitialContent) {\n      this._hadInitialContent = true\n    }\n\n    this._isHighlighting = true\n\n    try {\n      const result = await this._treeSitterClient.highlightOnce(content, filetype)\n\n      if (snapshotId !== this._highlightSnapshotId) {\n        return\n      }\n\n      if (this.isDestroyed) return\n\n      let highlights = result.highlights ?? []\n\n      if (this._onHighlight && highlights.length >= 0) {\n        const context: HighlightContext = {\n          content,\n          filetype,\n          syntaxStyle: this._syntaxStyle,\n        }\n        const modified = await this._onHighlight(highlights, context)\n        if (modified !== undefined) {\n          highlights = modified\n        }\n      }\n\n      if (snapshotId !== this._highlightSnapshotId) {\n        return\n      }\n\n      if (this.isDestroyed) return\n\n      if (highlights.length > 0) {\n        if (this._streaming) {\n          this._lastHighlights = highlights\n        }\n      }\n\n      if (highlights.length > 0 || this._onChunks) {\n        const context: ChunkRenderContext = {\n          content,\n          filetype,\n          syntaxStyle: this._syntaxStyle,\n          highlights,\n        }\n\n        let chunks = treeSitterToTextChunks(content, highlights, this._syntaxStyle, {\n          enabled: this._conceal,\n        })\n\n        chunks = await this.transformChunks(chunks, context)\n\n        if (snapshotId !== this._highlightSnapshotId) {\n          return\n        }\n\n        if (this.isDestroyed) return\n\n        const styledText = new StyledText(chunks)\n        this.textBuffer.setStyledText(styledText)\n      } else {\n        this.textBuffer.setText(content)\n      }\n\n      this._shouldRenderTextBuffer = true\n      this._isHighlighting = false\n      this._highlightsDirty = false\n      this.updateTextInfo()\n      this.requestRender()\n    } catch (error) {\n      if (snapshotId !== this._highlightSnapshotId) {\n        return\n      }\n\n      console.warn(\"Code highlighting failed, falling back to plain text:\", error)\n      if (this.isDestroyed) return\n      this.textBuffer.setText(content)\n      this._shouldRenderTextBuffer = true\n      this._isHighlighting = false\n      this._highlightsDirty = false\n      this.updateTextInfo()\n      this.requestRender()\n    }\n  }\n\n  public getLineHighlights(lineIdx: number) {\n    return this.textBuffer.getLineHighlights(lineIdx)\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    if (this._highlightsDirty) {\n      if (this.isDestroyed) return\n\n      if (this._content.length === 0) {\n        this._shouldRenderTextBuffer = false\n        this._highlightsDirty = false\n      } else if (!this._filetype) {\n        this._shouldRenderTextBuffer = true\n        this._highlightsDirty = false\n      } else {\n        this.ensureVisibleTextBeforeHighlight()\n        this._highlightsDirty = false\n        this._highlightingPromise = this.startHighlight()\n      }\n    }\n\n    if (!this._shouldRenderTextBuffer) return\n    super.renderSelf(buffer)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Diff.regression.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { DiffRenderable } from \"./Diff.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { createTestRenderer, type TestRenderer } from \"../testing.js\"\nimport { ManualClock } from \"../testing/manual-clock.js\"\nimport { MockTreeSitterClient } from \"../testing/mock-tree-sitter-client.js\"\nimport type { SimpleHighlight } from \"../lib/tree-sitter/types.js\"\nimport { BoxRenderable } from \"./Box.js\"\nimport { settleDiffHighlighting } from \"./__tests__/renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet captureFrame: () => string\nlet mockClient: MockTreeSitterClient\nlet clock: ManualClock\n\nbeforeEach(async () => {\n  mockClient = new MockTreeSitterClient()\n  clock = new ManualClock()\n\n  const testRenderer = await createTestRenderer({\n    width: 32,\n    height: 10,\n    gatherStats: true,\n    clock,\n  })\n  currentRenderer = testRenderer.renderer\n  renderOnce = testRenderer.renderOnce\n  captureFrame = testRenderer.captureCharFrame\n})\n\nafterEach(async () => {\n  if (currentRenderer) {\n    currentRenderer.destroy()\n  }\n})\n\n// When highlights conceal formatting characters (like **), line lengths change,\n// potentially triggering wrapping changes, height changes, and onResize.\n// This test ensures onResize doesn't cause content resets that create endless loops.\ntest(\"DiffRenderable - no endless loop when concealing markdown formatting\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const markdownDiff = `--- a/test.md\n+++ b/test.md\n@@ -1,2 +1,2 @@\n-Some text **boldtext**\n-Short\n+Some text **boldtext**\n+More text **formats**`\n\n  const mockHighlights: SimpleHighlight[] = [\n    [10, 11, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [11, 12, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [20, 21, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [21, 22, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [33, 34, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [34, 35, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [42, 43, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [43, 44, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n  ]\n\n  mockClient.setMockResult({ highlights: mockHighlights })\n\n  const box = new BoxRenderable(currentRenderer, {\n    id: \"background-box\",\n    border: true,\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: markdownDiff,\n    syntaxStyle,\n    filetype: \"markdown\",\n    conceal: true,\n    treeSitterClient: mockClient,\n  })\n\n  box.add(diffRenderable)\n  currentRenderer.root.add(box)\n\n  await renderOnce()\n  diffRenderable.view = \"split\"\n\n  await renderOnce()\n  diffRenderable.wrapMode = \"word\"\n\n  await settleDiffHighlighting(diffRenderable, mockClient, renderOnce)\n\n  const stats = currentRenderer.getStats()\n  expect(stats.frameCount).toBeLessThan(25)\n})\n\n// Tests that line numbers align correctly and gutter heights are properly sized\n// when switching between view modes and wrap modes in split view\ntest(\"DiffRenderable - line number alignment and gutter heights in split view with wrapping\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const markdownDiff = `--- a/test.md\n+++ b/test.md\n@@ -1,2 +1,2 @@\n-Some text **boldtext**\n-Short\n+Some text **boldtext**\n+More text **formats**`\n\n  const mockHighlights: SimpleHighlight[] = [\n    [10, 11, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [11, 12, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [20, 21, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [21, 22, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [33, 34, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [34, 35, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [42, 43, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n    [43, 44, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }],\n  ]\n\n  mockClient.setMockResult({ highlights: mockHighlights })\n\n  const box = new BoxRenderable(currentRenderer, {\n    id: \"background-box\",\n    border: true,\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: markdownDiff,\n    syntaxStyle,\n    filetype: \"markdown\",\n    conceal: true,\n    treeSitterClient: mockClient,\n  })\n\n  box.add(diffRenderable)\n  currentRenderer.root.add(box)\n\n  await renderOnce()\n  const unifiedFrame = captureFrame()\n\n  expect(unifiedFrame).toContain(\"1 - Some text\")\n  expect(unifiedFrame).toContain(\"2 - Short\")\n  expect(unifiedFrame).toContain(\"1 + Some text\")\n  expect(unifiedFrame).toContain(\"2 + More text\")\n\n  diffRenderable.view = \"split\"\n  await renderOnce()\n  const splitFrame = captureFrame()\n\n  expect(splitFrame).toContain(\"1 - Some text\")\n  expect(splitFrame).toContain(\"1 + Some text\")\n  expect(splitFrame).toContain(\"2 - Short\")\n  expect(splitFrame).toContain(\"2 + More text\")\n\n  // First wrapMode toggle: none → word\n  diffRenderable.wrapMode = \"word\"\n  await settleDiffHighlighting(diffRenderable, mockClient, renderOnce)\n  const splitWrapFrame = captureFrame()\n\n  const diffChildren = diffRenderable.getChildren()\n  const lines = splitWrapFrame.split(\"\\n\")\n\n  let leftLine2Row = -1\n  let rightLine2Row = -1\n\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i]\n    if (line.includes(\"2 - Short\")) {\n      leftLine2Row = i\n    }\n    if (line.includes(\"2 + More\")) {\n      rightLine2Row = i\n    }\n  }\n\n  expect(leftLine2Row).toBeGreaterThan(-1)\n  expect(rightLine2Row).toBeGreaterThan(-1)\n  expect(leftLine2Row).toBe(rightLine2Row)\n  const leftSide = diffChildren[0]\n  const rightSide = diffChildren[1]\n  const leftGutter = leftSide.getChildren()[0]\n  const rightGutter = rightSide.getChildren()[0]\n  const leftCode = leftSide.getChildren()[1]\n  const rightCode = rightSide.getChildren()[1]\n\n  const leftVisualLines = (leftCode as any).lineInfo?.lineSources?.length || 0\n  const rightVisualLines = (rightCode as any).lineInfo?.lineSources?.length || 0\n\n  expect(leftVisualLines).toBe(rightVisualLines)\n  expect(leftGutter.height).toBe(leftVisualLines)\n  expect(rightGutter.height).toBe(rightVisualLines)\n\n  // Second wrapMode toggle: word → none → word\n  diffRenderable.wrapMode = \"none\"\n  await renderOnce()\n  diffRenderable.wrapMode = \"word\"\n  await settleDiffHighlighting(diffRenderable, mockClient, renderOnce)\n  const splitWrapFrame2 = captureFrame()\n  const lines2 = splitWrapFrame2.split(\"\\n\")\n  let leftLine2Row2 = -1\n  let rightLine2Row2 = -1\n\n  for (let i = 0; i < lines2.length; i++) {\n    const line = lines2[i]\n    if (line.includes(\"2 - Short\")) {\n      leftLine2Row2 = i\n    }\n    if (line.includes(\"2 + More\")) {\n      rightLine2Row2 = i\n    }\n  }\n\n  expect(leftLine2Row2).toBeGreaterThan(-1)\n  expect(rightLine2Row2).toBeGreaterThan(-1)\n  expect(leftLine2Row2).toBe(rightLine2Row2)\n\n  expect(splitWrapFrame2).toContain(\"1 - Some text\")\n  expect(splitWrapFrame2).toContain(\"boldtext\")\n  expect(splitWrapFrame2).toContain(\"2 - Short\")\n  expect(splitWrapFrame2).toContain(\"2 + More text\")\n  expect(splitWrapFrame2).toContain(\"formats\")\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Diff.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { DiffRenderable } from \"./Diff.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { createMockMouse, createTestRenderer, type TestRenderer } from \"../testing.js\"\nimport { MockTreeSitterClient } from \"../testing/mock-tree-sitter-client.js\"\nimport type { SimpleHighlight } from \"../lib/tree-sitter/types.js\"\nimport { settleDiffHighlighting } from \"./__tests__/renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet captureFrame: () => string\n\nbeforeEach(async () => {\n  const testRenderer = await createTestRenderer({ width: 80, height: 20 })\n  currentRenderer = testRenderer.renderer\n  renderOnce = testRenderer.renderOnce\n  captureFrame = testRenderer.captureCharFrame\n})\n\nafterEach(async () => {\n  if (currentRenderer) {\n    currentRenderer.destroy()\n  }\n})\n\nconst simpleDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,3 +1,3 @@\n function hello() {\n-  console.log(\"Hello\");\n+  console.log(\"Hello, World!\");\n }`\n\nconst multiLineDiff = `--- a/math.js\n+++ b/math.js\n@@ -1,7 +1,11 @@\n function add(a, b) {\n   return a + b;\n }\n \n+function subtract(a, b) {\n+  return a - b;\n+}\n+\n function multiply(a, b) {\n-  return a * b;\n+  return a * b * 1;\n }`\n\nconst addOnlyDiff = `--- a/new.js\n+++ b/new.js\n@@ -0,0 +1,3 @@\n+function newFunction() {\n+  return true;\n+}`\n\nconst removeOnlyDiff = `--- a/old.js\n+++ b/old.js\n@@ -1,3 +0,0 @@\n-function oldFunction() {\n-  return false;\n-}`\n\nconst largeDiff = `--- a/large.js\n+++ b/large.js\n@@ -42,9 +42,10 @@\n const line42 = 'context';\n const line43 = 'context';\n-const line44 = 'removed';\n+const line44 = 'added';\n const line45 = 'context';\n+const line46 = 'added';\n const line47 = 'context';\n const line48 = 'context';\n-const line49 = 'removed';\n+const line49 = 'changed';\n const line50 = 'context';\n const line51 = 'context';`\n\ntest(\"DiffRenderable - basic construction with unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n  })\n\n  expect(diffRenderable.diff).toBe(simpleDiff)\n  expect(diffRenderable.view).toBe(\"unified\")\n})\n\ntest(\"DiffRenderable - basic construction with split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n  })\n\n  expect(diffRenderable.diff).toBe(simpleDiff)\n  expect(diffRenderable.view).toBe(\"split\")\n})\n\ntest(\"DiffRenderable - defaults to unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    syntaxStyle,\n  })\n\n  expect(diffRenderable.view).toBe(\"unified\")\n})\n\ntest(\"DiffRenderable - unified view renders correctly\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"unified view simple diff\")\n\n  // Check that both removed and added lines are present\n  expect(frame).toContain('console.log(\"Hello\")')\n  expect(frame).toContain('console.log(\"Hello, World!\")')\n})\n\ntest(\"DiffRenderable - split view renders correctly\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view simple diff\")\n\n  // In split view, both sides should be visible (may be wrapped)\n  expect(frame).toContain(\"console.log\")\n  expect(frame).toContain(\"Hello\")\n  expect(frame).toContain(\"World\")\n})\n\ntest(\"DiffRenderable - multi-line diff unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiLineDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"unified view multi-line diff\")\n\n  // Check for additions\n  expect(frame).toContain(\"function subtract\")\n  // Check for modifications\n  expect(frame).toContain(\"a * b * 1\")\n})\n\ntest(\"DiffRenderable - multi-line diff split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiLineDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view multi-line diff\")\n\n  // Left side should have old code\n  expect(frame).toContain(\"a * b\")\n  // Right side should have new code\n  expect(frame).toContain(\"subtract\")\n})\n\ntest(\"DiffRenderable - add-only diff unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: addOnlyDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"unified view add-only diff\")\n\n  expect(frame).toContain(\"newFunction\")\n})\n\ntest(\"DiffRenderable - add-only diff split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: addOnlyDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view add-only diff\")\n\n  // Right side should have the new function\n  expect(frame).toContain(\"newFunction\")\n})\n\ntest(\"DiffRenderable - remove-only diff unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: removeOnlyDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"unified view remove-only diff\")\n\n  expect(frame).toContain(\"oldFunction\")\n})\n\ntest(\"DiffRenderable - remove-only diff split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: removeOnlyDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view remove-only diff\")\n\n  // Left side should have the old function\n  expect(frame).toContain(\"oldFunction\")\n})\n\ntest(\"DiffRenderable - large line numbers displayed correctly\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: largeDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"unified view large line numbers\")\n\n  // Check that line numbers in the 40s are displayed\n  expect(frame).toMatch(/4[0-9]/)\n})\n\ntest(\"DiffRenderable - can toggle view mode\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const unifiedFrame = captureFrame()\n  expect(diffRenderable.view).toBe(\"unified\")\n\n  // Switch to split view\n  diffRenderable.view = \"split\"\n  await renderOnce()\n\n  const splitFrame = captureFrame()\n  expect(diffRenderable.view).toBe(\"split\")\n\n  // Frames should be different\n  expect(unifiedFrame).not.toBe(splitFrame)\n})\n\ntest(\"DiffRenderable - can update diff content\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame1 = captureFrame()\n  expect(frame1).toContain(\"Hello\")\n\n  // Update diff\n  diffRenderable.diff = multiLineDiff\n  await renderOnce()\n\n  const frame2 = captureFrame()\n  expect(frame2).toContain(\"subtract\")\n  expect(frame2).not.toContain('console.log(\"Hello\")')\n})\n\ntest(\"DiffRenderable - can toggle line numbers\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.showLineNumbers).toBe(true)\n\n  // Hide line numbers\n  diffRenderable.showLineNumbers = false\n  await renderOnce()\n\n  expect(diffRenderable.showLineNumbers).toBe(false)\n})\n\ntest(\"DiffRenderable - can update filetype\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    keyword: { fg: RGBA.fromValues(1, 0, 0, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    filetype: \"javascript\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.filetype).toBe(\"javascript\")\n\n  // Update filetype\n  diffRenderable.filetype = \"typescript\"\n  expect(diffRenderable.filetype).toBe(\"typescript\")\n})\n\ntest(\"DiffRenderable - handles empty diff\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: \"\",\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Should not crash with empty diff\n  expect(diffRenderable.diff).toBe(\"\")\n})\n\ntest(\"DiffRenderable - handles diff with no changes\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const noChangeDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,3 +1,3 @@\n function hello() {\n   console.log(\"Hello\");\n }`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: noChangeDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toContain(\"function hello\")\n})\n\ntest(\"DiffRenderable - can update wrapMode\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    wrapMode: \"word\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.wrapMode).toBe(\"word\")\n\n  diffRenderable.wrapMode = \"char\"\n  expect(diffRenderable.wrapMode).toBe(\"char\")\n})\n\ntest(\"DiffRenderable - split view alignment with empty lines\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  // Diff with additions that should create empty lines on left\n  const alignmentDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,2 +1,5 @@\n line1\n+line2_added\n+line3_added\n+line4_added\n line5`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: alignmentDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view alignment\")\n\n  // Both sides should have same number of lines (with empty lines for alignment)\n  expect(frame).toContain(\"line1\")\n  expect(frame).toContain(\"line5\")\n  expect(frame).toContain(\"line2_added\")\n})\n\ntest(\"DiffRenderable - context lines shown on both sides in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiLineDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n\n  // Context lines should appear on both sides\n  expect(frame).toContain(\"function add\")\n  expect(frame).toContain(\"function multiply\")\n})\n\ntest(\"DiffRenderable - custom colors applied correctly\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    addedBg: \"#00ff00\",\n    removedBg: \"#ff0000\",\n    addedSignColor: \"#00ff00\",\n    removedSignColor: \"#ff0000\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Should not crash with custom colors\n  const frame = captureFrame()\n  expect(frame).toContain('console.log(\"Hello\")')\n})\n\ntest(\"DiffRenderable - line numbers hidden for empty alignment lines in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: addOnlyDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view with hidden line numbers for empty lines\")\n\n  // Right side should have line numbers for new lines\n  // Left side should have empty lines without line numbers\n})\n\ntest(\"DiffRenderable - stable rendering across multiple frames (no visual glitches)\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiLineDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n\n  // Render the initial frame\n  await renderOnce()\n\n  const frameAfterAutoRender = captureFrame()\n\n  // Now call renderOnce explicitly (this would be the second render)\n  await renderOnce()\n  const firstFrame = captureFrame()\n\n  // Render a third time\n  await renderOnce()\n  const secondFrame = captureFrame()\n\n  // BEHAVIORAL EXPECTATION: All frames should be identical\n  // If frames differ, it indicates a visual glitch (e.g., gutter width changing,\n  // content shifting, or partial rendering)\n  expect(frameAfterAutoRender).toBe(firstFrame)\n  expect(firstFrame).toBe(secondFrame)\n\n  // Verify all frames have complete content (not partial rendering)\n  expect(frameAfterAutoRender).toContain(\"function add\")\n  expect(frameAfterAutoRender).toContain(\"function subtract\")\n  expect(frameAfterAutoRender).toContain(\"function multiply\")\n\n  // Verify line numbers are present and properly aligned\n  // If gutter width is wrong, line numbers will be misaligned or cut off\n  const frameLines = frameAfterAutoRender.split(\"\\n\")\n  const linesWithLineNumbers = frameLines.filter((l) => l.match(/^\\s*\\d+\\s+/))\n\n  // Should have multiple lines with line numbers\n  expect(linesWithLineNumbers.length).toBeGreaterThan(5)\n\n  // All line number widths should be consistent (not change between renders)\n  // Extract just the line number part (before the sign)\n  const lineNumberWidths = linesWithLineNumbers\n    .map((line) => {\n      const match = line.match(/^(\\s*\\d+)\\s/)\n      return match ? match[1].length : -1\n    })\n    .filter((w) => w > 0)\n\n  // All line numbers should have the same width (indicating stable gutter)\n  const uniqueWidths = new Set(lineNumberWidths)\n  expect(uniqueWidths.size).toBe(1) // Gutter width should be consistent\n})\n\ntest(\"DiffRenderable - can be constructed without diff and set via setter\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  // Construct without diff\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Should render empty\n  let frame = captureFrame()\n  expect(frame.trim()).toBe(\"\")\n\n  // Now set diff via setter\n  diffRenderable.diff = simpleDiff\n  await renderOnce()\n\n  frame = captureFrame()\n  expect(frame).toContain(\"function hello\")\n  expect(frame).toContain('console.log(\"Hello\")')\n  expect(frame).toContain('console.log(\"Hello, World!\")')\n})\n\ntest(\"DiffRenderable - consistent left padding for line numbers > 9\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  // Create a diff with line numbers that go into double digits\n  const diffWith10PlusLines = `--- a/test.js\n+++ b/test.js\n@@ -8,7 +8,9 @@\n line8\n line9\n-line10_old\n+line10_new\n line11\n+line12_added\n+line13_added\n line14\n line15\n-line16_old\n+line16_new`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: diffWith10PlusLines,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"unified view with double-digit line numbers\")\n\n  const frameLines = frame.split(\"\\n\")\n\n  // Find lines in the output\n  // Line 8 (single digit) should have left padding (appears as \" 8 line8\")\n  const line8 = frameLines.find((l) => l.includes(\"line8\"))\n  expect(line8).toBeTruthy()\n  const line8Match = line8!.match(/^( +)8 /)\n  expect(line8Match).toBeTruthy()\n  expect(line8Match![1].length).toBeGreaterThanOrEqual(1) // At least 1 space of left padding\n\n  // Line 10 (double digit) should have left padding (appears as \" 10 line10\" or \" 11 line10\")\n  const line10 = frameLines.find((l) => l.includes(\"line10\"))\n  expect(line10).toBeTruthy()\n  const line10Match = line10!.match(/^( +)1[01] /)\n  expect(line10Match).toBeTruthy()\n  expect(line10Match![1].length).toBeGreaterThanOrEqual(1) // At least 1 space of left padding\n\n  // Line 16 (double digit) should have left padding\n  // Note: With correct line numbers, the removed line shows as 14 - and added shows as 16 +\n  const line16 = frameLines.find((l) => l.includes(\"line16\"))\n  expect(line16).toBeTruthy()\n  // Match either 14 - or 16 + (the correct line numbers after the fix)\n  const line16Match = line16!.match(/^( +)(14 -|16 \\+) /)\n  expect(line16Match).toBeTruthy()\n  expect(line16Match![1].length).toBeGreaterThanOrEqual(1) // At least 1 space of left padding\n})\n\ntest(\"DiffRenderable - line numbers are correct in unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  const frameLines = frame.split(\"\\n\")\n\n  // Line 2 is removed (old file line 2)\n  const removedLine = frameLines.find((l) => l.includes('console.log(\"Hello\");'))\n  expect(removedLine).toBeTruthy()\n  expect(removedLine).toMatch(/^ *2 -/)\n\n  // Line 2 is added (new file line 2) - NOT line 3!\n  const addedLine = frameLines.find((l) => l.includes('console.log(\"Hello, World!\")'))\n  expect(addedLine).toBeTruthy()\n  expect(addedLine).toMatch(/^ *2 \\+/)\n})\n\ntest(\"DiffRenderable - line numbers are correct in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  const frameLines = frame.split(\"\\n\")\n\n  // In split view, both sides are on the same terminal line\n  // Left side: line 2 is removed, Right side: line 2 is added\n  const splitLine = frameLines.find((l) => l.includes('console.log(\"Hello, World!\")'))\n  expect(splitLine).toBeTruthy()\n  // Should contain line 2 with - on left side\n  expect(splitLine).toMatch(/^ *2 -/)\n  // Should contain line 2 with + on right side (later in the same line)\n  expect(splitLine).toMatch(/2 \\+.*console\\.log\\(\"Hello, World!\"\\)/)\n})\n\ntest(\"DiffRenderable - split view should not wrap lines prematurely\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  // Create a diff with long lines that should fit in split view\n  const longLineDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,4 +1,4 @@\n class Calculator {\n-  subtract(a: number, b: number): number {\n+  subtract(a: number, b: number, c: number = 0): number {\n   return a - b;\n }`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: longLineDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"word\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  const frameLines = frame.split(\"\\n\")\n\n  // Find the line with \"subtract\" on the left side\n  const leftSubtractLine = frameLines.find((l) => l.includes(\"subtract\") && l.includes(\"b: number):\"))\n  expect(leftSubtractLine).toBeTruthy()\n\n  // The line should NOT be wrapped - \"subtract(a: number, b: number):\" should be on one line\n  // In an 80-char terminal with split view, each side gets ~40 chars (minus line numbers)\n  // \"subtract(a: number, b: number):\" is 34 chars, so it should fit without wrapping\n  expect(leftSubtractLine).toMatch(/subtract\\(a: number, b: number\\):/)\n\n  // Find the line with \"subtract\" on the right side - it might be on the same line or next line\n  // The signature is longer and might wrap\n  const rightSubtractLines = frameLines.filter((l) => l.includes(\"subtract\") || l.includes(\"c: number\"))\n  expect(rightSubtractLines.length).toBeGreaterThan(0)\n\n  // The key assertion is that the left side doesn't wrap prematurely\n  // We've already verified that above\n})\n\ntest(\"DiffRenderable - split view alignment with calculator diff\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const calculatorDiff = `--- a/calculator.ts\n+++ b/calculator.ts\n@@ -1,13 +1,20 @@\n class Calculator {\n   add(a: number, b: number): number {\n     return a + b;\n   }\n \n-  subtract(a: number, b: number): number {\n-    return a - b;\n+  subtract(a: number, b: number, c: number = 0): number {\n+    return a - b - c;\n   }\n \n   multiply(a: number, b: number): number {\n     return a * b;\n   }\n+\n+  divide(a: number, b: number): number {\n+    if (b === 0) {\n+      throw new Error(\"Division by zero\");\n+    }\n+    return a / b;\n+  }\n }`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: calculatorDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"none\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  const frameLines = frame.split(\"\\n\")\n\n  // Find the closing brace on the left (old line 13)\n  const leftClosingBrace = frameLines.find((l) => l.match(/^\\s*13\\s+\\}/))\n  expect(leftClosingBrace).toBeTruthy()\n\n  // Find the closing brace on the right (new line 20)\n  const rightClosingBrace = frameLines.find((l) => l.match(/\\s*20\\s+\\}/))\n  expect(rightClosingBrace).toBeTruthy()\n\n  // They should be on the SAME line in the output\n  expect(leftClosingBrace).toBe(rightClosingBrace)\n})\n\ntest(\"DiffRenderable - switching between unified and split views multiple times\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Step 1: Verify unified view works\n  let frame = captureFrame()\n  expect(frame).toContain(\"function hello\")\n  expect(frame).toContain('console.log(\"Hello\")')\n  expect(frame).toContain('console.log(\"Hello, World!\")')\n\n  // Step 2: Switch to split view\n  diffRenderable.view = \"split\"\n  await renderOnce()\n\n  frame = captureFrame()\n  expect(frame).toContain(\"function hello\")\n  expect(frame).toContain('console.log(\"Hello\")')\n  expect(frame).toContain('console.log(\"Hello, World!\")')\n\n  // Step 3: Switch back to unified view\n  diffRenderable.view = \"unified\"\n  await renderOnce()\n\n  frame = captureFrame()\n  expect(frame).toContain(\"function hello\")\n  expect(frame).toContain('console.log(\"Hello\")')\n  expect(frame).toContain('console.log(\"Hello, World!\")')\n\n  // Step 4: Switch to split view again (this currently fails)\n  diffRenderable.view = \"split\"\n  await renderOnce()\n\n  frame = captureFrame()\n  expect(frame).toContain(\"function hello\")\n  expect(frame).toContain('console.log(\"Hello\")')\n  expect(frame).toContain('console.log(\"Hello, World!\")')\n})\n\ntest(\"DiffRenderable - wrapMode works in unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  // Create a diff with a very long line that will wrap\n  const longLineDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,3 +1,3 @@\n function hello() {\n-  console.log(\"This is a very long line that should wrap when wrapMode is set to word but not when it is set to none\");\n+  console.log(\"This is a very long line that has been modified and should wrap when wrapMode is set to word but not when it is set to none\");\n }`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: longLineDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"none\",\n    width: 80,\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Capture with wrapMode: none\n  const frameNone = captureFrame()\n  expect(frameNone).toMatchSnapshot(\"wrapMode-none\")\n\n  // Change to wrapMode: word\n  diffRenderable.wrapMode = \"word\"\n  await renderOnce()\n\n  // Capture with wrapMode: word\n  const frameWord = captureFrame()\n  expect(frameWord).toMatchSnapshot(\"wrapMode-word\")\n\n  // Frames should be different (word wrapping should create more lines)\n  expect(frameNone).not.toBe(frameWord)\n\n  // Change back to wrapMode: none\n  diffRenderable.wrapMode = \"none\"\n  await renderOnce()\n\n  // Should match the original\n  const frameNoneAgain = captureFrame()\n  expect(frameNoneAgain).toMatchSnapshot(\"wrapMode-none\")\n  expect(frameNoneAgain).toBe(frameNone)\n})\n\ntest(\"DiffRenderable - split view with wrapMode honors wrapping alignment\", async () => {\n  // Create a larger test renderer to fit the whole diff with wrapping\n  const testRenderer = await createTestRenderer({ width: 80, height: 40 })\n  const renderer = testRenderer.renderer\n  const renderOnce = testRenderer.renderOnce\n  const captureFrame = testRenderer.captureCharFrame\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const calculatorDiff = `--- a/calculator.ts\n+++ b/calculator.ts\n@@ -1,13 +1,20 @@\n class Calculator {\n   add(a: number, b: number): number {\n     return a + b;\n   }\n \n-  subtract(a: number, b: number): number {\n-    return a - b;\n+  subtract(a: number, b: number, c: number = 0): number {\n+    return a - b - c;\n   }\n \n   multiply(a: number, b: number): number {\n     return a * b;\n   }\n+\n+  divide(a: number, b: number): number {\n+    if (b === 0) {\n+      throw new Error(\"Division by zero\");\n+    }\n+    return a / b;\n+  }\n }`\n\n  const diffRenderable = new DiffRenderable(renderer, {\n    id: \"test-diff\",\n    diff: calculatorDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"word\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  renderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Flush microtask-based deferred rebuild for wrap alignment\n  await Promise.resolve()\n  await renderOnce()\n\n  const frame = captureFrame()\n  const frameLines = frame.split(\"\\n\")\n\n  // Find the closing brace on the left (old line 13)\n  const leftClosingBraceLine = frameLines.find((l) => l.match(/^\\s*13\\s+\\}/))\n  expect(leftClosingBraceLine).toBeTruthy()\n\n  // Find the closing brace on the right (new line 20)\n  const rightClosingBraceLine = frameLines.find((l) => l.match(/\\s*20\\s+\\}/))\n  expect(rightClosingBraceLine).toBeTruthy()\n\n  // They should be on the SAME line in the output (same visual row)\n  // even though the right side has wrapped lines above it\n  expect(leftClosingBraceLine).toBe(rightClosingBraceLine)\n\n  // Both sides should have the same number of final visual lines\n  // (counting both logical lines and wrap continuations)\n  // This is hard to assert directly, but if alignment is correct,\n  // the closing braces being on the same line proves it worked\n\n  // Clean up\n  renderer.destroy()\n})\n\ntest(\"DiffRenderable - context lines show new line numbers in unified view\", async () => {\n  // Create a larger test renderer to fit the whole diff\n  const testRenderer = await createTestRenderer({ width: 80, height: 30 })\n  const renderer = testRenderer.renderer\n  const renderOnce = testRenderer.renderOnce\n  const captureFrame = testRenderer.captureCharFrame\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  // This diff adds lines in the middle, so context lines after additions\n  // should show their NEW line numbers, not old ones\n  const calculatorDiff = `--- a/calculator.ts\n+++ b/calculator.ts\n@@ -1,13 +1,20 @@\n class Calculator {\n   add(a: number, b: number): number {\n     return a + b;\n   }\n \n-  subtract(a: number, b: number): number {\n-    return a - b;\n+  subtract(a: number, b: number, c: number = 0): number {\n+    return a - b - c;\n   }\n \n   multiply(a: number, b: number): number {\n     return a * b;\n   }\n+\n+  divide(a: number, b: number): number {\n+    if (b === 0) {\n+      throw new Error(\"Division by zero\");\n+    }\n+    return a / b;\n+  }\n }`\n\n  const diffRenderable = new DiffRenderable(renderer, {\n    id: \"test-diff\",\n    diff: calculatorDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  renderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  const frameLines = frame.split(\"\\n\")\n\n  // The closing brace \"}\" for the Calculator class is a context line\n  // In the old file it was at line 13\n  // In the new file it's at line 20 (after adding 7 lines for divide method)\n  // Unified view should show line 20, not line 13\n  // Find the LAST closing brace that's just \"}\" (at the beginning of indentation, not nested)\n  // This regex matches: optional spaces, digits, spaces, optional sign (+/-), spaces, \"}\", trailing spaces\n  const closingBraceLines = frameLines.filter((l) => l.match(/^\\s*\\d+\\s+[+-]?\\s*\\}\\s*$/))\n\n  // The last one should be the class closing brace\n  const classClosingBraceLine = closingBraceLines[closingBraceLines.length - 1]\n  expect(classClosingBraceLine).toBeTruthy()\n\n  // Extract the line number from the closing brace line\n  const lineNumberMatch = classClosingBraceLine!.match(/^\\s*(\\d+)/)\n  expect(lineNumberMatch).toBeTruthy()\n\n  const lineNumber = parseInt(lineNumberMatch![1])\n\n  // The closing brace should show line 20 (new file position), not 13 (old file position)\n  expect(lineNumber).toBe(20)\n\n  // Clean up\n  renderer.destroy()\n})\n\ntest(\"DiffRenderable - multiple hunks in unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  // Diff with three separate hunks\n  const multiHunkDiff = `--- a/file.js\n+++ b/file.js\n@@ -1,3 +1,3 @@\n function first() {\n-  return 1;\n+  return \"one\";\n }\n@@ -15,4 +15,5 @@\n function second() {\n   var x = 10;\n+  var y = 20;\n   return x;\n }\n@@ -30,3 +31,3 @@\n function third() {\n-  console.log(\"old\");\n+  console.log(\"new\");\n }`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiHunkDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"unified view multiple hunks\")\n\n  // All three hunks should be present\n  expect(frame).toContain('return \"one\"')\n  expect(frame).toContain(\"var y = 20\")\n  expect(frame).toContain('console.log(\"new\")')\n\n  // Line numbers should be correct for each hunk\n  const frameLines = frame.split(\"\\n\")\n\n  // First hunk around line 2\n  const firstHunkLine = frameLines.find((l) => l.includes('return \"one\"'))\n  expect(firstHunkLine).toMatch(/2 \\+/)\n\n  // Second hunk around line 17 (added line)\n  const secondHunkLine = frameLines.find((l) => l.includes(\"var y = 20\"))\n  expect(secondHunkLine).toMatch(/17 \\+/)\n\n  // Third hunk around line 32\n  const thirdHunkLine = frameLines.find((l) => l.includes('console.log(\"new\")'))\n  expect(thirdHunkLine).toMatch(/32 \\+/)\n})\n\ntest(\"DiffRenderable - multiple hunks in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const multiHunkDiff = `--- a/file.js\n+++ b/file.js\n@@ -1,3 +1,3 @@\n function first() {\n-  return 1;\n+  return \"one\";\n }\n@@ -15,4 +15,5 @@\n function second() {\n   var x = 10;\n+  var y = 20;\n   return x;\n }\n@@ -30,3 +31,3 @@\n function third() {\n-  console.log(\"old\");\n+  console.log(\"new\");\n }`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiHunkDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view multiple hunks\")\n\n  // All three hunks should be present in split view\n  expect(frame).toContain('return \"one\"')\n  expect(frame).toContain(\"var y = 20\")\n  expect(frame).toContain('console.log(\"new\")')\n\n  // Both old and new content should be visible\n  expect(frame).toContain(\"return 1\")\n  expect(frame).toContain('console.log(\"old\")')\n})\n\ntest(\"DiffRenderable - no newline at end of file in unified view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const noNewlineDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,3 +1,3 @@\n line1\n line2\n-line3\n\\\\ No newline at end of file\n+line3_modified\n\\\\ No newline at end of file`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: noNewlineDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"unified view with no newline marker\")\n\n  // Should show both old and new versions\n  expect(frame).toContain(\"line3\")\n  expect(frame).toContain(\"line3_modified\")\n\n  // Should NOT show the \"No newline\" marker as content\n  // (it's a special marker that should be skipped)\n  const frameLines = frame.split(\"\\n\")\n  const markerLines = frameLines.filter((l) => l.includes(\"No newline at end of file\"))\n  expect(markerLines.length).toBe(0)\n})\n\ntest(\"DiffRenderable - no newline at end of file in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const noNewlineDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,3 +1,3 @@\n line1\n line2\n-line3\n\\\\ No newline at end of file\n+line3_modified\n\\\\ No newline at end of file`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: noNewlineDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view with no newline marker\")\n\n  // Both sides should show their respective versions\n  expect(frame).toContain(\"line3\")\n  expect(frame).toContain(\"line3_modified\")\n\n  // Should NOT show the \"No newline\" marker\n  const frameLines = frame.split(\"\\n\")\n  const markerLines = frameLines.filter((l) => l.includes(\"No newline at end of file\"))\n  expect(markerLines.length).toBe(0)\n})\n\ntest(\"DiffRenderable - asymmetric block with more removes than adds in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const asymmetricDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,7 +1,4 @@\n context_before\n-remove1\n-remove2\n-remove3\n-remove4\n-remove5\n+add1\n+add2\n context_after`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: asymmetricDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view asymmetric block more removes\")\n\n  // Left side should have all 5 removes\n  expect(frame).toContain(\"remove1\")\n  expect(frame).toContain(\"remove2\")\n  expect(frame).toContain(\"remove3\")\n  expect(frame).toContain(\"remove4\")\n  expect(frame).toContain(\"remove5\")\n\n  // Right side should have 2 adds\n  expect(frame).toContain(\"add1\")\n  expect(frame).toContain(\"add2\")\n\n  // Context lines should appear on both sides at the same visual position\n  const frameLines = frame.split(\"\\n\")\n  const contextBeforeLines = frameLines.filter((l) => l.includes(\"context_before\"))\n  const contextAfterLines = frameLines.filter((l) => l.includes(\"context_after\"))\n\n  // context_before should appear once (on same visual line for both sides)\n  expect(contextBeforeLines.length).toBeGreaterThanOrEqual(1)\n\n  // context_after should appear once (on same visual line for both sides)\n  expect(contextAfterLines.length).toBeGreaterThanOrEqual(1)\n\n  // The right side should have empty padding lines to align with left side's extra removes\n  // We can verify this by checking that context_after appears at similar vertical positions\n})\n\ntest(\"DiffRenderable - asymmetric block with more adds than removes in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const asymmetricDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,4 +1,7 @@\n context_before\n-remove1\n-remove2\n+add1\n+add2\n+add3\n+add4\n+add5\n context_after`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: asymmetricDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view asymmetric block more adds\")\n\n  // Left side should have 2 removes\n  expect(frame).toContain(\"remove1\")\n  expect(frame).toContain(\"remove2\")\n\n  // Right side should have all 5 adds\n  expect(frame).toContain(\"add1\")\n  expect(frame).toContain(\"add2\")\n  expect(frame).toContain(\"add3\")\n  expect(frame).toContain(\"add4\")\n  expect(frame).toContain(\"add5\")\n\n  // Context lines should be aligned\n  const frameLines = frame.split(\"\\n\")\n  const contextBeforeLines = frameLines.filter((l) => l.includes(\"context_before\"))\n  const contextAfterLines = frameLines.filter((l) => l.includes(\"context_after\"))\n\n  expect(contextBeforeLines.length).toBeGreaterThanOrEqual(1)\n  expect(contextAfterLines.length).toBeGreaterThanOrEqual(1)\n})\n\ntest(\"DiffRenderable - back-to-back change blocks without context lines in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const backToBackDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,4 +1,4 @@\n-remove1\n-remove2\n-remove3\n-remove4\n+add1\n+add2\n+add3\n+add4`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: backToBackDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view back-to-back blocks\")\n\n  // All removes should be on left\n  expect(frame).toContain(\"remove1\")\n  expect(frame).toContain(\"remove2\")\n  expect(frame).toContain(\"remove3\")\n  expect(frame).toContain(\"remove4\")\n\n  // All adds should be on right\n  expect(frame).toContain(\"add1\")\n  expect(frame).toContain(\"add2\")\n  expect(frame).toContain(\"add3\")\n  expect(frame).toContain(\"add4\")\n\n  // Both sides should have same number of visual lines (with alignment)\n  const frameLines = frame.split(\"\\n\").filter((l) => l.trim().length > 0)\n  expect(frameLines.length).toBeGreaterThan(0)\n})\n\ntest(\"DiffRenderable - very long lines wrapping multiple times in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const longLineDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,3 +1,3 @@\n short line\n-This is an extremely long line that will definitely wrap multiple times when rendered in a split view with word wrapping enabled because it contains so many words and characters\n+This is an extremely long line that has been modified and will definitely wrap multiple times when rendered in a split view with word wrapping enabled because it contains so many words and characters and even more content\n another short line`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: longLineDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"word\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Flush microtask-based wrap alignment\n  await Promise.resolve()\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"split view multi-wrap lines\")\n\n  // Both versions of the long line should be present\n  expect(frame).toContain(\"extremely long line\")\n  expect(frame).toContain(\"has been modified\")\n\n  // Short lines should still be aligned\n  expect(frame).toContain(\"short line\")\n  expect(frame).toContain(\"another short line\")\n\n  const frameLines = frame.split(\"\\n\")\n\n  // Find the \"another short line\" on both sides\n  const shortLineMatches = frameLines.filter((l) => l.includes(\"another short line\"))\n\n  // Should appear (on the same visual line in split view)\n  expect(shortLineMatches.length).toBeGreaterThanOrEqual(1)\n})\n\ntest(\"DiffRenderable - rapid diff updates trigger microtask coalescing\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"word\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Rapidly update the diff multiple times\n  diffRenderable.diff = multiLineDiff\n  diffRenderable.diff = addOnlyDiff\n  diffRenderable.diff = removeOnlyDiff\n  diffRenderable.diff = simpleDiff\n\n  // Flush microtask-based coalesced rebuild\n  await Promise.resolve()\n  await renderOnce()\n\n  const frame = captureFrame()\n\n  // Should show the final diff (simpleDiff)\n  expect(frame).toContain(\"function hello\")\n  expect(frame).toContain('console.log(\"Hello\")')\n  expect(frame).toContain('console.log(\"Hello, World!\")')\n\n  // Should NOT show content from intermediate diffs\n  expect(frame).not.toContain(\"subtract\")\n  expect(frame).not.toContain(\"newFunction\")\n  expect(frame).not.toContain(\"oldFunction\")\n})\n\ntest(\"DiffRenderable - explicit content background colors differ from gutter\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    addedBg: \"#1a4d1a\",\n    removedBg: \"#4d1a1a\",\n    addedContentBg: \"#2a5d2a\",\n    removedContentBg: \"#5d2a2a\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n\n  // Verify content is rendered\n  expect(frame).toContain(\"function hello\")\n  expect(frame).toContain('console.log(\"Hello\")')\n  expect(frame).toContain('console.log(\"Hello, World!\")')\n\n  // Verify properties are set correctly\n  expect(diffRenderable.addedBg).toEqual(RGBA.fromHex(\"#1a4d1a\"))\n  expect(diffRenderable.removedBg).toEqual(RGBA.fromHex(\"#4d1a1a\"))\n  expect(diffRenderable.addedContentBg).toEqual(RGBA.fromHex(\"#2a5d2a\"))\n  expect(diffRenderable.removedContentBg).toEqual(RGBA.fromHex(\"#5d2a2a\"))\n\n  // Test that we can update them\n  diffRenderable.addedContentBg = \"#3a6d3a\"\n  expect(diffRenderable.addedContentBg).toEqual(RGBA.fromHex(\"#3a6d3a\"))\n\n  await renderOnce()\n  const frame2 = captureFrame()\n\n  // Should still render correctly after update\n  expect(frame2).toContain(\"function hello\")\n})\n\ntest(\"DiffRenderable - malformed diff string handled gracefully\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const malformedDiff = `This is not a valid diff format\nJust some random text\nWithout proper headers`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: malformedDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n\n  // Should not crash when rendering malformed diff\n  await renderOnce()\n\n  const frame = captureFrame()\n\n  // Should render empty/blank since diff can't be parsed\n  // The important thing is it doesn't crash\n  expect(diffRenderable.diff).toBe(malformedDiff)\n})\n\ntest(\"DiffRenderable - invalid diff format shows error with raw diff\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  // This diff has a malformed hunk header that will cause parsePatch to throw\n  // The hunk header must have the format @@ -oldStart,oldLines +newStart,newLines @@\n  const invalidDiff = `--- a/test.js\n+++ b/test.js\n@@ -a,b +c,d @@\n function hello() {\n-  console.log(\"Hello\");\n+  console.log(\"Hello, World!\");\n }`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: invalidDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n\n  // Should not crash when rendering invalid diff\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"invalid diff format with error\")\n\n  // Should contain error message (the error from parsePatch)\n  expect(frame).toContain(\"Unknown line\")\n\n  // Should show the raw diff content\n  expect(frame).toContain(\"@@ -a,b +c,d @@\")\n  expect(frame).toContain(\"function hello\")\n})\n\ntest(\"DiffRenderable - diff with only context lines (no changes)\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const contextOnlyDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,5 +1,5 @@\n line1\n line2\n line3\n line4\n line5`\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: contextOnlyDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const frame = captureFrame()\n  expect(frame).toMatchSnapshot(\"diff with only context lines\")\n\n  // All lines should be present as context\n  expect(frame).toContain(\"line1\")\n  expect(frame).toContain(\"line2\")\n  expect(frame).toContain(\"line3\")\n  expect(frame).toContain(\"line4\")\n  expect(frame).toContain(\"line5\")\n\n  // No +/- signs should be present (only context)\n  const frameLines = frame.split(\"\\n\")\n  const changedLines = frameLines.filter((l) => l.match(/[+-]\\s*line/))\n  expect(changedLines.length).toBe(0)\n})\n\ntest(\"DiffRenderable - should not leak listeners on unified view updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Get the underlying CodeRenderable (leftCodeRenderable in unified view)\n  const codeRenderable = (diffRenderable as any).leftCodeRenderable\n  expect(codeRenderable).toBeDefined()\n\n  // Check initial listener count\n  const initialListenerCount = codeRenderable.listenerCount(\"line-info-change\")\n  expect(initialListenerCount).toBeGreaterThanOrEqual(1)\n\n  // Update the diff multiple times - this should not add more listeners\n  for (let i = 0; i < 10; i++) {\n    diffRenderable.diff = simpleDiff.replace('\"Hello\"', `\"Hello${i}\"`)\n    await renderOnce()\n  }\n\n  // Check that listener count hasn't grown\n  const finalListenerCount = codeRenderable.listenerCount(\"line-info-change\")\n  expect(finalListenerCount).toBe(initialListenerCount)\n})\n\ntest(\"DiffRenderable - should not leak listeners on split view updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Get the underlying CodeRenderables\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n  expect(leftCodeRenderable).toBeDefined()\n  expect(rightCodeRenderable).toBeDefined()\n\n  // Check initial listener counts\n  const leftInitialCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n  const rightInitialCount = rightCodeRenderable.listenerCount(\"line-info-change\")\n  expect(leftInitialCount).toBeGreaterThanOrEqual(1)\n  expect(rightInitialCount).toBeGreaterThanOrEqual(1)\n\n  // Update the diff multiple times - this should not add more listeners\n  for (let i = 0; i < 10; i++) {\n    diffRenderable.diff = simpleDiff.replace('\"Hello\"', `\"Hello${i}\"`)\n    await renderOnce()\n  }\n\n  // Check that listener counts haven't grown\n  const leftFinalCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n  const rightFinalCount = rightCodeRenderable.listenerCount(\"line-info-change\")\n  expect(leftFinalCount).toBe(leftInitialCount)\n  expect(rightFinalCount).toBe(rightInitialCount)\n})\n\ntest(\"DiffRenderable - should not leak listeners when switching views\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Get initial renderables\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  expect(leftCodeRenderable).toBeDefined()\n  const initialLeftCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n\n  // Switch to split view and back multiple times\n  for (let i = 0; i < 5; i++) {\n    diffRenderable.view = \"split\"\n    await renderOnce()\n\n    diffRenderable.view = \"unified\"\n    await renderOnce()\n  }\n\n  const finalLeftCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n\n  // Listener count should remain stable (allow some flexibility for implementation details)\n  expect(finalLeftCount).toBeLessThanOrEqual(initialLeftCount + 2)\n})\n\ntest(\"DiffRenderable - should not leak listeners on rapid property changes\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n  const leftInitialCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n  const rightInitialCount = rightCodeRenderable.listenerCount(\"line-info-change\")\n\n  // Make rapid changes that trigger rebuilds\n  for (let i = 0; i < 10; i++) {\n    diffRenderable.wrapMode = i % 2 === 0 ? \"word\" : \"char\"\n    diffRenderable.addedBg = i % 2 === 0 ? \"#ff0000\" : \"#00ff00\"\n    diffRenderable.removedBg = i % 2 === 0 ? \"#0000ff\" : \"#ffff00\"\n    await renderOnce()\n  }\n\n  const leftFinalCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n  const rightFinalCount = rightCodeRenderable.listenerCount(\"line-info-change\")\n\n  // Listener counts should remain stable\n  expect(leftFinalCount).toBe(leftInitialCount)\n  expect(rightFinalCount).toBe(rightInitialCount)\n})\n\ntest(\"DiffRenderable - can toggle conceal with markdown diff\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n\n  const markdownDiff = `--- a/test.md\n+++ b/test.md\n@@ -1,3 +1,3 @@\n First line\n-Some text **old**\n+Some text **boldtext** and *italic*\n End line`\n\n  const mockHighlightsWithConceal: SimpleHighlight[] = [\n    [21, 23, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }], // **\n    [31, 33, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }], // **\n    [38, 39, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }], // *\n    [45, 46, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }], // *\n  ]\n\n  mockClient.setMockResult({ highlights: mockHighlightsWithConceal })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: markdownDiff,\n    view: \"unified\",\n    syntaxStyle,\n    filetype: \"markdown\",\n    conceal: true,\n    treeSitterClient: mockClient,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await settleDiffHighlighting(diffRenderable, mockClient, renderOnce)\n\n  const frameWithConceal = captureFrame()\n  expect(frameWithConceal).toMatchSnapshot(\"markdown diff with conceal enabled\")\n  expect(diffRenderable.conceal).toBe(true)\n\n  diffRenderable.conceal = false\n  await settleDiffHighlighting(diffRenderable, mockClient, renderOnce)\n\n  const frameWithoutConceal = captureFrame()\n  expect(frameWithoutConceal).toMatchSnapshot(\"markdown diff with conceal disabled\")\n  expect(diffRenderable.conceal).toBe(false)\n\n  expect(frameWithConceal).not.toBe(frameWithoutConceal)\n\n  diffRenderable.conceal = true\n  await settleDiffHighlighting(diffRenderable, mockClient, renderOnce)\n\n  const frameWithConcealAgain = captureFrame()\n  expect(frameWithConcealAgain).toBe(frameWithConceal)\n})\n\ntest(\"DiffRenderable - conceal works in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const mockClient = new MockTreeSitterClient()\n\n  const markdownDiff = `--- a/test.md\n+++ b/test.md\n@@ -1,3 +1,3 @@\n First line\n-Some **old** text\n+Some **new** text\n End line`\n\n  const mockHighlightsWithConceal: SimpleHighlight[] = [\n    [16, 18, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }], // **\n    [21, 23, \"conceal\", { isInjection: true, injectionLang: \"markdown_inline\", conceal: \"\" }], // **\n  ]\n\n  mockClient.setMockResult({ highlights: mockHighlightsWithConceal })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: markdownDiff,\n    view: \"split\",\n    syntaxStyle,\n    filetype: \"markdown\",\n    conceal: true,\n    treeSitterClient: mockClient,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await settleDiffHighlighting(diffRenderable, mockClient, renderOnce)\n\n  const frameWithConceal = captureFrame()\n  expect(frameWithConceal).toMatchSnapshot(\"split view markdown diff with conceal enabled\")\n  expect(diffRenderable.conceal).toBe(true)\n\n  diffRenderable.conceal = false\n  await settleDiffHighlighting(diffRenderable, mockClient, renderOnce)\n\n  const frameWithoutConceal = captureFrame()\n  expect(frameWithoutConceal).toMatchSnapshot(\"split view markdown diff with conceal disabled\")\n  expect(diffRenderable.conceal).toBe(false)\n\n  expect(frameWithConceal).not.toBe(frameWithoutConceal)\n})\n\ntest(\"DiffRenderable - conceal defaults to false when not specified\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    filetype: \"javascript\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.conceal).toBe(false)\n})\n\ntest(\"DiffRenderable - should handle resize with wrapping without leaking listeners\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    wrapMode: \"word\",\n    width: 100,\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n  const leftInitialCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n  const rightInitialCount = rightCodeRenderable.listenerCount(\"line-info-change\")\n\n  // Simulate multiple resizes (which trigger rebuilds in split view with wrapping)\n  for (let i = 0; i < 10; i++) {\n    diffRenderable.width = 50 + i * 5\n    await renderOnce()\n    // Flush microtask rebuild\n    await Promise.resolve()\n    await renderOnce()\n  }\n\n  const leftFinalCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n  const rightFinalCount = rightCodeRenderable.listenerCount(\"line-info-change\")\n\n  expect(leftFinalCount).toBe(leftInitialCount)\n  expect(rightFinalCount).toBe(rightInitialCount)\n})\n\ntest(\"DiffRenderable - gutter configuration updates work correctly\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const leftSide = (diffRenderable as any).leftSide\n\n  // Verify initial state\n  expect(leftSide).toBeDefined()\n  expect(leftCodeRenderable).toBeDefined()\n  const initialListenerCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n\n  // Get initial frame to verify line numbers are showing\n  let frame = captureFrame()\n  expect(frame).toContain(\"function hello\")\n\n  // Update multiple gutter configurations that trigger recreateGutter()\n  // Each of these calls setLineNumbers/setHideLineNumbers internally\n  for (let i = 0; i < 5; i++) {\n    diffRenderable.diff = simpleDiff.replace('\"Hello\"', `\"Hello${i}\"`)\n    await renderOnce()\n  }\n\n  // Verify listener count is stable\n  const finalListenerCount = leftCodeRenderable.listenerCount(\"line-info-change\")\n  expect(finalListenerCount).toBe(initialListenerCount)\n\n  // Verify rendering still works\n  frame = captureFrame()\n  expect(frame).toContain(\"function hello\")\n  expect(frame).toContain(\"Hello4\") // Last update should be visible\n})\n\ntest(\"DiffRenderable - target remains functional after multiple updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiLineDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n\n  // Verify targets are responding to line-info-change events\n  let leftEventFired = false\n  let rightEventFired = false\n\n  const leftListener = () => {\n    leftEventFired = true\n  }\n  const rightListener = () => {\n    rightEventFired = true\n  }\n\n  leftCodeRenderable.on(\"line-info-change\", leftListener)\n  rightCodeRenderable.on(\"line-info-change\", rightListener)\n\n  // Update diff multiple times\n  for (let i = 0; i < 5; i++) {\n    leftEventFired = false\n    rightEventFired = false\n\n    diffRenderable.diff = multiLineDiff.replace(\"add(a, b)\", `add(a, b, ${i})`)\n    await renderOnce()\n\n    // Events should have fired during the update\n    expect(leftEventFired).toBe(true)\n    expect(rightEventFired).toBe(true)\n  }\n\n  leftCodeRenderable.off(\"line-info-change\", leftListener)\n  rightCodeRenderable.off(\"line-info-change\", rightListener)\n})\n\ntest(\"DiffRenderable - split view scroll is not synchronized by default\", async () => {\n  const mockMouse = createMockMouse(currentRenderer)\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiLineDiff,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: 4,\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n\n  expect(leftCodeRenderable).toBeTruthy()\n  expect(rightCodeRenderable).toBeTruthy()\n\n  // Scroll over left pane\n  mockMouse.scroll(leftCodeRenderable.x, leftCodeRenderable.y + 1, \"down\")\n  await renderOnce()\n\n  expect(leftCodeRenderable.scrollY).toBe(1)\n  expect(rightCodeRenderable.scrollY).toBe(0)\n\n  // Scroll over right pane\n  mockMouse.scroll(rightCodeRenderable.x + 1, rightCodeRenderable.y + 1, \"down\")\n  await renderOnce()\n\n  expect(rightCodeRenderable.scrollY).toBe(1)\n  expect(leftCodeRenderable.scrollY).toBe(1)\n})\n\ntest(\"DiffRenderable - split view wheel scroll keeps panes synchronized\", async () => {\n  const mockMouse = createMockMouse(currentRenderer)\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiLineDiff,\n    syncScroll: true,\n    view: \"split\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: 4,\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n\n  expect(leftCodeRenderable).toBeTruthy()\n  expect(rightCodeRenderable).toBeTruthy()\n\n  // Scroll over left pane\n  await mockMouse.scroll(leftCodeRenderable.x + 1, leftCodeRenderable.y + 1, \"down\")\n  await renderOnce()\n\n  expect(leftCodeRenderable.scrollY).toBeGreaterThan(0)\n  expect(leftCodeRenderable.scrollY).toBe(rightCodeRenderable.scrollY)\n\n  // Scroll over right pane\n  await mockMouse.scroll(rightCodeRenderable.x + 1, rightCodeRenderable.y + 1, \"down\")\n  await renderOnce()\n\n  expect(rightCodeRenderable.scrollY).toBeGreaterThan(0)\n  expect(leftCodeRenderable.scrollY).toBe(rightCodeRenderable.scrollY)\n})\n\ntest(\"DiffRenderable - gutter remains in correct position after updates\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  // Initial frame should have line numbers on the left\n  let frame = captureFrame()\n  const lines = frame.split(\"\\n\")\n\n  // Find a line with content\n  const contentLine = lines.find((l) => l.includes(\"function hello\"))\n  expect(contentLine).toBeDefined()\n\n  // Line number should be at the start (before the content)\n  expect(contentLine).toMatch(/^\\s*\\d+/)\n\n  // Update diff multiple times\n  for (let i = 0; i < 5; i++) {\n    diffRenderable.diff = simpleDiff.replace('\"Hello\"', `\"Hello${i}\"`)\n    await renderOnce()\n\n    frame = captureFrame()\n    const updatedLines = frame.split(\"\\n\")\n    const updatedContentLine = updatedLines.find((l) => l.includes(\"function hello\"))\n\n    // Line numbers should still be at the start\n    expect(updatedContentLine).toBeDefined()\n    expect(updatedContentLine).toMatch(/^\\s*\\d+/)\n  }\n})\n\ntest(\"DiffRenderable - properly cleans up listeners on destroy\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n\n  // Update multiple times to potentially create leaks\n  for (let i = 0; i < 5; i++) {\n    diffRenderable.diff = simpleDiff.replace('\"Hello\"', `\"Hello${i}\"`)\n    await renderOnce()\n  }\n\n  const leftCountBeforeDestroy = leftCodeRenderable.listenerCount(\"line-info-change\")\n  const rightCountBeforeDestroy = rightCodeRenderable.listenerCount(\"line-info-change\")\n\n  // Verify listeners exist\n  expect(leftCountBeforeDestroy).toBeGreaterThan(0)\n  expect(rightCountBeforeDestroy).toBeGreaterThan(0)\n\n  // Destroy the diff\n  diffRenderable.destroyRecursively()\n\n  // The LineNumberRenderables should have been destroyed\n  // Check that they're either null or destroyed\n  const leftSide = (diffRenderable as any).leftSide\n  const rightSide = (diffRenderable as any).rightSide\n\n  if (leftSide) {\n    expect(leftSide.isDestroyed).toBe(true)\n  }\n  if (rightSide) {\n    expect(rightSide.isDestroyed).toBe(true)\n  }\n})\n\ntest(\"DiffRenderable - line numbers update correctly after resize causes wrapping changes\", async () => {\n  const testRenderer = await createTestRenderer({ width: 120, height: 40 })\n  const renderer = testRenderer.renderer\n  const renderOnce = testRenderer.renderOnce\n  const captureFrame = testRenderer.captureCharFrame\n  const resize = testRenderer.resize\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const longLineDiff = `--- a/test.js\n+++ b/test.js\n@@ -1,4 +1,4 @@\n function calculateSomethingVeryComplexWithALongFunctionNameThatWillWrap() {\n-  const oldResultWithAVeryLongVariableNameThatWillDefinitelyWrapWhenRenderedInASmallerTerminal = 42;\n+  const newResultWithAVeryLongVariableNameThatWillDefinitelyWrapWhenRenderedInASmallerTerminal = 100;\n   return result;\n }`\n\n  const diffRenderable = new DiffRenderable(renderer, {\n    id: \"test-diff\",\n    diff: longLineDiff,\n    view: \"unified\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"word\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  renderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n\n  let lineInfoChangeEmitted = false\n  const lineInfoChangeListener = () => {\n    lineInfoChangeEmitted = true\n  }\n  leftCodeRenderable.on(\"line-info-change\", lineInfoChangeListener)\n\n  const frameBefore = captureFrame()\n  expect(frameBefore).toMatchSnapshot(\"before resize - line numbers with no wrapping\")\n\n  const lineInfoBefore = leftCodeRenderable.lineInfo\n  expect(lineInfoBefore.lineSources).toEqual([0, 1, 2, 3, 4])\n  expect(leftCodeRenderable.virtualLineCount).toBe(5)\n\n  lineInfoChangeEmitted = false\n\n  resize(60, 40)\n\n  await Promise.resolve()\n  await renderOnce()\n\n  expect(lineInfoChangeEmitted).toBe(true)\n  expect(leftCodeRenderable.virtualLineCount).toBe(11)\n\n  await Promise.resolve()\n  await renderOnce()\n\n  const frameAfter = captureFrame()\n  expect(frameAfter).toMatchSnapshot(\"after resize - line numbers with wrapping\")\n\n  const lineInfoAfter = leftCodeRenderable.lineInfo\n  expect(lineInfoAfter.lineSources).toEqual([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 4])\n\n  const linesAfter = frameAfter.split(\"\\n\").filter((l) => l.trim().length > 0)\n\n  const lineNumberMatches = linesAfter\n    .map((line, idx) => {\n      const match = line.match(/^\\s*(\\d+)\\s+([+-]?)/)\n      if (match) {\n        return { lineIdx: idx, lineNum: parseInt(match[1]), sign: match[2], content: line }\n      }\n      return null\n    })\n    .filter((m) => m !== null)\n\n  expect(lineNumberMatches.length).toBe(5)\n\n  expect(lineNumberMatches[0]!.lineNum).toBe(1)\n  expect(lineNumberMatches[1]!.lineNum).toBe(2)\n  expect(lineNumberMatches[1]!.sign).toBe(\"-\")\n  expect(lineNumberMatches[2]!.lineNum).toBe(2)\n  expect(lineNumberMatches[2]!.sign).toBe(\"+\")\n  expect(lineNumberMatches[3]!.lineNum).toBe(3)\n  expect(lineNumberMatches[4]!.lineNum).toBe(4)\n\n  leftCodeRenderable.off(\"line-info-change\", lineInfoChangeListener)\n  renderer.destroy()\n})\n\ntest(\"DiffRenderable - fg prop is passed to CodeRenderable on construction\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n  const customFg = \"#000000\"\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    fg: customFg,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.fg).toEqual(RGBA.fromHex(customFg))\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  expect(leftCodeRenderable).toBeDefined()\n  expect(leftCodeRenderable.fg).toEqual(RGBA.fromHex(customFg))\n})\n\ntest(\"DiffRenderable - fg prop can be updated via setter\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n  const initialFg = \"#000000\"\n  const updatedFg = \"#333333\"\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    fg: initialFg,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  diffRenderable.fg = updatedFg\n  await renderOnce()\n\n  expect(diffRenderable.fg).toEqual(RGBA.fromHex(updatedFg))\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  expect(leftCodeRenderable.fg).toEqual(RGBA.fromHex(updatedFg))\n})\n\ntest(\"DiffRenderable - fg prop is passed to both CodeRenderables in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n  const customFg = \"#222222\"\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    fg: customFg,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.fg).toEqual(RGBA.fromHex(customFg))\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n\n  expect(leftCodeRenderable).toBeDefined()\n  expect(rightCodeRenderable).toBeDefined()\n  expect(leftCodeRenderable.fg).toEqual(RGBA.fromHex(customFg))\n  expect(rightCodeRenderable.fg).toEqual(RGBA.fromHex(customFg))\n})\n\ntest(\"DiffRenderable - fg prop updates both CodeRenderables in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n  const initialFg = \"#111111\"\n  const updatedFg = \"#444444\"\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n    fg: initialFg,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  const rightCodeRenderable = (diffRenderable as any).rightCodeRenderable\n\n  diffRenderable.fg = updatedFg\n  await renderOnce()\n\n  expect(diffRenderable.fg).toEqual(RGBA.fromHex(updatedFg))\n  expect(leftCodeRenderable.fg).toEqual(RGBA.fromHex(updatedFg))\n  expect(rightCodeRenderable.fg).toEqual(RGBA.fromHex(updatedFg))\n})\n\ntest(\"DiffRenderable - fg prop defaults to undefined when not specified\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.fg).toBeUndefined()\n})\n\ntest(\"DiffRenderable - fg prop can be set to undefined to clear it\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n  const initialFg = \"#000000\"\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    fg: initialFg,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.fg).toEqual(RGBA.fromHex(initialFg))\n\n  diffRenderable.fg = undefined\n  await renderOnce()\n\n  expect(diffRenderable.fg).toBeUndefined()\n})\n\ntest(\"DiffRenderable - fg prop accepts RGBA directly\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n  const customFg = RGBA.fromValues(0.2, 0.2, 0.2, 1)\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n    fg: customFg,\n    width: \"100%\",\n    height: \"100%\",\n  })\n\n  currentRenderer.root.add(diffRenderable)\n  await renderOnce()\n\n  expect(diffRenderable.fg).toEqual(customFg)\n\n  const leftCodeRenderable = (diffRenderable as any).leftCodeRenderable\n  expect(leftCodeRenderable.fg).toEqual(customFg)\n})\n\ntest(\"DiffRenderable - split view with word wrapping: changing diff content should not misalign sides\", async () => {\n  const { BoxRenderable } = await import(\"./Box\")\n  const { parseColor } = await import(\"../lib/RGBA\")\n\n  // Use terminal width that matches the demo (~116 chars)\n  const testRenderer = await createTestRenderer({ width: 116, height: 30 })\n  const renderer = testRenderer.renderer\n  const captureFrame = testRenderer.captureCharFrame\n\n  // GitHub Dark theme - EXACTLY as in diff-demo.ts\n  const theme = {\n    backgroundColor: \"#0D1117\",\n    addedBg: \"#1a4d1a\",\n    removedBg: \"#4d1a1a\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#22c55e\",\n    removedSignColor: \"#ef4444\",\n    lineNumberFg: \"#6b7280\",\n    lineNumberBg: \"#161b22\",\n    addedLineNumberBg: \"#0d3a0d\",\n    removedLineNumberBg: \"#3a0d0d\",\n    selectionBg: \"#264F78\",\n    selectionFg: \"#FFFFFF\",\n  }\n\n  // Syntax style EXACTLY as in diff-demo.ts GitHub Dark theme\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    keyword: { fg: parseColor(\"#FF7B72\"), bold: true },\n    \"keyword.import\": { fg: parseColor(\"#FF7B72\"), bold: true },\n    string: { fg: parseColor(\"#A5D6FF\") },\n    comment: { fg: parseColor(\"#8B949E\"), italic: true },\n    number: { fg: parseColor(\"#79C0FF\") },\n    boolean: { fg: parseColor(\"#79C0FF\") },\n    constant: { fg: parseColor(\"#79C0FF\") },\n    function: { fg: parseColor(\"#D2A8FF\") },\n    \"function.call\": { fg: parseColor(\"#D2A8FF\") },\n    constructor: { fg: parseColor(\"#FFA657\") },\n    type: { fg: parseColor(\"#FFA657\") },\n    operator: { fg: parseColor(\"#FF7B72\") },\n    variable: { fg: parseColor(\"#E6EDF3\") },\n    property: { fg: parseColor(\"#79C0FF\") },\n    bracket: { fg: parseColor(\"#F0F6FC\") },\n    punctuation: { fg: parseColor(\"#F0F6FC\") },\n    default: { fg: parseColor(\"#E6EDF3\") },\n  })\n\n  // contentExamples[0] - TypeScript Calculator diff\n  const calculatorDiff = `--- a/calculator.ts\n+++ b/calculator.ts\n@@ -1,13 +1,20 @@\n class Calculator {\n   add(a: number, b: number): number {\n     return a + b;\n   }\n \n-  subtract(a: number, b: number): number {\n-    return a - b;\n+  subtract(a: number, b: number, c: number = 0): number {\n+    return a - b - c;\n   }\n \n   multiply(a: number, b: number): number {\n     return a * b;\n   }\n+\n+  divide(a: number, b: number): number {\n+    if (b === 0) {\n+      throw new Error(\"Division by zero\");\n+    }\n+    return a / b;\n+  }\n }`\n\n  // contentExamples[1] - Real Session: Text Demo\n  const textDemoDiff = `Index: packages/core/src/examples/index.ts\n===================================================================\n--- packages/core/src/examples/index.ts\tbefore\n+++ packages/core/src/examples/index.ts\tafter\n@@ -56,6 +56,7 @@\n import * as terminalDemo from \"./terminal\"\n import * as diffDemo from \"./diff-demo\"\n import * as keypressDebugDemo from \"./keypress-debug-demo\"\n+import * as textTruncationDemo from \"./text-truncation-demo\"\n import { setupCommonDemoKeys } from \"./lib/standalone-keys\"\n \n interface Example {\n@@ -85,6 +86,12 @@\n     destroy: textSelectionExample.destroy,\n   },\n   {\n+    name: \"Text Truncation Demo\",\n+    description: \"Middle truncation with ellipsis - toggle with 'T' key and resize to test responsive behavior\",\n+    run: textTruncationDemo.run,\n+    destroy: textTruncationDemo.destroy,\n+  },\n+  {\n     name: \"ASCII Font Selection Demo\",\n     description: \"Text selection with ASCII fonts - precise character-level selection across different font types\",\n     run: asciiFontSelectionExample.run,`\n\n  renderer.setBackgroundColor(theme.backgroundColor)\n\n  // PART 1: CORRECT PATH\n  // Start with textDemoDiff, view=\"unified\", wrapMode=\"none\"\n  // Then toggle to split, then toggle to word wrap\n  // This produces CORRECT alignment\n  const parentContainer1 = new BoxRenderable(renderer, {\n    id: \"parent-container-1\",\n    padding: 1,\n  })\n  renderer.root.add(parentContainer1)\n\n  const correctDiff = new DiffRenderable(renderer, {\n    id: \"correct-diff\",\n    diff: textDemoDiff, // Start with textDemoDiff directly\n    view: \"unified\",\n    filetype: \"typescript\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"none\",\n    conceal: true,\n    addedBg: theme.addedBg,\n    removedBg: theme.removedBg,\n    contextBg: theme.contextBg,\n    addedSignColor: theme.addedSignColor,\n    removedSignColor: theme.removedSignColor,\n    lineNumberFg: theme.lineNumberFg,\n    lineNumberBg: theme.lineNumberBg,\n    addedLineNumberBg: theme.addedLineNumberBg,\n    removedLineNumberBg: theme.removedLineNumberBg,\n    selectionBg: theme.selectionBg,\n    selectionFg: theme.selectionFg,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  parentContainer1.add(correctDiff)\n  await renderOnce()\n\n  // Press V - toggle to split view\n  correctDiff.view = \"split\"\n  await Promise.resolve()\n  await renderOnce()\n\n  // Press W - toggle to word wrap\n  correctDiff.wrapMode = \"word\"\n  await Promise.resolve()\n  await renderOnce()\n  await Promise.resolve()\n  await renderOnce()\n\n  const correctFrame = captureFrame()\n\n  // Clean up\n  parentContainer1.destroyRecursively()\n  renderer.root.remove(\"parent-container-1\")\n  await renderOnce()\n\n  // PART 2: BUGGY PATH\n  // Start with calculatorDiff, view=\"unified\", wrapMode=\"none\"\n  // Press V (split), Press W (word), Press C (change to textDemoDiff)\n  // This produces WRONG alignment due to stale lineInfo\n  const parentContainer2 = new BoxRenderable(renderer, {\n    id: \"parent-container-2\",\n    padding: 1,\n  })\n  renderer.root.add(parentContainer2)\n\n  const buggyDiff = new DiffRenderable(renderer, {\n    id: \"buggy-diff\",\n    diff: calculatorDiff, // Start with calculatorDiff (contentExamples[0])\n    view: \"unified\",\n    filetype: \"typescript\",\n    syntaxStyle,\n    showLineNumbers: true,\n    wrapMode: \"none\",\n    conceal: true,\n    addedBg: theme.addedBg,\n    removedBg: theme.removedBg,\n    contextBg: theme.contextBg,\n    addedSignColor: theme.addedSignColor,\n    removedSignColor: theme.removedSignColor,\n    lineNumberFg: theme.lineNumberFg,\n    lineNumberBg: theme.lineNumberBg,\n    addedLineNumberBg: theme.addedLineNumberBg,\n    removedLineNumberBg: theme.removedLineNumberBg,\n    selectionBg: theme.selectionBg,\n    selectionFg: theme.selectionFg,\n    flexGrow: 1,\n    flexShrink: 1,\n  })\n\n  parentContainer2.add(buggyDiff)\n  await renderOnce()\n\n  // Press V - toggle to split view\n  buggyDiff.view = \"split\"\n  await Promise.resolve()\n  await renderOnce()\n\n  // Press W - toggle to word wrap\n  buggyDiff.wrapMode = \"word\"\n  await Promise.resolve()\n  await renderOnce()\n\n  // Press C - change diff content to textDemoDiff\n  // THIS IS WHERE THE BUG MANIFESTS - lineInfo is STALE\n  buggyDiff.diff = textDemoDiff\n  buggyDiff.filetype = \"typescript\"\n  await Promise.resolve()\n  await renderOnce()\n  await Promise.resolve()\n  await renderOnce()\n\n  const buggyFrame = captureFrame()\n\n  // Clean up\n  renderer.destroy()\n\n  // ASSERTION: Both frames should be identical since they show the same diff content\n  // with the same view settings (split + word wrap)\n  // But due to the bug, the buggy frame has misaligned left/right sides because\n  // the lineInfo from CodeRenderable is STALE after changing diff content\n  expect(buggyFrame).toBe(correctFrame)\n})\n\ntest(\"DiffRenderable - setLineColor applies color to line\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n  })\n\n  diffRenderable.setLineColor(0, \"#ff0000\")\n  diffRenderable.setLineColor(1, { gutter: \"#00ff00\", content: \"#0000ff\" })\n  diffRenderable.clearLineColor(0)\n  diffRenderable.clearLineColor(1)\n})\n\ntest(\"DiffRenderable - highlightLines applies color to range\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: multiLineDiff,\n    view: \"unified\",\n    syntaxStyle,\n  })\n\n  diffRenderable.highlightLines(0, 3, \"#ff0000\")\n  diffRenderable.clearHighlightLines(0, 3)\n})\n\ntest(\"DiffRenderable - setLineColors and clearAllLineColors\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"unified\",\n    syntaxStyle,\n  })\n\n  const lineColors = new Map<number, string>()\n  lineColors.set(0, \"#ff0000\")\n  lineColors.set(1, \"#00ff00\")\n  lineColors.set(2, \"#0000ff\")\n\n  diffRenderable.setLineColors(lineColors)\n  diffRenderable.clearAllLineColors()\n})\n\ntest(\"DiffRenderable - line highlighting works in split view\", async () => {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n  })\n\n  const diffRenderable = new DiffRenderable(currentRenderer, {\n    id: \"test-diff\",\n    diff: simpleDiff,\n    view: \"split\",\n    syntaxStyle,\n  })\n\n  diffRenderable.setLineColor(0, \"#ff0000\")\n  diffRenderable.highlightLines(0, 2, \"#00ff00\")\n  diffRenderable.clearHighlightLines(0, 2)\n  diffRenderable.clearAllLineColors()\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Diff.ts",
    "content": "import { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport type { RenderContext } from \"../types.js\"\nimport { CodeRenderable, type CodeOptions } from \"./Code.js\"\nimport { LineNumberRenderable, type LineSign, type LineColorConfig } from \"./LineNumberRenderable.js\"\nimport { RGBA, parseColor } from \"../lib/RGBA.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\nimport { parsePatch, type StructuredPatch } from \"diff\"\nimport { TextRenderable } from \"./Text.js\"\nimport type { TreeSitterClient } from \"../lib/tree-sitter/index.js\"\nimport type { MouseEvent } from \"../renderer.js\"\n\ninterface LogicalLine {\n  content: string\n  lineNum?: number\n  hideLineNumber?: boolean\n  color?: string | RGBA\n  sign?: LineSign\n  type: \"context\" | \"add\" | \"remove\" | \"empty\"\n}\n\nexport interface DiffRenderableOptions extends RenderableOptions<DiffRenderable> {\n  diff?: string\n  syncScroll?: boolean\n  view?: \"unified\" | \"split\"\n\n  // CodeRenderable options\n  fg?: string | RGBA\n  filetype?: string\n  syntaxStyle?: SyntaxStyle\n  wrapMode?: \"word\" | \"char\" | \"none\"\n  conceal?: boolean\n  selectionBg?: string | RGBA\n  selectionFg?: string | RGBA\n  treeSitterClient?: TreeSitterClient\n\n  // LineNumberRenderable options\n  showLineNumbers?: boolean\n  lineNumberFg?: string | RGBA\n  lineNumberBg?: string | RGBA\n\n  // Diff styling\n  addedBg?: string | RGBA\n  removedBg?: string | RGBA\n  contextBg?: string | RGBA\n  addedContentBg?: string | RGBA\n  removedContentBg?: string | RGBA\n  contextContentBg?: string | RGBA\n  addedSignColor?: string | RGBA\n  removedSignColor?: string | RGBA\n  addedLineNumberBg?: string | RGBA\n  removedLineNumberBg?: string | RGBA\n}\n\nexport class DiffRenderable extends Renderable {\n  private _diff: string\n  private _syncScroll: boolean = false\n  private _view: \"unified\" | \"split\"\n  private _parsedDiff: StructuredPatch | null = null\n  private _parseError: Error | null = null\n\n  // CodeRenderable options\n  private _fg?: RGBA\n  private _filetype?: string\n  private _syntaxStyle?: SyntaxStyle\n  private _wrapMode?: \"word\" | \"char\" | \"none\"\n  private _conceal: boolean\n  private _selectionBg?: RGBA\n  private _selectionFg?: RGBA\n  private _treeSitterClient?: TreeSitterClient\n\n  // LineNumberRenderable options\n  private _showLineNumbers: boolean\n  private _lineNumberFg: RGBA\n  private _lineNumberBg: RGBA\n\n  // Diff styling\n  private _addedBg: RGBA\n  private _removedBg: RGBA\n  private _contextBg: RGBA\n  private _addedContentBg: RGBA | null\n  private _removedContentBg: RGBA | null\n  private _contextContentBg: RGBA | null\n  private _addedSignColor: RGBA\n  private _removedSignColor: RGBA\n  private _addedLineNumberBg: RGBA\n  private _removedLineNumberBg: RGBA\n\n  private leftSide: LineNumberRenderable | null = null\n  private rightSide: LineNumberRenderable | null = null\n\n  private leftSideAdded: boolean = false\n  private rightSideAdded: boolean = false\n\n  private leftCodeRenderable: CodeRenderable | null = null\n  private rightCodeRenderable: CodeRenderable | null = null\n\n  private pendingRebuild: boolean = false\n  private _lastWidth: number = 0\n\n  private errorTextRenderable: TextRenderable | null = null\n  private errorCodeRenderable: CodeRenderable | null = null\n\n  private _waitingForHighlight: boolean = false\n  private _lineInfoChangeHandler: (() => void) | null = null\n\n  constructor(ctx: RenderContext, options: DiffRenderableOptions) {\n    super(ctx, {\n      ...options,\n      flexDirection: options.view === \"split\" ? \"row\" : \"column\",\n    })\n\n    this._diff = options.diff ?? \"\"\n    this._syncScroll = options.syncScroll ?? false\n    this._view = options.view ?? \"unified\"\n\n    // CodeRenderable options\n    this._fg = options.fg ? parseColor(options.fg) : undefined\n    this._filetype = options.filetype\n    this._syntaxStyle = options.syntaxStyle\n    this._wrapMode = options.wrapMode\n    this._conceal = options.conceal ?? false\n    this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined\n    this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined\n    this._treeSitterClient = options.treeSitterClient\n\n    // LineNumberRenderable options\n    this._showLineNumbers = options.showLineNumbers ?? true\n    this._lineNumberFg = parseColor(options.lineNumberFg ?? \"#888888\")\n    this._lineNumberBg = parseColor(options.lineNumberBg ?? \"transparent\")\n\n    // Diff styling\n    this._addedBg = parseColor(options.addedBg ?? \"#1a4d1a\")\n    this._removedBg = parseColor(options.removedBg ?? \"#4d1a1a\")\n    this._contextBg = parseColor(options.contextBg ?? \"transparent\")\n    this._addedContentBg = options.addedContentBg ? parseColor(options.addedContentBg) : null\n    this._removedContentBg = options.removedContentBg ? parseColor(options.removedContentBg) : null\n    this._contextContentBg = options.contextContentBg ? parseColor(options.contextContentBg) : null\n    this._addedSignColor = parseColor(options.addedSignColor ?? \"#22c55e\")\n    this._removedSignColor = parseColor(options.removedSignColor ?? \"#ef4444\")\n    this._addedLineNumberBg = parseColor(options.addedLineNumberBg ?? \"transparent\")\n    this._removedLineNumberBg = parseColor(options.removedLineNumberBg ?? \"transparent\")\n\n    if (this._diff) {\n      this.parseDiff()\n      this.buildView()\n    }\n  }\n\n  private parseDiff(): void {\n    if (!this._diff) {\n      this._parsedDiff = null\n      this._parseError = null\n      return\n    }\n\n    try {\n      const patches = parsePatch(this._diff)\n\n      if (patches.length === 0) {\n        this._parsedDiff = null\n        this._parseError = null\n        return\n      }\n\n      this._parsedDiff = patches[0]\n      this._parseError = null\n    } catch (error) {\n      this._parsedDiff = null\n      this._parseError = error instanceof Error ? error : new Error(String(error))\n    }\n  }\n\n  private buildView(): void {\n    if (this._parseError) {\n      this.buildErrorView()\n      return\n    }\n\n    if (!this._parsedDiff || this._parsedDiff.hunks.length === 0) {\n      return\n    }\n\n    if (this._view === \"unified\") {\n      this.buildUnifiedView()\n    } else {\n      this.buildSplitView()\n    }\n  }\n\n  protected override onMouseEvent(event: MouseEvent): void {\n    if (event.type !== \"scroll\" || this._view !== \"split\" || !this._syncScroll) return\n    if (!this.leftCodeRenderable || !this.rightCodeRenderable) return\n    if (!event.target) return\n\n    if (this.isInsideSide(event.target, \"left\")) {\n      this.rightCodeRenderable.scrollY = this.leftCodeRenderable.scrollY\n      this.rightCodeRenderable.scrollX = this.leftCodeRenderable.scrollX\n    } else if (this.isInsideSide(event.target, \"right\")) {\n      this.leftCodeRenderable.scrollY = this.rightCodeRenderable.scrollY\n      this.leftCodeRenderable.scrollX = this.rightCodeRenderable.scrollX\n    }\n  }\n\n  private isInsideSide(target: Renderable | null, side: \"left\" | \"right\"): boolean {\n    const container = side === \"left\" ? this.leftCodeRenderable : this.rightCodeRenderable\n    let current = target\n    while (current) {\n      if (current === container) return true\n      current = current.parent\n    }\n    return false\n  }\n\n  protected override onResize(width: number, height: number): void {\n    super.onResize(width, height)\n\n    if (this._view === \"split\" && this._wrapMode !== \"none\" && this._wrapMode !== undefined) {\n      if (this._lastWidth !== width) {\n        this._lastWidth = width\n        this.requestRebuild()\n      }\n    }\n  }\n\n  private requestRebuild(): void {\n    if (this.pendingRebuild) {\n      return\n    }\n\n    this.pendingRebuild = true\n    queueMicrotask(() => {\n      if (!this.isDestroyed && this.pendingRebuild) {\n        this.pendingRebuild = false\n        this.buildView()\n        this.requestRender()\n      }\n    })\n  }\n\n  private rebuildView(): void {\n    if (this._view === \"split\") {\n      this.requestRebuild()\n    } else {\n      this.buildView()\n    }\n  }\n\n  private handleLineInfoChange = (): void => {\n    if (!this._waitingForHighlight) return\n    if (!this.leftCodeRenderable || !this.rightCodeRenderable) return\n\n    const leftIsHighlighting = this.leftCodeRenderable.isHighlighting\n    const rightIsHighlighting = this.rightCodeRenderable.isHighlighting\n\n    if (!leftIsHighlighting && !rightIsHighlighting) {\n      this._waitingForHighlight = false\n      this.requestRebuild()\n    }\n  }\n\n  private attachLineInfoListeners(): void {\n    if (this._lineInfoChangeHandler) return\n    if (!this.leftCodeRenderable || !this.rightCodeRenderable) return\n\n    this._lineInfoChangeHandler = this.handleLineInfoChange\n    this.leftCodeRenderable.on(\"line-info-change\", this._lineInfoChangeHandler)\n    this.rightCodeRenderable.on(\"line-info-change\", this._lineInfoChangeHandler)\n  }\n\n  private detachLineInfoListeners(): void {\n    if (!this._lineInfoChangeHandler) return\n\n    if (this.leftCodeRenderable) {\n      this.leftCodeRenderable.off(\"line-info-change\", this._lineInfoChangeHandler)\n    }\n    if (this.rightCodeRenderable) {\n      this.rightCodeRenderable.off(\"line-info-change\", this._lineInfoChangeHandler)\n    }\n    this._lineInfoChangeHandler = null\n  }\n\n  public override destroyRecursively(): void {\n    this.detachLineInfoListeners()\n    this.pendingRebuild = false\n    this.leftSideAdded = false\n    this.rightSideAdded = false\n    super.destroyRecursively()\n  }\n\n  private buildErrorView(): void {\n    this.flexDirection = \"column\"\n\n    if (this.leftSide && this.leftSideAdded) {\n      super.remove(this.leftSide.id)\n      this.leftSideAdded = false\n    }\n    if (this.rightSide && this.rightSideAdded) {\n      super.remove(this.rightSide.id)\n      this.rightSideAdded = false\n    }\n\n    const errorMessage = `Error parsing diff: ${this._parseError?.message || \"Unknown error\"}\\n`\n    if (!this.errorTextRenderable) {\n      this.errorTextRenderable = new TextRenderable(this.ctx, {\n        id: this.id ? `${this.id}-error-text` : undefined,\n        content: errorMessage,\n        fg: \"#ef4444\",\n        width: \"100%\",\n        flexShrink: 0,\n      })\n      super.add(this.errorTextRenderable)\n    } else {\n      this.errorTextRenderable.content = errorMessage\n      const errorTextIndex = this.getChildren().indexOf(this.errorTextRenderable)\n      if (errorTextIndex === -1) {\n        super.add(this.errorTextRenderable)\n      }\n    }\n\n    if (!this.errorCodeRenderable) {\n      this.errorCodeRenderable = new CodeRenderable(this.ctx, {\n        id: this.id ? `${this.id}-error-code` : undefined,\n        content: this._diff,\n        filetype: \"diff\",\n        syntaxStyle: this._syntaxStyle ?? SyntaxStyle.create(),\n        wrapMode: this._wrapMode,\n        conceal: this._conceal,\n        width: \"100%\",\n        flexGrow: 1,\n        flexShrink: 1,\n        ...(this._treeSitterClient !== undefined && { treeSitterClient: this._treeSitterClient }),\n      })\n      super.add(this.errorCodeRenderable)\n    } else {\n      this.errorCodeRenderable.content = this._diff\n      this.errorCodeRenderable.wrapMode = this._wrapMode ?? \"none\"\n      if (this._syntaxStyle) {\n        this.errorCodeRenderable.syntaxStyle = this._syntaxStyle\n      }\n      const errorCodeIndex = this.getChildren().indexOf(this.errorCodeRenderable)\n      if (errorCodeIndex === -1) {\n        super.add(this.errorCodeRenderable)\n      }\n    }\n  }\n\n  private createOrUpdateCodeRenderable(\n    side: \"left\" | \"right\",\n    content: string,\n    wrapMode: \"word\" | \"char\" | \"none\" | undefined,\n    drawUnstyledText?: boolean,\n  ): CodeRenderable {\n    const existingRenderable = side === \"left\" ? this.leftCodeRenderable : this.rightCodeRenderable\n\n    if (!existingRenderable) {\n      const codeOptions: CodeOptions = {\n        id: this.id ? `${this.id}-${side}-code` : undefined,\n        content,\n        filetype: this._filetype,\n        wrapMode,\n        conceal: this._conceal,\n        syntaxStyle: this._syntaxStyle ?? SyntaxStyle.create(),\n        width: \"100%\",\n        height: \"100%\",\n        ...(this._fg !== undefined && { fg: this._fg }),\n        ...(drawUnstyledText !== undefined && { drawUnstyledText }),\n        ...(this._selectionBg !== undefined && { selectionBg: this._selectionBg }),\n        ...(this._selectionFg !== undefined && { selectionFg: this._selectionFg }),\n        ...(this._treeSitterClient !== undefined && { treeSitterClient: this._treeSitterClient }),\n      }\n      const newRenderable = new CodeRenderable(this.ctx, codeOptions)\n\n      if (side === \"left\") {\n        this.leftCodeRenderable = newRenderable\n      } else {\n        this.rightCodeRenderable = newRenderable\n      }\n\n      return newRenderable\n    } else {\n      existingRenderable.content = content\n      existingRenderable.wrapMode = wrapMode ?? \"none\"\n      existingRenderable.conceal = this._conceal\n      if (drawUnstyledText !== undefined) {\n        existingRenderable.drawUnstyledText = drawUnstyledText\n      }\n      if (this._filetype !== undefined) {\n        existingRenderable.filetype = this._filetype\n      }\n      if (this._syntaxStyle !== undefined) {\n        existingRenderable.syntaxStyle = this._syntaxStyle\n      }\n      if (this._selectionBg !== undefined) {\n        existingRenderable.selectionBg = this._selectionBg\n      }\n      if (this._selectionFg !== undefined) {\n        existingRenderable.selectionFg = this._selectionFg\n      }\n      if (this._fg !== undefined) {\n        existingRenderable.fg = this._fg\n      }\n\n      return existingRenderable\n    }\n  }\n\n  private createOrUpdateSide(\n    side: \"left\" | \"right\",\n    target: CodeRenderable,\n    lineColors: Map<number, string | RGBA | LineColorConfig>,\n    lineSigns: Map<number, LineSign>,\n    lineNumbers: Map<number, number>,\n    hideLineNumbers: Set<number>,\n    width: \"50%\" | \"100%\",\n  ): void {\n    const sideRef = side === \"left\" ? this.leftSide : this.rightSide\n    const addedFlag = side === \"left\" ? this.leftSideAdded : this.rightSideAdded\n\n    if (!sideRef) {\n      const newSide = new LineNumberRenderable(this.ctx, {\n        id: this.id ? `${this.id}-${side}` : undefined,\n        target,\n        fg: this._lineNumberFg,\n        bg: this._lineNumberBg,\n        lineColors,\n        lineSigns,\n        lineNumbers,\n        lineNumberOffset: 0,\n        hideLineNumbers,\n        width,\n        height: \"100%\",\n      })\n      newSide.showLineNumbers = this._showLineNumbers\n      super.add(newSide)\n\n      if (side === \"left\") {\n        this.leftSide = newSide\n        this.leftSideAdded = true\n      } else {\n        this.rightSide = newSide\n        this.rightSideAdded = true\n      }\n    } else {\n      sideRef.width = width\n      sideRef.setLineColors(lineColors)\n      sideRef.setLineSigns(lineSigns)\n      sideRef.setLineNumbers(lineNumbers)\n      sideRef.setHideLineNumbers(hideLineNumbers)\n\n      if (!addedFlag) {\n        super.add(sideRef)\n        if (side === \"left\") {\n          this.leftSideAdded = true\n        } else {\n          this.rightSideAdded = true\n        }\n      }\n    }\n  }\n\n  private buildUnifiedView(): void {\n    if (!this._parsedDiff) return\n\n    this.flexDirection = \"column\"\n\n    if (this.errorTextRenderable) {\n      const errorTextIndex = this.getChildren().indexOf(this.errorTextRenderable)\n      if (errorTextIndex !== -1) {\n        super.remove(this.errorTextRenderable.id)\n      }\n    }\n    if (this.errorCodeRenderable) {\n      const errorCodeIndex = this.getChildren().indexOf(this.errorCodeRenderable)\n      if (errorCodeIndex !== -1) {\n        super.remove(this.errorCodeRenderable.id)\n      }\n    }\n\n    const contentLines: string[] = []\n    const lineColors = new Map<number, string | RGBA | LineColorConfig>()\n    const lineSigns = new Map<number, LineSign>()\n    const lineNumbers = new Map<number, number>()\n\n    let lineIndex = 0\n\n    for (const hunk of this._parsedDiff.hunks) {\n      let oldLineNum = hunk.oldStart\n      let newLineNum = hunk.newStart\n\n      for (const line of hunk.lines) {\n        const firstChar = line[0]\n        const content = line.slice(1)\n\n        if (firstChar === \"+\") {\n          contentLines.push(content)\n          const config: LineColorConfig = {\n            gutter: this._addedLineNumberBg,\n          }\n          if (this._addedContentBg) {\n            config.content = this._addedContentBg\n          } else {\n            config.content = this._addedBg\n          }\n          lineColors.set(lineIndex, config)\n          lineSigns.set(lineIndex, {\n            after: \" +\",\n            afterColor: this._addedSignColor,\n          })\n          lineNumbers.set(lineIndex, newLineNum)\n          newLineNum++\n          lineIndex++\n        } else if (firstChar === \"-\") {\n          contentLines.push(content)\n          const config: LineColorConfig = {\n            gutter: this._removedLineNumberBg,\n          }\n          if (this._removedContentBg) {\n            config.content = this._removedContentBg\n          } else {\n            config.content = this._removedBg\n          }\n          lineColors.set(lineIndex, config)\n          lineSigns.set(lineIndex, {\n            after: \" -\",\n            afterColor: this._removedSignColor,\n          })\n          lineNumbers.set(lineIndex, oldLineNum)\n          oldLineNum++\n          lineIndex++\n        } else if (firstChar === \" \") {\n          contentLines.push(content)\n          const config: LineColorConfig = {\n            gutter: this._lineNumberBg,\n          }\n          if (this._contextContentBg) {\n            config.content = this._contextContentBg\n          } else {\n            config.content = this._contextBg\n          }\n          lineColors.set(lineIndex, config)\n          lineNumbers.set(lineIndex, newLineNum)\n          oldLineNum++\n          newLineNum++\n          lineIndex++\n        }\n      }\n    }\n\n    const content = contentLines.join(\"\\n\")\n\n    const codeRenderable = this.createOrUpdateCodeRenderable(\"left\", content, this._wrapMode)\n\n    this.createOrUpdateSide(\"left\", codeRenderable, lineColors, lineSigns, lineNumbers, new Set<number>(), \"100%\")\n\n    if (this.rightSide && this.rightSideAdded) {\n      super.remove(this.rightSide.id)\n      this.rightSideAdded = false\n    }\n  }\n\n  private buildSplitView(): void {\n    if (!this._parsedDiff) return\n\n    this.flexDirection = \"row\"\n\n    if (this.errorTextRenderable) {\n      const errorTextIndex = this.getChildren().indexOf(this.errorTextRenderable)\n      if (errorTextIndex !== -1) {\n        super.remove(this.errorTextRenderable.id)\n      }\n    }\n    if (this.errorCodeRenderable) {\n      const errorCodeIndex = this.getChildren().indexOf(this.errorCodeRenderable)\n      if (errorCodeIndex !== -1) {\n        super.remove(this.errorCodeRenderable.id)\n      }\n    }\n\n    const leftLogicalLines: LogicalLine[] = []\n    const rightLogicalLines: LogicalLine[] = []\n\n    for (const hunk of this._parsedDiff.hunks) {\n      let oldLineNum = hunk.oldStart\n      let newLineNum = hunk.newStart\n\n      let i = 0\n      while (i < hunk.lines.length) {\n        const line = hunk.lines[i]\n        const firstChar = line[0]\n\n        if (firstChar === \" \") {\n          const content = line.slice(1)\n          leftLogicalLines.push({\n            content,\n            lineNum: oldLineNum,\n            color: this._contextBg,\n            type: \"context\",\n          })\n          rightLogicalLines.push({\n            content,\n            lineNum: newLineNum,\n            color: this._contextBg,\n            type: \"context\",\n          })\n          oldLineNum++\n          newLineNum++\n          i++\n        } else if (firstChar === \"\\\\\") {\n          i++\n        } else {\n          const removes: { content: string; lineNum: number }[] = []\n          const adds: { content: string; lineNum: number }[] = []\n\n          while (i < hunk.lines.length) {\n            const currentLine = hunk.lines[i]\n            const currentChar = currentLine[0]\n\n            if (currentChar === \" \" || currentChar === \"\\\\\") {\n              break\n            }\n\n            const content = currentLine.slice(1)\n\n            if (currentChar === \"-\") {\n              removes.push({ content, lineNum: oldLineNum })\n              oldLineNum++\n            } else if (currentChar === \"+\") {\n              adds.push({ content, lineNum: newLineNum })\n              newLineNum++\n            }\n            i++\n          }\n\n          const maxLength = Math.max(removes.length, adds.length)\n\n          for (let j = 0; j < maxLength; j++) {\n            if (j < removes.length) {\n              leftLogicalLines.push({\n                content: removes[j].content,\n                lineNum: removes[j].lineNum,\n                color: this._removedBg,\n                sign: {\n                  after: \" -\",\n                  afterColor: this._removedSignColor,\n                },\n                type: \"remove\",\n              })\n            } else {\n              leftLogicalLines.push({\n                content: \"\",\n                hideLineNumber: true,\n                type: \"empty\",\n              })\n            }\n\n            if (j < adds.length) {\n              rightLogicalLines.push({\n                content: adds[j].content,\n                lineNum: adds[j].lineNum,\n                color: this._addedBg,\n                sign: {\n                  after: \" +\",\n                  afterColor: this._addedSignColor,\n                },\n                type: \"add\",\n              })\n            } else {\n              rightLogicalLines.push({\n                content: \"\",\n                hideLineNumber: true,\n                type: \"empty\",\n              })\n            }\n          }\n        }\n      }\n    }\n\n    const canDoWrapAlignment = this.width > 0 && (this._wrapMode === \"word\" || this._wrapMode === \"char\")\n\n    const preLeftContent = leftLogicalLines.map((l) => l.content).join(\"\\n\")\n    const preRightContent = rightLogicalLines.map((l) => l.content).join(\"\\n\")\n\n    const needsConsistentConcealing =\n      (this._wrapMode === \"word\" || this._wrapMode === \"char\") && this._conceal && this._filetype\n    const drawUnstyledText = !needsConsistentConcealing\n    const leftCodeRenderable = this.createOrUpdateCodeRenderable(\n      \"left\",\n      preLeftContent,\n      this._wrapMode,\n      drawUnstyledText,\n    )\n    const rightCodeRenderable = this.createOrUpdateCodeRenderable(\n      \"right\",\n      preRightContent,\n      this._wrapMode,\n      drawUnstyledText,\n    )\n\n    let finalLeftLines: LogicalLine[]\n    let finalRightLines: LogicalLine[]\n\n    const leftIsHighlighting = leftCodeRenderable.isHighlighting\n    const rightIsHighlighting = rightCodeRenderable.isHighlighting\n    const highlightingInProgress = needsConsistentConcealing && (leftIsHighlighting || rightIsHighlighting)\n\n    if (highlightingInProgress) {\n      this._waitingForHighlight = true\n      this.attachLineInfoListeners()\n    }\n\n    const shouldDoAlignment = canDoWrapAlignment && !highlightingInProgress\n\n    if (shouldDoAlignment) {\n      const leftLineInfo = leftCodeRenderable.lineInfo\n      const rightLineInfo = rightCodeRenderable.lineInfo\n\n      const leftSources = leftLineInfo.lineSources || []\n      const rightSources = rightLineInfo.lineSources || []\n\n      const leftVisualCounts = new Map<number, number>()\n      const rightVisualCounts = new Map<number, number>()\n\n      for (const logicalLine of leftSources) {\n        leftVisualCounts.set(logicalLine, (leftVisualCounts.get(logicalLine) || 0) + 1)\n      }\n      for (const logicalLine of rightSources) {\n        rightVisualCounts.set(logicalLine, (rightVisualCounts.get(logicalLine) || 0) + 1)\n      }\n\n      finalLeftLines = []\n      finalRightLines = []\n\n      let leftVisualPos = 0\n      let rightVisualPos = 0\n\n      for (let i = 0; i < leftLogicalLines.length; i++) {\n        const leftLine = leftLogicalLines[i]\n        const rightLine = rightLogicalLines[i]\n\n        const leftVisualCount = leftVisualCounts.get(i) || 1\n        const rightVisualCount = rightVisualCounts.get(i) || 1\n\n        if (leftVisualPos < rightVisualPos) {\n          const pad = rightVisualPos - leftVisualPos\n          for (let p = 0; p < pad; p++) {\n            finalLeftLines.push({ content: \"\", hideLineNumber: true, type: \"empty\" })\n          }\n          leftVisualPos += pad\n        } else if (rightVisualPos < leftVisualPos) {\n          const pad = leftVisualPos - rightVisualPos\n          for (let p = 0; p < pad; p++) {\n            finalRightLines.push({ content: \"\", hideLineNumber: true, type: \"empty\" })\n          }\n          rightVisualPos += pad\n        }\n\n        finalLeftLines.push(leftLine)\n        finalRightLines.push(rightLine)\n\n        leftVisualPos += leftVisualCount\n        rightVisualPos += rightVisualCount\n      }\n\n      if (leftVisualPos < rightVisualPos) {\n        const pad = rightVisualPos - leftVisualPos\n        for (let p = 0; p < pad; p++) {\n          finalLeftLines.push({ content: \"\", hideLineNumber: true, type: \"empty\" })\n        }\n      } else if (rightVisualPos < leftVisualPos) {\n        const pad = leftVisualPos - rightVisualPos\n        for (let p = 0; p < pad; p++) {\n          finalRightLines.push({ content: \"\", hideLineNumber: true, type: \"empty\" })\n        }\n      }\n    } else {\n      finalLeftLines = leftLogicalLines\n      finalRightLines = rightLogicalLines\n    }\n\n    const leftLineColors = new Map<number, string | RGBA | LineColorConfig>()\n    const rightLineColors = new Map<number, string | RGBA | LineColorConfig>()\n    const leftLineSigns = new Map<number, LineSign>()\n    const rightLineSigns = new Map<number, LineSign>()\n    const leftHideLineNumbers = new Set<number>()\n    const rightHideLineNumbers = new Set<number>()\n    const leftLineNumbers = new Map<number, number>()\n    const rightLineNumbers = new Map<number, number>()\n\n    finalLeftLines.forEach((line, index) => {\n      if (line.lineNum !== undefined) {\n        leftLineNumbers.set(index, line.lineNum)\n      }\n      if (line.hideLineNumber) {\n        leftHideLineNumbers.add(index)\n      }\n      if (line.type === \"remove\") {\n        const config: LineColorConfig = {\n          gutter: this._removedLineNumberBg,\n        }\n        if (this._removedContentBg) {\n          config.content = this._removedContentBg\n        } else {\n          config.content = this._removedBg\n        }\n        leftLineColors.set(index, config)\n      } else if (line.type === \"context\") {\n        const config: LineColorConfig = {\n          gutter: this._lineNumberBg,\n        }\n        if (this._contextContentBg) {\n          config.content = this._contextContentBg\n        } else {\n          config.content = this._contextBg\n        }\n        leftLineColors.set(index, config)\n      }\n      if (line.sign) {\n        leftLineSigns.set(index, line.sign)\n      }\n    })\n\n    finalRightLines.forEach((line, index) => {\n      if (line.lineNum !== undefined) {\n        rightLineNumbers.set(index, line.lineNum)\n      }\n      if (line.hideLineNumber) {\n        rightHideLineNumbers.add(index)\n      }\n      if (line.type === \"add\") {\n        const config: LineColorConfig = {\n          gutter: this._addedLineNumberBg,\n        }\n        if (this._addedContentBg) {\n          config.content = this._addedContentBg\n        } else {\n          config.content = this._addedBg\n        }\n        rightLineColors.set(index, config)\n      } else if (line.type === \"context\") {\n        const config: LineColorConfig = {\n          gutter: this._lineNumberBg,\n        }\n        if (this._contextContentBg) {\n          config.content = this._contextContentBg\n        } else {\n          config.content = this._contextBg\n        }\n        rightLineColors.set(index, config)\n      }\n      if (line.sign) {\n        rightLineSigns.set(index, line.sign)\n      }\n    })\n\n    const leftContentFinal = finalLeftLines.map((l) => l.content).join(\"\\n\")\n    const rightContentFinal = finalRightLines.map((l) => l.content).join(\"\\n\")\n\n    leftCodeRenderable.content = leftContentFinal\n    rightCodeRenderable.content = rightContentFinal\n\n    this.createOrUpdateSide(\n      \"left\",\n      leftCodeRenderable,\n      leftLineColors,\n      leftLineSigns,\n      leftLineNumbers,\n      leftHideLineNumbers,\n      \"50%\",\n    )\n    this.createOrUpdateSide(\n      \"right\",\n      rightCodeRenderable,\n      rightLineColors,\n      rightLineSigns,\n      rightLineNumbers,\n      rightHideLineNumbers,\n      \"50%\",\n    )\n  }\n\n  public get diff(): string {\n    return this._diff\n  }\n\n  public set diff(value: string) {\n    if (this._diff !== value) {\n      this._diff = value\n      this._waitingForHighlight = false\n      this.parseDiff()\n      this.rebuildView()\n    }\n  }\n\n  public get syncScroll(): boolean {\n    return this._syncScroll\n  }\n\n  public set syncScroll(value: boolean) {\n    if (this._syncScroll !== value) {\n      this._syncScroll = value\n      if (!value) {\n        this.detachLineInfoListeners()\n      }\n    }\n  }\n\n  public get view(): \"unified\" | \"split\" {\n    return this._view\n  }\n\n  public set view(value: \"unified\" | \"split\") {\n    if (this._view !== value) {\n      this._view = value\n      this.flexDirection = value === \"split\" ? \"row\" : \"column\"\n      this.buildView()\n    }\n  }\n\n  public get filetype(): string | undefined {\n    return this._filetype\n  }\n\n  public set filetype(value: string | undefined) {\n    if (this._filetype !== value) {\n      this._filetype = value\n      this.rebuildView()\n    }\n  }\n\n  public get syntaxStyle(): SyntaxStyle | undefined {\n    return this._syntaxStyle\n  }\n\n  public set syntaxStyle(value: SyntaxStyle | undefined) {\n    if (this._syntaxStyle !== value) {\n      this._syntaxStyle = value\n      this.rebuildView()\n    }\n  }\n\n  public get wrapMode(): \"word\" | \"char\" | \"none\" | undefined {\n    return this._wrapMode\n  }\n\n  public set wrapMode(value: \"word\" | \"char\" | \"none\" | undefined) {\n    if (this._wrapMode !== value) {\n      this._wrapMode = value\n\n      if (this._view === \"unified\" && this.leftCodeRenderable) {\n        this.leftCodeRenderable.wrapMode = value ?? \"none\"\n      } else if (this._view === \"split\") {\n        this.requestRebuild()\n      }\n    }\n  }\n\n  public get showLineNumbers(): boolean {\n    return this._showLineNumbers\n  }\n\n  public set showLineNumbers(value: boolean) {\n    if (this._showLineNumbers !== value) {\n      this._showLineNumbers = value\n      if (this.leftSide) {\n        this.leftSide.showLineNumbers = value\n      }\n      if (this.rightSide) {\n        this.rightSide.showLineNumbers = value\n      }\n    }\n  }\n\n  public get addedBg(): RGBA {\n    return this._addedBg\n  }\n\n  public set addedBg(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._addedBg !== parsed) {\n      this._addedBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get removedBg(): RGBA {\n    return this._removedBg\n  }\n\n  public set removedBg(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._removedBg !== parsed) {\n      this._removedBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get contextBg(): RGBA {\n    return this._contextBg\n  }\n\n  public set contextBg(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._contextBg !== parsed) {\n      this._contextBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get addedSignColor(): RGBA {\n    return this._addedSignColor\n  }\n\n  public set addedSignColor(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._addedSignColor !== parsed) {\n      this._addedSignColor = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get removedSignColor(): RGBA {\n    return this._removedSignColor\n  }\n\n  public set removedSignColor(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._removedSignColor !== parsed) {\n      this._removedSignColor = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get addedLineNumberBg(): RGBA {\n    return this._addedLineNumberBg\n  }\n\n  public set addedLineNumberBg(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._addedLineNumberBg !== parsed) {\n      this._addedLineNumberBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get removedLineNumberBg(): RGBA {\n    return this._removedLineNumberBg\n  }\n\n  public set removedLineNumberBg(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._removedLineNumberBg !== parsed) {\n      this._removedLineNumberBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get lineNumberFg(): RGBA {\n    return this._lineNumberFg\n  }\n\n  public set lineNumberFg(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._lineNumberFg !== parsed) {\n      this._lineNumberFg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get lineNumberBg(): RGBA {\n    return this._lineNumberBg\n  }\n\n  public set lineNumberBg(value: string | RGBA) {\n    const parsed = parseColor(value)\n    if (this._lineNumberBg !== parsed) {\n      this._lineNumberBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get addedContentBg(): RGBA | null {\n    return this._addedContentBg\n  }\n\n  public set addedContentBg(value: string | RGBA | null) {\n    const parsed = value ? parseColor(value) : null\n    if (this._addedContentBg !== parsed) {\n      this._addedContentBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get removedContentBg(): RGBA | null {\n    return this._removedContentBg\n  }\n\n  public set removedContentBg(value: string | RGBA | null) {\n    const parsed = value ? parseColor(value) : null\n    if (this._removedContentBg !== parsed) {\n      this._removedContentBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get contextContentBg(): RGBA | null {\n    return this._contextContentBg\n  }\n\n  public set contextContentBg(value: string | RGBA | null) {\n    const parsed = value ? parseColor(value) : null\n    if (this._contextContentBg !== parsed) {\n      this._contextContentBg = parsed\n      this.rebuildView()\n    }\n  }\n\n  public get selectionBg(): RGBA | undefined {\n    return this._selectionBg\n  }\n\n  public set selectionBg(value: string | RGBA | undefined) {\n    const parsed = value ? parseColor(value) : undefined\n    if (this._selectionBg !== parsed) {\n      this._selectionBg = parsed\n      if (this.leftCodeRenderable) {\n        this.leftCodeRenderable.selectionBg = parsed\n      }\n      if (this.rightCodeRenderable) {\n        this.rightCodeRenderable.selectionBg = parsed\n      }\n    }\n  }\n\n  public get selectionFg(): RGBA | undefined {\n    return this._selectionFg\n  }\n\n  public set selectionFg(value: string | RGBA | undefined) {\n    const parsed = value ? parseColor(value) : undefined\n    if (this._selectionFg !== parsed) {\n      this._selectionFg = parsed\n      if (this.leftCodeRenderable) {\n        this.leftCodeRenderable.selectionFg = parsed\n      }\n      if (this.rightCodeRenderable) {\n        this.rightCodeRenderable.selectionFg = parsed\n      }\n    }\n  }\n\n  public get conceal(): boolean {\n    return this._conceal\n  }\n\n  public set conceal(value: boolean) {\n    if (this._conceal !== value) {\n      this._conceal = value\n      this.rebuildView()\n    }\n  }\n\n  public get fg(): RGBA | undefined {\n    return this._fg\n  }\n\n  public set fg(value: string | RGBA | undefined) {\n    const parsed = value ? parseColor(value) : undefined\n    if (this._fg !== parsed) {\n      this._fg = parsed\n      if (this.leftCodeRenderable) {\n        this.leftCodeRenderable.fg = parsed\n      }\n      if (this.rightCodeRenderable) {\n        this.rightCodeRenderable.fg = parsed\n      }\n    }\n  }\n\n  public setLineColor(line: number, color: string | RGBA | LineColorConfig): void {\n    this.leftSide?.setLineColor(line, color)\n    this.rightSide?.setLineColor(line, color)\n  }\n\n  public clearLineColor(line: number): void {\n    this.leftSide?.clearLineColor(line)\n    this.rightSide?.clearLineColor(line)\n  }\n\n  public setLineColors(lineColors: Map<number, string | RGBA | LineColorConfig>): void {\n    this.leftSide?.setLineColors(lineColors)\n    this.rightSide?.setLineColors(lineColors)\n  }\n\n  public clearAllLineColors(): void {\n    this.leftSide?.clearAllLineColors()\n    this.rightSide?.clearAllLineColors()\n  }\n\n  public highlightLines(startLine: number, endLine: number, color: string | RGBA | LineColorConfig): void {\n    this.leftSide?.highlightLines(startLine, endLine, color)\n    this.rightSide?.highlightLines(startLine, endLine, color)\n  }\n\n  public clearHighlightLines(startLine: number, endLine: number): void {\n    this.leftSide?.clearHighlightLines(startLine, endLine)\n    this.rightSide?.clearHighlightLines(startLine, endLine)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/EditBufferRenderable.ts",
    "content": "import { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { convertGlobalToLocalSelection, Selection, type LocalSelectionBounds } from \"../lib/selection.js\"\nimport { EditBuffer, type LogicalCursor } from \"../edit-buffer.js\"\nimport { EditorView, type VisualCursor } from \"../editor-view.js\"\nimport { RGBA, parseColor } from \"../lib/RGBA.js\"\nimport type { RenderContext, Highlight, CursorStyleOptions, LineInfoProvider, LineInfo } from \"../types.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport { MeasureMode } from \"yoga-layout\"\nimport type { SyntaxStyle } from \"../syntax-style.js\"\n\nexport interface CursorChangeEvent {\n  line: number\n  visualColumn: number\n}\n\nexport interface ContentChangeEvent {\n  // No payload - use getText() to retrieve content if needed\n}\n\nexport interface EditBufferOptions extends RenderableOptions<EditBufferRenderable> {\n  textColor?: string | RGBA\n  backgroundColor?: string | RGBA\n  selectionBg?: string | RGBA\n  selectionFg?: string | RGBA\n  selectable?: boolean\n  attributes?: number\n  wrapMode?: \"none\" | \"char\" | \"word\"\n  scrollMargin?: number\n  scrollSpeed?: number\n  showCursor?: boolean\n  cursorColor?: string | RGBA\n  cursorStyle?: CursorStyleOptions\n  syntaxStyle?: SyntaxStyle\n  tabIndicator?: string | number\n  tabIndicatorColor?: string | RGBA\n  onCursorChange?: (event: CursorChangeEvent) => void\n  onContentChange?: (event: ContentChangeEvent) => void\n}\n\nexport abstract class EditBufferRenderable extends Renderable implements LineInfoProvider {\n  protected _focusable: boolean = true\n  public selectable: boolean = true\n\n  protected _textColor: RGBA\n  protected _backgroundColor: RGBA\n  protected _defaultAttributes: number\n  protected _selectionBg: RGBA | undefined\n  protected _selectionFg: RGBA | undefined\n  protected _wrapMode: \"none\" | \"char\" | \"word\" = \"word\"\n  protected _scrollMargin: number = 0.2\n  protected _showCursor: boolean = true\n  protected _cursorColor: RGBA\n  protected _cursorStyle: CursorStyleOptions\n  protected lastLocalSelection: LocalSelectionBounds | null = null\n  protected _tabIndicator?: string | number\n  protected _tabIndicatorColor?: RGBA\n\n  private _cursorChangeListener: ((event: CursorChangeEvent) => void) | undefined = undefined\n  private _contentChangeListener: ((event: ContentChangeEvent) => void) | undefined = undefined\n\n  private _autoScrollVelocity: number = 0\n  private _autoScrollAccumulator: number = 0\n  private _scrollSpeed: number = 16\n  private _keyboardSelectionActive: boolean = false\n\n  public readonly editBuffer: EditBuffer\n  public readonly editorView: EditorView\n\n  protected _defaultOptions = {\n    textColor: RGBA.fromValues(1, 1, 1, 1),\n    backgroundColor: \"transparent\",\n    selectionBg: undefined,\n    selectionFg: undefined,\n    selectable: true,\n    attributes: 0,\n    wrapMode: \"word\" as \"none\" | \"char\" | \"word\",\n    scrollMargin: 0.2,\n    scrollSpeed: 16,\n    showCursor: true,\n    cursorColor: RGBA.fromValues(1, 1, 1, 1),\n    cursorStyle: {\n      style: \"block\",\n      blinking: true,\n    },\n    tabIndicator: undefined,\n    tabIndicatorColor: undefined,\n  } satisfies Partial<EditBufferOptions>\n\n  constructor(ctx: RenderContext, options: EditBufferOptions) {\n    super(ctx, options)\n\n    this._textColor = parseColor(options.textColor ?? this._defaultOptions.textColor)\n    this._backgroundColor = parseColor(options.backgroundColor ?? this._defaultOptions.backgroundColor)\n    this._defaultAttributes = options.attributes ?? this._defaultOptions.attributes\n    this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : this._defaultOptions.selectionBg\n    this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : this._defaultOptions.selectionFg\n    this.selectable = options.selectable ?? this._defaultOptions.selectable\n    this._wrapMode = options.wrapMode ?? this._defaultOptions.wrapMode\n    this._scrollMargin = options.scrollMargin ?? this._defaultOptions.scrollMargin\n    this._scrollSpeed = options.scrollSpeed ?? this._defaultOptions.scrollSpeed\n    this._showCursor = options.showCursor ?? this._defaultOptions.showCursor\n    this._cursorColor = parseColor(options.cursorColor ?? this._defaultOptions.cursorColor)\n    this._cursorStyle = options.cursorStyle ?? this._defaultOptions.cursorStyle\n    this._tabIndicator = options.tabIndicator ?? this._defaultOptions.tabIndicator\n    this._tabIndicatorColor = options.tabIndicatorColor\n      ? parseColor(options.tabIndicatorColor)\n      : this._defaultOptions.tabIndicatorColor\n\n    this.editBuffer = EditBuffer.create(this._ctx.widthMethod)\n    this.editorView = EditorView.create(this.editBuffer, this.width || 80, this.height || 24)\n\n    this.editorView.setWrapMode(this._wrapMode)\n    this.editorView.setScrollMargin(this._scrollMargin)\n\n    this.editBuffer.setDefaultFg(this._textColor)\n    this.editBuffer.setDefaultBg(this._backgroundColor)\n    this.editBuffer.setDefaultAttributes(this._defaultAttributes)\n\n    if (options.syntaxStyle) {\n      this.editBuffer.setSyntaxStyle(options.syntaxStyle)\n    }\n\n    if (this._tabIndicator !== undefined) {\n      this.editorView.setTabIndicator(this._tabIndicator)\n    }\n    if (this._tabIndicatorColor !== undefined) {\n      this.editorView.setTabIndicatorColor(this._tabIndicatorColor)\n    }\n\n    this.setupMeasureFunc()\n    this.setupEventListeners(options)\n  }\n\n  public get lineInfo(): LineInfo {\n    return this.editorView.getLogicalLineInfo()\n  }\n\n  private setupEventListeners(options: EditBufferOptions): void {\n    this._cursorChangeListener = options.onCursorChange\n    this._contentChangeListener = options.onContentChange\n\n    this.editBuffer.on(\"cursor-changed\", () => {\n      if (this._cursorChangeListener) {\n        const cursor = this.editBuffer.getCursorPosition()\n        this._cursorChangeListener({\n          line: cursor.row,\n          visualColumn: cursor.col,\n        })\n      }\n    })\n\n    this.editBuffer.on(\"content-changed\", () => {\n      this.yogaNode.markDirty()\n      this.requestRender()\n      this.emit(\"line-info-change\")\n      if (this._contentChangeListener) {\n        this._contentChangeListener({})\n      }\n    })\n  }\n\n  public get lineCount(): number {\n    return this.editBuffer.getLineCount()\n  }\n\n  public get virtualLineCount(): number {\n    return this.editorView.getVirtualLineCount()\n  }\n\n  public get scrollY(): number {\n    return this.editorView.getViewport().offsetY\n  }\n\n  get plainText(): string {\n    return this.editBuffer.getText()\n  }\n\n  get logicalCursor(): LogicalCursor {\n    return this.editBuffer.getCursorPosition()\n  }\n\n  get visualCursor(): VisualCursor {\n    return this.editorView.getVisualCursor()\n  }\n\n  get cursorOffset(): number {\n    return this.editorView.getVisualCursor().offset\n  }\n\n  set cursorOffset(offset: number) {\n    this.editorView.setCursorByOffset(offset)\n    this.requestRender()\n  }\n\n  get textColor(): RGBA {\n    return this._textColor\n  }\n\n  set textColor(value: RGBA | string | undefined) {\n    const newColor = parseColor(value ?? this._defaultOptions.textColor)\n    if (this._textColor !== newColor) {\n      this._textColor = newColor\n      this.editBuffer.setDefaultFg(newColor)\n      this.requestRender()\n    }\n  }\n\n  get selectionBg(): RGBA | undefined {\n    return this._selectionBg\n  }\n\n  set selectionBg(value: RGBA | string | undefined) {\n    const newColor = value ? parseColor(value) : this._defaultOptions.selectionBg\n    if (this._selectionBg !== newColor) {\n      this._selectionBg = newColor\n      if (this.lastLocalSelection) {\n        this.updateLocalSelection(this.lastLocalSelection)\n      }\n      this.requestRender()\n    }\n  }\n\n  get selectionFg(): RGBA | undefined {\n    return this._selectionFg\n  }\n\n  set selectionFg(value: RGBA | string | undefined) {\n    const newColor = value ? parseColor(value) : this._defaultOptions.selectionFg\n    if (this._selectionFg !== newColor) {\n      this._selectionFg = newColor\n      if (this.lastLocalSelection) {\n        this.updateLocalSelection(this.lastLocalSelection)\n      }\n      this.requestRender()\n    }\n  }\n\n  get backgroundColor(): RGBA {\n    return this._backgroundColor\n  }\n\n  set backgroundColor(value: RGBA | string | undefined) {\n    const newColor = parseColor(value ?? this._defaultOptions.backgroundColor)\n    if (this._backgroundColor !== newColor) {\n      this._backgroundColor = newColor\n      this.editBuffer.setDefaultBg(newColor)\n      this.requestRender()\n    }\n  }\n\n  get attributes(): number {\n    return this._defaultAttributes\n  }\n\n  set attributes(value: number) {\n    if (this._defaultAttributes !== value) {\n      this._defaultAttributes = value\n      this.editBuffer.setDefaultAttributes(value)\n      this.requestRender()\n    }\n  }\n\n  get wrapMode(): \"none\" | \"char\" | \"word\" {\n    return this._wrapMode\n  }\n\n  set wrapMode(value: \"none\" | \"char\" | \"word\") {\n    if (this._wrapMode !== value) {\n      this._wrapMode = value\n      this.editorView.setWrapMode(value)\n      this.yogaNode.markDirty()\n      this.requestRender()\n    }\n  }\n\n  get showCursor(): boolean {\n    return this._showCursor\n  }\n\n  set showCursor(value: boolean) {\n    if (this._showCursor !== value) {\n      this._showCursor = value\n      if (!value && this._focused) {\n        this._ctx.setCursorPosition(0, 0, false)\n      }\n      this.requestRender()\n    }\n  }\n\n  get cursorColor(): RGBA {\n    return this._cursorColor\n  }\n\n  set cursorColor(value: RGBA | string) {\n    const newColor = parseColor(value)\n    if (this._cursorColor !== newColor) {\n      this._cursorColor = newColor\n      if (this._focused) {\n        this.requestRender()\n      }\n    }\n  }\n\n  get cursorStyle(): CursorStyleOptions {\n    return this._cursorStyle\n  }\n\n  set cursorStyle(style: CursorStyleOptions) {\n    const newStyle = style\n    if (this.cursorStyle.style !== newStyle.style || this.cursorStyle.blinking !== newStyle.blinking) {\n      this._cursorStyle = newStyle\n      if (this._focused) {\n        this.requestRender()\n      }\n    }\n  }\n\n  get tabIndicator(): string | number | undefined {\n    return this._tabIndicator\n  }\n\n  set tabIndicator(value: string | number | undefined) {\n    if (this._tabIndicator !== value) {\n      this._tabIndicator = value\n      if (value !== undefined) {\n        this.editorView.setTabIndicator(value)\n      }\n      this.requestRender()\n    }\n  }\n\n  get tabIndicatorColor(): RGBA | undefined {\n    return this._tabIndicatorColor\n  }\n\n  set tabIndicatorColor(value: RGBA | string | undefined) {\n    const newColor = value ? parseColor(value) : undefined\n    if (this._tabIndicatorColor !== newColor) {\n      this._tabIndicatorColor = newColor\n      if (newColor !== undefined) {\n        this.editorView.setTabIndicatorColor(newColor)\n      }\n      this.requestRender()\n    }\n  }\n\n  get scrollSpeed(): number {\n    return this._scrollSpeed\n  }\n\n  set scrollSpeed(value: number) {\n    this._scrollSpeed = Math.max(0, value)\n  }\n\n  protected override onMouseEvent(event: any): void {\n    if (event.type === \"scroll\") {\n      this.handleScroll(event)\n    }\n  }\n\n  protected handleScroll(event: any): void {\n    if (!event.scroll) return\n\n    const { direction, delta } = event.scroll\n    const viewport = this.editorView.getViewport()\n\n    if (direction === \"up\") {\n      const newOffsetY = Math.max(0, viewport.offsetY - delta)\n      this.editorView.setViewport(viewport.offsetX, newOffsetY, viewport.width, viewport.height, true)\n      this.requestRender()\n    } else if (direction === \"down\") {\n      const totalVirtualLines = this.editorView.getTotalVirtualLineCount()\n      const maxOffsetY = Math.max(0, totalVirtualLines - viewport.height)\n      const newOffsetY = Math.min(viewport.offsetY + delta, maxOffsetY)\n      this.editorView.setViewport(viewport.offsetX, newOffsetY, viewport.width, viewport.height, true)\n      this.requestRender()\n    }\n\n    if (this._wrapMode === \"none\") {\n      if (direction === \"left\") {\n        const newOffsetX = Math.max(0, viewport.offsetX - delta)\n        this.editorView.setViewport(newOffsetX, viewport.offsetY, viewport.width, viewport.height, true)\n        this.requestRender()\n      } else if (direction === \"right\") {\n        const newOffsetX = viewport.offsetX + delta\n        this.editorView.setViewport(newOffsetX, viewport.offsetY, viewport.width, viewport.height, true)\n        this.requestRender()\n      }\n    }\n  }\n\n  protected onResize(width: number, height: number): void {\n    this.editorView.setViewportSize(width, height)\n  }\n\n  protected refreshLocalSelection(): boolean {\n    if (this.lastLocalSelection) {\n      return this.updateLocalSelection(this.lastLocalSelection)\n    }\n    return false\n  }\n\n  private updateLocalSelection(localSelection: LocalSelectionBounds | null): boolean {\n    if (!localSelection?.isActive) {\n      this.editorView.resetLocalSelection()\n      return true\n    }\n    return this.editorView.setLocalSelection(\n      localSelection.anchorX,\n      localSelection.anchorY,\n      localSelection.focusX,\n      localSelection.focusY,\n      this._selectionBg,\n      this._selectionFg,\n      false,\n    )\n  }\n\n  shouldStartSelection(x: number, y: number): boolean {\n    if (!this.selectable) return false\n\n    const localX = x - this.x\n    const localY = y - this.y\n\n    return localX >= 0 && localX < this.width && localY >= 0 && localY < this.height\n  }\n\n  onSelectionChanged(selection: Selection | null): boolean {\n    const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y)\n    this.lastLocalSelection = localSelection\n\n    const updateCursor = true\n    const followCursor = this._keyboardSelectionActive\n\n    let changed: boolean\n    if (!localSelection?.isActive) {\n      this._keyboardSelectionActive = false\n      this.editorView.resetLocalSelection()\n      changed = true\n    } else if (selection?.isStart) {\n      changed = this.editorView.setLocalSelection(\n        localSelection.anchorX,\n        localSelection.anchorY,\n        localSelection.focusX,\n        localSelection.focusY,\n        this._selectionBg,\n        this._selectionFg,\n        updateCursor,\n        followCursor,\n      )\n    } else {\n      changed = this.editorView.updateLocalSelection(\n        localSelection.anchorX,\n        localSelection.anchorY,\n        localSelection.focusX,\n        localSelection.focusY,\n        this._selectionBg,\n        this._selectionFg,\n        updateCursor,\n        followCursor,\n      )\n    }\n\n    if (changed && localSelection?.isActive && selection?.isDragging) {\n      const viewport = this.editorView.getViewport()\n      const focusY = localSelection.focusY\n      const scrollMargin = Math.max(1, Math.floor(viewport.height * this._scrollMargin))\n\n      if (focusY < scrollMargin) {\n        this._autoScrollVelocity = -this._scrollSpeed\n      } else if (focusY >= viewport.height - scrollMargin) {\n        this._autoScrollVelocity = this._scrollSpeed\n      } else {\n        this._autoScrollVelocity = 0\n      }\n    } else {\n      this._keyboardSelectionActive = false\n      this._autoScrollVelocity = 0\n      this._autoScrollAccumulator = 0\n    }\n\n    if (changed) {\n      this.requestRender()\n    }\n\n    return this.hasSelection()\n  }\n\n  protected override onUpdate(deltaTime: number): void {\n    super.onUpdate(deltaTime)\n\n    if (this._autoScrollVelocity !== 0 && this.hasSelection()) {\n      const deltaSeconds = deltaTime / 1000\n      this._autoScrollAccumulator += this._autoScrollVelocity * deltaSeconds\n\n      const linesToScroll = Math.floor(Math.abs(this._autoScrollAccumulator))\n      if (linesToScroll > 0) {\n        const direction = this._autoScrollVelocity > 0 ? 1 : -1\n        const viewport = this.editorView.getViewport()\n        const totalVirtualLines = this.editorView.getTotalVirtualLineCount()\n        const maxOffsetY = Math.max(0, totalVirtualLines - viewport.height)\n        const newOffsetY = Math.max(0, Math.min(viewport.offsetY + direction * linesToScroll, maxOffsetY))\n\n        if (newOffsetY !== viewport.offsetY) {\n          this.editorView.setViewport(viewport.offsetX, newOffsetY, viewport.width, viewport.height, false)\n\n          this._ctx.requestSelectionUpdate()\n        }\n\n        this._autoScrollAccumulator -= direction * linesToScroll\n      }\n    }\n  }\n\n  getSelectedText(): string {\n    return this.editorView.getSelectedText()\n  }\n\n  hasSelection(): boolean {\n    return this.editorView.hasSelection()\n  }\n\n  getSelection(): { start: number; end: number } | null {\n    return this.editorView.getSelection()\n  }\n\n  // Undefined = 0,\n  // Exactly = 1,\n  // AtMost = 2\n  private setupMeasureFunc(): void {\n    const measureFunc = (\n      width: number,\n      widthMode: MeasureMode,\n      height: number,\n      heightMode: MeasureMode,\n    ): { width: number; height: number } => {\n      // When widthMode is Undefined, Yoga is asking for the intrinsic/natural width\n      // Pass width=0 to measureForDimensions to signal we want max-content (no wrapping)\n      // The Zig code treats width=0 with wrap_mode != none as null wrap_width,\n      // which triggers no-wrap mode and returns the text's intrinsic width\n      let effectiveWidth: number\n      if (widthMode === MeasureMode.Undefined || isNaN(width)) {\n        effectiveWidth = 0\n      } else {\n        effectiveWidth = width\n      }\n\n      const effectiveHeight = isNaN(height) ? 1 : height\n\n      const measureResult = this.editorView.measureForDimensions(\n        Math.floor(effectiveWidth),\n        Math.floor(effectiveHeight),\n      )\n\n      const measuredWidth = measureResult ? Math.max(1, measureResult.widthColsMax) : 1\n      const measuredHeight = measureResult ? Math.max(1, measureResult.lineCount) : 1\n\n      if (widthMode === MeasureMode.AtMost && this._positionType !== \"absolute\") {\n        return {\n          width: Math.min(effectiveWidth, measuredWidth),\n          height: Math.min(effectiveHeight, measuredHeight),\n        }\n      }\n\n      return {\n        width: measuredWidth,\n        height: measuredHeight,\n      }\n    }\n\n    this.yogaNode.setMeasureFunc(measureFunc)\n  }\n\n  render(buffer: OptimizedBuffer, deltaTime: number): void {\n    if (!this.visible) return\n    if (this.isDestroyed) return\n\n    this.markClean()\n    this._ctx.addToHitGrid(this.x, this.y, this.width, this.height, this.num)\n\n    this.renderSelf(buffer)\n    this.renderCursor(buffer)\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    buffer.drawEditorView(this.editorView, this.x, this.y)\n  }\n\n  protected renderCursor(buffer: OptimizedBuffer): void {\n    if (!this._showCursor || !this._focused) return\n\n    const visualCursor = this.editorView.getVisualCursor()\n\n    const cursorX = this.x + visualCursor.visualCol + 1 // +1 for 1-based terminal coords\n    const cursorY = this.y + visualCursor.visualRow + 1 // +1 for 1-based terminal coords\n\n    this._ctx.setCursorPosition(cursorX, cursorY, true)\n    this._ctx.setCursorStyle({ ...this._cursorStyle, color: this._cursorColor })\n  }\n\n  public focus(): void {\n    super.focus()\n    this._ctx.setCursorStyle({ ...this._cursorStyle, color: this._cursorColor })\n    this.requestRender()\n  }\n\n  public blur(): void {\n    super.blur()\n    this._ctx.setCursorPosition(0, 0, false)\n    this.requestRender()\n  }\n\n  protected onRemove(): void {\n    if (this._focused) {\n      this._ctx.setCursorPosition(0, 0, false)\n    }\n  }\n\n  override destroy(): void {\n    if (this.isDestroyed) return\n\n    if (this._focused) {\n      this._ctx.setCursorPosition(0, 0, false)\n      // Manually blur to unhook event handlers BEFORE setting destroyed flag\n      // This prevents the guard in super.destroy() from skipping blur()\n      this.blur()\n    }\n\n    // Destroy dependent resources in correct order BEFORE calling super\n    // EditorView depends on EditBuffer, so destroy it first\n    this.editorView.destroy()\n    this.editBuffer.destroy()\n\n    // Finally clean up parent resources\n    // Note: super.destroy() will try to blur() again, but blur() has guards to prevent double-blur\n    super.destroy()\n  }\n\n  public set onCursorChange(handler: ((event: CursorChangeEvent) => void) | undefined) {\n    this._cursorChangeListener = handler\n  }\n\n  public get onCursorChange(): ((event: CursorChangeEvent) => void) | undefined {\n    return this._cursorChangeListener\n  }\n\n  public set onContentChange(handler: ((event: ContentChangeEvent) => void) | undefined) {\n    this._contentChangeListener = handler\n  }\n\n  public get onContentChange(): ((event: ContentChangeEvent) => void) | undefined {\n    return this._contentChangeListener\n  }\n\n  get syntaxStyle(): SyntaxStyle | null {\n    return this.editBuffer.getSyntaxStyle()\n  }\n\n  set syntaxStyle(style: SyntaxStyle | null) {\n    this.editBuffer.setSyntaxStyle(style)\n    this.requestRender()\n  }\n\n  public addHighlight(lineIdx: number, highlight: Highlight): void {\n    this.editBuffer.addHighlight(lineIdx, highlight)\n    this.requestRender()\n  }\n\n  public addHighlightByCharRange(highlight: Highlight): void {\n    this.editBuffer.addHighlightByCharRange(highlight)\n    this.requestRender()\n  }\n\n  public removeHighlightsByRef(hlRef: number): void {\n    this.editBuffer.removeHighlightsByRef(hlRef)\n    this.requestRender()\n  }\n\n  public clearLineHighlights(lineIdx: number): void {\n    this.editBuffer.clearLineHighlights(lineIdx)\n    this.requestRender()\n  }\n\n  public clearAllHighlights(): void {\n    this.editBuffer.clearAllHighlights()\n    this.requestRender()\n  }\n\n  public getLineHighlights(lineIdx: number): Array<Highlight> {\n    return this.editBuffer.getLineHighlights(lineIdx)\n  }\n\n  /**\n   * Set text and completely reset the buffer state (clears history, resets add_buffer).\n   * Use this for initial text setting or when you want a clean slate.\n   */\n  public setText(text: string): void {\n    this.editBuffer.setText(text)\n    this.yogaNode.markDirty()\n    this.requestRender()\n  }\n\n  /**\n   * Replace text while preserving undo history (creates an undo point).\n   * Use this when you want the setText operation to be undoable.\n   */\n  public replaceText(text: string): void {\n    this.editBuffer.replaceText(text)\n    this.yogaNode.markDirty()\n    this.requestRender()\n  }\n\n  public clear(): void {\n    this.editBuffer.clear()\n    this.editBuffer.clearAllHighlights()\n    this.yogaNode.markDirty()\n    this.requestRender()\n  }\n\n  public deleteRange(startLine: number, startCol: number, endLine: number, endCol: number): void {\n    this.editBuffer.deleteRange(startLine, startCol, endLine, endCol)\n    this.yogaNode.markDirty()\n    this.requestRender()\n  }\n\n  public insertText(text: string): void {\n    this.editBuffer.insertText(text)\n    this.yogaNode.markDirty()\n    this.requestRender()\n  }\n\n  public getTextRange(startOffset: number, endOffset: number): string {\n    return this.editBuffer.getTextRange(startOffset, endOffset)\n  }\n\n  public getTextRangeByCoords(startRow: number, startCol: number, endRow: number, endCol: number): string {\n    return this.editBuffer.getTextRangeByCoords(startRow, startCol, endRow, endCol)\n  }\n\n  protected updateSelectionForMovement(shiftPressed: boolean, isBeforeMovement: boolean): void {\n    if (!this.selectable) return\n\n    if (!shiftPressed) {\n      this._keyboardSelectionActive = false\n      this._ctx.clearSelection()\n      return\n    }\n\n    this._keyboardSelectionActive = true\n\n    const visualCursor = this.editorView.getVisualCursor()\n    const cursorX = this.x + visualCursor.visualCol\n    const cursorY = this.y + visualCursor.visualRow\n\n    if (isBeforeMovement) {\n      if (!this._ctx.hasSelection) {\n        this._ctx.startSelection(this, cursorX, cursorY)\n      }\n      return\n    }\n\n    this._ctx.updateSelection(this, cursorX, cursorY, { finishDragging: true })\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/FrameBuffer.ts",
    "content": "import { type RenderableOptions, Renderable } from \"../Renderable.js\"\nimport { OptimizedBuffer } from \"../buffer.js\"\nimport type { RenderContext } from \"../types.js\"\n\nexport interface FrameBufferOptions extends RenderableOptions<FrameBufferRenderable> {\n  width: number\n  height: number\n  respectAlpha?: boolean\n}\n\nexport class FrameBufferRenderable extends Renderable {\n  public frameBuffer: OptimizedBuffer\n  protected respectAlpha: boolean\n\n  constructor(ctx: RenderContext, options: FrameBufferOptions) {\n    super(ctx, options)\n    this.respectAlpha = options.respectAlpha || false\n    this.frameBuffer = OptimizedBuffer.create(options.width, options.height, this._ctx.widthMethod, {\n      respectAlpha: this.respectAlpha,\n      id: options.id || `framebufferrenderable-${this.id}`,\n    })\n  }\n\n  protected onResize(width: number, height: number): void {\n    if (width <= 0 || height <= 0) {\n      throw new Error(`Invalid resize dimensions for FrameBufferRenderable ${this.id}: ${width}x${height}`)\n    }\n\n    this.frameBuffer.resize(width, height)\n    super.onResize(width, height)\n    this.requestRender()\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    if (!this.visible || this.isDestroyed) return\n    buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer)\n  }\n\n  protected destroySelf(): void {\n    // TODO: framebuffer collides with buffered Renderable, which holds a framebuffer\n    // and destroys it if it exists already. Maybe instead of extending FrameBufferRenderable,\n    // subclasses can use the buffered option on the base renderable instead,\n    // then this would become something that takes in an external framebuffer to bring it into layout.\n    this.frameBuffer?.destroy()\n    super.destroySelf()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Input.test.ts",
    "content": "import { describe, expect, it, afterAll, beforeAll } from \"bun:test\"\nimport { InputRenderable, type InputRenderableOptions, InputRenderableEvents } from \"./Input.js\"\nimport { decodePasteBytes } from \"../lib/paste.js\"\nimport { createTestRenderer } from \"../testing/test-renderer.js\"\nimport type { KeyEvent } from \"../lib/KeyHandler.js\"\n\nconst { renderer, mockInput } = await createTestRenderer({})\n\nfunction createInputRenderable(options: InputRenderableOptions): { input: InputRenderable; root: any } {\n  if (!renderer) {\n    throw new Error(\"Renderer not initialized\")\n  }\n\n  const inputRenderable = new InputRenderable(renderer, options)\n  renderer.root.add(inputRenderable)\n  renderer.requestRender()\n\n  return { input: inputRenderable, root: renderer.root }\n}\n\ndescribe(\"InputRenderable\", () => {\n  afterAll(() => {\n    if (renderer) {\n      renderer.destroy()\n    }\n  })\n\n  describe(\"Initialization\", () => {\n    it(\"should initialize properly with default options\", () => {\n      const { input, root } = createInputRenderable({ width: 20, height: 1 })\n\n      expect(input.x).toBeDefined()\n      expect(input.y).toBeDefined()\n      expect(input.width).toBeGreaterThan(0)\n      expect(input.height).toBeGreaterThan(0)\n      expect(input.value).toBe(\"\")\n      expect(input.focusable).toBe(true)\n    })\n\n    it(\"should initialize with custom options\", () => {\n      const { input } = createInputRenderable({\n        value: \"test\",\n        placeholder: \"Enter text\",\n        maxLength: 50,\n      })\n\n      expect(input.value).toBe(\"test\")\n      expect(input.focusable).toBe(true)\n    })\n  })\n\n  describe(\"Focus Management\", () => {\n    it(\"should handle focus and blur correctly\", () => {\n      const { input } = createInputRenderable({\n        value: \"test\",\n      })\n\n      expect(input.focused).toBe(false)\n\n      input.focus()\n      expect(input.focused).toBe(true)\n\n      input.blur()\n      expect(input.focused).toBe(false)\n    })\n\n    it(\"should emit change event on blur if value changed\", () => {\n      const { input } = createInputRenderable({\n        value: \"initial\",\n      })\n\n      let changeEventFired = false\n      let changeValue = \"\"\n\n      input.on(InputRenderableEvents.CHANGE, (value: string) => {\n        changeEventFired = true\n        changeValue = value\n      })\n\n      input.focus()\n      input.value = \"modified\"\n\n      // Change event should not fire during focus\n      expect(changeEventFired).toBe(false)\n\n      input.blur()\n\n      // Change event should fire on blur\n      expect(changeEventFired).toBe(true)\n      expect(changeValue).toBe(\"modified\")\n    })\n\n    it(\"should not emit change event on blur if value unchanged\", () => {\n      const { input } = createInputRenderable({\n        value: \"unchanged\",\n      })\n\n      let changeEventFired = false\n\n      input.on(InputRenderableEvents.CHANGE, () => {\n        changeEventFired = true\n      })\n\n      input.focus()\n      // Value remains the same\n      input.blur()\n\n      expect(changeEventFired).toBe(false)\n    })\n  })\n\n  describe(\"Single Input Key Handling\", () => {\n    it(\"should handle text input when focused\", () => {\n      const { input } = createInputRenderable({ width: 20, height: 1 })\n\n      input.focus()\n\n      let inputEventFired = false\n      let inputValue = \"\"\n\n      input.on(InputRenderableEvents.INPUT, (value: string) => {\n        inputEventFired = true\n        inputValue = value\n      })\n\n      // Simulate typing \"hello\"\n      mockInput.pressKey(\"h\")\n      expect(input.value).toBe(\"h\")\n      expect(inputEventFired).toBe(true)\n      expect(inputValue).toBe(\"h\")\n\n      mockInput.pressKey(\"e\")\n      expect(input.value).toBe(\"he\")\n\n      mockInput.pressKey(\"l\")\n      expect(input.value).toBe(\"hel\")\n\n      mockInput.pressKey(\"l\")\n      expect(input.value).toBe(\"hell\")\n\n      mockInput.pressKey(\"o\")\n      expect(input.value).toBe(\"hello\")\n    })\n\n    it(\"should not handle key events when not focused\", () => {\n      const { input } = createInputRenderable({ width: 20, height: 1 })\n\n      // Don't focus the input\n      expect(input.focused).toBe(false)\n\n      let inputEventFired = false\n\n      input.on(InputRenderableEvents.INPUT, () => {\n        inputEventFired = true\n      })\n\n      // Simulate key event through stdin - should be ignored since not focused\n      mockInput.pressKey(\"a\")\n      expect(input.value).toBe(\"\")\n      expect(inputEventFired).toBe(false)\n    })\n\n    it(\"should handle backspace correctly\", () => {\n      const { input } = createInputRenderable({\n        value: \"hello\",\n      })\n\n      input.focus()\n\n      mockInput.pressBackspace()\n      expect(input.value).toBe(\"hell\")\n\n      mockInput.pressBackspace()\n      expect(input.value).toBe(\"hel\")\n    })\n\n    it(\"should emit INPUT event on Ctrl+W (delete-word-backward)\", () => {\n      const { input } = createInputRenderable({\n        value: \"hello world\",\n      })\n\n      input.focus()\n\n      const inputValues: string[] = []\n      input.on(InputRenderableEvents.INPUT, (value: string) => {\n        inputValues.push(value)\n      })\n\n      // Ctrl+W should delete \"world\" and emit INPUT with updated value\n      mockInput.pressKey(\"w\", { ctrl: true })\n      expect(input.value).toBe(\"hello \")\n      expect(inputValues).toEqual([\"hello \"])\n    })\n\n    it(\"should emit INPUT event on Alt+Backspace (delete-word-backward)\", () => {\n      const { input } = createInputRenderable({\n        value: \"foo bar baz\",\n      })\n\n      input.focus()\n\n      const inputValues: string[] = []\n      input.on(InputRenderableEvents.INPUT, (value: string) => {\n        inputValues.push(value)\n      })\n\n      // Alt+Backspace is also bound to delete-word-backward\n      mockInput.pressBackspace({ meta: true })\n      expect(input.value).toBe(\"foo bar \")\n      expect(inputValues).toEqual([\"foo bar \"])\n    })\n\n    it(\"should emit INPUT event on deleteLine()\", () => {\n      const { input } = createInputRenderable({\n        value: \"hello world\",\n      })\n\n      input.focus()\n\n      const inputValues: string[] = []\n      input.on(InputRenderableEvents.INPUT, (value: string) => {\n        inputValues.push(value)\n      })\n\n      input.deleteLine()\n      expect(input.value).toBe(\"\")\n      expect(inputValues).toEqual([\"\"])\n    })\n\n    it(\"should handle delete correctly\", () => {\n      const { input } = createInputRenderable({\n        value: \"hello\",\n        width: 20,\n        height: 1,\n      })\n\n      input.focus()\n      input.cursorOffset = 1 // Move cursor after 'e'\n\n      mockInput.pressKey(\"DELETE\")\n      expect(input.value).toBe(\"hllo\")\n    })\n\n    it(\"should handle arrow keys for cursor movement\", () => {\n      const { input } = createInputRenderable({\n        value: \"hello\",\n      })\n\n      input.focus()\n      expect(input.cursorOffset).toBe(5) // Should be at end\n\n      mockInput.pressArrow(\"left\")\n      expect(input.cursorOffset).toBe(4)\n\n      mockInput.pressArrow(\"left\")\n      expect(input.cursorOffset).toBe(3)\n\n      mockInput.pressArrow(\"right\")\n      expect(input.cursorOffset).toBe(4)\n\n      mockInput.pressKey(\"HOME\")\n      expect(input.cursorOffset).toBe(0)\n\n      mockInput.pressKey(\"END\")\n      expect(input.cursorOffset).toBe(5)\n    })\n\n    it(\"should handle enter key\", () => {\n      const { input } = createInputRenderable({\n        value: \"test input\",\n      })\n\n      input.focus()\n\n      let enterEventFired = false\n      let enterValue = \"\"\n\n      input.on(InputRenderableEvents.ENTER, (value: string) => {\n        enterEventFired = true\n        enterValue = value\n      })\n\n      mockInput.pressEnter()\n      expect(enterEventFired).toBe(true)\n      expect(enterValue).toBe(\"test input\")\n    })\n\n    it(\"should respect maxLength\", () => {\n      const { input } = createInputRenderable({\n        maxLength: 3,\n      })\n\n      input.focus()\n\n      mockInput.pressKey(\"a\")\n      expect(input.value).toBe(\"a\")\n\n      mockInput.pressKey(\"b\")\n      expect(input.value).toBe(\"ab\")\n\n      mockInput.pressKey(\"c\")\n      expect(input.value).toBe(\"abc\")\n\n      // This should be ignored\n      mockInput.pressKey(\"d\")\n      expect(input.value).toBe(\"abc\")\n    })\n\n    it(\"should handle cursor position with text insertion\", () => {\n      const { input } = createInputRenderable({\n        value: \"hello\",\n      })\n\n      input.focus()\n      input.cursorOffset = 2 // Position after 'l'\n\n      mockInput.pressKey(\"x\")\n      expect(input.value).toBe(\"hexllo\")\n      expect(input.cursorOffset).toBe(3)\n    })\n\n    it(\"should handle onPaste option\", () => {\n      let pasteText = \"\"\n      let pasteCalled = false\n\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        onPaste: (event) => {\n          pasteText = decodePasteBytes(event.bytes)\n          pasteCalled = true\n        },\n      })\n\n      input.focus()\n\n      mockInput.pasteBracketedText(\"pasted text\")\n      // Input now automatically inserts pasted text (using Textarea's EditBuffer)\n      expect(input.value).toBe(\"pasted text\")\n      expect(pasteCalled).toBe(true)\n      expect(pasteText).toBe(\"pasted text\")\n    })\n\n    it(\"should strip ANSI sequences from pasted text before inserting\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n      })\n\n      input.focus()\n\n      mockInput.pasteBracketedText(\"hi \\x1b[31mred\\x1b[0m\")\n\n      expect(input.value).toBe(\"hi red\")\n    })\n  })\n\n  describe(\"Multiple Input Focus Management\", () => {\n    it(\"should allow only one input to be focused at a time\", () => {\n      const { input: input1 } = createInputRenderable({\n        value: \"first\",\n      })\n\n      const { input: input2 } = createInputRenderable({\n        value: \"second\",\n      })\n\n      // Initially neither should be focused\n      expect(input1.focused).toBe(false)\n      expect(input2.focused).toBe(false)\n\n      // Focus first input\n      input1.focus()\n      expect(input1.focused).toBe(true)\n      expect(input2.focused).toBe(false)\n\n      // Focus second input - first should lose focus\n      input2.focus()\n      expect(input1.focused).toBe(false)\n      expect(input2.focused).toBe(true)\n    })\n\n    it(\"should only handle key events for focused input\", () => {\n      const { input: input1 } = createInputRenderable({\n        value: \"first\",\n      })\n\n      const { input: input2 } = createInputRenderable({\n        value: \"second\",\n      })\n\n      let input1EventFired = false\n      let input2EventFired = false\n\n      input1.on(InputRenderableEvents.INPUT, () => {\n        input1EventFired = true\n      })\n\n      input2.on(InputRenderableEvents.INPUT, () => {\n        input2EventFired = true\n      })\n\n      // Focus first input\n      input1.focus()\n\n      // Send key event through stdin - only focused input1 should handle it\n      mockInput.pressKey(\"a\")\n\n      expect(input1EventFired).toBe(true)\n      expect(input2EventFired).toBe(false)\n      expect(input1.value).toBe(\"firsta\")\n      expect(input2.value).toBe(\"second\")\n\n      // Switch focus to input2\n      input2.focus()\n\n      // Reset flags\n      input1EventFired = false\n      input2EventFired = false\n\n      // Send key event through stdin - only focused input2 should handle it\n      mockInput.pressKey(\"b\")\n\n      expect(input1EventFired).toBe(false)\n      expect(input2EventFired).toBe(true)\n      expect(input1.value).toBe(\"firsta\")\n      expect(input2.value).toBe(\"secondb\")\n    })\n\n    it(\"should handle focus switching with blur events\", () => {\n      const { input: input1 } = createInputRenderable({\n        value: \"first\",\n      })\n\n      const { input: input2 } = createInputRenderable({\n        value: \"second\",\n      })\n\n      let input1ChangeFired = false\n      let input2ChangeFired = false\n\n      input1.on(InputRenderableEvents.CHANGE, () => {\n        input1ChangeFired = true\n      })\n\n      input2.on(InputRenderableEvents.CHANGE, () => {\n        input2ChangeFired = true\n      })\n\n      // Focus input1 and modify value\n      input1.focus()\n      mockInput.pressKey(\"x\")\n\n      // Switch to input2 - should trigger change event for input1\n      input2.focus()\n\n      expect(input1ChangeFired).toBe(true)\n      expect(input2ChangeFired).toBe(false)\n      expect(input1.focused).toBe(false)\n      expect(input2.focused).toBe(true)\n    })\n\n    it(\"should handle rapid focus switching\", () => {\n      const { input: input1 } = createInputRenderable({\n        value: \"first\",\n      })\n\n      const { input: input2 } = createInputRenderable({\n        value: \"second\",\n      })\n\n      const { input: input3 } = createInputRenderable({\n        value: \"third\",\n      })\n\n      // Rapid focus switching\n      input1.focus()\n      expect(input1.focused).toBe(true)\n      expect(input2.focused).toBe(false)\n      expect(input3.focused).toBe(false)\n\n      input2.focus()\n      expect(input1.focused).toBe(false)\n      expect(input2.focused).toBe(true)\n      expect(input3.focused).toBe(false)\n\n      input3.focus()\n      expect(input1.focused).toBe(false)\n      expect(input2.focused).toBe(false)\n      expect(input3.focused).toBe(true)\n\n      input1.focus()\n      expect(input1.focused).toBe(true)\n      expect(input2.focused).toBe(false)\n      expect(input3.focused).toBe(false)\n    })\n\n    it(\"should prevent multiple inputs from being focused simultaneously\", () => {\n      const { input: input1 } = createInputRenderable({\n        value: \"first\",\n      })\n\n      const { input: input2 } = createInputRenderable({\n        value: \"second\",\n      })\n\n      const { input: input3 } = createInputRenderable({\n        value: \"third\",\n      })\n\n      // Focus all three in sequence\n      input1.focus()\n      input2.focus()\n      input3.focus()\n\n      // Only the last focused input should be focused\n      expect(input1.focused).toBe(false)\n      expect(input2.focused).toBe(false)\n      expect(input3.focused).toBe(true)\n\n      // Focus input1 again\n      input1.focus()\n\n      expect(input1.focused).toBe(true)\n      expect(input2.focused).toBe(false)\n      expect(input3.focused).toBe(false)\n    })\n  })\n\n  describe(\"Input Value Management\", () => {\n    it(\"should handle value setting programmatically\", () => {\n      const { input } = createInputRenderable({ width: 20, height: 1 })\n\n      input.value = \"programmatic\"\n      expect(input.value).toBe(\"programmatic\")\n\n      // Cursor position should move to end when value is set programmatically\n      expect(input.cursorOffset).toBe(\"programmatic\".length)\n    })\n\n    it(\"should handle value changes with cursor moving to end\", () => {\n      const { input } = createInputRenderable({\n        value: \"hello\",\n      })\n\n      input.focus()\n      input.cursorOffset = 2\n\n      input.value = \"world\"\n      expect(input.value).toBe(\"world\")\n      expect(input.cursorOffset).toBe(\"world\".length) // Cursor should move to end\n    })\n\n    it(\"should handle empty value setting\", () => {\n      const { input } = createInputRenderable({\n        value: \"not empty\",\n      })\n\n      input.value = \"\"\n      expect(input.value).toBe(\"\")\n      expect(input.cursorOffset).toBe(0)\n    })\n\n    it(\"should emit input events when value changes programmatically\", () => {\n      const { input } = createInputRenderable({ width: 20, height: 1 })\n\n      let inputEventFired = false\n      let inputValue = \"\"\n\n      input.on(InputRenderableEvents.INPUT, (value: string) => {\n        inputEventFired = true\n        inputValue = value\n      })\n\n      input.value = \"changed\"\n\n      expect(inputEventFired).toBe(true)\n      expect(inputValue).toBe(\"changed\")\n    })\n  })\n\n  describe(\"Input Properties\", () => {\n    it(\"should handle maxLength changes\", () => {\n      const { input } = createInputRenderable({\n        value: \"verylongtext\",\n        maxLength: 20,\n      })\n\n      expect(input.value).toBe(\"verylongtext\")\n\n      // Reduce maxLength - should truncate existing value\n      input.maxLength = 5\n      expect(input.value).toBe(\"veryl\")\n    })\n\n    it(\"should handle placeholder changes\", () => {\n      const { input } = createInputRenderable({\n        placeholder: \"old placeholder\",\n      })\n\n      input.placeholder = \"new placeholder\"\n      // Placeholder change should trigger render request\n      expect(input).toBeDefined()\n    })\n\n    it(\"should handle color property changes\", () => {\n      const { input } = createInputRenderable({ width: 20, height: 1 })\n\n      input.backgroundColor = \"#ff0000\"\n      input.textColor = \"#00ff00\"\n      input.focusedBackgroundColor = \"#0000ff\"\n      input.focusedTextColor = \"#ffff00\"\n      input.placeholderColor = \"#ff00ff\"\n      input.cursorColor = \"#00ffff\"\n\n      // Color changes should trigger render requests\n      expect(input).toBeDefined()\n    })\n  })\n\n  describe(\"Global Key Event Prevention\", () => {\n    it(\"should not handle key events when preventDefault is called by global handler\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"initial\",\n      })\n\n      let globalHandlerCalled = false\n      let inputEventFired = false\n\n      // Register global handler that prevents 'a' key\n      renderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n        globalHandlerCalled = true\n        if (key.name === \"a\") {\n          key.preventDefault()\n        }\n      })\n\n      input.on(InputRenderableEvents.INPUT, () => {\n        inputEventFired = true\n      })\n\n      input.focus()\n      expect(input.focused).toBe(true)\n\n      // Press 'a' - should be prevented\n      mockInput.pressKey(\"a\")\n      expect(globalHandlerCalled).toBe(true)\n      expect(inputEventFired).toBe(false)\n      expect(input.value).toBe(\"initial\") // Value should not change\n\n      // Reset flags\n      globalHandlerCalled = false\n      inputEventFired = false\n\n      // Press 'b' - should not be prevented\n      mockInput.pressKey(\"b\")\n      expect(globalHandlerCalled).toBe(true)\n      expect(inputEventFired).toBe(true)\n      expect(input.value).toBe(\"initialb\") // Value should change\n\n      // Clean up\n      renderer.keyInput.removeAllListeners(\"keypress\")\n    })\n\n    it(\"should handle multiple global handlers with preventDefault\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n      })\n\n      let firstHandlerCalled = false\n      let secondHandlerCalled = false\n      let inputEventFired = false\n\n      // First global handler prevents 'x'\n      const firstHandler = (key: KeyEvent) => {\n        firstHandlerCalled = true\n        if (key.name === \"x\") {\n          key.preventDefault()\n        }\n      }\n\n      // Second global handler should not run for 'x' if first prevents it\n      const secondHandler = (key: KeyEvent) => {\n        secondHandlerCalled = true\n      }\n\n      renderer.keyInput.on(\"keypress\", firstHandler)\n      renderer.keyInput.on(\"keypress\", secondHandler)\n\n      input.on(InputRenderableEvents.INPUT, () => {\n        inputEventFired = true\n      })\n\n      input.focus()\n\n      // Press 'x' - should be prevented by first handler\n      mockInput.pressKey(\"x\")\n      expect(firstHandlerCalled).toBe(true)\n      expect(secondHandlerCalled).toBe(true) // EventEmitter still calls all handlers\n      expect(inputEventFired).toBe(false) // But input should not process it\n      expect(input.value).toBe(\"\")\n\n      // Clean up\n      renderer.keyInput.removeListener(\"keypress\", firstHandler)\n      renderer.keyInput.removeListener(\"keypress\", secondHandler)\n    })\n\n    it(\"should respect preventDefault from global handler registered AFTER input focus\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"initial\",\n      })\n\n      let globalHandlerCalled = false\n      let inputEventFired = false\n\n      input.on(InputRenderableEvents.INPUT, () => {\n        inputEventFired = true\n      })\n\n      // Focus the input FIRST\n      input.focus()\n      expect(input.focused).toBe(true)\n\n      // Type 'a' before global handler exists - should work\n      mockInput.pressKey(\"a\")\n      expect(input.value).toBe(\"initiala\")\n      expect(inputEventFired).toBe(true)\n\n      // Reset flag\n      inputEventFired = false\n\n      // NOW register a global handler that prevents 'b' key\n      const globalHandler = (key: KeyEvent) => {\n        globalHandlerCalled = true\n        if (key.name === \"b\") {\n          key.preventDefault()\n        }\n      }\n      renderer.keyInput.on(\"keypress\", globalHandler)\n\n      // Press 'b' - should be prevented even though handler was added after focus\n      mockInput.pressKey(\"b\")\n      expect(globalHandlerCalled).toBe(true)\n      expect(inputEventFired).toBe(false)\n      expect(input.value).toBe(\"initiala\") // Value should not change\n\n      // Reset flags\n      globalHandlerCalled = false\n      inputEventFired = false\n\n      // Press 'c' - should not be prevented\n      mockInput.pressKey(\"c\")\n      expect(globalHandlerCalled).toBe(true)\n      expect(inputEventFired).toBe(true)\n      expect(input.value).toBe(\"initialac\") // Value should change\n\n      // Clean up\n      renderer.keyInput.removeListener(\"keypress\", globalHandler)\n    })\n\n    it(\"should handle dynamic preventDefault conditions\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"\",\n      })\n\n      let preventNumbers = false\n      let inputEventFired = false\n\n      // Register handler that can dynamically change what it prevents\n      const dynamicHandler = (key: KeyEvent) => {\n        if (preventNumbers && /^[0-9]$/.test(key.name)) {\n          key.preventDefault()\n        }\n      }\n\n      renderer.keyInput.on(\"keypress\", dynamicHandler)\n\n      input.on(InputRenderableEvents.INPUT, () => {\n        inputEventFired = true\n      })\n\n      input.focus()\n\n      // Initially allow numbers\n      mockInput.pressKey(\"1\")\n      expect(input.value).toBe(\"1\")\n      expect(inputEventFired).toBe(true)\n\n      // Enable number prevention\n      preventNumbers = true\n      inputEventFired = false\n\n      // Now numbers should be prevented\n      mockInput.pressKey(\"2\")\n      expect(input.value).toBe(\"1\") // Should not change\n      expect(inputEventFired).toBe(false)\n\n      // Letters should still work\n      inputEventFired = false\n      mockInput.pressKey(\"a\")\n      expect(input.value).toBe(\"1a\")\n      expect(inputEventFired).toBe(true)\n\n      // Disable prevention again\n      preventNumbers = false\n      inputEventFired = false\n\n      // Numbers should work again\n      mockInput.pressKey(\"3\")\n      expect(input.value).toBe(\"1a3\")\n      expect(inputEventFired).toBe(true)\n\n      // Clean up\n      renderer.keyInput.removeListener(\"keypress\", dynamicHandler)\n    })\n  })\n\n  it(\"should respect preventDefault from onKeyDown handler\", () => {\n    const { input } = createInputRenderable({\n      width: 20,\n      height: 1,\n      value: \"initial\",\n    })\n\n    let onKeyDownCalled = false\n    let inputEventFired = false\n\n    input.onKeyDown = (key: KeyEvent) => {\n      onKeyDownCalled = true\n      if (key.name === \"a\") {\n        key.preventDefault()\n      }\n    }\n\n    input.on(InputRenderableEvents.INPUT, () => {\n      inputEventFired = true\n    })\n\n    input.focus()\n\n    mockInput.pressKey(\"a\")\n    expect(onKeyDownCalled).toBe(true)\n    expect(inputEventFired).toBe(false)\n    expect(input.value).toBe(\"initial\")\n\n    onKeyDownCalled = false\n    inputEventFired = false\n\n    mockInput.pressKey(\"b\")\n    expect(onKeyDownCalled).toBe(true)\n    expect(inputEventFired).toBe(true)\n    expect(input.value).toBe(\"initialb\")\n  })\n\n  describe(\"Shift+Space Key Handling with modifyOtherKeys\", () => {\n    let modRenderer: any\n    let modMockInput: any\n\n    beforeAll(async () => {\n      const result = await createTestRenderer({ otherModifiersMode: true })\n      modRenderer = result.renderer\n      modMockInput = result.mockInput\n    })\n\n    afterAll(() => {\n      if (modRenderer) {\n        modRenderer.destroy()\n      }\n    })\n\n    function createInputRenderableForMod(options: Partial<InputRenderableOptions>): {\n      input: InputRenderable\n      root: any\n    } {\n      const inputRenderable = new InputRenderable(modRenderer, {\n        width: 20,\n        height: 1,\n        ...options,\n      })\n      modRenderer.root.add(inputRenderable)\n      modRenderer.requestRender()\n\n      return { input: inputRenderable, root: modRenderer.root }\n    }\n\n    it(\"should insert a space when shift+space is pressed\", () => {\n      const { input } = createInputRenderableForMod({ value: \"\" })\n\n      input.focus()\n\n      // Type \"hello\"\n      modMockInput.pressKey(\"h\")\n      modMockInput.pressKey(\"e\")\n      modMockInput.pressKey(\"l\")\n      modMockInput.pressKey(\"l\")\n      modMockInput.pressKey(\"o\")\n      expect(input.value).toBe(\"hello\")\n\n      // Press shift+space - should insert a space\n      modMockInput.pressKey(\" \", { shift: true })\n      expect(input.value).toBe(\"hello \")\n      expect(input.cursorOffset).toBe(6)\n\n      // Type \"world\"\n      modMockInput.pressKey(\"w\")\n      modMockInput.pressKey(\"o\")\n      modMockInput.pressKey(\"r\")\n      modMockInput.pressKey(\"l\")\n      modMockInput.pressKey(\"d\")\n      expect(input.value).toBe(\"hello world\")\n    })\n\n    it(\"should insert multiple spaces with shift+space\", () => {\n      const { input } = createInputRenderableForMod({ value: \"test\" })\n\n      input.focus()\n\n      modMockInput.pressKey(\" \", { shift: true })\n      modMockInput.pressKey(\" \", { shift: true })\n      modMockInput.pressKey(\" \", { shift: true })\n\n      expect(input.value).toBe(\"test   \")\n      expect(input.cursorOffset).toBe(7)\n    })\n\n    it(\"should insert space at middle of text with shift+space\", () => {\n      const { input } = createInputRenderableForMod({ value: \"helloworld\" })\n\n      input.focus()\n      input.cursorOffset = 5\n\n      modMockInput.pressKey(\" \", { shift: true })\n\n      expect(input.value).toBe(\"hello world\")\n      expect(input.cursorOffset).toBe(6)\n    })\n  })\n\n  describe(\"Edge Cases\", () => {\n    it(\"should handle non-printable characters\", () => {\n      const { input } = createInputRenderable({ width: 20, height: 1 })\n\n      input.focus()\n\n      // Non-printable character should be ignored\n      mockInput.pressTab()\n      expect(input.value).toBe(\"\")\n\n      // Control character should be ignored\n      mockInput.pressKey(\"a\", { ctrl: true })\n      expect(input.value).toBe(\"\")\n    })\n\n    it(\"should handle cursor movement at boundaries\", () => {\n      const { input } = createInputRenderable({\n        value: \"hi\",\n      })\n\n      input.focus()\n\n      // Move cursor to start\n      input.cursorOffset = 0\n      mockInput.pressArrow(\"left\")\n      expect(input.cursorOffset).toBe(0) // Should not go below 0\n\n      // Move cursor to end\n      input.cursorOffset = 2\n      mockInput.pressArrow(\"right\")\n      expect(input.cursorOffset).toBe(2) // Should not go beyond length\n    })\n\n    it(\"should handle backspace at start of input\", () => {\n      const { input } = createInputRenderable({\n        value: \"hi\",\n      })\n\n      input.focus()\n      input.cursorOffset = 0\n\n      // Backspace at start should do nothing\n      mockInput.pressBackspace()\n      expect(input.value).toBe(\"hi\")\n      expect(input.cursorOffset).toBe(0)\n    })\n\n    it(\"should handle delete at end of input\", () => {\n      const { input } = createInputRenderable({\n        value: \"hi\",\n      })\n\n      input.focus()\n      input.cursorOffset = 2\n\n      // Delete at end should do nothing\n      mockInput.pressKey(\"DELETE\")\n      expect(input.value).toBe(\"hi\")\n      expect(input.cursorOffset).toBe(2)\n    })\n\n    it(\"should handle empty input operations\", () => {\n      const { input } = createInputRenderable({\n        value: \"\",\n      })\n\n      input.focus()\n\n      // Operations on empty input should be safe\n      mockInput.pressBackspace()\n      expect(input.value).toBe(\"\")\n      expect(input.cursorOffset).toBe(0)\n\n      mockInput.pressKey(\"DELETE\")\n      expect(input.value).toBe(\"\")\n      expect(input.cursorOffset).toBe(0)\n\n      mockInput.pressArrow(\"left\")\n      expect(input.cursorOffset).toBe(0)\n\n      mockInput.pressArrow(\"right\")\n      expect(input.cursorOffset).toBe(0)\n    })\n  })\n\n  describe(\"Key Bindings and Aliases\", () => {\n    it(\"should support custom key bindings\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"hello\",\n        keyBindings: [\n          { name: \"k\", ctrl: true, action: \"line-end\" },\n          { name: \"h\", ctrl: true, action: \"backspace\" },\n        ],\n      })\n\n      input.focus()\n      input.cursorOffset = 3\n\n      // Ctrl+K should move to end (custom binding)\n      mockInput.pressKey(\"k\", { ctrl: true })\n      expect(input.cursorOffset).toBe(5)\n\n      // Ctrl+H should delete backward (custom binding)\n      mockInput.pressKey(\"h\", { ctrl: true })\n      expect(input.value).toBe(\"hell\")\n    })\n\n    it(\"should support key aliases\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        keyAliasMap: {\n          enter: \"return\",\n        },\n      })\n\n      input.focus()\n      input.value = \"test\"\n\n      let enterEventFired = false\n      input.on(InputRenderableEvents.ENTER, () => {\n        enterEventFired = true\n      })\n\n      // \"enter\" should be aliased to \"return\"\n      mockInput.pressEnter()\n      expect(enterEventFired).toBe(true)\n    })\n\n    it(\"should merge custom bindings with defaults\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"hello\",\n        keyBindings: [{ name: \"x\", ctrl: true, action: \"line-home\" }],\n      })\n\n      input.focus()\n\n      // Default binding should still work\n      mockInput.pressArrow(\"left\")\n      expect(input.cursorOffset).toBe(4)\n\n      // Custom binding should also work\n      mockInput.pressKey(\"x\", { ctrl: true })\n      expect(input.cursorOffset).toBe(0)\n    })\n\n    it(\"should override default bindings with custom ones\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"hello\",\n        keyBindings: [\n          { name: \"left\", action: \"line-end\" }, // Override left to move to end\n        ],\n      })\n\n      input.focus()\n      input.cursorOffset = 2\n\n      // Left should now move to end instead of left\n      mockInput.pressArrow(\"left\")\n      expect(input.cursorOffset).toBe(5)\n    })\n\n    it(\"should support Emacs-style bindings by default\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"hello\",\n      })\n\n      input.focus()\n\n      // Ctrl+A should move to home\n      mockInput.pressKey(\"a\", { ctrl: true })\n      expect(input.cursorOffset).toBe(0)\n\n      // Ctrl+E should move to end\n      mockInput.pressKey(\"e\", { ctrl: true })\n      expect(input.cursorOffset).toBe(5)\n\n      // Ctrl+F should move right\n      mockInput.pressKey(\"f\", { ctrl: true })\n      expect(input.cursorOffset).toBe(5) // Can't go beyond end\n\n      input.cursorOffset = 2\n      mockInput.pressKey(\"f\", { ctrl: true })\n      expect(input.cursorOffset).toBe(3)\n\n      // Ctrl+B should move left\n      mockInput.pressKey(\"b\", { ctrl: true })\n      expect(input.cursorOffset).toBe(2)\n\n      // Ctrl+D should delete forward\n      mockInput.pressKey(\"d\", { ctrl: true })\n      expect(input.value).toBe(\"helo\")\n    })\n\n    it(\"should allow updating key bindings dynamically\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"hello\",\n      })\n\n      input.focus()\n      input.cursorOffset = 0\n\n      // Default behavior: left arrow moves left\n      mockInput.pressArrow(\"right\")\n      expect(input.cursorOffset).toBe(1)\n\n      // Update bindings\n      input.keyBindings = [\n        { name: \"right\", action: \"line-end\" }, // Override right to move to end\n      ]\n\n      // Right should now move to end\n      mockInput.pressArrow(\"right\")\n      expect(input.cursorOffset).toBe(5)\n    })\n\n    it(\"should allow updating key aliases dynamically\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n      })\n\n      input.focus()\n\n      // Add custom alias\n      input.keyAliasMap = {\n        ret: \"return\",\n      }\n\n      let enterEventFired = false\n      input.on(InputRenderableEvents.ENTER, () => {\n        enterEventFired = true\n      })\n\n      // The alias should work (if we could send \"ret\" key)\n      mockInput.pressEnter()\n      expect(enterEventFired).toBe(true)\n    })\n\n    it(\"should handle modifiers in custom bindings\", () => {\n      const { input } = createInputRenderable({\n        width: 20,\n        height: 1,\n        value: \"hello\",\n        keyBindings: [\n          { name: \"left\", shift: true, action: \"line-home\" },\n          { name: \"right\", shift: true, action: \"line-end\" },\n          { name: \"up\", ctrl: true, action: \"line-home\" },\n          { name: \"down\", ctrl: true, action: \"line-end\" },\n        ],\n      })\n\n      input.focus()\n      input.cursorOffset = 2\n\n      // Shift+Left should move to home\n      mockInput.pressArrow(\"left\", { shift: true })\n      expect(input.cursorOffset).toBe(0)\n\n      // Shift+Right should move to end\n      mockInput.pressArrow(\"right\", { shift: true })\n      expect(input.cursorOffset).toBe(5)\n\n      // Ctrl+Up should move to home\n      input.cursorOffset = 3\n      mockInput.pressArrow(\"up\", { ctrl: true })\n      expect(input.cursorOffset).toBe(0)\n\n      // Ctrl+Down should move to end\n      mockInput.pressArrow(\"down\", { ctrl: true })\n      expect(input.cursorOffset).toBe(5)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Input.ts",
    "content": "import type { PasteEvent } from \"../lib/KeyHandler.js\"\nimport { decodePasteBytes, stripAnsiSequences } from \"../lib/paste.js\"\nimport type { RenderContext } from \"../types.js\"\nimport {\n  TextareaRenderable,\n  type TextareaOptions,\n  type TextareaAction,\n  type KeyBinding as TextareaKeyBinding,\n} from \"./Textarea.js\"\n\nexport type InputAction = TextareaAction\nexport type InputKeyBinding = TextareaKeyBinding\n\nexport interface InputRenderableOptions\n  extends Omit<TextareaOptions, \"height\" | \"minHeight\" | \"maxHeight\" | \"initialValue\"> {\n  /** Initial text value (newlines are stripped) */\n  value?: string\n  /** Maximum number of characters allowed */\n  maxLength?: number\n  /** Placeholder text (Input only supports string, not StyledText) */\n  placeholder?: string\n}\n\n// TODO: make this just plain strings instead of an enum (same for other events)\nexport enum InputRenderableEvents {\n  INPUT = \"input\",\n  CHANGE = \"change\",\n  ENTER = \"enter\",\n}\n\n/**\n * InputRenderable - A single-line text input component.\n *\n * Extends TextareaRenderable with single-line constraints:\n * - Height is always 1\n * - No text wrapping\n * - Newlines are stripped from input\n * - Enter key submits instead of inserting newline\n *\n * Inherits all keybindings from TextareaRenderable.\n */\nexport class InputRenderable extends TextareaRenderable {\n  private _maxLength: number\n  private _lastCommittedValue: string = \"\"\n\n  // Only specify defaults that differ from TextareaRenderable/EditBufferRenderable\n  private static readonly defaultOptions = {\n    // Different from Textarea's null\n    placeholder: \"\",\n    // Input-specific\n    maxLength: 1000,\n    value: \"\",\n  } satisfies Partial<InputRenderableOptions>\n\n  constructor(ctx: RenderContext, options: InputRenderableOptions) {\n    const defaults = InputRenderable.defaultOptions\n    const maxLength = options.maxLength ?? defaults.maxLength\n    // Sanitize initial value: strip newlines and enforce maxLength\n    const rawValue = options.value ?? defaults.value\n    const initialValue = rawValue.replace(/[\\n\\r]/g, \"\").substring(0, maxLength)\n\n    super(ctx, {\n      ...options,\n      placeholder: options.placeholder ?? defaults.placeholder,\n      initialValue,\n      // Single-line constraints\n      height: 1,\n      wrapMode: \"none\",\n      // Override return/linefeed to submit instead of newline\n      keyBindings: [\n        { name: \"return\", action: \"submit\" },\n        { name: \"linefeed\", action: \"submit\" },\n        ...(options.keyBindings || []),\n      ],\n    })\n\n    this._maxLength = maxLength\n    this._lastCommittedValue = this.plainText\n\n    // Set cursor to end of initial value\n    if (initialValue) {\n      this.cursorOffset = initialValue.length\n    }\n  }\n\n  /**\n   * Prevent newlines in single-line input\n   */\n  public override newLine(): boolean {\n    return false\n  }\n\n  /**\n   * Handle paste - strip newlines and enforce maxLength\n   */\n  public override handlePaste(event: PasteEvent): void {\n    const sanitized = stripAnsiSequences(decodePasteBytes(event.bytes)).replace(/[\\n\\r]/g, \"\")\n    if (sanitized) {\n      this.insertText(sanitized)\n    }\n  }\n\n  /**\n   * Insert text - strip newlines and enforce maxLength\n   */\n  public override insertText(text: string): void {\n    const sanitized = text.replace(/[\\n\\r]/g, \"\")\n    if (!sanitized) return\n\n    const currentLength = this.plainText.length\n    const remaining = this._maxLength - currentLength\n    if (remaining <= 0) return\n\n    const toInsert = sanitized.substring(0, remaining)\n    super.insertText(toInsert)\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n  }\n\n  public get value(): string {\n    return this.plainText\n  }\n\n  public set value(value: string) {\n    const newValue = value.substring(0, this._maxLength).replace(/[\\n\\r]/g, \"\")\n    const currentValue = this.plainText\n    if (currentValue !== newValue) {\n      this.setText(newValue)\n      this.cursorOffset = newValue.length\n      this.emit(InputRenderableEvents.INPUT, newValue)\n    }\n  }\n\n  public override focus(): void {\n    super.focus()\n    this._lastCommittedValue = this.plainText\n  }\n\n  public override blur(): void {\n    if (!this.isDestroyed) {\n      const currentValue = this.plainText\n      if (currentValue !== this._lastCommittedValue) {\n        this._lastCommittedValue = currentValue\n        this.emit(InputRenderableEvents.CHANGE, currentValue)\n      }\n    }\n    super.blur()\n  }\n\n  public override submit(): boolean {\n    const currentValue = this.plainText\n    if (currentValue !== this._lastCommittedValue) {\n      this._lastCommittedValue = currentValue\n      this.emit(InputRenderableEvents.CHANGE, currentValue)\n    }\n    this.emit(InputRenderableEvents.ENTER, currentValue)\n    return true\n  }\n\n  public override deleteCharBackward(): boolean {\n    const result = super.deleteCharBackward()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public override deleteChar(): boolean {\n    const result = super.deleteChar()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public override deleteLine(): boolean {\n    const result = super.deleteLine()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public override deleteWordBackward(): boolean {\n    const result = super.deleteWordBackward()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public override deleteWordForward(): boolean {\n    const result = super.deleteWordForward()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public override deleteToLineStart(): boolean {\n    const result = super.deleteToLineStart()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public override deleteToLineEnd(): boolean {\n    const result = super.deleteToLineEnd()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public override undo(): boolean {\n    const result = super.undo()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public override redo(): boolean {\n    const result = super.redo()\n    this.emit(InputRenderableEvents.INPUT, this.plainText)\n    return result\n  }\n\n  public deleteCharacter(direction: \"backward\" | \"forward\"): void {\n    if (direction === \"backward\") {\n      this.deleteCharBackward()\n    } else {\n      this.deleteChar()\n    }\n  }\n\n  public set maxLength(maxLength: number) {\n    this._maxLength = maxLength\n    const currentValue = this.plainText\n    if (currentValue.length > maxLength) {\n      this.setText(currentValue.substring(0, maxLength))\n    }\n  }\n\n  public get maxLength(): number {\n    return this._maxLength\n  }\n\n  public override set placeholder(placeholder: string) {\n    super.placeholder = placeholder\n  }\n\n  public override get placeholder(): string {\n    const p = super.placeholder\n    return typeof p === \"string\" ? p : \"\"\n  }\n\n  public override set initialValue(value: string) {\n    void 0\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/LineNumberRenderable.ts",
    "content": "import { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { OptimizedBuffer } from \"../buffer.js\"\nimport type { RenderContext, LineInfoProvider } from \"../types.js\"\nimport { RGBA, parseColor } from \"../lib/RGBA.js\"\nimport { MeasureMode } from \"yoga-layout\"\n\nexport interface LineSign {\n  before?: string\n  beforeColor?: string | RGBA\n  after?: string\n  afterColor?: string | RGBA\n}\n\nexport interface LineColorConfig {\n  gutter?: string | RGBA\n  content?: string | RGBA\n}\n\nexport interface LineNumberOptions extends RenderableOptions<LineNumberRenderable> {\n  target?: Renderable & LineInfoProvider\n  fg?: string | RGBA\n  bg?: string | RGBA\n  minWidth?: number\n  paddingRight?: number\n  lineColors?: Map<number, string | RGBA | LineColorConfig>\n  lineSigns?: Map<number, LineSign>\n  lineNumberOffset?: number\n  hideLineNumbers?: Set<number>\n  lineNumbers?: Map<number, number>\n  showLineNumbers?: boolean\n}\n\nclass GutterRenderable extends Renderable {\n  private target: Renderable & LineInfoProvider\n  private _fg: RGBA\n  private _bg: RGBA\n  private _minWidth: number\n  private _paddingRight: number\n  private _lineColorsGutter: Map<number, RGBA>\n  private _lineColorsContent: Map<number, RGBA>\n  private _lineSigns: Map<number, LineSign>\n  private _lineNumberOffset: number\n  private _hideLineNumbers: Set<number>\n  private _lineNumbers: Map<number, number>\n  private _maxBeforeWidth: number = 0\n  private _maxAfterWidth: number = 0\n  private _lastKnownLineCount: number = 0\n  private _lastKnownScrollY: number = 0\n\n  constructor(\n    ctx: RenderContext,\n    target: Renderable & LineInfoProvider,\n    options: {\n      fg: RGBA\n      bg: RGBA\n      minWidth: number\n      paddingRight: number\n      lineColorsGutter: Map<number, RGBA>\n      lineColorsContent: Map<number, RGBA>\n      lineSigns: Map<number, LineSign>\n      lineNumberOffset: number\n      hideLineNumbers: Set<number>\n      lineNumbers?: Map<number, number>\n      id?: string\n      buffered?: boolean\n    },\n  ) {\n    super(ctx, {\n      id: options.id,\n      width: \"auto\",\n      height: \"auto\",\n      flexGrow: 0,\n      flexShrink: 0,\n      buffered: options.buffered,\n    })\n    this.target = target\n    this._fg = options.fg\n    this._bg = options.bg\n    this._minWidth = options.minWidth\n    this._paddingRight = options.paddingRight\n    this._lineColorsGutter = options.lineColorsGutter\n    this._lineColorsContent = options.lineColorsContent\n    this._lineSigns = options.lineSigns\n    this._lineNumberOffset = options.lineNumberOffset\n    this._hideLineNumbers = options.hideLineNumbers\n    this._lineNumbers = options.lineNumbers ?? new Map()\n    this._lastKnownLineCount = this.target.virtualLineCount\n    this._lastKnownScrollY = this.target.scrollY\n    this.calculateSignWidths()\n    this.setupMeasureFunc()\n\n    // Use lifecycle pass to detect line count changes BEFORE layout\n    this.onLifecyclePass = () => {\n      const currentLineCount = this.target.virtualLineCount\n      if (currentLineCount !== this._lastKnownLineCount) {\n        this._lastKnownLineCount = currentLineCount\n        this.yogaNode.markDirty()\n        this.requestRender()\n      }\n    }\n  }\n\n  private setupMeasureFunc(): void {\n    const measureFunc = (\n      width: number,\n      widthMode: MeasureMode,\n      height: number,\n      heightMode: MeasureMode,\n    ): { width: number; height: number } => {\n      // Calculate the gutter width based on the target's line count\n      const gutterWidth = this.calculateWidth()\n\n      // Calculate gutter height based on target's actual virtual line count\n      // The gutter should match the height of the content it's numbering\n      const gutterHeight = this.target.virtualLineCount\n\n      // Return calculated dimensions based on content, not parent constraints\n      return {\n        width: gutterWidth,\n        height: gutterHeight,\n      }\n    }\n\n    this.yogaNode.setMeasureFunc(measureFunc)\n  }\n\n  public remeasure(): void {\n    // Mark the yoga node as dirty to trigger re-measurement\n    this.yogaNode.markDirty()\n  }\n\n  public setLineNumberOffset(offset: number): void {\n    if (this._lineNumberOffset !== offset) {\n      this._lineNumberOffset = offset\n      this.yogaNode.markDirty()\n      this.requestRender()\n    }\n  }\n\n  public setHideLineNumbers(hideLineNumbers: Set<number>): void {\n    this._hideLineNumbers = hideLineNumbers\n    this.yogaNode.markDirty()\n    this.requestRender()\n  }\n\n  public setLineNumbers(lineNumbers: Map<number, number>): void {\n    this._lineNumbers = lineNumbers\n    this.yogaNode.markDirty()\n    this.requestRender()\n  }\n\n  private calculateSignWidths(): void {\n    this._maxBeforeWidth = 0\n    this._maxAfterWidth = 0\n\n    for (const sign of this._lineSigns.values()) {\n      if (sign.before) {\n        const width = Bun.stringWidth(sign.before)\n        this._maxBeforeWidth = Math.max(this._maxBeforeWidth, width)\n      }\n      if (sign.after) {\n        const width = Bun.stringWidth(sign.after)\n        this._maxAfterWidth = Math.max(this._maxAfterWidth, width)\n      }\n    }\n  }\n\n  private calculateWidth(): number {\n    const totalLines = this.target.virtualLineCount\n\n    // Find max line number, considering both calculated and custom line numbers\n    let maxLineNumber = totalLines + this._lineNumberOffset\n    if (this._lineNumbers.size > 0) {\n      for (const customLineNum of this._lineNumbers.values()) {\n        maxLineNumber = Math.max(maxLineNumber, customLineNum)\n      }\n    }\n\n    const digits = maxLineNumber > 0 ? Math.floor(Math.log10(maxLineNumber)) + 1 : 1\n    const baseWidth = Math.max(this._minWidth, digits + this._paddingRight + 1) // +1 for left padding\n    return baseWidth + this._maxBeforeWidth + this._maxAfterWidth\n  }\n\n  public setLineColors(lineColorsGutter: Map<number, RGBA>, lineColorsContent: Map<number, RGBA>): void {\n    this._lineColorsGutter = lineColorsGutter\n    this._lineColorsContent = lineColorsContent\n    this.requestRender()\n  }\n\n  public getLineColors(): { gutter: Map<number, RGBA>; content: Map<number, RGBA> } {\n    return {\n      gutter: this._lineColorsGutter,\n      content: this._lineColorsContent,\n    }\n  }\n\n  public setLineSigns(lineSigns: Map<number, LineSign>): void {\n    const oldMaxBefore = this._maxBeforeWidth\n    const oldMaxAfter = this._maxAfterWidth\n\n    this._lineSigns = lineSigns\n    this.calculateSignWidths()\n\n    // Mark dirty if sign widths changed - this will trigger remeasure\n    if (this._maxBeforeWidth !== oldMaxBefore || this._maxAfterWidth !== oldMaxAfter) {\n      this.yogaNode.markDirty()\n    }\n\n    // Always request render since signs themselves may have changed\n    this.requestRender()\n  }\n\n  public getLineSigns(): Map<number, LineSign> {\n    return this._lineSigns\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    // For buffered rendering, only re-render when dirty OR when scroll position changed\n    const currentScrollY = this.target.scrollY\n    const scrollChanged = currentScrollY !== this._lastKnownScrollY\n\n    if (this.buffered && !this.isDirty && !scrollChanged) {\n      return\n    }\n\n    this._lastKnownScrollY = currentScrollY\n    this.refreshFrameBuffer(buffer)\n  }\n\n  private refreshFrameBuffer(buffer: OptimizedBuffer): void {\n    const startX = this.buffered ? 0 : this.x\n    const startY = this.buffered ? 0 : this.y\n\n    if (this.buffered) {\n      buffer.clear(this._bg)\n    } else if (this._bg.a > 0) {\n      // Fill background if not buffered and opaque (if buffered, clear handles it)\n      // Note: this.height might be determined by parent (flex stretch)\n      buffer.fillRect(startX, startY, this.width, this.height, this._bg)\n    }\n\n    const lineInfo = this.target.lineInfo\n    if (!lineInfo || !lineInfo.lineSources) return\n\n    const sources = lineInfo.lineSources\n    let lastSource = -1\n\n    // lineSources contains the logical line index for each visual line\n    // We start iterating from the scroll offset (first visible line)\n    const startLine = this.target.scrollY\n\n    // If scrolled past content (shouldn't happen normally but good to be safe)\n    if (startLine >= sources.length) return\n\n    // Get the logical line index of the line *before* the first visible line\n    // This helps determine if the first visible line is a wrapped continuation\n    lastSource = startLine > 0 ? sources[startLine - 1] : -1\n\n    for (let i = 0; i < this.height; i++) {\n      const visualLineIndex = startLine + i\n      if (visualLineIndex >= sources.length) break\n\n      const logicalLine = sources[visualLineIndex]\n      const lineBg = this._lineColorsGutter.get(logicalLine) ?? this._bg\n\n      // Fill background for this line if it has a custom color\n      if (lineBg !== this._bg) {\n        buffer.fillRect(startX, startY + i, this.width, 1, lineBg)\n      }\n\n      // Draw line number only for the first visual line of a logical line (wrapping)\n      if (logicalLine === lastSource) {\n        // Continuation line, maybe draw a dot or nothing\n      } else {\n        let currentX = startX\n\n        // Draw 'before' sign if present\n        const sign = this._lineSigns.get(logicalLine)\n        if (sign?.before) {\n          const beforeWidth = Bun.stringWidth(sign.before)\n          // Pad to max before width for alignment\n          const padding = this._maxBeforeWidth - beforeWidth\n          currentX += padding\n          const beforeColor = sign.beforeColor ? parseColor(sign.beforeColor) : this._fg\n          buffer.drawText(sign.before, currentX, startY + i, beforeColor, lineBg)\n          currentX += beforeWidth\n        } else if (this._maxBeforeWidth > 0) {\n          currentX += this._maxBeforeWidth\n        }\n\n        // Draw line number (right-aligned in its space with left padding of 1)\n        if (!this._hideLineNumbers.has(logicalLine)) {\n          // Use custom line number if provided, otherwise use calculated line number\n          const customLineNum = this._lineNumbers.get(logicalLine)\n          const lineNum = customLineNum !== undefined ? customLineNum : logicalLine + 1 + this._lineNumberOffset\n          const lineNumStr = lineNum.toString()\n          const lineNumWidth = lineNumStr.length\n          const availableSpace = this.width - this._maxBeforeWidth - this._maxAfterWidth - this._paddingRight\n          const lineNumX = startX + this._maxBeforeWidth + 1 + availableSpace - lineNumWidth - 1\n\n          if (lineNumX >= startX + this._maxBeforeWidth + 1) {\n            buffer.drawText(lineNumStr, lineNumX, startY + i, this._fg, lineBg)\n          }\n        }\n\n        // Draw 'after' sign if present\n        if (sign?.after) {\n          const afterX = startX + this.width - this._paddingRight - this._maxAfterWidth\n          const afterColor = sign.afterColor ? parseColor(sign.afterColor) : this._fg\n          buffer.drawText(sign.after, afterX, startY + i, afterColor, lineBg)\n        }\n      }\n\n      lastSource = logicalLine\n    }\n  }\n}\n\n// Helper function to darken an RGBA color by 20%\nfunction darkenColor(color: RGBA): RGBA {\n  return RGBA.fromValues(color.r * 0.8, color.g * 0.8, color.b * 0.8, color.a)\n}\n\nexport class LineNumberRenderable extends Renderable {\n  private gutter: GutterRenderable | null = null\n  private target: (Renderable & LineInfoProvider) | null = null\n  private _lineColorsGutter: Map<number, RGBA>\n  private _lineColorsContent: Map<number, RGBA>\n  private _lineSigns: Map<number, LineSign>\n  private _fg: RGBA\n  private _bg: RGBA\n  private _minWidth: number\n  private _paddingRight: number\n  private _lineNumberOffset: number\n  private _hideLineNumbers: Set<number>\n  private _lineNumbers: Map<number, number>\n  private _isDestroying: boolean = false\n  private handleLineInfoChange = (): void => {\n    // When line info changes in the target, remeasure the gutter\n    this.gutter?.remeasure()\n    this.requestRender()\n  }\n\n  private parseLineColor(line: number, color: string | RGBA | LineColorConfig): void {\n    if (typeof color === \"object\" && \"gutter\" in color) {\n      // LineColorConfig format\n      const config = color as LineColorConfig\n      if (config.gutter) {\n        this._lineColorsGutter.set(line, parseColor(config.gutter))\n      }\n      if (config.content) {\n        this._lineColorsContent.set(line, parseColor(config.content))\n      } else if (config.gutter) {\n        // If only gutter is specified, use a darker version for content\n        this._lineColorsContent.set(line, darkenColor(parseColor(config.gutter)))\n      }\n    } else {\n      // Simple format - same color for both, but content is darker\n      const parsedColor = parseColor(color as string | RGBA)\n      this._lineColorsGutter.set(line, parsedColor)\n      this._lineColorsContent.set(line, darkenColor(parsedColor))\n    }\n  }\n\n  constructor(ctx: RenderContext, options: LineNumberOptions) {\n    super(ctx, {\n      ...options,\n      flexDirection: \"row\",\n      // CRITICAL:\n      // By forcing height=auto, we ensure the parent box properly accounts for our full height.\n      height: \"auto\",\n    })\n\n    this._fg = parseColor(options.fg ?? \"#888888\")\n    this._bg = parseColor(options.bg ?? \"transparent\")\n    this._minWidth = options.minWidth ?? 3\n    this._paddingRight = options.paddingRight ?? 1\n    this._lineNumberOffset = options.lineNumberOffset ?? 0\n    this._hideLineNumbers = options.hideLineNumbers ?? new Set()\n    this._lineNumbers = options.lineNumbers ?? new Map()\n\n    this._lineColorsGutter = new Map<number, RGBA>()\n    this._lineColorsContent = new Map<number, RGBA>()\n    if (options.lineColors) {\n      for (const [line, color] of options.lineColors) {\n        this.parseLineColor(line, color)\n      }\n    }\n\n    this._lineSigns = new Map<number, LineSign>()\n    if (options.lineSigns) {\n      for (const [line, sign] of options.lineSigns) {\n        this._lineSigns.set(line, sign)\n      }\n    }\n\n    // If target is provided in constructor, set it up immediately\n    if (options.target) {\n      this.setTarget(options.target)\n    }\n  }\n\n  private setTarget(target: Renderable & LineInfoProvider): void {\n    if (this.target === target) return\n\n    if (this.target) {\n      // Remove event listener from old target\n      this.target.off(\"line-info-change\", this.handleLineInfoChange)\n      super.remove(this.target.id)\n    }\n\n    if (this.gutter) {\n      super.remove(this.gutter.id)\n      this.gutter = null\n    }\n\n    this.target = target\n\n    // Listen for line info changes from target\n    this.target.on(\"line-info-change\", this.handleLineInfoChange)\n\n    this.gutter = new GutterRenderable(this.ctx, this.target, {\n      fg: this._fg,\n      bg: this._bg,\n      minWidth: this._minWidth,\n      paddingRight: this._paddingRight,\n      lineColorsGutter: this._lineColorsGutter,\n      lineColorsContent: this._lineColorsContent,\n      lineSigns: this._lineSigns,\n      lineNumberOffset: this._lineNumberOffset,\n      hideLineNumbers: this._hideLineNumbers,\n      lineNumbers: this._lineNumbers,\n      id: this.id ? `${this.id}-gutter` : undefined,\n      buffered: true,\n    })\n\n    super.add(this.gutter)\n    super.add(this.target)\n  }\n\n  // Override add to intercept and set as target if it's a LineInfoProvider\n  public override add(child: Renderable): number {\n    // If this is a LineInfoProvider and we don't have a target yet, set it\n    if (\n      !this.target &&\n      \"lineInfo\" in child &&\n      \"lineCount\" in child &&\n      \"virtualLineCount\" in child &&\n      \"scrollY\" in child\n    ) {\n      this.setTarget(child as Renderable & LineInfoProvider)\n      return this.getChildrenCount() - 1\n    }\n    // Otherwise ignore - SolidJS may try to add layout slots or other helpers\n    return -1\n  }\n\n  // Override remove to prevent removing gutter/target directly\n  public override remove(id: string): void {\n    if (this._isDestroying) {\n      super.remove(id)\n      return\n    }\n\n    if (this.gutter && id === this.gutter.id) {\n      throw new Error(\"LineNumberRenderable: Cannot remove gutter directly.\")\n    }\n    if (this.target && id === this.target.id) {\n      throw new Error(\"LineNumberRenderable: Cannot remove target directly. Use clearTarget() instead.\")\n    }\n    super.remove(id)\n  }\n\n  // Override destroyRecursively to properly clean up internal components\n  public override destroyRecursively(): void {\n    this._isDestroying = true\n\n    if (this.target) {\n      this.target.off(\"line-info-change\", this.handleLineInfoChange)\n    }\n\n    super.destroyRecursively()\n\n    this.gutter = null\n    this.target = null\n  }\n\n  public clearTarget(): void {\n    if (this.target) {\n      this.target.off(\"line-info-change\", this.handleLineInfoChange)\n      super.remove(this.target.id)\n      this.target = null\n    }\n    if (this.gutter) {\n      super.remove(this.gutter.id)\n      this.gutter = null\n    }\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    // Draw full-width line backgrounds before children render\n    if (!this.target || !this.gutter) return\n\n    const lineInfo = this.target.lineInfo\n    if (!lineInfo || !lineInfo.lineSources) return\n\n    const sources = lineInfo.lineSources\n    const startLine = this.target.scrollY\n\n    if (startLine >= sources.length) return\n\n    // Calculate the area to fill: from after the gutter (if visible) to the end of our width\n    const gutterWidth = this.gutter.visible ? this.gutter.width : 0\n    const contentWidth = this.width - gutterWidth\n\n    // Draw full-width background colors for lines with custom colors\n    for (let i = 0; i < this.height; i++) {\n      const visualLineIndex = startLine + i\n      if (visualLineIndex >= sources.length) break\n\n      const logicalLine = sources[visualLineIndex]\n      const lineBg = this._lineColorsContent.get(logicalLine)\n\n      if (lineBg) {\n        // Fill from after gutter to the end of the LineNumberRenderable\n        buffer.fillRect(this.x + gutterWidth, this.y + i, contentWidth, 1, lineBg)\n      }\n    }\n  }\n\n  public set showLineNumbers(value: boolean) {\n    if (this.gutter) {\n      this.gutter.visible = value\n    }\n  }\n\n  public get showLineNumbers(): boolean {\n    return this.gutter?.visible ?? false\n  }\n\n  public setLineColor(line: number, color: string | RGBA | LineColorConfig): void {\n    this.parseLineColor(line, color)\n    // Update gutter if it exists\n    if (this.gutter) {\n      this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent)\n    }\n  }\n\n  public clearLineColor(line: number): void {\n    this._lineColorsGutter.delete(line)\n    this._lineColorsContent.delete(line)\n    if (this.gutter) {\n      this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent)\n    }\n  }\n\n  public clearAllLineColors(): void {\n    this._lineColorsGutter.clear()\n    this._lineColorsContent.clear()\n    if (this.gutter) {\n      this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent)\n    }\n  }\n\n  public setLineColors(lineColors: Map<number, string | RGBA | LineColorConfig>): void {\n    this._lineColorsGutter.clear()\n    this._lineColorsContent.clear()\n    for (const [line, color] of lineColors) {\n      this.parseLineColor(line, color)\n    }\n    // Update gutter once after all colors are set\n    if (this.gutter) {\n      this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent)\n    }\n  }\n\n  public getLineColors(): { gutter: Map<number, RGBA>; content: Map<number, RGBA> } {\n    return {\n      gutter: this._lineColorsGutter,\n      content: this._lineColorsContent,\n    }\n  }\n\n  public setLineSign(line: number, sign: LineSign): void {\n    this._lineSigns.set(line, sign)\n    if (this.gutter) {\n      this.gutter.setLineSigns(this._lineSigns)\n    }\n  }\n\n  public clearLineSign(line: number): void {\n    this._lineSigns.delete(line)\n    if (this.gutter) {\n      this.gutter.setLineSigns(this._lineSigns)\n    }\n  }\n\n  public clearAllLineSigns(): void {\n    this._lineSigns.clear()\n    if (this.gutter) {\n      this.gutter.setLineSigns(this._lineSigns)\n    }\n  }\n\n  public setLineSigns(lineSigns: Map<number, LineSign>): void {\n    this._lineSigns.clear()\n    for (const [line, sign] of lineSigns) {\n      this._lineSigns.set(line, sign)\n    }\n    if (this.gutter) {\n      this.gutter.setLineSigns(this._lineSigns)\n    }\n  }\n\n  public getLineSigns(): Map<number, LineSign> {\n    return this._lineSigns\n  }\n\n  public set lineNumberOffset(value: number) {\n    if (this._lineNumberOffset !== value) {\n      this._lineNumberOffset = value\n      if (this.gutter) {\n        // Update the gutter's offset using its setter\n        this.gutter.setLineNumberOffset(value)\n      }\n    }\n  }\n\n  public get lineNumberOffset(): number {\n    return this._lineNumberOffset\n  }\n\n  public setHideLineNumbers(hideLineNumbers: Set<number>): void {\n    this._hideLineNumbers = hideLineNumbers\n    if (this.gutter) {\n      // Update the gutter's hideLineNumbers using its setter\n      this.gutter.setHideLineNumbers(hideLineNumbers)\n    }\n  }\n\n  public getHideLineNumbers(): Set<number> {\n    return this._hideLineNumbers\n  }\n\n  public setLineNumbers(lineNumbers: Map<number, number>): void {\n    this._lineNumbers = lineNumbers\n    if (this.gutter) {\n      // Update the gutter's lineNumbers using its setter\n      this.gutter.setLineNumbers(lineNumbers)\n    }\n  }\n\n  public getLineNumbers(): Map<number, number> {\n    return this._lineNumbers\n  }\n\n  public highlightLines(startLine: number, endLine: number, color: string | RGBA | LineColorConfig): void {\n    for (let i = startLine; i <= endLine; i++) {\n      this.parseLineColor(i, color)\n    }\n    if (this.gutter) {\n      this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent)\n    }\n  }\n\n  public clearHighlightLines(startLine: number, endLine: number): void {\n    for (let i = startLine; i <= endLine; i++) {\n      this._lineColorsGutter.delete(i)\n      this._lineColorsContent.delete(i)\n    }\n    if (this.gutter) {\n      this.gutter.setLineColors(this._lineColorsGutter, this._lineColorsContent)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Markdown.ts",
    "content": "import { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { type RenderContext } from \"../types.js\"\nimport { SyntaxStyle, type StyleDefinition } from \"../syntax-style.js\"\nimport type { TextChunk } from \"../text-buffer.js\"\nimport { createTextAttributes } from \"../utils.js\"\nimport type { BorderStyle } from \"../lib/border.js\"\nimport { RGBA, parseColor, type ColorInput } from \"../lib/RGBA.js\"\nimport { type MarkedToken, type Token, type Tokens } from \"marked\"\nimport { CodeRenderable, type OnChunksCallback } from \"./Code.js\"\nimport {\n  TextTableRenderable,\n  type TextTableCellContent,\n  type TextTableColumnFitter,\n  type TextTableColumnWidthMode,\n  type TextTableContent,\n} from \"./TextTable.js\"\nimport type { TreeSitterClient } from \"../lib/tree-sitter/index.js\"\nimport { infoStringToFiletype } from \"../lib/tree-sitter/resolve-ft.js\"\nimport { parseMarkdownIncremental, type ParseState } from \"./markdown-parser.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport { detectLinks } from \"../lib/detect-links.js\"\n\nexport interface MarkdownTableOptions {\n  /**\n   * Strategy for sizing table columns.\n   * - \"content\": columns fit to intrinsic content width.\n   * - \"full\": columns expand to fill available width.\n   */\n  widthMode?: TextTableColumnWidthMode\n  /**\n   * Column fitting method when shrinking constrained tables.\n   */\n  columnFitter?: TextTableColumnFitter\n  /**\n   * Wrapping strategy for table cell content.\n   */\n  wrapMode?: \"none\" | \"char\" | \"word\"\n  /**\n   * Padding applied on all sides of each table cell.\n   */\n  cellPadding?: number\n  /**\n   * Enables/disables table border rendering.\n   */\n  borders?: boolean\n  /**\n   * Overrides outer border visibility. Defaults to `borders`.\n   */\n  outerBorder?: boolean\n  /**\n   * Border style for markdown tables.\n   */\n  borderStyle?: BorderStyle\n  /**\n   * Border color for markdown tables. Defaults to conceal style color.\n   */\n  borderColor?: ColorInput\n  /**\n   * Enables/disables selection support on markdown tables.\n   */\n  selectable?: boolean\n}\n\nexport interface MarkdownOptions extends RenderableOptions<MarkdownRenderable> {\n  content?: string\n  syntaxStyle: SyntaxStyle\n  fg?: ColorInput\n  bg?: ColorInput\n  /** Controls concealment for markdown syntax markers in markdown text blocks. */\n  conceal?: boolean\n  /** Controls concealment inside fenced code blocks rendered by CodeRenderable. */\n  concealCode?: boolean\n  treeSitterClient?: TreeSitterClient\n  /**\n   * Enable streaming mode for incremental content updates.\n   *\n   * Semantics:\n   * - The trailing markdown block stays unstable while streaming is enabled.\n   * - Tables render all rows produced by the markdown parser (including trailing rows).\n   * - Incomplete table rows are normalized by the parser and rendered with empty cells\n   *   where data is missing.\n   *\n   * Expectations:\n   * - Keep this true while chunks are still being appended.\n   * - Set this to false once streaming is complete to finalize trailing token parsing.\n   */\n  streaming?: boolean\n  /**\n   * Options for internally rendered markdown tables.\n   */\n  tableOptions?: MarkdownTableOptions\n  /**\n   * Custom node renderer. Return a Renderable to override default rendering,\n   * or undefined/null to use default rendering.\n   */\n  renderNode?: (token: Token, context: RenderNodeContext) => Renderable | undefined | null\n}\n\nexport interface RenderNodeContext {\n  syntaxStyle: SyntaxStyle\n  conceal: boolean\n  concealCode: boolean\n  treeSitterClient?: TreeSitterClient\n  /** Creates default renderable for this token */\n  defaultRender: () => Renderable | null\n}\n\ninterface TableContentCache {\n  content: TextTableContent\n  cellKeys: Uint32Array[]\n}\n\ninterface ResolvedTableRenderableOptions {\n  columnWidthMode: TextTableColumnWidthMode\n  columnFitter: TextTableColumnFitter\n  wrapMode: \"none\" | \"char\" | \"word\"\n  cellPadding: number\n  border: boolean\n  outerBorder: boolean\n  showBorders: boolean\n  borderStyle: BorderStyle\n  borderColor: ColorInput\n  selectable: boolean\n}\n\nconst TRAILING_MARKDOWN_BLOCK_BREAKS_RE = /(?:\\r?\\n){2,}$/\n\nfunction colorsEqual(left?: RGBA, right?: RGBA): boolean {\n  if (!left || !right) return left === right\n  return left.equals(right)\n}\n\nexport interface BlockState {\n  token: MarkedToken\n  tokenRaw: string // Cache raw for comparison\n  renderable: Renderable\n  tableContentCache?: TableContentCache\n}\n\nexport type { ParseState }\n\nexport class MarkdownRenderable extends Renderable {\n  private _content: string = \"\"\n  private _syntaxStyle: SyntaxStyle\n  private _fg?: RGBA\n  private _bg?: RGBA\n  private _conceal: boolean\n  private _concealCode: boolean\n  private _treeSitterClient?: TreeSitterClient\n  private _tableOptions?: MarkdownTableOptions\n  private _renderNode?: MarkdownOptions[\"renderNode\"]\n\n  _parseState: ParseState | null = null\n  private _streaming: boolean = false\n  _blockStates: BlockState[] = []\n  private _styleDirty: boolean = false\n  private _linkifyMarkdownChunks: OnChunksCallback = (chunks, context) =>\n    detectLinks(chunks, {\n      content: context.content,\n      highlights: context.highlights,\n    })\n\n  protected _contentDefaultOptions = {\n    content: \"\",\n    conceal: true,\n    concealCode: false,\n    streaming: false,\n  } satisfies Partial<MarkdownOptions>\n\n  constructor(ctx: RenderContext, options: MarkdownOptions) {\n    super(ctx, {\n      ...options,\n      flexDirection: \"column\",\n      flexShrink: options.flexShrink ?? 0,\n    })\n\n    this._syntaxStyle = options.syntaxStyle\n    this._fg = options.fg ? parseColor(options.fg) : undefined\n    this._bg = options.bg ? parseColor(options.bg) : undefined\n    this._conceal = options.conceal ?? this._contentDefaultOptions.conceal\n    this._concealCode = options.concealCode ?? this._contentDefaultOptions.concealCode\n    this._content = options.content ?? this._contentDefaultOptions.content\n    this._treeSitterClient = options.treeSitterClient\n    this._tableOptions = options.tableOptions\n    this._renderNode = options.renderNode\n    this._streaming = options.streaming ?? this._contentDefaultOptions.streaming\n\n    this.updateBlocks()\n  }\n\n  get content(): string {\n    return this._content\n  }\n\n  set content(value: string) {\n    if (this.isDestroyed) return\n    if (this._content !== value) {\n      this._content = value\n      this.updateBlocks()\n      this.requestRender()\n    }\n  }\n\n  get syntaxStyle(): SyntaxStyle {\n    return this._syntaxStyle\n  }\n\n  set syntaxStyle(value: SyntaxStyle) {\n    if (this._syntaxStyle !== value) {\n      this._syntaxStyle = value\n      // Mark dirty - actual re-render happens in renderSelf\n      this._styleDirty = true\n    }\n  }\n\n  get fg(): RGBA | undefined {\n    return this._fg\n  }\n\n  set fg(value: ColorInput | undefined) {\n    const next = value ? parseColor(value) : undefined\n    if (!colorsEqual(this._fg, next)) {\n      this._fg = next\n      this._styleDirty = true\n    }\n  }\n\n  get bg(): RGBA | undefined {\n    return this._bg\n  }\n\n  set bg(value: ColorInput | undefined) {\n    const next = value ? parseColor(value) : undefined\n    if (!colorsEqual(this._bg, next)) {\n      this._bg = next\n      this._styleDirty = true\n    }\n  }\n\n  get conceal(): boolean {\n    return this._conceal\n  }\n\n  set conceal(value: boolean) {\n    if (this._conceal !== value) {\n      this._conceal = value\n      // Mark dirty - actual re-render happens in renderSelf\n      this._styleDirty = true\n    }\n  }\n\n  get concealCode(): boolean {\n    return this._concealCode\n  }\n\n  set concealCode(value: boolean) {\n    if (this._concealCode !== value) {\n      this._concealCode = value\n      // Mark dirty - actual re-render happens in renderSelf\n      this._styleDirty = true\n    }\n  }\n\n  get streaming(): boolean {\n    return this._streaming\n  }\n\n  set streaming(value: boolean) {\n    if (this.isDestroyed) return\n    if (this._streaming !== value) {\n      this._streaming = value\n      this.updateBlocks(true)\n    }\n  }\n\n  get tableOptions(): MarkdownTableOptions | undefined {\n    return this._tableOptions\n  }\n\n  set tableOptions(value: MarkdownTableOptions | undefined) {\n    this._tableOptions = value\n    this.applyTableOptionsToBlocks()\n  }\n\n  private getStyle(group: string): StyleDefinition | undefined {\n    // The solid reconciler applies props via setters in JSX declaration order.\n    // If `content` is set before `syntaxStyle`, updateBlocks() runs before\n    // _syntaxStyle is initialized.\n    if (!this._syntaxStyle) return undefined\n    let style = this._syntaxStyle.getStyle(group)\n    if (!style && group.includes(\".\")) {\n      const baseName = group.split(\".\")[0]\n      style = this._syntaxStyle.getStyle(baseName)\n    }\n    return style\n  }\n\n  private createChunk(text: string, group: string, link?: { url: string }): TextChunk {\n    const style = this.getStyle(group) || this.getStyle(\"default\")\n    return {\n      __isChunk: true,\n      text,\n      fg: style?.fg,\n      bg: style?.bg,\n      attributes: style\n        ? createTextAttributes({\n            bold: style.bold,\n            italic: style.italic,\n            underline: style.underline,\n            dim: style.dim,\n          })\n        : 0,\n      link,\n    }\n  }\n\n  private createDefaultChunk(text: string): TextChunk {\n    return this.createChunk(text, \"default\")\n  }\n\n  private renderInlineContent(tokens: Token[], chunks: TextChunk[]): void {\n    for (const token of tokens) {\n      this.renderInlineToken(token as MarkedToken, chunks)\n    }\n  }\n\n  private renderInlineToken(token: MarkedToken, chunks: TextChunk[]): void {\n    switch (token.type) {\n      case \"text\":\n        chunks.push(this.createDefaultChunk(token.text))\n        break\n\n      case \"escape\":\n        chunks.push(this.createDefaultChunk(token.text))\n        break\n\n      case \"codespan\":\n        if (this._conceal) {\n          chunks.push(this.createChunk(token.text, \"markup.raw\"))\n        } else {\n          chunks.push(this.createChunk(\"`\", \"markup.raw\"))\n          chunks.push(this.createChunk(token.text, \"markup.raw\"))\n          chunks.push(this.createChunk(\"`\", \"markup.raw\"))\n        }\n        break\n\n      case \"strong\":\n        if (!this._conceal) {\n          chunks.push(this.createChunk(\"**\", \"markup.strong\"))\n        }\n        for (const child of token.tokens) {\n          this.renderInlineTokenWithStyle(child as MarkedToken, chunks, \"markup.strong\")\n        }\n        if (!this._conceal) {\n          chunks.push(this.createChunk(\"**\", \"markup.strong\"))\n        }\n        break\n\n      case \"em\":\n        if (!this._conceal) {\n          chunks.push(this.createChunk(\"*\", \"markup.italic\"))\n        }\n        for (const child of token.tokens) {\n          this.renderInlineTokenWithStyle(child as MarkedToken, chunks, \"markup.italic\")\n        }\n        if (!this._conceal) {\n          chunks.push(this.createChunk(\"*\", \"markup.italic\"))\n        }\n        break\n\n      case \"del\":\n        if (!this._conceal) {\n          chunks.push(this.createChunk(\"~~\", \"markup.strikethrough\"))\n        }\n        for (const child of token.tokens) {\n          this.renderInlineTokenWithStyle(child as MarkedToken, chunks, \"markup.strikethrough\")\n        }\n        if (!this._conceal) {\n          chunks.push(this.createChunk(\"~~\", \"markup.strikethrough\"))\n        }\n        break\n\n      case \"link\": {\n        const linkHref = { url: token.href }\n        if (this._conceal) {\n          for (const child of token.tokens) {\n            this.renderInlineTokenWithStyle(child as MarkedToken, chunks, \"markup.link.label\", linkHref)\n          }\n          chunks.push(this.createChunk(\" (\", \"markup.link\", linkHref))\n          chunks.push(this.createChunk(token.href, \"markup.link.url\", linkHref))\n          chunks.push(this.createChunk(\")\", \"markup.link\", linkHref))\n        } else {\n          chunks.push(this.createChunk(\"[\", \"markup.link\", linkHref))\n          for (const child of token.tokens) {\n            this.renderInlineTokenWithStyle(child as MarkedToken, chunks, \"markup.link.label\", linkHref)\n          }\n          chunks.push(this.createChunk(\"](\", \"markup.link\", linkHref))\n          chunks.push(this.createChunk(token.href, \"markup.link.url\", linkHref))\n          chunks.push(this.createChunk(\")\", \"markup.link\", linkHref))\n        }\n        break\n      }\n\n      case \"image\": {\n        const imageHref = { url: token.href }\n        if (this._conceal) {\n          chunks.push(this.createChunk(token.text || \"image\", \"markup.link.label\", imageHref))\n        } else {\n          chunks.push(this.createChunk(\"![\", \"markup.link\", imageHref))\n          chunks.push(this.createChunk(token.text || \"\", \"markup.link.label\", imageHref))\n          chunks.push(this.createChunk(\"](\", \"markup.link\", imageHref))\n          chunks.push(this.createChunk(token.href, \"markup.link.url\", imageHref))\n          chunks.push(this.createChunk(\")\", \"markup.link\", imageHref))\n        }\n        break\n      }\n\n      case \"br\":\n        chunks.push(this.createDefaultChunk(\"\\n\"))\n        break\n\n      default:\n        if (\"tokens\" in token && Array.isArray(token.tokens)) {\n          this.renderInlineContent(token.tokens, chunks)\n        } else if (\"text\" in token && typeof token.text === \"string\") {\n          chunks.push(this.createDefaultChunk(token.text))\n        }\n        break\n    }\n  }\n\n  private renderInlineTokenWithStyle(\n    token: MarkedToken,\n    chunks: TextChunk[],\n    styleGroup: string,\n    link?: { url: string },\n  ): void {\n    switch (token.type) {\n      case \"text\":\n        chunks.push(this.createChunk(token.text, styleGroup, link))\n        break\n\n      case \"escape\":\n        chunks.push(this.createChunk(token.text, styleGroup, link))\n        break\n\n      case \"codespan\":\n        if (this._conceal) {\n          chunks.push(this.createChunk(token.text, \"markup.raw\", link))\n        } else {\n          chunks.push(this.createChunk(\"`\", \"markup.raw\", link))\n          chunks.push(this.createChunk(token.text, \"markup.raw\", link))\n          chunks.push(this.createChunk(\"`\", \"markup.raw\", link))\n        }\n        break\n\n      default:\n        this.renderInlineToken(token, chunks)\n        break\n    }\n  }\n\n  private createMarkdownCodeRenderable(content: string, id: string, marginBottom: number = 0): CodeRenderable {\n    return new CodeRenderable(this.ctx, {\n      id,\n      content,\n      filetype: \"markdown\",\n      syntaxStyle: this._syntaxStyle,\n      fg: this._fg,\n      bg: this._bg,\n      conceal: this._conceal,\n      drawUnstyledText: false,\n      streaming: true,\n      onChunks: this._linkifyMarkdownChunks,\n      treeSitterClient: this._treeSitterClient,\n      width: \"100%\",\n      marginBottom,\n    })\n  }\n\n  private createCodeRenderable(token: Tokens.Code, id: string, marginBottom: number = 0): Renderable {\n    return new CodeRenderable(this.ctx, {\n      id,\n      content: token.text,\n      filetype: infoStringToFiletype(token.lang ?? \"\"),\n      syntaxStyle: this._syntaxStyle,\n      fg: this._fg,\n      bg: this._bg,\n      conceal: this._concealCode,\n      drawUnstyledText: !(this._streaming && this._concealCode),\n      streaming: this._streaming,\n      treeSitterClient: this._treeSitterClient,\n      width: \"100%\",\n      marginBottom,\n    })\n  }\n\n  private applyMarkdownCodeRenderable(renderable: CodeRenderable, content: string, marginBottom: number): void {\n    renderable.content = content\n    renderable.filetype = \"markdown\"\n    renderable.syntaxStyle = this._syntaxStyle\n    renderable.fg = this._fg\n    renderable.bg = this._bg\n    renderable.conceal = this._conceal\n    renderable.drawUnstyledText = false\n    renderable.streaming = true\n    renderable.marginBottom = marginBottom\n  }\n\n  private applyCodeBlockRenderable(renderable: CodeRenderable, token: Tokens.Code, marginBottom: number): void {\n    renderable.content = token.text\n    renderable.filetype = infoStringToFiletype(token.lang ?? \"\")\n    renderable.syntaxStyle = this._syntaxStyle\n    renderable.fg = this._fg\n    renderable.bg = this._bg\n    renderable.conceal = this._concealCode\n    renderable.drawUnstyledText = !(this._streaming && this._concealCode)\n    renderable.streaming = this._streaming\n    renderable.marginBottom = marginBottom\n  }\n\n  private shouldRenderSeparately(token: MarkedToken): boolean {\n    return token.type === \"code\" || token.type === \"table\" || token.type === \"blockquote\"\n  }\n\n  private getInterBlockMargin(token: MarkedToken, hasNextToken: boolean): number {\n    if (!hasNextToken) return 0\n    return this.shouldRenderSeparately(token) ? 1 : 0\n  }\n\n  private createMarkdownBlockToken(raw: string): MarkedToken {\n    return {\n      type: \"paragraph\",\n      raw,\n      text: raw,\n      tokens: [],\n    } as MarkedToken\n  }\n\n  private normalizeMarkdownBlockRaw(raw: string): string {\n    return raw.replace(TRAILING_MARKDOWN_BLOCK_BREAKS_RE, \"\\n\")\n  }\n\n  private buildRenderableTokens(tokens: MarkedToken[]): MarkedToken[] {\n    if (this._renderNode) {\n      return tokens.filter((token) => token.type !== \"space\")\n    }\n\n    const renderTokens: MarkedToken[] = []\n    let markdownRaw = \"\"\n\n    const flushMarkdownRaw = (): void => {\n      if (markdownRaw.length === 0) return\n      const normalizedRaw = this.normalizeMarkdownBlockRaw(markdownRaw)\n      if (normalizedRaw.length > 0) {\n        renderTokens.push(this.createMarkdownBlockToken(normalizedRaw))\n      }\n      markdownRaw = \"\"\n    }\n\n    for (let i = 0; i < tokens.length; i += 1) {\n      const token = tokens[i]\n\n      if (token.type === \"space\") {\n        if (markdownRaw.length === 0) {\n          continue\n        }\n\n        let nextIndex = i + 1\n        while (nextIndex < tokens.length && tokens[nextIndex].type === \"space\") {\n          nextIndex += 1\n        }\n\n        const nextToken = tokens[nextIndex]\n        if (nextToken && !this.shouldRenderSeparately(nextToken)) {\n          markdownRaw += token.raw\n        }\n        continue\n      }\n\n      if (this.shouldRenderSeparately(token)) {\n        flushMarkdownRaw()\n        renderTokens.push(token)\n        continue\n      }\n\n      markdownRaw += token.raw\n    }\n\n    flushMarkdownRaw()\n\n    return renderTokens\n  }\n\n  private getTableRowsToRender(table: Tokens.Table): Tokens.TableCell[][] {\n    return table.rows\n  }\n\n  private hashString(value: string, seed: number): number {\n    let hash = seed >>> 0\n    for (let i = 0; i < value.length; i += 1) {\n      hash ^= value.charCodeAt(i)\n      hash = Math.imul(hash, 16777619)\n    }\n    return hash >>> 0\n  }\n\n  private hashTableToken(token: MarkedToken, seed: number, depth: number = 0): number {\n    let hash = this.hashString(token.type, seed)\n\n    if (\"raw\" in token && typeof token.raw === \"string\") {\n      return this.hashString(token.raw, hash)\n    }\n\n    if (\"text\" in token && typeof token.text === \"string\") {\n      hash = this.hashString(token.text, hash)\n    }\n\n    if (depth < 2 && \"tokens\" in token && Array.isArray(token.tokens)) {\n      for (const child of token.tokens) {\n        hash = this.hashTableToken(child as MarkedToken, hash, depth + 1)\n      }\n    }\n\n    return hash >>> 0\n  }\n\n  private getTableCellKey(cell: Tokens.TableCell | undefined, isHeader: boolean): number {\n    const seed = isHeader ? 2902232141 : 1371922141\n    if (!cell) {\n      return seed\n    }\n\n    if (typeof cell.text === \"string\") {\n      return this.hashString(cell.text, seed)\n    }\n\n    if (Array.isArray(cell.tokens) && cell.tokens.length > 0) {\n      let hash = seed ^ cell.tokens.length\n      for (const token of cell.tokens) {\n        hash = this.hashTableToken(token as MarkedToken, hash)\n      }\n      return hash >>> 0\n    }\n\n    return (seed ^ 2654435769) >>> 0\n  }\n\n  private createTableDataCellChunks(cell: Tokens.TableCell | undefined): TextChunk[] {\n    const chunks: TextChunk[] = []\n    if (cell) {\n      this.renderInlineContent(cell.tokens, chunks)\n    }\n    return chunks.length > 0 ? chunks : [this.createDefaultChunk(\" \")]\n  }\n\n  private createTableHeaderCellChunks(cell: Tokens.TableCell): TextChunk[] {\n    const chunks: TextChunk[] = []\n    this.renderInlineContent(cell.tokens, chunks)\n\n    const baseChunks = chunks.length > 0 ? chunks : [this.createDefaultChunk(\" \")]\n    const headingStyle = this.getStyle(\"markup.heading\") || this.getStyle(\"default\")\n    if (!headingStyle) {\n      return baseChunks\n    }\n\n    const headingAttributes = createTextAttributes({\n      bold: headingStyle.bold,\n      italic: headingStyle.italic,\n      underline: headingStyle.underline,\n      dim: headingStyle.dim,\n    })\n\n    return baseChunks.map((chunk) => ({\n      ...chunk,\n      fg: headingStyle.fg ?? chunk.fg,\n      bg: headingStyle.bg ?? chunk.bg,\n      attributes: headingAttributes,\n    }))\n  }\n\n  private buildTableContentCache(\n    table: Tokens.Table,\n    previous?: TableContentCache,\n    forceRegenerate: boolean = false,\n  ): { cache: TableContentCache | null; changed: boolean } {\n    const colCount = table.header.length\n    const rowsToRender = this.getTableRowsToRender(table)\n    if (colCount === 0 || rowsToRender.length === 0) {\n      return { cache: null, changed: previous !== undefined }\n    }\n\n    const content: TextTableContent = []\n    const cellKeys: Uint32Array[] = []\n    const totalRows = rowsToRender.length + 1\n\n    let changed = forceRegenerate || !previous\n\n    for (let rowIndex = 0; rowIndex < totalRows; rowIndex += 1) {\n      const rowContent: TextTableCellContent[] = []\n      const rowKeys = new Uint32Array(colCount)\n\n      for (let colIndex = 0; colIndex < colCount; colIndex += 1) {\n        const isHeader = rowIndex === 0\n        const cell = isHeader ? table.header[colIndex] : rowsToRender[rowIndex - 1]?.[colIndex]\n        const cellKey = this.getTableCellKey(cell, isHeader)\n        rowKeys[colIndex] = cellKey\n\n        const previousCellKey = previous?.cellKeys[rowIndex]?.[colIndex]\n        const previousCellContent = previous?.content[rowIndex]?.[colIndex]\n\n        if (!forceRegenerate && previousCellKey === cellKey && Array.isArray(previousCellContent)) {\n          rowContent.push(previousCellContent)\n          continue\n        }\n\n        changed = true\n        rowContent.push(\n          isHeader ? this.createTableHeaderCellChunks(table.header[colIndex]) : this.createTableDataCellChunks(cell),\n        )\n      }\n\n      content.push(rowContent)\n      cellKeys.push(rowKeys)\n    }\n\n    if (previous && !changed) {\n      if (previous.content.length !== content.length) {\n        changed = true\n      } else {\n        for (let rowIndex = 0; rowIndex < content.length; rowIndex += 1) {\n          if ((previous.content[rowIndex]?.length ?? 0) !== content[rowIndex].length) {\n            changed = true\n            break\n          }\n        }\n      }\n    }\n\n    return {\n      cache: {\n        content,\n        cellKeys,\n      },\n      changed,\n    }\n  }\n\n  private resolveTableRenderableOptions(): ResolvedTableRenderableOptions {\n    const borders = this._tableOptions?.borders ?? true\n\n    return {\n      columnWidthMode: this._tableOptions?.widthMode ?? \"full\",\n      columnFitter: this._tableOptions?.columnFitter ?? \"proportional\",\n      wrapMode: this._tableOptions?.wrapMode ?? \"word\",\n      cellPadding: this._tableOptions?.cellPadding ?? 0,\n      border: borders,\n      outerBorder: this._tableOptions?.outerBorder ?? borders,\n      showBorders: borders,\n      borderStyle: this._tableOptions?.borderStyle ?? \"single\",\n      borderColor: this._tableOptions?.borderColor ?? this.getStyle(\"conceal\")?.fg ?? \"#888888\",\n      selectable: this._tableOptions?.selectable ?? true,\n    }\n  }\n\n  private applyTableRenderableOptions(\n    tableRenderable: TextTableRenderable,\n    options: ResolvedTableRenderableOptions,\n  ): void {\n    tableRenderable.columnWidthMode = options.columnWidthMode\n    tableRenderable.columnFitter = options.columnFitter\n    tableRenderable.wrapMode = options.wrapMode\n    tableRenderable.cellPadding = options.cellPadding\n    tableRenderable.border = options.border\n    tableRenderable.outerBorder = options.outerBorder\n    tableRenderable.showBorders = options.showBorders\n    tableRenderable.borderStyle = options.borderStyle\n    tableRenderable.borderColor = options.borderColor\n    tableRenderable.selectable = options.selectable\n  }\n\n  private applyTableOptionsToBlocks(): void {\n    const options = this.resolveTableRenderableOptions()\n    let updated = false\n\n    for (const state of this._blockStates) {\n      if (state.renderable instanceof TextTableRenderable) {\n        this.applyTableRenderableOptions(state.renderable, options)\n        updated = true\n      }\n    }\n\n    if (updated) {\n      this.requestRender()\n    }\n  }\n\n  private createTextTableRenderable(\n    content: TextTableContent,\n    id: string,\n    marginBottom: number = 0,\n  ): TextTableRenderable {\n    const options = this.resolveTableRenderableOptions()\n    return new TextTableRenderable(this.ctx, {\n      id,\n      content,\n      width: \"100%\",\n      marginBottom,\n      columnWidthMode: options.columnWidthMode,\n      columnFitter: options.columnFitter,\n      wrapMode: options.wrapMode,\n      cellPadding: options.cellPadding,\n      border: options.border,\n      outerBorder: options.outerBorder,\n      showBorders: options.showBorders,\n      borderStyle: options.borderStyle,\n      borderColor: options.borderColor,\n      selectable: options.selectable,\n    })\n  }\n\n  private createTableBlock(\n    table: Tokens.Table,\n    id: string,\n    marginBottom: number = 0,\n    previousCache?: TableContentCache,\n    forceRegenerate: boolean = false,\n  ): { renderable: Renderable; tableContentCache?: TableContentCache } {\n    const { cache } = this.buildTableContentCache(table, previousCache, forceRegenerate)\n\n    if (!cache) {\n      return {\n        renderable: this.createMarkdownCodeRenderable(table.raw, id, marginBottom),\n      }\n    }\n\n    return {\n      renderable: this.createTextTableRenderable(cache.content, id, marginBottom),\n      tableContentCache: cache,\n    }\n  }\n\n  private createDefaultRenderable(token: MarkedToken, index: number, hasNextToken: boolean = false): Renderable | null {\n    const id = `${this.id}-block-${index}`\n    const marginBottom = this.getInterBlockMargin(token, hasNextToken)\n\n    if (token.type === \"code\") {\n      return this.createCodeRenderable(token, id, marginBottom)\n    }\n\n    if (token.type === \"table\") {\n      return this.createTableBlock(token, id, marginBottom).renderable\n    }\n\n    if (token.type === \"space\") {\n      return null\n    }\n\n    if (!token.raw) {\n      return null\n    }\n\n    return this.createMarkdownCodeRenderable(token.raw, id, marginBottom)\n  }\n\n  private updateBlockRenderable(state: BlockState, token: MarkedToken, index: number, hasNextToken: boolean): void {\n    const marginBottom = this.getInterBlockMargin(token, hasNextToken)\n\n    if (token.type === \"code\") {\n      this.applyCodeBlockRenderable(state.renderable as CodeRenderable, token as Tokens.Code, marginBottom)\n      return\n    }\n\n    if (token.type === \"table\") {\n      const tableToken = token as Tokens.Table\n      const { cache, changed } = this.buildTableContentCache(tableToken, state.tableContentCache)\n\n      if (!cache) {\n        if (state.renderable instanceof CodeRenderable) {\n          this.applyMarkdownCodeRenderable(state.renderable, tableToken.raw, marginBottom)\n          state.tableContentCache = undefined\n          return\n        }\n\n        state.renderable.destroyRecursively()\n        const fallbackRenderable = this.createMarkdownCodeRenderable(\n          tableToken.raw,\n          `${this.id}-block-${index}`,\n          marginBottom,\n        )\n        this.add(fallbackRenderable)\n        state.renderable = fallbackRenderable\n        state.tableContentCache = undefined\n        return\n      }\n\n      if (state.renderable instanceof TextTableRenderable) {\n        if (changed) {\n          state.renderable.content = cache.content\n        }\n        this.applyTableRenderableOptions(state.renderable, this.resolveTableRenderableOptions())\n        state.renderable.marginBottom = marginBottom\n        state.tableContentCache = cache\n        return\n      }\n\n      state.renderable.destroyRecursively()\n      const tableRenderable = this.createTextTableRenderable(cache.content, `${this.id}-block-${index}`, marginBottom)\n      this.add(tableRenderable)\n      state.renderable = tableRenderable\n      state.tableContentCache = cache\n      return\n    }\n\n    if (state.renderable instanceof CodeRenderable) {\n      this.applyMarkdownCodeRenderable(state.renderable, token.raw, marginBottom)\n      return\n    }\n\n    state.renderable.destroyRecursively()\n    const markdownRenderable = this.createMarkdownCodeRenderable(token.raw, `${this.id}-block-${index}`, marginBottom)\n    this.add(markdownRenderable)\n    state.renderable = markdownRenderable\n  }\n\n  private updateBlocks(forceTableRefresh: boolean = false): void {\n    if (this.isDestroyed) return\n    if (!this._content) {\n      this.clearBlockStates()\n      this._parseState = null\n      return\n    }\n\n    const trailingUnstable = this._streaming ? 2 : 0\n    this._parseState = parseMarkdownIncremental(this._content, this._parseState, trailingUnstable)\n\n    const tokens = this._parseState.tokens\n\n    // Parse failure fallback\n    if (tokens.length === 0 && this._content.length > 0) {\n      this.clearBlockStates()\n      const fallback = this.createMarkdownCodeRenderable(this._content, `${this.id}-fallback`)\n      this.add(fallback)\n      this._blockStates = [\n        {\n          token: { type: \"text\", raw: this._content, text: this._content } as MarkedToken,\n          tokenRaw: this._content,\n          renderable: fallback,\n        },\n      ]\n      return\n    }\n\n    const blockTokens = this.buildRenderableTokens(tokens)\n    const lastBlockIndex = blockTokens.length - 1\n\n    let blockIndex = 0\n    for (let i = 0; i < blockTokens.length; i++) {\n      const token = blockTokens[i]\n      const hasNextToken = i < lastBlockIndex\n      const existing = this._blockStates[blockIndex]\n\n      const shouldForceRefresh = forceTableRefresh\n\n      // Same token object reference means unchanged\n      if (existing && existing.token === token) {\n        if (shouldForceRefresh) {\n          this.updateBlockRenderable(existing, token, blockIndex, hasNextToken)\n          existing.tokenRaw = token.raw\n        }\n        blockIndex++\n        continue\n      }\n\n      // Same content, update reference\n      if (existing && existing.tokenRaw === token.raw && existing.token.type === token.type) {\n        existing.token = token\n        if (shouldForceRefresh) {\n          this.updateBlockRenderable(existing, token, blockIndex, hasNextToken)\n          existing.tokenRaw = token.raw\n        }\n        blockIndex++\n        continue\n      }\n\n      // Same type, different content - update in place\n      if (existing && existing.token.type === token.type) {\n        this.updateBlockRenderable(existing, token, blockIndex, hasNextToken)\n        existing.token = token\n        existing.tokenRaw = token.raw\n        blockIndex++\n        continue\n      }\n\n      // Different type or new block\n      if (existing) {\n        existing.renderable.destroyRecursively()\n      }\n\n      let renderable: Renderable | undefined\n      let tableContentCache: TableContentCache | undefined\n\n      if (this._renderNode) {\n        const context: RenderNodeContext = {\n          syntaxStyle: this._syntaxStyle,\n          conceal: this._conceal,\n          concealCode: this._concealCode,\n          treeSitterClient: this._treeSitterClient,\n          defaultRender: () => this.createDefaultRenderable(token, blockIndex, hasNextToken),\n        }\n        const custom = this._renderNode(token, context)\n        if (custom) {\n          renderable = custom\n        }\n      }\n\n      if (!renderable) {\n        if (token.type === \"table\") {\n          const tableBlock = this.createTableBlock(\n            token,\n            `${this.id}-block-${blockIndex}`,\n            this.getInterBlockMargin(token, hasNextToken),\n          )\n          renderable = tableBlock.renderable\n          tableContentCache = tableBlock.tableContentCache\n        } else {\n          renderable = this.createDefaultRenderable(token, blockIndex, hasNextToken) ?? undefined\n        }\n      }\n\n      if (token.type === \"table\" && !tableContentCache && renderable instanceof TextTableRenderable) {\n        const { cache } = this.buildTableContentCache(token as Tokens.Table)\n        tableContentCache = cache ?? undefined\n      }\n\n      if (renderable) {\n        this.add(renderable)\n        this._blockStates[blockIndex] = {\n          token,\n          tokenRaw: token.raw,\n          renderable,\n          tableContentCache,\n        }\n      }\n      blockIndex++\n    }\n\n    while (this._blockStates.length > blockIndex) {\n      const removed = this._blockStates.pop()!\n      removed.renderable.destroyRecursively()\n    }\n  }\n\n  private clearBlockStates(): void {\n    for (const state of this._blockStates) {\n      state.renderable.destroyRecursively()\n    }\n    this._blockStates = []\n  }\n\n  /**\n   * Re-render existing blocks without rebuilding the parse state or block structure.\n   * Used when only style/conceal changes - much faster than full rebuild.\n   */\n  private rerenderBlocks(): void {\n    for (let i = 0; i < this._blockStates.length; i++) {\n      const state = this._blockStates[i]\n      const hasNextToken = i < this._blockStates.length - 1\n      const marginBottom = this.getInterBlockMargin(state.token, hasNextToken)\n\n      if (state.token.type === \"code\") {\n        this.applyCodeBlockRenderable(state.renderable as CodeRenderable, state.token as Tokens.Code, marginBottom)\n        continue\n      }\n\n      if (state.token.type === \"table\") {\n        const tableToken = state.token as Tokens.Table\n        const { cache } = this.buildTableContentCache(tableToken, state.tableContentCache, true)\n\n        if (!cache) {\n          if (state.renderable instanceof CodeRenderable) {\n            this.applyMarkdownCodeRenderable(state.renderable, tableToken.raw, marginBottom)\n          } else {\n            state.renderable.destroyRecursively()\n            const fallbackRenderable = this.createMarkdownCodeRenderable(\n              tableToken.raw,\n              `${this.id}-block-${i}`,\n              marginBottom,\n            )\n            this.add(fallbackRenderable)\n            state.renderable = fallbackRenderable\n          }\n          state.tableContentCache = undefined\n          continue\n        }\n\n        if (state.renderable instanceof TextTableRenderable) {\n          state.renderable.content = cache.content\n          this.applyTableRenderableOptions(state.renderable, this.resolveTableRenderableOptions())\n          state.renderable.marginBottom = marginBottom\n          state.tableContentCache = cache\n          continue\n        }\n\n        state.renderable.destroyRecursively()\n        const tableRenderable = this.createTextTableRenderable(cache.content, `${this.id}-block-${i}`, marginBottom)\n        this.add(tableRenderable)\n        state.renderable = tableRenderable\n        state.tableContentCache = cache\n        continue\n      }\n\n      if (state.renderable instanceof CodeRenderable) {\n        this.applyMarkdownCodeRenderable(state.renderable, state.token.raw, marginBottom)\n        continue\n      }\n\n      state.renderable.destroyRecursively()\n      const markdownRenderable = this.createMarkdownCodeRenderable(\n        state.token.raw,\n        `${this.id}-block-${i}`,\n        marginBottom,\n      )\n      this.add(markdownRenderable)\n      state.renderable = markdownRenderable\n    }\n  }\n\n  public clearCache(): void {\n    this._parseState = null\n    this.clearBlockStates()\n    this.updateBlocks()\n    this.requestRender()\n  }\n\n  public refreshStyles(): void {\n    this._styleDirty = false\n    this.rerenderBlocks()\n    this.requestRender()\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    // Check if style/conceal changed - re-render blocks before rendering\n    if (this._styleDirty) {\n      this._styleDirty = false\n      this.rerenderBlocks()\n    }\n    super.renderSelf(buffer, deltaTime)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/ScrollBar.ts",
    "content": "import type { OptimizedBuffer } from \"../buffer.js\"\nimport { parseColor, RGBA, type ColorInput } from \"../lib/index.js\"\nimport type { KeyEvent } from \"../lib/KeyHandler.js\"\nimport { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport type { RenderContext, Timeout } from \"../types.js\"\nimport { type BoxOptions } from \"./Box.js\"\nimport { SliderRenderable, type SliderOptions } from \"./Slider.js\"\n\nexport interface ScrollBarOptions extends RenderableOptions<ScrollBarRenderable> {\n  orientation: \"vertical\" | \"horizontal\"\n  showArrows?: boolean\n  arrowOptions?: Omit<ArrowOptions, \"direction\">\n  trackOptions?: Partial<SliderOptions>\n  onChange?: (position: number) => void\n}\n\nexport type ScrollUnit = \"absolute\" | \"viewport\" | \"content\" | \"step\"\n\nexport class ScrollBarRenderable extends Renderable {\n  public readonly slider: SliderRenderable\n  public readonly startArrow: ArrowRenderable\n  public readonly endArrow: ArrowRenderable\n  public readonly orientation: \"vertical\" | \"horizontal\"\n\n  protected _focusable: boolean = true\n\n  private _scrollSize = 0\n  private _scrollPosition = 0\n  private _viewportSize = 0\n  private _showArrows = false\n  private _manualVisibility = false\n\n  private _onChange: ((position: number) => void) | undefined\n\n  scrollStep: number | undefined | null = null\n\n  get visible(): boolean {\n    return super.visible\n  }\n\n  set visible(value: boolean) {\n    this._manualVisibility = true\n    super.visible = value\n  }\n\n  public resetVisibilityControl(): void {\n    this._manualVisibility = false\n    this.recalculateVisibility()\n  }\n\n  get scrollSize(): number {\n    return this._scrollSize\n  }\n\n  get scrollPosition(): number {\n    return this._scrollPosition\n  }\n\n  get viewportSize(): number {\n    return this._viewportSize\n  }\n\n  set scrollSize(value: number) {\n    if (value === this.scrollSize) return\n    this._scrollSize = value\n    this.recalculateVisibility()\n    this.updateSliderFromScrollState()\n    this.scrollPosition = this.scrollPosition\n  }\n\n  set scrollPosition(value: number) {\n    const newPosition = Math.round(Math.min(Math.max(0, value), this.scrollSize - this.viewportSize))\n    if (newPosition !== this._scrollPosition) {\n      this._scrollPosition = newPosition\n      this.updateSliderFromScrollState()\n      // Events are triggered by the slider change event\n      // this._onChange?.(newPosition)\n      // this.emit(\"change\", { position: newPosition })\n    }\n  }\n\n  set viewportSize(value: number) {\n    if (value === this.viewportSize) return\n    this._viewportSize = value\n    this.slider.viewPortSize = Math.max(1, this._viewportSize)\n    this.recalculateVisibility()\n    this.updateSliderFromScrollState()\n    this.scrollPosition = this.scrollPosition\n  }\n\n  get showArrows(): boolean {\n    return this._showArrows\n  }\n\n  set showArrows(value: boolean) {\n    if (value === this._showArrows) return\n    this._showArrows = value\n    this.startArrow.visible = value\n    this.endArrow.visible = value\n  }\n\n  constructor(\n    ctx: RenderContext,\n    { trackOptions, arrowOptions, orientation, showArrows = false, ...options }: ScrollBarOptions,\n  ) {\n    super(ctx, {\n      flexDirection: orientation === \"vertical\" ? \"column\" : \"row\",\n      alignSelf: \"stretch\",\n      alignItems: \"stretch\",\n      ...(options as BoxOptions),\n    })\n\n    this._onChange = options.onChange\n\n    this.orientation = orientation\n    this._showArrows = showArrows\n\n    const scrollRange = Math.max(0, this._scrollSize - this._viewportSize)\n\n    const defaultStepSize = Math.max(1, this._viewportSize)\n    const stepSize = trackOptions?.viewPortSize ?? defaultStepSize\n\n    this.slider = new SliderRenderable(ctx, {\n      orientation,\n      min: 0,\n      max: scrollRange,\n      value: this._scrollPosition,\n      viewPortSize: stepSize,\n      onChange: (value) => {\n        this._scrollPosition = Math.round(value)\n        this._onChange?.(this._scrollPosition)\n        this.emit(\"change\", { position: this._scrollPosition })\n      },\n      ...(orientation === \"vertical\"\n        ? {\n            width: Math.max(1, Math.min(2, this.width)),\n            height: \"100%\",\n            marginLeft: \"auto\",\n          }\n        : {\n            width: \"100%\",\n            height: 1,\n            marginTop: \"auto\",\n          }),\n      flexGrow: 1,\n      flexShrink: 1,\n      ...trackOptions,\n    })\n\n    this.updateSliderFromScrollState()\n\n    const arrowOpts = arrowOptions\n      ? {\n          foregroundColor: arrowOptions.backgroundColor,\n          backgroundColor: arrowOptions.backgroundColor,\n          attributes: arrowOptions.attributes,\n          ...arrowOptions,\n        }\n      : {}\n\n    this.startArrow = new ArrowRenderable(ctx, {\n      alignSelf: \"center\",\n      visible: this.showArrows,\n      direction: this.orientation === \"vertical\" ? \"up\" : \"left\",\n      height: this.orientation === \"vertical\" ? 1 : 1,\n      ...arrowOpts,\n    })\n\n    this.endArrow = new ArrowRenderable(ctx, {\n      alignSelf: \"center\",\n      visible: this.showArrows,\n      direction: this.orientation === \"vertical\" ? \"down\" : \"right\",\n      height: this.orientation === \"vertical\" ? 1 : 1,\n      ...arrowOpts,\n    })\n\n    this.add(this.startArrow)\n    this.add(this.slider)\n    this.add(this.endArrow)\n\n    let startArrowMouseTimeout = undefined as Timeout\n    let endArrowMouseTimeout = undefined as Timeout\n\n    this.startArrow.onMouseDown = (event) => {\n      event.stopPropagation()\n      event.preventDefault()\n\n      this.scrollBy(-0.5, \"viewport\")\n\n      startArrowMouseTimeout = setTimeout(() => {\n        this.scrollBy(-0.5, \"viewport\")\n\n        startArrowMouseTimeout = setInterval(() => {\n          this.scrollBy(-0.2, \"viewport\")\n        }, 200)\n      }, 500)\n    }\n\n    this.startArrow.onMouseUp = (event) => {\n      event.stopPropagation()\n      clearInterval(startArrowMouseTimeout!)\n    }\n\n    this.endArrow.onMouseDown = (event) => {\n      event.stopPropagation()\n      event.preventDefault()\n\n      this.scrollBy(0.5, \"viewport\")\n\n      endArrowMouseTimeout = setTimeout(() => {\n        this.scrollBy(0.5, \"viewport\")\n\n        endArrowMouseTimeout = setInterval(() => {\n          this.scrollBy(0.2, \"viewport\")\n        }, 200)\n      }, 500)\n    }\n\n    this.endArrow.onMouseUp = (event) => {\n      event.stopPropagation()\n      clearInterval(endArrowMouseTimeout!)\n    }\n  }\n\n  public set arrowOptions(options: ScrollBarOptions[\"arrowOptions\"]) {\n    Object.assign(this.startArrow, options)\n    Object.assign(this.endArrow, options)\n    this.requestRender()\n  }\n\n  public set trackOptions(options: ScrollBarOptions[\"trackOptions\"]) {\n    Object.assign(this.slider, options)\n    this.requestRender()\n  }\n\n  private updateSliderFromScrollState(): void {\n    const scrollRange = Math.max(0, this._scrollSize - this._viewportSize)\n\n    this.slider.min = 0\n    this.slider.max = scrollRange\n\n    this.slider.value = Math.min(this._scrollPosition, scrollRange)\n  }\n\n  public scrollBy(delta: number, unit: ScrollUnit = \"absolute\"): void {\n    const multiplier =\n      unit === \"viewport\"\n        ? this.viewportSize\n        : unit === \"content\"\n          ? this.scrollSize\n          : unit === \"step\"\n            ? (this.scrollStep ?? 1)\n            : 1\n\n    const resolvedDelta = multiplier * delta\n    this.scrollPosition += resolvedDelta\n  }\n\n  private recalculateVisibility(): void {\n    if (!this._manualVisibility) {\n      const sizeRatio = this.scrollSize <= this.viewportSize ? 1 : this.viewportSize / this.scrollSize\n      super.visible = sizeRatio < 1\n    }\n  }\n\n  public handleKeyPress(key: KeyEvent): boolean {\n    switch (key.name) {\n      case \"left\":\n      case \"h\":\n        if (this.orientation !== \"horizontal\") return false\n        this.scrollBy(-1 / 5, \"viewport\")\n        return true\n      case \"right\":\n      case \"l\":\n        if (this.orientation !== \"horizontal\") return false\n        this.scrollBy(1 / 5, \"viewport\")\n        return true\n      case \"up\":\n      case \"k\":\n        if (this.orientation !== \"vertical\") return false\n        this.scrollBy(-1 / 5, \"viewport\")\n        return true\n      case \"down\":\n      case \"j\":\n        if (this.orientation !== \"vertical\") return false\n        this.scrollBy(1 / 5, \"viewport\")\n        return true\n      case \"pageup\":\n        this.scrollBy(-1 / 2, \"viewport\")\n        return true\n      case \"pagedown\":\n        this.scrollBy(1 / 2, \"viewport\")\n        return true\n      case \"home\":\n        this.scrollBy(-1, \"content\")\n        return true\n      case \"end\":\n        this.scrollBy(1, \"content\")\n        return true\n    }\n\n    return false\n  }\n}\n\nexport interface ArrowOptions extends RenderableOptions<ArrowRenderable> {\n  direction: \"up\" | \"down\" | \"left\" | \"right\"\n  foregroundColor?: ColorInput\n  backgroundColor?: ColorInput\n  attributes?: number\n  arrowChars?: {\n    up?: string\n    down?: string\n    left?: string\n    right?: string\n  }\n}\n\nexport class ArrowRenderable extends Renderable {\n  private _direction: \"up\" | \"down\" | \"left\" | \"right\"\n  private _foregroundColor: RGBA\n  private _backgroundColor: RGBA\n  private _attributes: number\n  private _arrowChars: {\n    up: string\n    down: string\n    left: string\n    right: string\n  }\n\n  constructor(ctx: RenderContext, options: ArrowOptions) {\n    super(ctx, options)\n    this._direction = options.direction\n    this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : RGBA.fromValues(1, 1, 1, 1)\n    this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : RGBA.fromValues(0, 0, 0, 0)\n    this._attributes = options.attributes ?? 0\n\n    this._arrowChars = {\n      up: \"▲\",\n      down: \"▼\",\n      left: \"◀\",\n      right: \"▶\",\n      ...options.arrowChars,\n    }\n\n    if (!options.width) {\n      this.width = Bun.stringWidth(this.getArrowChar())\n    }\n  }\n\n  get direction(): \"up\" | \"down\" | \"left\" | \"right\" {\n    return this._direction\n  }\n\n  set direction(value: \"up\" | \"down\" | \"left\" | \"right\") {\n    if (this._direction !== value) {\n      this._direction = value\n      this.requestRender()\n    }\n  }\n\n  get foregroundColor(): RGBA {\n    return this._foregroundColor\n  }\n\n  set foregroundColor(value: ColorInput) {\n    if (this._foregroundColor !== value) {\n      this._foregroundColor = parseColor(value)\n      this.requestRender()\n    }\n  }\n\n  get backgroundColor(): RGBA {\n    return this._backgroundColor\n  }\n\n  set backgroundColor(value: ColorInput) {\n    if (this._backgroundColor !== value) {\n      this._backgroundColor = parseColor(value)\n      this.requestRender()\n    }\n  }\n\n  get attributes(): number {\n    return this._attributes\n  }\n\n  set attributes(value: number) {\n    if (this._attributes !== value) {\n      this._attributes = value\n      this.requestRender()\n    }\n  }\n\n  set arrowChars(value: ArrowOptions[\"arrowChars\"]) {\n    this._arrowChars = {\n      ...this._arrowChars,\n      ...value,\n    }\n    this.requestRender()\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    const char = this.getArrowChar()\n    buffer.drawText(char, this.x, this.y, this._foregroundColor, this._backgroundColor, this._attributes)\n  }\n\n  private getArrowChar(): string {\n    switch (this._direction) {\n      case \"up\":\n        return this._arrowChars.up\n      case \"down\":\n        return this._arrowChars.down\n      case \"left\":\n        return this._arrowChars.left\n      case \"right\":\n        return this._arrowChars.right\n      default:\n        return \"?\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/ScrollBox.ts",
    "content": "import { type KeyEvent } from \"../lib/index.js\"\nimport { getObjectsInViewport } from \"../lib/objects-in-viewport.js\"\nimport { LinearScrollAccel, MacOSScrollAccel, type ScrollAcceleration } from \"../lib/scroll-acceleration.js\"\nimport type { Renderable, RenderableOptions } from \"../Renderable.js\"\nimport type { MouseEvent } from \"../renderer.js\"\nimport type { RenderContext } from \"../types.js\"\nimport { BoxRenderable, type BoxOptions } from \"./Box.js\"\nimport type { VNode } from \"./composition/vnode.js\"\nimport { ScrollBarRenderable, type ScrollBarOptions, type ScrollUnit } from \"./ScrollBar.js\"\n\nclass ContentRenderable extends BoxRenderable {\n  private viewport: BoxRenderable\n  private _viewportCulling: boolean\n\n  constructor(\n    ctx: RenderContext,\n    viewport: BoxRenderable,\n    viewportCulling: boolean,\n    options: RenderableOptions<BoxRenderable>,\n  ) {\n    super(ctx, options)\n    this.viewport = viewport\n    this._viewportCulling = viewportCulling\n  }\n\n  get viewportCulling(): boolean {\n    return this._viewportCulling\n  }\n\n  set viewportCulling(value: boolean) {\n    this._viewportCulling = value\n  }\n\n  protected _getVisibleChildren(): number[] {\n    if (this._viewportCulling) {\n      return getObjectsInViewport(this.viewport, this.getChildrenSortedByPrimaryAxis(), this.primaryAxis, 0).map(\n        (child) => child.num,\n      )\n    }\n    return this.getChildrenSortedByPrimaryAxis().map((child) => child.num)\n  }\n}\n\nexport interface ScrollBoxOptions extends BoxOptions<ScrollBoxRenderable> {\n  rootOptions?: BoxOptions\n  wrapperOptions?: BoxOptions\n  viewportOptions?: BoxOptions\n  contentOptions?: BoxOptions\n  scrollbarOptions?: Omit<ScrollBarOptions, \"orientation\">\n  verticalScrollbarOptions?: Omit<ScrollBarOptions, \"orientation\">\n  horizontalScrollbarOptions?: Omit<ScrollBarOptions, \"orientation\">\n  stickyScroll?: boolean\n  stickyStart?: \"bottom\" | \"top\" | \"left\" | \"right\"\n  scrollX?: boolean\n  scrollY?: boolean\n  scrollAcceleration?: ScrollAcceleration\n  viewportCulling?: boolean\n}\n\nconst SCROLLBOX_PADDING_KEYS = [\n  \"padding\",\n  \"paddingX\",\n  \"paddingY\",\n  \"paddingTop\",\n  \"paddingRight\",\n  \"paddingBottom\",\n  \"paddingLeft\",\n] as const\n\ntype ScrollBoxPaddingKey = (typeof SCROLLBOX_PADDING_KEYS)[number]\ntype ScrollBoxPaddingOptions = Pick<ScrollBoxOptions, ScrollBoxPaddingKey>\n\nfunction pickScrollBoxPadding(options: Partial<ScrollBoxOptions> | undefined): Partial<ScrollBoxPaddingOptions> {\n  if (!options) return {}\n\n  const picked: Partial<ScrollBoxPaddingOptions> = {}\n  for (const key of SCROLLBOX_PADDING_KEYS) {\n    const value = options[key]\n    if (value !== undefined) {\n      picked[key] = value\n    }\n  }\n\n  return picked\n}\n\nfunction stripScrollBoxPadding<T extends object>(options: T): Omit<T, ScrollBoxPaddingKey> {\n  const sanitized = { ...options }\n  for (const key of SCROLLBOX_PADDING_KEYS) {\n    delete (sanitized as Partial<Record<ScrollBoxPaddingKey, unknown>>)[key]\n  }\n  return sanitized as Omit<T, ScrollBoxPaddingKey>\n}\n\nexport class ScrollBoxRenderable extends BoxRenderable {\n  static idCounter = 0\n  private internalId = 0\n  public readonly wrapper: BoxRenderable\n  public readonly viewport: BoxRenderable\n  public readonly content: ContentRenderable\n  public readonly horizontalScrollBar: ScrollBarRenderable\n  public readonly verticalScrollBar: ScrollBarRenderable\n\n  protected _focusable: boolean = true\n  private selectionListener?: () => void\n\n  private autoScrollMouseX: number = 0\n  private autoScrollMouseY: number = 0\n  private readonly autoScrollThresholdVertical = 3\n  private readonly autoScrollThresholdHorizontal = 3\n  private readonly autoScrollSpeedSlow = 6\n  private readonly autoScrollSpeedMedium = 36\n  private readonly autoScrollSpeedFast = 72\n  private isAutoScrolling: boolean = false\n  private cachedAutoScrollSpeed: number = 3\n  private autoScrollAccumulatorX: number = 0\n  private autoScrollAccumulatorY: number = 0\n\n  private scrollAccumulatorX: number = 0\n  private scrollAccumulatorY: number = 0\n\n  private _stickyScroll: boolean\n  private _stickyScrollTop: boolean = false\n  private _stickyScrollBottom: boolean = false\n  private _stickyScrollLeft: boolean = false\n  private _stickyScrollRight: boolean = false\n  private _stickyStart?: \"bottom\" | \"top\" | \"left\" | \"right\"\n  private _hasManualScroll: boolean = false\n  private _isApplyingStickyScroll: boolean = false\n  private scrollAccel: ScrollAcceleration\n\n  get stickyScroll(): boolean {\n    return this._stickyScroll\n  }\n\n  set stickyScroll(value: boolean) {\n    this._stickyScroll = value\n    this.updateStickyState()\n  }\n\n  get stickyStart(): \"bottom\" | \"top\" | \"left\" | \"right\" | undefined {\n    return this._stickyStart\n  }\n\n  set stickyStart(value: \"bottom\" | \"top\" | \"left\" | \"right\" | undefined) {\n    this._stickyStart = value\n    this.updateStickyState()\n  }\n\n  get scrollTop(): number {\n    return this.verticalScrollBar.scrollPosition\n  }\n\n  set scrollTop(value: number) {\n    this.verticalScrollBar.scrollPosition = value\n    if (!this._isApplyingStickyScroll) {\n      // Only mark as manual scroll if:\n      // 1. We're not at a sticky position after scrolling (prevents programmatic scrolls to sticky edges from disabling sticky)\n      // 2. There's actually meaningful scrollable content (prevents accidental scrolls when content is smaller than viewport from disabling sticky)\n      // Use a small threshold (>1) to account for rounding/layout quirks\n      const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height)\n      if (!this.isAtStickyPosition() && maxScrollTop > 1) {\n        this._hasManualScroll = true\n      }\n    }\n    this.updateStickyState()\n  }\n\n  get scrollLeft(): number {\n    return this.horizontalScrollBar.scrollPosition\n  }\n\n  set scrollLeft(value: number) {\n    this.horizontalScrollBar.scrollPosition = value\n    if (!this._isApplyingStickyScroll) {\n      // Only mark as manual scroll if:\n      // 1. We're not at a sticky position after scrolling (prevents programmatic scrolls to sticky edges from disabling sticky)\n      // 2. There's actually meaningful scrollable content (prevents accidental scrolls when content is smaller than viewport from disabling sticky)\n      // Use a small threshold (>1) to account for rounding/layout quirks\n      const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width)\n      if (!this.isAtStickyPosition() && maxScrollLeft > 1) {\n        this._hasManualScroll = true\n      }\n    }\n    this.updateStickyState()\n  }\n\n  get scrollWidth(): number {\n    return this.horizontalScrollBar.scrollSize\n  }\n\n  get scrollHeight(): number {\n    return this.verticalScrollBar.scrollSize\n  }\n\n  private updateStickyState(): void {\n    if (!this._stickyScroll) return\n\n    const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height)\n    const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width)\n\n    if (this.scrollTop <= 0) {\n      this._stickyScrollTop = true\n      this._stickyScrollBottom = false\n      if (\n        !this._isApplyingStickyScroll &&\n        (this._stickyStart === \"top\" || (this._stickyStart === \"bottom\" && maxScrollTop === 0))\n      ) {\n        this._hasManualScroll = false\n      }\n    } else if (this.scrollTop >= maxScrollTop) {\n      this._stickyScrollTop = false\n      this._stickyScrollBottom = true\n      if (!this._isApplyingStickyScroll && this._stickyStart === \"bottom\") {\n        this._hasManualScroll = false\n      }\n    } else {\n      this._stickyScrollTop = false\n      this._stickyScrollBottom = false\n    }\n\n    if (this.scrollLeft <= 0) {\n      this._stickyScrollLeft = true\n      this._stickyScrollRight = false\n      if (\n        !this._isApplyingStickyScroll &&\n        (this._stickyStart === \"left\" || (this._stickyStart === \"right\" && maxScrollLeft === 0))\n      ) {\n        this._hasManualScroll = false\n      }\n    } else if (this.scrollLeft >= maxScrollLeft) {\n      this._stickyScrollLeft = false\n      this._stickyScrollRight = true\n      if (!this._isApplyingStickyScroll && this._stickyStart === \"right\") {\n        this._hasManualScroll = false\n      }\n    } else {\n      this._stickyScrollLeft = false\n      this._stickyScrollRight = false\n    }\n  }\n\n  private applyStickyStart(stickyStart: \"bottom\" | \"top\" | \"left\" | \"right\"): void {\n    const wasApplyingStickyScroll = this._isApplyingStickyScroll\n    this._isApplyingStickyScroll = true\n    try {\n      switch (stickyStart) {\n        case \"top\":\n          this._stickyScrollTop = true\n          this._stickyScrollBottom = false\n          this.verticalScrollBar.scrollPosition = 0\n          break\n        case \"bottom\":\n          this._stickyScrollTop = false\n          this._stickyScrollBottom = true\n          this.verticalScrollBar.scrollPosition = Math.max(0, this.scrollHeight - this.viewport.height)\n          break\n        case \"left\":\n          this._stickyScrollLeft = true\n          this._stickyScrollRight = false\n          this.horizontalScrollBar.scrollPosition = 0\n          break\n        case \"right\":\n          this._stickyScrollLeft = false\n          this._stickyScrollRight = true\n          this.horizontalScrollBar.scrollPosition = Math.max(0, this.scrollWidth - this.viewport.width)\n          break\n      }\n    } finally {\n      this._isApplyingStickyScroll = wasApplyingStickyScroll\n    }\n  }\n\n  constructor(ctx: RenderContext, options: ScrollBoxOptions) {\n    const {\n      wrapperOptions,\n      viewportOptions,\n      contentOptions,\n      rootOptions,\n      scrollbarOptions,\n      verticalScrollbarOptions,\n      horizontalScrollbarOptions,\n      stickyScroll = false,\n      stickyStart,\n      scrollX = false,\n      scrollY = true,\n      scrollAcceleration,\n      viewportCulling = true,\n      ...rootBoxOptions\n    } = options\n\n    const forwardedContentPadding = {\n      ...pickScrollBoxPadding(rootBoxOptions),\n      ...pickScrollBoxPadding(rootOptions),\n    }\n\n    const sanitizedRootBoxOptions = stripScrollBoxPadding(rootBoxOptions)\n    const sanitizedRootOptions = rootOptions ? stripScrollBoxPadding(rootOptions) : undefined\n    const mergedContentOptions = {\n      ...forwardedContentPadding,\n      ...contentOptions,\n    }\n\n    // Root\n    super(ctx, {\n      flexDirection: \"row\",\n      alignItems: \"stretch\",\n      ...(sanitizedRootBoxOptions as BoxOptions),\n      ...(sanitizedRootOptions as BoxOptions),\n    })\n\n    this.internalId = ScrollBoxRenderable.idCounter++\n    this._stickyScroll = stickyScroll\n    this._stickyStart = stickyStart\n    this.scrollAccel = scrollAcceleration ?? new LinearScrollAccel()\n\n    this.wrapper = new BoxRenderable(ctx, {\n      flexDirection: \"column\",\n      flexGrow: 1,\n      ...wrapperOptions,\n      id: `scroll-box-wrapper-${this.internalId}`,\n    })\n    super.add(this.wrapper)\n\n    this.viewport = new BoxRenderable(ctx, {\n      flexDirection: \"column\",\n      flexGrow: 1,\n      // NOTE: Overflow scroll makes the content size behave weird\n      // when the scrollbox is in a container with max-width/height\n      overflow: \"hidden\",\n      onSizeChange: () => {\n        this.recalculateBarProps()\n      },\n      ...viewportOptions,\n      id: `scroll-box-viewport-${this.internalId}`,\n    })\n    this.wrapper.add(this.viewport)\n\n    this.content = new ContentRenderable(ctx, this.viewport, viewportCulling, {\n      alignSelf: \"flex-start\",\n      flexShrink: 0,\n      ...(scrollX ? { minWidth: \"100%\" } : { minWidth: \"100%\", maxWidth: \"100%\" }),\n      ...(scrollY ? { minHeight: \"100%\" } : { minHeight: \"100%\", maxHeight: \"100%\" }),\n      onSizeChange: () => {\n        this.recalculateBarProps()\n      },\n      ...mergedContentOptions,\n      id: `scroll-box-content-${this.internalId}`,\n    })\n    this.viewport.add(this.content)\n\n    this.verticalScrollBar = new ScrollBarRenderable(ctx, {\n      ...scrollbarOptions,\n      ...verticalScrollbarOptions,\n      arrowOptions: {\n        ...scrollbarOptions?.arrowOptions,\n        ...verticalScrollbarOptions?.arrowOptions,\n      },\n      id: `scroll-box-vertical-scrollbar-${this.internalId}`,\n      orientation: \"vertical\",\n      onChange: (position) => {\n        this.content.translateY = -position\n        if (!this._isApplyingStickyScroll) {\n          // Only mark as manual scroll if we're not at a sticky position and there's meaningful scrollable content\n          const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height)\n          if (!this.isAtStickyPosition() && maxScrollTop > 1) {\n            this._hasManualScroll = true\n          }\n        }\n        this.updateStickyState()\n      },\n    })\n    super.add(this.verticalScrollBar)\n\n    this.horizontalScrollBar = new ScrollBarRenderable(ctx, {\n      ...scrollbarOptions,\n      ...horizontalScrollbarOptions,\n      arrowOptions: {\n        ...scrollbarOptions?.arrowOptions,\n        ...horizontalScrollbarOptions?.arrowOptions,\n      },\n      id: `scroll-box-horizontal-scrollbar-${this.internalId}`,\n      orientation: \"horizontal\",\n      onChange: (position) => {\n        this.content.translateX = -position\n        if (!this._isApplyingStickyScroll) {\n          // Only mark as manual scroll if we're not at a sticky position and there's meaningful scrollable content\n          const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width)\n          if (!this.isAtStickyPosition() && maxScrollLeft > 1) {\n            this._hasManualScroll = true\n          }\n        }\n        this.updateStickyState()\n      },\n    })\n    this.wrapper.add(this.horizontalScrollBar)\n\n    this.recalculateBarProps()\n\n    if (stickyStart && stickyScroll) {\n      this.applyStickyStart(stickyStart)\n    }\n\n    this.selectionListener = () => {\n      const selection = this._ctx.getSelection()\n      if (!selection || !selection.isDragging) {\n        this.stopAutoScroll()\n      }\n    }\n    this._ctx.on(\"selection\", this.selectionListener)\n  }\n\n  protected onUpdate(deltaTime: number): void {\n    this.handleAutoScroll(deltaTime)\n  }\n\n  public scrollBy(delta: number | { x: number; y: number }, unit: ScrollUnit = \"absolute\"): void {\n    if (typeof delta === \"number\") {\n      this.verticalScrollBar.scrollBy(delta, unit)\n    } else {\n      this.verticalScrollBar.scrollBy(delta.y, unit)\n      this.horizontalScrollBar.scrollBy(delta.x, unit)\n    }\n    // Note: scrollBy doesn't need to set _hasManualScroll here because the scrollbar\n    // change will trigger the scrollTop setter which handles it\n  }\n\n  public scrollChildIntoView(childId: string): void {\n    const child = this.content.findDescendantById(childId)\n    if (!child) return\n\n    const getNearestDelta = (\n      elementStart: number,\n      elementEnd: number,\n      viewportStart: number,\n      viewportEnd: number,\n    ): number => {\n      const elementSize = elementEnd - elementStart\n      const viewportSize = viewportEnd - viewportStart\n      const elementStartOutside = elementStart < viewportStart\n      const elementEndOutside = elementEnd > viewportEnd\n\n      if (elementStartOutside && elementEndOutside) {\n        return 0\n      }\n\n      if ((elementStartOutside && elementSize < viewportSize) || (elementEndOutside && elementSize > viewportSize)) {\n        return elementStart - viewportStart\n      }\n\n      if ((elementStartOutside && elementSize > viewportSize) || (elementEndOutside && elementSize < viewportSize)) {\n        return elementEnd - viewportEnd\n      }\n\n      return 0\n    }\n\n    const childTop = child.y\n    const childBottom = child.y + child.height\n    const viewportTop = this.viewport.y\n    const viewportBottom = this.viewport.y + this.viewport.height\n\n    const dy = getNearestDelta(childTop, childBottom, viewportTop, viewportBottom)\n\n    const childLeft = child.x\n    const childRight = child.x + child.width\n    const viewportLeft = this.viewport.x\n    const viewportRight = this.viewport.x + this.viewport.width\n\n    const dx = getNearestDelta(childLeft, childRight, viewportLeft, viewportRight)\n\n    if (dx !== 0 || dy !== 0) {\n      this.scrollBy({ x: dx, y: dy })\n    }\n  }\n\n  public scrollTo(position: number | { x: number; y: number }): void {\n    if (typeof position === \"number\") {\n      this.scrollTop = position\n    } else {\n      this.scrollTop = position.y\n      this.scrollLeft = position.x\n    }\n    // Note: scrollTo doesn't need to set _hasManualScroll here because\n    // the scrollTop/scrollLeft setters handle it\n  }\n\n  private isAtStickyPosition(): boolean {\n    if (!this._stickyScroll || !this._stickyStart) {\n      return false\n    }\n\n    const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height)\n    const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width)\n\n    switch (this._stickyStart) {\n      case \"top\":\n        return this.scrollTop === 0\n      case \"bottom\":\n        return this.scrollTop >= maxScrollTop\n      case \"left\":\n        return this.scrollLeft === 0\n      case \"right\":\n        return this.scrollLeft >= maxScrollLeft\n      default:\n        return false\n    }\n  }\n\n  public add(obj: Renderable | VNode<any, any[]>, index?: number): number {\n    return this.content.add(obj, index)\n  }\n\n  public insertBefore(obj: Renderable | VNode<any, any[]> | unknown, anchor?: Renderable | unknown): number {\n    return this.content.insertBefore(obj, anchor)\n  }\n\n  public remove(id: string): void {\n    this.content.remove(id)\n  }\n\n  public getChildren(): Renderable[] {\n    return this.content.getChildren()\n  }\n\n  protected onMouseEvent(event: MouseEvent): void {\n    if (event.type === \"scroll\") {\n      let dir = event.scroll?.direction\n      if (event.modifiers.shift)\n        dir = dir === \"up\" ? \"left\" : dir === \"down\" ? \"right\" : dir === \"right\" ? \"down\" : \"up\"\n\n      const baseDelta = event.scroll?.delta ?? 0\n      const now = Date.now()\n      const multiplier = this.scrollAccel.tick(now)\n      const scrollAmount = baseDelta * multiplier\n\n      if (dir === \"up\") {\n        this.scrollAccumulatorY -= scrollAmount\n        const integerScroll = Math.trunc(this.scrollAccumulatorY)\n        if (integerScroll !== 0) {\n          this.scrollTop += integerScroll\n          this.scrollAccumulatorY -= integerScroll\n        }\n      } else if (dir === \"down\") {\n        this.scrollAccumulatorY += scrollAmount\n        const integerScroll = Math.trunc(this.scrollAccumulatorY)\n        if (integerScroll !== 0) {\n          this.scrollTop += integerScroll\n          this.scrollAccumulatorY -= integerScroll\n        }\n      } else if (dir === \"left\") {\n        this.scrollAccumulatorX -= scrollAmount\n        const integerScroll = Math.trunc(this.scrollAccumulatorX)\n        if (integerScroll !== 0) {\n          this.scrollLeft += integerScroll\n          this.scrollAccumulatorX -= integerScroll\n        }\n      } else if (dir === \"right\") {\n        this.scrollAccumulatorX += scrollAmount\n        const integerScroll = Math.trunc(this.scrollAccumulatorX)\n        if (integerScroll !== 0) {\n          this.scrollLeft += integerScroll\n          this.scrollAccumulatorX -= integerScroll\n        }\n      }\n\n      // Only mark as manual scroll if there's meaningful scrollable content\n      const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height)\n      const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width)\n      if (maxScrollTop > 1 || maxScrollLeft > 1) {\n        this._hasManualScroll = true\n      }\n    }\n\n    if (event.type === \"drag\" && event.isDragging) {\n      this.updateAutoScroll(event.x, event.y)\n    } else if (event.type === \"up\") {\n      this.stopAutoScroll()\n    }\n  }\n\n  public handleKeyPress(key: KeyEvent): boolean {\n    // Let scrollbars handle their own acceleration\n    if (this.verticalScrollBar.handleKeyPress(key)) {\n      this._hasManualScroll = true\n      this.scrollAccel.reset()\n      this.resetScrollAccumulators()\n      return true\n    }\n    if (this.horizontalScrollBar.handleKeyPress(key)) {\n      this._hasManualScroll = true\n      this.scrollAccel.reset()\n      this.resetScrollAccumulators()\n      return true\n    }\n    return false\n  }\n\n  private resetScrollAccumulators(): void {\n    this.scrollAccumulatorX = 0\n    this.scrollAccumulatorY = 0\n  }\n\n  public startAutoScroll(mouseX: number, mouseY: number): void {\n    this.stopAutoScroll()\n    this.autoScrollMouseX = mouseX\n    this.autoScrollMouseY = mouseY\n    this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY)\n    this.isAutoScrolling = true\n\n    if (!this.live) {\n      this.live = true\n    }\n  }\n\n  public updateAutoScroll(mouseX: number, mouseY: number): void {\n    this.autoScrollMouseX = mouseX\n    this.autoScrollMouseY = mouseY\n\n    // Cache the speed based on current mouse position\n    this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY)\n\n    const scrollX = this.getAutoScrollDirectionX(mouseX)\n    const scrollY = this.getAutoScrollDirectionY(mouseY)\n\n    if (scrollX === 0 && scrollY === 0) {\n      this.stopAutoScroll()\n    } else if (!this.isAutoScrolling) {\n      this.startAutoScroll(mouseX, mouseY)\n    }\n  }\n\n  public stopAutoScroll(): void {\n    const wasAutoScrolling = this.isAutoScrolling\n    this.isAutoScrolling = false\n    this.autoScrollAccumulatorX = 0\n    this.autoScrollAccumulatorY = 0\n\n    // Only turn off live if no other features need it\n    // For now, auto-scroll is the only feature using live, but this could be extended\n    if (wasAutoScrolling && !this.hasOtherLiveReasons()) {\n      this.live = false\n    }\n  }\n\n  private hasOtherLiveReasons(): boolean {\n    // Placeholder for future features that might need live mode\n    // For now, always return false since auto-scroll is the only user\n    return false\n  }\n\n  private handleAutoScroll(deltaTime: number): void {\n    if (!this.isAutoScrolling) return\n\n    const scrollX = this.getAutoScrollDirectionX(this.autoScrollMouseX)\n    const scrollY = this.getAutoScrollDirectionY(this.autoScrollMouseY)\n    const scrollAmount = this.cachedAutoScrollSpeed * (deltaTime / 1000)\n\n    let scrolled = false\n\n    if (scrollX !== 0) {\n      this.autoScrollAccumulatorX += scrollX * scrollAmount\n      const integerScrollX = Math.trunc(this.autoScrollAccumulatorX)\n      if (integerScrollX !== 0) {\n        this.scrollLeft += integerScrollX\n        this.autoScrollAccumulatorX -= integerScrollX\n        scrolled = true\n      }\n    }\n\n    if (scrollY !== 0) {\n      this.autoScrollAccumulatorY += scrollY * scrollAmount\n      const integerScrollY = Math.trunc(this.autoScrollAccumulatorY)\n      if (integerScrollY !== 0) {\n        this.scrollTop += integerScrollY\n        this.autoScrollAccumulatorY -= integerScrollY\n        scrolled = true\n      }\n    }\n\n    if (scrolled) {\n      this._ctx.requestSelectionUpdate()\n    }\n\n    if (scrollX === 0 && scrollY === 0) {\n      this.stopAutoScroll()\n    }\n  }\n\n  private getAutoScrollDirectionX(mouseX: number): number {\n    const relativeX = mouseX - this.x\n    const distToLeft = relativeX\n    const distToRight = this.width - relativeX\n\n    if (distToLeft <= this.autoScrollThresholdHorizontal) {\n      return this.scrollLeft > 0 ? -1 : 0\n    } else if (distToRight <= this.autoScrollThresholdHorizontal) {\n      const maxScrollLeft = this.scrollWidth - this.viewport.width\n      return this.scrollLeft < maxScrollLeft ? 1 : 0\n    }\n    return 0\n  }\n\n  private getAutoScrollDirectionY(mouseY: number): number {\n    const relativeY = mouseY - this.y\n    const distToTop = relativeY\n    const distToBottom = this.height - relativeY\n\n    if (distToTop <= this.autoScrollThresholdVertical) {\n      return this.scrollTop > 0 ? -1 : 0\n    } else if (distToBottom <= this.autoScrollThresholdVertical) {\n      const maxScrollTop = this.scrollHeight - this.viewport.height\n      return this.scrollTop < maxScrollTop ? 1 : 0\n    }\n    return 0\n  }\n\n  private getAutoScrollSpeed(mouseX: number, mouseY: number): number {\n    const relativeX = mouseX - this.x\n    const relativeY = mouseY - this.y\n\n    const distToLeft = relativeX\n    const distToRight = this.width - relativeX\n    const distToTop = relativeY\n    const distToBottom = this.height - relativeY\n\n    const minDistance = Math.min(distToLeft, distToRight, distToTop, distToBottom)\n\n    if (minDistance <= 1) {\n      return this.autoScrollSpeedFast\n    } else if (minDistance <= 2) {\n      return this.autoScrollSpeedMedium\n    } else {\n      return this.autoScrollSpeedSlow\n    }\n  }\n\n  private recalculateBarProps(): void {\n    // Wrap entire method to prevent scroll changes from being treated as manual\n    const wasApplyingStickyScroll = this._isApplyingStickyScroll\n    this._isApplyingStickyScroll = true\n\n    try {\n      this.verticalScrollBar.scrollSize = this.content.height\n      this.verticalScrollBar.viewportSize = this.viewport.height\n      this.horizontalScrollBar.scrollSize = this.content.width\n      this.horizontalScrollBar.viewportSize = this.viewport.width\n\n      if (this._stickyScroll) {\n        const newMaxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height)\n        const newMaxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width)\n\n        if (this._stickyStart && !this._hasManualScroll) {\n          this.applyStickyStart(this._stickyStart)\n        } else {\n          if (this._stickyScrollTop) {\n            this.scrollTop = 0\n          } else if (this._stickyScrollBottom && newMaxScrollTop > 0) {\n            this.scrollTop = newMaxScrollTop\n          }\n\n          if (this._stickyScrollLeft) {\n            this.scrollLeft = 0\n          } else if (this._stickyScrollRight && newMaxScrollLeft > 0) {\n            this.scrollLeft = newMaxScrollLeft\n          }\n        }\n      }\n    } finally {\n      this._isApplyingStickyScroll = wasApplyingStickyScroll\n    }\n\n    // NOTE: This is obviously a workaround for something,\n    // which is that the bar props are recalculated when the viewport is resized,\n    // which intially happens onUpdate but is the viewport does not have the correct dimensions yet,\n    // then when it does, no update is triggered and when we do we are in the middle of a render,\n    // which just ignores the request. ¯\\_(ツ)_/¯\n    // TODO: Fix this properly. How? Move yoga to native, get all changes for elements in one go\n    // and update all renderables in one go before rendering.\n    // OR: Move this logic to the viewport. IMHO the wrapper and viewport are overkill and not necessary.\n    //     The Scrollbox can be the viewport, we are using translations on the content anyway.\n    process.nextTick(() => {\n      this.requestRender()\n    })\n  }\n\n  // Setters for reactive properties\n  public set padding(value: number | `${number}%` | null | undefined) {\n    this.content.padding = value\n    this.requestRender()\n  }\n\n  public set paddingX(value: number | `${number}%` | null | undefined) {\n    this.content.paddingX = value\n    this.requestRender()\n  }\n\n  public set paddingY(value: number | `${number}%` | null | undefined) {\n    this.content.paddingY = value\n    this.requestRender()\n  }\n\n  public set paddingTop(value: number | `${number}%` | null | undefined) {\n    this.content.paddingTop = value\n    this.requestRender()\n  }\n\n  public set paddingRight(value: number | `${number}%` | null | undefined) {\n    this.content.paddingRight = value\n    this.requestRender()\n  }\n\n  public set paddingBottom(value: number | `${number}%` | null | undefined) {\n    this.content.paddingBottom = value\n    this.requestRender()\n  }\n\n  public set paddingLeft(value: number | `${number}%` | null | undefined) {\n    this.content.paddingLeft = value\n    this.requestRender()\n  }\n\n  public set rootOptions(options: ScrollBoxOptions[\"rootOptions\"]) {\n    Object.assign(this, options)\n    this.requestRender()\n  }\n\n  public set wrapperOptions(options: ScrollBoxOptions[\"wrapperOptions\"]) {\n    Object.assign(this.wrapper, options)\n    this.requestRender()\n  }\n\n  public set viewportOptions(options: ScrollBoxOptions[\"viewportOptions\"]) {\n    Object.assign(this.viewport, options)\n    this.requestRender()\n  }\n\n  public set contentOptions(options: ScrollBoxOptions[\"contentOptions\"]) {\n    Object.assign(this.content, options)\n    this.requestRender()\n  }\n\n  public set scrollbarOptions(options: ScrollBoxOptions[\"scrollbarOptions\"]) {\n    Object.assign(this.verticalScrollBar, options)\n    Object.assign(this.horizontalScrollBar, options)\n    this.requestRender()\n  }\n\n  public set verticalScrollbarOptions(options: ScrollBoxOptions[\"verticalScrollbarOptions\"]) {\n    Object.assign(this.verticalScrollBar, options)\n    this.requestRender()\n  }\n\n  public set horizontalScrollbarOptions(options: ScrollBoxOptions[\"horizontalScrollbarOptions\"]) {\n    Object.assign(this.horizontalScrollBar, options)\n    this.requestRender()\n  }\n\n  public get scrollAcceleration(): ScrollAcceleration {\n    return this.scrollAccel\n  }\n\n  public set scrollAcceleration(value: ScrollAcceleration) {\n    this.scrollAccel = value\n  }\n\n  get viewportCulling(): boolean {\n    return this.content.viewportCulling\n  }\n\n  set viewportCulling(value: boolean) {\n    this.content.viewportCulling = value\n    this.requestRender()\n  }\n\n  protected destroySelf(): void {\n    if (this.selectionListener) {\n      this._ctx.off(\"selection\", this.selectionListener)\n      this.selectionListener = undefined\n    }\n    super.destroySelf()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Select.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe } from \"bun:test\"\nimport { SelectRenderable, type SelectRenderableOptions, SelectRenderableEvents, type SelectOption } from \"./Select.js\"\nimport { createTestRenderer, type MockInput, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { KeyEvent } from \"../lib/KeyHandler.js\"\n\n// Helper function to create a KeyEvent from a string or object\nfunction createKeyEvent(\n  input: string | { name: string; shift?: boolean; ctrl?: boolean; meta?: boolean; super?: boolean },\n): KeyEvent {\n  if (typeof input === \"string\") {\n    return new KeyEvent({\n      name: input,\n      sequence: input,\n      ctrl: false,\n      meta: false,\n      shift: false,\n      option: false,\n      number: false,\n      raw: input,\n      eventType: \"press\",\n      source: \"raw\",\n    })\n  } else {\n    return new KeyEvent({\n      name: input.name,\n      sequence: input.name === \"space\" ? \" \" : input.name,\n      ctrl: input.ctrl ?? false,\n      meta: input.meta ?? false,\n      shift: input.shift ?? false,\n      super: input.super ?? false,\n      option: false,\n      number: false,\n      raw: input.name,\n      eventType: \"press\",\n      source: \"raw\",\n    })\n  }\n}\n\nlet currentRenderer: TestRenderer\nlet currentMockInput: MockInput\nlet renderOnce: () => Promise<void>\n\nconst sampleOptions: SelectOption[] = [\n  { name: \"Option 1\", description: \"First option\" },\n  { name: \"Option 2\", description: \"Second option\" },\n  { name: \"Option 3\", description: \"Third option\" },\n  { name: \"Option 4\", description: \"Fourth option\" },\n  { name: \"Option 5\", description: \"Fifth option\" },\n]\n\nasync function createSelectRenderable(\n  renderer: TestRenderer,\n  options: SelectRenderableOptions,\n): Promise<{ select: SelectRenderable; root: any }> {\n  const selectRenderable = new SelectRenderable(renderer, { left: 0, top: 0, ...options })\n  renderer.root.add(selectRenderable)\n  await renderOnce()\n\n  return { select: selectRenderable, root: renderer.root }\n}\n\nbeforeEach(async () => {\n  ;({ renderer: currentRenderer, mockInput: currentMockInput, renderOnce } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  currentRenderer.destroy()\n})\n\ndescribe(\"SelectRenderable\", () => {\n  describe(\"Initialization\", () => {\n    test(\"should initialize with default options\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n      })\n\n      expect(select.options).toEqual(sampleOptions)\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(select.getSelectedOption()).toEqual(sampleOptions[0])\n      expect(select.focusable).toBe(true)\n      expect(select.showScrollIndicator).toBe(false)\n      expect(select.showDescription).toBe(true)\n      expect(select.wrapSelection).toBe(false)\n    })\n\n    test(\"should initialize with custom selected index\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      expect(select.getSelectedIndex()).toBe(2)\n      expect(select.getSelectedOption()).toEqual(sampleOptions[2])\n    })\n\n    test(\"should initialize with custom options\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        showScrollIndicator: true,\n        showDescription: false,\n        wrapSelection: true,\n        itemSpacing: 1,\n        fastScrollStep: 3,\n      })\n\n      expect(select.showScrollIndicator).toBe(true)\n      expect(select.showDescription).toBe(false)\n      expect(select.wrapSelection).toBe(true)\n    })\n\n    test(\"should handle empty options array\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: [],\n      })\n\n      expect(select.options).toEqual([])\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(select.getSelectedOption()).toBe(null)\n    })\n\n    test(\"should clamp selectedIndex to valid range\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 10, // Out of range\n      })\n\n      expect(select.getSelectedIndex()).toBe(sampleOptions.length - 1)\n      expect(select.getSelectedOption()).toEqual(sampleOptions[sampleOptions.length - 1])\n    })\n  })\n\n  describe(\"Options Management\", () => {\n    test(\"should update options dynamically\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      const newOptions: SelectOption[] = [\n        { name: \"New Option 1\", description: \"New first option\" },\n        { name: \"New Option 2\", description: \"New second option\" },\n      ]\n\n      select.options = newOptions\n\n      expect(select.options).toEqual(newOptions)\n      expect(select.getSelectedIndex()).toBe(1) // Should be clamped to valid index\n      expect(select.getSelectedOption()).toEqual(newOptions[1])\n    })\n\n    test(\"should handle setting empty options\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      select.options = []\n\n      expect(select.options).toEqual([])\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(select.getSelectedOption()).toBe(null)\n    })\n\n    test(\"should preserve valid selected index when options change\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 1,\n      })\n\n      const extendedOptions = [...sampleOptions, { name: \"Option 6\", description: \"Sixth option\" }]\n      select.options = extendedOptions\n\n      expect(select.getSelectedIndex()).toBe(1) // Should remain the same\n      expect(select.getSelectedOption()).toEqual(sampleOptions[1])\n    })\n  })\n\n  describe(\"Selection Management\", () => {\n    test(\"should set selected index programmatically\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n      })\n\n      let selectionChangedFired = false\n      let selectionIndex = -1\n      let selectionOption: SelectOption | null = null\n\n      select.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {\n        selectionChangedFired = true\n        selectionIndex = index\n        selectionOption = option\n      })\n\n      select.setSelectedIndex(3)\n\n      expect(select.getSelectedIndex()).toBe(3)\n      expect(select.getSelectedOption()).toEqual(sampleOptions[3])\n      expect(selectionChangedFired).toBe(true)\n      expect(selectionIndex).toBe(3)\n      expect(selectionOption).toEqual(sampleOptions[3])\n    })\n\n    test(\"should ignore invalid selected index\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      const originalIndex = select.getSelectedIndex()\n      const originalOption = select.getSelectedOption()\n\n      select.setSelectedIndex(-1) // Invalid\n      expect(select.getSelectedIndex()).toBe(originalIndex)\n\n      select.setSelectedIndex(10) // Out of range\n      expect(select.getSelectedIndex()).toBe(originalIndex)\n\n      expect(select.getSelectedOption()).toEqual(originalOption)\n    })\n\n    test(\"should move up correctly\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      select.moveUp()\n      expect(select.getSelectedIndex()).toBe(1)\n\n      select.moveUp()\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // Should not move beyond first item without wrap\n      select.moveUp()\n      expect(select.getSelectedIndex()).toBe(0)\n    })\n\n    test(\"should move down correctly\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      select.moveDown()\n      expect(select.getSelectedIndex()).toBe(3)\n\n      select.moveDown()\n      expect(select.getSelectedIndex()).toBe(4)\n\n      // Should not move beyond last item without wrap\n      select.moveDown()\n      expect(select.getSelectedIndex()).toBe(4)\n    })\n\n    test(\"should wrap selection when enabled\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        wrapSelection: true,\n      })\n\n      // Move up from first item should wrap to last\n      expect(select.getSelectedIndex()).toBe(0)\n      select.moveUp()\n      expect(select.getSelectedIndex()).toBe(4)\n\n      // Move down from last item should wrap to first\n      select.moveDown()\n      expect(select.getSelectedIndex()).toBe(0)\n    })\n\n    test(\"should move multiple steps\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 0,\n      })\n\n      select.moveDown(3)\n      expect(select.getSelectedIndex()).toBe(3)\n\n      select.moveUp(2)\n      expect(select.getSelectedIndex()).toBe(1)\n    })\n\n    test(\"should select current item\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      let itemSelectedFired = false\n      let selectedIndex = -1\n      let selectedOption: SelectOption | null = null\n\n      select.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {\n        itemSelectedFired = true\n        selectedIndex = index\n        selectedOption = option\n      })\n\n      select.selectCurrent()\n\n      expect(itemSelectedFired).toBe(true)\n      expect(selectedIndex).toBe(2)\n      expect(selectedOption).toEqual(sampleOptions[2])\n    })\n\n    test(\"should not select when no options available\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: [],\n      })\n\n      let itemSelectedFired = false\n\n      select.on(SelectRenderableEvents.ITEM_SELECTED, () => {\n        itemSelectedFired = true\n      })\n\n      select.selectCurrent()\n\n      expect(itemSelectedFired).toBe(false)\n    })\n  })\n\n  describe(\"Keyboard Interaction\", () => {\n    test(\"should handle up/down arrow keys\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 1,\n      })\n\n      select.focus()\n\n      // Test down arrow\n      const downHandled = select.handleKeyPress(createKeyEvent(\"down\"))\n      expect(downHandled).toBe(true)\n      expect(select.getSelectedIndex()).toBe(2)\n\n      // Test up arrow\n      const upHandled = select.handleKeyPress(createKeyEvent(\"up\"))\n      expect(upHandled).toBe(true)\n      expect(select.getSelectedIndex()).toBe(1)\n    })\n\n    test(\"should handle j/k keys (vim-style)\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 1,\n      })\n\n      select.focus()\n\n      // Test 'j' (down)\n      const jHandled = select.handleKeyPress(createKeyEvent(\"j\"))\n      expect(jHandled).toBe(true)\n      expect(select.getSelectedIndex()).toBe(2)\n\n      // Test 'k' (up)\n      const kHandled = select.handleKeyPress(createKeyEvent(\"k\"))\n      expect(kHandled).toBe(true)\n      expect(select.getSelectedIndex()).toBe(1)\n    })\n\n    test(\"should handle enter key\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      select.focus()\n\n      let itemSelectedFired = false\n      let selectedIndex = -1\n\n      select.on(SelectRenderableEvents.ITEM_SELECTED, (index: number) => {\n        itemSelectedFired = true\n        selectedIndex = index\n      })\n\n      const enterHandled = select.handleKeyPress(createKeyEvent(\"return\"))\n      expect(enterHandled).toBe(true)\n      expect(itemSelectedFired).toBe(true)\n      expect(selectedIndex).toBe(2)\n    })\n\n    test(\"should handle linefeed key\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      select.focus()\n\n      let itemSelectedFired = false\n\n      select.on(SelectRenderableEvents.ITEM_SELECTED, () => {\n        itemSelectedFired = true\n      })\n\n      const linefeedHandled = select.handleKeyPress(createKeyEvent(\"linefeed\"))\n      expect(linefeedHandled).toBe(true)\n      expect(itemSelectedFired).toBe(true)\n    })\n\n    test(\"should handle fast scroll with shift modifier\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 0,\n        fastScrollStep: 3,\n      })\n\n      select.focus()\n\n      // Test shift+down\n      const shiftDownHandled = select.handleKeyPress(createKeyEvent({ name: \"down\", shift: true }))\n      expect(shiftDownHandled).toBe(true)\n      expect(select.getSelectedIndex()).toBe(3) // Should move 3 steps\n\n      // Test shift+up\n      const shiftUpHandled = select.handleKeyPress(createKeyEvent({ name: \"up\", shift: true }))\n      expect(shiftUpHandled).toBe(true)\n      expect(select.getSelectedIndex()).toBe(0) // Should move back 3 steps\n    })\n\n    test(\"should ignore unhandled keys\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 1,\n      })\n\n      select.focus()\n\n      const originalIndex = select.getSelectedIndex()\n\n      // Test unhandled key\n      const handled = select.handleKeyPress(createKeyEvent(\"a\"))\n      expect(handled).toBe(false)\n      expect(select.getSelectedIndex()).toBe(originalIndex)\n    })\n  })\n\n  describe(\"Property Changes\", () => {\n    test(\"should update showScrollIndicator\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        showScrollIndicator: false,\n      })\n\n      expect(select.showScrollIndicator).toBe(false)\n\n      select.showScrollIndicator = true\n      expect(select.showScrollIndicator).toBe(true)\n    })\n\n    test(\"should update showDescription\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        showDescription: true,\n      })\n\n      expect(select.showDescription).toBe(true)\n\n      select.showDescription = false\n      expect(select.showDescription).toBe(false)\n    })\n\n    test(\"should update wrapSelection\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        wrapSelection: false,\n      })\n\n      expect(select.wrapSelection).toBe(false)\n\n      select.wrapSelection = true\n      expect(select.wrapSelection).toBe(true)\n    })\n\n    test(\"should update colors\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n      })\n\n      // Test all color setters\n      select.backgroundColor = \"#ff0000\"\n      select.textColor = \"#00ff00\"\n      select.focusedBackgroundColor = \"#0000ff\"\n      select.focusedTextColor = \"#ffff00\"\n      select.selectedBackgroundColor = \"#ff00ff\"\n      select.selectedTextColor = \"#00ffff\"\n      select.descriptionColor = \"#808080\"\n      select.selectedDescriptionColor = \"#ffffff\"\n\n      // Should not throw errors\n      expect(select).toBeDefined()\n    })\n\n    test(\"should update font\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n      })\n\n      select.font = \"tiny\"\n      // Should not throw errors\n      expect(select).toBeDefined()\n    })\n\n    test(\"should update itemSpacing\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        itemSpacing: 0,\n      })\n\n      select.itemSpacing = 2\n      // Should not throw errors\n      expect(select).toBeDefined()\n    })\n\n    test(\"should update fastScrollStep\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        fastScrollStep: 5,\n      })\n\n      select.fastScrollStep = 10\n      // Should not throw errors\n      expect(select).toBeDefined()\n    })\n\n    test(\"should update selectedIndex via setter\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 0,\n      })\n\n      select.selectedIndex = 3\n      expect(select.getSelectedIndex()).toBe(3)\n    })\n  })\n\n  describe(\"Event Emission\", () => {\n    test(\"should emit SELECTION_CHANGED when moving\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 1,\n      })\n\n      let eventCount = 0\n      let lastIndex = -1\n      let lastOption: SelectOption | null = null\n\n      select.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {\n        eventCount++\n        lastIndex = index\n        lastOption = option\n      })\n\n      select.moveDown()\n      expect(eventCount).toBe(1)\n      expect(lastIndex).toBe(2)\n      expect(lastOption).toEqual(sampleOptions[2])\n\n      select.moveUp()\n      expect(eventCount).toBe(2)\n      expect(lastIndex).toBe(1)\n      expect(lastOption).toEqual(sampleOptions[1])\n    })\n\n    test(\"should emit ITEM_SELECTED when selecting\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 2,\n      })\n\n      let eventCount = 0\n      let lastIndex = -1\n      let lastOption: SelectOption | null = null\n\n      select.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {\n        eventCount++\n        lastIndex = index\n        lastOption = option\n      })\n\n      select.selectCurrent()\n      expect(eventCount).toBe(1)\n      expect(lastIndex).toBe(2)\n      expect(lastOption).toEqual(sampleOptions[2])\n    })\n\n    test(\"should not reuse the same keypress after focusing another select\", async () => {\n      const { select: first } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 1,\n      })\n      const { select: second } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: [\n          { name: \"A\", description: \"A\" },\n          { name: \"B\", description: \"B\" },\n        ],\n      })\n\n      let firstSelections = 0\n      let secondSelections = 0\n\n      first.on(SelectRenderableEvents.ITEM_SELECTED, () => {\n        firstSelections++\n        second.focus()\n      })\n      second.on(SelectRenderableEvents.ITEM_SELECTED, () => {\n        secondSelections++\n      })\n\n      first.focus()\n      currentMockInput.pressKey(\"RETURN\")\n\n      expect(firstSelections).toBe(1)\n      expect(secondSelections).toBe(0)\n      expect(second.focused).toBe(true)\n    })\n\n    test(\"should emit events even when movement is blocked\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n        selectedIndex: 0, // At the beginning\n        wrapSelection: false,\n      })\n\n      let eventCount = 0\n\n      select.on(SelectRenderableEvents.SELECTION_CHANGED, () => {\n        eventCount++\n      })\n\n      // Try to move up from first item (index stays the same but event is emitted)\n      select.moveUp()\n      expect(eventCount).toBe(1)\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // Try to move down to last item and then try to move down again\n      select.setSelectedIndex(4) // Move to last item\n      eventCount = 0 // Reset counter\n\n      select.moveDown()\n      expect(eventCount).toBe(1)\n      expect(select.getSelectedIndex()).toBe(4) // Should stay at last item\n    })\n  })\n\n  describe(\"Resize Handling\", () => {\n    test(\"should handle resize events\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n      })\n\n      // Simulate resize by calling onResize directly\n      // @ts-expect-error - Testing protected method\n      select.onResize(30, 10)\n\n      // Should not throw errors and should be able to continue functioning\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(select.getSelectedOption()).toEqual(sampleOptions[0])\n    })\n  })\n\n  describe(\"Edge Cases\", () => {\n    test(\"should handle options with undefined values\", async () => {\n      const optionsWithValues: SelectOption[] = [\n        { name: \"Option 1\", description: \"First option\", value: \"value1\" },\n        { name: \"Option 2\", description: \"Second option\", value: undefined },\n        { name: \"Option 3\", description: \"Third option\" },\n      ]\n\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: optionsWithValues,\n      })\n\n      expect(select.options).toEqual(optionsWithValues)\n      expect(select.getSelectedOption()?.value).toBe(\"value1\")\n\n      select.setSelectedIndex(1)\n      expect(select.getSelectedOption()?.value).toBe(undefined)\n\n      select.setSelectedIndex(2)\n      expect(select.getSelectedOption()?.value).toBe(undefined)\n    })\n\n    test(\"should handle single option\", async () => {\n      const singleOption: SelectOption[] = [{ name: \"Only Option\", description: \"The only choice\" }]\n\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: singleOption,\n      })\n\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(select.getSelectedOption()).toEqual(singleOption[0])\n\n      let eventCount = 0\n      select.on(SelectRenderableEvents.SELECTION_CHANGED, () => {\n        eventCount++\n      })\n\n      // Movement should not change selection but events are still emitted\n      select.moveUp()\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(eventCount).toBe(1)\n\n      select.moveDown()\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(eventCount).toBe(2)\n    })\n\n    test(\"should handle very small dimensions\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 1,\n        height: 1,\n        options: sampleOptions,\n      })\n\n      // Should still function even with minimal space\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(select.getSelectedOption()).toEqual(sampleOptions[0])\n\n      select.moveDown()\n      expect(select.getSelectedIndex()).toBe(1)\n    })\n\n    test(\"should handle long option names and descriptions\", async () => {\n      const longOptions: SelectOption[] = [\n        {\n          name: \"This is a very long option name that exceeds normal width\",\n          description:\n            \"This is an extremely long description that definitely exceeds the available width and should be handled gracefully\",\n        },\n        {\n          name: \"Short\",\n          description: \"Short desc\",\n        },\n      ]\n\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 10,\n        height: 5,\n        options: longOptions,\n      })\n\n      expect(select.getSelectedIndex()).toBe(0)\n      expect(select.getSelectedOption()).toEqual(longOptions[0])\n\n      select.moveDown()\n      expect(select.getSelectedIndex()).toBe(1)\n      expect(select.getSelectedOption()).toEqual(longOptions[1])\n    })\n\n    test(\"should handle focus state changes\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 5,\n        options: sampleOptions,\n      })\n\n      expect(select.focused).toBe(false)\n\n      select.focus()\n      expect(select.focused).toBe(true)\n\n      select.blur()\n      expect(select.focused).toBe(false)\n    })\n  })\n\n  describe(\"Key Bindings and Aliases\", () => {\n    test(\"should support custom key bindings\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 10,\n        options: sampleOptions,\n        keyBindings: [\n          { name: \"h\", action: \"move-up\" },\n          { name: \"l\", action: \"move-down\" },\n        ],\n      })\n\n      select.focus()\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // H should move up (but we're at top)\n      currentMockInput.pressKey(\"h\")\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // L should move down\n      currentMockInput.pressKey(\"l\")\n      expect(select.getSelectedIndex()).toBe(1)\n    })\n\n    test(\"should support key aliases\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 10,\n        options: sampleOptions,\n        keyAliasMap: {\n          enter: \"return\",\n        },\n      })\n\n      select.focus()\n      select.setSelectedIndex(1)\n\n      let itemSelected = false\n      select.on(SelectRenderableEvents.ITEM_SELECTED, () => {\n        itemSelected = true\n      })\n\n      currentMockInput.pressEnter()\n      expect(itemSelected).toBe(true)\n    })\n\n    test(\"should merge custom bindings with defaults\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 10,\n        options: sampleOptions,\n        keyBindings: [{ name: \"n\", action: \"move-down\" }],\n      })\n\n      select.focus()\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // Default binding should still work\n      currentMockInput.pressArrow(\"down\")\n      expect(select.getSelectedIndex()).toBe(1)\n\n      // Custom binding should also work\n      currentMockInput.pressKey(\"n\")\n      expect(select.getSelectedIndex()).toBe(2)\n    })\n\n    test(\"should override default bindings with custom ones\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 10,\n        options: sampleOptions,\n        keyBindings: [\n          { name: \"k\", action: \"move-down\" }, // Override k to move down instead of up\n        ],\n      })\n\n      select.focus()\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // K should now move down instead of up\n      currentMockInput.pressKey(\"k\")\n      expect(select.getSelectedIndex()).toBe(1)\n    })\n\n    test(\"should support fast scroll with shift by default\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 10,\n        options: sampleOptions,\n        fastScrollStep: 3,\n      })\n\n      select.focus()\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // Shift+Down should fast scroll\n      currentMockInput.pressArrow(\"down\", { shift: true })\n      expect(select.getSelectedIndex()).toBe(3)\n    })\n\n    test(\"should allow custom bindings for fast scroll\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 10,\n        options: sampleOptions,\n        fastScrollStep: 2,\n        keyBindings: [{ name: \"down\", ctrl: true, action: \"move-down-fast\" }],\n      })\n\n      select.focus()\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // Ctrl+Down should fast scroll down\n      currentMockInput.pressArrow(\"down\", { ctrl: true })\n      expect(select.getSelectedIndex()).toBe(2)\n    })\n\n    test(\"should allow updating key bindings dynamically\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 10,\n        options: sampleOptions,\n      })\n\n      select.focus()\n      expect(select.getSelectedIndex()).toBe(0)\n\n      // Move down with default binding\n      currentMockInput.pressArrow(\"down\")\n      expect(select.getSelectedIndex()).toBe(1)\n\n      // Update bindings\n      select.keyBindings = [{ name: \"x\", action: \"move-down\" }]\n\n      // X should now move down\n      currentMockInput.pressKey(\"x\")\n      expect(select.getSelectedIndex()).toBe(2)\n    })\n\n    test(\"should handle modifiers in custom bindings\", async () => {\n      const { select } = await createSelectRenderable(currentRenderer, {\n        width: 20,\n        height: 10,\n        options: sampleOptions,\n        keyBindings: [\n          { name: \"n\", ctrl: true, action: \"move-down\" },\n          { name: \"p\", ctrl: true, action: \"move-up\" },\n        ],\n      })\n\n      select.focus()\n      select.setSelectedIndex(2)\n\n      // Ctrl+P should move up\n      currentMockInput.pressKey(\"p\", { ctrl: true })\n      expect(select.getSelectedIndex()).toBe(1)\n\n      // Ctrl+N should move down\n      currentMockInput.pressKey(\"n\", { ctrl: true })\n      expect(select.getSelectedIndex()).toBe(2)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Select.ts",
    "content": "import { OptimizedBuffer } from \"../buffer.js\"\nimport { fonts, measureText, renderFontToFrameBuffer } from \"../lib/ascii.font.js\"\nimport type { KeyEvent } from \"../lib/KeyHandler.js\"\nimport { RGBA, parseColor, type ColorInput } from \"../lib/RGBA.js\"\nimport { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport type { RenderContext } from \"../types.js\"\nimport {\n  type KeyBinding as BaseKeyBinding,\n  mergeKeyBindings,\n  getKeyBindingKey,\n  buildKeyBindingsMap,\n  type KeyAliasMap,\n  defaultKeyAliases,\n  mergeKeyAliases,\n} from \"../lib/keymapping.js\"\n\nexport interface SelectOption {\n  name: string\n  description: string\n  value?: any\n}\n\nexport type SelectAction = \"move-up\" | \"move-down\" | \"move-up-fast\" | \"move-down-fast\" | \"select-current\"\n\nexport type SelectKeyBinding = BaseKeyBinding<SelectAction>\n\nconst defaultSelectKeybindings: SelectKeyBinding[] = [\n  { name: \"up\", action: \"move-up\" },\n  { name: \"k\", action: \"move-up\" },\n  { name: \"down\", action: \"move-down\" },\n  { name: \"j\", action: \"move-down\" },\n  { name: \"up\", shift: true, action: \"move-up-fast\" },\n  { name: \"down\", shift: true, action: \"move-down-fast\" },\n  { name: \"return\", action: \"select-current\" },\n  { name: \"linefeed\", action: \"select-current\" },\n]\n\nexport interface SelectRenderableOptions extends RenderableOptions<SelectRenderable> {\n  backgroundColor?: ColorInput\n  textColor?: ColorInput\n  focusedBackgroundColor?: ColorInput\n  focusedTextColor?: ColorInput\n  options?: SelectOption[]\n  selectedIndex?: number\n  selectedBackgroundColor?: ColorInput\n  selectedTextColor?: ColorInput\n  descriptionColor?: ColorInput\n  selectedDescriptionColor?: ColorInput\n  showScrollIndicator?: boolean\n  wrapSelection?: boolean\n  showDescription?: boolean\n  font?: keyof typeof fonts\n  itemSpacing?: number\n  fastScrollStep?: number\n  keyBindings?: SelectKeyBinding[]\n  keyAliasMap?: KeyAliasMap\n}\n\nexport enum SelectRenderableEvents {\n  SELECTION_CHANGED = \"selectionChanged\",\n  ITEM_SELECTED = \"itemSelected\",\n}\n\nexport class SelectRenderable extends Renderable {\n  protected _focusable: boolean = true\n\n  private _options: SelectOption[] = []\n  private _selectedIndex: number = 0\n  private scrollOffset: number = 0\n  private maxVisibleItems: number\n\n  private _backgroundColor: RGBA\n  private _textColor: RGBA\n  private _focusedBackgroundColor: RGBA\n  private _focusedTextColor: RGBA\n  private _selectedBackgroundColor: RGBA\n  private _selectedTextColor: RGBA\n  private _descriptionColor: RGBA\n  private _selectedDescriptionColor: RGBA\n  private _showScrollIndicator: boolean\n  private _wrapSelection: boolean\n  private _showDescription: boolean\n  private _font?: keyof typeof fonts\n  private _itemSpacing: number\n  private linesPerItem: number\n  private fontHeight: number\n  private _fastScrollStep: number\n  private _keyBindingsMap: Map<string, SelectAction>\n  private _keyAliasMap: KeyAliasMap\n  private _keyBindings: SelectKeyBinding[]\n\n  protected _defaultOptions = {\n    backgroundColor: \"transparent\",\n    textColor: \"#FFFFFF\",\n    focusedBackgroundColor: \"#1a1a1a\",\n    focusedTextColor: \"#FFFFFF\",\n    selectedBackgroundColor: \"#334455\",\n    selectedTextColor: \"#FFFF00\",\n    selectedIndex: 0,\n    descriptionColor: \"#888888\",\n    selectedDescriptionColor: \"#CCCCCC\",\n    showScrollIndicator: false,\n    wrapSelection: false,\n    showDescription: true,\n    itemSpacing: 0,\n    fastScrollStep: 5,\n  } satisfies Partial<SelectRenderableOptions>\n\n  constructor(ctx: RenderContext, options: SelectRenderableOptions) {\n    super(ctx, { ...options, buffered: true })\n    this._options = options.options || []\n    const requestedIndex = options.selectedIndex ?? this._defaultOptions.selectedIndex\n    this._selectedIndex = this._options.length > 0 ? Math.min(requestedIndex, this._options.length - 1) : 0\n    this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor)\n    this._textColor = parseColor(options.textColor || this._defaultOptions.textColor)\n    this._focusedBackgroundColor = parseColor(\n      options.focusedBackgroundColor || this._defaultOptions.focusedBackgroundColor,\n    )\n    this._focusedTextColor = parseColor(options.focusedTextColor || this._defaultOptions.focusedTextColor)\n\n    this._showScrollIndicator = options.showScrollIndicator ?? this._defaultOptions.showScrollIndicator\n    this._wrapSelection = options.wrapSelection ?? this._defaultOptions.wrapSelection\n    this._showDescription = options.showDescription ?? this._defaultOptions.showDescription\n    this._font = options.font\n    this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing\n\n    this.fontHeight = this._font ? measureText({ text: \"A\", font: this._font }).height : 1\n    this.linesPerItem = this._showDescription\n      ? this._font\n        ? this.fontHeight + 1\n        : 2\n      : this._font\n        ? this.fontHeight\n        : 1\n    this.linesPerItem += this._itemSpacing\n\n    this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem))\n\n    this._selectedBackgroundColor = parseColor(\n      options.selectedBackgroundColor || this._defaultOptions.selectedBackgroundColor,\n    )\n    this._selectedTextColor = parseColor(options.selectedTextColor || this._defaultOptions.selectedTextColor)\n    this._descriptionColor = parseColor(options.descriptionColor || this._defaultOptions.descriptionColor)\n    this._selectedDescriptionColor = parseColor(\n      options.selectedDescriptionColor || this._defaultOptions.selectedDescriptionColor,\n    )\n    this._fastScrollStep = options.fastScrollStep || this._defaultOptions.fastScrollStep\n\n    this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {})\n    this._keyBindings = options.keyBindings || []\n    const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, this._keyBindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n\n    this.requestRender() // Initial render needed\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    if (!this.visible || !this.frameBuffer) return\n\n    if (this.isDirty) {\n      this.refreshFrameBuffer()\n    }\n  }\n\n  private refreshFrameBuffer(): void {\n    if (!this.frameBuffer || this._options.length === 0) return\n\n    const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor\n    this.frameBuffer.clear(bgColor)\n\n    const contentX = 0\n    const contentY = 0\n    const contentWidth = this.width\n    const contentHeight = this.height\n\n    const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleItems)\n\n    for (let i = 0; i < visibleOptions.length; i++) {\n      const actualIndex = this.scrollOffset + i\n      const option = visibleOptions[i]\n      const isSelected = actualIndex === this._selectedIndex\n      const itemY = contentY + i * this.linesPerItem\n\n      if (itemY + this.linesPerItem - 1 >= contentY + contentHeight) break\n\n      if (isSelected) {\n        const contentHeight = this.linesPerItem - this._itemSpacing\n        this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight, this._selectedBackgroundColor)\n      }\n\n      const nameContent = `${isSelected ? \"▶ \" : \"  \"}${option.name}`\n      const baseTextColor = this._focused ? this._focusedTextColor : this._textColor\n      const nameColor = isSelected ? this._selectedTextColor : baseTextColor\n      let descX = contentX + 3\n\n      if (this._font) {\n        const indicator = isSelected ? \"▶ \" : \"  \"\n        this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor)\n\n        const indicatorWidth = 2\n        renderFontToFrameBuffer(this.frameBuffer, {\n          text: option.name,\n          x: contentX + 1 + indicatorWidth,\n          y: itemY,\n          color: nameColor,\n          backgroundColor: isSelected ? this._selectedBackgroundColor : bgColor,\n          font: this._font,\n        })\n        descX = contentX + 1 + indicatorWidth\n      } else {\n        this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor)\n      }\n\n      if (this._showDescription && itemY + this.fontHeight < contentY + contentHeight) {\n        const descColor = isSelected ? this._selectedDescriptionColor : this._descriptionColor\n        this.frameBuffer.drawText(option.description, descX, itemY + this.fontHeight, descColor)\n      }\n    }\n\n    if (this._showScrollIndicator && this._options.length > this.maxVisibleItems) {\n      this.renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight)\n    }\n  }\n\n  private renderScrollIndicatorToFrameBuffer(\n    contentX: number,\n    contentY: number,\n    contentWidth: number,\n    contentHeight: number,\n  ): void {\n    if (!this.frameBuffer) return\n\n    const scrollPercent = this._selectedIndex / Math.max(1, this._options.length - 1)\n    const indicatorHeight = Math.max(1, contentHeight - 2)\n    const indicatorY = contentY + 1 + Math.floor(scrollPercent * indicatorHeight)\n    const indicatorX = contentX + contentWidth - 1\n\n    this.frameBuffer.drawText(\"█\", indicatorX, indicatorY, parseColor(\"#666666\"))\n  }\n\n  public get options(): SelectOption[] {\n    return this._options\n  }\n\n  public set options(options: SelectOption[]) {\n    this._options = options\n    this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, options.length - 1))\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public getSelectedOption(): SelectOption | null {\n    return this._options[this._selectedIndex] || null\n  }\n\n  public getSelectedIndex(): number {\n    return this._selectedIndex\n  }\n\n  public moveUp(steps: number = 1): void {\n    const newIndex = this._selectedIndex - steps\n\n    if (newIndex >= 0) {\n      this._selectedIndex = newIndex\n    } else if (this._wrapSelection && this._options.length > 0) {\n      this._selectedIndex = this._options.length - 1\n    } else {\n      this._selectedIndex = 0\n    }\n\n    this.updateScrollOffset()\n    this.requestRender()\n    this.emit(SelectRenderableEvents.SELECTION_CHANGED, this._selectedIndex, this.getSelectedOption())\n  }\n\n  public moveDown(steps: number = 1): void {\n    const newIndex = this._selectedIndex + steps\n\n    if (newIndex < this._options.length) {\n      this._selectedIndex = newIndex\n    } else if (this._wrapSelection && this._options.length > 0) {\n      this._selectedIndex = 0\n    } else {\n      this._selectedIndex = this._options.length - 1\n    }\n\n    this.updateScrollOffset()\n    this.requestRender()\n    this.emit(SelectRenderableEvents.SELECTION_CHANGED, this._selectedIndex, this.getSelectedOption())\n  }\n\n  public selectCurrent(): void {\n    const selected = this.getSelectedOption()\n    if (selected) {\n      this.emit(SelectRenderableEvents.ITEM_SELECTED, this._selectedIndex, selected)\n    }\n  }\n\n  public setSelectedIndex(index: number): void {\n    if (index >= 0 && index < this._options.length) {\n      this._selectedIndex = index\n      this.updateScrollOffset()\n      this.requestRender()\n      this.emit(SelectRenderableEvents.SELECTION_CHANGED, this._selectedIndex, this.getSelectedOption())\n    }\n  }\n\n  private updateScrollOffset(): void {\n    if (!this._options) return\n\n    const halfVisible = Math.floor(this.maxVisibleItems / 2)\n    const newScrollOffset = Math.max(\n      0,\n      Math.min(this._selectedIndex - halfVisible, this._options.length - this.maxVisibleItems),\n    )\n\n    if (newScrollOffset !== this.scrollOffset) {\n      this.scrollOffset = newScrollOffset\n      this.requestRender()\n    }\n  }\n\n  protected onResize(width: number, height: number): void {\n    this.maxVisibleItems = Math.max(1, Math.floor(height / this.linesPerItem))\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public handleKeyPress(key: KeyEvent): boolean {\n    const bindingKey = getKeyBindingKey({\n      name: key.name,\n      ctrl: key.ctrl,\n      shift: key.shift,\n      meta: key.meta,\n      super: key.super,\n      action: \"move-up\" as SelectAction,\n    })\n\n    const action = this._keyBindingsMap.get(bindingKey)\n\n    if (action) {\n      switch (action) {\n        case \"move-up\":\n          this.moveUp(1)\n          return true\n        case \"move-down\":\n          this.moveDown(1)\n          return true\n        case \"move-up-fast\":\n          this.moveUp(this._fastScrollStep)\n          return true\n        case \"move-down-fast\":\n          this.moveDown(this._fastScrollStep)\n          return true\n        case \"select-current\":\n          this.selectCurrent()\n          return true\n      }\n    }\n\n    return false\n  }\n\n  public get showScrollIndicator(): boolean {\n    return this._showScrollIndicator\n  }\n\n  public set showScrollIndicator(show: boolean) {\n    this._showScrollIndicator = show\n    this.requestRender()\n  }\n\n  public get showDescription(): boolean {\n    return this._showDescription\n  }\n\n  public set showDescription(show: boolean) {\n    if (this._showDescription !== show) {\n      this._showDescription = show\n      this.linesPerItem = this._showDescription\n        ? this._font\n          ? this.fontHeight + 1\n          : 2\n        : this._font\n          ? this.fontHeight\n          : 1\n      this.linesPerItem += this._itemSpacing\n\n      this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem))\n      this.updateScrollOffset()\n      this.requestRender()\n    }\n  }\n\n  public get wrapSelection(): boolean {\n    return this._wrapSelection\n  }\n\n  public set wrapSelection(wrap: boolean) {\n    this._wrapSelection = wrap\n  }\n\n  public set backgroundColor(value: ColorInput) {\n    const newColor = parseColor(value ?? this._defaultOptions.backgroundColor)\n    if (this._backgroundColor !== newColor) {\n      this._backgroundColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public set textColor(value: ColorInput) {\n    const newColor = parseColor(value ?? this._defaultOptions.textColor)\n    if (this._textColor !== newColor) {\n      this._textColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public set focusedBackgroundColor(value: ColorInput) {\n    const newColor = parseColor(value ?? this._defaultOptions.focusedBackgroundColor)\n    if (this._focusedBackgroundColor !== newColor) {\n      this._focusedBackgroundColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public set focusedTextColor(value: ColorInput) {\n    const newColor = parseColor(value ?? this._defaultOptions.focusedTextColor)\n    if (this._focusedTextColor !== newColor) {\n      this._focusedTextColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public set selectedBackgroundColor(value: ColorInput) {\n    const newColor = parseColor(value ?? this._defaultOptions.selectedBackgroundColor)\n    if (this._selectedBackgroundColor !== newColor) {\n      this._selectedBackgroundColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public set selectedTextColor(value: ColorInput) {\n    const newColor = parseColor(value ?? this._defaultOptions.selectedTextColor)\n    if (this._selectedTextColor !== newColor) {\n      this._selectedTextColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public set descriptionColor(value: ColorInput) {\n    const newColor = parseColor(value ?? this._defaultOptions.descriptionColor)\n    if (this._descriptionColor !== newColor) {\n      this._descriptionColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public set selectedDescriptionColor(value: ColorInput) {\n    const newColor = parseColor(value ?? this._defaultOptions.selectedDescriptionColor)\n    if (this._selectedDescriptionColor !== newColor) {\n      this._selectedDescriptionColor = newColor\n      this.requestRender()\n    }\n  }\n\n  public set font(font: keyof typeof fonts) {\n    this._font = font\n    this.fontHeight = measureText({ text: \"A\", font: this._font }).height\n    this.linesPerItem = this._showDescription\n      ? this._font\n        ? this.fontHeight + 1\n        : 2\n      : this._font\n        ? this.fontHeight\n        : 1\n    this.linesPerItem += this._itemSpacing\n    this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem))\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public set itemSpacing(spacing: number) {\n    this._itemSpacing = spacing\n    this.linesPerItem = this._showDescription\n      ? this._font\n        ? this.fontHeight + 1\n        : 2\n      : this._font\n        ? this.fontHeight\n        : 1\n    this.linesPerItem += this._itemSpacing\n    this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem))\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public set fastScrollStep(step: number) {\n    this._fastScrollStep = step\n  }\n\n  public set keyBindings(bindings: SelectKeyBinding[]) {\n    this._keyBindings = bindings\n    const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, bindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n  }\n\n  public set keyAliasMap(aliases: KeyAliasMap) {\n    this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, aliases)\n    const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, this._keyBindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n  }\n\n  public set selectedIndex(value: number) {\n    const newIndex = value ?? this._defaultOptions.selectedIndex\n    const clampedIndex = this._options.length > 0 ? Math.min(Math.max(0, newIndex), this._options.length - 1) : 0\n    if (this._selectedIndex !== clampedIndex) {\n      this._selectedIndex = clampedIndex\n      this.updateScrollOffset()\n      this.requestRender()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Slider.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { SliderRenderable, type SliderOptions } from \"./Slider.js\"\nimport { createTestRenderer, type MockMouse, type TestRenderer } from \"../testing/test-renderer.js\"\n\nlet currentRenderer: TestRenderer\nlet currentMockMouse: MockMouse\nlet renderOnce: () => Promise<void>\n\nasync function createSliderRenderable(\n  renderer: TestRenderer,\n  options: SliderOptions,\n): Promise<{ slider: SliderRenderable; root: any }> {\n  const sliderRenderable = new SliderRenderable(renderer, { left: 0, top: 0, ...options })\n  renderer.root.add(sliderRenderable)\n  await renderOnce()\n\n  return { slider: sliderRenderable, root: renderer.root }\n}\n\nbeforeEach(async () => {\n  ;({ renderer: currentRenderer, mockMouse: currentMockMouse, renderOnce } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  currentRenderer.destroy()\n})\n\ntest(\"SliderRenderable > Value-based API\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 50,\n  })\n\n  expect(slider.value).toBe(50)\n  expect(slider.min).toBe(0)\n  expect(slider.max).toBe(100)\n\n  slider.value = 75\n  expect(slider.value).toBe(75)\n\n  slider.value = 150\n  expect(slider.value).toBe(100)\n\n  slider.value = -10\n  expect(slider.value).toBe(0)\n\n  slider.min = 20\n  expect(slider.value).toBe(20) // Should clamp to new min\n\n  slider.max = 80\n  slider.value = 90\n  expect(slider.value).toBe(80) // Should clamp to new max\n})\n\ntest(\"SliderRenderable > Automatic thumb size calculation\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 50,\n    width: 20,\n    height: 1,\n  })\n\n  expect(slider.width).toBe(20)\n  expect(slider.height).toBe(1)\n  expect(slider.min).toBe(0)\n  expect(slider.max).toBe(100)\n  expect(slider.value).toBe(50)\n})\n\ntest(\"SliderRenderable > Custom step size\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 50,\n    width: 100,\n    height: 1,\n    viewPortSize: 10,\n  })\n\n  expect(slider.viewPortSize).toBe(10)\n  expect(slider.width).toBe(100)\n  expect(slider.min).toBe(0)\n  expect(slider.max).toBe(100)\n  expect(slider.value).toBe(50)\n\n  slider.viewPortSize = 20\n  expect(slider.viewPortSize).toBe(20)\n\n  slider.viewPortSize = 150 // Should be clamped to max range (100)\n  expect(slider.viewPortSize).toBe(100)\n\n  slider.viewPortSize = 0 // Should be clamped to minimum (0.01)\n  expect(slider.viewPortSize).toBe(0.01)\n})\n\ntest(\"SliderRenderable > Minimum thumb size\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"vertical\",\n    min: 0,\n    max: 10000,\n    value: 0,\n    width: 2,\n    height: 100,\n    viewPortSize: 1,\n  })\n\n  expect(slider.viewPortSize).toBe(1)\n  expect(slider.min).toBe(0)\n  expect(slider.max).toBe(10000)\n})\n\ntest(\"SliderRenderable > onChange callback\", async () => {\n  let changedValue: number | undefined\n\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 0,\n    onChange: (value) => {\n      changedValue = value\n    },\n  })\n\n  slider.value = 42\n  expect(changedValue).toBe(42)\n})\n\ntest(\"SliderRenderable > Vertical thumb size calculation\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"vertical\",\n    min: 0,\n    max: 100,\n    value: 0,\n    width: 3,\n    height: 50,\n    viewPortSize: 10,\n  })\n\n  // @ts-expect-error - Testing private method\n  const thumbSize = slider.getVirtualThumbSize()\n  expect(thumbSize).toBe(9)\n\n  slider.viewPortSize = 1\n  // @ts-expect-error - Testing private method\n  expect(slider.getVirtualThumbSize()).toBe(1)\n\n  slider.viewPortSize = 150\n  // @ts-expect-error - Testing private method\n  expect(slider.getVirtualThumbSize()).toBe(50)\n})\n\ntest(\"SliderRenderable > Horizontal thumb size calculation\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 200,\n    value: 0,\n    width: 80,\n    height: 2,\n    viewPortSize: 20,\n  })\n\n  // @ts-expect-error - Testing private method\n  const thumbSize = slider.getVirtualThumbSize()\n  expect(thumbSize).toBe(14)\n\n  slider.viewPortSize = 40\n  // @ts-expect-error - Testing private method\n  expect(slider.getVirtualThumbSize()).toBe(26)\n\n  slider.viewPortSize = 0.1\n  // @ts-expect-error - Testing private method\n  expect(slider.getVirtualThumbSize()).toBe(1)\n})\n\ntest(\"SliderRenderable > Edge cases in thumb size calculation\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"vertical\",\n    min: 50,\n    max: 50,\n    value: 50,\n    width: 2,\n    height: 30,\n    viewPortSize: 10,\n  })\n\n  // @ts-expect-error - Testing private method\n  expect(slider.getVirtualThumbSize()).toBe(60)\n\n  slider.min = 0\n  slider.max = 100000\n  slider.viewPortSize = 1\n\n  // @ts-expect-error - Testing private method\n  expect(slider.getVirtualThumbSize()).toBe(1)\n\n  slider.max = 30\n  slider.viewPortSize = 30\n\n  // @ts-expect-error - Testing private method\n  expect(slider.getVirtualThumbSize()).toBe(30)\n})\n\ntest(\"SliderRenderable > Thumb size minimum clamping\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 1000,\n    value: 0,\n    width: 10,\n    height: 1,\n    viewPortSize: 1,\n  })\n\n  // @ts-expect-error - Testing private method\n  const thumbSize = slider.getVirtualThumbSize()\n  expect(thumbSize).toBe(1)\n\n  const { slider: extremeSlider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"vertical\",\n    min: 0,\n    max: 10000,\n    value: 0,\n    width: 1,\n    height: 2,\n    viewPortSize: 0.01,\n  })\n\n  // @ts-expect-error - Testing private method\n  expect(extremeSlider.getVirtualThumbSize()).toBe(1)\n\n  expect(thumbSize).toBeGreaterThanOrEqual(1)\n  // @ts-expect-error - Testing private method\n  expect(extremeSlider.getVirtualThumbSize()).toBeGreaterThanOrEqual(1)\n})\n\ntest(\"SliderRenderable > Thumb size can be less than 2\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 200,\n    value: 0,\n    width: 20,\n    height: 1,\n    viewPortSize: 2,\n  })\n\n  // @ts-expect-error - Testing private method\n  const thumbSize = slider.getVirtualThumbSize()\n  expect(thumbSize).toBe(1)\n\n  const { slider: largerRatioSlider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"vertical\",\n    min: 0,\n    max: 100,\n    value: 0,\n    width: 1,\n    height: 10,\n    viewPortSize: 1,\n  })\n\n  // @ts-expect-error - Testing private method\n  expect(largerRatioSlider.getVirtualThumbSize()).toBe(1)\n\n  const { slider: exactSlider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 40,\n    value: 0,\n    width: 20,\n    height: 1,\n    viewPortSize: 1,\n  })\n\n  // @ts-expect-error - Testing private method\n  expect(exactSlider.getVirtualThumbSize()).toBe(1)\n})\n\ntest(\"SliderRenderable > Mouse interaction - horizontal click on thumb\", async () => {\n  process.stdout.write(\"SliderRenderable > Mouse interaction - horizontal click on thumb 1\\n\")\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 50,\n    width: 20,\n    height: 1,\n  })\n  process.stdout.write(\"SliderRenderable > Mouse interaction - horizontal click on thumb 2\\n\")\n  await currentMockMouse.click(10, 0)\n  process.stdout.write(\"SliderRenderable > Mouse interaction - horizontal click on thumb 3\\n\")\n  expect(slider.value).toBeCloseTo(51, 0)\n})\n\ntest(\"SliderRenderable > Mouse interaction - horizontal click on track\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 50,\n    width: 20,\n    height: 1,\n  })\n\n  await currentMockMouse.pressDown(15, 0)\n\n  expect(slider.value).toBeCloseTo(75, 1)\n})\n\ntest(\"SliderRenderable > Mouse interaction - vertical click on thumb\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"vertical\",\n    min: 0,\n    max: 100,\n    value: 50,\n    width: 2,\n    height: 20,\n  })\n\n  currentMockMouse.click(0, 10)\n\n  expect(slider.value).toBe(50)\n})\n\n// TODO: This seems flaky suddenly, because it now fails for all previous commits\ntest.skip(\"SliderRenderable > Mouse interaction - vertical click on track\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"vertical\",\n    min: 0,\n    max: 100,\n    value: 50,\n    width: 2,\n    height: 20,\n  })\n\n  currentMockMouse.click(0, 15)\n\n  expect(slider.value).toBeCloseTo(75, 5)\n})\n\ntest(\"SliderRenderable > Mouse interaction - horizontal drag\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 0,\n    width: 20,\n    height: 1,\n  })\n\n  currentMockMouse.drag(5, 0, 15, 0)\n\n  expect(slider.value).toBeCloseTo(25, 5)\n})\n\ntest(\"SliderRenderable > Mouse interaction - vertical drag\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"vertical\",\n    min: 0,\n    max: 100,\n    value: 0,\n    width: 2,\n    height: 20,\n  })\n\n  currentMockMouse.drag(0, 5, 0, 15)\n\n  expect(slider.value).toBeCloseTo(25, 5)\n})\n\ntest(\"SliderRenderable > Mouse interaction - drag with onChange callback\", async () => {\n  let changedValue: number | undefined\n\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 0,\n    width: 20,\n    height: 1,\n    onChange: (value) => {\n      changedValue = value\n    },\n  })\n\n  // Drag from position 5 to position 15\n  currentMockMouse.drag(5, 0, 15, 0)\n\n  // onChange should be called with the new value\n  expect(changedValue).toBeDefined()\n  expect(changedValue).toBeCloseTo(25, 10)\n  expect(slider.value).toBeCloseTo(25, 10)\n})\n\ntest(\"SliderRenderable > Mouse interaction - drag beyond bounds\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 10,\n    max: 90,\n    value: 50,\n    width: 20,\n    height: 1,\n  })\n\n  currentMockMouse.drag(10, 0, 25, 0)\n\n  expect(slider.value).toBeCloseTo(50, 5)\n\n  currentMockMouse.drag(10, 0, -5, 0)\n\n  expect(slider.value).toBeCloseTo(50, 5)\n})\n\ntest(\"SliderRenderable > Mouse interaction - click outside slider bounds\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 100,\n    value: 50,\n    width: 20,\n    height: 1,\n    left: 5,\n    top: 5,\n  })\n\n  currentMockMouse.click(30, 5)\n\n  expect(slider.value).toBe(50)\n})\n\ntest(\"SliderRenderable > Mouse interaction - precision dragging with small viewport\", async () => {\n  const { slider } = await createSliderRenderable(currentRenderer, {\n    orientation: \"horizontal\",\n    min: 0,\n    max: 1000,\n    value: 0,\n    width: 50,\n    height: 1,\n    viewPortSize: 10,\n  })\n\n  // @ts-expect-error - Testing private method\n  const thumbSize = slider.getVirtualThumbSize()\n  expect(thumbSize).toBeLessThan(10) // Thumb should be smaller than full width\n\n  currentMockMouse.drag(5, 0, 7, 0)\n\n  expect(slider.value).toBeGreaterThan(0)\n  expect(slider.value).toBeCloseTo(100, 10) // Approximately 5/50 * 1000 = 100\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Slider.ts",
    "content": "import {\n  type ColorInput,\n  OptimizedBuffer,\n  parseColor,\n  Renderable,\n  type RenderableOptions,\n  type RenderContext,\n  RGBA,\n} from \"../index.js\"\n\nconst defaultThumbBackgroundColor = RGBA.fromHex(\"#9a9ea3\")\nconst defaultTrackBackgroundColor = RGBA.fromHex(\"#252527\")\n\nexport interface SliderOptions extends RenderableOptions<SliderRenderable> {\n  orientation: \"vertical\" | \"horizontal\"\n  value?: number\n  min?: number\n  max?: number\n  viewPortSize?: number\n  backgroundColor?: ColorInput\n  foregroundColor?: ColorInput\n  onChange?: (value: number) => void\n}\n\nexport class SliderRenderable extends Renderable {\n  public readonly orientation: \"vertical\" | \"horizontal\"\n  private _value: number\n  private _min: number\n  private _max: number\n  private _viewPortSize: number\n  private _backgroundColor: RGBA\n  private _foregroundColor: RGBA\n  private _onChange?: (value: number) => void\n\n  constructor(ctx: RenderContext, options: SliderOptions) {\n    super(ctx, { flexShrink: 0, ...options })\n    this.orientation = options.orientation\n    this._min = options.min ?? 0\n    this._max = options.max ?? 100\n    this._value = options.value ?? this._min\n    this._viewPortSize = options.viewPortSize ?? Math.max(1, (this._max - this._min) * 0.1)\n    this._onChange = options.onChange\n    this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : defaultTrackBackgroundColor\n    this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : defaultThumbBackgroundColor\n\n    this.setupMouseHandling()\n  }\n\n  get value(): number {\n    return this._value\n  }\n\n  set value(newValue: number) {\n    const clamped = Math.max(this._min, Math.min(this._max, newValue))\n    if (clamped !== this._value) {\n      this._value = clamped\n      this._onChange?.(clamped)\n      this.emit(\"change\", { value: clamped })\n      this.requestRender()\n    }\n  }\n\n  get min(): number {\n    return this._min\n  }\n\n  set min(newMin: number) {\n    if (newMin !== this._min) {\n      this._min = newMin\n      if (this._value < newMin) {\n        this.value = newMin\n      }\n      this.requestRender()\n    }\n  }\n\n  get max(): number {\n    return this._max\n  }\n\n  set max(newMax: number) {\n    if (newMax !== this._max) {\n      this._max = newMax\n      if (this._value > newMax) {\n        this.value = newMax\n      }\n      this.requestRender()\n    }\n  }\n\n  set viewPortSize(size: number) {\n    const clampedSize = Math.max(0.01, Math.min(size, this._max - this._min))\n    if (clampedSize !== this._viewPortSize) {\n      this._viewPortSize = clampedSize\n      this.requestRender()\n    }\n  }\n\n  get viewPortSize(): number {\n    return this._viewPortSize\n  }\n\n  get backgroundColor(): RGBA {\n    return this._backgroundColor\n  }\n\n  set backgroundColor(value: ColorInput) {\n    this._backgroundColor = parseColor(value)\n    this.requestRender()\n  }\n\n  get foregroundColor(): RGBA {\n    return this._foregroundColor\n  }\n\n  set foregroundColor(value: ColorInput) {\n    this._foregroundColor = parseColor(value)\n    this.requestRender()\n  }\n\n  private calculateDragOffsetVirtual(event: any): number {\n    const trackStart = this.orientation === \"vertical\" ? this.y : this.x\n    const mousePos = (this.orientation === \"vertical\" ? event.y : event.x) - trackStart\n    const virtualMousePos = Math.max(\n      0,\n      Math.min((this.orientation === \"vertical\" ? this.height : this.width) * 2, mousePos * 2),\n    )\n    const virtualThumbStart = this.getVirtualThumbStart()\n    const virtualThumbSize = this.getVirtualThumbSize()\n\n    return Math.max(0, Math.min(virtualThumbSize, virtualMousePos - virtualThumbStart))\n  }\n\n  private setupMouseHandling(): void {\n    let isDragging = false\n    let dragOffsetVirtual = 0\n\n    this.onMouseDown = (event) => {\n      event.stopPropagation()\n      event.preventDefault()\n\n      const thumb = this.getThumbRect()\n      const inThumb =\n        event.x >= thumb.x && event.x < thumb.x + thumb.width && event.y >= thumb.y && event.y < thumb.y + thumb.height\n\n      if (inThumb) {\n        isDragging = true\n\n        dragOffsetVirtual = this.calculateDragOffsetVirtual(event)\n      } else {\n        this.updateValueFromMouseDirect(event)\n        isDragging = true\n\n        dragOffsetVirtual = this.calculateDragOffsetVirtual(event)\n      }\n    }\n\n    this.onMouseDrag = (event) => {\n      if (!isDragging) return\n      event.stopPropagation()\n      this.updateValueFromMouseWithOffset(event, dragOffsetVirtual)\n    }\n\n    this.onMouseUp = (event) => {\n      if (isDragging) {\n        this.updateValueFromMouseWithOffset(event, dragOffsetVirtual)\n      }\n      isDragging = false\n    }\n  }\n\n  private updateValueFromMouseDirect(event: any): void {\n    const trackStart = this.orientation === \"vertical\" ? this.y : this.x\n    const trackSize = this.orientation === \"vertical\" ? this.height : this.width\n    const mousePos = this.orientation === \"vertical\" ? event.y : event.x\n\n    const relativeMousePos = mousePos - trackStart\n    const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos))\n    const ratio = trackSize === 0 ? 0 : clampedMousePos / trackSize\n    const range = this._max - this._min\n    const newValue = this._min + ratio * range\n\n    this.value = newValue\n  }\n\n  private updateValueFromMouseWithOffset(event: any, offsetVirtual: number): void {\n    const trackStart = this.orientation === \"vertical\" ? this.y : this.x\n    const trackSize = this.orientation === \"vertical\" ? this.height : this.width\n    const mousePos = this.orientation === \"vertical\" ? event.y : event.x\n\n    const virtualTrackSize = trackSize * 2\n    const relativeMousePos = mousePos - trackStart\n    const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos))\n    const virtualMousePos = clampedMousePos * 2\n\n    const virtualThumbSize = this.getVirtualThumbSize()\n    const maxThumbStart = Math.max(0, virtualTrackSize - virtualThumbSize)\n\n    let desiredThumbStart = virtualMousePos - offsetVirtual\n    desiredThumbStart = Math.max(0, Math.min(maxThumbStart, desiredThumbStart))\n\n    const ratio = maxThumbStart === 0 ? 0 : desiredThumbStart / maxThumbStart\n    const range = this._max - this._min\n    const newValue = this._min + ratio * range\n\n    this.value = newValue\n  }\n\n  private getThumbRect(): { x: number; y: number; width: number; height: number } {\n    const virtualThumbSize = this.getVirtualThumbSize()\n    const virtualThumbStart = this.getVirtualThumbStart()\n\n    const realThumbStart = Math.floor(virtualThumbStart / 2)\n    const realThumbSize = Math.ceil((virtualThumbStart + virtualThumbSize) / 2) - realThumbStart\n\n    if (this.orientation === \"vertical\") {\n      return {\n        x: this.x,\n        y: this.y + realThumbStart,\n        width: this.width,\n        height: Math.max(1, realThumbSize),\n      }\n    } else {\n      return {\n        x: this.x + realThumbStart,\n        y: this.y,\n        width: Math.max(1, realThumbSize),\n        height: this.height,\n      }\n    }\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    if (this.orientation === \"horizontal\") {\n      this.renderHorizontal(buffer)\n    } else {\n      this.renderVertical(buffer)\n    }\n  }\n\n  private renderHorizontal(buffer: OptimizedBuffer): void {\n    const virtualThumbSize = this.getVirtualThumbSize()\n    const virtualThumbStart = this.getVirtualThumbStart()\n    const virtualThumbEnd = virtualThumbStart + virtualThumbSize\n\n    buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor)\n\n    const realStartCell = Math.floor(virtualThumbStart / 2)\n    const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1\n    const startX = Math.max(0, realStartCell)\n    const endX = Math.min(this.width - 1, realEndCell)\n\n    for (let realX = startX; realX <= endX; realX++) {\n      const virtualCellStart = realX * 2\n      const virtualCellEnd = virtualCellStart + 2\n\n      const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart)\n      const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd)\n      const coverage = thumbEndInCell - thumbStartInCell\n\n      let char = \" \"\n\n      if (coverage >= 2) {\n        char = \"█\"\n      } else {\n        const isLeftHalf = thumbStartInCell === virtualCellStart\n        if (isLeftHalf) {\n          char = \"▌\"\n        } else {\n          char = \"▐\"\n        }\n      }\n\n      for (let y = 0; y < this.height; y++) {\n        buffer.setCellWithAlphaBlending(this.x + realX, this.y + y, char, this._foregroundColor, this._backgroundColor)\n      }\n    }\n  }\n\n  private renderVertical(buffer: OptimizedBuffer): void {\n    const virtualThumbSize = this.getVirtualThumbSize()\n    const virtualThumbStart = this.getVirtualThumbStart()\n    const virtualThumbEnd = virtualThumbStart + virtualThumbSize\n\n    buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor)\n\n    const realStartCell = Math.floor(virtualThumbStart / 2)\n    const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1\n    const startY = Math.max(0, realStartCell)\n    const endY = Math.min(this.height - 1, realEndCell)\n\n    for (let realY = startY; realY <= endY; realY++) {\n      const virtualCellStart = realY * 2\n      const virtualCellEnd = virtualCellStart + 2\n\n      const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart)\n      const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd)\n      const coverage = thumbEndInCell - thumbStartInCell\n\n      let char = \" \"\n\n      if (coverage >= 2) {\n        char = \"█\"\n      } else if (coverage > 0) {\n        const virtualPositionInCell = thumbStartInCell - virtualCellStart\n        if (virtualPositionInCell === 0) {\n          char = \"▀\"\n        } else {\n          char = \"▄\"\n        }\n      }\n\n      for (let x = 0; x < this.width; x++) {\n        buffer.setCellWithAlphaBlending(this.x + x, this.y + realY, char, this._foregroundColor, this._backgroundColor)\n      }\n    }\n  }\n\n  private getVirtualThumbSize(): number {\n    const virtualTrackSize = this.orientation === \"vertical\" ? this.height * 2 : this.width * 2\n    const range = this._max - this._min\n\n    if (range === 0) return virtualTrackSize\n\n    const viewportSize = Math.max(1, this._viewPortSize)\n    const contentSize = range + viewportSize\n\n    if (contentSize <= viewportSize) return virtualTrackSize\n\n    const thumbRatio = viewportSize / contentSize\n    const calculatedSize = Math.floor(virtualTrackSize * thumbRatio)\n\n    return Math.max(1, Math.min(calculatedSize, virtualTrackSize))\n  }\n\n  private getVirtualThumbStart(): number {\n    const virtualTrackSize = this.orientation === \"vertical\" ? this.height * 2 : this.width * 2\n    const range = this._max - this._min\n\n    if (range === 0) return 0\n\n    const valueRatio = (this._value - this._min) / range\n    const virtualThumbSize = this.getVirtualThumbSize()\n\n    return Math.round(valueRatio * (virtualTrackSize - virtualThumbSize))\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/TabSelect.test.ts",
    "content": "import { test, expect, describe, beforeEach, afterEach } from \"bun:test\"\nimport {\n  TabSelectRenderable,\n  type TabSelectRenderableOptions,\n  TabSelectRenderableEvents,\n  type TabSelectOption,\n} from \"./TabSelect.js\"\nimport { createTestRenderer, type MockInput, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { ManualClock } from \"../testing/manual-clock.js\"\n\nlet currentRenderer: TestRenderer\nlet currentMockInput: MockInput\nlet renderOnce: () => Promise<void>\nlet currentClock: ManualClock\n\nconst sampleOptions: TabSelectOption[] = [\n  { name: \"Tab 1\", description: \"First tab\" },\n  { name: \"Tab 2\", description: \"Second tab\" },\n  { name: \"Tab 3\", description: \"Third tab\" },\n  { name: \"Tab 4\", description: \"Fourth tab\" },\n  { name: \"Tab 5\", description: \"Fifth tab\" },\n]\n\nasync function createTabSelectRenderable(\n  renderer: TestRenderer,\n  options: TabSelectRenderableOptions,\n): Promise<{ tabSelect: TabSelectRenderable; root: any }> {\n  const tabSelectRenderable = new TabSelectRenderable(renderer, { left: 0, top: 0, ...options })\n  renderer.root.add(tabSelectRenderable)\n  await renderOnce()\n\n  return { tabSelect: tabSelectRenderable, root: renderer.root }\n}\n\nbeforeEach(async () => {\n  currentClock = new ManualClock()\n  ;({\n    renderer: currentRenderer,\n    mockInput: currentMockInput,\n    renderOnce,\n  } = await createTestRenderer({\n    clock: currentClock,\n  }))\n})\n\nafterEach(() => {\n  currentRenderer.destroy()\n})\n\ndescribe(\"TabSelectRenderable\", () => {\n  describe(\"Key Bindings and Aliases\", () => {\n    test(\"should support custom key bindings\", async () => {\n      const { tabSelect } = await createTabSelectRenderable(currentRenderer, {\n        width: 100,\n        options: sampleOptions,\n        keyBindings: [\n          { name: \"h\", action: \"move-left\" },\n          { name: \"l\", action: \"move-right\" },\n        ],\n      })\n\n      tabSelect.focus()\n      expect(tabSelect.getSelectedIndex()).toBe(0)\n\n      // L should move right\n      currentMockInput.pressKey(\"l\")\n      expect(tabSelect.getSelectedIndex()).toBe(1)\n\n      // H should move left\n      currentMockInput.pressKey(\"h\")\n      expect(tabSelect.getSelectedIndex()).toBe(0)\n    })\n\n    test(\"should support key aliases\", async () => {\n      const { tabSelect } = await createTabSelectRenderable(currentRenderer, {\n        width: 100,\n        options: sampleOptions,\n        keyAliasMap: {\n          enter: \"return\",\n        },\n      })\n\n      tabSelect.focus()\n      tabSelect.setSelectedIndex(1)\n\n      let itemSelected = false\n      tabSelect.on(TabSelectRenderableEvents.ITEM_SELECTED, () => {\n        itemSelected = true\n      })\n\n      currentMockInput.pressEnter()\n      expect(itemSelected).toBe(true)\n    })\n\n    test(\"should merge custom bindings with defaults\", async () => {\n      const { tabSelect } = await createTabSelectRenderable(currentRenderer, {\n        width: 100,\n        options: sampleOptions,\n        keyBindings: [{ name: \"n\", action: \"move-right\" }],\n      })\n\n      tabSelect.focus()\n      expect(tabSelect.getSelectedIndex()).toBe(0)\n\n      // Default binding should still work\n      currentMockInput.pressArrow(\"right\")\n      expect(tabSelect.getSelectedIndex()).toBe(1)\n\n      // Custom binding should also work\n      currentMockInput.pressKey(\"n\")\n      expect(tabSelect.getSelectedIndex()).toBe(2)\n    })\n\n    test(\"should override default bindings with custom ones\", async () => {\n      const { tabSelect } = await createTabSelectRenderable(currentRenderer, {\n        width: 100,\n        options: sampleOptions,\n        keyBindings: [\n          { name: \"[\", action: \"move-right\" }, // Override [ to move right instead of left\n        ],\n      })\n\n      tabSelect.focus()\n      expect(tabSelect.getSelectedIndex()).toBe(0)\n\n      currentMockInput.pressKey(\"[\")\n      currentClock.advance(10)\n      expect(tabSelect.getSelectedIndex()).toBe(1)\n    })\n\n    test(\"should allow updating key bindings dynamically\", async () => {\n      const { tabSelect } = await createTabSelectRenderable(currentRenderer, {\n        width: 100,\n        options: sampleOptions,\n      })\n\n      tabSelect.focus()\n      expect(tabSelect.getSelectedIndex()).toBe(0)\n\n      // Move right with default binding\n      currentMockInput.pressArrow(\"right\")\n      expect(tabSelect.getSelectedIndex()).toBe(1)\n\n      // Update bindings\n      tabSelect.keyBindings = [{ name: \"space\", action: \"move-right\" }]\n\n      // Space should now move right\n      currentMockInput.pressKey(\" \")\n      expect(tabSelect.getSelectedIndex()).toBe(2)\n    })\n\n    test(\"should handle modifiers in custom bindings\", async () => {\n      const { tabSelect } = await createTabSelectRenderable(currentRenderer, {\n        width: 100,\n        options: sampleOptions,\n        keyBindings: [\n          { name: \"left\", ctrl: true, action: \"move-right\" },\n          { name: \"right\", ctrl: true, action: \"move-left\" },\n        ],\n      })\n\n      tabSelect.focus()\n      tabSelect.setSelectedIndex(2)\n\n      // Ctrl+Right should move left\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(tabSelect.getSelectedIndex()).toBe(1)\n\n      // Ctrl+Left should move right\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(tabSelect.getSelectedIndex()).toBe(2)\n    })\n\n    test(\"should handle wrap selection with custom bindings\", async () => {\n      const { tabSelect } = await createTabSelectRenderable(currentRenderer, {\n        width: 100,\n        options: sampleOptions,\n        wrapSelection: true,\n        keyBindings: [\n          { name: \"n\", action: \"move-right\" },\n          { name: \"p\", action: \"move-left\" },\n        ],\n      })\n\n      tabSelect.focus()\n      expect(tabSelect.getSelectedIndex()).toBe(0)\n\n      // P should wrap to end\n      currentMockInput.pressKey(\"p\")\n      expect(tabSelect.getSelectedIndex()).toBe(4)\n\n      // N should wrap to start\n      currentMockInput.pressKey(\"n\")\n      expect(tabSelect.getSelectedIndex()).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/TabSelect.ts",
    "content": "import { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { OptimizedBuffer } from \"../buffer.js\"\nimport { RGBA, parseColor, type ColorInput } from \"../lib/RGBA.js\"\nimport type { KeyEvent } from \"../lib/KeyHandler.js\"\nimport type { RenderContext } from \"../types.js\"\nimport {\n  type KeyBinding as BaseKeyBinding,\n  mergeKeyBindings,\n  getKeyBindingKey,\n  buildKeyBindingsMap,\n  type KeyAliasMap,\n  defaultKeyAliases,\n  mergeKeyAliases,\n} from \"../lib/keymapping.js\"\n\nexport interface TabSelectOption {\n  name: string\n  description: string\n  value?: any\n}\n\nexport type TabSelectAction = \"move-left\" | \"move-right\" | \"select-current\"\n\nexport type TabSelectKeyBinding = BaseKeyBinding<TabSelectAction>\n\nconst defaultTabSelectKeybindings: TabSelectKeyBinding[] = [\n  { name: \"left\", action: \"move-left\" },\n  { name: \"[\", action: \"move-left\" },\n  { name: \"right\", action: \"move-right\" },\n  { name: \"]\", action: \"move-right\" },\n  { name: \"return\", action: \"select-current\" },\n  { name: \"linefeed\", action: \"select-current\" },\n]\n\nexport interface TabSelectRenderableOptions extends Omit<RenderableOptions<TabSelectRenderable>, \"height\"> {\n  height?: number\n  options?: TabSelectOption[]\n  tabWidth?: number\n  backgroundColor?: ColorInput\n  textColor?: ColorInput\n  focusedBackgroundColor?: ColorInput\n  focusedTextColor?: ColorInput\n  selectedBackgroundColor?: ColorInput\n  selectedTextColor?: ColorInput\n  selectedDescriptionColor?: ColorInput\n  showScrollArrows?: boolean\n  showDescription?: boolean\n  showUnderline?: boolean\n  wrapSelection?: boolean\n  keyBindings?: TabSelectKeyBinding[]\n  keyAliasMap?: KeyAliasMap\n}\n\nexport enum TabSelectRenderableEvents {\n  SELECTION_CHANGED = \"selectionChanged\",\n  ITEM_SELECTED = \"itemSelected\",\n}\n\nfunction calculateDynamicHeight(showUnderline: boolean, showDescription: boolean): number {\n  let height = 1\n\n  if (showUnderline) {\n    height += 1\n  }\n\n  if (showDescription) {\n    height += 1\n  }\n\n  return height\n}\n\nexport class TabSelectRenderable extends Renderable {\n  protected _focusable: boolean = true\n\n  private _options: TabSelectOption[] = []\n  private selectedIndex: number = 0\n  private scrollOffset: number = 0\n  private _tabWidth: number\n  private maxVisibleTabs: number\n\n  private _backgroundColor: RGBA\n  private _textColor: RGBA\n  private _focusedBackgroundColor: RGBA\n  private _focusedTextColor: RGBA\n  private _selectedBackgroundColor: RGBA\n  private _selectedTextColor: RGBA\n  private _selectedDescriptionColor: RGBA\n  private _showScrollArrows: boolean\n  private _showDescription: boolean\n  private _showUnderline: boolean\n  private _wrapSelection: boolean\n  private _keyBindingsMap: Map<string, TabSelectAction>\n  private _keyAliasMap: KeyAliasMap\n  private _keyBindings: TabSelectKeyBinding[]\n\n  constructor(ctx: RenderContext, options: TabSelectRenderableOptions) {\n    const calculatedHeight = calculateDynamicHeight(options.showUnderline ?? true, options.showDescription ?? true)\n\n    super(ctx, { ...options, height: calculatedHeight, buffered: true })\n\n    this._backgroundColor = parseColor(options.backgroundColor || \"transparent\")\n    this._textColor = parseColor(options.textColor || \"#FFFFFF\")\n    this._focusedBackgroundColor = parseColor(options.focusedBackgroundColor || options.backgroundColor || \"#1a1a1a\")\n    this._focusedTextColor = parseColor(options.focusedTextColor || options.textColor || \"#FFFFFF\")\n    this._options = options.options || []\n    this._tabWidth = options.tabWidth || 20\n    this._showDescription = options.showDescription ?? true\n    this._showUnderline = options.showUnderline ?? true\n    this._showScrollArrows = options.showScrollArrows ?? true\n    this._wrapSelection = options.wrapSelection ?? false\n\n    this.maxVisibleTabs = Math.max(1, Math.floor(this.width / this._tabWidth))\n\n    this._selectedBackgroundColor = parseColor(options.selectedBackgroundColor || \"#334455\")\n    this._selectedTextColor = parseColor(options.selectedTextColor || \"#FFFF00\")\n    this._selectedDescriptionColor = parseColor(options.selectedDescriptionColor || \"#CCCCCC\")\n\n    this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {})\n    this._keyBindings = options.keyBindings || []\n    const mergedBindings = mergeKeyBindings(defaultTabSelectKeybindings, this._keyBindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n  }\n\n  private calculateDynamicHeight(): number {\n    return calculateDynamicHeight(this._showUnderline, this._showDescription)\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    if (!this.visible || !this.frameBuffer) return\n\n    if (this.isDirty) {\n      this.refreshFrameBuffer()\n    }\n  }\n\n  private refreshFrameBuffer(): void {\n    if (!this.frameBuffer) return\n    // Use focused colors if focused\n    const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor\n    this.frameBuffer.clear(bgColor)\n    if (this._options.length === 0) return\n\n    const contentX = 0\n    const contentY = 0\n    const contentWidth = this.width\n    const contentHeight = this.height\n\n    const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleTabs)\n\n    // Render tab names\n    for (let i = 0; i < visibleOptions.length; i++) {\n      const actualIndex = this.scrollOffset + i\n      const option = visibleOptions[i]\n      const isSelected = actualIndex === this.selectedIndex\n      const tabX = contentX + i * this._tabWidth\n\n      if (tabX >= contentX + contentWidth) break\n\n      const actualTabWidth = Math.min(this._tabWidth, contentWidth - i * this._tabWidth)\n\n      if (isSelected) {\n        this.frameBuffer.fillRect(tabX, contentY, actualTabWidth, 1, this._selectedBackgroundColor)\n      }\n\n      const baseTextColor = this._focused ? this._focusedTextColor : this._textColor\n      const nameColor = isSelected ? this._selectedTextColor : baseTextColor\n      const nameContent = this.truncateText(option.name, actualTabWidth - 2)\n      this.frameBuffer.drawText(nameContent, tabX + 1, contentY, nameColor)\n\n      if (isSelected && this._showUnderline && contentHeight >= 2) {\n        const underlineY = contentY + 1\n        const underlineBg = isSelected ? this._selectedBackgroundColor : bgColor\n        this.frameBuffer.drawText(\"▬\".repeat(actualTabWidth), tabX, underlineY, nameColor, underlineBg)\n      }\n    }\n\n    if (this._showDescription && contentHeight >= (this._showUnderline ? 3 : 2)) {\n      const selectedOption = this.getSelectedOption()\n      if (selectedOption) {\n        const descriptionY = contentY + (this._showUnderline ? 2 : 1)\n        const descColor = this._selectedDescriptionColor\n        const descContent = this.truncateText(selectedOption.description, contentWidth - 2)\n        this.frameBuffer.drawText(descContent, contentX + 1, descriptionY, descColor)\n      }\n    }\n\n    if (this._showScrollArrows && this._options.length > this.maxVisibleTabs) {\n      this.renderScrollArrowsToFrameBuffer(contentX, contentY, contentWidth, contentHeight)\n    }\n  }\n\n  private truncateText(text: string, maxWidth: number): string {\n    if (text.length <= maxWidth) return text\n    return text.substring(0, Math.max(0, maxWidth - 1)) + \"…\"\n  }\n\n  private renderScrollArrowsToFrameBuffer(\n    contentX: number,\n    contentY: number,\n    contentWidth: number,\n    contentHeight: number,\n  ): void {\n    if (!this.frameBuffer) return\n\n    const hasMoreLeft = this.scrollOffset > 0\n    const hasMoreRight = this.scrollOffset + this.maxVisibleTabs < this._options.length\n\n    if (hasMoreLeft) {\n      this.frameBuffer.drawText(\"‹\", contentX, contentY, parseColor(\"#AAAAAA\"))\n    }\n\n    if (hasMoreRight) {\n      this.frameBuffer.drawText(\"›\", contentX + contentWidth - 1, contentY, parseColor(\"#AAAAAA\"))\n    }\n  }\n\n  public setOptions(options: TabSelectOption[]): void {\n    this._options = options\n    this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, options.length - 1))\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public getSelectedOption(): TabSelectOption | null {\n    return this._options[this.selectedIndex] || null\n  }\n\n  public getSelectedIndex(): number {\n    return this.selectedIndex\n  }\n\n  public moveLeft(): void {\n    if (this.selectedIndex > 0) {\n      this.selectedIndex--\n    } else if (this._wrapSelection && this._options.length > 0) {\n      this.selectedIndex = this._options.length - 1\n    } else {\n      return\n    }\n\n    this.updateScrollOffset()\n    this.requestRender()\n    this.emit(TabSelectRenderableEvents.SELECTION_CHANGED, this.selectedIndex, this.getSelectedOption())\n  }\n\n  public moveRight(): void {\n    if (this.selectedIndex < this._options.length - 1) {\n      this.selectedIndex++\n    } else if (this._wrapSelection && this._options.length > 0) {\n      this.selectedIndex = 0\n    } else {\n      return\n    }\n\n    this.updateScrollOffset()\n    this.requestRender()\n    this.emit(TabSelectRenderableEvents.SELECTION_CHANGED, this.selectedIndex, this.getSelectedOption())\n  }\n\n  public selectCurrent(): void {\n    const selected = this.getSelectedOption()\n    if (selected) {\n      this.emit(TabSelectRenderableEvents.ITEM_SELECTED, this.selectedIndex, selected)\n    }\n  }\n\n  public setSelectedIndex(index: number): void {\n    if (index >= 0 && index < this._options.length) {\n      this.selectedIndex = index\n      this.updateScrollOffset()\n      this.requestRender()\n      this.emit(TabSelectRenderableEvents.SELECTION_CHANGED, this.selectedIndex, this.getSelectedOption())\n    }\n  }\n\n  private updateScrollOffset(): void {\n    const halfVisible = Math.floor(this.maxVisibleTabs / 2)\n    const newScrollOffset = Math.max(\n      0,\n      Math.min(this.selectedIndex - halfVisible, this._options.length - this.maxVisibleTabs),\n    )\n\n    if (newScrollOffset !== this.scrollOffset) {\n      this.scrollOffset = newScrollOffset\n      this.requestRender()\n    }\n  }\n\n  protected onResize(width: number, height: number): void {\n    this.maxVisibleTabs = Math.max(1, Math.floor(width / this._tabWidth))\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public setTabWidth(tabWidth: number): void {\n    if (this._tabWidth === tabWidth) return\n\n    this._tabWidth = tabWidth\n    this.maxVisibleTabs = Math.max(1, Math.floor(this.width / this._tabWidth))\n\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public getTabWidth(): number {\n    return this._tabWidth\n  }\n\n  public handleKeyPress(key: KeyEvent): boolean {\n    const bindingKey = getKeyBindingKey({\n      name: key.name,\n      ctrl: key.ctrl,\n      shift: key.shift,\n      meta: key.meta,\n      super: key.super,\n      action: \"move-left\" as TabSelectAction,\n    })\n\n    const action = this._keyBindingsMap.get(bindingKey)\n\n    if (action) {\n      switch (action) {\n        case \"move-left\":\n          this.moveLeft()\n          return true\n        case \"move-right\":\n          this.moveRight()\n          return true\n        case \"select-current\":\n          this.selectCurrent()\n          return true\n      }\n    }\n\n    return false\n  }\n\n  public get options(): TabSelectOption[] {\n    return this._options\n  }\n\n  public set options(options: TabSelectOption[]) {\n    this._options = options\n    this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, options.length - 1))\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public set backgroundColor(color: ColorInput) {\n    this._backgroundColor = parseColor(color)\n    this.requestRender()\n  }\n\n  public set textColor(color: ColorInput) {\n    this._textColor = parseColor(color)\n    this.requestRender()\n  }\n\n  public set focusedBackgroundColor(color: ColorInput) {\n    this._focusedBackgroundColor = parseColor(color)\n    this.requestRender()\n  }\n\n  public set focusedTextColor(color: ColorInput) {\n    this._focusedTextColor = parseColor(color)\n    this.requestRender()\n  }\n\n  public set selectedBackgroundColor(color: ColorInput) {\n    this._selectedBackgroundColor = parseColor(color)\n    this.requestRender()\n  }\n\n  public set selectedTextColor(color: ColorInput) {\n    this._selectedTextColor = parseColor(color)\n    this.requestRender()\n  }\n\n  public set selectedDescriptionColor(color: ColorInput) {\n    this._selectedDescriptionColor = parseColor(color)\n    this.requestRender()\n  }\n\n  public get showDescription(): boolean {\n    return this._showDescription\n  }\n\n  public set showDescription(show: boolean) {\n    if (this._showDescription !== show) {\n      this._showDescription = show\n      const newHeight = this.calculateDynamicHeight()\n      this.height = newHeight\n      this.requestRender()\n    }\n  }\n\n  public get showUnderline(): boolean {\n    return this._showUnderline\n  }\n\n  public set showUnderline(show: boolean) {\n    if (this._showUnderline !== show) {\n      this._showUnderline = show\n      const newHeight = this.calculateDynamicHeight()\n      this.height = newHeight\n      this.requestRender()\n    }\n  }\n\n  public get showScrollArrows(): boolean {\n    return this._showScrollArrows\n  }\n\n  public set showScrollArrows(show: boolean) {\n    if (this._showScrollArrows !== show) {\n      this._showScrollArrows = show\n      this.requestRender()\n    }\n  }\n\n  public get wrapSelection(): boolean {\n    return this._wrapSelection\n  }\n\n  public set wrapSelection(wrap: boolean) {\n    this._wrapSelection = wrap\n  }\n\n  public get tabWidth(): number {\n    return this._tabWidth\n  }\n\n  public set tabWidth(tabWidth: number) {\n    if (this._tabWidth === tabWidth) return\n\n    this._tabWidth = tabWidth\n    this.maxVisibleTabs = Math.max(1, Math.floor(this.width / this._tabWidth))\n\n    this.updateScrollOffset()\n    this.requestRender()\n  }\n\n  public set keyBindings(bindings: TabSelectKeyBinding[]) {\n    this._keyBindings = bindings\n    const mergedBindings = mergeKeyBindings(defaultTabSelectKeybindings, bindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n  }\n\n  public set keyAliasMap(aliases: KeyAliasMap) {\n    this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, aliases)\n    const mergedBindings = mergeKeyBindings(defaultTabSelectKeybindings, this._keyBindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Text.selection-buffer.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { TextRenderable } from \"./Text.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { createTestRenderer, type MockMouse, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { BoxRenderable } from \"./Box.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMouse: MockMouse\n\ndescribe(\"TextRenderable Selection - Buffer Validation\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockMouse: currentMouse,\n    } = await createTestRenderer({\n      width: 50,\n      height: 10,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  it(\"applies selection background colors to selected text renderables\", async () => {\n    const box1 = new BoxRenderable(currentRenderer, {\n      id: \"box1\",\n      left: 2,\n      top: 2,\n      width: 45,\n      height: 7,\n      backgroundColor: \"#1e2936\",\n      borderColor: \"#58a6ff\",\n      title: \"Document Section 1\",\n      flexDirection: \"column\",\n      padding: 1,\n      border: true,\n    })\n    currentRenderer.root.add(box1)\n\n    const text1 = new TextRenderable(currentRenderer, {\n      id: \"text1\",\n      content: \"This is a paragraph in the first box.\",\n      fg: \"#f0f6fc\",\n      selectionBg: \"#4a5568\",\n      selectionFg: \"#ffffff\",\n    })\n    box1.add(text1)\n\n    const text2 = new TextRenderable(currentRenderer, {\n      id: \"text2\",\n      content: \"It contains multiple lines of text\",\n      fg: \"#f0f6fc\",\n      selectionBg: \"#4a5568\",\n      selectionFg: \"#ffffff\",\n    })\n    box1.add(text2)\n\n    const text3 = new TextRenderable(currentRenderer, {\n      id: \"text3\",\n      content: \"that can be selected independently.\",\n      fg: \"#f0f6fc\",\n      selectionBg: \"#4a5568\",\n      selectionFg: \"#ffffff\",\n    })\n    box1.add(text3)\n\n    await renderOnce()\n\n    await currentMouse.drag(text1.x, text1.y, text2.x + 10, text2.y)\n    await renderOnce()\n\n    expect(text1.hasSelection()).toBe(true)\n    expect(text2.hasSelection()).toBe(true)\n    expect(text3.hasSelection()).toBe(false)\n\n    expect(text1.getSelectedText()).toBe(\"This is a paragraph in the first box.\")\n    expect(text2.getSelectedText()).toBe(\"It contain\")\n\n    const buffers = currentRenderer.currentRenderBuffer.buffers\n    const width = currentRenderer.currentRenderBuffer.width\n    const expectedBg = RGBA.fromHex(\"#4a5568\")\n\n    const getBgAt = (x: number, y: number) => {\n      const index = y * width + x\n      return RGBA.fromValues(\n        buffers.bg[index * 4],\n        buffers.bg[index * 4 + 1],\n        buffers.bg[index * 4 + 2],\n        buffers.bg[index * 4 + 3],\n      )\n    }\n\n    for (let col = text1.x; col < text1.x + text1.plainText.length; col++) {\n      const bg = getBgAt(col, text1.y)\n      const bgMatches =\n        Math.abs(bg.r - expectedBg.r) < 0.01 &&\n        Math.abs(bg.g - expectedBg.g) < 0.01 &&\n        Math.abs(bg.b - expectedBg.b) < 0.01\n      expect(bgMatches).toBe(true)\n    }\n\n    for (let col = text2.x; col < text2.x + 10; col++) {\n      const bg = getBgAt(col, text2.y)\n      const bgMatches =\n        Math.abs(bg.r - expectedBg.r) < 0.01 &&\n        Math.abs(bg.g - expectedBg.g) < 0.01 &&\n        Math.abs(bg.b - expectedBg.b) < 0.01\n      expect(bgMatches).toBe(true)\n    }\n\n    for (let col = text2.x + 10; col < text2.x + text2.plainText.length; col++) {\n      const bg = getBgAt(col, text2.y)\n      const bgMatches =\n        Math.abs(bg.r - expectedBg.r) < 0.01 &&\n        Math.abs(bg.g - expectedBg.g) < 0.01 &&\n        Math.abs(bg.b - expectedBg.b) < 0.01\n      expect(bgMatches).toBe(false)\n    }\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Text.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { TextRenderable, type TextOptions } from \"./Text.js\"\nimport { TextNodeRenderable } from \"./TextNode.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { stringToStyledText, StyledText } from \"../lib/styled-text.js\"\nimport { createTestRenderer, type MockMouse, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { BoxRenderable } from \"./Box.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMouse: MockMouse\nlet captureFrame: () => string\nlet resize: (width: number, height: number) => void\n\nasync function createTextRenderable(\n  renderer: TestRenderer,\n  options: TextOptions,\n): Promise<{ text: TextRenderable; root: any }> {\n  const textRenderable = new TextRenderable(renderer, { left: 0, top: 0, ...options })\n  renderer.root.add(textRenderable)\n  await renderOnce()\n\n  return { text: textRenderable, root: renderer.root }\n}\n\ndescribe(\"TextRenderable Selection\", () => {\n  describe(\"Native getSelectedText\", () => {\n    it(\"should use native implementation\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x, text.y, text.x + 5, text.y)\n      await renderOnce()\n\n      const selectedText = text.getSelectedText()\n      expect(selectedText).toBe(\"Hello\")\n    })\n\n    it(\"should handle graphemes correctly\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello 🌍 World\",\n        selectable: true,\n      })\n\n      // Select \"Hello 🌍\" (7 characters: H,e,l,l,o, ,🌍)\n      await currentMouse.drag(text.x, text.y, text.x + 7, text.y)\n      await renderOnce()\n\n      const selectedText = text.getSelectedText()\n      expect(selectedText).toBe(\"Hello 🌍\")\n    })\n  })\n\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockMouse: currentMouse,\n      captureCharFrame: captureFrame,\n      resize,\n    } = await createTestRenderer({\n      width: 20,\n      height: 5,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Initialization\", () => {\n    it(\"should initialize properly\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        selectable: true,\n      })\n\n      expect(text.x).toBeDefined()\n      expect(text.y).toBeDefined()\n      expect(text.width).toBeGreaterThan(0)\n      expect(text.height).toBeGreaterThan(0)\n    })\n  })\n\n  describe(\"Basic Selection Flow\", () => {\n    it(\"should handle selection from start to end\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        selectable: true,\n      })\n\n      expect(text.hasSelection()).toBe(false)\n      expect(text.getSelection()).toBe(null)\n      expect(text.getSelectedText()).toBe(\"\")\n\n      expect(text.shouldStartSelection(6, 0)).toBe(true)\n\n      await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)\n      await renderOnce()\n\n      expect(text.hasSelection()).toBe(true)\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(selection!.start).toBe(6)\n      expect(selection!.end).toBe(11)\n\n      expect(text.getSelectedText()).toBe(\"World\")\n    })\n\n    it(\"should handle selection with newline characters\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Line 1\\nLine 2\\nLine 3\",\n        selectable: true,\n      })\n\n      // Select from middle of line 2 to middle of line 3\n      await currentMouse.drag(text.x + 2, text.y + 1, text.x + 4, text.y + 2)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 starts at 7\n      // Position \"n\" in \"Line 2\" is at 7 + 2 = 9\n      expect(selection!.start).toBe(9)\n      // Line 2 starts at 14, position after \"Line\" is 14 + 4 = 18\n      expect(selection!.end).toBe(18)\n\n      expect(text.getSelectedText()).toBe(\"ne 2\\nLine\")\n    })\n\n    it(\"should handle selection across empty lines\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Line 1\\nLine 2\\n\\nLine 4\",\n        selectable: true,\n      })\n\n      // Select from start of line 1 to position 2 on empty line 3\n      await currentMouse.drag(text.x, text.y, text.x + 2, text.y + 2)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 (7-12) + newline (13) + Line 2 empty (14)\n      // Selecting to (col=2, row=2) on empty line clamps to col=0, so end=14\n      expect(selection!.start).toBe(0)\n      expect(selection!.end).toBe(14)\n      expect(text.getSelectedText()).toBe(\"Line 1\\nLine 2\")\n    })\n\n    it(\"should handle selection ending in empty line\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Line 1\\n\\nLine 3\",\n        selectable: true,\n      })\n\n      // Select from start of line 1 into the empty line 2\n      await currentMouse.drag(text.x, text.y, text.x + 3, text.y + 1)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 empty (7)\n      // Selecting to (col=3, row=1) on empty line clamps to col=0, so end=7\n      expect(selection!.start).toBe(0)\n      expect(selection!.end).toBe(7)\n      expect(text.getSelectedText()).toBe(\"Line 1\")\n    })\n\n    it(\"should handle selection spanning multiple lines completely\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"First\\nSecond\\nThird\",\n        selectable: true,\n      })\n\n      // Select from start of line 1 to end of line 2 (actually selecting Second)\n      await currentMouse.drag(text.x, text.y + 1, text.x + 6, text.y + 1)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(text.getSelectedText()).toBe(\"Second\")\n    })\n\n    it(\"should handle selection including multiple line breaks\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"A\\nB\\nC\\nD\",\n        selectable: true,\n      })\n\n      // Select from middle of first line to middle of last line\n      await currentMouse.drag(text.x, text.y + 1, text.x + 1, text.y + 2)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      const selectedText = text.getSelectedText()\n      expect(selectedText).toContain(\"\\n\")\n      expect(selectedText).toContain(\"B\")\n      expect(selectedText).toContain(\"C\")\n    })\n\n    it(\"should handle selection that includes line breaks at boundaries\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Line1\\nLine2\\nLine3\",\n        selectable: true,\n      })\n\n      // Select across line boundaries\n      await currentMouse.drag(text.x + 4, text.y, text.x + 2, text.y + 1)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      const selectedText = text.getSelectedText()\n      expect(selectedText).toContain(\"1\")\n      expect(selectedText).toContain(\"\\n\")\n      expect(selectedText).toContain(\"Li\")\n    })\n\n    it(\"should handle reverse selection (end before start)\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x + 11, text.y, text.x + 6, text.y)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(selection!.start).toBe(6)\n      expect(selection!.end).toBe(11)\n\n      expect(text.getSelectedText()).toBe(\"World\")\n    })\n  })\n\n  describe(\"Selection Edge Cases\", () => {\n    it(\"should handle empty text\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x, text.y, text.x, text.y)\n      await renderOnce()\n\n      expect(text.hasSelection()).toBe(false)\n      expect(text.getSelection()).toBe(null)\n      expect(text.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should handle single character selection\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"A\",\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x, text.y, text.x + 1, text.y)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(selection!.start).toBe(0)\n      expect(selection!.end).toBe(1)\n\n      expect(text.getSelectedText()).toBe(\"A\")\n    })\n\n    it(\"should handle zero-width selection\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x + 5, text.y, text.x + 5, text.y)\n      await renderOnce()\n\n      expect(text.hasSelection()).toBe(false)\n      expect(text.getSelection()).toBe(null)\n      expect(text.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should handle selection beyond text bounds\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hi\",\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x, text.y, text.x + 10, text.y)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(selection!.start).toBe(0)\n      expect(selection!.end).toBe(2)\n\n      expect(text.getSelectedText()).toBe(\"Hi\")\n    })\n  })\n\n  describe(\"Selection with Styled Text\", () => {\n    it(\"should handle styled text selection\", async () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text\n\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: styledText,\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(selection!.start).toBe(6)\n      expect(selection!.end).toBe(11)\n\n      expect(text.getSelectedText()).toBe(\"World\")\n    })\n\n    it(\"should handle selection with different text colors\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Red and Blue\",\n        selectable: true,\n        selectionBg: RGBA.fromValues(1, 1, 0, 1),\n        selectionFg: RGBA.fromValues(0, 0, 0, 1),\n      })\n\n      await currentMouse.drag(text.x + 8, text.y, text.x + 12, text.y)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(selection!.start).toBe(8)\n      expect(selection!.end).toBe(12)\n\n      expect(text.getSelectedText()).toBe(\"Blue\")\n    })\n  })\n\n  describe(\"Selection State Management\", () => {\n    it(\"should clear selection when selection is cleared\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)\n      await renderOnce()\n      expect(text.hasSelection()).toBe(true)\n\n      currentRenderer.clearSelection()\n      await renderOnce()\n\n      expect(text.hasSelection()).toBe(false)\n      expect(text.getSelection()).toBe(null)\n      expect(text.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should handle multiple selection changes\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World Test\",\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x + 0, text.y, text.x + 5, text.y)\n      await renderOnce()\n      expect(text.getSelectedText()).toBe(\"Hello\")\n      expect(text.getSelection()).toEqual({ start: 0, end: 5 })\n\n      await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)\n      await renderOnce()\n      expect(text.getSelectedText()).toBe(\"World\")\n      expect(text.getSelection()).toEqual({ start: 6, end: 11 })\n\n      await currentMouse.drag(text.x + 12, text.y, text.x + 16, text.y)\n      await renderOnce()\n      expect(text.getSelectedText()).toBe(\"Test\")\n      expect(text.getSelection()).toEqual({ start: 12, end: 16 })\n    })\n  })\n\n  describe(\"shouldStartSelection\", () => {\n    it(\"should return false for non-selectable text\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        selectable: false,\n      })\n\n      expect(text.shouldStartSelection(0, 0)).toBe(false)\n      expect(text.shouldStartSelection(5, 0)).toBe(false)\n    })\n\n    it(\"should return true for selectable text within bounds\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        selectable: true,\n      })\n\n      expect(text.shouldStartSelection(0, 0)).toBe(true) // Start of text\n      expect(text.shouldStartSelection(5, 0)).toBe(true) // Middle of text\n      expect(text.shouldStartSelection(10, 0)).toBe(true) // End of text\n    })\n\n    it(\"should handle shouldStartSelection with multi-line text\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Line 1\\nLine 2\\nLine 3\",\n        selectable: true,\n      })\n\n      expect(text.shouldStartSelection(0, 0)).toBe(true) // Line 1 start\n      expect(text.shouldStartSelection(2, 1)).toBe(true) // Line 2 middle\n      expect(text.shouldStartSelection(5, 2)).toBe(true) // Line 3 end\n    })\n  })\n\n  describe(\"Selection with Custom Dimensions\", () => {\n    it(\"should handle selection in constrained width\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"This is a very long text that should wrap to multiple lines\",\n        width: 10,\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x, text.y, text.x + 10, text.y + 2)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(selection!.start).toBeGreaterThanOrEqual(0)\n      expect(selection!.end).toBeGreaterThan(selection!.start)\n      expect(text.getSelectedText().length).toBeGreaterThan(0)\n    })\n  })\n\n  describe(\"Cross-Renderable Selection in Nested Boxes\", () => {\n    it(\"should handle selection across multiple nested text renderables in boxes\", async () => {\n      const { text: statusText } = await createTextRenderable(currentRenderer, {\n        content: \"Selected 5 chars:\",\n        selectable: true,\n        fg: \"#f0f6fc\",\n        top: 0,\n      })\n\n      const { text: selectionStartText } = await createTextRenderable(currentRenderer, {\n        content: '\"Hello\"',\n        selectable: true,\n        fg: \"#7dd3fc\",\n        top: 1,\n      })\n\n      const { text: selectionMiddleText } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: \"#94a3b8\",\n        top: 2,\n      })\n\n      const { text: selectionEndText } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: \"#7dd3fc\",\n        top: 3,\n      })\n\n      const { text: debugText } = await createTextRenderable(currentRenderer, {\n        content: \"Selected renderables: 2/5\",\n        selectable: true,\n        fg: \"#e6edf3\",\n        top: 4,\n      })\n\n      // Simulate starting selection above the box and ending below/right of the box\n      // This should cover all renderables in the \"box\"\n      const allRenderables = [statusText, selectionStartText, selectionMiddleText, selectionEndText, debugText]\n\n      await currentMouse.drag(0, 0, 50, 10)\n      await renderOnce()\n\n      expect(statusText.hasSelection()).toBe(true)\n      expect(statusText.getSelectedText()).toBe(\"Selected 5 chars:\")\n\n      expect(selectionStartText.hasSelection()).toBe(true)\n      expect(selectionStartText.getSelectedText()).toBe('\"Hello\"')\n\n      // Empty text renderables should not have selections since there's no content to select\n      expect(selectionMiddleText.hasSelection()).toBe(false)\n      expect(selectionMiddleText.getSelectedText()).toBe(\"\")\n\n      expect(selectionEndText.hasSelection()).toBe(false)\n      expect(selectionEndText.getSelectedText()).toBe(\"\")\n\n      expect(debugText.hasSelection()).toBe(true)\n      expect(debugText.getSelectedText()).toBe(\"Selected renderables: 2/5\")\n\n      const globalSelectedText = currentRenderer.getSelection()?.getSelectedText()\n\n      expect(globalSelectedText).toContain(\"Selected 5 chars:\")\n      expect(globalSelectedText).toContain('\"Hello\"')\n      expect(globalSelectedText).toContain(\"Selected renderables: 2/5\")\n    })\n\n    it(\"should automatically update selection when text content changes within covered area\", async () => {\n      const { text: statusText } = await createTextRenderable(currentRenderer, {\n        content: \"Selected 5 chars:\",\n        selectable: true,\n        fg: \"#f0f6fc\",\n        top: 0,\n        wrapMode: \"none\",\n      })\n\n      const { text: selectionStartText } = await createTextRenderable(currentRenderer, {\n        top: 1,\n        content: '\"Hello\"',\n        selectable: true,\n        fg: \"#7dd3fc\",\n        wrapMode: \"none\",\n      })\n\n      const { text: debugText } = await createTextRenderable(currentRenderer, {\n        top: 2,\n        content: \"Selected renderables: 2/5\",\n        selectable: true,\n        fg: \"#e6edf3\",\n        wrapMode: \"none\",\n      })\n\n      await currentMouse.drag(0, 0, 50, 5)\n      await renderOnce()\n\n      expect(statusText.getSelectedText()).toBe(\"Selected 5 chars:\")\n      expect(selectionStartText.getSelectedText()).toBe('\"Hello\"')\n      expect(debugText.getSelectedText()).toBe(\"Selected renderables: 2/5\")\n\n      selectionStartText.content = '\"Hello World Extended Selection\"'\n\n      expect(statusText.getSelectedText()).toBe(\"Selected 5 chars:\")\n      expect(selectionStartText.getSelectedText()).toBe('\"Hello World Extended Selection\"')\n      expect(debugText.getSelectedText()).toBe(\"Selected renderables: 2/5\")\n\n      const updatedGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()\n\n      expect(updatedGlobalSelectedText).toContain('\"Hello World Extended Selection\"')\n      expect(updatedGlobalSelectedText).toContain(\"Selected 5 chars:\")\n      expect(updatedGlobalSelectedText).toContain(\"Selected renderables: 2/5\")\n\n      debugText.content = \"Selected renderables: 3/5 | Container: statusBox\"\n\n      expect(debugText.getSelectedText()).toBe(\"Selected renderables: 3/5 | Container: statusBox\")\n\n      const finalGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()\n\n      expect(finalGlobalSelectedText).toContain(\"Selected renderables: 3/5 | Container: statusBox\")\n    })\n\n    it(\"should automatically update selection when text node content changes with clear and add\", async () => {\n      const { text: statusText } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: \"#f0f6fc\",\n        top: 0,\n        wrapMode: \"none\",\n      })\n\n      const statusNode = new TextNodeRenderable({})\n      statusNode.add(\"Selected 5 chars:\")\n      statusText.add(statusNode)\n\n      const { text: selectionStartText } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: \"#7dd3fc\",\n        top: 1,\n        wrapMode: \"none\",\n      })\n\n      const selectionNode = new TextNodeRenderable({})\n      selectionNode.add('\"Hello\"')\n      selectionStartText.add(selectionNode)\n\n      const { text: debugText } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: \"#e6edf3\",\n        top: 2,\n        wrapMode: \"none\",\n      })\n\n      const debugNode = new TextNodeRenderable({})\n      debugNode.add(\"Selected renderables: 2/5\")\n      debugText.add(debugNode)\n\n      await currentMouse.drag(0, 0, 50, 5)\n      await renderOnce()\n\n      expect(statusText.getSelectedText()).toBe(\"Selected 5 chars:\")\n      expect(selectionStartText.getSelectedText()).toBe('\"Hello\"')\n      expect(debugText.getSelectedText()).toBe(\"Selected renderables: 2/5\")\n\n      // Clear and add new content to the selection node\n      selectionNode.clear()\n      selectionNode.add('\"Hello World Extended Selection\"')\n      await renderOnce()\n\n      expect(statusText.getSelectedText()).toBe(\"Selected 5 chars:\")\n      expect(selectionStartText.getSelectedText()).toBe('\"Hello World Extended Selection\"')\n      expect(debugText.getSelectedText()).toBe(\"Selected renderables: 2/5\")\n\n      const updatedGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()\n\n      expect(updatedGlobalSelectedText).toContain('\"Hello World Extended Selection\"')\n      expect(updatedGlobalSelectedText).toContain(\"Selected 5 chars:\")\n      expect(updatedGlobalSelectedText).toContain(\"Selected renderables: 2/5\")\n\n      // Clear and add new content to the debug node\n      debugNode.clear()\n      debugNode.add(\"Selected renderables: 3/5 | Container: statusBox\")\n      await renderOnce()\n\n      expect(debugText.getSelectedText()).toBe(\"Selected renderables: 3/5 | Container: statusBox\")\n\n      const finalGlobalSelectedText = currentRenderer.getSelection()?.getSelectedText()\n\n      expect(finalGlobalSelectedText).toContain(\"Selected renderables: 3/5 | Container: statusBox\")\n    })\n\n    it(\"should handle selection that starts above box and ends below/right of box\", async () => {\n      const { text: statusText } = await createTextRenderable(currentRenderer, {\n        content: \"Status: Selection active\",\n        selectable: true,\n        fg: \"#f0f6fc\",\n        top: 2,\n        wrapMode: \"none\",\n      })\n\n      const { text: selectionStartText } = await createTextRenderable(currentRenderer, {\n        content: \"Start: (10,5)\",\n        selectable: true,\n        fg: \"#7dd3fc\",\n        top: 3,\n        wrapMode: \"none\",\n      })\n\n      const { text: selectionEndText } = await createTextRenderable(currentRenderer, {\n        content: \"End: (45,12)\",\n        selectable: true,\n        fg: \"#7dd3fc\",\n        top: 4,\n        wrapMode: \"none\",\n      })\n\n      const { text: debugText } = await createTextRenderable(currentRenderer, {\n        content: \"Debug: Cross-renderable selection spanning 3 elements\",\n        selectable: true,\n        fg: \"#e6edf3\",\n        top: 5,\n        wrapMode: \"none\",\n      })\n\n      const allRenderables = [statusText, selectionStartText, selectionEndText, debugText]\n\n      await currentMouse.drag(statusText.x, statusText.y, 60, 10)\n      await renderOnce()\n\n      allRenderables.forEach((renderable) => {\n        expect(renderable.hasSelection()).toBe(true)\n      })\n\n      expect(statusText.getSelectedText()).toBe(\"Status: Selection active\")\n      expect(selectionStartText.getSelectedText()).toBe(\"Start: (10,5)\")\n      expect(selectionEndText.getSelectedText()).toBe(\"End: (45,12)\")\n      expect(debugText.getSelectedText()).toBe(\"Debug: Cross-renderable selection spanning 3 elements\")\n\n      const globalSelectedText = currentRenderer.getSelection()?.getSelectedText()\n\n      expect(globalSelectedText).toContain(\"Status: Selection active\")\n      expect(globalSelectedText).toContain(\"Start: (10,5)\")\n      expect(globalSelectedText).toContain(\"End: (45,12)\")\n      expect(globalSelectedText).toContain(\"Debug: Cross-renderable selection spanning 3 elements\")\n    })\n  })\n\n  describe(\"TextNode Integration with getPlainText\", () => {\n    it(\"should render correct plain text after adding TextNodes\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const node1 = new TextNodeRenderable({\n        fg: RGBA.fromValues(1, 0, 0, 1),\n        bg: RGBA.fromValues(0, 0, 0, 1),\n      })\n      node1.add(\"Hello\")\n\n      const node2 = new TextNodeRenderable({\n        fg: RGBA.fromValues(0, 1, 0, 1),\n        bg: RGBA.fromValues(0, 0, 0, 1),\n      })\n      node2.add(\" World\")\n\n      text.add(node1)\n      text.add(node2)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should render correct plain text after inserting TextNodes\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const node1 = new TextNodeRenderable({})\n      node1.add(\"Hello\")\n\n      const node2 = new TextNodeRenderable({})\n      node2.add(\" World\")\n\n      const node3 = new TextNodeRenderable({})\n      node3.add(\"!\")\n\n      text.add(node1)\n      text.add(node2)\n\n      text.insertBefore(node3, node2)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Hello! World\")\n    })\n\n    it(\"should render correct plain text after removing TextNodes\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const node1 = new TextNodeRenderable({})\n      node1.add(\"Hello\")\n\n      const node2 = new TextNodeRenderable({})\n      node2.add(\" Cruel\")\n\n      const node3 = new TextNodeRenderable({})\n      node3.add(\" World\")\n\n      text.add(node1)\n      text.add(node2)\n      text.add(node3)\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"Hello Cruel World\")\n\n      text.remove(node2.id)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should handle simple add and remove operations\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const node = new TextNodeRenderable({})\n      node.add(\"Test\")\n\n      text.add(node)\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"Test\")\n\n      text.remove(node.id)\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"\")\n    })\n\n    it(\"should render correct plain text after clearing all TextNodes\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const node1 = new TextNodeRenderable({})\n      node1.add(\"Hello\")\n\n      const node2 = new TextNodeRenderable({})\n      node2.add(\" World\")\n\n      text.add(node1)\n      text.add(node2)\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"Hello World\")\n\n      text.clear()\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"\")\n    })\n\n    it(\"should handle nested TextNode structures correctly\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      // Create nested structure: Parent -> [Child1, Child2]\n      const parent = new TextNodeRenderable({\n        fg: RGBA.fromValues(1, 1, 0, 1),\n      })\n\n      const child1 = new TextNodeRenderable({\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n      child1.add(\"Red\")\n\n      const child2 = new TextNodeRenderable({\n        fg: RGBA.fromValues(0, 1, 0, 1),\n      })\n      child2.add(\" Green\")\n\n      parent.add(child1)\n      parent.add(child2)\n\n      const standalone = new TextNodeRenderable({\n        fg: RGBA.fromValues(0, 0, 1, 1),\n      })\n      standalone.add(\" Blue\")\n\n      text.add(parent)\n      text.add(standalone)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Red Green Blue\")\n    })\n\n    it(\"should handle mixed string and TextNode content\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const startNode = new TextNodeRenderable({})\n      startNode.add(\"Start \")\n\n      const node1 = new TextNodeRenderable({})\n      node1.add(\"middle\")\n\n      const node2 = new TextNodeRenderable({})\n      node2.add(\" end\")\n\n      text.add(startNode)\n      text.add(node1)\n      text.add(node2)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Start middle end\")\n    })\n\n    it(\"should handle TextNode operations with inherited styles\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: RGBA.fromValues(1, 1, 1, 1), // White default\n      })\n\n      const redParent = new TextNodeRenderable({\n        fg: RGBA.fromValues(1, 0, 0, 1), // Red\n      })\n\n      const redChild = new TextNodeRenderable({})\n\n      const greenGrandchild = new TextNodeRenderable({\n        fg: RGBA.fromValues(0, 1, 0, 1), // Green\n      })\n      greenGrandchild.add(\"Green\")\n\n      redChild.add(greenGrandchild)\n      redParent.add(redChild)\n\n      const blueNode = new TextNodeRenderable({\n        fg: RGBA.fromValues(0, 0, 1, 1), // Blue\n      })\n      blueNode.add(\" Blue\")\n\n      text.add(redParent)\n      text.add(blueNode)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Green Blue\")\n    })\n\n    it(\"should handle empty TextNodes correctly\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const emptyNode1 = new TextNodeRenderable({})\n      const nodeWithText = new TextNodeRenderable({})\n      nodeWithText.add(\"Text\")\n      const emptyNode2 = new TextNodeRenderable({})\n\n      text.add(emptyNode1)\n      text.add(nodeWithText)\n      text.add(emptyNode2)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Text\")\n    })\n\n    it(\"should handle complex TextNode operations sequence\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const initialNode = new TextNodeRenderable({})\n      initialNode.add(\"Initial\")\n\n      const nodeA = new TextNodeRenderable({})\n      nodeA.add(\" A\")\n\n      const nodeB = new TextNodeRenderable({})\n      nodeB.add(\" B\")\n\n      const nodeC = new TextNodeRenderable({})\n      nodeC.add(\" C\")\n\n      const nodeD = new TextNodeRenderable({})\n      nodeD.add(\" D\")\n\n      text.add(initialNode)\n      text.add(nodeA)\n      text.add(nodeB)\n      text.add(nodeC)\n      text.add(nodeD)\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"Initial A B C D\")\n\n      text.remove(nodeB.id)\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"Initial A C D\")\n\n      const nodeX = new TextNodeRenderable({})\n      nodeX.add(\" X\")\n      text.insertBefore(nodeX, nodeC)\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"Initial A X C D\")\n\n      nodeX.add(\" Y\")\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"Initial A X Y C D\")\n    })\n\n    it(\"should inherit fg/bg colors from TextRenderable to TextNode children\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: RGBA.fromValues(1, 0, 0, 1),\n        bg: RGBA.fromValues(0, 0, 1, 1),\n      })\n\n      const child1 = new TextNodeRenderable({})\n      child1.add(\"Child1\")\n\n      const child2 = new TextNodeRenderable({})\n      child2.add(\" Child2\")\n\n      text.add(child1)\n      text.add(child2)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Child1 Child2\")\n\n      const chunks = text.textNode.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(2)\n\n      chunks.forEach((chunk) => {\n        expect(chunk.fg).toEqual(RGBA.fromValues(1, 0, 0, 1))\n        expect(chunk.bg).toEqual(RGBA.fromValues(0, 0, 1, 1))\n        expect(chunk.attributes).toBe(0)\n      })\n\n      expect(chunks[0].text).toBe(\"Child1\")\n      expect(chunks[1].text).toBe(\" Child2\")\n    })\n\n    it(\"should allow TextNode children to override parent TextRenderable colors\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: RGBA.fromValues(1, 0, 0, 1),\n        bg: RGBA.fromValues(0, 0, 1, 1),\n      })\n\n      const inheritingChild = new TextNodeRenderable({})\n      inheritingChild.add(\"Inherit\")\n\n      const overridingChild = new TextNodeRenderable({\n        fg: RGBA.fromValues(0, 1, 0, 1),\n        bg: RGBA.fromValues(1, 1, 0, 1),\n      })\n      overridingChild.add(\" Override\")\n\n      const partialOverrideChild = new TextNodeRenderable({\n        fg: RGBA.fromValues(0, 0, 1, 1),\n      })\n      partialOverrideChild.add(\" Partial\")\n\n      text.add(inheritingChild)\n      text.add(overridingChild)\n      text.add(partialOverrideChild)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Inherit Override Partial\")\n\n      const chunks = text.textNode.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(3)\n\n      // First child: inherits both fg and bg from parent\n      expect(chunks[0].text).toBe(\"Inherit\")\n      expect(chunks[0].fg).toEqual(RGBA.fromValues(1, 0, 0, 1))\n      expect(chunks[0].bg).toEqual(RGBA.fromValues(0, 0, 1, 1))\n\n      // Second child: overrides both fg and bg\n      expect(chunks[1].text).toBe(\" Override\")\n      expect(chunks[1].fg).toEqual(RGBA.fromValues(0, 1, 0, 1))\n      expect(chunks[1].bg).toEqual(RGBA.fromValues(1, 1, 0, 1))\n\n      // Third child: overrides fg, inherits bg\n      expect(chunks[2].text).toBe(\" Partial\")\n      expect(chunks[2].fg).toEqual(RGBA.fromValues(0, 0, 1, 1))\n      expect(chunks[2].bg).toEqual(RGBA.fromValues(0, 0, 1, 1))\n    })\n\n    it(\"should inherit TextRenderable colors through nested TextNode hierarchies\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: RGBA.fromValues(0, 1, 0, 1),\n        bg: RGBA.fromValues(0, 0, 0, 1),\n      })\n\n      const grandparent = new TextNodeRenderable({})\n      const parent = new TextNodeRenderable({})\n      const child = new TextNodeRenderable({})\n\n      child.add(\"Deep\")\n      parent.add(\"Nested \")\n      parent.add(child)\n      grandparent.add(\"Very \")\n      grandparent.add(parent)\n\n      text.add(grandparent)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Very Nested Deep\")\n\n      const chunks = text.textNode.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(3)\n\n      // All chunks should inherit the TextRenderable's green fg and black bg\n      chunks.forEach((chunk) => {\n        expect(chunk.fg).toEqual(RGBA.fromValues(0, 1, 0, 1))\n        expect(chunk.bg).toEqual(RGBA.fromValues(0, 0, 0, 1))\n        expect(chunk.attributes).toBe(0)\n      })\n\n      expect(chunks[0].text).toBe(\"Very \")\n      expect(chunks[1].text).toBe(\"Nested \")\n      expect(chunks[2].text).toBe(\"Deep\")\n    })\n\n    it(\"should handle TextRenderable color changes affecting existing TextNode children\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n        fg: RGBA.fromValues(1, 0, 0, 1),\n        bg: RGBA.fromValues(0, 0, 0, 1),\n      })\n\n      const child1 = new TextNodeRenderable({})\n      child1.add(\"Before\")\n\n      const child2 = new TextNodeRenderable({})\n      child2.add(\" Change\")\n\n      text.add(child1)\n      text.add(child2)\n\n      await renderOnce()\n      expect(text.plainText).toBe(\"Before Change\")\n\n      text.fg = RGBA.fromValues(0, 0, 1, 1)\n      text.bg = RGBA.fromValues(1, 1, 1, 1)\n\n      await renderOnce()\n\n      const chunks = text.textNode.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(2)\n\n      chunks.forEach((chunk) => {\n        expect(chunk.fg).toEqual(RGBA.fromValues(0, 0, 1, 1))\n        expect(chunk.bg).toEqual(RGBA.fromValues(1, 1, 1, 1))\n      })\n\n      expect(chunks[0].text).toBe(\"Before\")\n      expect(chunks[1].text).toBe(\" Change\")\n    })\n\n    it(\"should handle TextNode commands with multiple operations per render\", async () => {\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const node1 = new TextNodeRenderable({})\n      node1.add(\"First\")\n\n      const node2 = new TextNodeRenderable({})\n      node2.add(\"Second\")\n\n      const node3 = new TextNodeRenderable({})\n      node3.add(\"Third\")\n\n      text.add(node1)\n      text.add(node2)\n      text.insertBefore(node3, node1)\n\n      node2.add(\" Modified\")\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"ThirdFirstSecond Modified\")\n    })\n  })\n\n  describe(\"StyledText Integration\", () => {\n    it(\"should render StyledText content correctly\", async () => {\n      const styledText = stringToStyledText(\"Hello World\")\n\n      styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text\n      styledText.chunks[0].bg = RGBA.fromValues(0, 0, 0, 1) // Black background\n\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: styledText,\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Hello World\")\n      expect(text.width).toBeGreaterThan(0)\n      expect(text.height).toBeGreaterThan(0)\n    })\n\n    it(\"should handle selection with StyledText content\", async () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      styledText.chunks[0].fg = RGBA.fromValues(1, 0, 0, 1) // Red text\n\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: styledText,\n        selectable: true,\n      })\n\n      await currentMouse.drag(text.x + 6, text.y, text.x + 11, text.y)\n      await renderOnce()\n\n      const selection = text.getSelection()\n      expect(selection).not.toBe(null)\n      expect(selection!.start).toBe(6)\n      expect(selection!.end).toBe(11)\n      expect(text.getSelectedText()).toBe(\"World\")\n    })\n\n    it(\"should handle empty StyledText\", async () => {\n      const emptyStyledText = stringToStyledText(\"\")\n\n      const { text, root } = await createTextRenderable(currentRenderer, {\n        content: emptyStyledText,\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"\")\n      expect(text.hasSelection()).toBe(false)\n      expect(text.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should handle StyledText with multiple chunks\", async () => {\n      const styledText = new StyledText([\n        { __isChunk: true, text: \"Red\", fg: RGBA.fromValues(1, 0, 0, 1), attributes: 1 },\n        { __isChunk: true, text: \" \", fg: undefined, attributes: 0 },\n        { __isChunk: true, text: \"Green\", fg: RGBA.fromValues(0, 1, 0, 1), attributes: 2 },\n        { __isChunk: true, text: \" \", fg: undefined, attributes: 0 },\n        { __isChunk: true, text: \"Blue\", fg: RGBA.fromValues(0, 0, 1, 1), attributes: 0 },\n      ])\n\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: styledText,\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Red Green Blue\")\n\n      await currentMouse.drag(text.x + 4, text.y, text.x + 9, text.y)\n      await renderOnce()\n\n      expect(text.getSelectedText()).toBe(\"Green\")\n    })\n\n    it(\"should handle StyledText with TextNodeRenderable children\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        selectable: true,\n      })\n\n      const baseNode = new TextNodeRenderable({})\n      baseNode.add(\"Base \")\n      text.add(baseNode)\n\n      const styledNode = new TextNodeRenderable({\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      const nodeStyledText = new StyledText([\n        { __isChunk: true, text: \"Styled\", fg: RGBA.fromValues(0, 1, 0, 1), attributes: 1 },\n      ])\n\n      styledNode.add(nodeStyledText)\n      text.add(styledNode)\n\n      await renderOnce()\n\n      expect(text.plainText).toBe(\"Base Styled\")\n\n      await currentMouse.drag(text.x + 5, text.y, text.x + 11, text.y)\n      await renderOnce()\n      expect(text.getSelectedText()).toBe(\"Styled\")\n    })\n  })\n\n  describe(\"Text Selection with Truncation\", () => {\n    it(\"should not extend selection across ellipsis in single line\", async () => {\n      const buffer = currentRenderer.currentRenderBuffer\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"0123456789ABCDEFGHIJ\",\n        width: 10,\n        height: 1,\n        selectable: true,\n        selectionBg: RGBA.fromValues(1, 0, 0, 1),\n        truncate: true,\n      })\n\n      await currentMouse.drag(text.x + 6, text.y, text.x + 3, text.y)\n      await renderOnce()\n\n      expect(text.hasSelection()).toBe(true)\n\n      const { bg } = buffer.buffers\n      const bufferWidth = buffer.width\n\n      const ellipsisIdx = text.y * bufferWidth + text.x + 3\n      const ellipsisBgR = bg[ellipsisIdx * 4 + 0]\n      const ellipsisBgG = bg[ellipsisIdx * 4 + 1]\n      const ellipsisBgB = bg[ellipsisIdx * 4 + 2]\n\n      expect(Math.abs(ellipsisBgR - 1.0)).toBeLessThan(0.05)\n      expect(Math.abs(ellipsisBgG - 0.0)).toBeLessThan(0.05)\n      expect(Math.abs(ellipsisBgB - 0.0)).toBeLessThan(0.05)\n    })\n\n    it(\"should render selection end correctly across ellipsis in last line\", async () => {\n      const buffer = currentRenderer.currentRenderBuffer\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Line 1: This is a long line without wrapping\\nLine 2: Another very long line that will be truncated\",\n        width: 10,\n        height: 2,\n        selectable: true,\n        selectionBg: RGBA.fromValues(1, 0, 0, 1),\n        truncate: true,\n        wrapMode: \"none\",\n      })\n\n      await currentMouse.drag(text.x + 6, text.y, text.x + 2, text.y + 1)\n      await renderOnce()\n\n      expect(text.hasSelection()).toBe(true)\n\n      const { bg } = buffer.buffers\n      const bufferWidth = buffer.width\n\n      const ellipsisIdx = (text.y + 1) * bufferWidth + text.x + 3\n      const ellipsisBgR = bg[ellipsisIdx * 4 + 0]\n      const ellipsisBgG = bg[ellipsisIdx * 4 + 1]\n      const ellipsisBgB = bg[ellipsisIdx * 4 + 2]\n\n      expect(Math.abs(ellipsisBgR - 1.0)).toBeGreaterThan(0.05)\n      expect(Math.abs(ellipsisBgG - 0.0)).toBeLessThan(0.05)\n      expect(Math.abs(ellipsisBgB - 0.0)).toBeLessThan(0.05)\n    })\n  })\n\n  describe(\"Text Content Snapshots\", () => {\n    it(\"should render basic text content correctly\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n        left: 5,\n        top: 3,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render multiline text content correctly\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"Line 1: Hello\\nLine 2: World\\nLine 3: Testing\\nLine 4: Multiline\",\n        left: 1,\n        top: 1,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text with graphemes/emojis correctly\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"Hello 🌍 World 👋\\n Test 🚀 Emoji\",\n        left: 0,\n        top: 2,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render TextNode text composition correctly\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        left: 0,\n        top: 0,\n      })\n\n      const node1 = new TextNodeRenderable({})\n      node1.add(\"First\")\n\n      const node2 = new TextNodeRenderable({})\n      node2.add(\" Second\")\n\n      const node3 = new TextNodeRenderable({})\n      node3.add(\" Third\")\n\n      text.add(node1)\n      text.add(node2)\n      text.add(node3)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text positioning correctly\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"Top\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n      })\n\n      await createTextRenderable(currentRenderer, {\n        content: \"Mid\",\n        position: \"absolute\",\n        left: 8,\n        top: 2,\n      })\n\n      await createTextRenderable(currentRenderer, {\n        content: \"Bot\",\n        position: \"absolute\",\n        left: 16,\n        top: 4,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render empty buffer correctly\", async () => {\n      currentRenderer.currentRenderBuffer.clear()\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text with character wrapping correctly\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"This is a very long text that should wrap to multiple lines when wrap is enabled\",\n        wrapMode: \"char\", // Explicitly test character wrapping\n        width: 15, // Force wrapping at 15 characters width\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render wrapped text with different content\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789\",\n        wrapMode: \"char\", // Explicitly test character wrapping\n        width: 10, // Force wrapping at 10 characters width\n        left: 2,\n        top: 1,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render wrapped text with emojis and graphemes\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"Hello 🌍 World 👋 This is a test with emojis 🚀 that should wrap properly\",\n        wrapMode: \"char\", // Explicitly test character wrapping\n        width: 12, // Force wrapping at 12 characters width\n        left: 1,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render wrapped multiline text correctly\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"First line with long content\\nSecond line also with content\\nThird line\",\n        wrapMode: \"char\", // Explicitly test character wrapping\n        width: 8, // Force wrapping at 8 characters width\n        left: 0,\n        top: 1,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text with tab indicator correctly\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"Line 1\\tTabbed\\nLine 2\\t\\tDouble tab\",\n        tabIndicator: \"→\",\n        tabIndicatorColor: RGBA.fromValues(0.5, 0.5, 0.5, 1),\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render word wrapped text with CJK and English correctly\", async () => {\n      resize(60, 10)\n\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"🌟 Unicode test: こんにちは世界 Hello World 你好世界\",\n        wrapMode: \"word\",\n        width: 35,\n        left: 0,\n        top: 0,\n      })\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      const lines = frame.split(\"\\n\").filter((l) => l.trim().length > 0)\n\n      // Verify no character duplication - each character should appear only once\n      const line0 = lines[0] || \"\"\n      const line1 = lines[1] || \"\"\n\n      const line0_ends_with_kai = line0.trimEnd().endsWith(\"界\")\n      const line1_starts_with_kai = line1.trimStart().startsWith(\"界\")\n\n      // \"界\" should not appear on both lines (would indicate duplication bug)\n      expect(line0_ends_with_kai && line1_starts_with_kai).toBe(false)\n    })\n\n    it(\"should not split English word 'Hello' in middle when word wrapping with CJK characters\", async () => {\n      // This test reproduces the exact issue from text-truncation-demo.ts where \"Hello\"\n      // is incorrectly split as \"Hell\" on first line and \"o World\" on second line\n      // when word wrapping is enabled with CJK/emoji characters before it.\n      resize(60, 10)\n\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"🌟 Unicode test: こんにちは世界 Hello World 你好世界 안녕하세요 🚀 More emoji: 🎨🎭🎪🎬🎮🎯\",\n        wrapMode: \"word\",\n        width: 50, // Width that causes wrapping in the demo\n        left: 0,\n        top: 0,\n      })\n\n      await renderOnce()\n\n      const frame = captureFrame()\n\n      const lines = frame.split(\"\\n\").filter((l) => l.trim().length > 0)\n\n      // The word \"Hello\" should NOT be split in the middle\n      // Check for the specific incorrect split: \"Hell\" on one line, \"o\" starting the next\n      let foundIncorrectSplit = false\n      for (let i = 0; i < lines.length - 1; i++) {\n        const currentLine = lines[i] || \"\"\n        const nextLine = lines[i + 1] || \"\"\n\n        // Check if current line ends with \"Hell\" (incorrect split)\n        if (currentLine.trimEnd().endsWith(\"Hell\")) {\n          // And next line starts with \"o\" (the rest of \"Hello\")\n          if (nextLine.trimStart().startsWith(\"o\")) {\n            foundIncorrectSplit = true\n          }\n        }\n      }\n\n      // Verify \"Hello\" is not split as \"Hell\" + \"o\"\n      expect(foundIncorrectSplit).toBe(false)\n\n      // Verify the word \"Hello\" appears complete on a single line\n      const fullText = lines.join(\" \")\n      expect(fullText).toContain(\"Hello\")\n\n      // Verify \"Hello\" is not split in the middle\n      const helloLineIndex = lines.findIndex((line) => line.includes(\"Hello\"))\n      expect(helloLineIndex).toBeGreaterThanOrEqual(0) // \"Hello\" should be found\n\n      const helloLine = lines[helloLineIndex] || \"\"\n      // Verify \"Hello\" appears as a complete word on this line\n      expect(helloLine).toMatch(/Hello/)\n\n      // Verify no previous line ends with \"Hell\" without \"o\"\n      if (helloLineIndex > 0) {\n        const prevLine = lines[helloLineIndex - 1] || \"\"\n        expect(prevLine.trimEnd().endsWith(\"Hell\")).toBe(false)\n      }\n\n      // Additional verification: \"Hello World\" should ideally be together\n      // (this is a nice-to-have, showing improved wrapping behavior)\n      expect(helloLine).toContain(\"Hello World\")\n    })\n  })\n\n  describe(\"Text Node Dimension Updates\", () => {\n    it(\"should update dimensions and reposition subsequent elements when text nodes expand\", async () => {\n      const { text: firstText } = await createTextRenderable(currentRenderer, {\n        content: \"\",\n        width: 20,\n        wrapMode: \"char\",\n      })\n\n      const shortNode = new TextNodeRenderable({})\n      shortNode.add(\"Short\")\n      firstText.add(shortNode)\n\n      const { text: secondText } = await createTextRenderable(currentRenderer, {\n        content: \"Second text\",\n      })\n\n      await renderOnce()\n      const initialFrame = captureFrame()\n      expect(initialFrame).toMatchSnapshot()\n\n      expect(firstText.height).toEqual(1)\n      expect(secondText.y).toEqual(1)\n\n      shortNode.add(\" text that will definitely wrap\")\n\n      await renderOnce()\n\n      const finalFrame = captureFrame()\n\n      expect(firstText.height).toEqual(2)\n      expect(secondText.y).toEqual(2)\n\n      expect(finalFrame).not.toBe(initialFrame)\n      expect(finalFrame).toMatchSnapshot()\n    })\n\n    it(\"should handle multiple text node updates with complex layout changes\", async () => {\n      resize(20, 10)\n      const { text: firstText } = await createTextRenderable(currentRenderer, {\n        width: 10,\n        wrapMode: \"word\",\n      })\n\n      const node1 = TextNodeRenderable.fromString(\"First\")\n      const node2 = TextNodeRenderable.fromString(\" part\")\n\n      firstText.add(node1)\n      firstText.add(node2)\n\n      const { text: secondText } = await createTextRenderable(currentRenderer, {\n        width: 12,\n        wrapMode: \"word\",\n      })\n      secondText.add(\"Middle text\")\n\n      const { text: thirdText } = await createTextRenderable(currentRenderer, {})\n      thirdText.add(\"Bottom text\")\n\n      await renderOnce()\n      const initialFrame = captureFrame()\n      expect(initialFrame).toMatchSnapshot()\n\n      // Record initial positions\n      expect(firstText.height).toEqual(1)\n      expect(secondText.y).toEqual(1)\n      expect(thirdText.y).toEqual(2)\n\n      node1.add(\" of a sentence\")\n      node2.add(\"that will wrap\")\n\n      await renderOnce()\n\n      const finalFrame = captureFrame()\n      expect(finalFrame).toMatchSnapshot()\n\n      expect(firstText.height).toEqual(5)\n      expect(secondText.y).toEqual(5)\n      expect(thirdText.y).toEqual(6)\n    })\n  })\n\n  describe(\"Height and Width Measurement\", () => {\n    it(\"should grow height for multiline text without wrapping\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\",\n        wrapMode: \"none\",\n      })\n\n      await renderOnce()\n\n      expect(text.height).toBe(5)\n      expect(text.width).toBeGreaterThanOrEqual(6)\n    })\n\n    it(\"should grow height for wrapped text when wrapping enabled\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"This is a very long line that will definitely wrap to multiple lines\",\n        wrapMode: \"word\",\n        width: 15,\n      })\n\n      await renderOnce()\n\n      expect(text.height).toBeGreaterThan(1)\n      expect(text.width).toBeLessThanOrEqual(15)\n    })\n\n    it(\"should measure full width when wrapping is disabled and not constrained by parent\", async () => {\n      const longLine = \"This is a very long line that would wrap but wrapping is disabled\"\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: longLine,\n        wrapMode: \"none\",\n        position: \"absolute\",\n      })\n\n      await renderOnce()\n\n      expect(text.height).toBe(1)\n      expect(text.width).toBe(longLine.length)\n    })\n\n    it(\"should update height when content changes from single to multiline\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Single line\",\n        wrapMode: \"none\",\n      })\n\n      await renderOnce()\n      expect(text.height).toBe(1)\n\n      text.content = \"Line 1\\nLine 2\\nLine 3\"\n      await renderOnce()\n\n      expect(text.height).toBe(3)\n    })\n\n    it(\"should update height when wrapping mode changes\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"This is a long line that will wrap to multiple lines\",\n        wrapMode: \"none\",\n        width: 15,\n      })\n\n      await renderOnce()\n      const unwrappedHeight = text.height\n      expect(unwrappedHeight).toBe(1)\n      expect(text.width).toBe(15)\n\n      text.wrapMode = \"word\"\n      await renderOnce()\n\n      const wrappedHeight = text.height\n\n      expect(wrappedHeight).toBeGreaterThan(unwrappedHeight)\n      expect(wrappedHeight).toBeGreaterThanOrEqual(3)\n    })\n\n    it(\"should shrink height when content changes from multi-line to single line\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\",\n        wrapMode: \"none\",\n      })\n\n      await renderOnce()\n      expect(text.height).toBe(5)\n\n      text.content = \"Single line\"\n      await renderOnce()\n\n      expect(text.height).toBe(1)\n    })\n\n    it(\"should shrink width when replacing long line with shorter (wrapMode: none, position: absolute)\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"This is a very long line with many characters\",\n        wrapMode: \"none\",\n        position: \"absolute\",\n      })\n\n      await renderOnce()\n      const initialWidth = text.width\n      expect(initialWidth).toBe(45) // length of the long line\n\n      text.content = \"Short\"\n      await renderOnce()\n\n      expect(text.width).toBe(5)\n      expect(text.width).toBeLessThan(initialWidth)\n    })\n  })\n\n  describe(\"Width/Height Setter Layout Tests\", () => {\n    it(\"should not shrink box when width is set via setter\", async () => {\n      resize(40, 10)\n\n      const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })\n      currentRenderer.root.add(container)\n\n      const row = new BoxRenderable(currentRenderer, { flexDirection: \"row\", width: \"100%\" })\n      container.add(row)\n\n      const indicator = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\" })\n      row.add(indicator)\n\n      const indicatorText = new TextRenderable(currentRenderer, { content: \">\" })\n      indicator.add(indicatorText)\n\n      const content = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      row.add(content)\n\n      const contentText = new TextRenderable(currentRenderer, { content: \"Content that takes up space\" })\n      content.add(contentText)\n\n      await renderOnce()\n\n      const initialIndicatorWidth = indicator.width\n\n      indicator.width = 5\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(indicator.width).toBe(5)\n      expect(content.width).toBeGreaterThan(0)\n      expect(content.width).toBeLessThan(30) // Should be compressed but not zero\n    })\n\n    it(\"should not shrink box when height is set via setter in column layout with text\", async () => {\n      resize(30, 15)\n\n      const outerBox = new BoxRenderable(currentRenderer, { border: true, width: 25, height: 10 })\n      currentRenderer.root.add(outerBox)\n\n      const column = new BoxRenderable(currentRenderer, { flexDirection: \"column\", height: \"100%\" })\n      outerBox.add(column)\n\n      const header = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\" })\n      column.add(header)\n\n      const headerText = new TextRenderable(currentRenderer, { content: \"Header\" })\n      header.add(headerText)\n\n      const mainContent = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      column.add(mainContent)\n\n      const mainText = new TextRenderable(currentRenderer, {\n        content: \"Line1\\nLine2\\nLine3\\nLine4\\nLine5\\nLine6\\nLine7\\nLine8\",\n      })\n      mainContent.add(mainText)\n\n      const footer = new BoxRenderable(currentRenderer, { height: 2, backgroundColor: \"#00f\" })\n      column.add(footer)\n\n      const footerText = new TextRenderable(currentRenderer, { content: \"Footer\" })\n      footer.add(footerText)\n\n      await renderOnce()\n\n      header.height = 3\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(header.height).toBe(3)\n      expect(mainContent.height).toBeGreaterThan(0)\n      expect(footer.height).toBe(2)\n    })\n\n    it(\"should not shrink box when minWidth is set via setter\", async () => {\n      resize(40, 10)\n\n      const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })\n      currentRenderer.root.add(container)\n\n      const row = new BoxRenderable(currentRenderer, { flexDirection: \"row\", width: \"100%\" })\n      container.add(row)\n\n      const indicator = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\", flexShrink: 1 })\n      row.add(indicator)\n\n      const indicatorText = new TextRenderable(currentRenderer, { content: \">\" })\n      indicator.add(indicatorText)\n\n      const content = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      row.add(content)\n\n      const contentText = new TextRenderable(currentRenderer, { content: \"Content that takes up space\" })\n      content.add(contentText)\n\n      await renderOnce()\n\n      indicator.minWidth = 5\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(indicator.width).toBeGreaterThanOrEqual(5)\n      expect(content.width).toBeGreaterThan(0)\n    })\n\n    it(\"should not shrink box when minHeight is set via setter in column layout with text\", async () => {\n      resize(30, 15)\n\n      const outerBox = new BoxRenderable(currentRenderer, { border: true, width: 25, height: 10 })\n      currentRenderer.root.add(outerBox)\n\n      const column = new BoxRenderable(currentRenderer, { flexDirection: \"column\", height: \"100%\" })\n      outerBox.add(column)\n\n      const header = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\", flexShrink: 1 })\n      column.add(header)\n\n      const headerText = new TextRenderable(currentRenderer, { content: \"Header\" })\n      header.add(headerText)\n\n      const mainContent = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      column.add(mainContent)\n\n      const mainText = new TextRenderable(currentRenderer, {\n        content: \"Line1\\nLine2\\nLine3\\nLine4\\nLine5\\nLine6\\nLine7\\nLine8\",\n      })\n      mainContent.add(mainText)\n\n      const footer = new BoxRenderable(currentRenderer, { height: 2, backgroundColor: \"#00f\" })\n      column.add(footer)\n\n      const footerText = new TextRenderable(currentRenderer, { content: \"Footer\" })\n      footer.add(footerText)\n\n      await renderOnce()\n\n      header.minHeight = 3\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(header.height).toBeGreaterThanOrEqual(3)\n      expect(mainContent.height).toBeGreaterThan(0)\n      expect(footer.height).toBe(2)\n    })\n\n    it(\"should not shrink box when width is set from undefined via setter\", async () => {\n      resize(40, 10)\n\n      const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })\n      currentRenderer.root.add(container)\n\n      const row = new BoxRenderable(currentRenderer, { flexDirection: \"row\", width: \"100%\" })\n      container.add(row)\n\n      const indicator = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\", flexShrink: 1 })\n      row.add(indicator)\n\n      const indicatorText = new TextRenderable(currentRenderer, { content: \">\" })\n      indicator.add(indicatorText)\n\n      const content = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      row.add(content)\n\n      const contentText = new TextRenderable(currentRenderer, { content: \"Content that takes up space\" })\n      content.add(contentText)\n\n      await renderOnce()\n\n      indicator.width = 5\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(indicator.width).toBe(5)\n      expect(content.width).toBeGreaterThan(0)\n    })\n  })\n\n  describe(\"Absolute Positioned Box with Text\", () => {\n    it(\"should render text in absolute positioned box with padding and borders correctly\", async () => {\n      resize(80, 20)\n\n      const notificationBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        justifyContent: \"center\",\n        alignItems: \"flex-start\",\n        top: 2,\n        right: 2,\n        width: Math.min(60, 80 - 6),\n        paddingLeft: 2,\n        paddingRight: 2,\n        paddingTop: 1,\n        paddingBottom: 1,\n        backgroundColor: \"#1e293b\",\n        borderColor: \"#3b82f6\",\n        border: [\"left\", \"right\"],\n      })\n\n      currentRenderer.root.add(notificationBox)\n\n      // Wrap content in nested boxes with row layout and gap\n      const outerWrapperBox = new BoxRenderable(currentRenderer, {\n        flexDirection: \"row\",\n        paddingBottom: 1,\n        paddingTop: 1,\n        paddingLeft: 2,\n        paddingRight: 2,\n        gap: 2,\n      })\n      notificationBox.add(outerWrapperBox)\n\n      const innerContentBox = new BoxRenderable(currentRenderer, {\n        flexGrow: 1,\n        gap: 1,\n      })\n      outerWrapperBox.add(innerContentBox)\n\n      const titleText = new TextRenderable(currentRenderer, {\n        content: \"Important Notification\",\n        attributes: 1, // BOLD\n        marginBottom: 1,\n        fg: \"#f8fafc\",\n      })\n      innerContentBox.add(titleText)\n\n      const messageText = new TextRenderable(currentRenderer, {\n        content:\n          \"This is a longer message that should wrap properly within the absolutely positioned box with appropriate width constraints and padding applied.\",\n        fg: \"#e2e8f0\",\n        wrapMode: \"word\",\n        width: \"100%\",\n      })\n      innerContentBox.add(messageText)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      // Verify the box is positioned correctly\n      expect(notificationBox.x).toBeGreaterThan(0)\n      expect(notificationBox.y).toBe(2)\n      expect(notificationBox.width).toBe(60)\n\n      // Note: With current Yoga behavior, nested flex boxes with width:\"100%\" inside\n      // an absolutely positioned parent with only maxWidth (no explicit width) causes\n      // the children to grow to their intrinsic size rather than being constrained\n      // This is Yoga's shrink-to-fit behavior with the circular dependency\n      // See: https://github.com/facebook/yoga/issues/1409\n      expect(outerWrapperBox.width).toBeGreaterThan(100)\n      expect(innerContentBox.width).toBeGreaterThan(100)\n      expect(messageText.width).toBeGreaterThan(100)\n      expect(messageText.height).toBe(1)\n      expect(messageText.plainText).toBe(\n        \"This is a longer message that should wrap properly within the absolutely positioned box with appropriate width constraints and padding applied.\",\n      )\n    })\n\n    it(\"should render text fully visible in absolute positioned box at various positions\", async () => {\n      resize(100, 25)\n\n      // Top-right positioned box\n      const topRightBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        top: 1,\n        right: 1,\n        maxWidth: 40,\n        paddingLeft: 1,\n        paddingRight: 1,\n        paddingTop: 0,\n        paddingBottom: 0,\n        backgroundColor: \"#fef2f2\",\n        borderColor: \"#ef4444\",\n        border: true,\n      })\n      currentRenderer.root.add(topRightBox)\n\n      const topRightText = new TextRenderable(currentRenderer, {\n        content: \"Error: File not found in the specified directory path\",\n        fg: \"#991b1b\",\n        wrapMode: \"word\",\n        width: \"100%\",\n      })\n      topRightBox.add(topRightText)\n\n      // Bottom-left positioned box\n      const bottomLeftBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        bottom: 1,\n        left: 1,\n        maxWidth: 35,\n        paddingLeft: 1,\n        paddingRight: 1,\n        backgroundColor: \"#f0fdf4\",\n        borderColor: \"#22c55e\",\n        border: [\"top\", \"bottom\"],\n      })\n      currentRenderer.root.add(bottomLeftBox)\n\n      const bottomLeftText = new TextRenderable(currentRenderer, {\n        content: \"Success: Operation completed successfully!\",\n        fg: \"#166534\",\n        wrapMode: \"word\",\n        width: \"100%\",\n      })\n      bottomLeftBox.add(bottomLeftText)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      // Verify top-right box positioning and dimensions\n      expect(topRightBox.y).toBe(1)\n      expect(topRightBox.x).toBeGreaterThan(50)\n      expect(topRightBox.width).toBeGreaterThan(30)\n      expect(topRightBox.width).toBeLessThanOrEqual(40)\n\n      // Verify top-right text renders with proper width\n      expect(topRightText.plainText).toBe(\"Error: File not found in the specified directory path\")\n      expect(topRightText.width).toBeGreaterThan(25)\n      expect(topRightText.width).toBeLessThanOrEqual(38)\n      expect(topRightText.height).toBeGreaterThan(1)\n\n      // Verify bottom-left box positioning and dimensions\n      expect(bottomLeftBox.x).toBe(1)\n      expect(bottomLeftBox.y).toBeGreaterThan(15)\n      expect(bottomLeftBox.width).toBeGreaterThan(25)\n      expect(bottomLeftBox.width).toBeLessThanOrEqual(35)\n\n      // Verify bottom-left text renders with proper width\n      expect(bottomLeftText.plainText).toBe(\"Success: Operation completed successfully!\")\n      expect(bottomLeftText.width).toBeGreaterThan(25)\n      expect(bottomLeftText.width).toBeLessThanOrEqual(33)\n      expect(bottomLeftText.height).toBeGreaterThan(1)\n      expect(bottomLeftText.width).toBeGreaterThan(0)\n      expect(bottomLeftText.width).toBeLessThanOrEqual(33) // maxWidth 35 - padding 2\n    })\n\n    it(\"should handle width:100% text in absolute positioned box with constrained maxWidth\", async () => {\n      resize(70, 15)\n\n      const constrainedBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        top: 5,\n        left: 10,\n        maxWidth: 50,\n        paddingLeft: 3,\n        paddingRight: 3,\n        paddingTop: 2,\n        paddingBottom: 2,\n        backgroundColor: \"#1e1e2e\",\n      })\n      currentRenderer.root.add(constrainedBox)\n\n      const longText = new TextRenderable(currentRenderer, {\n        content:\n          \"This is an extremely long piece of text that needs to wrap multiple times within the constrained width of the absolutely positioned container box with significant padding on all sides.\",\n        fg: \"#cdd6f4\",\n        wrapMode: \"word\",\n        width: \"100%\",\n      })\n      constrainedBox.add(longText)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      // Verify the box respects maxWidth\n      expect(constrainedBox.width).toBeLessThanOrEqual(50)\n      expect(constrainedBox.width).toBeGreaterThan(40)\n      expect(constrainedBox.x).toBe(10)\n      expect(constrainedBox.y).toBe(5)\n\n      // Verify text wraps and fills available width\n      expect(longText.width).toBeGreaterThan(35)\n      expect(longText.width).toBeLessThanOrEqual(44)\n      expect(longText.height).toBeGreaterThanOrEqual(5)\n      expect(longText.plainText).toBe(\n        \"This is an extremely long piece of text that needs to wrap multiple times within the constrained width of the absolutely positioned container box with significant padding on all sides.\",\n      )\n    })\n\n    it(\"should render multiple text elements in absolute positioned box with proper spacing\", async () => {\n      resize(90, 20)\n\n      const infoBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        justifyContent: \"flex-start\",\n        alignItems: \"flex-start\",\n        top: 3,\n        right: 5,\n        maxWidth: 45,\n        paddingLeft: 2,\n        paddingRight: 2,\n        paddingTop: 1,\n        paddingBottom: 1,\n        backgroundColor: \"#eff6ff\",\n        borderColor: \"#3b82f6\",\n        border: true,\n      })\n      currentRenderer.root.add(infoBox)\n\n      const headerText = new TextRenderable(currentRenderer, {\n        content: \"System Update\",\n        attributes: 1, // BOLD\n        fg: \"#1e40af\",\n      })\n      infoBox.add(headerText)\n\n      const bodyText = new TextRenderable(currentRenderer, {\n        content: \"A new version is available with bug fixes and performance improvements.\",\n        fg: \"#1e3a8a\",\n        wrapMode: \"word\",\n        width: \"100%\",\n        marginTop: 1,\n      })\n      infoBox.add(bodyText)\n\n      const footerText = new TextRenderable(currentRenderer, {\n        content: \"Click to install\",\n        fg: \"#60a5fa\",\n        marginTop: 1,\n      })\n      infoBox.add(footerText)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      // Verify all texts are rendered with correct content\n      expect(headerText.plainText).toBe(\"System Update\")\n      expect(bodyText.plainText).toBe(\"A new version is available with bug fixes and performance improvements.\")\n      expect(footerText.plainText).toBe(\"Click to install\")\n\n      // Verify box dimensions are reasonable\n      expect(infoBox.width).toBeGreaterThan(35)\n      expect(infoBox.width).toBeLessThanOrEqual(45)\n\n      // Verify header text renders properly\n      expect(headerText.width).toBeGreaterThan(10)\n      expect(headerText.height).toBe(1)\n\n      // Verify body text fills width and wraps\n      expect(bodyText.width).toBeGreaterThan(30)\n      expect(bodyText.height).toBeGreaterThanOrEqual(2)\n\n      // Verify footer text renders properly\n      expect(footerText.width).toBeGreaterThan(10)\n      expect(footerText.height).toBe(1)\n\n      // Verify vertical spacing\n      expect(bodyText.y).toBeGreaterThan(headerText.y)\n      expect(footerText.y).toBeGreaterThan(bodyText.y)\n    })\n  })\n\n  describe(\"Word Wrapping\", () => {\n    it(\"should default to word wrap mode\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Hello World\",\n      })\n\n      expect(text.wrapMode).toBe(\"word\")\n    })\n\n    it(\"should wrap at word boundaries when using word mode\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"The quick brown fox jumps over the lazy dog\",\n        wrapMode: \"word\",\n        width: 15,\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should wrap at character boundaries when using char mode\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"The quick brown fox jumps over the lazy dog\",\n        wrapMode: \"char\",\n        width: 15,\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle word wrapping with punctuation\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"Hello,World.Test-Example/Path\",\n        wrapMode: \"word\",\n        width: 10,\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle word wrapping with hyphens and dashes\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"self-contained multi-line text-wrapping example\",\n        wrapMode: \"word\",\n        width: 12,\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"regression #651: should keep multi-byte UTF-8 words intact when wrapping in word mode\", async () => {\n      resize(80, 24)\n\n      await createTextRenderable(currentRenderer, {\n        content: \"gyorskiszolgáló éttermek közül. Azóta alapjaiban értelmeztük újra a vendéglátást\",\n        wrapMode: \"word\",\n        width: 40,\n        left: 0,\n        top: 0,\n      })\n\n      const lines = captureFrame()\n        .split(\"\\n\")\n        .map((line) => line.trimEnd())\n        .filter((line) => line.length > 0)\n\n      const expectedLines = [\"gyorskiszolgáló éttermek közül. Azóta\", \"alapjaiban értelmeztük újra a\", \"vendéglátást\"]\n\n      expect(lines).toEqual(expectedLines)\n    })\n\n    it(\"should dynamically change wrap mode\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"The quick brown fox jumps\",\n        wrapMode: \"char\",\n        width: 10,\n        left: 0,\n        top: 0,\n      })\n\n      expect(text.wrapMode).toBe(\"char\")\n\n      // Change to word mode\n      text.wrapMode = \"word\"\n      await renderOnce()\n\n      expect(text.wrapMode).toBe(\"word\")\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle long words that exceed wrap width in word mode\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n        wrapMode: \"word\",\n        width: 10,\n        left: 0,\n        top: 0,\n      })\n\n      // Since there's no word boundary, it should fall back to character wrapping\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should preserve empty lines with word wrapping\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"First line\\n\\nThird line\",\n        wrapMode: \"word\",\n        width: 8,\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle word wrapping with single character words\", async () => {\n      await createTextRenderable(currentRenderer, {\n        content: \"a b c d e f g h i j k l m n o p\",\n        wrapMode: \"word\",\n        width: 8,\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should compare char vs word wrapping with same content\", async () => {\n      const content = \"Hello wonderful world of text wrapping\"\n\n      // Test with char mode\n      const { text: charText } = await createTextRenderable(currentRenderer, {\n        content,\n        wrapMode: \"char\",\n        width: 12,\n        left: 0,\n        top: 0,\n      })\n\n      const charFrame = captureFrame()\n\n      // Remove the char text and add word text\n      currentRenderer.root.remove(charText.id)\n      await renderOnce()\n\n      await createTextRenderable(currentRenderer, {\n        content,\n        wrapMode: \"word\",\n        width: 12,\n        left: 0,\n        top: 0,\n      })\n\n      const wordFrame = captureFrame()\n\n      // The frames should be different as word wrapping preserves word boundaries\n      expect(charFrame).not.toBe(wordFrame)\n      expect(wordFrame).toMatchSnapshot()\n    })\n\n    it(\"should correctly wrap text when updating content via text.content\", async () => {\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: \"Short text\",\n        wrapMode: \"word\",\n        left: 0,\n        top: 0,\n      })\n\n      await renderOnce()\n      const initialFrame = captureFrame()\n      expect(initialFrame).toMatchSnapshot()\n\n      text.content = \"This is a much longer text that should definitely wrap to multiple lines\"\n\n      await renderOnce()\n      const updatedFrame = captureFrame()\n      expect(updatedFrame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Mouse Scrolling\", () => {\n    it(\"should receive mouse scroll events\", async () => {\n      resize(20, 10)\n\n      const longText = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\"\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: longText,\n        wrapMode: \"none\",\n      })\n\n      await renderOnce()\n\n      let scrollEventReceived = false\n      let scrollInfo: any = null\n\n      // Override the handler to capture events\n      const originalHandler = text.onMouseScroll\n      text.onMouseScroll = (event: any) => {\n        scrollEventReceived = true\n        scrollInfo = event.scroll\n        // Call original handler\n        if (originalHandler) {\n          originalHandler.call(text, event)\n        }\n      }\n\n      await currentMouse.scroll(text.x + 1, text.y + 1, \"down\")\n      await renderOnce()\n\n      expect(scrollEventReceived).toBe(true)\n      expect(scrollInfo).toBeDefined()\n      expect(scrollInfo?.direction).toBe(\"down\")\n    })\n\n    it(\"should handle mouse scroll events for vertical scrolling\", async () => {\n      resize(20, 5)\n\n      const longText = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\"\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: longText,\n        wrapMode: \"none\",\n      })\n\n      await renderOnce()\n\n      // Initially should be at scroll position 0\n      expect(text.scrollY).toBe(0)\n      expect(text.scrollX).toBe(0)\n\n      // Scroll down (each scroll event typically moves by 1)\n      await currentMouse.scroll(text.x + 1, text.y + 1, \"down\")\n      await currentMouse.scroll(text.x + 1, text.y + 1, \"down\")\n      await currentMouse.scroll(text.x + 1, text.y + 1, \"down\")\n      await renderOnce()\n\n      expect(text.scrollY).toBe(3)\n\n      // Scroll up\n      await currentMouse.scroll(text.x + 1, text.y + 1, \"up\")\n      await renderOnce()\n\n      expect(text.scrollY).toBe(2)\n    })\n\n    it(\"should handle mouse scroll events for horizontal scrolling with unwrapped text\", async () => {\n      resize(80, 5)\n\n      const wideText =\n        \"This is a very long line that extends way beyond the visible area and should definitely need scrolling\"\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: wideText,\n        wrapMode: \"none\",\n        width: 20,\n        maxWidth: 20,\n      })\n\n      await renderOnce()\n\n      expect(text.scrollX).toBe(0)\n      expect(text.scrollY).toBe(0)\n\n      // Scroll right\n      for (let i = 0; i < 5; i++) {\n        await currentMouse.scroll(text.x + 1, text.y, \"right\")\n      }\n      await renderOnce()\n\n      expect(text.scrollX).toBe(5)\n\n      // Scroll left\n      await currentMouse.scroll(text.x + 1, text.y, \"left\")\n      await currentMouse.scroll(text.x + 1, text.y, \"left\")\n      await renderOnce()\n\n      expect(text.scrollX).toBe(3)\n    })\n\n    it(\"should not allow horizontal scrolling when text is wrapped\", async () => {\n      resize(20, 5)\n\n      const longText =\n        \"Line 1 text\\nLine 2 text\\nLine 3 text\\nLine 4 text\\nLine 5 text\\nLine 6 text\\nLine 7 text\\nLine 8 text\"\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: longText,\n        wrapMode: \"word\",\n        width: 15,\n        height: 3, // Constrain height to enable vertical scrolling\n      })\n\n      await renderOnce()\n\n      // Try to scroll horizontally\n      for (let i = 0; i < 5; i++) {\n        await currentMouse.scroll(text.x + 1, text.y + 1, \"right\")\n      }\n      await renderOnce()\n\n      // Should not scroll horizontally when wrapped\n      expect(text.scrollX).toBe(0)\n\n      // But vertical scrolling should still work if there's content\n      if (text.maxScrollY > 0) {\n        await currentMouse.scroll(text.x + 1, text.y + 1, \"down\")\n        await currentMouse.scroll(text.x + 1, text.y + 1, \"down\")\n        await renderOnce()\n\n        expect(text.scrollY).toBe(2)\n      }\n    })\n\n    it(\"should clamp scroll position to valid bounds\", async () => {\n      resize(20, 5)\n\n      const shortText = \"Line 1\\nLine 2\\nLine 3\"\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: shortText,\n        wrapMode: \"none\",\n      })\n\n      await renderOnce()\n\n      // Try to scroll beyond content\n      for (let i = 0; i < 10; i++) {\n        await currentMouse.scroll(text.x + 1, text.y + 1, \"down\")\n      }\n      await renderOnce()\n\n      // Should be clamped to maxScrollY\n      expect(text.scrollY).toBeLessThanOrEqual(text.maxScrollY)\n      expect(text.scrollY).toBeGreaterThanOrEqual(0)\n\n      // Try to scroll up beyond 0\n      for (let i = 0; i < 20; i++) {\n        await currentMouse.scroll(text.x + 1, text.y + 1, \"up\")\n      }\n      await renderOnce()\n\n      expect(text.scrollY).toBe(0)\n    })\n\n    it(\"should expose scrollWidth and scrollHeight getters\", async () => {\n      resize(20, 5)\n\n      const text = \"Line 1\\nLine 2 with more content\\nLine 3\"\n      const { text: textRenderable } = await createTextRenderable(currentRenderer, {\n        content: text,\n        wrapMode: \"none\",\n      })\n\n      await renderOnce()\n\n      expect(textRenderable.scrollHeight).toBe(3) // 3 lines\n      expect(textRenderable.scrollWidth).toBeGreaterThan(0) // Max width of lines\n    })\n\n    it(\"should calculate maxScrollY and maxScrollX correctly\", async () => {\n      resize(20, 5)\n\n      const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\"\n      const { text: textRenderable } = await createTextRenderable(currentRenderer, {\n        content: text,\n        wrapMode: \"none\",\n        height: 5,\n      })\n\n      await renderOnce()\n\n      // maxScrollY should be scrollHeight - viewport height\n      expect(textRenderable.maxScrollY).toBe(Math.max(0, textRenderable.scrollHeight - textRenderable.height))\n\n      // maxScrollX should be scrollWidth - viewport width\n      expect(textRenderable.maxScrollX).toBe(Math.max(0, textRenderable.scrollWidth - textRenderable.width))\n    })\n\n    it(\"should update scroll position via setters\", async () => {\n      resize(20, 5)\n\n      const longText =\n        \"Line 1 with some extra content\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\"\n      const { text } = await createTextRenderable(currentRenderer, {\n        content: longText,\n        wrapMode: \"none\",\n        width: 20, // Constrain width\n        height: 5, // Constrain height\n      })\n\n      await renderOnce()\n\n      // Set scroll position directly\n      text.scrollY = 3\n      await renderOnce()\n\n      expect(text.scrollY).toBe(3)\n\n      // Set scrollX (only works if there's horizontal scrollable content)\n      if (text.maxScrollX > 0) {\n        text.scrollX = 2\n        await renderOnce()\n        expect(text.scrollX).toBe(2)\n      }\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/Text.ts",
    "content": "import { BaseRenderable } from \"../Renderable.js\"\nimport { stringToStyledText, StyledText } from \"../lib/styled-text.js\"\nimport { type TextChunk } from \"../text-buffer.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { type RenderContext } from \"../types.js\"\nimport { RootTextNodeRenderable, TextNodeRenderable } from \"./TextNode.js\"\nimport { TextBufferRenderable, type TextBufferOptions } from \"./TextBufferRenderable.js\"\n\nexport interface TextOptions extends TextBufferOptions {\n  content?: StyledText | string\n}\n\nexport class TextRenderable extends TextBufferRenderable {\n  private _text: StyledText\n\n  // TODO: The TextRenderable is currently juggling both a StyledText and a RootTextNodeRenderable.\n  // We should refactor this to only use the RootTextNodeRenderable here and have a separate StyledTextRenderable with `content`.\n  private _hasManualStyledText: boolean = false\n\n  protected rootTextNode: RootTextNodeRenderable\n\n  protected _contentDefaultOptions = {\n    content: \"\",\n  } satisfies Partial<TextOptions>\n\n  constructor(ctx: RenderContext, options: TextOptions) {\n    super(ctx, options)\n\n    const content = options.content ?? this._contentDefaultOptions.content\n    const styledText = typeof content === \"string\" ? stringToStyledText(content) : content\n    this._text = styledText\n    this._hasManualStyledText = options.content !== undefined && content !== \"\"\n\n    this.rootTextNode = new RootTextNodeRenderable(\n      ctx,\n      {\n        id: `${this.id}-root`,\n        fg: this._defaultFg,\n        bg: this._defaultBg,\n        attributes: this._defaultAttributes,\n      },\n      this,\n    )\n\n    this.updateTextBuffer(styledText)\n  }\n\n  private updateTextBuffer(styledText: StyledText): void {\n    this.textBuffer.setStyledText(styledText)\n    this.clearChunks(styledText)\n  }\n\n  private clearChunks(styledText: StyledText): void {\n    // Clearing chunks that were already writtend to the text buffer,\n    // to not retain references to the text data in js\n    // TODO: This is causing issues in the solid renderer\n    // styledText.chunks.forEach((chunk) => {\n    //   // @ts-ignore\n    //   chunk.text = undefined\n    // })\n  }\n\n  get content(): StyledText {\n    return this._text\n  }\n\n  get chunks(): TextChunk[] {\n    return this._text.chunks\n  }\n\n  get textNode(): RootTextNodeRenderable {\n    return this.rootTextNode\n  }\n\n  set content(value: StyledText | string) {\n    this._hasManualStyledText = true\n    const styledText = typeof value === \"string\" ? stringToStyledText(value) : value\n    if (this._text !== styledText) {\n      this._text = styledText\n      this.updateTextBuffer(styledText)\n      this.updateTextInfo()\n    }\n  }\n\n  private updateTextFromNodes(): void {\n    if (this.rootTextNode.isDirty && !this._hasManualStyledText) {\n      const chunks = this.rootTextNode.gatherWithInheritedStyle({\n        fg: this._defaultFg,\n        bg: this._defaultBg,\n        attributes: this._defaultAttributes,\n        link: undefined,\n      })\n      this.textBuffer.setStyledText(new StyledText(chunks))\n      this.refreshLocalSelection()\n      this.yogaNode.markDirty()\n    }\n  }\n\n  public add(obj: TextNodeRenderable | StyledText | string, index?: number): number {\n    return this.rootTextNode.add(obj, index)\n  }\n\n  public remove(id: string): void {\n    this.rootTextNode.remove(id)\n  }\n\n  public insertBefore(obj: BaseRenderable | any, anchor?: TextNodeRenderable): number {\n    this.rootTextNode.insertBefore(obj, anchor)\n    return this.rootTextNode.children.indexOf(obj)\n  }\n\n  public getTextChildren(): BaseRenderable[] {\n    return this.rootTextNode.getChildren()\n  }\n\n  public clear(): void {\n    this.rootTextNode.clear()\n\n    const emptyStyledText = stringToStyledText(\"\")\n    this._text = emptyStyledText\n    this.updateTextBuffer(emptyStyledText)\n    this.updateTextInfo()\n\n    this.requestRender()\n  }\n\n  public onLifecyclePass = () => {\n    this.updateTextFromNodes()\n  }\n\n  protected onFgChanged(newColor: RGBA): void {\n    this.rootTextNode.fg = newColor\n  }\n\n  protected onBgChanged(newColor: RGBA): void {\n    this.rootTextNode.bg = newColor\n  }\n\n  protected onAttributesChanged(newAttributes: number): void {\n    this.rootTextNode.attributes = newAttributes\n  }\n\n  destroy(): void {\n    this.rootTextNode.children.length = 0\n    super.destroy()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/TextBufferRenderable.ts",
    "content": "import { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { convertGlobalToLocalSelection, Selection, type LocalSelectionBounds } from \"../lib/selection.js\"\nimport { TextBuffer, type TextChunk } from \"../text-buffer.js\"\nimport { TextBufferView } from \"../text-buffer-view.js\"\nimport { RGBA, parseColor } from \"../lib/RGBA.js\"\nimport { type RenderContext, type LineInfoProvider } from \"../types.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport { MeasureMode } from \"yoga-layout\"\nimport type { LineInfo } from \"../zig.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\n\nexport interface TextBufferOptions extends RenderableOptions<TextBufferRenderable> {\n  fg?: string | RGBA\n  bg?: string | RGBA\n  selectionBg?: string | RGBA\n  selectionFg?: string | RGBA\n  selectable?: boolean\n  attributes?: number\n  wrapMode?: \"none\" | \"char\" | \"word\"\n  tabIndicator?: string | number\n  tabIndicatorColor?: string | RGBA\n  truncate?: boolean\n}\n\nexport abstract class TextBufferRenderable extends Renderable implements LineInfoProvider {\n  public selectable: boolean = true\n\n  protected _defaultFg: RGBA\n  protected _defaultBg: RGBA\n  protected _defaultAttributes: number\n  protected _selectionBg: RGBA | undefined\n  protected _selectionFg: RGBA | undefined\n  protected _wrapMode: \"none\" | \"char\" | \"word\" = \"word\"\n  protected lastLocalSelection: LocalSelectionBounds | null = null\n  protected _tabIndicator?: string | number\n  protected _tabIndicatorColor?: RGBA\n  protected _scrollX: number = 0\n  protected _scrollY: number = 0\n  protected _truncate: boolean = false\n\n  protected textBuffer: TextBuffer\n  protected textBufferView: TextBufferView\n  protected _textBufferSyntaxStyle: SyntaxStyle\n\n  protected _defaultOptions = {\n    fg: RGBA.fromValues(1, 1, 1, 1),\n    bg: RGBA.fromValues(0, 0, 0, 0),\n    selectionBg: undefined,\n    selectionFg: undefined,\n    selectable: true,\n    attributes: 0,\n    wrapMode: \"word\" as \"none\" | \"char\" | \"word\",\n    tabIndicator: undefined,\n    tabIndicatorColor: undefined,\n    truncate: false,\n  } satisfies Partial<TextBufferOptions>\n\n  constructor(ctx: RenderContext, options: TextBufferOptions) {\n    super(ctx, options)\n\n    this._defaultFg = parseColor(options.fg ?? this._defaultOptions.fg)\n    this._defaultBg = parseColor(options.bg ?? this._defaultOptions.bg)\n    this._defaultAttributes = options.attributes ?? this._defaultOptions.attributes\n    this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : this._defaultOptions.selectionBg\n    this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : this._defaultOptions.selectionFg\n    this.selectable = options.selectable ?? this._defaultOptions.selectable\n    this._wrapMode = options.wrapMode ?? this._defaultOptions.wrapMode\n    this._tabIndicator = options.tabIndicator ?? this._defaultOptions.tabIndicator\n    this._tabIndicatorColor = options.tabIndicatorColor\n      ? parseColor(options.tabIndicatorColor)\n      : this._defaultOptions.tabIndicatorColor\n    this._truncate = options.truncate ?? this._defaultOptions.truncate\n\n    this.textBuffer = TextBuffer.create(this._ctx.widthMethod)\n    this.textBufferView = TextBufferView.create(this.textBuffer)\n\n    this._textBufferSyntaxStyle = SyntaxStyle.create()\n    this.textBuffer.setSyntaxStyle(this._textBufferSyntaxStyle)\n\n    this.textBufferView.setWrapMode(this._wrapMode)\n    this.setupMeasureFunc()\n\n    this.textBuffer.setDefaultFg(this._defaultFg)\n    this.textBuffer.setDefaultBg(this._defaultBg)\n    this.textBuffer.setDefaultAttributes(this._defaultAttributes)\n\n    if (this._tabIndicator !== undefined) {\n      this.textBufferView.setTabIndicator(this._tabIndicator)\n    }\n    if (this._tabIndicatorColor !== undefined) {\n      this.textBufferView.setTabIndicatorColor(this._tabIndicatorColor)\n    }\n\n    if (this._wrapMode !== \"none\" && this.width > 0) {\n      this.textBufferView.setWrapWidth(this.width)\n    }\n\n    if (this.width > 0 && this.height > 0) {\n      this.textBufferView.setViewport(this._scrollX, this._scrollY, this.width, this.height)\n    }\n\n    this.textBufferView.setTruncate(this._truncate)\n\n    this.updateTextInfo()\n  }\n\n  protected onMouseEvent(event: any): void {\n    if (event.type === \"scroll\") {\n      this.handleScroll(event)\n    }\n  }\n\n  protected handleScroll(event: any): void {\n    if (!event.scroll) return\n\n    const { direction, delta } = event.scroll\n\n    if (direction === \"up\") {\n      this.scrollY -= delta\n    } else if (direction === \"down\") {\n      this.scrollY += delta\n    }\n\n    if (this._wrapMode === \"none\") {\n      if (direction === \"left\") {\n        this.scrollX -= delta\n      } else if (direction === \"right\") {\n        this.scrollX += delta\n      }\n    }\n  }\n\n  public get lineInfo(): LineInfo {\n    return this.textBufferView.logicalLineInfo\n  }\n\n  public get lineCount(): number {\n    return this.textBuffer.getLineCount()\n  }\n\n  public get virtualLineCount(): number {\n    return this.textBufferView.getVirtualLineCount()\n  }\n\n  public get scrollY(): number {\n    return this._scrollY\n  }\n\n  public set scrollY(value: number) {\n    const maxScrollY = Math.max(0, this.scrollHeight - this.height)\n    const clamped = Math.max(0, Math.min(value, maxScrollY))\n    if (this._scrollY !== clamped) {\n      this._scrollY = clamped\n      this.updateViewportOffset()\n      this.requestRender()\n    }\n  }\n\n  public get scrollX(): number {\n    return this._scrollX\n  }\n\n  public set scrollX(value: number) {\n    const maxScrollX = Math.max(0, this.scrollWidth - this.width)\n    const clamped = Math.max(0, Math.min(value, maxScrollX))\n    if (this._scrollX !== clamped) {\n      this._scrollX = clamped\n      this.updateViewportOffset()\n      this.requestRender()\n    }\n  }\n\n  public get scrollWidth(): number {\n    return this.lineInfo.lineWidthColsMax\n  }\n\n  public get scrollHeight(): number {\n    return this.lineInfo.lineStartCols.length\n  }\n\n  public get maxScrollY(): number {\n    return Math.max(0, this.scrollHeight - this.height)\n  }\n\n  public get maxScrollX(): number {\n    return Math.max(0, this.scrollWidth - this.width)\n  }\n\n  protected updateViewportOffset(): void {\n    // Update the viewport with the new scroll position\n    if (this.width > 0 && this.height > 0) {\n      this.textBufferView.setViewport(this._scrollX, this._scrollY, this.width, this.height)\n    }\n  }\n\n  get plainText(): string {\n    return this.textBuffer.getPlainText()\n  }\n\n  get textLength(): number {\n    return this.textBuffer.length\n  }\n\n  get fg(): RGBA {\n    return this._defaultFg\n  }\n\n  set fg(value: RGBA | string | undefined) {\n    const newColor = parseColor(value ?? this._defaultOptions.fg)\n    if (this._defaultFg !== newColor) {\n      this._defaultFg = newColor\n      this.textBuffer.setDefaultFg(this._defaultFg)\n      this.onFgChanged(newColor)\n      this.requestRender()\n    }\n  }\n\n  get selectionBg(): RGBA | undefined {\n    return this._selectionBg\n  }\n\n  set selectionBg(value: RGBA | string | undefined) {\n    const newColor = value ? parseColor(value) : this._defaultOptions.selectionBg\n    if (this._selectionBg !== newColor) {\n      this._selectionBg = newColor\n      if (this.lastLocalSelection) {\n        this.updateLocalSelection(this.lastLocalSelection)\n      }\n      this.requestRender()\n    }\n  }\n\n  get selectionFg(): RGBA | undefined {\n    return this._selectionFg\n  }\n\n  set selectionFg(value: RGBA | string | undefined) {\n    const newColor = value ? parseColor(value) : this._defaultOptions.selectionFg\n    if (this._selectionFg !== newColor) {\n      this._selectionFg = newColor\n      if (this.lastLocalSelection) {\n        this.updateLocalSelection(this.lastLocalSelection)\n      }\n      this.requestRender()\n    }\n  }\n\n  get bg(): RGBA {\n    return this._defaultBg\n  }\n\n  set bg(value: RGBA | string | undefined) {\n    const newColor = parseColor(value ?? this._defaultOptions.bg)\n    if (this._defaultBg !== newColor) {\n      this._defaultBg = newColor\n      this.textBuffer.setDefaultBg(this._defaultBg)\n      this.onBgChanged(newColor)\n      this.requestRender()\n    }\n  }\n\n  get attributes(): number {\n    return this._defaultAttributes\n  }\n\n  set attributes(value: number) {\n    if (this._defaultAttributes !== value) {\n      this._defaultAttributes = value\n      this.textBuffer.setDefaultAttributes(this._defaultAttributes)\n      this.onAttributesChanged(value)\n      this.requestRender()\n    }\n  }\n\n  get wrapMode(): \"none\" | \"char\" | \"word\" {\n    return this._wrapMode\n  }\n\n  set wrapMode(value: \"none\" | \"char\" | \"word\") {\n    if (this._wrapMode !== value) {\n      this._wrapMode = value\n      this.textBufferView.setWrapMode(this._wrapMode)\n      if (value !== \"none\" && this.width > 0) {\n        this.textBufferView.setWrapWidth(this.width)\n      }\n      // Changing wrap mode can change dimensions, so mark yoga node dirty to trigger re-measurement\n      this.yogaNode.markDirty()\n      this.requestRender()\n    }\n  }\n\n  get tabIndicator(): string | number | undefined {\n    return this._tabIndicator\n  }\n\n  set tabIndicator(value: string | number | undefined) {\n    if (this._tabIndicator !== value) {\n      this._tabIndicator = value\n      if (value !== undefined) {\n        this.textBufferView.setTabIndicator(value)\n      }\n      this.requestRender()\n    }\n  }\n\n  get tabIndicatorColor(): RGBA | undefined {\n    return this._tabIndicatorColor\n  }\n\n  set tabIndicatorColor(value: RGBA | string | undefined) {\n    const newColor = value ? parseColor(value) : undefined\n    if (this._tabIndicatorColor !== newColor) {\n      this._tabIndicatorColor = newColor\n      if (newColor !== undefined) {\n        this.textBufferView.setTabIndicatorColor(newColor)\n      }\n      this.requestRender()\n    }\n  }\n\n  get truncate(): boolean {\n    return this._truncate\n  }\n\n  set truncate(value: boolean) {\n    if (this._truncate !== value) {\n      this._truncate = value\n      this.textBufferView.setTruncate(value)\n      this.requestRender()\n    }\n  }\n\n  protected onResize(width: number, height: number): void {\n    this.textBufferView.setViewport(this._scrollX, this._scrollY, width, height)\n    this.yogaNode.markDirty()\n    this.requestRender()\n    this.emit(\"line-info-change\")\n  }\n\n  protected refreshLocalSelection(): boolean {\n    if (this.lastLocalSelection) {\n      return this.updateLocalSelection(this.lastLocalSelection)\n    }\n    return false\n  }\n\n  private updateLocalSelection(localSelection: LocalSelectionBounds | null): boolean {\n    if (!localSelection?.isActive) {\n      this.textBufferView.resetLocalSelection()\n      return true\n    }\n\n    return this.textBufferView.setLocalSelection(\n      localSelection.anchorX,\n      localSelection.anchorY,\n      localSelection.focusX,\n      localSelection.focusY,\n      this._selectionBg,\n      this._selectionFg,\n    )\n  }\n\n  protected updateTextInfo(): void {\n    if (this.lastLocalSelection) {\n      this.updateLocalSelection(this.lastLocalSelection)\n    }\n\n    this.yogaNode.markDirty()\n    this.requestRender()\n    this.emit(\"line-info-change\")\n  }\n\n  // Undefined = 0,\n  // Exactly = 1,\n  // AtMost = 2\n  private setupMeasureFunc(): void {\n    const measureFunc = (\n      width: number,\n      widthMode: MeasureMode,\n      height: number,\n      heightMode: MeasureMode,\n    ): { width: number; height: number } => {\n      // When widthMode is Undefined, Yoga is asking for the intrinsic/natural width\n      // Pass width=0 to measureForDimensions to signal we want max-content (no wrapping)\n      // The Zig code treats width=0 with wrap_mode != none as null wrap_width,\n      // which triggers no-wrap mode and returns iter_mod.getMaxLineWidth()\n      let effectiveWidth: number\n      if (widthMode === MeasureMode.Undefined || isNaN(width)) {\n        effectiveWidth = 0\n      } else {\n        effectiveWidth = width\n      }\n\n      const effectiveHeight = isNaN(height) ? 1 : height\n\n      const measureResult = this.textBufferView.measureForDimensions(\n        Math.floor(effectiveWidth),\n        Math.floor(effectiveHeight),\n      )\n\n      const measuredWidth = measureResult ? Math.max(1, measureResult.widthColsMax) : 1\n      const measuredHeight = measureResult ? Math.max(1, measureResult.lineCount) : 1\n\n      if (widthMode === MeasureMode.AtMost && this._positionType !== \"absolute\") {\n        return {\n          width: Math.min(effectiveWidth, measuredWidth),\n          height: Math.min(effectiveHeight, measuredHeight),\n        }\n      }\n\n      // NOTE: Yoga may use these measurements or not.\n      // If the yoga node settings and the parent allow this node to grow, it will.\n      return {\n        width: measuredWidth,\n        height: measuredHeight,\n      }\n    }\n\n    this.yogaNode.setMeasureFunc(measureFunc)\n  }\n\n  shouldStartSelection(x: number, y: number): boolean {\n    if (!this.selectable) return false\n\n    const localX = x - this.x\n    const localY = y - this.y\n\n    return localX >= 0 && localX < this.width && localY >= 0 && localY < this.height\n  }\n\n  onSelectionChanged(selection: Selection | null): boolean {\n    const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y)\n    this.lastLocalSelection = localSelection\n\n    let changed: boolean\n    if (!localSelection?.isActive) {\n      this.textBufferView.resetLocalSelection()\n      changed = true\n    } else if (selection?.isStart) {\n      changed = this.textBufferView.setLocalSelection(\n        localSelection.anchorX,\n        localSelection.anchorY,\n        localSelection.focusX,\n        localSelection.focusY,\n        this._selectionBg,\n        this._selectionFg,\n      )\n    } else {\n      changed = this.textBufferView.updateLocalSelection(\n        localSelection.anchorX,\n        localSelection.anchorY,\n        localSelection.focusX,\n        localSelection.focusY,\n        this._selectionBg,\n        this._selectionFg,\n      )\n    }\n\n    if (changed) {\n      this.requestRender()\n    }\n\n    return this.hasSelection()\n  }\n\n  getSelectedText(): string {\n    return this.textBufferView.getSelectedText()\n  }\n\n  hasSelection(): boolean {\n    return this.textBufferView.hasSelection()\n  }\n\n  getSelection(): { start: number; end: number } | null {\n    return this.textBufferView.getSelection()\n  }\n\n  render(buffer: OptimizedBuffer, deltaTime: number): void {\n    if (!this.visible) return\n\n    this.markClean()\n    this._ctx.addToHitGrid(this.x, this.y, this.width, this.height, this.num)\n\n    this.renderSelf(buffer)\n\n    if (this.buffered && this.frameBuffer) {\n      buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer)\n    }\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    if (this.textBuffer.ptr) {\n      buffer.drawTextBuffer(this.textBufferView, this.x, this.y)\n    }\n  }\n\n  destroy(): void {\n    if (this.isDestroyed) return\n\n    this.textBuffer.setSyntaxStyle(null)\n    this._textBufferSyntaxStyle.destroy()\n    this.textBufferView.destroy()\n    this.textBuffer.destroy()\n    super.destroy()\n  }\n\n  protected onFgChanged(newColor: RGBA): void {\n    // Override in subclasses if needed\n  }\n\n  protected onBgChanged(newColor: RGBA): void {\n    // Override in subclasses if needed\n  }\n\n  protected onAttributesChanged(newAttributes: number): void {\n    // Override in subclasses if needed\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/TextNode.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { TextNodeRenderable, isTextNodeRenderable } from \"./TextNode.js\"\nimport { RGBA } from \"../lib/RGBA.js\"\nimport { StyledText, red, bold, t } from \"../lib/styled-text.js\"\n\ndescribe(\"TextNodeRenderable\", () => {\n  describe(\"Constructor and Options\", () => {\n    it(\"should create TextNode with default options\", () => {\n      const node = new TextNodeRenderable({})\n\n      expect(node.fg).toBeUndefined()\n      expect(node.bg).toBeUndefined()\n      expect(node.attributes).toBe(0)\n      expect(node.children).toEqual([])\n    })\n\n    it(\"should create TextNode with custom options\", () => {\n      const fgColor = RGBA.fromInts(255, 0, 0, 255)\n      const bgColor = RGBA.fromInts(0, 0, 255, 255)\n      const attributes = 1\n\n      const node = new TextNodeRenderable({\n        fg: fgColor,\n        bg: bgColor,\n        attributes,\n      })\n\n      expect(node.fg).toEqual(fgColor)\n      expect(node.bg).toEqual(bgColor)\n      expect(node.attributes).toBe(attributes)\n    })\n\n    it(\"should parse color strings in constructor\", () => {\n      const node = new TextNodeRenderable({\n        fg: \"#ff0000\",\n        bg: \"blue\",\n      })\n\n      expect(node.fg?.r).toBe(1)\n      expect(node.fg?.g).toBe(0)\n      expect(node.fg?.b).toBe(0)\n      expect(node.fg?.a).toBe(1)\n      expect(node.bg?.r).toBe(0)\n      expect(node.bg?.g).toBe(0)\n      expect(node.bg?.b).toBe(1)\n      expect(node.bg?.a).toBe(1)\n    })\n\n    it(\"should handle undefined colors\", () => {\n      const node = new TextNodeRenderable({\n        fg: undefined,\n        bg: undefined,\n      })\n\n      expect(node.fg).toBeUndefined()\n      expect(node.bg).toBeUndefined()\n    })\n  })\n\n  describe(\"Type Guard\", () => {\n    it(\"should identify TextNodeRenderable instances\", () => {\n      const node = new TextNodeRenderable({})\n      const plainObject = {}\n\n      expect(isTextNodeRenderable(node)).toBe(true)\n      expect(isTextNodeRenderable(plainObject)).toBe(false)\n      expect(isTextNodeRenderable(null)).toBe(false)\n      expect(isTextNodeRenderable(undefined)).toBe(false)\n    })\n  })\n\n  describe(\"add Method\", () => {\n    it(\"should add string child using add\", () => {\n      const node = new TextNodeRenderable({})\n\n      const index = node.add(\"Hello\")\n\n      expect(index).toBe(0)\n      expect(node.children).toEqual([\"Hello\"])\n    })\n\n    it(\"should add TextNode child using add\", () => {\n      const parent = new TextNodeRenderable({})\n      const child = new TextNodeRenderable({})\n\n      const index = parent.add(child)\n\n      expect(index).toBe(0)\n      expect(parent.children).toEqual([child])\n    })\n\n    it(\"should add multiple children sequentially\", () => {\n      const node = new TextNodeRenderable({})\n\n      node.add(\"First\")\n      node.add(\"Second\")\n      const childNode = new TextNodeRenderable({})\n      node.add(childNode)\n\n      expect(node.children).toEqual([\"First\", \"Second\", childNode])\n    })\n\n    it(\"should add child at specific index using add method\", () => {\n      const node = new TextNodeRenderable({})\n      const child1 = new TextNodeRenderable({})\n      const child2 = new TextNodeRenderable({})\n\n      node.add(\"First\")\n      node.add(child1, 0) // Insert at beginning\n      node.add(child2, 2) // Insert at end\n\n      expect(node.children).toEqual([child1, \"First\", child2])\n    })\n\n    it(\"should add string at specific index using add method\", () => {\n      const node = new TextNodeRenderable({})\n      const child1 = new TextNodeRenderable({})\n\n      node.add(\"First\")\n      node.add(child1, 0) // Insert at beginning\n      node.add(\"Middle\", 1) // Insert in middle\n      node.add(\"Last\") // Append at end\n\n      expect(node.children).toEqual([child1, \"Middle\", \"First\", \"Last\"])\n    })\n\n    it(\"should reject non-TextNode children in add method\", () => {\n      const node = new TextNodeRenderable({})\n      const invalidChild = { id: \"invalid\" }\n\n      expect(() => {\n        node.add(invalidChild as any, 0)\n      }).toThrow(\"TextNodeRenderable only accepts strings, TextNodeRenderable instances, or StyledText instances\")\n    })\n\n    it(\"should add StyledText child using add method\", () => {\n      const node = new TextNodeRenderable({})\n      const styledText = new StyledText([\n        { __isChunk: true, text: \"Hello\", fg: RGBA.fromInts(255, 0, 0, 255), attributes: 1 },\n        { __isChunk: true, text: \" World\", fg: RGBA.fromInts(0, 255, 0, 255), attributes: 0 },\n      ])\n\n      const index = node.add(styledText)\n\n      expect(index).toBe(0)\n      expect(node.children).toHaveLength(2)\n      expect(node.children[0]).toBeInstanceOf(TextNodeRenderable)\n      expect(node.children[1]).toBeInstanceOf(TextNodeRenderable)\n\n      const firstChild = node.children[0] as TextNodeRenderable\n      const secondChild = node.children[1] as TextNodeRenderable\n\n      expect(firstChild.children).toEqual([\"Hello\"])\n      expect(firstChild.fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(firstChild.attributes).toBe(1)\n\n      expect(secondChild.children).toEqual([\" World\"])\n      expect(secondChild.fg).toEqual(RGBA.fromInts(0, 255, 0, 255))\n      expect(secondChild.attributes).toBe(0)\n    })\n\n    it(\"should add StyledText child at specific index using add method\", () => {\n      const node = new TextNodeRenderable({})\n      node.add(\"First\")\n      node.add(\"Third\")\n\n      const styledText = new StyledText([\n        { __isChunk: true, text: \"Second\", fg: RGBA.fromInts(255, 255, 0, 255), attributes: 2 },\n      ])\n\n      node.add(styledText, 1)\n\n      expect(node.children).toHaveLength(3)\n      expect(node.children[0]).toBe(\"First\")\n      expect(node.children[1]).toBeInstanceOf(TextNodeRenderable)\n      expect(node.children[2]).toBe(\"Third\")\n\n      const styledChild = node.children[1] as TextNodeRenderable\n      expect(styledChild.children).toEqual([\"Second\"])\n      expect(styledChild.fg).toEqual(RGBA.fromInts(255, 255, 0, 255))\n      expect(styledChild.attributes).toBe(2)\n    })\n  })\n\n  describe(\"insertBefore and remove Methods\", () => {\n    it(\"should insert child before anchor node\", () => {\n      const node = new TextNodeRenderable({})\n      const anchor = new TextNodeRenderable({})\n\n      node.add(\"First\")\n      node.add(anchor)\n      node.add(\"Last\")\n\n      node.insertBefore(\"Middle\", anchor)\n\n      expect(node.children).toEqual([\"First\", \"Middle\", anchor, \"Last\"])\n    })\n\n    it(\"should throw error when anchor node not found in insertBefore\", () => {\n      const node = new TextNodeRenderable({})\n      const anchor = new TextNodeRenderable({})\n\n      expect(() => {\n        node.insertBefore(\"Test\", anchor)\n      }).toThrow(\"Anchor node not found in children\")\n    })\n\n    it(\"should insert StyledText before anchor node\", () => {\n      const node = new TextNodeRenderable({})\n      const anchor = new TextNodeRenderable({})\n      anchor.add(\"Anchor\")\n\n      node.add(\"First\")\n      node.add(anchor)\n      node.add(\"Last\")\n\n      const styledText = new StyledText([\n        { __isChunk: true, text: \"Middle\", fg: RGBA.fromInts(128, 128, 128, 255), attributes: 4 },\n      ])\n\n      node.insertBefore(styledText, anchor)\n\n      expect(node.children).toHaveLength(4)\n      expect(node.children[0]).toBe(\"First\")\n      expect(node.children[1]).toBeInstanceOf(TextNodeRenderable)\n      expect(node.children[2]).toBe(anchor)\n      expect(node.children[3]).toBe(\"Last\")\n\n      const styledChild = node.children[1] as TextNodeRenderable\n      expect(styledChild.children).toEqual([\"Middle\"])\n      expect(styledChild.fg).toEqual(RGBA.fromInts(128, 128, 128, 255))\n      expect(styledChild.attributes).toBe(4)\n    })\n\n    it(\"should remove child from node\", () => {\n      const node = new TextNodeRenderable({})\n      const child = new TextNodeRenderable({})\n\n      node.add(\"First\")\n      node.add(child)\n      node.add(\"Last\")\n\n      node.remove(child.id)\n\n      expect(node.children).toEqual([\"First\", \"Last\"])\n      expect(child.parent).toBeNull()\n    })\n\n    it(\"should throw error when child not found in remove\", () => {\n      const node = new TextNodeRenderable({})\n\n      expect(() => {\n        node.remove(\"nonexistent-id\")\n      }).toThrow(\"Child not found in children\")\n    })\n  })\n\n  describe(\"clear Method\", () => {\n    it(\"should clear all children and change log\", () => {\n      const node = new TextNodeRenderable({})\n\n      node.add(\"Test\")\n      node.add(new TextNodeRenderable({}))\n\n      expect(node.children).toHaveLength(2)\n\n      node.clear()\n\n      expect(node.children).toEqual([])\n    })\n  })\n\n  describe(\"Style Inheritance and Merging\", () => {\n    it(\"should merge styles with parent styles\", () => {\n      const node = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 0, 0, 255),\n        attributes: 1,\n      })\n\n      const parentStyle = {\n        bg: RGBA.fromInts(0, 0, 255, 255),\n        attributes: 2,\n      }\n\n      const merged = node.mergeStyles(parentStyle)\n\n      expect(merged.fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(merged.bg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(merged.attributes).toBe(3)\n    })\n\n    it(\"should inherit undefined styles from parent\", () => {\n      const node = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 0, 0, 255),\n        // bg and attributes undefined (attributes defaults to 0)\n      })\n\n      const parentStyle = {\n        bg: RGBA.fromInts(0, 0, 255, 255),\n        attributes: 2,\n      }\n\n      const merged = node.mergeStyles(parentStyle)\n\n      expect(merged.fg?.r).toBe(1)\n      expect(merged.fg?.g).toBe(0)\n      expect(merged.fg?.b).toBe(0)\n      expect(merged.bg?.r).toBe(0)\n      expect(merged.bg?.g).toBe(0)\n      expect(merged.bg?.b).toBe(1)\n      expect(merged.attributes).toBe(2)\n    })\n\n    it(\"should inherit nothing when parent has no styling\", () => {\n      const node = new TextNodeRenderable({}) // No styles defined\n\n      const parentStyle = {\n        fg: undefined,\n        bg: undefined,\n        attributes: 0,\n      }\n\n      const merged = node.mergeStyles(parentStyle)\n\n      expect(merged.fg).toBeUndefined()\n      expect(merged.bg).toBeUndefined()\n      expect(merged.attributes).toBe(0)\n    })\n\n    it(\"should combine attributes using bitwise OR\", () => {\n      // Test various attribute combinations\n      const testCases = [\n        { nodeAttrs: 0, parentAttrs: 0, expected: 0 }, // 0 | 0 = 0\n        { nodeAttrs: 1, parentAttrs: 0, expected: 1 }, // 1 | 0 = 1 (bold)\n        { nodeAttrs: 0, parentAttrs: 2, expected: 2 }, // 0 | 2 = 2 (italic)\n        { nodeAttrs: 1, parentAttrs: 2, expected: 3 }, // 1 | 2 = 3 (bold + italic)\n        { nodeAttrs: 3, parentAttrs: 4, expected: 7 }, // 3 | 4 = 7 (bold + italic + underline)\n        { nodeAttrs: 7, parentAttrs: 8, expected: 15 }, // 7 | 8 = 15 (all previous + strikethrough)\n      ]\n\n      testCases.forEach(({ nodeAttrs, parentAttrs, expected }) => {\n        const node = new TextNodeRenderable({ attributes: nodeAttrs })\n        const parentStyle = { fg: undefined, bg: undefined, attributes: parentAttrs }\n\n        const merged = node.mergeStyles(parentStyle)\n        expect(merged.attributes).toBe(expected)\n      })\n    })\n  })\n\n  describe(\"gatherWithInheritedStyle Method\", () => {\n    it(\"should gather chunks with inherited styles\", () => {\n      const node = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 0, 0, 255),\n        bg: RGBA.fromInts(0, 0, 255, 255),\n        attributes: 1,\n      })\n\n      node.add(\"Hello\")\n      node.add(\" \")\n      node.add(\"World\")\n\n      const chunks = node.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(3)\n      chunks.forEach((chunk) => {\n        expect(chunk.fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n        expect(chunk.bg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n        expect(chunk.attributes).toBe(1)\n      })\n\n      expect(chunks[0].text).toBe(\"Hello\")\n      expect(chunks[1].text).toBe(\" \")\n      expect(chunks[2].text).toBe(\"World\")\n    })\n\n    it(\"should recursively gather from child TextNodes\", () => {\n      const parent = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 0, 0, 255),\n      })\n\n      const child = new TextNodeRenderable({\n        bg: RGBA.fromInts(0, 255, 0, 255),\n      })\n\n      child.add(\"Child\")\n      parent.add(\"Parent\")\n      parent.add(child)\n\n      const chunks = parent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(2)\n      expect(chunks[0].text).toBe(\"Parent\")\n      expect(chunks[0].fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(chunks[0].bg).toBeUndefined()\n\n      expect(chunks[1].text).toBe(\"Child\")\n      expect(chunks[1].fg).toEqual(RGBA.fromInts(255, 0, 0, 255)) // Inherited from parent\n      expect(chunks[1].bg).toEqual(RGBA.fromInts(0, 255, 0, 255)) // Own style\n    })\n\n    it(\"should inherit nothing when parent has no default styling\", () => {\n      const parent = new TextNodeRenderable({}) // No styles\n\n      const child = new TextNodeRenderable({}) // No styles\n      child.add(\"Child\")\n\n      parent.add(\"Parent\")\n      parent.add(child)\n\n      const chunks = parent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(2)\n      expect(chunks[0].text).toBe(\"Parent\")\n      expect(chunks[0].fg).toBeUndefined()\n      expect(chunks[0].bg).toBeUndefined()\n      expect(chunks[0].attributes).toBe(0)\n\n      expect(chunks[1].text).toBe(\"Child\")\n      expect(chunks[1].fg).toBeUndefined() // Nothing inherited\n      expect(chunks[1].bg).toBeUndefined() // Nothing inherited\n      expect(chunks[1].attributes).toBe(0) // Nothing inherited\n    })\n\n    it(\"should allow children to override parent styles independently\", () => {\n      const parent = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 0, 0, 255),\n        bg: RGBA.fromInts(0, 0, 255, 255),\n        attributes: 1,\n      })\n\n      const childOverrideFg = new TextNodeRenderable({\n        fg: RGBA.fromInts(0, 255, 0, 255),\n      })\n      childOverrideFg.add(\"Green Text\")\n\n      const childOverrideBg = new TextNodeRenderable({\n        bg: RGBA.fromInts(255, 255, 0, 255),\n      })\n      childOverrideBg.add(\"Yellow BG\")\n\n      const childOverrideAttrs = new TextNodeRenderable({\n        attributes: 2,\n      })\n      childOverrideAttrs.add(\"Italic\")\n\n      parent.add(childOverrideFg)\n      parent.add(childOverrideBg)\n      parent.add(childOverrideAttrs)\n\n      const chunks = parent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(3)\n\n      // First child: overrides fg, inherits bg and attributes\n      expect(chunks[0].text).toBe(\"Green Text\")\n      expect(chunks[0].fg).toEqual(RGBA.fromInts(0, 255, 0, 255)) // Overridden\n      expect(chunks[0].bg).toEqual(RGBA.fromInts(0, 0, 255, 255)) // Inherited\n      expect(chunks[0].attributes).toBe(1) // Inherited\n\n      // Second child: overrides bg, inherits fg and attributes\n      expect(chunks[1].text).toBe(\"Yellow BG\")\n      expect(chunks[1].fg).toEqual(RGBA.fromInts(255, 0, 0, 255)) // Inherited\n      expect(chunks[1].bg).toEqual(RGBA.fromInts(255, 255, 0, 255)) // Overridden\n      expect(chunks[1].attributes).toBe(1) // Inherited\n\n      // Third child: overrides attributes (OR'd), inherits fg and bg\n      expect(chunks[2].text).toBe(\"Italic\")\n      expect(chunks[2].fg).toEqual(RGBA.fromInts(255, 0, 0, 255)) // Inherited\n      expect(chunks[2].bg).toEqual(RGBA.fromInts(0, 0, 255, 255)) // Inherited\n      expect(chunks[2].attributes).toBe(3) // 1 | 2 = 3\n    })\n\n    it(\"should support multi-level inheritance (grandparent -> parent -> child)\", () => {\n      const grandparent = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 0, 0, 255),\n        attributes: 1,\n      })\n\n      const parent = new TextNodeRenderable({\n        bg: RGBA.fromInts(0, 0, 255, 255),\n      })\n\n      const child = new TextNodeRenderable({\n        fg: RGBA.fromInts(0, 255, 0, 255),\n        attributes: 2,\n      })\n\n      child.add(\"Grandchild\")\n      parent.add(\"Parent\")\n      parent.add(child)\n      grandparent.add(\"Grandparent\")\n      grandparent.add(parent)\n\n      const chunks = grandparent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(3)\n\n      expect(chunks[0].text).toBe(\"Grandparent\")\n      expect(chunks[0].fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(chunks[0].bg).toBeUndefined()\n      expect(chunks[0].attributes).toBe(1)\n\n      expect(chunks[1].text).toBe(\"Parent\")\n      expect(chunks[1].fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(chunks[1].bg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(chunks[1].attributes).toBe(1)\n\n      expect(chunks[2].text).toBe(\"Grandchild\")\n      expect(chunks[2].fg).toEqual(RGBA.fromInts(0, 255, 0, 255))\n      expect(chunks[2].bg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(chunks[2].attributes).toBe(3)\n    })\n\n    it(\"should support partial style overrides in children\", () => {\n      const parent = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 0, 0, 255),\n        bg: RGBA.fromInts(0, 0, 255, 255),\n        attributes: 1,\n      })\n\n      const childPartial1 = new TextNodeRenderable({\n        fg: RGBA.fromInts(0, 255, 0, 255),\n      })\n      childPartial1.add(\"Green on Blue\")\n\n      const childPartial2 = new TextNodeRenderable({\n        bg: RGBA.fromInts(255, 255, 0, 255),\n      })\n      childPartial2.add(\"Red on Yellow\")\n\n      const childPartial3 = new TextNodeRenderable({\n        attributes: 2,\n      })\n      childPartial3.add(\"Red on Blue Bold+Italic\")\n\n      const childPartial4 = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 255, 255, 255),\n        attributes: 4,\n      })\n      childPartial4.add(\"White on Blue Bold+Underline\")\n\n      parent.add(childPartial1)\n      parent.add(childPartial2)\n      parent.add(childPartial3)\n      parent.add(childPartial4)\n\n      const chunks = parent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(4)\n\n      // Child 1: only fg overridden\n      expect(chunks[0].text).toBe(\"Green on Blue\")\n      expect(chunks[0].fg).toEqual(RGBA.fromInts(0, 255, 0, 255))\n      expect(chunks[0].bg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(chunks[0].attributes).toBe(1)\n\n      // Child 2: only bg overridden\n      expect(chunks[1].text).toBe(\"Red on Yellow\")\n      expect(chunks[1].fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(chunks[1].bg).toEqual(RGBA.fromInts(255, 255, 0, 255))\n      expect(chunks[1].attributes).toBe(1)\n\n      // Child 3: only attributes overridden (OR'd)\n      expect(chunks[2].text).toBe(\"Red on Blue Bold+Italic\")\n      expect(chunks[2].fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(chunks[2].bg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(chunks[2].attributes).toBe(3) // 1 | 2 = 3\n\n      // Child 4: fg and attributes overridden, bg inherited\n      expect(chunks[3].text).toBe(\"White on Blue Bold+Underline\")\n      expect(chunks[3].fg).toEqual(RGBA.fromInts(255, 255, 255, 255))\n      expect(chunks[3].bg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(chunks[3].attributes).toBe(5) // 1 | 4 = 5\n    })\n  })\n\n  describe(\"Static Factory Methods\", () => {\n    it(\"should create TextNode from string using fromString\", () => {\n      const node = TextNodeRenderable.fromString(\"Hello World\", {\n        fg: \"#ff0000\",\n        attributes: 1,\n      })\n\n      expect(node.children).toEqual([\"Hello World\"])\n      expect(node.fg?.r).toBe(1)\n      expect(node.fg?.g).toBe(0)\n      expect(node.fg?.b).toBe(0)\n      expect(node.attributes).toBe(1)\n    })\n\n    it(\"should create TextNode from nodes using fromNodes\", () => {\n      const child1 = new TextNodeRenderable({})\n      const child2 = new TextNodeRenderable({})\n\n      child1.add(\"First\")\n      child2.add(\"Second\")\n\n      const parent = TextNodeRenderable.fromNodes([child1, child2], {\n        fg: RGBA.fromInts(255, 255, 0, 255),\n      })\n\n      expect(parent.children).toEqual([child1, child2])\n      expect(parent.fg).toEqual(RGBA.fromInts(255, 255, 0, 255))\n    })\n  })\n\n  describe(\"Utility Methods\", () => {\n    it(\"should convert to chunks using toChunks\", () => {\n      const node = new TextNodeRenderable({\n        fg: \"#00ff00\",\n      })\n\n      node.add(\"Test\")\n\n      const chunks = node.toChunks()\n\n      expect(chunks).toHaveLength(1)\n      expect(chunks[0].text).toBe(\"Test\")\n      expect(chunks[0].fg?.r).toBe(0)\n      expect(chunks[0].fg?.g).toBe(1)\n      expect(chunks[0].fg?.b).toBe(0)\n    })\n\n    it(\"should get children using getChildren\", () => {\n      const node = new TextNodeRenderable({})\n\n      node.add(\"String child\")\n      const textNodeChild = new TextNodeRenderable({})\n      node.add(textNodeChild)\n\n      const children = node.getChildren()\n\n      expect(children).toHaveLength(1)\n      expect(children[0]).toBe(textNodeChild)\n    })\n\n    it(\"should get children count\", () => {\n      const node = new TextNodeRenderable({})\n\n      expect(node.getChildrenCount()).toBe(0)\n\n      node.add(\"First\")\n      expect(node.getChildrenCount()).toBe(1)\n\n      node.add(new TextNodeRenderable({}))\n      expect(node.getChildrenCount()).toBe(2)\n    })\n\n    it(\"should find renderable by id\", () => {\n      const node = new TextNodeRenderable({})\n\n      const child1 = new TextNodeRenderable({ id: \"child1\" })\n      const child2 = new TextNodeRenderable({ id: \"child2\" })\n\n      node.add(child1)\n      node.add(child2)\n\n      expect(node.getRenderable(\"child1\")).toBe(child1)\n      expect(node.getRenderable(\"child2\")).toBe(child2)\n      expect(node.getRenderable(\"nonexistent\")).toBeUndefined()\n    })\n  })\n\n  describe(\"StyledText Integration\", () => {\n    it(\"should work with template literal styled text\", () => {\n      const node = new TextNodeRenderable({})\n      const styled = t`Hello ${red(\"World\")} with ${bold(\"bold\")} text!`\n\n      node.add(styled)\n\n      expect(node.children).toHaveLength(5) // All parts become TextNodeRenderable instances\n      expect(node.children[0]).toBeInstanceOf(TextNodeRenderable)\n      expect(node.children[1]).toBeInstanceOf(TextNodeRenderable)\n      expect(node.children[2]).toBeInstanceOf(TextNodeRenderable)\n      expect(node.children[3]).toBeInstanceOf(TextNodeRenderable)\n      expect(node.children[4]).toBeInstanceOf(TextNodeRenderable)\n\n      // Check first chunk: \"Hello \" (no styling)\n      const helloChild = node.children[0] as TextNodeRenderable\n      expect(helloChild.children).toEqual([\"Hello \"])\n      expect(helloChild.fg).toBeUndefined()\n      expect(helloChild.attributes).toBe(0)\n\n      // Check second chunk: \"World\" (red styling)\n      const redChild = node.children[1] as TextNodeRenderable\n      expect(redChild.children).toEqual([\"World\"])\n      expect(redChild.fg?.r).toBe(1)\n      expect(redChild.fg?.g).toBe(0)\n      expect(redChild.fg?.b).toBe(0)\n      expect(redChild.attributes).toBe(0)\n\n      // Check third chunk: \" with \" (no styling)\n      const withChild = node.children[2] as TextNodeRenderable\n      expect(withChild.children).toEqual([\" with \"])\n      expect(withChild.fg).toBeUndefined()\n      expect(withChild.attributes).toBe(0)\n\n      // Check fourth chunk: \"bold\" (bold styling)\n      const boldChild = node.children[3] as TextNodeRenderable\n      expect(boldChild.children).toEqual([\"bold\"])\n      expect(boldChild.fg).toBeUndefined()\n      expect(boldChild.attributes).toBe(1) // bold attribute\n\n      // Check fifth chunk: \" text!\" (no styling)\n      const textChild = node.children[4] as TextNodeRenderable\n      expect(textChild.children).toEqual([\" text!\"])\n      expect(textChild.fg).toBeUndefined()\n      expect(textChild.attributes).toBe(0)\n    })\n\n    it(\"should preserve styles when converting StyledText to TextNodes\", () => {\n      const node = new TextNodeRenderable({})\n      const styledText = new StyledText([\n        {\n          __isChunk: true,\n          text: \"Red\",\n          fg: RGBA.fromInts(255, 0, 0, 255),\n          bg: RGBA.fromInts(0, 0, 0, 255),\n          attributes: 1,\n        },\n        { __isChunk: true, text: \"Blue\", fg: RGBA.fromInts(0, 0, 255, 255), attributes: 2 },\n        { __isChunk: true, text: \"Green\", fg: RGBA.fromInts(0, 255, 0, 255), attributes: 0 },\n      ])\n\n      node.add(styledText)\n\n      expect(node.children).toHaveLength(3)\n\n      const redNode = node.children[0] as TextNodeRenderable\n      expect(redNode.children).toEqual([\"Red\"])\n      expect(redNode.fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(redNode.bg).toEqual(RGBA.fromInts(0, 0, 0, 255))\n      expect(redNode.attributes).toBe(1)\n\n      const blueNode = node.children[1] as TextNodeRenderable\n      expect(blueNode.children).toEqual([\"Blue\"])\n      expect(blueNode.fg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(blueNode.bg).toBeUndefined()\n      expect(blueNode.attributes).toBe(2)\n\n      const greenNode = node.children[2] as TextNodeRenderable\n      expect(greenNode.children).toEqual([\"Green\"])\n      expect(greenNode.fg).toEqual(RGBA.fromInts(0, 255, 0, 255))\n      expect(greenNode.bg).toBeUndefined()\n      expect(greenNode.attributes).toBe(0)\n    })\n\n    it(\"should handle empty StyledText\", () => {\n      const node = new TextNodeRenderable({})\n      const emptyStyledText = new StyledText([])\n\n      // Add empty StyledText\n      const index = node.add(emptyStyledText)\n      expect(index).toBe(0)\n\n      // Should have no children since empty StyledText produces no TextNodes\n      expect(node.children).toHaveLength(0)\n\n      // Verify that gatherWithInheritedStyle returns empty array\n      const chunks = node.gatherWithInheritedStyle()\n      expect(chunks).toHaveLength(0)\n    })\n\n    it(\"should handle StyledText with empty text chunks\", () => {\n      const node = new TextNodeRenderable({})\n      const styledTextWithEmptyChunks = new StyledText([\n        { __isChunk: true, text: \"\", fg: RGBA.fromInts(255, 0, 0, 255), attributes: 1 },\n        { __isChunk: true, text: \"middle\", fg: RGBA.fromInts(0, 255, 0, 255), attributes: 0 },\n        { __isChunk: true, text: \"\", fg: RGBA.fromInts(0, 0, 255, 255), attributes: 2 },\n      ])\n\n      node.add(styledTextWithEmptyChunks)\n\n      expect(node.children).toHaveLength(3)\n\n      // First chunk: empty text with red styling\n      const emptyRedNode = node.children[0] as TextNodeRenderable\n      expect(emptyRedNode.children).toEqual([\"\"])\n      expect(emptyRedNode.fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(emptyRedNode.attributes).toBe(1)\n\n      // Second chunk: \"middle\" with green styling\n      const middleNode = node.children[1] as TextNodeRenderable\n      expect(middleNode.children).toEqual([\"middle\"])\n      expect(middleNode.fg).toEqual(RGBA.fromInts(0, 255, 0, 255))\n      expect(middleNode.attributes).toBe(0)\n\n      // Third chunk: empty text with blue styling\n      const emptyBlueNode = node.children[2] as TextNodeRenderable\n      expect(emptyBlueNode.children).toEqual([\"\"])\n      expect(emptyBlueNode.fg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(emptyBlueNode.attributes).toBe(2)\n    })\n  })\n\n  describe(\"Link Inheritance\", () => {\n    it(\"should inherit link from parent to child\", () => {\n      const parent = new TextNodeRenderable({\n        link: { url: \"https://opentui.com\" },\n      })\n\n      const child = new TextNodeRenderable({})\n      child.add(\"Child text\")\n\n      parent.add(\"Parent text\")\n      parent.add(child)\n\n      const chunks = parent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(2)\n      expect(chunks[0].text).toBe(\"Parent text\")\n      expect(chunks[0].link?.url).toBe(\"https://opentui.com\")\n\n      expect(chunks[1].text).toBe(\"Child text\")\n      expect(chunks[1].link?.url).toBe(\"https://opentui.com\")\n    })\n\n    it(\"should allow child to override parent link\", () => {\n      const parent = new TextNodeRenderable({\n        link: { url: \"https://parent.com\" },\n      })\n\n      const child = new TextNodeRenderable({\n        link: { url: \"https://child.com\" },\n      })\n      child.add(\"Child text\")\n\n      parent.add(\"Parent text\")\n      parent.add(child)\n\n      const chunks = parent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(2)\n      expect(chunks[0].link?.url).toBe(\"https://parent.com\")\n      expect(chunks[1].link?.url).toBe(\"https://child.com\")\n    })\n\n    it(\"should inherit link through multiple nesting levels\", () => {\n      const grandparent = new TextNodeRenderable({\n        link: { url: \"https://example.com\" },\n      })\n\n      const parent = new TextNodeRenderable({})\n      const child = new TextNodeRenderable({})\n\n      child.add(\"Grandchild\")\n      parent.add(child)\n      grandparent.add(parent)\n\n      const chunks = grandparent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(1)\n      expect(chunks[0].text).toBe(\"Grandchild\")\n      expect(chunks[0].link?.url).toBe(\"https://example.com\")\n    })\n\n    it(\"should merge link with other styles\", () => {\n      const parent = new TextNodeRenderable({\n        fg: RGBA.fromInts(255, 0, 0, 255),\n        attributes: 1,\n        link: { url: \"https://opentui.com\" },\n      })\n\n      const child = new TextNodeRenderable({\n        bg: RGBA.fromInts(0, 0, 255, 255),\n        attributes: 2,\n      })\n      child.add(\"Styled linked text\")\n\n      parent.add(child)\n\n      const chunks = parent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(1)\n      expect(chunks[0].text).toBe(\"Styled linked text\")\n      expect(chunks[0].fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(chunks[0].bg).toEqual(RGBA.fromInts(0, 0, 255, 255))\n      expect(chunks[0].attributes).toBe(3) // 1 | 2\n      expect(chunks[0].link?.url).toBe(\"https://opentui.com\")\n    })\n\n    it(\"should handle undefined link in parent\", () => {\n      const parent = new TextNodeRenderable({})\n\n      const child = new TextNodeRenderable({\n        link: { url: \"https://child.com\" },\n      })\n      child.add(\"Child with link\")\n\n      parent.add(\"Parent without link\")\n      parent.add(child)\n\n      const chunks = parent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(2)\n      expect(chunks[0].link).toBeUndefined()\n      expect(chunks[1].link?.url).toBe(\"https://child.com\")\n    })\n\n    it(\"should preserve link when merging styles\", () => {\n      const node = new TextNodeRenderable({\n        link: { url: \"https://example.com\" },\n        attributes: 1,\n      })\n\n      const parentStyle = {\n        fg: RGBA.fromInts(255, 0, 0, 255),\n        bg: undefined,\n        attributes: 2,\n      }\n\n      const merged = node.mergeStyles(parentStyle)\n\n      expect(merged.link?.url).toBe(\"https://example.com\")\n      expect(merged.fg).toEqual(RGBA.fromInts(255, 0, 0, 255))\n      expect(merged.attributes).toBe(3)\n    })\n\n    it(\"should inherit link when node has no link\", () => {\n      const node = new TextNodeRenderable({\n        fg: RGBA.fromInts(0, 255, 0, 255),\n      })\n\n      const parentStyle = {\n        fg: undefined,\n        bg: undefined,\n        attributes: 0,\n        link: { url: \"https://inherited.com\" },\n      }\n\n      const merged = node.mergeStyles(parentStyle)\n\n      expect(merged.link?.url).toBe(\"https://inherited.com\")\n      expect(merged.fg).toEqual(RGBA.fromInts(0, 255, 0, 255))\n    })\n\n    it(\"should handle complex link inheritance tree\", () => {\n      // Grandparent with link\n      const grandparent = new TextNodeRenderable({\n        link: { url: \"https://grandparent.com\" },\n        fg: RGBA.fromInts(255, 0, 0, 255),\n      })\n\n      // Parent inherits link, adds bg\n      const parent = new TextNodeRenderable({\n        bg: RGBA.fromInts(0, 0, 255, 255),\n      })\n\n      // Child1 inherits link, overrides fg\n      const child1 = new TextNodeRenderable({\n        fg: RGBA.fromInts(0, 255, 0, 255),\n      })\n      child1.add(\"Child1\")\n\n      // Child2 overrides link\n      const child2 = new TextNodeRenderable({\n        link: { url: \"https://child2.com\" },\n      })\n      child2.add(\"Child2\")\n\n      // Child3 has no link set, should inherit from parent\n      const child3 = new TextNodeRenderable({\n        attributes: 1,\n      })\n      child3.add(\"Child3\")\n\n      parent.add(child1)\n      parent.add(child2)\n      parent.add(child3)\n      grandparent.add(parent)\n\n      const chunks = grandparent.gatherWithInheritedStyle()\n\n      expect(chunks).toHaveLength(3)\n\n      // Child1: inherits link from grandparent\n      expect(chunks[0].text).toBe(\"Child1\")\n      expect(chunks[0].link?.url).toBe(\"https://grandparent.com\")\n      expect(chunks[0].fg).toEqual(RGBA.fromInts(0, 255, 0, 255))\n\n      // Child2: overrides link\n      expect(chunks[1].text).toBe(\"Child2\")\n      expect(chunks[1].link?.url).toBe(\"https://child2.com\")\n\n      // Child3: inherits link from grandparent\n      expect(chunks[2].text).toBe(\"Child3\")\n      expect(chunks[2].link?.url).toBe(\"https://grandparent.com\")\n    })\n  })\n\n  describe(\"Edge Cases and Error Handling\", () => {\n    it(\"should handle empty strings\", () => {\n      const node = new TextNodeRenderable({})\n\n      node.add(\"\")\n      node.add(\" \")\n\n      const chunks = node.gatherWithInheritedStyle()\n      expect(chunks).toHaveLength(2)\n      expect(chunks[0].text).toBe(\"\")\n      expect(chunks[1].text).toBe(\" \")\n    })\n\n    it(\"should handle nested empty TextNodes\", () => {\n      const parent = new TextNodeRenderable({})\n      const child = new TextNodeRenderable({})\n\n      parent.add(child)\n\n      const chunks = parent.gatherWithInheritedStyle()\n      expect(chunks).toHaveLength(0)\n    })\n\n    it(\"should handle multiple operations in sequence\", () => {\n      const node = new TextNodeRenderable({})\n\n      // Add multiple items\n      for (let i = 0; i < 5; i++) {\n        node.add(`Item ${i}`)\n      }\n\n      expect(node.children).toHaveLength(5)\n\n      // Clear and verify\n      node.clear()\n      expect(node.children).toHaveLength(0)\n    })\n\n    it(\"should efficiently calculate positions for large trees\", () => {\n      const root = new TextNodeRenderable({})\n\n      // Add many children to test position calculation efficiency\n      for (let i = 0; i < 10; i++) {\n        const child = new TextNodeRenderable({})\n        for (let j = 0; j < 5; j++) {\n          child.add(`Child ${i}-${j}`)\n        }\n        root.add(child)\n      }\n\n      // Insert before the 5th child - should be efficient\n      const fifthChild = root.children[4] as TextNodeRenderable\n      const insertChild = new TextNodeRenderable({})\n      insertChild.add(\"Inserted\")\n\n      const startTime = performance.now()\n      root.insertBefore(insertChild, fifthChild)\n      const endTime = performance.now()\n\n      // Should complete quickly (less than 1ms for this small tree)\n      expect(endTime - startTime).toBeLessThan(1)\n\n      // Verify correct position\n      expect(root.children.indexOf(insertChild)).toBe(4)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/TextNode.ts",
    "content": "import type { TextRenderable } from \"./index.js\"\nimport { BaseRenderable, type BaseRenderableOptions } from \"../Renderable.js\"\nimport { RGBA, parseColor } from \"../lib/RGBA.js\"\nimport { isStyledText, StyledText } from \"../lib/styled-text.js\"\nimport { type TextChunk } from \"../text-buffer.js\"\nimport type { RenderContext } from \"../types.js\"\n\nexport interface TextNodeOptions extends BaseRenderableOptions {\n  fg?: string | RGBA\n  bg?: string | RGBA\n  attributes?: number\n  link?: { url: string }\n}\n\nconst BrandedTextNodeRenderable: unique symbol = Symbol.for(\"@opentui/core/TextNodeRenderable\")\n\nexport function isTextNodeRenderable(obj: any): obj is TextNodeRenderable {\n  return !!obj?.[BrandedTextNodeRenderable]\n}\n\nfunction styledTextToTextNodes(styledText: StyledText): TextNodeRenderable[] {\n  return styledText.chunks.map((chunk) => {\n    const node = new TextNodeRenderable({\n      fg: chunk.fg,\n      bg: chunk.bg,\n      attributes: chunk.attributes,\n      link: chunk.link,\n    })\n    node.add(chunk.text)\n    return node\n  })\n}\n\nexport class TextNodeRenderable extends BaseRenderable {\n  [BrandedTextNodeRenderable] = true\n\n  private _fg?: RGBA\n  private _bg?: RGBA\n  private _attributes: number\n  private _link?: { url: string }\n  private _children: (string | TextNodeRenderable)[] = []\n  public parent: TextNodeRenderable | null = null\n\n  constructor(options: TextNodeOptions) {\n    super(options)\n\n    this._fg = options.fg ? parseColor(options.fg) : undefined\n    this._bg = options.bg ? parseColor(options.bg) : undefined\n    this._attributes = options.attributes ?? 0\n    this._link = options.link\n  }\n\n  public get children(): (string | TextNodeRenderable)[] {\n    return this._children\n  }\n\n  public set children(children: (string | TextNodeRenderable)[]) {\n    this._children = children\n    this.requestRender()\n  }\n\n  public requestRender(): void {\n    this.markDirty()\n    this.parent?.requestRender()\n  }\n\n  public add(obj: TextNodeRenderable | StyledText | string, index?: number): number {\n    if (typeof obj === \"string\") {\n      if (index !== undefined) {\n        this._children.splice(index, 0, obj)\n        this.requestRender()\n        return index\n      }\n\n      const insertIndex = this._children.length\n      this._children.push(obj)\n      this.requestRender()\n      return insertIndex\n    }\n\n    if (isTextNodeRenderable(obj)) {\n      if (index !== undefined) {\n        this._children.splice(index, 0, obj)\n        obj.parent = this\n        this.requestRender()\n        return index\n      }\n\n      const insertIndex = this._children.length\n      this._children.push(obj)\n      obj.parent = this\n      this.requestRender()\n      return insertIndex\n    }\n\n    if (isStyledText(obj)) {\n      const textNodes = styledTextToTextNodes(obj)\n      if (index !== undefined) {\n        this._children.splice(index, 0, ...textNodes)\n        textNodes.forEach((node) => (node.parent = this))\n        this.requestRender()\n        return index\n      }\n\n      const insertIndex = this._children.length\n      this._children.push(...textNodes)\n      textNodes.forEach((node) => (node.parent = this))\n      this.requestRender()\n      return insertIndex\n    }\n\n    throw new Error(\"TextNodeRenderable only accepts strings, TextNodeRenderable instances, or StyledText instances\")\n  }\n\n  public replace(obj: TextNodeRenderable | string, index: number) {\n    this._children[index] = obj\n    if (typeof obj !== \"string\") {\n      obj.parent = this\n    }\n    this.requestRender()\n  }\n\n  public insertBefore(\n    child: string | TextNodeRenderable | StyledText,\n    anchorNode: TextNodeRenderable | string | unknown,\n  ): this {\n    if (!anchorNode || !isTextNodeRenderable(anchorNode)) {\n      throw new Error(\"Anchor must be a TextNodeRenderable\")\n    }\n\n    const anchorIndex = this._children.indexOf(anchorNode)\n    if (anchorIndex === -1) {\n      throw new Error(\"Anchor node not found in children\")\n    }\n\n    if (typeof child === \"string\") {\n      this._children.splice(anchorIndex, 0, child)\n    } else if (isTextNodeRenderable(child)) {\n      this._children.splice(anchorIndex, 0, child)\n      child.parent = this\n    } else if (child instanceof StyledText) {\n      const textNodes = styledTextToTextNodes(child)\n      this._children.splice(anchorIndex, 0, ...textNodes)\n      textNodes.forEach((node) => (node.parent = this))\n    } else {\n      throw new Error(\"Child must be a string, TextNodeRenderable, or StyledText instance\")\n    }\n\n    this.requestRender()\n    return this\n  }\n\n  public remove(id: string): this {\n    const childIndex = this.getRenderableIndex(id)\n    if (childIndex === -1) {\n      throw new Error(\"Child not found in children\")\n    }\n\n    const child = this._children[childIndex] as TextNodeRenderable\n\n    this._children.splice(childIndex, 1)\n    child.parent = null\n    this.requestRender()\n    return this\n  }\n\n  public clear(): void {\n    this._children = []\n    this.requestRender()\n  }\n\n  public mergeStyles(parentStyle: { fg?: RGBA; bg?: RGBA; attributes: number; link?: { url: string } }): {\n    fg?: RGBA\n    bg?: RGBA\n    attributes: number\n    link?: { url: string }\n  } {\n    return {\n      fg: this._fg ?? parentStyle.fg,\n      bg: this._bg ?? parentStyle.bg,\n      attributes: this._attributes | parentStyle.attributes,\n      link: this._link ?? parentStyle.link,\n    }\n  }\n\n  public gatherWithInheritedStyle(\n    parentStyle: { fg?: RGBA; bg?: RGBA; attributes: number; link?: { url: string } } = {\n      fg: undefined,\n      bg: undefined,\n      attributes: 0,\n    },\n  ): TextChunk[] {\n    const currentStyle = this.mergeStyles(parentStyle)\n\n    const chunks: TextChunk[] = []\n\n    for (const child of this._children) {\n      if (typeof child === \"string\") {\n        chunks.push({\n          __isChunk: true,\n          text: child,\n          fg: currentStyle.fg,\n          bg: currentStyle.bg,\n          attributes: currentStyle.attributes,\n          link: currentStyle.link,\n        })\n      } else {\n        const childChunks = child.gatherWithInheritedStyle(currentStyle)\n        chunks.push(...childChunks)\n      }\n    }\n\n    this.markClean()\n\n    return chunks\n  }\n\n  public static fromString(text: string, options: Partial<TextNodeOptions> = {}): TextNodeRenderable {\n    const node = new TextNodeRenderable(options)\n    node.add(text)\n    return node\n  }\n\n  public static fromNodes(nodes: TextNodeRenderable[], options: Partial<TextNodeOptions> = {}): TextNodeRenderable {\n    const node = new TextNodeRenderable(options)\n    for (const childNode of nodes) {\n      node.add(childNode)\n    }\n    return node\n  }\n\n  public toChunks(\n    parentStyle: { fg?: RGBA; bg?: RGBA; attributes: number; link?: { url: string } } = {\n      fg: undefined,\n      bg: undefined,\n      attributes: 0,\n    },\n  ): TextChunk[] {\n    return this.gatherWithInheritedStyle(parentStyle)\n  }\n\n  public getChildren(): BaseRenderable[] {\n    return this._children.filter((child): child is TextNodeRenderable => typeof child !== \"string\")\n  }\n\n  public getChildrenCount(): number {\n    return this._children.length\n  }\n\n  public getRenderable(id: string): BaseRenderable | undefined {\n    return this._children.find((child): child is TextNodeRenderable => typeof child !== \"string\" && child.id === id)\n  }\n\n  public getRenderableIndex(id: string): number {\n    return this._children.findIndex((child) => isTextNodeRenderable(child) && child.id === id)\n  }\n\n  public get fg(): RGBA | undefined {\n    return this._fg\n  }\n\n  public set fg(fg: RGBA | string | undefined) {\n    if (!fg) {\n      this._fg = undefined\n      this.requestRender()\n      return\n    }\n    this._fg = parseColor(fg)\n    this.requestRender()\n  }\n\n  public set bg(bg: RGBA | string | undefined) {\n    if (!bg) {\n      this._bg = undefined\n      this.requestRender()\n      return\n    }\n    this._bg = parseColor(bg)\n    this.requestRender()\n  }\n\n  public get bg(): RGBA | undefined {\n    return this._bg\n  }\n\n  public set attributes(attributes: number) {\n    this._attributes = attributes\n    this.requestRender()\n  }\n\n  public get attributes(): number {\n    return this._attributes\n  }\n\n  public set link(link: { url: string } | undefined) {\n    this._link = link\n    this.requestRender()\n  }\n\n  public get link(): { url: string } | undefined {\n    return this._link\n  }\n\n  public findDescendantById(id: string): BaseRenderable | undefined {\n    return undefined\n  }\n}\n\nexport class RootTextNodeRenderable extends TextNodeRenderable {\n  textParent: TextRenderable\n\n  constructor(\n    private readonly ctx: RenderContext,\n    options: TextNodeOptions,\n    textParent: TextRenderable,\n  ) {\n    super(options)\n    this.textParent = textParent\n  }\n\n  public requestRender(): void {\n    this.markDirty()\n    this.ctx.requestRender()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/TextTable.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, test } from \"bun:test\"\nimport { OptimizedBuffer } from \"../buffer\"\nimport { RGBA } from \"../lib/RGBA\"\nimport { bold, green, red, yellow } from \"../lib/styled-text\"\nimport { createTestRenderer, type MockMouse, type TestRenderer } from \"../testing/test-renderer\"\nimport type { CapturedFrame } from \"../types\"\nimport { BoxRenderable } from \"./Box\"\nimport { ScrollBoxRenderable } from \"./ScrollBox\"\nimport { TextRenderable } from \"./Text\"\nimport { TextTableRenderable, type TextTableCellContent, type TextTableContent } from \"./TextTable\"\n\nconst VERTICAL_BORDER_CP = \"│\".codePointAt(0)!\nconst BORDER_CHAR_PATTERN = /[┌┐└┘├┤┬┴┼│─]/\n\nlet renderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet captureFrame: () => string\nlet captureSpans: () => CapturedFrame\nlet resizeRenderer: (width: number, height: number) => void\nlet mockMouse: MockMouse\n\nfunction getCharAt(buffer: TestRenderer[\"currentRenderBuffer\"], x: number, y: number): number {\n  return buffer.buffers.char[y * buffer.width + x] ?? 0\n}\n\nfunction getFgAt(buffer: TestRenderer[\"currentRenderBuffer\"], x: number, y: number): RGBA {\n  const index = (y * buffer.width + x) * 4\n  return RGBA.fromValues(\n    buffer.buffers.fg[index] ?? 0,\n    buffer.buffers.fg[index + 1] ?? 0,\n    buffer.buffers.fg[index + 2] ?? 0,\n    buffer.buffers.fg[index + 3] ?? 0,\n  )\n}\n\nfunction getBgAt(buffer: TestRenderer[\"currentRenderBuffer\"], x: number, y: number): RGBA {\n  const index = (y * buffer.width + x) * 4\n  return RGBA.fromValues(\n    buffer.buffers.bg[index] ?? 0,\n    buffer.buffers.bg[index + 1] ?? 0,\n    buffer.buffers.bg[index + 2] ?? 0,\n    buffer.buffers.bg[index + 3] ?? 0,\n  )\n}\n\nfunction findVerticalBorderXs(buffer: TestRenderer[\"currentRenderBuffer\"], y: number): number[] {\n  const xs: number[] = []\n\n  for (let x = 0; x < buffer.width; x++) {\n    if (getCharAt(buffer, x, y) === VERTICAL_BORDER_CP) {\n      xs.push(x)\n    }\n  }\n\n  return xs\n}\n\nfunction countChar(text: string, target: string): number {\n  return [...text].filter((char) => char === target).length\n}\n\nfunction normalizeFrameBlock(lines: string[]): string {\n  const trimmed = lines.map((line) => line.trimEnd())\n  const nonEmpty = trimmed.filter((line) => line.length > 0)\n  const minIndent = nonEmpty.reduce((min, line) => {\n    const indent = line.match(/^ */)?.[0].length ?? 0\n    return Math.min(min, indent)\n  }, Number.POSITIVE_INFINITY)\n  const indent = Number.isFinite(minIndent) ? minIndent : 0\n\n  return trimmed.map((line) => line.slice(indent)).join(\"\\n\") + \"\\n\"\n}\n\nfunction extractTableBlock(frame: string, headerMatcher: (line: string) => boolean): string {\n  const lines = frame.split(\"\\n\")\n  const headerY = lines.findIndex(headerMatcher)\n  if (headerY < 0) {\n    throw new Error(\"Unable to find table header line\")\n  }\n\n  let topY = headerY\n  while (topY >= 0 && !lines[topY]?.includes(\"┌\")) {\n    topY -= 1\n  }\n  if (topY < 0) {\n    throw new Error(\"Unable to find table top border\")\n  }\n\n  let bottomY = headerY\n  while (bottomY < lines.length && !lines[bottomY]?.includes(\"└\")) {\n    bottomY += 1\n  }\n  if (bottomY >= lines.length) {\n    throw new Error(\"Unable to find table bottom border\")\n  }\n\n  return normalizeFrameBlock(lines.slice(topY, bottomY + 1))\n}\n\nasync function renderStandaloneTableBlock(\n  width: number,\n  content: TextTableContent,\n  headerMatcher: (line: string) => boolean,\n): Promise<string> {\n  const testRenderer = await createTestRenderer({ width, height: 120 })\n\n  try {\n    const table = new TextTableRenderable(testRenderer.renderer, {\n      left: 0,\n      top: 0,\n      width,\n      wrapMode: \"word\",\n      content,\n    })\n\n    testRenderer.renderer.root.add(table)\n    await testRenderer.renderOnce()\n    return extractTableBlock(testRenderer.captureCharFrame(), headerMatcher)\n  } finally {\n    testRenderer.renderer.destroy()\n  }\n}\n\nfunction findSelectablePoint(\n  table: TextTableRenderable,\n  direction: \"top-left\" | \"bottom-right\",\n): { x: number; y: number } {\n  const points: Array<{ x: number; y: number }> = []\n\n  for (let y = table.y; y < table.y + table.height; y++) {\n    for (let x = table.x; x < table.x + table.width; x++) {\n      if (table.shouldStartSelection(x, y)) {\n        points.push({ x, y })\n      }\n    }\n  }\n\n  expect(points.length).toBeGreaterThan(0)\n\n  if (direction === \"top-left\") {\n    points.sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x))\n    return points[0]!\n  }\n\n  points.sort((a, b) => (a.y !== b.y ? b.y - a.y : b.x - a.x))\n  return points[0]!\n}\n\nfunction findTextPoint(frame: string, text: string): { x: number; y: number } {\n  const lines = frame.split(\"\\n\")\n\n  for (let y = 0; y < lines.length; y++) {\n    const x = lines[y]?.indexOf(text) ?? -1\n    if (x >= 0) {\n      return { x, y }\n    }\n  }\n\n  throw new Error(`Unable to find '${text}' in frame`)\n}\n\nfunction cell(text: string): TextTableCellContent {\n  return [\n    {\n      __isChunk: true,\n      text,\n    },\n  ]\n}\n\nfunction getScrollContentBottom(scrollBox: ScrollBoxRenderable): number {\n  const children = scrollBox.content.getChildren()\n  const lastChild = children[children.length - 1]\n\n  if (!lastChild) {\n    return Math.max(0, Math.ceil(scrollBox.content.height))\n  }\n\n  const relativeBottom = lastChild.y - scrollBox.content.y + lastChild.height\n  return Math.max(0, Math.ceil(relativeBottom))\n}\n\nbeforeEach(async () => {\n  const testRenderer = await createTestRenderer({ width: 60, height: 16 })\n  renderer = testRenderer.renderer\n  renderOnce = testRenderer.renderOnce\n  captureFrame = testRenderer.captureCharFrame\n  captureSpans = testRenderer.captureSpans\n  resizeRenderer = testRenderer.resize\n  mockMouse = testRenderer.mockMouse\n})\n\nafterEach(() => {\n  renderer.destroy()\n})\n\ndescribe(\"TextTableRenderable\", () => {\n  test(\"renders a basic table with styled cell chunks\", async () => {\n    const content: TextTableContent = [\n      [[bold(\"Name\")], [bold(\"Status\")], [bold(\"Notes\")]],\n      [cell(\"Alpha\"), [green(\"OK\")], cell(\"All systems nominal\")],\n      [cell(\"Bravo\"), [red(\"WARN\")], cell(\"Pending checks\")],\n    ]\n\n    const table = new TextTableRenderable(renderer, {\n      left: 1,\n      top: 1,\n      columnWidthMode: \"content\",\n      content,\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    expect(frame).toMatchSnapshot(\"basic table\")\n    expect(frame).toContain(\"Alpha\")\n    expect(frame).toContain(\"WARN\")\n\n    const spans = captureSpans().lines.flatMap((line) => line.spans)\n    const okSpan = spans.find((span) => span.text.includes(\"OK\"))\n\n    expect(okSpan).toBeDefined()\n    expect(okSpan?.fg.equals(RGBA.fromHex(\"#008000\"))).toBe(true)\n  })\n\n  test(\"wraps content and fits columns when width is constrained\", async () => {\n    const content: TextTableContent = [\n      [[bold(\"ID\")], [bold(\"Description\")]],\n      [cell(\"1\"), cell(\"This is a long sentence that should wrap across multiple visual lines\")],\n      [cell(\"2\"), cell(\"Short\")],\n    ]\n\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      width: 34,\n      wrapMode: \"word\",\n      content,\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    expect(frame).toMatchSnapshot(\"wrapped constrained width\")\n    expect(frame).toContain(\"Description\")\n  })\n\n  test(\"keeps intrinsic width in content mode when extra space is available\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      width: 34,\n      wrapMode: \"word\",\n      columnWidthMode: \"content\",\n      content: [\n        [cell(\"A\"), cell(\"B\")],\n        [cell(\"1\"), cell(\"2\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const lines = captureFrame().split(\"\\n\")\n    const headerY = lines.findIndex((line) => line.includes(\"A\") && line.includes(\"B\"))\n    expect(headerY).toBeGreaterThanOrEqual(0)\n\n    const buffer = renderer.currentRenderBuffer\n    const borderXs = findVerticalBorderXs(buffer, headerY)\n\n    expect(borderXs.length).toBe(3)\n    expect(borderXs[0]).toBe(0)\n    expect(borderXs[borderXs.length - 1]).toBeLessThan(33)\n  })\n\n  test(\"fills available width by default in full mode\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      width: 34,\n      wrapMode: \"word\",\n      content: [\n        [cell(\"A\"), cell(\"B\")],\n        [cell(\"1\"), cell(\"2\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const lines = captureFrame().split(\"\\n\")\n    const headerY = lines.findIndex((line) => line.includes(\"A\") && line.includes(\"B\"))\n    expect(headerY).toBeGreaterThanOrEqual(0)\n\n    const buffer = renderer.currentRenderBuffer\n    const borderXs = findVerticalBorderXs(buffer, headerY)\n\n    expect(borderXs).toEqual([0, 17, 33])\n  })\n\n  test(\"fills available width in no-wrap mode when columnWidthMode is full\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      width: 24,\n      wrapMode: \"none\",\n      columnWidthMode: \"full\",\n      content: [\n        [cell(\"Key\"), cell(\"Value\")],\n        [cell(\"A\"), cell(\"B\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const lines = captureFrame().split(\"\\n\")\n    const headerY = lines.findIndex((line) => line.includes(\"Key\") && line.includes(\"Value\"))\n    expect(headerY).toBeGreaterThanOrEqual(0)\n\n    const buffer = renderer.currentRenderBuffer\n    const borderXs = findVerticalBorderXs(buffer, headerY)\n\n    expect(borderXs).toEqual([0, 11, 23])\n  })\n\n  test(\"preserves bordered layout when border glyphs are hidden\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      border: true,\n      outerBorder: true,\n      showBorders: false,\n      columnWidthMode: \"content\",\n      content: [[cell(\"A\"), cell(\"B\")]],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    expect(BORDER_CHAR_PATTERN.test(frame)).toBe(false)\n\n    const row = frame.split(\"\\n\").find((line) => line.includes(\"A\") && line.includes(\"B\"))\n    expect(row).toBeDefined()\n    expect(row?.indexOf(\"A\")).toBe(1)\n    expect(row?.indexOf(\"B\")).toBe(3)\n  })\n\n  test(\"applies cell padding when provided\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      cellPadding: 1,\n      columnWidthMode: \"content\",\n      content: [\n        [cell(\"A\"), cell(\"B\")],\n        [cell(\"1\"), cell(\"2\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    expect(frame).toContain(\"│   │   │\")\n    expect(frame).toContain(\"│ A │ B │\")\n\n    const lines = frame.split(\"\\n\")\n    const headerY = lines.findIndex((line) => line.includes(\" A \") && line.includes(\" B \"))\n    expect(headerY).toBeGreaterThanOrEqual(0)\n\n    const borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, headerY)\n    expect(borderXs).toEqual([0, 4, 8])\n  })\n\n  test(\"reflows when columnWidthMode is changed after initial render\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      width: 34,\n      wrapMode: \"word\",\n      columnWidthMode: \"content\",\n      content: [\n        [cell(\"A\"), cell(\"B\")],\n        [cell(\"1\"), cell(\"2\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    let lines = captureFrame().split(\"\\n\")\n    let headerY = lines.findIndex((line) => line.includes(\"A\") && line.includes(\"B\"))\n    expect(headerY).toBeGreaterThanOrEqual(0)\n\n    let borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, headerY)\n    expect(borderXs[borderXs.length - 1]).toBeLessThan(33)\n\n    table.columnWidthMode = \"full\"\n    await renderOnce()\n\n    lines = captureFrame().split(\"\\n\")\n    headerY = lines.findIndex((line) => line.includes(\"A\") && line.includes(\"B\"))\n    expect(headerY).toBeGreaterThanOrEqual(0)\n\n    borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, headerY)\n    expect(borderXs).toEqual([0, 17, 33])\n  })\n\n  test(\"accepts columnFitter in options and setter\", () => {\n    const table = new TextTableRenderable(renderer, {\n      columnFitter: \"balanced\",\n      content: [[cell(\"A\")]],\n    })\n\n    expect(table.columnFitter).toBe(\"balanced\")\n\n    table.columnFitter = \"proportional\"\n    expect(table.columnFitter).toBe(\"proportional\")\n  })\n\n  test(\"balanced fitter keeps constrained columns visually closer\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      width: 58,\n      wrapMode: \"word\",\n      columnWidthMode: \"full\",\n      columnFitter: \"proportional\",\n      content: [\n        [\n          cell(\"Provider\"),\n          cell(\"Compute Services\"),\n          cell(\"Storage Solutions\"),\n          cell(\"Pricing Model\"),\n          cell(\"Regions\"),\n          cell(\"Use Cases\"),\n        ],\n        [\n          cell(\"Amazon Web Services\"),\n          cell(\"EC2 instances with extensive options for general, memory, and accelerated workloads\"),\n          cell(\"S3 tiers, EBS, EFS, and archive classes for long retention\"),\n          cell(\"Pay as you go, reserved terms, and discounted spot capacity\"),\n          cell(\"Global regions and many edge locations\"),\n          cell(\"Enterprise migration, analytics, ML, and backend services\"),\n        ],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const proportionalFrame = captureFrame()\n    expect(proportionalFrame).toMatchSnapshot(\"fitter proportional constrained\")\n\n    const getRenderedWidths = (): number[] => {\n      const lines = captureFrame().split(\"\\n\")\n      const headerY = lines.findIndex((line) => line.includes(\"Compute\") && line.includes(\"Pricing\"))\n      expect(headerY).toBeGreaterThanOrEqual(0)\n\n      const borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, headerY)\n      expect(borderXs.length).toBeGreaterThan(2)\n\n      return borderXs.slice(1).map((x, idx) => x - borderXs[idx] - 1)\n    }\n\n    const proportionalWidths = getRenderedWidths()\n    const proportionalSpread = Math.max(...proportionalWidths) - Math.min(...proportionalWidths)\n\n    table.columnFitter = \"balanced\"\n    await renderOnce()\n\n    const balancedFrame = captureFrame()\n    expect(balancedFrame).toMatchSnapshot(\"fitter balanced constrained\")\n\n    const balancedWidths = getRenderedWidths()\n    const balancedSpread = Math.max(...balancedWidths) - Math.min(...balancedWidths)\n\n    expect(table.columnFitter).toBe(\"balanced\")\n    expect(balancedFrame).not.toBe(proportionalFrame)\n    expect(balancedWidths[0]).toBeGreaterThan(proportionalWidths[0] ?? 0)\n    expect(balancedSpread).toBeLessThan(proportionalSpread)\n  })\n\n  test(\"uses native border draw for inner-only mode\", async () => {\n    const originalDrawGrid = OptimizedBuffer.prototype.drawGrid\n    let nativeCalls = 0\n\n    OptimizedBuffer.prototype.drawGrid = function (...args: Parameters<OptimizedBuffer[\"drawGrid\"]>) {\n      nativeCalls += 1\n      return originalDrawGrid.apply(this, args)\n    }\n\n    try {\n      const table = new TextTableRenderable(renderer, {\n        left: 0,\n        top: 0,\n        border: true,\n        outerBorder: false,\n        columnWidthMode: \"content\",\n        content: [\n          [cell(\"A\"), cell(\"B\")],\n          [cell(\"1\"), cell(\"2\")],\n        ],\n      })\n\n      renderer.root.add(table)\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).not.toContain(\"┌\")\n      expect(frame).not.toContain(\"┐\")\n      expect(frame).not.toContain(\"└\")\n      expect(frame).not.toContain(\"┘\")\n      expect(frame).toContain(\"┼\")\n      expect(nativeCalls).toBe(1)\n\n      const lines = frame.split(\"\\n\")\n      const rowY = lines.findIndex((line) => line.includes(\"A\") && line.includes(\"B\"))\n      expect(rowY).toBeGreaterThanOrEqual(0)\n\n      const borderXs = findVerticalBorderXs(renderer.currentRenderBuffer, rowY)\n      expect(borderXs).toEqual([1])\n    } finally {\n      OptimizedBuffer.prototype.drawGrid = originalDrawGrid\n    }\n  })\n\n  test(\"defaults outerBorder to false when border is false\", async () => {\n    const originalDrawGrid = OptimizedBuffer.prototype.drawGrid\n    let nativeCalls = 0\n\n    OptimizedBuffer.prototype.drawGrid = function (...args: Parameters<OptimizedBuffer[\"drawGrid\"]>) {\n      nativeCalls += 1\n      return originalDrawGrid.apply(this, args)\n    }\n\n    try {\n      const table = new TextTableRenderable(renderer, {\n        left: 0,\n        top: 0,\n        border: false,\n        columnWidthMode: \"content\",\n        content: [\n          [cell(\"A\"), cell(\"B\")],\n          [cell(\"1\"), cell(\"2\")],\n        ],\n      })\n\n      renderer.root.add(table)\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(table.outerBorder).toBe(false)\n      expect(BORDER_CHAR_PATTERN.test(frame)).toBe(false)\n      expect(frame).toContain(\"AB\")\n      expect(nativeCalls).toBe(0)\n    } finally {\n      OptimizedBuffer.prototype.drawGrid = originalDrawGrid\n    }\n  })\n\n  test(\"allows outer border even when inner border is off\", async () => {\n    const originalDrawGrid = OptimizedBuffer.prototype.drawGrid\n    let nativeCalls = 0\n\n    OptimizedBuffer.prototype.drawGrid = function (...args: Parameters<OptimizedBuffer[\"drawGrid\"]>) {\n      nativeCalls += 1\n      return originalDrawGrid.apply(this, args)\n    }\n\n    try {\n      const table = new TextTableRenderable(renderer, {\n        left: 0,\n        top: 0,\n        border: false,\n        outerBorder: true,\n        content: [\n          [cell(\"A\"), cell(\"B\")],\n          [cell(\"1\"), cell(\"2\")],\n        ],\n      })\n\n      renderer.root.add(table)\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toContain(\"┌\")\n      expect(frame).toContain(\"┐\")\n      expect(frame).toContain(\"└\")\n      expect(frame).toContain(\"┘\")\n      expect(frame).not.toContain(\"┼\")\n      expect(nativeCalls).toBe(1)\n    } finally {\n      OptimizedBuffer.prototype.drawGrid = originalDrawGrid\n    }\n  })\n\n  test(\"rebuilds table when content setter is used\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      columnWidthMode: \"content\",\n      content: [[cell(\"A\"), cell(\"B\")]],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const before = captureFrame()\n\n    table.content = [\n      [[bold(\"Col 1\")], [bold(\"Col 2\")]],\n      [cell(\"row-1\"), cell(\"updated\")],\n      [cell(\"row-2\"), [green(\"active\")]],\n    ]\n\n    await renderOnce()\n\n    const after = captureFrame()\n    expect(before).not.toBe(after)\n    expect(after).toMatchSnapshot(\"content setter update\")\n  })\n\n  test(\"renders a final bottom border\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [\n        [[bold(\"A\")], [bold(\"B\")]],\n        [cell(\"1\"), cell(\"2\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    const lines = frame\n      .split(\"\\n\")\n      .map((line) => line.trimEnd())\n      .filter((line) => line.length > 0)\n\n    const lastLine = lines[lines.length - 1] ?? \"\"\n\n    expect(lastLine).toContain(\"└\")\n    expect(lastLine).toContain(\"┴\")\n    expect(lastLine).toContain(\"┘\")\n  })\n\n  test(\"keeps borders aligned with CJK and emoji content\", async () => {\n    const content: TextTableContent = [\n      [[bold(\"Locale\")], [bold(\"Sample\")]],\n      [cell(\"ja-JP\"), cell(\"東京で寿司 🍣\")],\n      [cell(\"zh-CN\"), cell(\"你好世界 🚀\")],\n      [cell(\"ko-KR\"), cell(\"한글 테스트 😄\")],\n    ]\n\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      width: 36,\n      wrapMode: \"none\",\n      columnWidthMode: \"content\",\n      content,\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    expect(frame).toMatchSnapshot(\"unicode border alignment\")\n    expect(frame).toContain(\"東京で寿司\")\n    expect(frame).toContain(\"🚀\")\n    expect(frame).toContain(\"😄\")\n\n    const lines = frame.split(\"\\n\")\n    const headerY = lines.findIndex((line) => line.includes(\"Locale\"))\n    expect(headerY).toBeGreaterThanOrEqual(0)\n\n    const buffer = renderer.currentRenderBuffer\n    const borderXs = findVerticalBorderXs(buffer, headerY)\n    expect(borderXs.length).toBe(3)\n\n    const sampleRowYs = [\n      lines.findIndex((line) => line.includes(\"ja-JP\")),\n      lines.findIndex((line) => line.includes(\"zh-CN\")),\n      lines.findIndex((line) => line.includes(\"ko-KR\")),\n    ]\n\n    for (const y of sampleRowYs) {\n      expect(y).toBeGreaterThanOrEqual(0)\n      for (const x of borderXs) {\n        expect(getCharAt(buffer, x, y)).toBe(VERTICAL_BORDER_CP)\n      }\n    }\n  })\n\n  test(\"wraps CJK and emoji without grapheme duplication\", async () => {\n    const content: TextTableContent = [\n      [[bold(\"Item\")], [bold(\"Details\")]],\n      [cell(\"mixed\"), cell(\"東京界 🌍 emoji wrapping continues across lines for width checks\")],\n      [cell(\"emoji\"), cell(\"Faces 😀😃😄 should remain stable\")],\n    ]\n\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      width: 30,\n      wrapMode: \"word\",\n      content,\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    expect(frame).toMatchSnapshot(\"unicode wrapping\")\n    expect(frame).not.toContain(\"�\")\n    expect(countChar(frame, \"界\")).toBe(1)\n    expect(countChar(frame, \"🌍\")).toBe(1)\n\n    const lines = frame.split(\"\\n\")\n    const wrappedRowStartY = lines.findIndex((line) => line.includes(\"mix\") && line.includes(\"東京界\"))\n    const wrappedRowEndBorderY = lines.findIndex((line, idx) => idx > wrappedRowStartY && line.includes(\"├\"))\n\n    expect(wrappedRowStartY).toBeGreaterThanOrEqual(0)\n    expect(wrappedRowEndBorderY).toBeGreaterThan(wrappedRowStartY)\n\n    const wrappedRowYs: number[] = []\n    for (let y = wrappedRowStartY; y < wrappedRowEndBorderY; y++) {\n      wrappedRowYs.push(y)\n    }\n\n    expect(wrappedRowYs.length).toBeGreaterThan(1)\n\n    const headerY = lines.findIndex((line) => line.includes(\"Ite\") && line.includes(\"Details\"))\n    expect(headerY).toBeGreaterThanOrEqual(0)\n\n    const buffer = renderer.currentRenderBuffer\n    const borderXs = findVerticalBorderXs(buffer, headerY)\n    expect(borderXs.length).toBe(3)\n\n    for (const y of wrappedRowYs) {\n      for (const x of borderXs) {\n        expect(getCharAt(buffer, x, y)).toBe(VERTICAL_BORDER_CP)\n      }\n    }\n  })\n\n  test(\"starts selection only on table cell content\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [\n        [[bold(\"A\")], [bold(\"B\")]],\n        [cell(\"1\"), cell(\"2\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    expect(table.shouldStartSelection(table.x, table.y)).toBe(false)\n    expect(table.shouldStartSelection(table.x + 1, table.y)).toBe(false)\n    expect(table.shouldStartSelection(table.x, table.y + 1)).toBe(false)\n    expect(table.shouldStartSelection(table.x + 1, table.y + 1)).toBe(true)\n  })\n\n  test(\"selection text excludes border glyphs\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      columnWidthMode: \"content\",\n      content: [\n        [[bold(\"c1\")], [bold(\"c2\")]],\n        [cell(\"aa\"), cell(\"bb\")],\n        [cell(\"cc\"), cell(\"dd\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    await mockMouse.drag(table.x + 1, table.y + 1, table.x + 5, table.y + 3)\n    await renderOnce()\n\n    expect(table.hasSelection()).toBe(true)\n\n    const selected = table.getSelectedText()\n    expect(selected).toContain(\"c1\\tc2\")\n    expect(selected).toContain(\"aa\\tb\")\n    expect(selected).not.toContain(\"│\")\n    expect(selected).not.toContain(\"┌\")\n    expect(selected).not.toContain(\"┼\")\n\n    const rendererSelection = renderer.getSelection()\n    expect(rendererSelection).not.toBeNull()\n    expect(rendererSelection?.getSelectedText()).not.toContain(\"│\")\n  })\n\n  test(\"keeps partial selection when focus stays in the anchor cell\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [[cell(\"alphabet\"), cell(\"status\")]],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const anchor = findTextPoint(captureFrame(), \"alphabet\")\n\n    await mockMouse.drag(anchor.x + 3, anchor.y, anchor.x + 5, anchor.y)\n    await renderOnce()\n\n    expect(table.getSelectedText()).toBe(\"ha\")\n  })\n\n  test(\"selects the full anchor cell once focus leaves that cell\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [[cell(\"alphabet\"), cell(\"status\")]],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    const anchor = findTextPoint(frame, \"alphabet\")\n    const focus = findTextPoint(frame, \"status\")\n\n    await mockMouse.drag(anchor.x + 3, anchor.y, focus.x + 2, focus.y)\n    await renderOnce()\n\n    const [firstCell] = table.getSelectedText().split(\"\\t\")\n    expect(firstCell).toBe(\"alphabet\")\n  })\n\n  test(\"locks vertical drag to the anchor column while focus stays in that column\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [\n        [cell(\"colA\"), cell(\"colB\"), cell(\"colC\")],\n        [cell(\"a1\"), cell(\"b1\"), cell(\"c1\")],\n        [cell(\"a2\"), cell(\"b2\"), cell(\"c2\")],\n        [cell(\"a3\"), cell(\"b3\"), cell(\"c3\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const anchor = findTextPoint(captureFrame(), \"colB\")\n\n    await mockMouse.drag(anchor.x, anchor.y, anchor.x, table.y + table.height + 2)\n    await renderOnce()\n\n    expect(table.getSelectedText()).toBe(\"colB\\nb1\\nb2\\nb3\")\n  })\n\n  test(\"returns to normal grid selection after focus leaves the anchor column\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [\n        [cell(\"colA\"), cell(\"colB\"), cell(\"colC\")],\n        [cell(\"a1\"), cell(\"b1\"), cell(\"c1\")],\n        [cell(\"a2\"), cell(\"b2\"), cell(\"c2\")],\n        [cell(\"a3\"), cell(\"b3\"), cell(\"c3\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const frame = captureFrame()\n    const anchor = findTextPoint(frame, \"colB\")\n    const focus = findTextPoint(frame, \"colC\")\n\n    await mockMouse.drag(anchor.x, anchor.y, focus.x, table.y + table.height + 2)\n    await renderOnce()\n\n    expect(table.getSelectedText()).toBe(\"colB\\tcolC\\na1\\tb1\\tc1\\na2\\tb2\\tc2\\na3\\tb3\\tc3\")\n  })\n\n  test(\"selection colors reset when drag retracts back to the anchor\", async () => {\n    const defaultFg = RGBA.fromHex(\"#111111\")\n    const defaultBg = RGBA.fromValues(0, 0, 0, 0)\n    const selectionFg = RGBA.fromHex(\"#fefefe\")\n    const selectionBg = RGBA.fromHex(\"#cc5500\")\n\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      fg: defaultFg,\n      bg: \"transparent\",\n      selectionFg,\n      selectionBg,\n      columnWidthMode: \"content\",\n      content: [\n        [\"A\", \"B\"],\n        [\"C\", \"D\"],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const anchorX = table.x + 1\n    const anchorY = table.y + 1\n    const farX = table.x + 3\n    const farY = table.y + 3\n\n    await mockMouse.pressDown(anchorX, anchorY)\n    await mockMouse.moveTo(farX, farY)\n    await renderOnce()\n\n    expect(table.hasSelection()).toBe(true)\n\n    let buffer = renderer.currentRenderBuffer\n    const selectedCells: Array<{ x: number; y: number }> = []\n\n    for (let y = table.y; y < table.y + table.height; y++) {\n      for (let x = table.x; x < table.x + table.width; x++) {\n        if (getBgAt(buffer, x, y).equals(selectionBg)) {\n          selectedCells.push({ x, y })\n        }\n      }\n    }\n\n    expect(selectedCells.length).toBeGreaterThan(1)\n\n    await mockMouse.moveTo(anchorX, anchorY)\n    await renderOnce()\n\n    const assertDeselectedCellsRestored = (frameBuffer: TestRenderer[\"currentRenderBuffer\"]): void => {\n      const mismatches: string[] = []\n\n      for (const { x, y } of selectedCells) {\n        if (x === anchorX && y === anchorY) continue\n\n        const cp = getCharAt(frameBuffer, x, y)\n        if (cp === 0 || cp === VERTICAL_BORDER_CP) continue\n\n        if (!getFgAt(frameBuffer, x, y).equals(defaultFg)) {\n          mismatches.push(`fg@${x},${y}`)\n        }\n\n        if (!getBgAt(frameBuffer, x, y).equals(defaultBg)) {\n          mismatches.push(`bg@${x},${y}`)\n        }\n      }\n\n      expect(mismatches).toEqual([])\n    }\n\n    buffer = renderer.currentRenderBuffer\n    expect(table.getSelectedText()).toBe(\"\")\n    assertDeselectedCellsRestored(buffer)\n\n    await mockMouse.release(anchorX, anchorY)\n    await renderOnce()\n\n    buffer = renderer.currentRenderBuffer\n    assertDeselectedCellsRestored(buffer)\n    expect(getCharAt(buffer, farX, farY)).toBe(\"D\".codePointAt(0))\n  })\n\n  test(\"does not start selection when drag begins on border\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [\n        [[bold(\"A\")], [bold(\"B\")]],\n        [cell(\"1\"), cell(\"2\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    await mockMouse.drag(table.x, table.y, table.x + 4, table.y + 1)\n    await renderOnce()\n\n    expect(table.hasSelection()).toBe(false)\n    expect(table.getSelectedText()).toBe(\"\")\n  })\n\n  test(\"clears stale per-cell local selection state between drags\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 1,\n      top: 8,\n      width: 44,\n      content: [\n        [[bold(\"Service\")], [bold(\"Status\")], [bold(\"Notes\")]],\n        [cell(\"api\"), [green(\"OK\")], cell(\"latency 28ms\")],\n        [cell(\"worker\"), [yellow(\"DEGRADED\")], cell(\"queue depth: 124\")],\n        [cell(\"billing\"), [red(\"ERROR\")], cell(\"retrying payment provider\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    await mockMouse.drag(14, 9, 40, 18)\n    await renderOnce()\n\n    await mockMouse.click(27, 13)\n    await renderOnce()\n\n    await mockMouse.pressDown(13, 9)\n    await renderOnce()\n\n    await mockMouse.moveTo(13, 10)\n    await renderOnce()\n    await mockMouse.moveTo(13, 11)\n    await renderOnce()\n    await mockMouse.moveTo(13, 13)\n    await renderOnce()\n    await mockMouse.moveTo(13, 16)\n    await renderOnce()\n    await mockMouse.moveTo(13, 20)\n    await renderOnce()\n\n    await mockMouse.release(13, 20)\n    await renderOnce()\n\n    expect(table.getSelectedText()).toBe(\"Status\\nOK\\nDEGRADED\\nERROR\")\n  })\n\n  test(\"reverse drag across full table keeps left cells selected\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [\n        [[bold(\"H1\")], [bold(\"H2\")], [bold(\"H3\")]],\n        [cell(\"R1C1\"), cell(\"R1C2\"), cell(\"R1C3\")],\n        [cell(\"R2C1\"), cell(\"R2C2\"), cell(\"R2C3\")],\n        [cell(\"R3C1\"), cell(\"R3C2\"), cell(\"R3C3\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const start = findSelectablePoint(table, \"bottom-right\")\n    const end = findSelectablePoint(table, \"top-left\")\n\n    await mockMouse.drag(start.x, start.y, end.x, end.y)\n    await renderOnce()\n\n    const selected = table.getSelectedText()\n\n    expect(selected).toBe(\"H1\\tH2\\tH3\\nR1C1\\tR1C2\\tR1C3\\nR2C1\\tR2C2\\tR2C3\\nR3C1\\tR3C2\\tR3C3\")\n  })\n\n  test(\"reverse drag ending on left border still includes first column\", async () => {\n    const table = new TextTableRenderable(renderer, {\n      left: 0,\n      top: 0,\n      content: [\n        [[bold(\"Name\")], [bold(\"Status\")]],\n        [cell(\"Alice\"), cell(\"Done\")],\n        [cell(\"Bob\"), cell(\"In Progress\")],\n      ],\n    })\n\n    renderer.root.add(table)\n    await renderOnce()\n\n    const start = findSelectablePoint(table, \"bottom-right\")\n    const endX = table.x\n    const endY = findSelectablePoint(table, \"top-left\").y\n\n    await mockMouse.drag(start.x, start.y, endX, endY)\n    await renderOnce()\n\n    const selected = table.getSelectedText()\n\n    expect(selected).toContain(\"Name\")\n    expect(selected).toContain(\"Alice\")\n    expect(selected).toContain(\"Bob\")\n  })\n\n  test(\"keeps full wrapped table layouts after a wide-to-narrow demo-style resize\", async () => {\n    resizeRenderer(108, 38)\n    await renderOnce()\n\n    const primaryContent: TextTableContent = [\n      [[bold(\"Task\")], [bold(\"Owner\")], [bold(\"ETA\")]],\n      [\n        cell(\n          \"Wrap regression in operational status dashboard with dynamic row heights and constrained layout validation\",\n        ),\n        cell(\"core platform and runtime reliability squad\"),\n        cell(\n          \"done after validating none, word, and char wrap modes across narrow, medium, wide, and ultra-wide terminal widths\",\n        ),\n      ],\n      [\n        cell(\n          \"Unicode layout stabilization for mixed Latin, punctuation, symbols, and long identifiers in adjacent columns\",\n        ),\n        cell(\"render pipeline maintainers with fallback shaping support\"),\n        cell(\n          \"in review with follow-up checks for border style transitions, cell padding variants, and selection range consistency\",\n        ),\n      ],\n      [\n        cell(\n          \"Snapshot pass for table rendering in content mode and full mode with heavy and double border combinations\",\n        ),\n        cell(\"qa automation and visual diff triage group\"),\n        cell(\n          \"today pending final baseline updates for oversized fixtures that intentionally stress wrapping behavior on high-resolution terminals\",\n        ),\n      ],\n      [\n        cell(\n          \"Document edge cases where long tokens without spaces force char wrapping and reveal per-cell clipping regressions\",\n        ),\n        cell(\"developer experience and docs tooling\"),\n        cell(\n          \"planned for this sprint once final reproducible examples are captured and linked to regression tracking tickets\",\n        ),\n      ],\n      [\n        cell(\n          \"Performance sweep of wrapping algorithm under large datasets to confirm stable frame times during rapid key toggling\",\n        ),\n        cell(\"runtime performance task force\"),\n        cell(\"scheduled after review, with benchmark runs on laptop and desktop terminals at 200-plus column widths\"),\n      ],\n    ]\n\n    const unicodeContent: TextTableContent = [\n      [[bold(\"Column\")], [bold(\"Wrapped Text\")]],\n      [\n        cell(\"mixed-languages\"),\n        cell(\n          \"CJK and emoji wrapping stress case: こんにちは世界 and 안녕하세요 세계 and 你好，世界 followed by long English prose that keeps flowing to test whether each cell wraps naturally even when the terminal is extremely wide and the row still needs multiple visual lines for readability 🌍🚀\",\n        ),\n      ],\n      [\n        cell(\"emoji-and-symbols\"),\n        cell(\n          \"Faces 😀😃😄😁😆 plus symbols 🧪📦🛰️🔧📊 mixed with version tags like release-candidate-build-2026-02-very-long-token-without-breaks to ensure char wrapping remains stable and no glyph alignment issues appear at column boundaries\",\n        ),\n      ],\n      [\n        cell(\"long-cjk-phrase\"),\n        cell(\n          \"長文の日本語テキストと中文段落和한국어문장을連続して配置し、その後に additional English context describing renderer behavior, border intersection handling, and selection extraction so that this single cell remains a reliable wrapping torture test.\",\n        ),\n      ],\n      [\n        cell(\"mixed-punctuation\"),\n        cell(\n          \"Wrap behavior with punctuation-heavy content: [alpha]{beta}(gamma)<delta>|epsilon| then repeated fragments, commas, semicolons, and slashes to verify token boundaries do not break border drawing logic or spacing consistency in neighboring columns.\",\n        ),\n      ],\n    ]\n\n    const container = new BoxRenderable(renderer, {\n      width: \"100%\",\n      height: \"100%\",\n      flexDirection: \"column\",\n      padding: 1,\n      gap: 1,\n    })\n\n    const tableAreaScrollBox = new ScrollBoxRenderable(renderer, {\n      width: \"100%\",\n      flexGrow: 1,\n      flexShrink: 1,\n      scrollY: true,\n      scrollX: false,\n      border: false,\n      contentOptions: {\n        flexDirection: \"column\",\n        gap: 1,\n      },\n    })\n\n    const controlsText = new TextRenderable(renderer, {\n      content: \"TextTable Demo\",\n      wrapMode: \"word\",\n      selectable: false,\n    })\n\n    const primaryLabel = new TextRenderable(renderer, {\n      content: \"Operational Table\",\n      selectable: false,\n    })\n\n    const primaryTable = new TextTableRenderable(renderer, {\n      width: \"100%\",\n      wrapMode: \"word\",\n      content: primaryContent,\n    })\n\n    const unicodeLabel = new TextRenderable(renderer, {\n      content: \"Unicode/CJK/Emoji Table\",\n      selectable: false,\n    })\n\n    const unicodeTable = new TextTableRenderable(renderer, {\n      width: \"100%\",\n      wrapMode: \"word\",\n      content: unicodeContent,\n    })\n\n    const selectionBox = new BoxRenderable(renderer, {\n      width: \"100%\",\n      height: 10,\n      flexGrow: 0,\n      flexShrink: 0,\n      border: true,\n      title: \"Selected Text\",\n      titleAlignment: \"left\",\n      padding: 1,\n    })\n\n    tableAreaScrollBox.add(controlsText)\n    tableAreaScrollBox.add(primaryLabel)\n    tableAreaScrollBox.add(primaryTable)\n    tableAreaScrollBox.add(unicodeLabel)\n    tableAreaScrollBox.add(unicodeTable)\n\n    container.add(tableAreaScrollBox)\n    container.add(selectionBox)\n    renderer.root.add(container)\n\n    await renderOnce()\n\n    resizeRenderer(72, 38)\n    await renderOnce()\n    await renderOnce()\n\n    const expectedPrimaryFrame = await renderStandaloneTableBlock(primaryTable.width, primaryContent, (line) =>\n      line.includes(\"Task\"),\n    )\n    const expectedUnicodeFrame = await renderStandaloneTableBlock(unicodeTable.width, unicodeContent, (line) =>\n      line.includes(\"Wrapped\"),\n    )\n\n    expect(expectedPrimaryFrame).toMatchSnapshot(\"demo resize expected primary table\")\n    expect(expectedUnicodeFrame).toMatchSnapshot(\"demo resize expected unicode table\")\n\n    const resizedFrame = captureFrame()\n    expect(resizedFrame).toContain(\"Operational Table\")\n    expect(resizedFrame).toContain(\"Task\")\n\n    const contentBottom = getScrollContentBottom(tableAreaScrollBox)\n    expect(contentBottom).toBeGreaterThan(tableAreaScrollBox.viewport.height)\n    expect(tableAreaScrollBox.scrollHeight).toBe(contentBottom)\n\n    const maxScrollTop = Math.max(0, tableAreaScrollBox.scrollHeight - tableAreaScrollBox.viewport.height)\n    expect(maxScrollTop).toBeGreaterThan(0)\n\n    tableAreaScrollBox.scrollTop = maxScrollTop\n    await renderOnce()\n\n    const scrolledToBottomFrame = captureFrame()\n    expect(scrolledToBottomFrame).toContain(\"epsilon\")\n  })\n\n  test(\"keeps scroll height aligned with content bottom after word-wrap resize\", async () => {\n    resizeRenderer(104, 34)\n    await renderOnce()\n\n    const tableContent: TextTableContent = [\n      [[bold(\"Key\")], [bold(\"Value\")]],\n      [\n        cell(\"alpha\"),\n        cell(\n          \"word wrapping should preserve intrinsic table height even when parent measure passes provide a smaller at-most height\",\n        ),\n      ],\n      [\n        cell(\"beta\"),\n        cell(\n          \"this row is intentionally verbose and pushes the wrapped table height so that scrolling must include all visual lines\",\n        ),\n      ],\n      [cell(\"marker\"), cell(\"ENDWORD\")],\n    ]\n\n    const root = new BoxRenderable(renderer, {\n      width: \"100%\",\n      height: \"100%\",\n      flexDirection: \"column\",\n      padding: 1,\n      gap: 1,\n    })\n\n    const scrollBox = new ScrollBoxRenderable(renderer, {\n      width: \"100%\",\n      flexGrow: 1,\n      flexShrink: 1,\n      scrollY: true,\n      scrollX: false,\n      border: false,\n      contentOptions: {\n        flexDirection: \"column\",\n        gap: 1,\n      },\n    })\n\n    const table = new TextTableRenderable(renderer, {\n      width: \"100%\",\n      wrapMode: \"word\",\n      content: tableContent,\n    })\n\n    root.add(scrollBox)\n    root.add(\n      new BoxRenderable(renderer, {\n        width: \"100%\",\n        height: 16,\n        flexGrow: 0,\n        flexShrink: 0,\n      }),\n    )\n\n    scrollBox.add(new TextRenderable(renderer, { content: \"Word Wrap Table\", selectable: false }))\n    scrollBox.add(table)\n    renderer.root.add(root)\n\n    await renderOnce()\n\n    resizeRenderer(66, 34)\n    await renderOnce()\n    await renderOnce()\n\n    const contentBottom = getScrollContentBottom(scrollBox)\n    expect(contentBottom).toBeGreaterThan(scrollBox.viewport.height)\n    expect(scrollBox.scrollHeight).toBe(contentBottom)\n\n    scrollBox.scrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n    await renderOnce()\n\n    expect(captureFrame()).toContain(\"ENDWORD\")\n  })\n\n  test(\"keeps scroll height aligned with content bottom in char-wrap full mode\", async () => {\n    resizeRenderer(104, 34)\n    await renderOnce()\n\n    const tableContent: TextTableContent = [\n      [[bold(\"Name\")], [bold(\"Payload\")]],\n      [cell(\"row-1\"), cell(\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\")],\n      [cell(\"row-2\"), cell(\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\")],\n      [cell(\"row-3\"), cell(\"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\")],\n      [cell(\"marker\"), cell(\"ENDCHAR\")],\n    ]\n\n    const root = new BoxRenderable(renderer, {\n      width: \"100%\",\n      height: \"100%\",\n      flexDirection: \"column\",\n      padding: 1,\n      gap: 1,\n    })\n\n    const scrollBox = new ScrollBoxRenderable(renderer, {\n      width: \"100%\",\n      flexGrow: 1,\n      flexShrink: 1,\n      scrollY: true,\n      scrollX: false,\n      border: false,\n      contentOptions: {\n        flexDirection: \"column\",\n        gap: 1,\n      },\n    })\n\n    const table = new TextTableRenderable(renderer, {\n      width: \"100%\",\n      wrapMode: \"char\",\n      columnWidthMode: \"full\",\n      content: tableContent,\n    })\n\n    root.add(scrollBox)\n    root.add(\n      new BoxRenderable(renderer, {\n        width: \"100%\",\n        height: 16,\n        flexGrow: 0,\n        flexShrink: 0,\n      }),\n    )\n\n    scrollBox.add(new TextRenderable(renderer, { content: \"Char Wrap Fill Table\", selectable: false }))\n    scrollBox.add(table)\n    renderer.root.add(root)\n\n    await renderOnce()\n\n    resizeRenderer(58, 34)\n    await renderOnce()\n    await renderOnce()\n\n    const contentBottom = getScrollContentBottom(scrollBox)\n    expect(contentBottom).toBeGreaterThan(scrollBox.viewport.height)\n    expect(scrollBox.scrollHeight).toBe(contentBottom)\n\n    scrollBox.scrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n    await renderOnce()\n\n    expect(captureFrame()).toContain(\"ENDCHAR\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/TextTable.ts",
    "content": "import { MeasureMode } from \"yoga-layout\"\nimport { type RenderableOptions, Renderable } from \"../Renderable\"\nimport type { OptimizedBuffer } from \"../buffer\"\nimport { type BorderStyle, BorderCharArrays, parseBorderStyle } from \"../lib/border\"\nimport { convertGlobalToLocalSelection, type Selection, type LocalSelectionBounds } from \"../lib/selection\"\nimport { StyledText, stringToStyledText } from \"../lib/styled-text\"\nimport { RGBA, parseColor, type ColorInput } from \"../lib/RGBA\"\nimport { SyntaxStyle } from \"../syntax-style\"\nimport { type TextChunk, TextBuffer } from \"../text-buffer\"\nimport { TextBufferView } from \"../text-buffer-view\"\nimport type { RenderContext } from \"../types\"\n\n// Large sentinel height for text measurement. The Zig measure path currently\n// ignores height, but we pass an effectively unbounded value so if height-aware\n// measuring is introduced later, table sizing remains stable.\nconst MEASURE_HEIGHT = 10_000\n\nexport type TextTableCellContent = TextChunk[] | null | undefined\nexport type TextTableContent = TextTableCellContent[][]\nexport type TextTableColumnWidthMode = \"content\" | \"full\"\nexport type TextTableColumnFitter = \"proportional\" | \"balanced\"\n\ninterface ResolvedTableBorderLayout {\n  left: boolean\n  right: boolean\n  top: boolean\n  bottom: boolean\n  innerVertical: boolean\n  innerHorizontal: boolean\n}\n\ninterface TextTableCellState {\n  textBuffer: TextBuffer\n  textBufferView: TextBufferView\n  syntaxStyle: SyntaxStyle\n}\n\ninterface TextTableLayout {\n  columnWidths: number[]\n  rowHeights: number[]\n  columnOffsets: number[]\n  rowOffsets: number[]\n  columnOffsetsI32: Int32Array\n  rowOffsetsI32: Int32Array\n  tableWidth: number\n  tableHeight: number\n}\n\ninterface CellPosition {\n  rowIdx: number\n  colIdx: number\n}\n\ninterface RowRange {\n  firstRow: number\n  lastRow: number\n}\n\ntype TableSelectionMode = \"single-cell\" | \"column-locked\" | \"grid\"\n\ninterface SelectionResolution {\n  mode: TableSelectionMode\n  anchorCell: CellPosition | null\n  anchorColumn: number | null\n}\n\ninterface CellSelectionCoords {\n  anchorX: number\n  anchorY: number\n  focusX: number\n  focusY: number\n}\n\nexport interface TextTableOptions extends RenderableOptions<TextTableRenderable> {\n  content?: TextTableContent\n  wrapMode?: \"none\" | \"char\" | \"word\"\n  columnWidthMode?: TextTableColumnWidthMode\n  columnFitter?: TextTableColumnFitter\n  cellPadding?: number\n  showBorders?: boolean\n  border?: boolean\n  outerBorder?: boolean\n  selectable?: boolean\n  selectionBg?: ColorInput\n  selectionFg?: ColorInput\n  borderStyle?: BorderStyle\n  borderColor?: ColorInput\n  borderBackgroundColor?: ColorInput\n  backgroundColor?: ColorInput\n  fg?: ColorInput\n  bg?: ColorInput\n  attributes?: number\n}\n\nexport class TextTableRenderable extends Renderable {\n  private _content: TextTableContent\n  private _wrapMode: \"none\" | \"char\" | \"word\"\n  private _columnWidthMode: TextTableColumnWidthMode\n  private _columnFitter: TextTableColumnFitter\n  private _cellPadding: number\n  private _showBorders: boolean\n  private _border: boolean\n  private _outerBorder: boolean\n  private _hasExplicitOuterBorder: boolean\n  private _borderStyle: BorderStyle\n  private _borderColor: RGBA\n  private _borderBackgroundColor: RGBA\n  private _backgroundColor: RGBA\n  private _defaultFg: RGBA\n  private _defaultBg: RGBA\n  private _defaultAttributes: number\n  private _selectionBg: RGBA | undefined\n  private _selectionFg: RGBA | undefined\n  private _lastLocalSelection: LocalSelectionBounds | null = null\n  private _lastSelectionMode: TableSelectionMode | null = null\n\n  private _cells: TextTableCellState[][] = []\n  private _prevCellContent: TextTableCellContent[][] = []\n  private _rowCount: number = 0\n  private _columnCount: number = 0\n\n  private _layout: TextTableLayout = this.createEmptyLayout()\n  private _layoutDirty: boolean = true\n  private _rasterDirty: boolean = true\n\n  private _cachedMeasureLayout: TextTableLayout | null = null\n  private _cachedMeasureWidth: number | undefined = undefined\n\n  private readonly _defaultOptions = {\n    content: [] as TextTableContent,\n    wrapMode: \"word\" as \"none\" | \"char\" | \"word\",\n    columnWidthMode: \"full\" as TextTableColumnWidthMode,\n    columnFitter: \"proportional\" as TextTableColumnFitter,\n    cellPadding: 0,\n    showBorders: true,\n    border: true,\n    outerBorder: true,\n    selectable: true,\n    selectionBg: undefined as ColorInput | undefined,\n    selectionFg: undefined as ColorInput | undefined,\n    borderStyle: \"single\" as BorderStyle,\n    borderColor: \"#FFFFFF\",\n    borderBackgroundColor: \"transparent\",\n    backgroundColor: \"transparent\",\n    fg: \"#FFFFFF\",\n    bg: \"transparent\",\n    attributes: 0,\n  } satisfies Partial<TextTableOptions>\n\n  constructor(ctx: RenderContext, options: TextTableOptions = {}) {\n    super(ctx, { ...options, flexShrink: options.flexShrink ?? 0, buffered: true })\n\n    this._content = options.content ?? this._defaultOptions.content\n    this._wrapMode = options.wrapMode ?? this._defaultOptions.wrapMode\n    this._columnWidthMode = options.columnWidthMode ?? this._defaultOptions.columnWidthMode\n    this._columnFitter = this.resolveColumnFitter(options.columnFitter)\n    this._cellPadding = this.resolveCellPadding(options.cellPadding)\n    this._showBorders = options.showBorders ?? this._defaultOptions.showBorders\n    this._border = options.border ?? this._defaultOptions.border\n    this._hasExplicitOuterBorder = options.outerBorder !== undefined\n    this._outerBorder = options.outerBorder ?? this._border\n    this.selectable = options.selectable ?? this._defaultOptions.selectable\n    this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined\n    this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined\n    this._borderStyle = parseBorderStyle(options.borderStyle, this._defaultOptions.borderStyle)\n    this._borderColor = parseColor(options.borderColor ?? this._defaultOptions.borderColor)\n    this._borderBackgroundColor = parseColor(\n      options.borderBackgroundColor ?? this._defaultOptions.borderBackgroundColor,\n    )\n    this._backgroundColor = parseColor(options.backgroundColor ?? this._defaultOptions.backgroundColor)\n    this._defaultFg = parseColor(options.fg ?? this._defaultOptions.fg)\n    this._defaultBg = parseColor(options.bg ?? this._defaultOptions.bg)\n    this._defaultAttributes = options.attributes ?? this._defaultOptions.attributes\n\n    this.setupMeasureFunc()\n    this.rebuildCells()\n  }\n\n  public get content(): TextTableContent {\n    return this._content\n  }\n\n  public set content(value: TextTableContent) {\n    this._content = value ?? []\n    this.rebuildCells()\n  }\n\n  public get wrapMode(): \"none\" | \"char\" | \"word\" {\n    return this._wrapMode\n  }\n\n  public set wrapMode(value: \"none\" | \"char\" | \"word\") {\n    if (this._wrapMode === value) return\n    this._wrapMode = value\n    for (const row of this._cells) {\n      for (const cell of row) {\n        cell.textBufferView.setWrapMode(value)\n      }\n    }\n    this.invalidateLayoutAndRaster()\n  }\n\n  public get columnWidthMode(): TextTableColumnWidthMode {\n    return this._columnWidthMode\n  }\n\n  public set columnWidthMode(value: TextTableColumnWidthMode) {\n    if (this._columnWidthMode === value) return\n    this._columnWidthMode = value\n    this.invalidateLayoutAndRaster()\n  }\n\n  public get columnFitter(): TextTableColumnFitter {\n    return this._columnFitter\n  }\n\n  public set columnFitter(value: TextTableColumnFitter) {\n    const next = this.resolveColumnFitter(value)\n    if (this._columnFitter === next) return\n    this._columnFitter = next\n    this.invalidateLayoutAndRaster()\n  }\n\n  public get cellPadding(): number {\n    return this._cellPadding\n  }\n\n  public set cellPadding(value: number) {\n    const next = this.resolveCellPadding(value)\n    if (this._cellPadding === next) return\n    this._cellPadding = next\n    this.invalidateLayoutAndRaster()\n  }\n\n  public get showBorders(): boolean {\n    return this._showBorders\n  }\n\n  public set showBorders(value: boolean) {\n    if (this._showBorders === value) return\n    this._showBorders = value\n    this.invalidateRasterOnly()\n  }\n\n  public get outerBorder(): boolean {\n    return this._outerBorder\n  }\n\n  public set outerBorder(value: boolean) {\n    if (this._outerBorder === value) return\n\n    this._hasExplicitOuterBorder = true\n    this._outerBorder = value\n    this.invalidateLayoutAndRaster()\n  }\n\n  public get border(): boolean {\n    return this._border\n  }\n\n  public set border(value: boolean) {\n    if (this._border === value) return\n\n    this._border = value\n\n    if (!this._hasExplicitOuterBorder) {\n      this._outerBorder = value\n    }\n\n    this.invalidateLayoutAndRaster()\n  }\n\n  public get borderStyle(): BorderStyle {\n    return this._borderStyle\n  }\n\n  public set borderStyle(value: BorderStyle) {\n    const next = parseBorderStyle(value, this._defaultOptions.borderStyle)\n    if (this._borderStyle === next) return\n    this._borderStyle = next\n    this.invalidateRasterOnly()\n  }\n\n  public get borderColor(): RGBA {\n    return this._borderColor\n  }\n\n  public set borderColor(value: ColorInput) {\n    const next = parseColor(value)\n    if (this._borderColor === next) return\n    this._borderColor = next\n    this.invalidateRasterOnly()\n  }\n\n  public shouldStartSelection(x: number, y: number): boolean {\n    if (!this.selectable) return false\n\n    this.ensureLayoutReady()\n\n    const localX = x - this.x\n    const localY = y - this.y\n    return this.getCellAtLocalPosition(localX, localY) !== null\n  }\n\n  public onSelectionChanged(selection: Selection | null): boolean {\n    this.ensureLayoutReady()\n\n    const previousLocalSelection = this._lastLocalSelection\n    const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y)\n    this._lastLocalSelection = localSelection\n    const dirtyRows = this.getDirtySelectionRowRange(previousLocalSelection, localSelection)\n\n    if (!localSelection?.isActive) {\n      this.resetCellSelections()\n      this._lastSelectionMode = null\n    } else {\n      this.applySelectionToCells(localSelection, selection?.isStart ?? false)\n    }\n\n    if (dirtyRows !== null) {\n      this.redrawSelectionRows(dirtyRows.firstRow, dirtyRows.lastRow)\n    }\n\n    return this.hasSelection()\n  }\n\n  public hasSelection(): boolean {\n    for (const row of this._cells) {\n      for (const cell of row) {\n        if (cell.textBufferView.hasSelection()) {\n          return true\n        }\n      }\n    }\n\n    return false\n  }\n\n  public getSelection(): { start: number; end: number } | null {\n    for (const row of this._cells) {\n      for (const cell of row) {\n        const selection = cell.textBufferView.getSelection()\n        if (selection) {\n          return selection\n        }\n      }\n    }\n\n    return null\n  }\n\n  public getSelectedText(): string {\n    const selectedRows: string[] = []\n\n    for (let rowIdx = 0; rowIdx < this._rowCount; rowIdx++) {\n      const rowSelections: string[] = []\n\n      for (let colIdx = 0; colIdx < this._columnCount; colIdx++) {\n        const cell = this._cells[rowIdx]?.[colIdx]\n        if (!cell || !cell.textBufferView.hasSelection()) continue\n\n        const selectedText = cell.textBufferView.getSelectedText()\n        if (selectedText.length > 0) {\n          rowSelections.push(selectedText)\n        }\n      }\n\n      if (rowSelections.length > 0) {\n        selectedRows.push(rowSelections.join(\"\\t\"))\n      }\n    }\n\n    return selectedRows.join(\"\\n\")\n  }\n\n  protected onResize(width: number, height: number): void {\n    this.invalidateLayoutAndRaster(false)\n    super.onResize(width, height)\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    if (!this.visible || this.isDestroyed) return\n\n    if (this._layoutDirty) {\n      this.rebuildLayoutForCurrentWidth()\n    }\n\n    if (!this._rasterDirty) return\n\n    buffer.clear(this._backgroundColor)\n\n    if (this._rowCount === 0 || this._columnCount === 0) {\n      this._rasterDirty = false\n      return\n    }\n\n    this.drawBorders(buffer)\n    this.drawCells(buffer)\n\n    this._rasterDirty = false\n  }\n\n  protected destroySelf(): void {\n    this.destroyCells()\n    super.destroySelf()\n  }\n\n  private setupMeasureFunc(): void {\n    const measureFunc = (\n      width: number,\n      widthMode: MeasureMode,\n      _height: number,\n      _heightMode: MeasureMode,\n    ): { width: number; height: number } => {\n      const hasWidthConstraint = widthMode !== MeasureMode.Undefined && Number.isFinite(width)\n      const rawWidthConstraint = hasWidthConstraint ? Math.max(1, Math.floor(width)) : undefined\n      const widthConstraint = this.resolveLayoutWidthConstraint(rawWidthConstraint)\n      const measuredLayout = this.computeLayout(widthConstraint)\n      this._cachedMeasureLayout = measuredLayout\n      this._cachedMeasureWidth = widthConstraint\n\n      let measuredWidth = measuredLayout.tableWidth > 0 ? measuredLayout.tableWidth : 1\n      let measuredHeight = measuredLayout.tableHeight > 0 ? measuredLayout.tableHeight : 1\n\n      if (widthMode === MeasureMode.AtMost && rawWidthConstraint !== undefined && this._positionType !== \"absolute\") {\n        measuredWidth = Math.min(rawWidthConstraint, measuredWidth)\n      }\n\n      // Keep intrinsic height even under AtMost constraints. Clamping here can under-report\n      // content height during Yoga measure passes and leave parent scroll extents stale.\n      return {\n        width: measuredWidth,\n        height: measuredHeight,\n      }\n    }\n\n    this.yogaNode.setMeasureFunc(measureFunc)\n  }\n\n  private rebuildCells(): void {\n    const newRowCount = this._content.length\n    const newColumnCount = this._content.reduce((max, row) => Math.max(max, row.length), 0)\n\n    if (this._cells.length === 0) {\n      this._rowCount = newRowCount\n      this._columnCount = newColumnCount\n      this._cells = []\n      this._prevCellContent = []\n\n      for (let rowIdx = 0; rowIdx < newRowCount; rowIdx++) {\n        const row = this._content[rowIdx] ?? []\n        const rowCells: TextTableCellState[] = []\n        const rowRefs: TextTableCellContent[] = []\n\n        for (let colIdx = 0; colIdx < newColumnCount; colIdx++) {\n          const cellContent = row[colIdx]\n          rowCells.push(this.createCell(cellContent))\n          rowRefs.push(cellContent)\n        }\n\n        this._cells.push(rowCells)\n        this._prevCellContent.push(rowRefs)\n      }\n\n      this.invalidateLayoutAndRaster()\n      return\n    }\n\n    this.updateCellsDiff(newRowCount, newColumnCount)\n    this.invalidateLayoutAndRaster()\n  }\n\n  private updateCellsDiff(newRowCount: number, newColumnCount: number): void {\n    const oldRowCount = this._rowCount\n    const oldColumnCount = this._columnCount\n    const keepRows = Math.min(oldRowCount, newRowCount)\n    const keepCols = Math.min(oldColumnCount, newColumnCount)\n\n    for (let rowIdx = 0; rowIdx < keepRows; rowIdx++) {\n      const newRow = this._content[rowIdx] ?? []\n      const cellRow = this._cells[rowIdx]\n      const refRow = this._prevCellContent[rowIdx]\n\n      for (let colIdx = 0; colIdx < keepCols; colIdx++) {\n        const cellContent = newRow[colIdx]\n        if (cellContent === refRow[colIdx]) continue\n\n        const oldCell = cellRow[colIdx]\n        oldCell.textBufferView.destroy()\n        oldCell.textBuffer.destroy()\n        oldCell.syntaxStyle.destroy()\n\n        cellRow[colIdx] = this.createCell(cellContent)\n        refRow[colIdx] = cellContent\n      }\n\n      if (newColumnCount > oldColumnCount) {\n        for (let colIdx = oldColumnCount; colIdx < newColumnCount; colIdx++) {\n          const cellContent = newRow[colIdx]\n          cellRow.push(this.createCell(cellContent))\n          refRow.push(cellContent)\n        }\n      } else if (newColumnCount < oldColumnCount) {\n        for (let colIdx = newColumnCount; colIdx < oldColumnCount; colIdx++) {\n          const cell = cellRow[colIdx]\n          cell.textBufferView.destroy()\n          cell.textBuffer.destroy()\n          cell.syntaxStyle.destroy()\n        }\n        cellRow.length = newColumnCount\n        refRow.length = newColumnCount\n      }\n    }\n\n    if (newRowCount > oldRowCount) {\n      for (let rowIdx = oldRowCount; rowIdx < newRowCount; rowIdx++) {\n        const newRow = this._content[rowIdx] ?? []\n        const rowCells: TextTableCellState[] = []\n        const rowRefs: TextTableCellContent[] = []\n\n        for (let colIdx = 0; colIdx < newColumnCount; colIdx++) {\n          const cellContent = newRow[colIdx]\n          rowCells.push(this.createCell(cellContent))\n          rowRefs.push(cellContent)\n        }\n\n        this._cells.push(rowCells)\n        this._prevCellContent.push(rowRefs)\n      }\n    } else if (newRowCount < oldRowCount) {\n      for (let rowIdx = newRowCount; rowIdx < oldRowCount; rowIdx++) {\n        const row = this._cells[rowIdx]\n        for (const cell of row) {\n          cell.textBufferView.destroy()\n          cell.textBuffer.destroy()\n          cell.syntaxStyle.destroy()\n        }\n      }\n      this._cells.length = newRowCount\n      this._prevCellContent.length = newRowCount\n    }\n\n    this._rowCount = newRowCount\n    this._columnCount = newColumnCount\n  }\n\n  private createCell(content: TextTableCellContent): TextTableCellState {\n    const styledText = this.toStyledText(content)\n    const textBuffer = TextBuffer.create(this._ctx.widthMethod)\n    const syntaxStyle = SyntaxStyle.create()\n\n    textBuffer.setDefaultFg(this._defaultFg)\n    textBuffer.setDefaultBg(this._defaultBg)\n    textBuffer.setDefaultAttributes(this._defaultAttributes)\n    textBuffer.setSyntaxStyle(syntaxStyle)\n    textBuffer.setStyledText(styledText)\n\n    const textBufferView = TextBufferView.create(textBuffer)\n    textBufferView.setWrapMode(this._wrapMode)\n\n    return { textBuffer, textBufferView, syntaxStyle }\n  }\n\n  private toStyledText(content: TextTableCellContent): StyledText {\n    if (Array.isArray(content)) {\n      return new StyledText(content)\n    }\n\n    if (content === null || content === undefined) {\n      return stringToStyledText(\"\")\n    }\n\n    return stringToStyledText(String(content))\n  }\n\n  private destroyCells(): void {\n    for (const row of this._cells) {\n      for (const cell of row) {\n        cell.textBufferView.destroy()\n        cell.textBuffer.destroy()\n        cell.syntaxStyle.destroy()\n      }\n    }\n\n    this._cells = []\n    this._prevCellContent = []\n    this._rowCount = 0\n    this._columnCount = 0\n    this._layout = this.createEmptyLayout()\n  }\n\n  private rebuildLayoutForCurrentWidth(): void {\n    const maxTableWidth = this.resolveLayoutWidthConstraint(this.width)\n\n    let layout: TextTableLayout\n    if (this._cachedMeasureLayout !== null && this._cachedMeasureWidth === maxTableWidth) {\n      layout = this._cachedMeasureLayout\n    } else {\n      layout = this.computeLayout(maxTableWidth)\n    }\n    this._cachedMeasureLayout = null\n    this._cachedMeasureWidth = undefined\n\n    this._layout = layout\n    this.applyLayoutToViews(layout)\n    this._layoutDirty = false\n\n    if (this._lastLocalSelection?.isActive) {\n      this.applySelectionToCells(this._lastLocalSelection, true)\n    }\n  }\n\n  private computeLayout(maxTableWidth?: number): TextTableLayout {\n    if (this._rowCount === 0 || this._columnCount === 0) {\n      return this.createEmptyLayout()\n    }\n\n    const borderLayout = this.resolveBorderLayout()\n    const columnWidths = this.computeColumnWidths(maxTableWidth, borderLayout)\n    const rowHeights = this.computeRowHeights(columnWidths)\n    const columnOffsets = this.computeOffsets(\n      columnWidths,\n      borderLayout.left,\n      borderLayout.right,\n      borderLayout.innerVertical,\n    )\n    const rowOffsets = this.computeOffsets(\n      rowHeights,\n      borderLayout.top,\n      borderLayout.bottom,\n      borderLayout.innerHorizontal,\n    )\n    return {\n      columnWidths,\n      rowHeights,\n      columnOffsets,\n      rowOffsets,\n      columnOffsetsI32: new Int32Array(columnOffsets),\n      rowOffsetsI32: new Int32Array(rowOffsets),\n      tableWidth: (columnOffsets[columnOffsets.length - 1] ?? 0) + 1,\n      tableHeight: (rowOffsets[rowOffsets.length - 1] ?? 0) + 1,\n    }\n  }\n\n  private isFullWidthMode(): boolean {\n    return this._columnWidthMode === \"full\"\n  }\n\n  private computeColumnWidths(maxTableWidth: number | undefined, borderLayout: ResolvedTableBorderLayout): number[] {\n    const horizontalPadding = this.getHorizontalCellPadding()\n    const intrinsicWidths = new Array(this._columnCount).fill(1 + horizontalPadding)\n\n    for (let rowIdx = 0; rowIdx < this._rowCount; rowIdx++) {\n      for (let colIdx = 0; colIdx < this._columnCount; colIdx++) {\n        const cell = this._cells[rowIdx]?.[colIdx]\n        if (!cell) continue\n\n        const measure = cell.textBufferView.measureForDimensions(0, MEASURE_HEIGHT)\n        const measuredWidth = Math.max(1, measure?.widthColsMax ?? 0) + horizontalPadding\n        intrinsicWidths[colIdx] = Math.max(intrinsicWidths[colIdx], measuredWidth)\n      }\n    }\n\n    if (maxTableWidth === undefined || !Number.isFinite(maxTableWidth) || maxTableWidth <= 0) {\n      return intrinsicWidths\n    }\n\n    const maxContentWidth = Math.max(1, Math.floor(maxTableWidth) - this.getVerticalBorderCount(borderLayout))\n    const currentWidth = intrinsicWidths.reduce((sum, width) => sum + width, 0)\n\n    if (currentWidth === maxContentWidth) {\n      return intrinsicWidths\n    }\n\n    if (currentWidth < maxContentWidth) {\n      if (this.isFullWidthMode()) {\n        return this.expandColumnWidths(intrinsicWidths, maxContentWidth)\n      }\n\n      return intrinsicWidths\n    }\n\n    if (this._wrapMode === \"none\") {\n      return intrinsicWidths\n    }\n\n    return this.fitColumnWidths(intrinsicWidths, maxContentWidth)\n  }\n\n  private expandColumnWidths(widths: number[], targetContentWidth: number): number[] {\n    const baseWidths = widths.map((width) => Math.max(1, Math.floor(width)))\n    const totalBaseWidth = baseWidths.reduce((sum, width) => sum + width, 0)\n\n    if (totalBaseWidth >= targetContentWidth) {\n      return baseWidths\n    }\n\n    const expanded = [...baseWidths]\n    const columns = expanded.length\n    const extraWidth = targetContentWidth - totalBaseWidth\n    const sharedWidth = Math.floor(extraWidth / columns)\n    const remainder = extraWidth % columns\n\n    for (let idx = 0; idx < columns; idx++) {\n      expanded[idx] += sharedWidth\n      if (idx < remainder) {\n        expanded[idx] += 1\n      }\n    }\n\n    return expanded\n  }\n\n  private fitColumnWidths(widths: number[], targetContentWidth: number): number[] {\n    if (this._columnFitter === \"balanced\") {\n      return this.fitColumnWidthsBalanced(widths, targetContentWidth)\n    }\n\n    return this.fitColumnWidthsProportional(widths, targetContentWidth)\n  }\n\n  private fitColumnWidthsProportional(widths: number[], targetContentWidth: number): number[] {\n    const minWidth = 1 + this.getHorizontalCellPadding()\n    const hardMinWidths = new Array(widths.length).fill(minWidth)\n    const baseWidths = widths.map((width) => Math.max(1, Math.floor(width)))\n\n    const preferredMinWidths = baseWidths.map((width) => Math.min(width, minWidth + 1))\n    const preferredMinTotal = preferredMinWidths.reduce((sum, width) => sum + width, 0)\n\n    const floorWidths = preferredMinTotal <= targetContentWidth ? preferredMinWidths : hardMinWidths\n    const floorTotal = floorWidths.reduce((sum, width) => sum + width, 0)\n    const clampedTarget = Math.max(floorTotal, targetContentWidth)\n\n    const totalBaseWidth = baseWidths.reduce((sum, width) => sum + width, 0)\n\n    if (totalBaseWidth <= clampedTarget) {\n      return baseWidths\n    }\n\n    const shrinkable = baseWidths.map((width, idx) => width - floorWidths[idx])\n    const totalShrinkable = shrinkable.reduce((sum, value) => sum + value, 0)\n    if (totalShrinkable <= 0) {\n      return [...floorWidths]\n    }\n\n    const targetShrink = totalBaseWidth - clampedTarget\n    const integerShrink = new Array(baseWidths.length).fill(0)\n    const fractions = new Array(baseWidths.length).fill(0)\n    let usedShrink = 0\n\n    for (let idx = 0; idx < baseWidths.length; idx++) {\n      if (shrinkable[idx] <= 0) continue\n\n      const exact = (shrinkable[idx] / totalShrinkable) * targetShrink\n      const whole = Math.min(shrinkable[idx], Math.floor(exact))\n      integerShrink[idx] = whole\n      fractions[idx] = exact - whole\n      usedShrink += whole\n    }\n\n    let remainingShrink = targetShrink - usedShrink\n\n    while (remainingShrink > 0) {\n      let bestIdx = -1\n      let bestFraction = -1\n\n      for (let idx = 0; idx < baseWidths.length; idx++) {\n        if (shrinkable[idx] - integerShrink[idx] <= 0) continue\n        if (fractions[idx] > bestFraction) {\n          bestFraction = fractions[idx]\n          bestIdx = idx\n        }\n      }\n\n      if (bestIdx === -1) break\n\n      integerShrink[bestIdx] += 1\n      fractions[bestIdx] = 0\n      remainingShrink -= 1\n    }\n\n    return baseWidths.map((width, idx) => Math.max(floorWidths[idx], width - integerShrink[idx]))\n  }\n\n  private fitColumnWidthsBalanced(widths: number[], targetContentWidth: number): number[] {\n    const minWidth = 1 + this.getHorizontalCellPadding()\n    const hardMinWidths = new Array(widths.length).fill(minWidth)\n    const baseWidths = widths.map((width) => Math.max(1, Math.floor(width)))\n    const totalBaseWidth = baseWidths.reduce((sum, width) => sum + width, 0)\n    const columns = baseWidths.length\n\n    if (columns === 0 || totalBaseWidth <= targetContentWidth) {\n      return baseWidths\n    }\n\n    const evenShare = Math.max(minWidth, Math.floor(targetContentWidth / columns))\n    const preferredMinWidths = baseWidths.map((width) => Math.min(width, evenShare))\n    const preferredMinTotal = preferredMinWidths.reduce((sum, width) => sum + width, 0)\n    const floorWidths = preferredMinTotal <= targetContentWidth ? preferredMinWidths : hardMinWidths\n    const floorTotal = floorWidths.reduce((sum, width) => sum + width, 0)\n    const clampedTarget = Math.max(floorTotal, targetContentWidth)\n\n    if (totalBaseWidth <= clampedTarget) {\n      return baseWidths\n    }\n\n    const shrinkable = baseWidths.map((width, idx) => width - floorWidths[idx])\n    const totalShrinkable = shrinkable.reduce((sum, value) => sum + value, 0)\n    if (totalShrinkable <= 0) {\n      return [...floorWidths]\n    }\n\n    const targetShrink = totalBaseWidth - clampedTarget\n    const shrink = this.allocateShrinkByWeight(shrinkable, targetShrink, \"sqrt\")\n\n    return baseWidths.map((width, idx) => Math.max(floorWidths[idx], width - shrink[idx]))\n  }\n\n  private allocateShrinkByWeight(shrinkable: number[], targetShrink: number, mode: \"linear\" | \"sqrt\"): number[] {\n    const shrink = new Array(shrinkable.length).fill(0)\n\n    if (targetShrink <= 0) {\n      return shrink\n    }\n\n    const weights = shrinkable.map((value) => {\n      if (value <= 0) {\n        return 0\n      }\n\n      return mode === \"sqrt\" ? Math.sqrt(value) : value\n    })\n    const totalWeight = weights.reduce((sum, value) => sum + value, 0)\n\n    if (totalWeight <= 0) {\n      return shrink\n    }\n\n    const fractions = new Array(shrinkable.length).fill(0)\n    let usedShrink = 0\n\n    for (let idx = 0; idx < shrinkable.length; idx++) {\n      if (shrinkable[idx] <= 0 || weights[idx] <= 0) continue\n\n      const exact = (weights[idx] / totalWeight) * targetShrink\n      const whole = Math.min(shrinkable[idx], Math.floor(exact))\n      shrink[idx] = whole\n      fractions[idx] = exact - whole\n      usedShrink += whole\n    }\n\n    let remainingShrink = targetShrink - usedShrink\n\n    while (remainingShrink > 0) {\n      let bestIdx = -1\n      let bestFraction = -1\n\n      for (let idx = 0; idx < shrinkable.length; idx++) {\n        if (shrinkable[idx] - shrink[idx] <= 0) continue\n\n        if (\n          bestIdx === -1 ||\n          fractions[idx] > bestFraction ||\n          (fractions[idx] === bestFraction && shrinkable[idx] > shrinkable[bestIdx])\n        ) {\n          bestIdx = idx\n          bestFraction = fractions[idx]\n        }\n      }\n\n      if (bestIdx === -1) {\n        break\n      }\n\n      shrink[bestIdx] += 1\n      fractions[bestIdx] = 0\n      remainingShrink -= 1\n    }\n\n    return shrink\n  }\n\n  private computeRowHeights(columnWidths: number[]): number[] {\n    const horizontalPadding = this.getHorizontalCellPadding()\n    const verticalPadding = this.getVerticalCellPadding()\n    const rowHeights = new Array(this._rowCount).fill(1 + verticalPadding)\n\n    for (let rowIdx = 0; rowIdx < this._rowCount; rowIdx++) {\n      for (let colIdx = 0; colIdx < this._columnCount; colIdx++) {\n        const cell = this._cells[rowIdx]?.[colIdx]\n        if (!cell) continue\n\n        const width = Math.max(1, (columnWidths[colIdx] ?? 1) - horizontalPadding)\n        const measure = cell.textBufferView.measureForDimensions(width, MEASURE_HEIGHT)\n        const lineCount = Math.max(1, measure?.lineCount ?? 1)\n        rowHeights[rowIdx] = Math.max(rowHeights[rowIdx], lineCount + verticalPadding)\n      }\n    }\n\n    return rowHeights\n  }\n\n  private computeOffsets(\n    parts: number[],\n    startBoundary: boolean,\n    endBoundary: boolean,\n    includeInnerBoundaries: boolean,\n  ): number[] {\n    const offsets: number[] = [startBoundary ? 0 : -1]\n    let cursor = offsets[0] ?? 0\n\n    for (let idx = 0; idx < parts.length; idx++) {\n      const size = parts[idx] ?? 1\n      const hasBoundaryAfter = idx < parts.length - 1 ? includeInnerBoundaries : endBoundary\n      cursor += size + (hasBoundaryAfter ? 1 : 0)\n      offsets.push(cursor)\n    }\n\n    return offsets\n  }\n\n  private applyLayoutToViews(layout: TextTableLayout): void {\n    const horizontalPadding = this.getHorizontalCellPadding()\n    const verticalPadding = this.getVerticalCellPadding()\n\n    for (let rowIdx = 0; rowIdx < this._rowCount; rowIdx++) {\n      for (let colIdx = 0; colIdx < this._columnCount; colIdx++) {\n        const cell = this._cells[rowIdx]?.[colIdx]\n        if (!cell) continue\n\n        const colWidth = layout.columnWidths[colIdx] ?? 1\n        const rowHeight = layout.rowHeights[rowIdx] ?? 1\n        const contentWidth = Math.max(1, colWidth - horizontalPadding)\n        const contentHeight = Math.max(1, rowHeight - verticalPadding)\n\n        if (this._wrapMode === \"none\") {\n          cell.textBufferView.setWrapWidth(null)\n        } else {\n          cell.textBufferView.setWrapWidth(contentWidth)\n        }\n\n        cell.textBufferView.setViewport(0, 0, contentWidth, contentHeight)\n      }\n    }\n  }\n\n  private resolveBorderLayout(): ResolvedTableBorderLayout {\n    return {\n      left: this._outerBorder,\n      right: this._outerBorder,\n      top: this._outerBorder,\n      bottom: this._outerBorder,\n      innerVertical: this._border && this._columnCount > 1,\n      innerHorizontal: this._border && this._rowCount > 1,\n    }\n  }\n\n  private getVerticalBorderCount(borderLayout: ResolvedTableBorderLayout): number {\n    return (\n      (borderLayout.left ? 1 : 0) +\n      (borderLayout.right ? 1 : 0) +\n      (borderLayout.innerVertical ? Math.max(0, this._columnCount - 1) : 0)\n    )\n  }\n\n  private getHorizontalBorderCount(borderLayout: ResolvedTableBorderLayout): number {\n    return (\n      (borderLayout.top ? 1 : 0) +\n      (borderLayout.bottom ? 1 : 0) +\n      (borderLayout.innerHorizontal ? Math.max(0, this._rowCount - 1) : 0)\n    )\n  }\n\n  private drawBorders(buffer: OptimizedBuffer): void {\n    if (!this._showBorders) {\n      return\n    }\n\n    const borderLayout = this.resolveBorderLayout()\n\n    if (this.getVerticalBorderCount(borderLayout) === 0 && this.getHorizontalBorderCount(borderLayout) === 0) {\n      return\n    }\n\n    buffer.drawGrid({\n      borderChars: BorderCharArrays[this._borderStyle],\n      borderFg: this._borderColor,\n      borderBg: this._borderBackgroundColor,\n      columnOffsets: this._layout.columnOffsetsI32,\n      rowOffsets: this._layout.rowOffsetsI32,\n      drawInner: this._border,\n      drawOuter: this._outerBorder,\n    })\n  }\n\n  private drawCells(buffer: OptimizedBuffer): void {\n    this.drawCellRange(buffer, 0, this._rowCount - 1)\n  }\n\n  private drawCellRange(buffer: OptimizedBuffer, firstRow: number, lastRow: number): void {\n    const colOffsets = this._layout.columnOffsets\n    const rowOffsets = this._layout.rowOffsets\n    const cellPadding = this._cellPadding\n\n    for (let rowIdx = firstRow; rowIdx <= lastRow; rowIdx++) {\n      const cellY = (rowOffsets[rowIdx] ?? 0) + 1 + cellPadding\n\n      for (let colIdx = 0; colIdx < this._columnCount; colIdx++) {\n        const cell = this._cells[rowIdx]?.[colIdx]\n        if (!cell) continue\n        buffer.drawTextBuffer(cell.textBufferView, (colOffsets[colIdx] ?? 0) + 1 + cellPadding, cellY)\n      }\n    }\n  }\n\n  private redrawSelectionRows(firstRow: number, lastRow: number): void {\n    if (firstRow > lastRow) return\n\n    if (this._backgroundColor.a < 1) {\n      this.invalidateRasterOnly()\n      return\n    }\n\n    const buffer = this.frameBuffer\n    if (!buffer) return\n\n    this.clearCellRange(buffer, firstRow, lastRow)\n    this.drawCellRange(buffer, firstRow, lastRow)\n    this.requestRender()\n  }\n\n  private clearCellRange(buffer: OptimizedBuffer, firstRow: number, lastRow: number): void {\n    const colWidths = this._layout.columnWidths\n    const rowHeights = this._layout.rowHeights\n    const colOffsets = this._layout.columnOffsets\n    const rowOffsets = this._layout.rowOffsets\n\n    for (let rowIdx = firstRow; rowIdx <= lastRow; rowIdx++) {\n      const cellY = (rowOffsets[rowIdx] ?? 0) + 1\n      const rowHeight = rowHeights[rowIdx] ?? 1\n\n      for (let colIdx = 0; colIdx < this._columnCount; colIdx++) {\n        const cellX = (colOffsets[colIdx] ?? 0) + 1\n        const colWidth = colWidths[colIdx] ?? 1\n        buffer.fillRect(cellX, cellY, colWidth, rowHeight, this._backgroundColor)\n      }\n    }\n  }\n\n  private ensureLayoutReady(): void {\n    if (!this._layoutDirty) return\n    this.rebuildLayoutForCurrentWidth()\n  }\n\n  private getCellAtLocalPosition(localX: number, localY: number): CellPosition | null {\n    if (this._rowCount === 0 || this._columnCount === 0) return null\n    if (localX < 0 || localY < 0 || localX >= this._layout.tableWidth || localY >= this._layout.tableHeight) {\n      return null\n    }\n\n    let rowIdx = -1\n    for (let idx = 0; idx < this._rowCount; idx++) {\n      const top = (this._layout.rowOffsets[idx] ?? 0) + 1\n      const bottom = top + (this._layout.rowHeights[idx] ?? 1) - 1\n      if (localY >= top && localY <= bottom) {\n        rowIdx = idx\n        break\n      }\n    }\n\n    if (rowIdx < 0) return null\n\n    let colIdx = -1\n    for (let idx = 0; idx < this._columnCount; idx++) {\n      const left = (this._layout.columnOffsets[idx] ?? 0) + 1\n      const right = left + (this._layout.columnWidths[idx] ?? 1) - 1\n      if (localX >= left && localX <= right) {\n        colIdx = idx\n        break\n      }\n    }\n\n    if (colIdx < 0) return null\n\n    return { rowIdx, colIdx }\n  }\n\n  private applySelectionToCells(localSelection: LocalSelectionBounds, isStart: boolean): void {\n    const minSelY = Math.min(localSelection.anchorY, localSelection.focusY)\n    const maxSelY = Math.max(localSelection.anchorY, localSelection.focusY)\n\n    const firstRow = this.findRowForLocalY(minSelY)\n    const lastRow = this.findRowForLocalY(maxSelY)\n    const selection = this.resolveSelectionResolution(localSelection)\n    const modeChanged = this._lastSelectionMode !== selection.mode\n    this._lastSelectionMode = selection.mode\n    const lockToAnchorColumn = selection.mode === \"column-locked\" && selection.anchorColumn !== null\n\n    for (let rowIdx = 0; rowIdx < this._rowCount; rowIdx++) {\n      if (rowIdx < firstRow || rowIdx > lastRow) {\n        this.resetRowSelection(rowIdx)\n        continue\n      }\n\n      const cellTop = (this._layout.rowOffsets[rowIdx] ?? 0) + 1 + this._cellPadding\n\n      for (let colIdx = 0; colIdx < this._columnCount; colIdx++) {\n        const cell = this._cells[rowIdx]?.[colIdx]\n        if (!cell) continue\n\n        if (lockToAnchorColumn && colIdx !== selection.anchorColumn) {\n          cell.textBufferView.resetLocalSelection()\n          continue\n        }\n\n        const cellLeft = (this._layout.columnOffsets[colIdx] ?? 0) + 1 + this._cellPadding\n        let coords: CellSelectionCoords = {\n          anchorX: localSelection.anchorX - cellLeft,\n          anchorY: localSelection.anchorY - cellTop,\n          focusX: localSelection.focusX - cellLeft,\n          focusY: localSelection.focusY - cellTop,\n        }\n\n        const isAnchorCell =\n          selection.anchorCell !== null &&\n          selection.anchorCell.rowIdx === rowIdx &&\n          selection.anchorCell.colIdx === colIdx\n        const forceSet = isAnchorCell && selection.mode !== \"single-cell\"\n\n        if (forceSet) {\n          coords = this.getFullCellSelectionCoords(rowIdx, colIdx)\n        }\n\n        const shouldUseSet = isStart || modeChanged || forceSet\n\n        if (shouldUseSet) {\n          cell.textBufferView.setLocalSelection(\n            coords.anchorX,\n            coords.anchorY,\n            coords.focusX,\n            coords.focusY,\n            this._selectionBg,\n            this._selectionFg,\n          )\n        } else {\n          cell.textBufferView.updateLocalSelection(\n            coords.anchorX,\n            coords.anchorY,\n            coords.focusX,\n            coords.focusY,\n            this._selectionBg,\n            this._selectionFg,\n          )\n        }\n      }\n    }\n  }\n\n  private resolveSelectionResolution(localSelection: LocalSelectionBounds): SelectionResolution {\n    const anchorCell = this.getCellAtLocalPosition(localSelection.anchorX, localSelection.anchorY)\n    const focusCell = this.getCellAtLocalPosition(localSelection.focusX, localSelection.focusY)\n    const anchorColumn = anchorCell?.colIdx ?? this.getColumnAtLocalX(localSelection.anchorX)\n\n    if (\n      anchorCell !== null &&\n      focusCell !== null &&\n      anchorCell.rowIdx === focusCell.rowIdx &&\n      anchorCell.colIdx === focusCell.colIdx\n    ) {\n      return {\n        mode: \"single-cell\",\n        anchorCell,\n        anchorColumn,\n      }\n    }\n\n    const focusColumn = this.getColumnAtLocalX(localSelection.focusX)\n    if (anchorColumn !== null && focusColumn === anchorColumn) {\n      return {\n        mode: \"column-locked\",\n        anchorCell,\n        anchorColumn,\n      }\n    }\n\n    return {\n      mode: \"grid\",\n      anchorCell,\n      anchorColumn,\n    }\n  }\n\n  private getColumnAtLocalX(localX: number): number | null {\n    if (this._columnCount === 0) return null\n    if (localX < 0 || localX >= this._layout.tableWidth) return null\n\n    for (let colIdx = 0; colIdx < this._columnCount; colIdx++) {\n      const colStart = (this._layout.columnOffsets[colIdx] ?? 0) + 1\n      const colEnd = colStart + (this._layout.columnWidths[colIdx] ?? 1) - 1\n      if (localX >= colStart && localX <= colEnd) {\n        return colIdx\n      }\n    }\n\n    return null\n  }\n\n  private getFullCellSelectionCoords(rowIdx: number, colIdx: number): CellSelectionCoords {\n    const colWidth = this._layout.columnWidths[colIdx] ?? 1\n    const rowHeight = this._layout.rowHeights[rowIdx] ?? 1\n    const contentWidth = Math.max(1, colWidth - this.getHorizontalCellPadding())\n    const contentHeight = Math.max(1, rowHeight - this.getVerticalCellPadding())\n\n    return {\n      anchorX: -1,\n      anchorY: 0,\n      focusX: contentWidth,\n      focusY: contentHeight,\n    }\n  }\n\n  private findRowForLocalY(localY: number): number {\n    if (this._rowCount === 0) return 0\n    if (localY < 0) return 0\n\n    for (let rowIdx = 0; rowIdx < this._rowCount; rowIdx++) {\n      const rowStart = (this._layout.rowOffsets[rowIdx] ?? 0) + 1\n      const rowEnd = rowStart + (this._layout.rowHeights[rowIdx] ?? 1) - 1\n      if (localY <= rowEnd) return rowIdx\n    }\n\n    return this._rowCount - 1\n  }\n\n  private getSelectionRowRange(selection: LocalSelectionBounds | null): RowRange | null {\n    if (!selection?.isActive || this._rowCount === 0) return null\n\n    const minSelY = Math.min(selection.anchorY, selection.focusY)\n    const maxSelY = Math.max(selection.anchorY, selection.focusY)\n\n    return {\n      firstRow: this.findRowForLocalY(minSelY),\n      lastRow: this.findRowForLocalY(maxSelY),\n    }\n  }\n\n  private getDirtySelectionRowRange(\n    previousSelection: LocalSelectionBounds | null,\n    currentSelection: LocalSelectionBounds | null,\n  ): RowRange | null {\n    const previousRange = this.getSelectionRowRange(previousSelection)\n    const currentRange = this.getSelectionRowRange(currentSelection)\n\n    if (previousRange === null) return currentRange\n    if (currentRange === null) return previousRange\n\n    return {\n      firstRow: Math.min(previousRange.firstRow, currentRange.firstRow),\n      lastRow: Math.max(previousRange.lastRow, currentRange.lastRow),\n    }\n  }\n\n  private resetRowSelection(rowIdx: number): void {\n    const row = this._cells[rowIdx]\n    if (!row) return\n\n    for (const cell of row) {\n      cell.textBufferView.resetLocalSelection()\n    }\n  }\n\n  private resetCellSelections(): void {\n    for (let rowIdx = 0; rowIdx < this._rowCount; rowIdx++) {\n      this.resetRowSelection(rowIdx)\n    }\n  }\n\n  private createEmptyLayout(): TextTableLayout {\n    return {\n      columnWidths: [],\n      rowHeights: [],\n      columnOffsets: [0],\n      rowOffsets: [0],\n      columnOffsetsI32: new Int32Array([0]),\n      rowOffsetsI32: new Int32Array([0]),\n      tableWidth: 0,\n      tableHeight: 0,\n    }\n  }\n\n  private resolveLayoutWidthConstraint(width: number | undefined): number | undefined {\n    if (width === undefined || !Number.isFinite(width) || width <= 0) {\n      return undefined\n    }\n\n    if (this._wrapMode !== \"none\" || this.isFullWidthMode()) {\n      return Math.max(1, Math.floor(width))\n    }\n\n    return undefined\n  }\n\n  private getHorizontalCellPadding(): number {\n    return this._cellPadding * 2\n  }\n\n  private getVerticalCellPadding(): number {\n    return this._cellPadding * 2\n  }\n\n  private resolveColumnFitter(value: TextTableColumnFitter | undefined): TextTableColumnFitter {\n    if (value === undefined) {\n      return this._defaultOptions.columnFitter\n    }\n\n    return value === \"balanced\" ? \"balanced\" : \"proportional\"\n  }\n\n  private resolveCellPadding(value: number | undefined): number {\n    if (value === undefined || !Number.isFinite(value)) {\n      return this._defaultOptions.cellPadding\n    }\n\n    return Math.max(0, Math.floor(value))\n  }\n\n  private invalidateLayoutAndRaster(markYogaDirty: boolean = true): void {\n    this._layoutDirty = true\n    this._rasterDirty = true\n    this._cachedMeasureLayout = null\n    this._cachedMeasureWidth = undefined\n\n    if (markYogaDirty) {\n      this.yogaNode.markDirty()\n    }\n\n    this.requestRender()\n  }\n\n  private invalidateRasterOnly(): void {\n    this._rasterDirty = true\n    this.requestRender()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/Textarea.ts",
    "content": "import type { KeyEvent, PasteEvent } from \"../lib/KeyHandler.js\"\nimport { decodePasteBytes, stripAnsiSequences } from \"../lib/paste.js\"\nimport { RGBA, parseColor, type ColorInput } from \"../lib/RGBA.js\"\nimport { type RenderContext } from \"../types.js\"\nimport { EditBufferRenderable, type EditBufferOptions } from \"./EditBufferRenderable.js\"\nimport {\n  type KeyBinding as BaseKeyBinding,\n  mergeKeyBindings,\n  getKeyBindingKey,\n  buildKeyBindingsMap,\n  type KeyAliasMap,\n  defaultKeyAliases,\n  mergeKeyAliases,\n} from \"../lib/keymapping.js\"\nimport { type StyledText, fg } from \"../lib/styled-text.js\"\nimport type { ExtmarksController } from \"../lib/extmarks.js\"\n\nexport type TextareaAction =\n  | \"move-left\"\n  | \"move-right\"\n  | \"move-up\"\n  | \"move-down\"\n  | \"select-left\"\n  | \"select-right\"\n  | \"select-up\"\n  | \"select-down\"\n  | \"line-home\"\n  | \"line-end\"\n  | \"select-line-home\"\n  | \"select-line-end\"\n  | \"visual-line-home\"\n  | \"visual-line-end\"\n  | \"select-visual-line-home\"\n  | \"select-visual-line-end\"\n  | \"buffer-home\"\n  | \"buffer-end\"\n  | \"select-buffer-home\"\n  | \"select-buffer-end\"\n  | \"delete-line\"\n  | \"delete-to-line-end\"\n  | \"delete-to-line-start\"\n  | \"backspace\"\n  | \"delete\"\n  | \"newline\"\n  | \"undo\"\n  | \"redo\"\n  | \"word-forward\"\n  | \"word-backward\"\n  | \"select-word-forward\"\n  | \"select-word-backward\"\n  | \"delete-word-forward\"\n  | \"delete-word-backward\"\n  | \"select-all\"\n  | \"submit\"\n\nexport type KeyBinding = BaseKeyBinding<TextareaAction>\n\nconst defaultTextareaKeybindings: KeyBinding[] = [\n  { name: \"left\", action: \"move-left\" },\n  { name: \"right\", action: \"move-right\" },\n  { name: \"up\", action: \"move-up\" },\n  { name: \"down\", action: \"move-down\" },\n  { name: \"left\", shift: true, action: \"select-left\" },\n  { name: \"right\", shift: true, action: \"select-right\" },\n  { name: \"up\", shift: true, action: \"select-up\" },\n  { name: \"down\", shift: true, action: \"select-down\" },\n  { name: \"home\", action: \"buffer-home\" },\n  { name: \"end\", action: \"buffer-end\" },\n  { name: \"home\", shift: true, action: \"select-buffer-home\" },\n  { name: \"end\", shift: true, action: \"select-buffer-end\" },\n  { name: \"a\", ctrl: true, action: \"line-home\" },\n  { name: \"e\", ctrl: true, action: \"line-end\" },\n  { name: \"a\", ctrl: true, shift: true, action: \"select-line-home\" },\n  { name: \"e\", ctrl: true, shift: true, action: \"select-line-end\" },\n  { name: \"a\", meta: true, action: \"visual-line-home\" },\n  { name: \"e\", meta: true, action: \"visual-line-end\" },\n  { name: \"a\", meta: true, shift: true, action: \"select-visual-line-home\" },\n  { name: \"e\", meta: true, shift: true, action: \"select-visual-line-end\" },\n  { name: \"f\", ctrl: true, action: \"move-right\" },\n  { name: \"b\", ctrl: true, action: \"move-left\" },\n  { name: \"w\", ctrl: true, action: \"delete-word-backward\" },\n  { name: \"backspace\", ctrl: true, action: \"delete-word-backward\" },\n  { name: \"d\", meta: true, action: \"delete-word-forward\" },\n  { name: \"delete\", meta: true, action: \"delete-word-forward\" },\n  { name: \"delete\", ctrl: true, action: \"delete-word-forward\" },\n  { name: \"d\", ctrl: true, shift: true, action: \"delete-line\" },\n  { name: \"k\", ctrl: true, action: \"delete-to-line-end\" },\n  { name: \"u\", ctrl: true, action: \"delete-to-line-start\" },\n  { name: \"backspace\", action: \"backspace\" },\n  { name: \"backspace\", shift: true, action: \"backspace\" },\n  { name: \"d\", ctrl: true, action: \"delete\" },\n  { name: \"delete\", action: \"delete\" },\n  { name: \"delete\", shift: true, action: \"delete\" },\n  { name: \"return\", action: \"newline\" },\n  { name: \"linefeed\", action: \"newline\" },\n  { name: \"return\", meta: true, action: \"submit\" },\n\n  // undo/redo\n  { name: \"-\", ctrl: true, action: \"undo\" },\n  { name: \".\", ctrl: true, action: \"redo\" },\n  { name: \"z\", super: true, action: \"undo\" },\n  { name: \"z\", super: true, shift: true, action: \"redo\" },\n\n  { name: \"f\", meta: true, action: \"word-forward\" },\n  { name: \"b\", meta: true, action: \"word-backward\" },\n  { name: \"right\", meta: true, action: \"word-forward\" },\n  { name: \"left\", meta: true, action: \"word-backward\" },\n  { name: \"right\", ctrl: true, action: \"word-forward\" },\n  { name: \"left\", ctrl: true, action: \"word-backward\" },\n  { name: \"f\", meta: true, shift: true, action: \"select-word-forward\" },\n  { name: \"b\", meta: true, shift: true, action: \"select-word-backward\" },\n  { name: \"right\", meta: true, shift: true, action: \"select-word-forward\" },\n  { name: \"left\", meta: true, shift: true, action: \"select-word-backward\" },\n  { name: \"backspace\", meta: true, action: \"delete-word-backward\" },\n\n  // super (cmd/win) + arrow keys for Kitty Keyboard mode\n  { name: \"left\", super: true, action: \"visual-line-home\" },\n  { name: \"right\", super: true, action: \"visual-line-end\" },\n  { name: \"up\", super: true, action: \"buffer-home\" },\n  { name: \"down\", super: true, action: \"buffer-end\" },\n  { name: \"left\", super: true, shift: true, action: \"select-visual-line-home\" },\n  { name: \"right\", super: true, shift: true, action: \"select-visual-line-end\" },\n  { name: \"up\", super: true, shift: true, action: \"select-buffer-home\" },\n  { name: \"down\", super: true, shift: true, action: \"select-buffer-end\" },\n  { name: \"a\", super: true, action: \"select-all\" },\n]\n\nexport interface SubmitEvent {}\n\nexport interface TextareaOptions extends EditBufferOptions {\n  initialValue?: string\n  backgroundColor?: ColorInput\n  textColor?: ColorInput\n  focusedBackgroundColor?: ColorInput\n  focusedTextColor?: ColorInput\n  placeholder?: StyledText | string | null\n  placeholderColor?: ColorInput\n  keyBindings?: KeyBinding[]\n  keyAliasMap?: KeyAliasMap\n  onSubmit?: (event: SubmitEvent) => void\n}\n\nexport class TextareaRenderable extends EditBufferRenderable {\n  private _placeholder: StyledText | string | null\n  private _placeholderColor: RGBA\n  private _unfocusedBackgroundColor: RGBA\n  private _unfocusedTextColor: RGBA\n  private _focusedBackgroundColor: RGBA\n  private _focusedTextColor: RGBA\n  private _keyBindingsMap: Map<string, TextareaAction>\n  private _keyAliasMap: KeyAliasMap\n  private _keyBindings: KeyBinding[]\n  private _actionHandlers: Map<TextareaAction, () => boolean>\n  private _initialValueSet: boolean = false\n  private _submitListener: ((event: SubmitEvent) => void) | undefined = undefined\n\n  private static readonly defaults = {\n    backgroundColor: \"transparent\",\n    textColor: \"#FFFFFF\",\n    focusedBackgroundColor: \"transparent\",\n    focusedTextColor: \"#FFFFFF\",\n    placeholder: null,\n    placeholderColor: \"#666666\",\n  } satisfies Partial<TextareaOptions>\n\n  constructor(ctx: RenderContext, options: TextareaOptions) {\n    const defaults = TextareaRenderable.defaults\n\n    // Pass base colors to parent constructor (these become the unfocused colors)\n    const baseOptions = {\n      ...options,\n      backgroundColor: options.backgroundColor || defaults.backgroundColor,\n      textColor: options.textColor || defaults.textColor,\n    }\n    super(ctx, baseOptions)\n\n    // Store unfocused colors separately (parent's properties get overwritten when focused)\n    this._unfocusedBackgroundColor = parseColor(options.backgroundColor || defaults.backgroundColor)\n    this._unfocusedTextColor = parseColor(options.textColor || defaults.textColor)\n    this._focusedBackgroundColor = parseColor(\n      options.focusedBackgroundColor || options.backgroundColor || defaults.focusedBackgroundColor,\n    )\n    this._focusedTextColor = parseColor(options.focusedTextColor || options.textColor || defaults.focusedTextColor)\n    this._placeholder = options.placeholder ?? defaults.placeholder\n    this._placeholderColor = parseColor(options.placeholderColor ?? defaults.placeholderColor)\n\n    this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {})\n    this._keyBindings = options.keyBindings || []\n    const mergedBindings = mergeKeyBindings(defaultTextareaKeybindings, this._keyBindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n    this._actionHandlers = this.buildActionHandlers()\n    this._submitListener = options.onSubmit\n\n    if (options.initialValue) {\n      this.setText(options.initialValue)\n      this._initialValueSet = true\n    }\n    this.updateColors()\n\n    this.applyPlaceholder(this._placeholder)\n  }\n\n  private applyPlaceholder(placeholder: StyledText | string | null): void {\n    if (placeholder === null) {\n      this.editorView.setPlaceholderStyledText([])\n      return\n    }\n\n    if (typeof placeholder === \"string\") {\n      const colorStyle = fg(this._placeholderColor)\n      const chunks = [colorStyle(placeholder)]\n      this.editorView.setPlaceholderStyledText(chunks)\n    } else {\n      this.editorView.setPlaceholderStyledText(placeholder.chunks)\n    }\n  }\n\n  private buildActionHandlers(): Map<TextareaAction, () => boolean> {\n    return new Map([\n      [\"move-left\", () => this.moveCursorLeft()],\n      [\"move-right\", () => this.moveCursorRight()],\n      [\"move-up\", () => this.moveCursorUp()],\n      [\"move-down\", () => this.moveCursorDown()],\n      [\"select-left\", () => this.moveCursorLeft({ select: true })],\n      [\"select-right\", () => this.moveCursorRight({ select: true })],\n      [\"select-up\", () => this.moveCursorUp({ select: true })],\n      [\"select-down\", () => this.moveCursorDown({ select: true })],\n      [\"line-home\", () => this.gotoLineHome()],\n      [\"line-end\", () => this.gotoLineEnd()],\n      [\"select-line-home\", () => this.gotoLineHome({ select: true })],\n      [\"select-line-end\", () => this.gotoLineEnd({ select: true })],\n      [\"visual-line-home\", () => this.gotoVisualLineHome()],\n      [\"visual-line-end\", () => this.gotoVisualLineEnd()],\n      [\"select-visual-line-home\", () => this.gotoVisualLineHome({ select: true })],\n      [\"select-visual-line-end\", () => this.gotoVisualLineEnd({ select: true })],\n      [\"select-buffer-home\", () => this.gotoBufferHome({ select: true })],\n      [\"select-buffer-end\", () => this.gotoBufferEnd({ select: true })],\n      [\"buffer-home\", () => this.gotoBufferHome()],\n      [\"buffer-end\", () => this.gotoBufferEnd()],\n      [\"delete-line\", () => this.deleteLine()],\n      [\"delete-to-line-end\", () => this.deleteToLineEnd()],\n      [\"delete-to-line-start\", () => this.deleteToLineStart()],\n      [\"backspace\", () => this.deleteCharBackward()],\n      [\"delete\", () => this.deleteChar()],\n      [\"newline\", () => this.newLine()],\n      [\"undo\", () => this.undo()],\n      [\"redo\", () => this.redo()],\n      [\"word-forward\", () => this.moveWordForward()],\n      [\"word-backward\", () => this.moveWordBackward()],\n      [\"select-word-forward\", () => this.moveWordForward({ select: true })],\n      [\"select-word-backward\", () => this.moveWordBackward({ select: true })],\n      [\"delete-word-forward\", () => this.deleteWordForward()],\n      [\"delete-word-backward\", () => this.deleteWordBackward()],\n      [\"select-all\", () => this.selectAll()],\n      [\"submit\", () => this.submit()],\n    ])\n  }\n\n  public handlePaste(event: PasteEvent): void {\n    this.insertText(stripAnsiSequences(decodePasteBytes(event.bytes)))\n  }\n\n  public handleKeyPress(key: KeyEvent): boolean {\n    const bindingKey = getKeyBindingKey({\n      name: key.name,\n      ctrl: key.ctrl,\n      shift: key.shift,\n      meta: key.meta,\n      super: key.super,\n      action: \"move-left\" as TextareaAction,\n    })\n\n    const action = this._keyBindingsMap.get(bindingKey)\n\n    if (action) {\n      const handler = this._actionHandlers.get(action)\n      if (handler) {\n        return handler()\n      }\n    }\n\n    if (!key.ctrl && !key.meta && !key.super && !key.hyper) {\n      if (key.name === \"space\") {\n        this.insertText(\" \")\n        return true\n      }\n\n      if (key.sequence) {\n        const firstCharCode = key.sequence.charCodeAt(0)\n\n        if (firstCharCode < 32) {\n          return false\n        }\n\n        if (firstCharCode === 127) {\n          return false\n        }\n\n        this.insertText(key.sequence)\n        return true\n      }\n    }\n\n    return false\n  }\n\n  private updateColors(): void {\n    const effectiveBg = this._focused ? this._focusedBackgroundColor : this._unfocusedBackgroundColor\n    const effectiveFg = this._focused ? this._focusedTextColor : this._unfocusedTextColor\n\n    super.backgroundColor = effectiveBg\n    super.textColor = effectiveFg\n  }\n\n  public insertChar(char: string): void {\n    if (this.hasSelection()) {\n      this.deleteSelectedText()\n    }\n\n    this.editBuffer.insertChar(char)\n    this.requestRender()\n  }\n\n  public insertText(text: string): void {\n    if (this.hasSelection()) {\n      this.deleteSelectedText()\n    }\n\n    this.editBuffer.insertText(text)\n    this.requestRender()\n  }\n\n  public deleteChar(): boolean {\n    if (this.hasSelection()) {\n      this.deleteSelectedText()\n      return true\n    }\n\n    this._ctx.clearSelection()\n    this.editBuffer.deleteChar()\n    this.requestRender()\n    return true\n  }\n\n  public deleteCharBackward(): boolean {\n    if (this.hasSelection()) {\n      this.deleteSelectedText()\n      return true\n    }\n\n    this._ctx.clearSelection()\n    this.editBuffer.deleteCharBackward()\n    this.requestRender()\n    return true\n  }\n\n  private deleteSelectedText(): void {\n    this.editorView.deleteSelectedText()\n\n    this._ctx.clearSelection()\n    this.requestRender()\n  }\n\n  public newLine(): boolean {\n    this._ctx.clearSelection()\n    this.editBuffer.newLine()\n    this.requestRender()\n    return true\n  }\n\n  public deleteLine(): boolean {\n    this._ctx.clearSelection()\n    this.editBuffer.deleteLine()\n    this.requestRender()\n    return true\n  }\n\n  public moveCursorLeft(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n\n    // if there's a selection and shift is not pressed,\n    // move cursor to the start of the selection\n    if (!select && this.hasSelection()) {\n      const selection = this.getSelection()!\n      this.editBuffer.setCursorByOffset(selection.start)\n      this._ctx.clearSelection()\n      this.requestRender()\n      return true\n    }\n\n    this.updateSelectionForMovement(select, true)\n    this.editBuffer.moveCursorLeft()\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public moveCursorRight(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n\n    // if there's a selection and shift is not pressed,\n    // move cursor to the end of the selection\n    if (!select && this.hasSelection()) {\n      const selection = this.getSelection()!\n      const targetOffset = this.cursorOffset === selection.start ? selection.end - 1 : selection.end\n      this.editBuffer.setCursorByOffset(targetOffset)\n      this._ctx.clearSelection()\n      this.requestRender()\n      return true\n    }\n\n    this.updateSelectionForMovement(select, true)\n    this.editBuffer.moveCursorRight()\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public moveCursorUp(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n    this.editorView.moveUpVisual()\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public moveCursorDown(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n    this.editorView.moveDownVisual()\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public gotoLine(line: number): void {\n    this.editBuffer.gotoLine(line)\n    this.requestRender()\n  }\n\n  public gotoLineHome(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n    const cursor = this.editorView.getCursor()\n    if (cursor.col === 0 && cursor.row > 0) {\n      this.editBuffer.setCursor(cursor.row - 1, 0)\n      const prevLineEol = this.editBuffer.getEOL()\n      this.editBuffer.setCursor(prevLineEol.row, prevLineEol.col)\n    } else {\n      this.editBuffer.setCursor(cursor.row, 0)\n    }\n\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public gotoLineEnd(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n    const cursor = this.editorView.getCursor()\n    const eol = this.editBuffer.getEOL()\n    const lineCount = this.editBuffer.getLineCount()\n    if (cursor.col === eol.col && cursor.row < lineCount - 1) {\n      this.editBuffer.setCursor(cursor.row + 1, 0)\n    } else {\n      this.editBuffer.setCursor(eol.row, eol.col)\n    }\n\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public gotoVisualLineHome(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n\n    const sol = this.editorView.getVisualSOL()\n    this.editBuffer.setCursor(sol.logicalRow, sol.logicalCol)\n\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public gotoVisualLineEnd(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n\n    const eol = this.editorView.getVisualEOL()\n    this.editBuffer.setCursor(eol.logicalRow, eol.logicalCol)\n\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public gotoBufferHome(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n    this.editBuffer.setCursor(0, 0)\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public gotoBufferEnd(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n    this.editBuffer.gotoLine(999999)\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public selectAll(): boolean {\n    this.updateSelectionForMovement(false, true)\n    this.editBuffer.setCursor(0, 0)\n    return this.gotoBufferEnd({ select: true })\n  }\n\n  public deleteToLineEnd(): boolean {\n    const cursor = this.editorView.getCursor()\n    const eol = this.editBuffer.getEOL()\n\n    if (eol.col > cursor.col) {\n      this.editBuffer.deleteRange(cursor.row, cursor.col, eol.row, eol.col)\n    }\n\n    this.requestRender()\n    return true\n  }\n\n  public deleteToLineStart(): boolean {\n    const cursor = this.editorView.getCursor()\n\n    if (cursor.col > 0) {\n      this.editBuffer.deleteRange(cursor.row, 0, cursor.row, cursor.col)\n    }\n\n    this.requestRender()\n    return true\n  }\n\n  public undo(): boolean {\n    this._ctx.clearSelection()\n    this.editBuffer.undo()\n    this.requestRender()\n    return true\n  }\n\n  public redo(): boolean {\n    this._ctx.clearSelection()\n    this.editBuffer.redo()\n    this.requestRender()\n    return true\n  }\n\n  public moveWordForward(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n    const nextWord = this.editBuffer.getNextWordBoundary()\n    this.editBuffer.setCursorByOffset(nextWord.offset)\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public moveWordBackward(options?: { select?: boolean }): boolean {\n    const select = options?.select ?? false\n    this.updateSelectionForMovement(select, true)\n    const prevWord = this.editBuffer.getPrevWordBoundary()\n    this.editBuffer.setCursorByOffset(prevWord.offset)\n    this.updateSelectionForMovement(select, false)\n    this.requestRender()\n    return true\n  }\n\n  public deleteWordForward(): boolean {\n    if (this.hasSelection()) {\n      this.deleteSelectedText()\n      return true\n    }\n\n    const currentCursor = this.editBuffer.getCursorPosition()\n    const nextWord = this.editBuffer.getNextWordBoundary()\n\n    if (nextWord.offset > currentCursor.offset) {\n      this.editBuffer.deleteRange(currentCursor.row, currentCursor.col, nextWord.row, nextWord.col)\n    }\n\n    this._ctx.clearSelection()\n    this.requestRender()\n    return true\n  }\n\n  public deleteWordBackward(): boolean {\n    if (this.hasSelection()) {\n      this.deleteSelectedText()\n      return true\n    }\n\n    const currentCursor = this.editBuffer.getCursorPosition()\n    const prevWord = this.editBuffer.getPrevWordBoundary()\n\n    if (prevWord.offset < currentCursor.offset) {\n      this.editBuffer.deleteRange(prevWord.row, prevWord.col, currentCursor.row, currentCursor.col)\n    }\n\n    this._ctx.clearSelection()\n    this.requestRender()\n    return true\n  }\n\n  public focus(): void {\n    super.focus()\n    this.updateColors()\n  }\n\n  public blur(): void {\n    super.blur()\n    if (!this.isDestroyed) {\n      this.updateColors()\n    }\n  }\n\n  get placeholder(): StyledText | string | null {\n    return this._placeholder\n  }\n\n  set placeholder(value: StyledText | string | null | undefined) {\n    const normalizedValue = value ?? null\n    if (this._placeholder !== normalizedValue) {\n      this._placeholder = normalizedValue\n      this.applyPlaceholder(normalizedValue)\n      this.requestRender()\n    }\n  }\n\n  get placeholderColor(): RGBA {\n    return this._placeholderColor\n  }\n\n  set placeholderColor(value: ColorInput) {\n    const newColor = parseColor(value ?? TextareaRenderable.defaults.placeholderColor)\n    if (this._placeholderColor !== newColor) {\n      this._placeholderColor = newColor\n      this.applyPlaceholder(this._placeholder)\n      this.requestRender()\n    }\n  }\n\n  override get backgroundColor(): RGBA {\n    return this._unfocusedBackgroundColor\n  }\n\n  override set backgroundColor(value: RGBA | string | undefined) {\n    const newColor = parseColor(value ?? TextareaRenderable.defaults.backgroundColor)\n    if (this._unfocusedBackgroundColor !== newColor) {\n      this._unfocusedBackgroundColor = newColor\n      this.updateColors()\n    }\n  }\n\n  override get textColor(): RGBA {\n    return this._unfocusedTextColor\n  }\n\n  override set textColor(value: RGBA | string | undefined) {\n    const newColor = parseColor(value ?? TextareaRenderable.defaults.textColor)\n    if (this._unfocusedTextColor !== newColor) {\n      this._unfocusedTextColor = newColor\n      this.updateColors()\n    }\n  }\n\n  set focusedBackgroundColor(value: ColorInput) {\n    const newColor = parseColor(value ?? TextareaRenderable.defaults.focusedBackgroundColor)\n    if (this._focusedBackgroundColor !== newColor) {\n      this._focusedBackgroundColor = newColor\n      this.updateColors()\n    }\n  }\n\n  set focusedTextColor(value: ColorInput) {\n    const newColor = parseColor(value ?? TextareaRenderable.defaults.focusedTextColor)\n    if (this._focusedTextColor !== newColor) {\n      this._focusedTextColor = newColor\n      this.updateColors()\n    }\n  }\n\n  set initialValue(value: string) {\n    if (!this._initialValueSet) {\n      this.setText(value)\n      this._initialValueSet = true\n    }\n  }\n\n  public submit(): boolean {\n    if (this._submitListener) {\n      this._submitListener({})\n    }\n    return true\n  }\n\n  public set onSubmit(handler: ((event: SubmitEvent) => void) | undefined) {\n    this._submitListener = handler\n  }\n\n  public get onSubmit(): ((event: SubmitEvent) => void) | undefined {\n    return this._submitListener\n  }\n\n  public set keyBindings(bindings: KeyBinding[]) {\n    this._keyBindings = bindings\n    const mergedBindings = mergeKeyBindings(defaultTextareaKeybindings, bindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n  }\n\n  public set keyAliasMap(aliases: KeyAliasMap) {\n    this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, aliases)\n    const mergedBindings = mergeKeyBindings(defaultTextareaKeybindings, this._keyBindings)\n    this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap)\n  }\n\n  public get extmarks(): ExtmarksController {\n    return this.editorView.extmarks\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/TimeToFirstDraw.ts",
    "content": "import type { OptimizedBuffer } from \"../buffer\"\nimport { parseColor, RGBA, type ColorInput } from \"../lib/RGBA\"\nimport { Renderable, type RenderableOptions } from \"../Renderable\"\nimport type { RenderContext } from \"../types\"\n\nexport interface TimeToFirstDrawOptions extends RenderableOptions<TimeToFirstDrawRenderable> {\n  fg?: ColorInput\n  label?: string\n  precision?: number\n}\n\nexport class TimeToFirstDrawRenderable extends Renderable {\n  private _runtimeMs: number | null = null\n  private textColor: RGBA\n  private label: string\n  private precision: number\n\n  constructor(ctx: RenderContext, options: TimeToFirstDrawOptions = {}) {\n    super(ctx, {\n      width: \"100%\",\n      height: 1,\n      flexShrink: 0,\n      alignSelf: \"center\",\n      ...options,\n    })\n\n    this.textColor = parseColor(options.fg ?? \"#AAAAAA\")\n    this.label = options.label ?? \"Time to first draw\"\n    this.precision = this.normalizePrecision(options.precision ?? 2)\n  }\n\n  public get runtimeMs(): number | null {\n    return this._runtimeMs\n  }\n\n  public set fg(value: ColorInput) {\n    this.textColor = parseColor(value)\n    this.requestRender()\n  }\n\n  public set color(value: ColorInput) {\n    this.fg = value\n  }\n\n  public set textLabel(value: string) {\n    if (value === this.label) {\n      return\n    }\n\n    this.label = value\n    this.requestRender()\n  }\n\n  public set decimals(value: number) {\n    const nextPrecision = this.normalizePrecision(value)\n    if (nextPrecision === this.precision) {\n      return\n    }\n\n    this.precision = nextPrecision\n    this.requestRender()\n  }\n\n  public reset(): void {\n    this._runtimeMs = null\n    this.requestRender()\n  }\n\n  protected override renderSelf(buffer: OptimizedBuffer): void {\n    if (this._runtimeMs === null) {\n      this._runtimeMs = performance.now()\n    }\n\n    const content = `${this.label}: ${this._runtimeMs.toFixed(this.precision)}ms`\n    const maxWidth = Math.max(this.width, 1)\n    const visibleContent = content.length > maxWidth ? content.slice(0, maxWidth) : content\n    const centeredX = this.x + Math.max(0, Math.floor((maxWidth - visibleContent.length) / 2))\n\n    buffer.drawText(visibleContent, centeredX, this.y, this.textColor)\n  }\n\n  private normalizePrecision(value: number): number {\n    if (!Number.isFinite(value)) {\n      return 2\n    }\n\n    return Math.max(0, Math.floor(value))\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/__snapshots__/Code.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`CodeRenderable - text renders immediately before highlighting completes: text visible before highlighting completes 1`] = `\n\"const message = 'hello world';  \n                                \n\"\n`;\n\nexports[`CodeRenderable - text renders immediately before highlighting completes: text visible after highlighting completes 1`] = `\n\"const message = 'hello world';  \n                                \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__snapshots__/Diff.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`DiffRenderable - unified view renders correctly: unified view simple diff 1`] = `\n\" 1   function hello() {                                                         \n 2 -   console.log(\"Hello\");                                                    \n 2 +   console.log(\"Hello, World!\");                                            \n 3   }                                                                          \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - split view renders correctly: split view simple diff 1`] = `\n\" 1   function hello() {                  1   function hello() {                 \n 2 -   console.log(\"Hello\");             2 +   console.log(\"Hello, World!\");    \n 3   }                                   3   }                                  \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - multi-line diff unified view: unified view multi-line diff 1`] = `\n\"  1   function add(a, b) {                                                      \n  2     return a + b;                                                           \n  3   }                                                                         \n  4                                                                             \n  5 + function subtract(a, b) {                                                 \n  6 +   return a - b;                                                           \n  7 + }                                                                         \n  8 +                                                                           \n  9   function multiply(a, b) {                                                 \n  6 -   return a * b;                                                           \n 10 +   return a * b * 1;                                                       \n 11   }                                                                         \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - multi-line diff split view: split view multi-line diff 1`] = `\n\"  1   function add(a, b) {                1   function add(a, b) {              \n  2     return a + b;                     2     return a + b;                   \n  3   }                                   3   }                                 \n  4                                       4                                     \n                                          5 + function subtract(a, b) {         \n                                          6 +   return a - b;                   \n                                          7 + }                                 \n                                          8 +                                   \n  5   function multiply(a, b) {           9   function multiply(a, b) {         \n  6 -   return a * b;                    10 +   return a * b * 1;               \n  7   }                                  11   }                                 \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - add-only diff unified view: unified view add-only diff 1`] = `\n\" 1 + function newFunction() {                                                   \n 2 +   return true;                                                             \n 3 + }                                                                          \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - add-only diff split view: split view add-only diff 1`] = `\n\"                                         1 + function newFunction() {           \n                                         2 +   return true;                     \n                                         3 + }                                  \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - remove-only diff unified view: unified view remove-only diff 1`] = `\n\" 1 - function oldFunction() {                                                   \n 2 -   return false;                                                            \n 3 - }                                                                          \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - remove-only diff split view: split view remove-only diff 1`] = `\n\" 1 - function oldFunction() {                                                   \n 2 -   return false;                                                            \n 3 - }                                                                          \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - large line numbers displayed correctly: unified view large line numbers 1`] = `\n\" 42   const line42 = 'context';                                                 \n 43   const line43 = 'context';                                                 \n 44 - const line44 = 'removed';                                                 \n 44 + const line44 = 'added';                                                   \n 45   const line45 = 'context';                                                 \n 46 + const line46 = 'added';                                                   \n 47   const line47 = 'context';                                                 \n 48   const line48 = 'context';                                                 \n 48 - const line49 = 'removed';                                                 \n 49 + const line49 = 'changed';                                                 \n 50   const line50 = 'context';                                                 \n 51   const line51 = 'context';                                                 \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - split view alignment with empty lines: split view alignment 1`] = `\n\" 1 line1                                 1   line1                              \n                                         2 + line2_added                        \n                                         3 + line3_added                        \n                                         4 + line4_added                        \n 2 line5                                 5   line5                              \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - line numbers hidden for empty alignment lines in split view: split view with hidden line numbers for empty lines 1`] = `\n\"                                         1 + function newFunction() {           \n                                         2 +   return true;                     \n                                         3 + }                                  \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - consistent left padding for line numbers > 9: unified view with double-digit line numbers 1`] = `\n\"  8   line8                                                                     \n  9   line9                                                                     \n 10 - line10_old                                                                \n 10 + line10_new                                                                \n 11   line11                                                                    \n 12 + line12_added                                                              \n 13 + line13_added                                                              \n 14   line14                                                                    \n 15   line15                                                                    \n 14 - line16_old                                                                \n 16 + line16_new                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - wrapMode works in unified view: wrapMode-none 1`] = `\n\" 1   function hello() {                                                         \n 2 -   console.log(\"This is a very long line that should wrap when wrapMode is s\n 2 +   console.log(\"This is a very long line that has been modified and should w\n 3   }                                                                          \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - wrapMode works in unified view: wrapMode-word 1`] = `\n\" 1   function hello() {                                                         \n 2 -   console.log(\"This is a very long line that should wrap when wrapMode is  \n     set to word but not when it is set to none\");                              \n 2 +   console.log(\"This is a very long line that has been modified and should  \n     wrap when wrapMode is set to word but not when it is set to none\");        \n 3   }                                                                          \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - wrapMode works in unified view: wrapMode-none 2`] = `\n\" 1   function hello() {                                                         \n 2 -   console.log(\"This is a very long line that should wrap when wrapMode is s\n 2 +   console.log(\"This is a very long line that has been modified and should w\n 3   }                                                                          \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - multiple hunks in unified view: unified view multiple hunks 1`] = `\n\"  1   function first() {                                                        \n  2 -   return 1;                                                               \n  2 +   return \"one\";                                                           \n  3   }                                                                         \n 15   function second() {                                                       \n 16     var x = 10;                                                             \n 17 +   var y = 20;                                                             \n 18     return x;                                                               \n 19   }                                                                         \n 31   function third() {                                                        \n 31 -   console.log(\"old\");                                                     \n 32 +   console.log(\"new\");                                                     \n 33   }                                                                         \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - multiple hunks in split view: split view multiple hunks 1`] = `\n\"  1   function first() {                  1   function first() {                \n  2 -   return 1;                         2 +   return \"one\";                   \n  3   }                                   3   }                                 \n 15   function second() {                15   function second() {               \n 16     var x = 10;                      16     var x = 10;                     \n                                         17 +   var y = 20;                     \n 17     return x;                        18     return x;                       \n 18   }                                  19   }                                 \n 30   function third() {                 31   function third() {                \n 31 -   console.log(\"old\");              32 +   console.log(\"new\");             \n 32   }                                  33   }                                 \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - no newline at end of file in unified view: unified view with no newline marker 1`] = `\n\" 1   line1                                                                      \n 2   line2                                                                      \n 3 - line3                                                                      \n 3 + line3_modified                                                             \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - no newline at end of file in split view: split view with no newline marker 1`] = `\n\" 1   line1                               1   line1                              \n 2   line2                               2   line2                              \n 3 - line3                                                                      \n                                         3 + line3_modified                     \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - asymmetric block with more removes than adds in split view: split view asymmetric block more removes 1`] = `\n\" 1   context_before                      1   context_before                     \n 2 - remove1                             2 + add1                               \n 3 - remove2                             3 + add2                               \n 4 - remove3                                                                    \n 5 - remove4                                                                    \n 6 - remove5                                                                    \n 7   context_after                       4   context_after                      \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - asymmetric block with more adds than removes in split view: split view asymmetric block more adds 1`] = `\n\" 1   context_before                      1   context_before                     \n 2 - remove1                             2 + add1                               \n 3 - remove2                             3 + add2                               \n                                         4 + add3                               \n                                         5 + add4                               \n                                         6 + add5                               \n 4   context_after                       7   context_after                      \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - back-to-back change blocks without context lines in split view: split view back-to-back blocks 1`] = `\n\" 1 - remove1                             1 + add1                               \n 2 - remove2                             2 + add2                               \n 3 - remove3                             3 + add3                               \n 4 - remove4                             4 + add4                               \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - very long lines wrapping multiple times in split view: split view multi-wrap lines 1`] = `\n\" 1   short line                          1   short line                         \n 2 - This is an extremely long line      2 + This is an extremely long line     \n     that will definitely wrap multiple      that has been modified and will    \n     times when rendered in a split          definitely wrap multiple times     \n     view with word wrapping enabled         when rendered in a split view with \n     because it contains so many words       word wrapping enabled because it   \n     and characters                          contains so many words and         \n                                             characters and even more content   \n 3   another short line                  3   another short line                 \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - invalid diff format shows error with raw diff: invalid diff format with error 1`] = `\n\"Error parsing diff: Unknown line 5 \"-  console.log(\\\\\"Hello\\\\\");\"                 \n                                                                                \n--- a/test.js                                                                   \n+++ b/test.js                                                                   \n@@ -a,b +c,d @@                                                                 \n function hello() {                                                             \n-  console.log(\"Hello\");                                                        \n+  console.log(\"Hello, World!\");                                                \n }                                                                              \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - diff with only context lines (no changes): diff with only context lines 1`] = `\n\" 1 line1                                                                        \n 2 line2                                                                        \n 3 line3                                                                        \n 4 line4                                                                        \n 5 line5                                                                        \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - can toggle conceal with markdown diff: markdown diff with conceal enabled 1`] = `\n\" 1   First line                                                                 \n 2 - Some text old**                                                            \n 2 + So text**boldext** and *italic*                                            \n 3   End line                                                                   \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - can toggle conceal with markdown diff: markdown diff with conceal disabled 1`] = `\n\" 1   First line                                                                 \n 2 - Some text **old**                                                          \n 2 + Some text **boldtext** and *italic*                                        \n 3   End line                                                                   \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - conceal works in split view: split view markdown diff with conceal enabled 1`] = `\n\" 1   First line                          1   First line                         \n 2 - Some old text                       2 + Some new text                      \n 3   End line                            3   End line                           \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - conceal works in split view: split view markdown diff with conceal disabled 1`] = `\n\" 1   First line                          1   First line                         \n 2 - Some **old** text                   2 + Some **new** text                  \n 3   End line                            3   End line                           \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`DiffRenderable - line numbers update correctly after resize causes wrapping changes: before resize - line numbers with no wrapping 1`] = `\n\" 1   function calculateSomethingVeryComplexWithALongFunctionNameThatWillWrap() {                                        \n 2 -   const oldResultWithAVeryLongVariableNameThatWillDefinitelyWrapWhenRenderedInASmallerTerminal = 42;               \n 2 +   const newResultWithAVeryLongVariableNameThatWillDefinitelyWrapWhenRenderedInASmallerTerminal = 100;              \n 3     return result;                                                                                                   \n 4   }                                                                                                                  \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n                                                                                                                        \n\"\n`;\n\nexports[`DiffRenderable - line numbers update correctly after resize causes wrapping changes: after resize - line numbers with wrapping 1`] = `\n\"  1   function                                              \n      calculateSomethingVeryComplexWithALongFunctionNameThat\n      WillWrap() {                                          \n  2 -   const                                               \n      oldResultWithAVeryLongVariableNameThatWillDefinitelyWr\n      apWhenRenderedInASmallerTerminal = 42;                \n  2 +   const                                               \n      newResultWithAVeryLongVariableNameThatWillDefinitelyWr\n      apWhenRenderedInASmallerTerminal = 100;               \n  3     return result;                                      \n  4   }                                                     \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__snapshots__/Text.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`TextRenderable Selection Text Content Snapshots should render basic text content correctly 1`] = `\n\"                    \n                    \n                    \n     Hello World    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render multiline text content correctly 1`] = `\n\"                    \n Line 1: Hello      \n Line 2: World      \n Line 3: Testing    \n Line 4: Multiline  \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render text with graphemes/emojis correctly 1`] = `\n\"                    \n                    \nHello 🌍 World 👋   \n Test 🚀 Emoji      \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render TextNode text composition correctly 1`] = `\n\"First Second Third  \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render text positioning correctly 1`] = `\n\"Top                 \n                    \n        Mid         \n                    \n                Bot \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render empty buffer correctly 1`] = `\n\"                    \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render text with character wrapping correctly 1`] = `\n\"This is a very      \nlong text that      \nshould wrap to      \nmultiple lines      \nwhen wrap is en     \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render wrapped text with different content 1`] = `\n\"                    \n  ABCDEFGHIJ        \n  KLMNOPQRST        \n  UVWXYZ abc        \n  defghijklm        \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render wrapped text with emojis and graphemes 1`] = `\n\" Hello 🌍 Wor       \n ld 👋 This i       \n s a test wit       \n h emojis 🚀        \n that should        \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render wrapped multiline text correctly 1`] = `\n\"                    \nFirst li            \nne with             \nlong con            \ntent                \n\"\n`;\n\nexports[`TextRenderable Selection Text Content Snapshots should render text with tab indicator correctly 1`] = `\n\"Line 1→ Tabbed      \nLine 2→ → Double tab\n                    \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Text Node Dimension Updates should update dimensions and reposition subsequent elements when text nodes expand 1`] = `\n\"Short               \nSecond text         \n                    \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Text Node Dimension Updates should update dimensions and reposition subsequent elements when text nodes expand 2`] = `\n\"Short text that will\n definitely wrap    \nSecond text         \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Text Node Dimension Updates should handle multiple text node updates with complex layout changes 1`] = `\n\"First part          \nMiddle text         \nBottom text         \n                    \n                    \n                    \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Text Node Dimension Updates should handle multiple text node updates with complex layout changes 2`] = `\n\"First of            \na                   \nsentence            \npartthat            \nwill wrap           \nMiddle text         \nBottom text         \n                    \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when width is set via setter 1`] = `\n\"┌────────────────────────────┐          \n│>    Content that takes up  │          \n│     space                  │          \n└────────────────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when height is set via setter in column layout with text 1`] = `\n\"┌───────────────────────┐     \n│Header                 │     \n│                       │     \n│                       │     \n│Line1                  │     \n│Line2                  │     \n│Line3                  │     \n│Footer                 │     \n│                       │     \n└───────────────────────┘     \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n\nexports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when minWidth is set via setter 1`] = `\n\"┌────────────────────────────┐          \n│>    Content that takes up  │          \n│     space                  │          \n└────────────────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when minHeight is set via setter in column layout with text 1`] = `\n\"┌───────────────────────┐     \n│Header                 │     \n│                       │     \n│                       │     \n│Line1                  │     \n│Line2                  │     \n│Line3                  │     \n│Footer                 │     \n│                       │     \n└───────────────────────┘     \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n\nexports[`TextRenderable Selection Width/Height Setter Layout Tests should not shrink box when width is set from undefined via setter 1`] = `\n\"┌────────────────────────────┐          \n│>    Content that takes up  │          \n│     space                  │          \n└────────────────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`TextRenderable Selection Absolute Positioned Box with Text should render text in absolute positioned box with padding and borders correctly 1`] = `\n\"                                                                                \n                                                                                \n                  │                                                          │  \n                  │                                                          │  \n                  │    Important Notification                                │  \n                  │                                                          │  \n                  │                                                          │  \n                  │    This is a longer message that should wrap properly within\n                  │                                                          │  \n                  │                                                          │  \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`TextRenderable Selection Absolute Positioned Box with Text should render text fully visible in absolute positioned box at various positions 1`] = `\n\"                                                                                                    \n                                                           ┌──────────────────────────────────────┐ \n                                                           │ Error: File not found in the         │ \n                                                           │ specified directory path             │ \n                                                           └──────────────────────────────────────┘ \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n ───────────────────────────────────                                                                \n  Success: Operation completed                                                                      \n  successfully!                                                                                     \n ───────────────────────────────────                                                                \n                                                                                                    \n\"\n`;\n\nexports[`TextRenderable Selection Absolute Positioned Box with Text should handle width:100% text in absolute positioned box with constrained maxWidth 1`] = `\n\"                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n             This is an extremely long piece of text                  \n             that needs to wrap multiple times within                 \n             the constrained width of the absolutely                  \n             positioned container box with significant                \n             padding on all sides.                                    \n                                                                      \n                                                                      \n                                                                      \n\"\n`;\n\nexports[`TextRenderable Selection Absolute Positioned Box with Text should render multiple text elements in absolute positioned box with proper spacing 1`] = `\n\"                                                                                          \n                                                                                          \n                                                                                          \n                                        ┌───────────────────────────────────────────┐     \n                                        │                                           │     \n                                        │  System Update                            │     \n                                        │                                           │     \n                                        │  A new version is available with bug      │     \n                                        │  fixes and performance improvements.      │     \n                                        │                                           │     \n                                        │  Click to install                         │     \n                                        │                                           │     \n                                        └───────────────────────────────────────────┘     \n                                                                                          \n                                                                                          \n                                                                                          \n                                                                                          \n                                                                                          \n                                                                                          \n                                                                                          \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should wrap at word boundaries when using word mode 1`] = `\n\"The quick           \nbrown fox           \njumps over the      \nlazy dog            \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should wrap at character boundaries when using char mode 1`] = `\n\"The quick brown     \n fox jumps over     \n the lazy dog       \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should handle word wrapping with punctuation 1`] = `\n\"Hello,              \nWorld.              \nTest-               \nExample/            \nPath                \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should handle word wrapping with hyphens and dashes 1`] = `\n\"self-               \ncontained           \nmulti-line          \ntext-               \nwrapping            \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should dynamically change wrap mode 1`] = `\n\"The quick           \nbrown fox           \njumps               \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should handle long words that exceed wrap width in word mode 1`] = `\n\"ABCDEFGHIJ          \nKLMNOPQRST          \nUVWXYZ              \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should preserve empty lines with word wrapping 1`] = `\n\"First               \nline                \n                    \nThird               \nline                \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should handle word wrapping with single character words 1`] = `\n\"a b c d             \ne f g h             \ni j k l             \nm n o p             \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should compare char vs word wrapping with same content 1`] = `\n\"Hello               \nwonderful           \nworld of            \ntext                \nwrapping            \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should correctly wrap text when updating content via text.content 1`] = `\n\"Short text          \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`TextRenderable Selection Word Wrapping should correctly wrap text when updating content via text.content 2`] = `\n\"This is a much      \nlonger text that    \nshould definitely   \nwrap to multiple    \nlines               \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__snapshots__/TextTable.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`TextTableRenderable renders a basic table with styled cell chunks: basic table 1`] = `\n\"                                                            \n ┌─────┬──────┬───────────────────┐                         \n │Name │Status│Notes              │                         \n ├─────┼──────┼───────────────────┤                         \n │Alpha│OK    │All systems nominal│                         \n ├─────┼──────┼───────────────────┤                         \n │Bravo│WARN  │Pending checks     │                         \n └─────┴──────┴───────────────────┘                         \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`TextTableRenderable wraps content and fits columns when width is constrained: wrapped constrained width 1`] = `\n\"┌──┬─────────────────────────────┐                          \n│ID│Description                  │                          \n├──┼─────────────────────────────┤                          \n│1 │This is a long sentence that │                          \n│  │should wrap across multiple  │                          \n│  │visual lines                 │                          \n├──┼─────────────────────────────┤                          \n│2 │Short                        │                          \n└──┴─────────────────────────────┘                          \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`TextTableRenderable balanced fitter keeps constrained columns visually closer: fitter proportional constrained 1`] = `\n\"┌────┬─────────────┬─────────┬─────────┬───────┬─────────┐  \n│Prov│Compute      │Storage  │Pricing  │Regions│Use Cases│  \n│ider│Services     │Solutions│Model    │       │         │  \n├────┼─────────────┼─────────┼─────────┼───────┼─────────┤  \n│Amaz│EC2          │S3 tiers,│Pay as   │Global │Enterpris│  \n│on W│instances    │ EBS,    │you go,  │regions│e migrati│  \n│eb S│with         │EFS, and │reserved │ and ma│on, analy│  \n│ervi│extensive    │archive  │terms,   │ny edge│tics, ML,│  \n│ces │options for  │classes  │and      │ locati│ and back│  \n│    │general,     │for long │discounte│ons    │end servi│  \n│    │memory, and  │retention│d spot ca│       │ces      │  \n│    │accelerated  │         │pacity   │       │         │  \n│    │workloads    │         │         │       │         │  \n└────┴─────────────┴─────────┴─────────┴───────┴─────────┘  \n                                                            \n                                                            \n\"\n`;\n\nexports[`TextTableRenderable balanced fitter keeps constrained columns visually closer: fitter balanced constrained 1`] = `\n\"┌────────┬────────┬─────────┬─────────┬────────┬─────────┐  \n│Provider│Compute │Storage  │Pricing  │Regions │Use Cases│  \n│        │Services│Solutions│Model    │        │         │  \n├────────┼────────┼─────────┼─────────┼────────┼─────────┤  \n│Amazon  │EC2     │S3 tiers,│Pay as   │Global  │Enterpris│  \n│Web     │instance│ EBS,    │you go,  │regions │e migrati│  \n│Services│s with e│EFS, and │reserved │and     │on, analy│  \n│        │xtensive│archive  │terms,   │many    │tics, ML,│  \n│        │ options│classes  │and      │edge    │ and back│  \n│        │ for gen│for long │discounte│location│end servi│  \n│        │eral, me│retention│d spot ca│s       │ces      │  \n│        │mory, an│         │pacity   │        │         │  \n│        │d accele│         │         │        │         │  \n│        │rated wo│         │         │        │         │  \n│        │rkloads │         │         │        │         │  \n└────────┴────────┴─────────┴─────────┴────────┴─────────┘  \n\"\n`;\n\nexports[`TextTableRenderable rebuilds table when content setter is used: content setter update 1`] = `\n\"┌─────┬───────┐                                             \n│Col 1│Col 2  │                                             \n├─────┼───────┤                                             \n│row-1│updated│                                             \n├─────┼───────┤                                             \n│row-2│active │                                             \n└─────┴───────┘                                             \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`TextTableRenderable keeps borders aligned with CJK and emoji content: unicode border alignment 1`] = `\n\"┌──────┬──────────────┐                                     \n│Locale│Sample        │                                     \n├──────┼──────────────┤                                     \n│ja-JP │東京で寿司 🍣 │                                     \n├──────┼──────────────┤                                     \n│zh-CN │你好世界 🚀   │                                     \n├──────┼──────────────┤                                     \n│ko-KR │한글 테스트 😄│                                     \n└──────┴──────────────┘                                     \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`TextTableRenderable wraps CJK and emoji without grapheme duplication: unicode wrapping 1`] = `\n\"┌───┬────────────────────────┐                              \n│Ite│Details                 │                              \n│m  │                        │                              \n├───┼────────────────────────┤                              \n│mix│東京界 🌍 emoji         │                              \n│ed │wrapping continues      │                              \n│   │across lines for width  │                              \n│   │checks                  │                              \n├───┼────────────────────────┤                              \n│emo│Faces 😀😃😄 should     │                              \n│ji │remain stable           │                              \n└───┴────────────────────────┘                              \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`TextTableRenderable keeps full wrapped table layouts after a wide-to-narrow demo-style resize: demo resize expected primary table 1`] = `\n\"┌─────────────────────────┬─────────────┬────────────────────────────┐\n│Task                     │Owner        │ETA                         │\n├─────────────────────────┼─────────────┼────────────────────────────┤\n│Wrap regression in       │core         │done after validating none, │\n│operational status       │platform and │word, and char wrap modes   │\n│dashboard with dynamic   │runtime      │across narrow, medium, wide,│\n│row heights and          │reliability  │ and ultra-wide terminal    │\n│constrained layout       │squad        │widths                      │\n│validation               │             │                            │\n├─────────────────────────┼─────────────┼────────────────────────────┤\n│Unicode layout           │render       │in review with follow-up    │\n│stabilization for mixed  │pipeline     │checks for border style     │\n│Latin, punctuation,      │maintainers  │transitions, cell padding   │\n│symbols, and long        │with         │variants, and selection     │\n│identifiers in adjacent  │fallback     │range consistency           │\n│columns                  │shaping      │                            │\n│                         │support      │                            │\n├─────────────────────────┼─────────────┼────────────────────────────┤\n│Snapshot pass for table  │qa           │today pending final         │\n│rendering in content     │automation   │baseline updates for        │\n│mode and full mode with  │and visual   │oversized fixtures that     │\n│heavy and double border  │diff triage  │intentionally stress        │\n│combinations             │group        │wrapping behavior on high-  │\n│                         │             │resolution terminals        │\n├─────────────────────────┼─────────────┼────────────────────────────┤\n│Document edge cases      │developer    │planned for this sprint     │\n│where long tokens        │experience   │once final reproducible     │\n│without spaces force     │and docs     │examples are captured and   │\n│char wrapping and reveal │tooling      │linked to regression        │\n│per-cell clipping        │             │tracking tickets            │\n│regressions              │             │                            │\n├─────────────────────────┼─────────────┼────────────────────────────┤\n│Performance sweep of     │runtime      │scheduled after review,     │\n│wrapping algorithm under │performance  │with benchmark runs on      │\n│large datasets to        │task force   │laptop and desktop          │\n│confirm stable frame     │             │terminals at 200-plus       │\n│times during rapid key   │             │column widths               │\n│toggling                 │             │                            │\n└─────────────────────────┴─────────────┴────────────────────────────┘\n\"\n`;\n\nexports[`TextTableRenderable keeps full wrapped table layouts after a wide-to-narrow demo-style resize: demo resize expected unicode table 1`] = `\n\"┌─────┬──────────────────────────────────────────────────────────────┐\n│Colum│Wrapped Text                                                  │\n│n    │                                                              │\n├─────┼──────────────────────────────────────────────────────────────┤\n│mixed│CJK and emoji wrapping stress case: こんにちは世界 and        │\n│-lang│안녕하세요 세계 and 你好，世界 followed by long English prose │\n│uages│that keeps flowing to test whether each cell wraps naturally  │\n│     │even when the terminal is extremely wide and the row still    │\n│     │needs multiple visual lines for readability 🌍🚀              │\n├─────┼──────────────────────────────────────────────────────────────┤\n│emoji│Faces 😀😃😄😁😆 plus symbols 🧪📦🛰️🔧📊 mixed with version   │\n│-and-│tags like release-candidate-build-2026-02-very-long-token-    │\n│symbo│without-breaks to ensure char wrapping remains stable and no  │\n│ls   │glyph alignment issues appear at column boundaries            │\n├─────┼──────────────────────────────────────────────────────────────┤\n│long-│長文の日本語テキストと中文段落和한국어문장을連続して配置し、  │\n│cjk- │その後に additional English context describing renderer       │\n│phras│behavior, border intersection handling, and selection         │\n│e    │extraction so that this single cell remains a reliable        │\n│     │wrapping torture test.                                        │\n├─────┼──────────────────────────────────────────────────────────────┤\n│mixed│Wrap behavior with punctuation-heavy content: [alpha]{beta}(  │\n│-punc│gamma)<delta>|epsilon| then repeated fragments, commas,       │\n│tuati│semicolons, and slashes to verify token boundaries do not     │\n│on   │break border drawing logic or spacing consistency in          │\n│     │neighboring columns.                                          │\n└─────┴──────────────────────────────────────────────────────────────┘\n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts",
    "content": "import { describe, test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../../testing.js\"\nimport { LineNumberRenderable } from \"../LineNumberRenderable.js\"\nimport { CodeRenderable } from \"../Code.js\"\nimport { ScrollBoxRenderable } from \"../ScrollBox.js\"\nimport { SyntaxStyle } from \"../../syntax-style.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet captureCharFrame: () => string\n\nbeforeEach(async () => {\n  const testRenderer = await createTestRenderer({ width: 50, height: 40 })\n  currentRenderer = testRenderer.renderer\n  renderOnce = testRenderer.renderOnce\n  captureCharFrame = testRenderer.captureCharFrame\n})\n\nafterEach(async () => {\n  if (currentRenderer) {\n    currentRenderer.destroy()\n  }\n})\n\ndescribe(\"LineNumber in ScrollBox - Simple Core Test\", () => {\n  test(\"LineNumber with Code in ScrollBox should wrap content height\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const codeContent = `function test() {\n  return true;\n}`\n\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code\",\n      content: codeContent,\n      filetype: \"javascript\",\n      syntaxStyle,\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number\",\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#888888\",\n    })\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll\",\n      width: \"100%\",\n      height: \"100%\",\n      scrollbarOptions: { visible: false },\n    })\n\n    scrollBox.add(lineNumberRenderable)\n    currentRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    expect(codeRenderable.lineCount).toBe(3)\n\n    // LineNumber should wrap to content height (3 lines), not fill viewport (40 lines)\n    expect(lineNumberRenderable.height).toBe(3)\n    expect(codeRenderable.height).toBe(3)\n\n    // Gutter should also be 3 lines\n    expect(lineNumberRenderable[\"gutter\"]!.height).toBe(3)\n\n    // Check content is visible\n    expect(frame).toContain(\"function test\")\n    expect(frame).toContain(\"return true\")\n  })\n\n  test(\"Multiple LineNumber blocks in ScrollBox should each wrap content\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-multi\",\n      width: \"100%\",\n      height: \"100%\",\n      scrollbarOptions: { visible: false },\n    })\n\n    currentRenderer.root.add(scrollBox)\n\n    // Add first code block (1 line)\n    const code1 = new CodeRenderable(currentRenderer, {\n      id: \"code1\",\n      content: \"const x = 1;\",\n      filetype: \"javascript\",\n      syntaxStyle,\n    })\n\n    const lineNum1 = new LineNumberRenderable(currentRenderer, {\n      id: \"linenum1\",\n      target: code1,\n      minWidth: 2,\n      paddingRight: 1,\n      fg: \"#888888\",\n    })\n\n    scrollBox.add(lineNum1)\n\n    // Add second code block (1 line)\n    const code2 = new CodeRenderable(currentRenderer, {\n      id: \"code2\",\n      content: \"const y = 2;\",\n      filetype: \"javascript\",\n      syntaxStyle,\n    })\n\n    const lineNum2 = new LineNumberRenderable(currentRenderer, {\n      id: \"linenum2\",\n      target: code2,\n      minWidth: 2,\n      paddingRight: 1,\n      fg: \"#888888\",\n    })\n\n    scrollBox.add(lineNum2)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    expect(lineNum1.height).toBe(1)\n    expect(code1.height).toBe(1)\n    expect(lineNum2.height).toBe(1)\n    expect(code2.height).toBe(1)\n\n    // Both code blocks should be visible\n    expect(frame).toContain(\"const x = 1\")\n    expect(frame).toContain(\"const y = 2\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts",
    "content": "import { describe, test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../../testing.js\"\nimport { LineNumberRenderable } from \"../LineNumberRenderable.js\"\nimport { CodeRenderable } from \"../Code.js\"\nimport { BoxRenderable } from \"../Box.js\"\nimport { ScrollBoxRenderable } from \"../ScrollBox.js\"\nimport { SyntaxStyle } from \"../../syntax-style.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet captureCharFrame: () => string\n\nbeforeEach(async () => {\n  const testRenderer = await createTestRenderer({ width: 60, height: 20 })\n  currentRenderer = testRenderer.renderer\n  renderOnce = testRenderer.renderOnce\n  captureCharFrame = testRenderer.captureCharFrame\n})\n\nafterEach(async () => {\n  if (currentRenderer) {\n    currentRenderer.destroy()\n  }\n})\n\n// Helper to generate multi-line code content\nfunction generateCode(lineCount: number): string {\n  const lines: string[] = []\n  for (let i = 1; i <= lineCount; i++) {\n    lines.push(`function test${i}() {`)\n    lines.push(`  console.log(\"Line ${i}\");`)\n    lines.push(`  return ${i};`)\n    lines.push(`}`)\n  }\n  return lines.join(\"\\n\")\n}\n\ndescribe(\"LineNumberRenderable in ScrollBox\", () => {\n  test(\"single Code renderable with line numbers in ScrollBox - correct dimensions\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const code = generateCode(20) // 80 lines of code\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code-1\",\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      width: \"100%\",\n      height: \"auto\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number-1\",\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#888888\",\n      bg: \"transparent\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const box = new BoxRenderable(currentRenderer, {\n      id: \"box-1\",\n      border: true,\n      borderStyle: \"single\",\n      borderColor: \"#ffffff\",\n      width: 30,\n      height: 10,\n    })\n\n    box.add(lineNumberRenderable)\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-1\",\n      width: \"100%\",\n      height: \"100%\",\n      scrollY: true,\n      scrollX: false,\n    })\n\n    scrollBox.add(box)\n    currentRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    // Check initial dimensions\n    const gutter = lineNumberRenderable[\"gutter\"]\n    expect(gutter).toBeDefined()\n    expect(gutter!.width).toBeGreaterThan(0)\n    expect(gutter!.height).toBeGreaterThan(0)\n\n    // Box should have correct dimensions (minus borders)\n    expect(box.width).toBe(30)\n    expect(box.height).toBe(10)\n\n    // LineNumberRenderable should fill the box (minus borders)\n    expect(lineNumberRenderable.width).toBe(28) // 30 - 2 for borders\n    expect(lineNumberRenderable.height).toBe(8) // 10 - 2 for borders\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n  })\n\n  test(\"single Code renderable in ScrollBox - scroll and verify dimensions\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const code = generateCode(30) // 120 lines of code\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code-scroll\",\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      width: \"100%\",\n      height: \"auto\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number-scroll\",\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#888888\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const box = new BoxRenderable(currentRenderer, {\n      id: \"box-scroll\",\n      border: true,\n      width: 40,\n      height: 12,\n    })\n\n    box.add(lineNumberRenderable)\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-scroll\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    scrollBox.add(box)\n    currentRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    const gutterBeforeScroll = lineNumberRenderable[\"gutter\"]!\n    const widthBeforeScroll = gutterBeforeScroll.width\n    const heightBeforeScroll = gutterBeforeScroll.height\n    const lineNumWidthBeforeScroll = lineNumberRenderable.width\n    const lineNumHeightBeforeScroll = lineNumberRenderable.height\n\n    const frameBeforeScroll = captureCharFrame()\n    expect(frameBeforeScroll).toMatchSnapshot()\n\n    // Scroll down\n    scrollBox.scrollBy(10)\n    await renderOnce()\n\n    const gutterAfterScroll = lineNumberRenderable[\"gutter\"]!\n    const widthAfterScroll = gutterAfterScroll.width\n    const heightAfterScroll = gutterAfterScroll.height\n    const lineNumWidthAfterScroll = lineNumberRenderable.width\n    const lineNumHeightAfterScroll = lineNumberRenderable.height\n\n    // Dimensions should remain stable after scrolling\n    expect(widthAfterScroll).toBe(widthBeforeScroll)\n    expect(heightAfterScroll).toBe(heightBeforeScroll)\n    expect(lineNumWidthAfterScroll).toBe(lineNumWidthBeforeScroll)\n    expect(lineNumHeightAfterScroll).toBe(lineNumHeightBeforeScroll)\n\n    const frameAfterScroll = captureCharFrame()\n    expect(frameAfterScroll).toMatchSnapshot()\n\n    // Scroll to bottom\n    scrollBox.scrollBy(1000)\n    await renderOnce()\n\n    const widthAtBottom = lineNumberRenderable[\"gutter\"]!.width\n    const heightAtBottom = lineNumberRenderable[\"gutter\"]!.height\n\n    expect(widthAtBottom).toBe(widthBeforeScroll)\n    expect(heightAtBottom).toBe(heightBeforeScroll)\n\n    const frameAtBottom = captureCharFrame()\n    expect(frameAtBottom).toMatchSnapshot()\n  })\n\n  test(\"multiple Code renderables with line numbers in ScrollBox - correct dimensions\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-multi\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    currentRenderer.root.add(scrollBox)\n\n    const boxes: BoxRenderable[] = []\n    const lineNumberRenderables: LineNumberRenderable[] = []\n\n    // Create 3 code blocks with line numbers in boxes\n    for (let i = 1; i <= 3; i++) {\n      const code = generateCode(5 + i * 2) // Different sizes\n      const codeRenderable = new CodeRenderable(currentRenderer, {\n        id: `code-${i}`,\n        content: code,\n        filetype: \"javascript\",\n        syntaxStyle,\n        width: \"100%\",\n        height: \"auto\",\n      })\n\n      const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n        id: `line-number-${i}`,\n        target: codeRenderable,\n        minWidth: 3,\n        paddingRight: 1,\n        fg: \"#888888\",\n        width: \"100%\",\n        height: \"100%\",\n      })\n\n      const box = new BoxRenderable(currentRenderer, {\n        id: `box-${i}`,\n        border: true,\n        width: 50,\n        height: 8,\n        marginBottom: 2,\n      })\n\n      box.add(lineNumberRenderable)\n      scrollBox.add(box)\n\n      boxes.push(box)\n      lineNumberRenderables.push(lineNumberRenderable)\n    }\n\n    await renderOnce()\n\n    const frame1 = captureCharFrame()\n    expect(frame1).toMatchSnapshot()\n\n    // Verify all boxes have correct dimensions\n    for (let i = 0; i < 3; i++) {\n      const box = boxes[i]\n      expect(box.width).toBe(50)\n      expect(box.height).toBe(8)\n\n      const lineNumberRenderable = lineNumberRenderables[i]\n      expect(lineNumberRenderable.width).toBe(48) // 50 - 2 for borders\n      expect(lineNumberRenderable.height).toBe(6) // 8 - 2 for borders\n\n      const gutter = lineNumberRenderable[\"gutter\"]\n      expect(gutter).toBeDefined()\n      expect(gutter!.width).toBeGreaterThan(0)\n      expect(gutter!.height).toBe(6)\n    }\n\n    // Scroll down\n    scrollBox.scrollBy(5)\n    await renderOnce()\n\n    const frame2 = captureCharFrame()\n    expect(frame2).toMatchSnapshot()\n\n    // Dimensions should remain stable\n    for (let i = 0; i < 3; i++) {\n      const lineNumberRenderable = lineNumberRenderables[i]\n      expect(lineNumberRenderable.width).toBe(48)\n      expect(lineNumberRenderable.height).toBe(6)\n\n      const gutter = lineNumberRenderable[\"gutter\"]\n      expect(gutter!.width).toBeGreaterThan(0)\n      expect(gutter!.height).toBe(6)\n    }\n  })\n\n  test(\"nested boxes with different border styles - dimensions correct\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const code = generateCode(25)\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code-nested\",\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      width: \"100%\",\n      height: \"auto\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number-nested\",\n      target: codeRenderable,\n      minWidth: 4,\n      paddingRight: 2,\n      fg: \"#888888\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    // Inner box with border\n    const innerBox = new BoxRenderable(currentRenderer, {\n      id: \"inner-box\",\n      border: true,\n      borderStyle: \"rounded\",\n      borderColor: \"#00ff00\",\n      padding: 1,\n      width: 45,\n      height: 15,\n    })\n\n    innerBox.add(lineNumberRenderable)\n\n    // Outer box with border\n    const outerBox = new BoxRenderable(currentRenderer, {\n      id: \"outer-box\",\n      border: true,\n      borderStyle: \"double\",\n      borderColor: \"#ff0000\",\n      padding: 2,\n      width: 55,\n      height: 19,\n    })\n\n    outerBox.add(innerBox)\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-nested\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    scrollBox.add(outerBox)\n    currentRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    const frame1 = captureCharFrame()\n    expect(frame1).toMatchSnapshot()\n\n    // Check outer box\n    expect(outerBox.width).toBe(55)\n    expect(outerBox.height).toBe(19)\n\n    // Check inner box (inside outer box padding and borders)\n    expect(innerBox.width).toBe(45)\n    expect(innerBox.height).toBe(15)\n\n    // Check line number renderable (inside inner box padding and borders)\n    // Inner box: 45 - 2 (borders) - 2 (padding) = 41\n    expect(lineNumberRenderable.width).toBe(41)\n    // Inner box: 15 - 2 (borders) - 2 (padding) = 11\n    expect(lineNumberRenderable.height).toBe(11)\n\n    const gutter = lineNumberRenderable[\"gutter\"]!\n    expect(gutter.width).toBeGreaterThan(4) // At least minWidth + padding\n    expect(gutter.height).toBe(11)\n\n    // Scroll and verify dimensions remain stable\n    scrollBox.scrollBy(20)\n    await renderOnce()\n\n    const frame2 = captureCharFrame()\n    expect(frame2).toMatchSnapshot()\n\n    expect(lineNumberRenderable.width).toBe(41)\n    expect(lineNumberRenderable.height).toBe(11)\n    expect(gutter.width).toBeGreaterThan(4)\n    expect(gutter.height).toBe(11)\n  })\n\n  test(\"ScrollBox with horizontal and vertical scrolling - dimensions stable\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    // Create very long lines\n    const longLines: string[] = []\n    for (let i = 1; i <= 50; i++) {\n      longLines.push(\n        `const veryLongVariableName${i} = \"This is a very long line that should require horizontal scrolling to view completely\";`,\n      )\n    }\n    const code = longLines.join(\"\\n\")\n\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code-long\",\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      width: \"auto\",\n      height: \"auto\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number-long\",\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#888888\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const box = new BoxRenderable(currentRenderer, {\n      id: \"box-long\",\n      border: true,\n      width: 50,\n      height: 15,\n    })\n\n    box.add(lineNumberRenderable)\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-long\",\n      width: \"100%\",\n      height: \"100%\",\n      scrollX: true,\n      scrollY: true,\n    })\n\n    scrollBox.add(box)\n    currentRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    const initialWidth = lineNumberRenderable.width\n    const initialHeight = lineNumberRenderable.height\n    const initialGutterWidth = lineNumberRenderable[\"gutter\"]!.width\n    const initialGutterHeight = lineNumberRenderable[\"gutter\"]!.height\n\n    const frame1 = captureCharFrame()\n    expect(frame1).toMatchSnapshot()\n\n    // Scroll vertically\n    scrollBox.scrollBy({ x: 0, y: 10 })\n    await renderOnce()\n\n    expect(lineNumberRenderable.width).toBe(initialWidth)\n    expect(lineNumberRenderable.height).toBe(initialHeight)\n    expect(lineNumberRenderable[\"gutter\"]!.width).toBe(initialGutterWidth)\n    expect(lineNumberRenderable[\"gutter\"]!.height).toBe(initialGutterHeight)\n\n    const frame2 = captureCharFrame()\n    expect(frame2).toMatchSnapshot()\n\n    // Scroll horizontally (shouldn't affect line numbers much)\n    scrollBox.scrollBy({ x: 20, y: 0 })\n    await renderOnce()\n\n    expect(lineNumberRenderable.width).toBe(initialWidth)\n    expect(lineNumberRenderable.height).toBe(initialHeight)\n    expect(lineNumberRenderable[\"gutter\"]!.width).toBe(initialGutterWidth)\n    expect(lineNumberRenderable[\"gutter\"]!.height).toBe(initialGutterHeight)\n\n    const frame3 = captureCharFrame()\n    expect(frame3).toMatchSnapshot()\n\n    // Scroll both\n    scrollBox.scrollBy({ x: 10, y: 15 })\n    await renderOnce()\n\n    expect(lineNumberRenderable.width).toBe(initialWidth)\n    expect(lineNumberRenderable.height).toBe(initialHeight)\n    expect(lineNumberRenderable[\"gutter\"]!.width).toBe(initialGutterWidth)\n    expect(lineNumberRenderable[\"gutter\"]!.height).toBe(initialGutterHeight)\n\n    const frame4 = captureCharFrame()\n    expect(frame4).toMatchSnapshot()\n  })\n\n  test(\"gutter width changes with line count - verify remeasure\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    // Start with 9 lines (1 digit line numbers)\n    let code = generateCode(2) // 8 lines\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code-growing\",\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      width: \"100%\",\n      height: \"auto\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number-growing\",\n      target: codeRenderable,\n      minWidth: 2,\n      paddingRight: 1,\n      fg: \"#888888\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const box = new BoxRenderable(currentRenderer, {\n      id: \"box-growing\",\n      border: true,\n      width: 40,\n      height: 12,\n    })\n\n    box.add(lineNumberRenderable)\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-growing\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    scrollBox.add(box)\n    currentRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    const widthWith1Digit = lineNumberRenderable[\"gutter\"]!.width\n    const frame1 = captureCharFrame()\n    expect(frame1).toMatchSnapshot()\n    // minWidth is 2, paddingRight is 1, so minimum is 3 (2 + 1)\n    // But also includes +1 for left padding and maxBeforeWidth/maxAfterWidth (0 in this case)\n    // So base minimum is 4 total for 1 digit numbers\n\n    // Now update to have more than 9 lines (2 digit line numbers)\n    code = generateCode(5) // 20 lines\n    codeRenderable.content = code\n    await renderOnce()\n\n    const widthWith2Digits = lineNumberRenderable[\"gutter\"]!.width\n    const frame2 = captureCharFrame()\n    expect(frame2).toMatchSnapshot()\n\n    // Width stays the same because minWidth 2 is still enough for 2-digit numbers\n    // The gutter width calculation is: max(minWidth, digits + paddingRight + 1)\n    // For 20 lines: max(2, 2 + 1 + 1) = max(2, 4) = 4\n    // But we started with at least 3 or 4\n    expect(widthWith2Digits).toBeGreaterThanOrEqual(widthWith1Digit)\n\n    // Now update to have more than 99 lines (3 digit line numbers)\n    code = generateCode(30) // 120 lines\n    codeRenderable.content = code\n    await renderOnce()\n\n    const widthWith3Digits = lineNumberRenderable[\"gutter\"]!.width\n    const frame3 = captureCharFrame()\n    expect(frame3).toMatchSnapshot()\n\n    // Width should increase for 3-digit numbers\n    // For 120 lines: max(2, 3 + 1 + 1) = max(2, 5) = 5\n    expect(widthWith3Digits).toBeGreaterThan(widthWith2Digits)\n  })\n\n  test(\"line colors span full width in ScrollBox\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const code = generateCode(15)\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code-colors\",\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      width: \"100%\",\n      height: \"auto\",\n    })\n\n    const lineColors = new Map<number, string>()\n    lineColors.set(2, \"#2d4a2e\") // Green for line 3\n    lineColors.set(5, \"#4a2d2d\") // Red for line 6\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number-colors\",\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const box = new BoxRenderable(currentRenderer, {\n      id: \"box-colors\",\n      border: true,\n      width: 50,\n      height: 12,\n    })\n\n    box.add(lineNumberRenderable)\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-colors\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    scrollBox.add(box)\n    currentRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Scroll to make line 5 visible at top\n    scrollBox.scrollBy(5)\n    await renderOnce()\n\n    const frame2 = captureCharFrame()\n    expect(frame2).toMatchSnapshot()\n  })\n\n  test(\"viewport culling with line numbers - dimensions stable\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-culling\",\n      width: \"100%\",\n      height: \"100%\",\n      viewportCulling: true,\n    })\n\n    currentRenderer.root.add(scrollBox)\n\n    const boxes: BoxRenderable[] = []\n\n    // Add many boxes - only visible ones should be rendered\n    for (let i = 1; i <= 20; i++) {\n      const code = generateCode(3)\n      const codeRenderable = new CodeRenderable(currentRenderer, {\n        id: `code-culling-${i}`,\n        content: code,\n        filetype: \"javascript\",\n        syntaxStyle,\n        width: \"100%\",\n        height: \"auto\",\n      })\n\n      const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n        id: `line-number-culling-${i}`,\n        target: codeRenderable,\n        minWidth: 3,\n        paddingRight: 1,\n        fg: \"#888888\",\n        width: \"100%\",\n        height: \"100%\",\n      })\n\n      const box = new BoxRenderable(currentRenderer, {\n        id: `box-culling-${i}`,\n        border: true,\n        width: 45,\n        height: 6,\n        marginBottom: 1,\n      })\n\n      box.add(lineNumberRenderable)\n      scrollBox.add(box)\n      boxes.push(box)\n    }\n\n    await renderOnce()\n\n    const frame1 = captureCharFrame()\n    expect(frame1).toMatchSnapshot()\n\n    // Scroll through content\n    for (let scroll = 0; scroll < 100; scroll += 10) {\n      scrollBox.scrollBy(10)\n      await renderOnce()\n\n      // Check that first few boxes have correct dimensions\n      for (let i = 0; i < 5 && i < boxes.length; i++) {\n        const box = boxes[i]\n        expect(box.width).toBe(45)\n        expect(box.height).toBe(6)\n      }\n    }\n\n    const frame2 = captureCharFrame()\n    expect(frame2).toMatchSnapshot()\n  })\n\n  test(\"EXPECTED FAILURE: Box width changes unexpectedly on first few renders\", async () => {\n    // This test documents a known issue where box widths may flicker on initial renders\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const code = generateCode(30)\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code-flicker\",\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      width: \"100%\",\n      height: \"auto\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number-flicker\",\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#888888\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const box = new BoxRenderable(currentRenderer, {\n      id: \"box-flicker\",\n      border: true,\n      width: 40,\n      height: 12,\n    })\n\n    box.add(lineNumberRenderable)\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-flicker\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    scrollBox.add(box)\n    currentRenderer.root.add(scrollBox)\n\n    // Capture dimensions across multiple renders\n    const widths: number[] = []\n    const heights: number[] = []\n\n    for (let i = 0; i < 5; i++) {\n      await renderOnce()\n      widths.push(box.width)\n      heights.push(box.height)\n    }\n\n    // This assertion SHOULD pass if the bug is fixed\n    // If it fails, it documents the flickering issue\n    const allWidthsSame = widths.every((w) => w === widths[0])\n    const allHeightsSame = heights.every((h) => h === heights[0])\n\n    expect(allWidthsSame).toBe(true)\n    expect(allHeightsSame).toBe(true)\n  })\n\n  test(\"EXPECTED FAILURE: Gutter height may not match parent height initially\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const code = generateCode(50)\n    const codeRenderable = new CodeRenderable(currentRenderer, {\n      id: \"code-height\",\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      width: \"100%\",\n      height: \"auto\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(currentRenderer, {\n      id: \"line-number-height\",\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#888888\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const box = new BoxRenderable(currentRenderer, {\n      id: \"box-height\",\n      border: true,\n      width: 40,\n      height: 15,\n    })\n\n    box.add(lineNumberRenderable)\n\n    const scrollBox = new ScrollBoxRenderable(currentRenderer, {\n      id: \"scroll-height\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    scrollBox.add(box)\n    currentRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    const gutter = lineNumberRenderable[\"gutter\"]!\n    const expectedHeight = lineNumberRenderable.height\n\n    // Gutter should have same height as its parent LineNumberRenderable\n    // This may fail if there's a layout issue\n    expect(gutter.height).toBe(expectedHeight)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/LineNumberRenderable.test.ts",
    "content": "import { describe, test, expect } from \"bun:test\"\nimport { createTestRenderer } from \"../../testing/test-renderer.js\"\nimport { TextBufferRenderable } from \"../TextBufferRenderable.js\"\nimport { LineNumberRenderable } from \"../LineNumberRenderable.js\"\nimport { BoxRenderable } from \"../Box.js\"\nimport { TextareaRenderable } from \"../Textarea.js\"\nimport { t, fg, bold, cyan } from \"../../lib/styled-text.js\"\n\nconst initialContent = `Welcome to the TextareaRenderable Demo!\n\nThis is an interactive text editor powered by EditBuffer and EditorView.\n\n\\tThis is a tab\n\\t\\t\\tMultiple tabs\n\nEmojis:\n👩🏽‍💻  👨‍👩‍👧‍👦  🏳️‍🌈  🇺🇸  🇩🇪  🇯🇵  🇮🇳\n\nNAVIGATION:\n  • Arrow keys to move cursor\n  • Home/End for line navigation\n  • Ctrl+A/Ctrl+E for buffer start/end\n  • Alt+F/Alt+B for word forward/backward\n  • Alt+Left/Alt+Right for word forward/backward\n\nSELECTION:\n  • Shift+Arrow keys to select\n  • Shift+Home/End to select to line start/end\n  • Alt+Shift+F/B to select word forward/backward\n  • Alt+Shift+Left/Right to select word forward/backward\n\nEDITING:\n  • Type any text to insert\n  • Backspace/Delete to remove text\n  • Enter to create new lines\n  • Ctrl+D to delete current line\n  • Ctrl+K to delete to line end\n  • Alt+D to delete word forward\n  • Alt+Backspace or Ctrl+W to delete word backward\n\nUNDO/REDO:\n  • Ctrl+Z to undo\n  • Ctrl+Shift+Z or Ctrl+Y to redo\n\nVIEW:\n  • Shift+W to toggle wrap mode (word/char/none)\n  • Shift+L to toggle line numbers\n\nFEATURES:\n  ✓ Grapheme-aware cursor movement\n  ✓ Unicode (emoji 🌟 and CJK 世界, 你好世界, 中文, 한글)\n  ✓ Incremental editing\n  ✓ Text wrapping and viewport management\n  ✓ Undo/redo support\n  ✓ Word-based navigation and deletion\n  ✓ Text selection with shift keys\n\nPress ESC to return to main menu`\n\nclass MockTextBuffer extends TextBufferRenderable {\n  constructor(ctx: any, options: any) {\n    super(ctx, options)\n    this.textBuffer.setText(options.text || \"\")\n  }\n}\n\ndescribe(\"LineNumberRenderable\", () => {\n  test(\"renders line numbers correctly\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    expect(frame).toContain(\" 1 Line 1\")\n    expect(frame).toContain(\" 2 Line 2\")\n    expect(frame).toContain(\" 3 Line 3\")\n  })\n\n  test(\"renders line numbers for wrapping text\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1 is very long and should wrap around multiple lines\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"auto\",\n      height: \"100%\",\n      wrapMode: \"char\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    expect(frame).toContain(\" 1 Line 1\")\n  })\n\n  test(\"renders line colors for diff highlighting\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineColors = new Map<number, string>()\n    lineColors.set(1, \"#2d4a2e\") // Green for line 2 (index 1)\n    lineColors.set(3, \"#4a2d2d\") // Red for line 4 (index 3)\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors: lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n\n    // Helper to get RGBA values from buffer at position\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    // Check line 2 (index 1) has green background in gutter (x=2 is in the gutter)\n    const line2GutterBg = getBgColor(2, 1)\n    expect(line2GutterBg.r).toBeCloseTo(0x2d / 255, 2)\n    expect(line2GutterBg.g).toBeCloseTo(0x4a / 255, 2)\n    expect(line2GutterBg.b).toBeCloseTo(0x2e / 255, 2)\n\n    // Check line 2 (index 1) has darker green background in content area (x=10 is in content)\n    // Content color should be 80% of gutter color\n    const line2ContentBg = getBgColor(10, 1)\n    expect(line2ContentBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line2ContentBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line2ContentBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    // Check line 4 (index 3) has red background in gutter\n    const line4GutterBg = getBgColor(2, 3)\n    expect(line4GutterBg.r).toBeCloseTo(0x4a / 255, 2)\n    expect(line4GutterBg.g).toBeCloseTo(0x2d / 255, 2)\n    expect(line4GutterBg.b).toBeCloseTo(0x2d / 255, 2)\n\n    // Check line 4 (index 3) has darker red background in content area (80% of gutter color)\n    const line4ContentBg = getBgColor(10, 3)\n    expect(line4ContentBg.r).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line4ContentBg.g).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line4ContentBg.b).toBeCloseTo((0x2d / 255) * 0.8, 2)\n\n    // Check line 1 (index 0) has default black background in gutter\n    const line1GutterBg = getBgColor(2, 0)\n    expect(line1GutterBg.r).toBeCloseTo(0, 2)\n    expect(line1GutterBg.g).toBeCloseTo(0, 2)\n    expect(line1GutterBg.b).toBeCloseTo(0, 2)\n  })\n\n  test(\"can dynamically update line colors\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n\n    // Helper to get RGBA values from buffer at position\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    // Initially no colors\n    const line2InitialBg = getBgColor(2, 1)\n    expect(line2InitialBg.r).toBeCloseTo(0, 2)\n    expect(line2InitialBg.g).toBeCloseTo(0, 2)\n    expect(line2InitialBg.b).toBeCloseTo(0, 2)\n\n    // Set line color using setter (gutter will be full color, content will be 80%)\n    lineNumberRenderable.setLineColor(1, \"#2d4a2e\")\n    await renderOnce()\n\n    // Check gutter has full color\n    const line2AfterSetBg = getBgColor(2, 1)\n    expect(line2AfterSetBg.r).toBeCloseTo(0x2d / 255, 2)\n    expect(line2AfterSetBg.g).toBeCloseTo(0x4a / 255, 2)\n    expect(line2AfterSetBg.b).toBeCloseTo(0x2e / 255, 2)\n\n    // Check content has darker color (80%)\n    const line2ContentAfterSetBg = getBgColor(10, 1)\n    expect(line2ContentAfterSetBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line2ContentAfterSetBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line2ContentAfterSetBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    // Clear the line color\n    lineNumberRenderable.clearLineColor(1)\n    await renderOnce()\n\n    const line2AfterClearBg = getBgColor(2, 1)\n    expect(line2AfterClearBg.r).toBeCloseTo(0, 2)\n    expect(line2AfterClearBg.g).toBeCloseTo(0, 2)\n    expect(line2AfterClearBg.b).toBeCloseTo(0, 2)\n\n    // Set multiple colors\n    const newColors = new Map<number, string>()\n    newColors.set(0, \"#2d4a2e\") // Green for line 1\n    newColors.set(2, \"#4a2d2d\") // Red for line 3\n    lineNumberRenderable.setLineColors(newColors)\n    await renderOnce()\n\n    // Check gutter colors (full color)\n    const line1Bg = getBgColor(2, 0)\n    expect(line1Bg.r).toBeCloseTo(0x2d / 255, 2)\n    expect(line1Bg.g).toBeCloseTo(0x4a / 255, 2)\n    expect(line1Bg.b).toBeCloseTo(0x2e / 255, 2)\n\n    const line3Bg = getBgColor(2, 2)\n    expect(line3Bg.r).toBeCloseTo(0x4a / 255, 2)\n    expect(line3Bg.g).toBeCloseTo(0x2d / 255, 2)\n    expect(line3Bg.b).toBeCloseTo(0x2d / 255, 2)\n\n    // Check content colors (80% darker)\n    const line1ContentBg = getBgColor(10, 0)\n    expect(line1ContentBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line1ContentBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line1ContentBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    const line3ContentBg = getBgColor(10, 2)\n    expect(line3ContentBg.r).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line3ContentBg.g).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line3ContentBg.b).toBeCloseTo((0x2d / 255) * 0.8, 2)\n\n    // Clear all colors\n    lineNumberRenderable.clearAllLineColors()\n    await renderOnce()\n\n    const line1AfterClearAllBg = getBgColor(2, 0)\n    expect(line1AfterClearAllBg.r).toBeCloseTo(0, 2)\n    expect(line1AfterClearAllBg.g).toBeCloseTo(0, 2)\n    expect(line1AfterClearAllBg.b).toBeCloseTo(0, 2)\n  })\n\n  test(\"renders line colors for wrapped lines\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1 is very long and should wrap around multiple lines\\nLine 2\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"auto\",\n      height: \"100%\",\n      wrapMode: \"char\",\n    })\n\n    const lineColors = new Map<number, string>()\n    lineColors.set(0, \"#2d4a2e\") // Green for line 1 (index 0, which wraps)\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors: lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n\n    // Helper to get RGBA values from buffer at position\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    // First visual line of logical line 0 should have green background in gutter\n    const line0Visual0GutterBg = getBgColor(2, 0)\n    expect(line0Visual0GutterBg.r).toBeCloseTo(0x2d / 255, 2)\n    expect(line0Visual0GutterBg.g).toBeCloseTo(0x4a / 255, 2)\n    expect(line0Visual0GutterBg.b).toBeCloseTo(0x2e / 255, 2)\n\n    // First visual line of logical line 0 should have darker green background in content (80%)\n    const line0Visual0ContentBg = getBgColor(10, 0)\n    expect(line0Visual0ContentBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line0Visual0ContentBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line0Visual0ContentBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    // Second visual line of logical line 0 should also have darker green background (wrapped continuation)\n    const line0Visual1Bg = getBgColor(10, 1)\n    expect(line0Visual1Bg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line0Visual1Bg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line0Visual1Bg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    // Third visual line of logical line 0 should also have darker green background (wrapped continuation)\n    const line0Visual2Bg = getBgColor(10, 2)\n    expect(line0Visual2Bg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line0Visual2Bg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line0Visual2Bg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n  })\n\n  test(\"renders line colors correctly within a box with borders\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineColors = new Map<number, string>()\n    lineColors.set(1, \"#2d4a2e\") // Green for line 2 (index 1)\n    lineColors.set(3, \"#4a2d2d\") // Red for line 4 (index 3)\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors: lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const box = new BoxRenderable(renderer, {\n      border: true,\n      borderStyle: \"single\",\n      borderColor: \"#ffffff\",\n      backgroundColor: \"#000000\",\n      width: \"100%\",\n      height: \"100%\",\n      padding: 1,\n    })\n\n    box.add(lineNumberRenderable)\n    renderer.root.add(box)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n    const charBuffer = buffer.buffers.char\n\n    // Helper to get RGBA values from buffer at position\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    const getChar = (x: number, y: number) => {\n      return charBuffer[y * buffer.width + x]\n    }\n\n    // Box has borders at x=0 (left) and x=29 (right)\n    // Box has padding of 1, so content starts at x=2 (after left border + padding)\n    // Gutter is about 5 chars wide (minWidth 3 + padding + margin)\n    // Content starts around x=7\n\n    // Line 2 (y=3, accounting for top border + padding + 1 line)\n    const line2Y = 3\n\n    // Check that left border is NOT colored (should be white border)\n    const leftBorderChar = getChar(0, line2Y)\n    expect(leftBorderChar).toBe(0x2502) // Vertical line character │\n\n    // Check that right border is NOT colored (should be white border)\n    const rightBorderChar = getChar(29, line2Y)\n    expect(rightBorderChar).toBe(0x2502) // Vertical line character │\n\n    // Check that gutter area (inside padding) has green background\n    const gutterBg = getBgColor(4, line2Y)\n    expect(gutterBg.r).toBeCloseTo(0x2d / 255, 2)\n    expect(gutterBg.g).toBeCloseTo(0x4a / 255, 2)\n    expect(gutterBg.b).toBeCloseTo(0x2e / 255, 2)\n\n    // Check that content area has darker green background (80% of gutter color)\n    const contentBg = getBgColor(15, line2Y)\n    expect(contentBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(contentBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(contentBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    // Check that area near right border (but not the border itself) has darker green background\n    const nearRightBg = getBgColor(27, line2Y)\n    expect(nearRightBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(nearRightBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(nearRightBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    // Verify line without color (line 1, y=2) doesn't have green background\n    const line1Y = 2\n    const line1ContentBg = getBgColor(15, line1Y)\n    expect(line1ContentBg.r).toBeCloseTo(0, 2)\n    expect(line1ContentBg.g).toBeCloseTo(0, 2)\n    expect(line1ContentBg.b).toBeCloseTo(0, 2)\n  })\n\n  test(\"renders full-width line colors when line numbers are hidden\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineColors = new Map<number, string>()\n    lineColors.set(1, \"#2d4a2e\") // Green for line 2 (index 1)\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors: lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    // First render with line numbers visible\n    await renderOnce()\n    const frameWithLineNumbers = captureCharFrame()\n\n    // Hide line numbers\n    lineNumberRenderable.showLineNumbers = false\n\n    await renderOnce()\n    const frameWithoutLineNumbers = captureCharFrame()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n\n    // Helper to get RGBA values from buffer at position\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    // Debug: check if text moved to x=0\n    expect(frameWithoutLineNumbers).toContain(\"Line 1\")\n    expect(frameWithoutLineNumbers.split(\"\\n\")[1]).toMatch(/^Line 2/)\n\n    // When line numbers are hidden, the content background (darker, 80%) should start at x=0\n    const line2LeftEdgeBg = getBgColor(0, 1)\n    expect(line2LeftEdgeBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line2LeftEdgeBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line2LeftEdgeBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    // Check middle of line also has darker background\n    const line2MiddleBg = getBgColor(10, 1)\n    expect(line2MiddleBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line2MiddleBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line2MiddleBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n\n    // Check right edge has darker background\n    const line2RightEdgeBg = getBgColor(19, 1)\n    expect(line2RightEdgeBg.r).toBeCloseTo((0x2d / 255) * 0.8, 2)\n    expect(line2RightEdgeBg.g).toBeCloseTo((0x4a / 255) * 0.8, 2)\n    expect(line2RightEdgeBg.b).toBeCloseTo((0x2e / 255) * 0.8, 2)\n  })\n\n  test(\"renders line signs before and after line numbers\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineSigns = new Map<number, any>()\n    lineSigns.set(1, { after: \"+\" }) // Line 2: Added\n    lineSigns.set(3, { after: \"-\" }) // Line 4: Removed\n    lineSigns.set(0, { before: \"⚠️\" }) // Line 1: Warning\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineSigns: lineSigns,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n\n    // Check that signs are present\n    expect(frame).toContain(\"⚠️\") // Warning emoji before line 1\n    expect(frame).toContain(\"+\") // Plus after line 2\n    expect(frame).toContain(\"-\") // Minus after line 4\n\n    // Verify structure: should have emoji, line number, and +/- signs\n    const lines = frame.split(\"\\n\")\n    expect(lines[0]).toMatch(/⚠️.*1/) // Line 1 has warning before number\n    expect(lines[1]).toMatch(/2.*\\+/) // Line 2 has + after number\n    expect(lines[3]).toMatch(/4.*-/) // Line 4 has - after number\n  })\n\n  test(\"renders line signs with custom colors\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineSigns = new Map<number, any>()\n    lineSigns.set(1, { after: \" +\", afterColor: \"#22c55e\" }) // Bright green plus\n    lineSigns.set(0, { before: \"❌\", beforeColor: \"#ef4444\" }) // Bright red error\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#888888\",\n      bg: \"#000000\",\n      lineSigns: lineSigns,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const fgBuffer = buffer.buffers.fg\n\n    // Helper to get RGBA values from buffer at position\n    const getFgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: fgBuffer[offset],\n        g: fgBuffer[offset + 1],\n        b: fgBuffer[offset + 2],\n        a: fgBuffer[offset + 3],\n      }\n    }\n\n    // Find the position of the + sign on line 2 (y=1)\n    // It should be after the line number, so around x=7-8\n    // Check that it has green color (#22c55e = rgb(34, 197, 94))\n    let foundGreenPlus = false\n    for (let x = 5; x < 10; x++) {\n      const fg = getFgColor(x, 1)\n      // Check if color is close to green\n      if (Math.abs(fg.g - 197 / 255) < 0.05 && fg.r < 0.2 && fg.b < 0.5) {\n        foundGreenPlus = true\n        break\n      }\n    }\n    expect(foundGreenPlus).toBe(true)\n\n    // Find the emoji on line 1 (y=0)\n    // It should have red color (#ef4444 = rgb(239, 68, 68))\n    let foundRedEmoji = false\n    for (let x = 0; x < 5; x++) {\n      const fg = getFgColor(x, 0)\n      // Check if color is close to red\n      if (Math.abs(fg.r - 239 / 255) < 0.05 && fg.g < 0.4 && fg.b < 0.4) {\n        foundRedEmoji = true\n        break\n      }\n    }\n    expect(foundRedEmoji).toBe(true)\n  })\n\n  test(\"dynamically updates line signs\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n    let frame = captureCharFrame()\n    expect(frame).not.toContain(\"+\")\n\n    // Add a sign\n    lineNumberRenderable.setLineSign(1, { after: \"+\" })\n    await renderOnce()\n    frame = captureCharFrame()\n    expect(frame).toContain(\"+\")\n\n    // Clear the sign\n    lineNumberRenderable.clearLineSign(1)\n    await renderOnce()\n    frame = captureCharFrame()\n    expect(frame).not.toContain(\"+\")\n  })\n\n  test(\"renders line numbers with offset\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      lineNumberOffset: 41, // Start at line 42\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Line numbers should start at 42 instead of 1\n    expect(frame).toContain(\"42 Line 1\")\n    expect(frame).toContain(\"43 Line 2\")\n    expect(frame).toContain(\"44 Line 3\")\n  })\n\n  test(\"can dynamically update line number offset\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      lineNumberOffset: 0,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    let frame = captureCharFrame()\n    expect(frame).toContain(\" 1 Line 1\")\n    expect(frame).toContain(\" 2 Line 2\")\n\n    // Update offset\n    lineNumberRenderable.lineNumberOffset = 99\n    await renderOnce()\n\n    frame = captureCharFrame()\n    expect(frame).toContain(\"100 Line 1\")\n    expect(frame).toContain(\"101 Line 2\")\n    expect(frame).toContain(\"102 Line 3\")\n  })\n\n  test(\"hides line numbers for specific lines\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const hideLineNumbers = new Set<number>()\n    hideLineNumbers.add(1) // Hide line 2\n    hideLineNumbers.add(3) // Hide line 4\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      hideLineNumbers,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Check that lines 1, 3, 5 have line numbers\n    expect(frame).toContain(\" 1 Line 1\")\n    expect(frame).toContain(\" 3 Line 3\")\n    expect(frame).toContain(\" 5 Line 5\")\n\n    // Lines 2 and 4 should not have line numbers (but text is still visible)\n    const lines = frame.split(\"\\n\")\n\n    // Line 2 should have text but no line number visible\n    expect(lines[1]).toContain(\"Line 2\")\n    expect(lines[1]).not.toMatch(/2\\s+Line 2/)\n\n    // Line 4 should have text but no line number visible\n    expect(lines[3]).toContain(\"Line 4\")\n    expect(lines[3]).not.toMatch(/4\\s+Line 4/)\n  })\n\n  test(\"can dynamically update hidden line numbers\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    let frame = captureCharFrame()\n    expect(frame).toContain(\" 1 Line 1\")\n    expect(frame).toContain(\" 2 Line 2\")\n    expect(frame).toContain(\" 3 Line 3\")\n\n    // Hide line 2\n    const hideSet = new Set<number>()\n    hideSet.add(1)\n    lineNumberRenderable.setHideLineNumbers(hideSet)\n    await renderOnce()\n\n    frame = captureCharFrame()\n    expect(frame).toContain(\" 1 Line 1\")\n    expect(frame).toContain(\"Line 2\") // Text still visible\n    expect(frame).toContain(\" 3 Line 3\")\n\n    const lines = frame.split(\"\\n\")\n    expect(lines[1]).not.toMatch(/2\\s+Line 2/)\n  })\n\n  test(\"combines line number offset with hidden line numbers\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const hideLineNumbers = new Set<number>()\n    hideLineNumbers.add(1) // Hide line at logical index 1\n    hideLineNumbers.add(3) // Hide line at logical index 3\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      lineNumberOffset: 41, // Start at line 42\n      hideLineNumbers,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Line 1 (index 0) should show as line 42\n    expect(frame).toContain(\"42 Line 1\")\n\n    // Line 2 (index 1) should be hidden (but text visible)\n    expect(frame).toContain(\"Line 2\")\n    const lines = frame.split(\"\\n\")\n    expect(lines[1]).not.toMatch(/43\\s+Line 2/)\n\n    // Line 3 (index 2) should show as line 44\n    expect(frame).toContain(\"44 Line 3\")\n\n    // Line 4 (index 3) should be hidden\n    expect(frame).toContain(\"Line 4\")\n    expect(lines[3]).not.toMatch(/45\\s+Line 4/)\n\n    // Line 5 (index 4) should show as line 46\n    expect(frame).toContain(\"46 Line 5\")\n  })\n\n  test(\"gutter width is stable from first render - no width glitch\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    // First render - this is when layout happens\n    await renderOnce()\n\n    // Capture width after first render\n    const gutterAfterFirstRender = lineNumberRenderable[\"gutter\"]\n    const widthAfterFirstRender = gutterAfterFirstRender?.width\n\n    expect(widthAfterFirstRender).toBeGreaterThan(0)\n\n    // Render a second time - width should NOT change (no glitch)\n    await renderOnce()\n\n    const widthAfterSecondRender = lineNumberRenderable[\"gutter\"]?.width\n    expect(widthAfterSecondRender).toBe(widthAfterFirstRender)\n\n    // Render a third time to be absolutely sure\n    await renderOnce()\n\n    const widthAfterThirdRender = lineNumberRenderable[\"gutter\"]?.width\n    expect(widthAfterThirdRender).toBe(widthAfterFirstRender)\n  })\n\n  test(\"gutter width accounts for large line numbers from first render\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      lineNumberOffset: 997, // Will show lines 998, 999, 1000 (4 digits)\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    // First render - this is when layout happens\n    await renderOnce()\n\n    // Capture width after first render\n    const gutterAfterFirstRender = lineNumberRenderable[\"gutter\"]\n    const widthAfterFirstRender = gutterAfterFirstRender?.width\n\n    // Width should be at least 5 (for \"1000\" which is 4 digits + padding)\n    expect(widthAfterFirstRender).toBeGreaterThanOrEqual(5)\n\n    // Render again - width should NOT change (no glitch)\n    await renderOnce()\n\n    const widthAfterSecondRender = lineNumberRenderable[\"gutter\"]?.width\n    expect(widthAfterSecondRender).toBe(widthAfterFirstRender)\n\n    // Render a third time to be absolutely sure\n    await renderOnce()\n\n    const widthAfterThirdRender = lineNumberRenderable[\"gutter\"]?.width\n    expect(widthAfterThirdRender).toBe(widthAfterFirstRender)\n  })\n\n  // TODO: flaky - works locally but fails in CI every time\n  test.skip(\"handles async content loading in Code renderable with drawUnstyledText=false\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    // Import Code renderable\n    const { CodeRenderable } = await import(\"../Code\")\n    const { SyntaxStyle } = await import(\"../../syntax-style\")\n\n    const syntaxStyle = SyntaxStyle.create()\n\n    // Create Code renderable with no initial content and drawUnstyledText=false\n    const codeRenderable = new CodeRenderable(renderer, {\n      content: \"\",\n      filetype: \"typescript\",\n      syntaxStyle,\n      drawUnstyledText: false,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    // First render - no content yet\n    await renderOnce()\n\n    let frame = captureCharFrame()\n\n    // Should have minimal lines (empty buffer may show 1 line)\n    const initialLineCount = codeRenderable.virtualLineCount\n    expect(initialLineCount).toBeLessThanOrEqual(1)\n\n    // Now set content on the Code renderable\n    const code = `function hello() {\\n  console.log(\"Hello\");\\n}`\n    codeRenderable.content = code\n\n    // Wait for render and highlighting\n    await renderOnce()\n    // Give highlighting time to complete (increased for CI)\n    await Bun.sleep(1000)\n    await renderOnce()\n    await Bun.sleep(100)\n    await renderOnce()\n\n    frame = captureCharFrame()\n\n    // Should now show line numbers for the content\n    expect(codeRenderable.virtualLineCount).toBe(3)\n    expect(frame).toContain(\"function\")\n    expect(frame).toContain(\"console\")\n\n    // Check that line numbers are present\n    const lines = frame.split(\"\\n\")\n    expect(lines[0]).toMatch(/1/)\n    expect(lines[1]).toMatch(/2/)\n    expect(lines[2]).toMatch(/3/)\n  })\n\n  test(\"updates line numbers when Code renderable content changes\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    const { CodeRenderable } = await import(\"../Code\")\n    const { SyntaxStyle } = await import(\"../../syntax-style\")\n\n    const syntaxStyle = SyntaxStyle.create()\n\n    // Create Code renderable with initial content\n    const codeRenderable = new CodeRenderable(renderer, {\n      content: \"line 1\\nline 2\",\n      filetype: \"typescript\",\n      syntaxStyle,\n      drawUnstyledText: true,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    // First render\n    await renderOnce()\n    await Bun.sleep(50)\n    await renderOnce()\n\n    let frame = captureCharFrame()\n\n    // Should show 2 lines\n    expect(codeRenderable.virtualLineCount).toBe(2)\n    expect(frame).toContain(\"line 1\")\n    expect(frame).toContain(\"line 2\")\n\n    // Now update content to have more lines\n    codeRenderable.content = \"line 1\\nline 2\\nline 3\\nline 4\\nline 5\"\n\n    await renderOnce()\n    await Bun.sleep(50)\n    await renderOnce()\n\n    frame = captureCharFrame()\n\n    // Should now show 5 lines\n    expect(codeRenderable.virtualLineCount).toBe(5)\n    expect(frame).toContain(\"line 5\")\n\n    // Check that line numbers are present for all 5 lines\n    const lines = frame.split(\"\\n\")\n    expect(lines.length).toBeGreaterThanOrEqual(5)\n    expect(lines[0]).toMatch(/1/)\n    expect(lines[4]).toMatch(/5/)\n  })\n\n  test(\"handles Code renderable switching from no filetype to having filetype\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    const { CodeRenderable } = await import(\"../Code\")\n    const { SyntaxStyle } = await import(\"../../syntax-style\")\n\n    const syntaxStyle = SyntaxStyle.create()\n\n    // Create Code renderable with content but no filetype (plain text fallback)\n    const codeRenderable = new CodeRenderable(renderer, {\n      content: \"function test() {\\n  return 42;\\n}\",\n      filetype: undefined,\n      syntaxStyle,\n      drawUnstyledText: true,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: codeRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    // First render - plain text\n    await renderOnce()\n\n    let frame = captureCharFrame()\n\n    expect(codeRenderable.virtualLineCount).toBe(3)\n    expect(frame).toContain(\"function\")\n\n    // Now set filetype to enable syntax highlighting\n    codeRenderable.filetype = \"typescript\"\n\n    await renderOnce()\n    await Bun.sleep(100)\n    await renderOnce()\n\n    frame = captureCharFrame()\n\n    // Should still show 3 lines with highlighting\n    expect(codeRenderable.virtualLineCount).toBe(3)\n    expect(frame).toContain(\"function\")\n\n    // Line numbers should be present\n    const lines = frame.split(\"\\n\")\n    expect(lines[0]).toMatch(/1/)\n    expect(lines[2]).toMatch(/3/)\n  })\n\n  test(\"maintains consistent left padding for all line numbers\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 30,\n      height: 15,\n    })\n\n    // Create content with 12 lines so we have both 1-digit (1-9) and 2-digit (10-12) line numbers\n    const lines = []\n    for (let i = 1; i <= 12; i++) {\n      lines.push(`Line ${i}`)\n    }\n    const text = lines.join(\"\\n\")\n\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"white\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    const frameLines = frame.split(\"\\n\")\n\n    // Extract the gutter portion (everything before \"Line X\")\n    // For 1-digit line numbers (1-9), they are right-aligned in a 2-digit space\n    // For 2-digit line numbers (10-12), they fill the 2-digit space\n    // Both should have 1 space of left padding\n\n    // Line 1 should have format: \"  1 Line 1\" (1 left pad + 1 space for alignment + \"1\" + 1 paddingRight)\n    expect(frameLines[0]).toMatch(/^  1 Line 1/)\n    const line1Match = frameLines[0].match(/^( +)1 /)\n    expect(line1Match).toBeTruthy()\n    expect(line1Match![1].length).toBe(2) // 1 left padding + 1 alignment space\n\n    // Line 9 should also have the same format as line 1\n    expect(frameLines[8]).toMatch(/^  9 Line 9/)\n    const line9Match = frameLines[8].match(/^( +)9 /)\n    expect(line9Match).toBeTruthy()\n    expect(line9Match![1].length).toBe(2) // 1 left padding + 1 alignment space\n\n    // Line 10 should have format: \" 10 Line 10\" (1 left pad + \"10\" + 1 paddingRight)\n    expect(frameLines[9]).toMatch(/^ 10 Line 10/)\n    const line10Match = frameLines[9].match(/^( +)10 /)\n    expect(line10Match).toBeTruthy()\n    expect(line10Match![1].length).toBe(1) // Just 1 left padding\n\n    // All lines should have at least 1 space of left padding before the first digit\n    for (let i = 0; i < 12; i++) {\n      const lineMatch = frameLines[i].match(/^( +)\\d+/)\n      expect(lineMatch).toBeTruthy()\n      expect(lineMatch![1].length).toBeGreaterThanOrEqual(1)\n    }\n  })\n\n  test(\"supports separate gutter and content colors with LineColorConfig\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineColors = new Map<number, any>()\n    lineColors.set(1, { gutter: \"#2d4a2e\", content: \"#1a2e1f\" }) // Different colors for gutter and content\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors: lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    // Check line 2 (index 1) has the specified gutter color in gutter area (x=2)\n    const line2GutterBg = getBgColor(2, 1)\n    expect(line2GutterBg.r).toBeCloseTo(0x2d / 255, 2)\n    expect(line2GutterBg.g).toBeCloseTo(0x4a / 255, 2)\n    expect(line2GutterBg.b).toBeCloseTo(0x2e / 255, 2)\n\n    // Check line 2 (index 1) has the specified content color in content area (x=10)\n    const line2ContentBg = getBgColor(10, 1)\n    expect(line2ContentBg.r).toBeCloseTo(0x1a / 255, 2)\n    expect(line2ContentBg.g).toBeCloseTo(0x2e / 255, 2)\n    expect(line2ContentBg.b).toBeCloseTo(0x1f / 255, 2)\n  })\n\n  test(\"defaults content color to darker gutter color when only gutter is specified\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineColors = new Map<number, any>()\n    lineColors.set(1, { gutter: \"#50fa7b\" }) // Only gutter color specified\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors: lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    // Check line 2 (index 1) has the specified gutter color in gutter area (x=2)\n    const line2GutterBg = getBgColor(2, 1)\n    const expectedGutterR = 0x50 / 255\n    const expectedGutterG = 0xfa / 255\n    const expectedGutterB = 0x7b / 255\n    expect(line2GutterBg.r).toBeCloseTo(expectedGutterR, 2)\n    expect(line2GutterBg.g).toBeCloseTo(expectedGutterG, 2)\n    expect(line2GutterBg.b).toBeCloseTo(expectedGutterB, 2)\n\n    // Check line 2 (index 1) has a darker color (80%) in content area (x=10)\n    const line2ContentBg = getBgColor(10, 1)\n    expect(line2ContentBg.r).toBeCloseTo(expectedGutterR * 0.8, 2)\n    expect(line2ContentBg.g).toBeCloseTo(expectedGutterG * 0.8, 2)\n    expect(line2ContentBg.b).toBeCloseTo(expectedGutterB * 0.8, 2)\n  })\n\n  test(\"defaults content color to 80% of gutter when using simple string color format\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineColors = new Map<number, string>()\n    lineColors.set(1, \"#ff5555\") // Simple string format\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors: lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    // Check line 2 (index 1) has the specified color in gutter area (x=2)\n    const line2GutterBg = getBgColor(2, 1)\n    const expectedGutterR = 0xff / 255\n    const expectedGutterG = 0x55 / 255\n    const expectedGutterB = 0x55 / 255\n    expect(line2GutterBg.r).toBeCloseTo(expectedGutterR, 2)\n    expect(line2GutterBg.g).toBeCloseTo(expectedGutterG, 2)\n    expect(line2GutterBg.b).toBeCloseTo(expectedGutterB, 2)\n\n    // Check line 2 (index 1) has a darker color (80%) in content area (x=10)\n    const line2ContentBg = getBgColor(10, 1)\n    expect(line2ContentBg.r).toBeCloseTo(expectedGutterR * 0.8, 2)\n    expect(line2ContentBg.g).toBeCloseTo(expectedGutterG * 0.8, 2)\n    expect(line2ContentBg.b).toBeCloseTo(expectedGutterB * 0.8, 2)\n  })\n\n  test(\"dynamically updates line colors with LineColorConfig\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const buffer = renderer.currentRenderBuffer\n    const bgBuffer = buffer.buffers.bg\n\n    const getBgColor = (x: number, y: number) => {\n      const offset = (y * buffer.width + x) * 4\n      return {\n        r: bgBuffer[offset],\n        g: bgBuffer[offset + 1],\n        b: bgBuffer[offset + 2],\n        a: bgBuffer[offset + 3],\n      }\n    }\n\n    // Set line color using LineColorConfig with setLineColor\n    lineNumberRenderable.setLineColor(1, { gutter: \"#2d4a2e\", content: \"#1a2e1f\" })\n    await renderOnce()\n\n    // Check gutter color\n    const line2GutterBg = getBgColor(2, 1)\n    expect(line2GutterBg.r).toBeCloseTo(0x2d / 255, 2)\n    expect(line2GutterBg.g).toBeCloseTo(0x4a / 255, 2)\n    expect(line2GutterBg.b).toBeCloseTo(0x2e / 255, 2)\n\n    // Check content color\n    const line2ContentBg = getBgColor(10, 1)\n    expect(line2ContentBg.r).toBeCloseTo(0x1a / 255, 2)\n    expect(line2ContentBg.g).toBeCloseTo(0x2e / 255, 2)\n    expect(line2ContentBg.b).toBeCloseTo(0x1f / 255, 2)\n\n    // Clear the line color\n    lineNumberRenderable.clearLineColor(1)\n    await renderOnce()\n\n    const line2AfterClearBg = getBgColor(2, 1)\n    expect(line2AfterClearBg.r).toBeCloseTo(0, 2)\n    expect(line2AfterClearBg.g).toBeCloseTo(0, 2)\n    expect(line2AfterClearBg.b).toBeCloseTo(0, 2)\n  })\n\n  test(\"getLineColors returns both gutter and content color maps\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\"\n    const textRenderable = new MockTextBuffer(renderer, {\n      text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineColors = new Map<number, any>()\n    lineColors.set(1, { gutter: \"#2d4a2e\", content: \"#1a2e1f\" })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      lineColors: lineColors,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n\n    await renderOnce()\n\n    const colors = lineNumberRenderable.getLineColors()\n    expect(colors.gutter.size).toBe(1)\n    expect(colors.content.size).toBe(1)\n\n    const gutterColor = colors.gutter.get(1)\n    expect(gutterColor).toBeDefined()\n    expect(gutterColor!.r).toBeCloseTo(0x2d / 255, 2)\n    expect(gutterColor!.g).toBeCloseTo(0x4a / 255, 2)\n    expect(gutterColor!.b).toBeCloseTo(0x2e / 255, 2)\n\n    const contentColor = colors.content.get(1)\n    expect(contentColor).toBeDefined()\n    expect(contentColor!.r).toBeCloseTo(0x1a / 255, 2)\n    expect(contentColor!.g).toBeCloseTo(0x2e / 255, 2)\n    expect(contentColor!.b).toBeCloseTo(0x1f / 255, 2)\n  })\n\n  test(\"highlightLines applies color to a range of lines\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new TextBufferRenderable(renderer, {\n      content: text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n    await renderOnce()\n\n    lineNumberRenderable.highlightLines(1, 3, \"#2d4a2e\")\n    await renderOnce()\n\n    const colors = lineNumberRenderable.getLineColors()\n    expect(colors.gutter.has(0)).toBe(false)\n    expect(colors.gutter.has(1)).toBe(true)\n    expect(colors.gutter.has(2)).toBe(true)\n    expect(colors.gutter.has(3)).toBe(true)\n    expect(colors.gutter.has(4)).toBe(false)\n  })\n\n  test(\"clearHighlightLines removes color from a range of lines\", async () => {\n    const { renderer, renderOnce } = await createTestRenderer({\n      width: 20,\n      height: 10,\n    })\n\n    const text = \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\"\n    const textRenderable = new TextBufferRenderable(renderer, {\n      content: text,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    const lineNumberRenderable = new LineNumberRenderable(renderer, {\n      target: textRenderable,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#ffffff\",\n      bg: \"#000000\",\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(lineNumberRenderable)\n    await renderOnce()\n\n    lineNumberRenderable.highlightLines(0, 4, \"#2d4a2e\")\n    await renderOnce()\n\n    lineNumberRenderable.clearHighlightLines(1, 3)\n    await renderOnce()\n\n    const colors = lineNumberRenderable.getLineColors()\n    expect(colors.gutter.has(0)).toBe(true)\n    expect(colors.gutter.has(1)).toBe(false)\n    expect(colors.gutter.has(2)).toBe(false)\n    expect(colors.gutter.has(3)).toBe(false)\n    expect(colors.gutter.has(4)).toBe(true)\n  })\n\n  test(\"maintains stable visual line count when scrolling and typing with word wrap\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 35,\n      height: 30,\n    })\n\n    const parentContainer = new BoxRenderable(renderer, {\n      id: \"parent-container\",\n      zIndex: 10,\n      padding: 1,\n    })\n    renderer.root.add(parentContainer)\n\n    const editorBox = new BoxRenderable(renderer, {\n      id: \"editor-box\",\n      borderStyle: \"single\",\n      borderColor: \"#6BCF7F\",\n      backgroundColor: \"#0D1117\",\n      title: \"Interactive Editor (TextareaRenderable)\",\n      titleAlignment: \"left\",\n      paddingLeft: 1,\n      paddingRight: 1,\n      border: true,\n    })\n    parentContainer.add(editorBox)\n\n    const editor = new TextareaRenderable(renderer, {\n      id: \"editor\",\n      initialValue: initialContent,\n      textColor: \"#F0F6FC\",\n      selectionBg: \"#264F78\",\n      selectionFg: \"#FFFFFF\",\n      wrapMode: \"word\",\n      showCursor: true,\n      cursorColor: \"#4ECDC4\",\n      placeholder: t`${fg(\"#333333\")(\"Enter\")} ${cyan(bold(\"text\"))} ${fg(\"#333333\")(\"here...\")}`,\n      tabIndicator: \"→\",\n      tabIndicatorColor: \"#30363D\",\n    })\n\n    const editorWithLines = new LineNumberRenderable(renderer, {\n      id: \"editor-lines\",\n      target: editor,\n      minWidth: 3,\n      paddingRight: 1,\n      fg: \"#4b5563\", // gray-600\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    editorBox.add(editorWithLines)\n\n    // Initial render\n    await renderOnce()\n\n    const lineInfoInitial = editor.editorView.getLogicalLineInfo()\n    const visualLinesInitial = lineInfoInitial.lineStartCols.length\n\n    // Move cursor to bottom to trigger scrolling\n    editor.gotoBufferEnd()\n    await renderOnce()\n\n    const lineInfoAfterScroll = editor.editorView.getLogicalLineInfo()\n    const visualLinesAfterScroll = lineInfoAfterScroll.lineStartCols.length\n\n    const frame1 = captureCharFrame()\n    expect(frame1).toMatchSnapshot()\n\n    // Visual line count should remain stable after scrolling\n    expect(visualLinesInitial).toBe(visualLinesAfterScroll)\n\n    // Move cursor to line 49 (index 48) which is an empty line and insert a character\n    editor.editBuffer.setCursor(48, 0)\n    editor.insertChar(\"a\")\n    await renderOnce()\n\n    const lineInfoAfterTyping = editor.editorView.getLogicalLineInfo()\n    const visualLinesAfterTyping = lineInfoAfterTyping.lineStartCols.length\n\n    const frame2 = captureCharFrame()\n    expect(frame2).toMatchSnapshot()\n\n    // Visual lines should remain stable after typing\n    expect(visualLinesAfterScroll).toBe(visualLinesAfterTyping)\n\n    // Verify borders are intact\n    const checkBorder = (frame: string, frameName: string) => {\n      const lines = frame.split(\"\\n\")\n      for (let i = 0; i < lines.length; i++) {\n        const line = lines[i]\n        if (line.startsWith(\" │\")) {\n          if (!line.trimEnd().endsWith(\"│\")) {\n            throw new Error(`${frameName}: Line ${i} missing right border: \"${line}\"`)\n          }\n        }\n      }\n    }\n    checkBorder(frame2, \"Frame2\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts",
    "content": "import { describe, test, expect } from \"bun:test\"\nimport { createTestRenderer } from \"../../testing/test-renderer.js\"\nimport { TextareaRenderable } from \"../Textarea.js\"\nimport { LineNumberRenderable } from \"../LineNumberRenderable.js\"\n\ndescribe(\"LineNumberRenderable Wrapping & Scrolling\", () => {\n  test(\"renders correct line numbers when scrolled\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 20,\n      height: 5, // Small height to force scrolling\n    })\n\n    const content = \"1111111111 1111111\\n2222222222 2222222\\n333\\n444\\n555\"\n\n    const editor = new TextareaRenderable(renderer, {\n      width: \"100%\",\n      height: \"100%\",\n      initialValue: content,\n      wrapMode: \"char\",\n    })\n\n    const editorWithLines = new LineNumberRenderable(renderer, {\n      target: editor,\n      minWidth: 3,\n      paddingRight: 1,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(editorWithLines)\n\n    await renderOnce()\n    let frame = captureCharFrame()\n    // Note: Line numbers should appear only on first visual line of logical line\n    expect(frame).toContain(\" 1 1111111111\")\n\n    // Move cursor to bottom to force scroll\n    editor.editBuffer.setCursor(4, 0)\n\n    await renderOnce()\n    frame = captureCharFrame()\n\n    expect(frame).toContain(\" 5 555\")\n    expect(frame).toContain(\" 2 2222222222\")\n    expect(frame).not.toContain(\" 1 1111111111\")\n  })\n\n  test(\"renders correct line numbers with complex wrapping and empty lines\", async () => {\n    const { renderer, renderOnce, captureCharFrame } = await createTestRenderer({\n      width: 30,\n      height: 10,\n    })\n\n    const content = \"A\".repeat(20) + \"\\n\\n\" + \"B\".repeat(40) + \"\\n\\nC\"\n\n    const editor = new TextareaRenderable(renderer, {\n      width: \"100%\",\n      height: \"100%\",\n      initialValue: content,\n      wrapMode: \"char\",\n    })\n\n    const editorWithLines = new LineNumberRenderable(renderer, {\n      target: editor,\n      minWidth: 3,\n      paddingRight: 1,\n      width: \"100%\",\n      height: \"100%\",\n    })\n\n    renderer.root.add(editorWithLines)\n\n    await renderOnce()\n    const frame = captureCharFrame()\n\n    const lines = frame.split(\"\\n\")\n\n    expect(lines[0]).toMatch(/ 1 A{20}/)\n    expect(lines[1]).toMatch(/ 2\\s*$/)\n    expect(lines[2]).toMatch(/ 3 B{26}/)\n    expect(lines[3]).toMatch(/^ {3}B{14}/)\n    expect(lines[4]).toMatch(/ 4\\s*$/)\n    expect(lines[5]).toMatch(/ 5 C/)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Markdown.code-colors.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { MarkdownRenderable, type MarkdownOptions } from \"../Markdown.js\"\nimport { CodeRenderable } from \"../Code.js\"\nimport { SyntaxStyle } from \"../../syntax-style.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\nimport { createTestRenderer, type TestRenderer, MockTreeSitterClient } from \"../../testing.js\"\nimport type { CapturedFrame } from \"../../types.js\"\n\nlet renderer: TestRenderer\nlet captureSpans: () => CapturedFrame\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n})\n\nbeforeEach(async () => {\n  const testRenderer = await createTestRenderer({ width: 60, height: 20 })\n  renderer = testRenderer.renderer\n  captureSpans = testRenderer.captureSpans\n})\n\nafterEach(async () => {\n  if (renderer) {\n    renderer.destroy()\n  }\n})\n\nfunction createMarkdownRenderable(options: MarkdownOptions): MarkdownRenderable {\n  return new MarkdownRenderable(renderer, options)\n}\n\nclass RecordingMockTreeSitterClient extends MockTreeSitterClient {\n  highlightCalls: Array<{ content: string; filetype: string }> = []\n\n  async highlightOnce(content: string, filetype: string) {\n    this.highlightCalls.push({ content, filetype })\n    return super.highlightOnce(content, filetype)\n  }\n}\n\nfunction findSpanContaining(frame: CapturedFrame, text: string) {\n  for (const line of frame.lines) {\n    const span = line.spans.find((candidate) => candidate.text.includes(text))\n    if (span) return span\n  }\n\n  return undefined\n}\n\nfunction expectSpanColors(text: string, fg: RGBA, bg: RGBA): void {\n  const span = findSpanContaining(captureSpans(), text)\n  expect(span).toBeDefined()\n  expect(span!.fg.equals(fg)).toBe(true)\n  expect(span!.bg.equals(bg)).toBe(true)\n}\n\ntest(\"unlabeled fenced code blocks inherit markdown fg/bg defaults\", async () => {\n  const fg = RGBA.fromValues(0.1, 0.1, 0.1, 1)\n  const bg = RGBA.fromValues(0.95, 0.95, 0.95, 1)\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-code-default-colors\",\n    content: \"```\\nplain code block\\n```\",\n    syntaxStyle,\n    fg,\n    bg,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const codeBlock = md._blockStates[0]?.renderable as CodeRenderable\n  expect(codeBlock).toBeInstanceOf(CodeRenderable)\n  expect(codeBlock.filetype).toBeUndefined()\n  expect(codeBlock.fg.equals(fg)).toBe(true)\n  expect(codeBlock.bg.equals(bg)).toBe(true)\n  expectSpanColors(\"plain code block\", fg, bg)\n})\n\ntest(\"unsupported fenced code blocks keep inherited markdown fg/bg after highlight fallback\", async () => {\n  const fg = RGBA.fromValues(0.15, 0.15, 0.15, 1)\n  const bg = RGBA.fromValues(0.9, 0.9, 0.9, 1)\n  const mockTreeSitterClient = new MockTreeSitterClient()\n  mockTreeSitterClient.setMockResult({\n    highlights: [],\n    warning: \"No parser available for filetype toml\",\n  })\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-code-unsupported-colors\",\n    content: \"```toml\\nanswer = 42\\n```\",\n    syntaxStyle,\n    fg,\n    bg,\n    treeSitterClient: mockTreeSitterClient,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n  expect(mockTreeSitterClient.isHighlighting()).toBe(true)\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  const codeBlock = md._blockStates[0]?.renderable as CodeRenderable\n  expect(codeBlock).toBeInstanceOf(CodeRenderable)\n  expect(codeBlock.filetype).toBe(\"toml\")\n  expect(codeBlock.fg.equals(fg)).toBe(true)\n  expect(codeBlock.bg.equals(bg)).toBe(true)\n  expectSpanColors(\"answer = 42\", fg, bg)\n})\n\ntest(\"fenced tsx code blocks normalize the language before highlighting\", async () => {\n  const mockTreeSitterClient = new RecordingMockTreeSitterClient()\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-code-tsx-normalized-filetype\",\n    content: \"```tsx\\nconst view = <div>Hello</div>\\n```\",\n    syntaxStyle,\n    treeSitterClient: mockTreeSitterClient,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const codeBlock = md._blockStates[0]?.renderable as CodeRenderable\n  expect(codeBlock).toBeInstanceOf(CodeRenderable)\n  expect(codeBlock.filetype).toBe(\"typescriptreact\")\n  expect(mockTreeSitterClient.highlightCalls[0]?.filetype).toBe(\"typescriptreact\")\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n})\n\ntest(\"updating fenced code blocks reapplies normalized filetypes\", async () => {\n  const mockTreeSitterClient = new RecordingMockTreeSitterClient()\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-code-react-filetype-update\",\n    content: \"```jsx\\nconst view = <div>Hello</div>\\n```\",\n    syntaxStyle,\n    treeSitterClient: mockTreeSitterClient,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const codeBlock = md._blockStates[0]?.renderable as CodeRenderable\n  expect(codeBlock).toBeInstanceOf(CodeRenderable)\n  expect(codeBlock.filetype).toBe(\"javascriptreact\")\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  md.content = \"```tsx\\nconst view = <div>Hello</div>\\n```\"\n  await renderer.idle()\n\n  expect(md._blockStates[0]?.renderable).toBe(codeBlock)\n  expect(codeBlock.filetype).toBe(\"typescriptreact\")\n  expect(mockTreeSitterClient.highlightCalls.at(-1)?.filetype).toBe(\"typescriptreact\")\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n})\n\ntest(\"updating markdown fg/bg rerenders existing fenced code block renderables\", async () => {\n  const initialFg = RGBA.fromValues(0.1, 0.1, 0.1, 1)\n  const initialBg = RGBA.fromValues(0.95, 0.95, 0.95, 1)\n  const nextFg = RGBA.fromValues(0.8, 0.8, 0.8, 1)\n  const nextBg = RGBA.fromValues(0.2, 0.2, 0.2, 1)\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-code-color-update\",\n    content: \"```\\nplain code block\\n```\",\n    syntaxStyle,\n    fg: initialFg,\n    bg: initialBg,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const codeBlock = md._blockStates[0]?.renderable as CodeRenderable\n  expect(codeBlock).toBeInstanceOf(CodeRenderable)\n  expectSpanColors(\"plain code block\", initialFg, initialBg)\n\n  md.fg = nextFg\n  md.bg = nextBg\n  renderer.requestRender()\n  await renderer.idle()\n\n  expect(md._blockStates[0]?.renderable).toBe(codeBlock)\n  expect(codeBlock.fg.equals(nextFg)).toBe(true)\n  expect(codeBlock.bg.equals(nextBg)).toBe(true)\n  expectSpanColors(\"plain code block\", nextFg, nextBg)\n})\n\ntest(\"updating markdown fg/bg rerenders markdown fallback renderables\", async () => {\n  const initialFg = RGBA.fromValues(0.15, 0.15, 0.15, 1)\n  const initialBg = RGBA.fromValues(0.94, 0.94, 0.94, 1)\n  const nextFg = RGBA.fromValues(0.75, 0.75, 0.75, 1)\n  const nextBg = RGBA.fromValues(0.18, 0.18, 0.18, 1)\n  const mockTreeSitterClient = new MockTreeSitterClient()\n  mockTreeSitterClient.highlightOnce = async () => {\n    throw new Error(\"Highlighting failed\")\n  }\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-paragraph-color-update\",\n    content: \"Plain paragraph text\",\n    syntaxStyle,\n    fg: initialFg,\n    bg: initialBg,\n    treeSitterClient: mockTreeSitterClient,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  const paragraphBlock = md._blockStates[0]?.renderable as CodeRenderable\n  expect(paragraphBlock).toBeInstanceOf(CodeRenderable)\n  expect(paragraphBlock.filetype).toBe(\"markdown\")\n  expectSpanColors(\"Plain paragraph text\", initialFg, initialBg)\n\n  md.fg = nextFg\n  md.bg = nextBg\n  renderer.requestRender()\n  await renderer.idle()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  expect(md._blockStates[0]?.renderable).toBe(paragraphBlock)\n  expect(paragraphBlock.fg.equals(nextFg)).toBe(true)\n  expect(paragraphBlock.bg.equals(nextBg)).toBe(true)\n  expectSpanColors(\"Plain paragraph text\", nextFg, nextBg)\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Markdown.test.ts",
    "content": "import { test, expect, beforeAll, beforeEach, afterEach, afterAll } from \"bun:test\"\nimport { MarkdownRenderable, type MarkdownOptions } from \"../Markdown.js\"\nimport { CodeRenderable } from \"../Code.js\"\nimport { TextRenderable } from \"../Text.js\"\nimport { TextTableRenderable } from \"../TextTable.js\"\nimport { SyntaxStyle } from \"../../syntax-style.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\nimport { TreeSitterClient } from \"../../lib/tree-sitter/index.js\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { mkdir } from \"node:fs/promises\"\nimport {\n  createTestRenderer,\n  type MockMouse,\n  type TestRenderer,\n  MockTreeSitterClient,\n  TestRecorder,\n} from \"../../testing.js\"\nimport { TextAttributes, type CapturedFrame } from \"../../types.js\"\n\nlet renderer: TestRenderer\nlet mockMouse: MockMouse\nlet renderOnce: () => Promise<void>\nlet captureFrame: () => string\nlet captureSpans: () => CapturedFrame\nlet markdownTreeSitterClient: TreeSitterClient\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n})\n\nbeforeAll(async () => {\n  const dataPath = join(tmpdir(), \"tree-sitter-markdown-renderable-test-data\")\n  await mkdir(dataPath, { recursive: true })\n\n  markdownTreeSitterClient = new TreeSitterClient({ dataPath })\n  await markdownTreeSitterClient.initialize()\n})\n\nbeforeEach(async () => {\n  const testRenderer = await createTestRenderer({ width: 60, height: 40 })\n  renderer = testRenderer.renderer\n  mockMouse = testRenderer.mockMouse\n  renderOnce = testRenderer.renderOnce\n  captureFrame = testRenderer.captureCharFrame\n  captureSpans = testRenderer.captureSpans\n})\n\nafterEach(async () => {\n  if (renderer) {\n    renderer.destroy()\n  }\n})\n\nafterAll(async () => {\n  await markdownTreeSitterClient.destroy()\n})\n\nfunction createMarkdownRenderable(options: MarkdownOptions): MarkdownRenderable {\n  return new MarkdownRenderable(renderer, {\n    treeSitterClient: markdownTreeSitterClient,\n    ...options,\n  })\n}\n\nasync function renderMarkdownRenderable(md: MarkdownRenderable, timeoutMs: number = 2000): Promise<void> {\n  const hasPendingMarkdownParagraphHighlights = (): boolean =>\n    md\n      .getChildren()\n      .some((child) => child instanceof CodeRenderable && child.filetype === \"markdown\" && child.isHighlighting)\n\n  const startedAt = Date.now()\n\n  await renderOnce()\n\n  while (hasPendingMarkdownParagraphHighlights() && Date.now() - startedAt < timeoutMs) {\n    await Bun.sleep(10)\n    await renderOnce()\n  }\n\n  if (hasPendingMarkdownParagraphHighlights()) {\n    throw new Error(\"Timed out waiting for markdown paragraph highlights\")\n  }\n\n  await renderOnce()\n}\n\nasync function renderMarkdown(markdown: string, conceal: boolean = true): Promise<string> {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: markdown,\n    syntaxStyle,\n    conceal,\n    tableOptions: { widthMode: \"content\" },\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const lines = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n  return \"\\n\" + lines.join(\"\\n\").trimEnd()\n}\n\nfunction findSpanContaining(frame: CapturedFrame, text: string) {\n  for (const line of frame.lines) {\n    const span = line.spans.find((candidate) => candidate.text.includes(text))\n    if (span) return span\n  }\n\n  return undefined\n}\n\ntest(\"basic table alignment\", async () => {\n  const markdown = `| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 5 |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─────┬───┐\n    │Name │Age│\n    ├─────┼───┤\n    │Alice│30 │\n    ├─────┼───┤\n    │Bob  │5  │\n    └─────┴───┘\"\n  `)\n})\n\ntest(\"tableOptions.widthMode configures markdown table layout\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown-table-width-mode\",\n    content: \"| Name | Age |\\n|---|---|\\n| Alice | 30 |\",\n    syntaxStyle,\n    tableOptions: {\n      widthMode: \"full\",\n      columnFitter: \"balanced\",\n    },\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const table = md._blockStates[0]?.renderable as TextTableRenderable\n  expect(table).toBeInstanceOf(TextTableRenderable)\n  expect(table.columnWidthMode).toBe(\"full\")\n  expect(table.columnFitter).toBe(\"balanced\")\n})\n\ntest(\"tableOptions updates existing markdown table renderable\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown-table-updates\",\n    content: \"| Name | Age |\\n|---|---|\\n| Alice | 30 |\",\n    syntaxStyle,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const table = md._blockStates[0]?.renderable as TextTableRenderable\n  expect(table).toBeInstanceOf(TextTableRenderable)\n  expect(table.columnWidthMode).toBe(\"full\")\n\n  md.tableOptions = {\n    widthMode: \"full\",\n    columnFitter: \"balanced\",\n    wrapMode: \"word\",\n    cellPadding: 1,\n    borders: false,\n    selectable: false,\n  }\n\n  await renderer.idle()\n\n  const updatedTable = md._blockStates[0]?.renderable as TextTableRenderable\n  expect(updatedTable).toBe(table)\n  expect(updatedTable.columnWidthMode).toBe(\"full\")\n  expect(updatedTable.columnFitter).toBe(\"balanced\")\n  expect(updatedTable.wrapMode).toBe(\"word\")\n  expect(updatedTable.cellPadding).toBe(1)\n  expect(updatedTable.border).toBe(false)\n  expect(updatedTable.outerBorder).toBe(false)\n  expect(updatedTable.showBorders).toBe(false)\n  expect(updatedTable.selectable).toBe(false)\n})\n\ntest(\"table with inline code (backticks)\", async () => {\n  const markdown = `| Command | Description |\n|---|---|\n| \\`npm install\\` | Install deps |\n| \\`npm run build\\` | Build project |\n| \\`npm test\\` | Run tests |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─────────────┬─────────────┐\n    │Command      │Description  │\n    ├─────────────┼─────────────┤\n    │npm install  │Install deps │\n    ├─────────────┼─────────────┤\n    │npm run build│Build project│\n    ├─────────────┼─────────────┤\n    │npm test     │Run tests    │\n    └─────────────┴─────────────┘\"\n  `)\n})\n\ntest(\"table with bold text\", async () => {\n  const markdown = `| Feature | Status |\n|---|---|\n| **Authentication** | Done |\n| **API** | WIP |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌──────────────┬──────┐\n    │Feature       │Status│\n    ├──────────────┼──────┤\n    │Authentication│Done  │\n    ├──────────────┼──────┤\n    │API           │WIP   │\n    └──────────────┴──────┘\"\n  `)\n})\n\ntest(\"table with italic text\", async () => {\n  const markdown = `| Item | Note |\n|---|---|\n| One | *important* |\n| Two | *ok* |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌────┬─────────┐\n    │Item│Note     │\n    ├────┼─────────┤\n    │One │important│\n    ├────┼─────────┤\n    │Two │ok       │\n    └────┴─────────┘\"\n  `)\n})\n\ntest(\"table with mixed formatting\", async () => {\n  const markdown = `| Type | Value | Notes |\n|---|---|---|\n| **Bold** | \\`code\\` | *italic* |\n| Plain | **strong** | \\`cmd\\` |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─────┬──────┬──────┐\n    │Type │Value │Notes │\n    ├─────┼──────┼──────┤\n    │Bold │code  │italic│\n    ├─────┼──────┼──────┤\n    │Plain│strong│cmd   │\n    └─────┴──────┴──────┘\"\n  `)\n})\n\ntest(\"table with alignment markers (left, center, right)\", async () => {\n  const markdown = `| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |\n| Long text | X | Y |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─────────┬──────┬─────┐\n    │Left     │Center│Right│\n    ├─────────┼──────┼─────┤\n    │A        │B     │C    │\n    ├─────────┼──────┼─────┤\n    │Long text│X     │Y    │\n    └─────────┴──────┴─────┘\"\n  `)\n})\n\ntest(\"table with empty cells\", async () => {\n  const markdown = `| A | B |\n|---|---|\n| X |  |\n|  | Y |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─┬─┐\n    │A│B│\n    ├─┼─┤\n    │X│ │\n    ├─┼─┤\n    │ │Y│\n    └─┴─┘\"\n  `)\n})\n\ntest(\"table with long header and short content\", async () => {\n  const markdown = `| Very Long Column Header | Short |\n|---|---|\n| A | B |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌───────────────────────┬─────┐\n    │Very Long Column Header│Short│\n    ├───────────────────────┼─────┤\n    │A                      │B    │\n    └───────────────────────┴─────┘\"\n  `)\n})\n\ntest(\"table with short header and long content\", async () => {\n  const markdown = `| X | Y |\n|---|---|\n| This is very long content | Short |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─────────────────────────┬─────┐\n    │X                        │Y    │\n    ├─────────────────────────┼─────┤\n    │This is very long content│Short│\n    └─────────────────────────┴─────┘\"\n  `)\n})\n\ntest(\"table inside code block should NOT be formatted\", async () => {\n  const markdown = `\\`\\`\\`\n| Not | A | Table |\n|---|---|---|\n| Should | Stay | Raw |\n\\`\\`\\`\n\n| Real | Table |\n|---|---|\n| Is | Formatted |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    | Not | A | Table |\n    |---|---|---|\n    | Should | Stay | Raw |\n\n    ┌────┬─────────┐\n    │Real│Table    │\n    ├────┼─────────┤\n    │Is  │Formatted│\n    └────┴─────────┘\"\n  `)\n})\n\ntest(\"multiple tables in same document\", async () => {\n  const markdown = `| Table1 | A |\n|---|---|\n| X | Y |\n\nSome text between.\n\n| Table2 | BB |\n|---|---|\n| Long content | Z |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌──────┬─┐\n    │Table1│A│\n    ├──────┼─┤\n    │X     │Y│\n    └──────┴─┘\n\n    Some text between.\n    ┌────────────┬──┐\n    │Table2      │BB│\n    ├────────────┼──┤\n    │Long content│Z │\n    └────────────┴──┘\"\n  `)\n})\n\ntest(\"table with escaped pipe character\", async () => {\n  const markdown = `| Command | Output |\n|---|---|\n| echo | Hello |\n| ls \\\\| grep | Filtered |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─────────┬────────┐\n    │Command  │Output  │\n    ├─────────┼────────┤\n    │echo     │Hello   │\n    ├─────────┼────────┤\n    │ls | grep│Filtered│\n    └─────────┴────────┘\"\n  `)\n})\n\ntest(\"table with unicode characters\", async () => {\n  const markdown = `| Emoji | Name |\n|---|---|\n| 🎉 | Party |\n| 🚀 | Rocket |\n| 日本語 | Japanese |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌──────┬────────┐\n    │Emoji │Name    │\n    ├──────┼────────┤\n    │🎉    │Party   │\n    ├──────┼────────┤\n    │🚀    │Rocket  │\n    ├──────┼────────┤\n    │日本語│Japanese│\n    └──────┴────────┘\"\n  `)\n})\n\ntest(\"table with links\", async () => {\n  const markdown = `| Name | Link |\n|---|---|\n| Google | [link](https://google.com) |\n| GitHub | [gh](https://github.com) |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌──────┬─────────────────────────┐\n    │Name  │Link                     │\n    ├──────┼─────────────────────────┤\n    │Google│link (https://google.com)│\n    ├──────┼─────────────────────────┤\n    │GitHub│gh (https://github.com)  │\n    └──────┴─────────────────────────┘\"\n  `)\n})\n\ntest(\"single row table (header + delimiter only)\", async () => {\n  const markdown = `| Only | Header |\n|---|---|`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    | Only | Header |\n    |---|---|\"\n  `)\n})\n\ntest(\"table with many columns\", async () => {\n  const markdown = `| A | B | C | D | E |\n|---|---|---|---|---|\n| 1 | 2 | 3 | 4 | 5 |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─┬─┬─┬─┬─┐\n    │A│B│C│D│E│\n    ├─┼─┼─┼─┼─┤\n    │1│2│3│4│5│\n    └─┴─┴─┴─┴─┘\"\n  `)\n})\n\ntest(\"no tables returns original content\", async () => {\n  const markdown = `# Just a heading\n\nSome paragraph text.\n\n- List item`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Just a heading\n\n    Some paragraph text.\n\n    - List item\"\n  `)\n})\n\ntest(\"table with nested inline formatting\", async () => {\n  const markdown = `| Description |\n|---|\n| This has **bold and \\`code\\`** together |\n| And *italic with **nested bold*** |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌───────────────────────────────┐\n    │Description                    │\n    ├───────────────────────────────┤\n    │This has bold and code together│\n    ├───────────────────────────────┤\n    │And italic with nested bold    │\n    └───────────────────────────────┘\"\n  `)\n})\n\n// Tests with conceal=false - formatting markers should be visible and columns sized accordingly\n\ntest(\"conceal=false: table with bold text\", async () => {\n  const markdown = `| Feature | Status |\n|---|---|\n| **Authentication** | Done |\n| **API** | WIP |`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    ┌──────────────────┬──────┐\n    │Feature           │Status│\n    ├──────────────────┼──────┤\n    │**Authentication**│Done  │\n    ├──────────────────┼──────┤\n    │**API**           │WIP   │\n    └──────────────────┴──────┘\"\n  `)\n})\n\ntest(\"conceal=false: table with inline code\", async () => {\n  const markdown = `| Command | Description |\n|---|---|\n| \\`npm install\\` | Install deps |\n| \\`npm run build\\` | Build project |`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    ┌───────────────┬─────────────┐\n    │Command        │Description  │\n    ├───────────────┼─────────────┤\n    │\\`npm install\\`  │Install deps │\n    ├───────────────┼─────────────┤\n    │\\`npm run build\\`│Build project│\n    └───────────────┴─────────────┘\"\n  `)\n})\n\ntest(\"conceal=false: table with italic text\", async () => {\n  const markdown = `| Item | Note |\n|---|---|\n| One | *important* |\n| Two | *ok* |`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    ┌────┬───────────┐\n    │Item│Note       │\n    ├────┼───────────┤\n    │One │*important*│\n    ├────┼───────────┤\n    │Two │*ok*       │\n    └────┴───────────┘\"\n  `)\n})\n\ntest(\"conceal=false: table with mixed formatting\", async () => {\n  const markdown = `| Type | Value | Notes |\n|---|---|---|\n| **Bold** | \\`code\\` | *italic* |\n| Plain | **strong** | \\`cmd\\` |`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    ┌────────┬──────────┬────────┐\n    │Type    │Value     │Notes   │\n    ├────────┼──────────┼────────┤\n    │**Bold**│\\`code\\`    │*italic*│\n    ├────────┼──────────┼────────┤\n    │Plain   │**strong**│\\`cmd\\`   │\n    └────────┴──────────┴────────┘\"\n  `)\n})\n\ntest(\"conceal=false: table with unicode characters\", async () => {\n  const markdown = `| Emoji | Name |\n|---|---|\n| 🎉 | Party |\n| 🚀 | Rocket |\n| 日本語 | Japanese |`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    ┌──────┬────────┐\n    │Emoji │Name    │\n    ├──────┼────────┤\n    │🎉    │Party   │\n    ├──────┼────────┤\n    │🚀    │Rocket  │\n    ├──────┼────────┤\n    │日本語│Japanese│\n    └──────┴────────┘\"\n  `)\n})\n\ntest(\"conceal=false: basic table alignment\", async () => {\n  const markdown = `| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 5 |`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    ┌─────┬───┐\n    │Name │Age│\n    ├─────┼───┤\n    │Alice│30 │\n    ├─────┼───┤\n    │Bob  │5  │\n    └─────┴───┘\"\n  `)\n})\n\ntest(\"table with paragraphs before and after\", async () => {\n  const markdown = `This is a paragraph before the table.\n\n| Name | Age |\n|---|---|\n| Alice | 30 |\n\nThis is a paragraph after the table.`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    This is a paragraph before the table.\n    ┌─────┬───┐\n    │Name │Age│\n    ├─────┼───┤\n    │Alice│30 │\n    └─────┴───┘\n\n    This is a paragraph after the table.\"\n  `)\n})\n\ntest(\"selection across markdown table includes table data\", async () => {\n  const markdown = `Intro line above table.\n\n| Component | Status | Notes |\n|---|---|---|\n| Authentication | **Done** | OAuth2 + SSO |\n| Payments API | *In Progress* | Retry + idempotency |\n| Search Indexer | \\`Done\\` | Ranking + typo fix |\n\nOutro line below table.`\n\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: markdown,\n    syntaxStyle,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const topBlock = md._blockStates[0]?.renderable as CodeRenderable | undefined\n  const tableBlock = md._blockStates[1]?.renderable as TextTableRenderable | undefined\n  const bottomBlock = md._blockStates[2]?.renderable as CodeRenderable | undefined\n\n  expect(topBlock).toBeInstanceOf(CodeRenderable)\n  expect(tableBlock).toBeInstanceOf(TextTableRenderable)\n  expect(bottomBlock).toBeInstanceOf(CodeRenderable)\n\n  const startX = topBlock!.x + 1\n  const startY = topBlock!.y\n  const endX = Math.max(bottomBlock!.x + bottomBlock!.width - 2, startX + 1)\n  const endY = bottomBlock!.y\n\n  await mockMouse.drag(startX, startY, endX, endY)\n  await renderer.idle()\n\n  const selectedText = renderer.getSelection()?.getSelectedText() ?? \"\"\n\n  expect(selectedText).toContain(\"Authentication\")\n  expect(selectedText).toContain(\"Payments API\")\n  expect(selectedText).toContain(\"Retry + idempotency\")\n})\n\n// Code block tests\n\ntest(\"code block with language\", async () => {\n  const markdown = `\\`\\`\\`typescript\nconst x = 1;\nconsole.log(x);\n\\`\\`\\``\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    const x = 1;\n    console.log(x);\"\n  `)\n})\n\ntest(\"code block without language\", async () => {\n  const markdown = `\\`\\`\\`\nplain code block\nwith multiple lines\n\\`\\`\\``\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    plain code block\n    with multiple lines\"\n  `)\n})\n\ntest(\"code block mixed with text\", async () => {\n  const markdown = `Here is some code:\n\n\\`\\`\\`js\nfunction hello() {\n  return \"world\";\n}\n\\`\\`\\`\n\nAnd here is more text after.`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Here is some code:\n    function hello() {\n      return \"world\";\n    }\n\n    And here is more text after.\"\n  `)\n})\n\ntest(\"multiple code blocks\", async () => {\n  const markdown = `First block:\n\n\\`\\`\\`python\nprint(\"hello\")\n\\`\\`\\`\n\nSecond block:\n\n\\`\\`\\`rust\nfn main() {}\n\\`\\`\\``\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    First block:\n    print(\"hello\")\n\n    Second block:\n    fn main() {}\"\n  `)\n})\n\ntest(\"code block in conceal=false mode\", async () => {\n  const markdown = `\\`\\`\\`js\nconst x = 1;\n\\`\\`\\``\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    const x = 1;\"\n  `)\n})\n\ntest(\"code block concealment is disabled by default\", async () => {\n  const mockTreeSitterClient = new MockTreeSitterClient()\n  mockTreeSitterClient.setMockResult({\n    highlights: [[0, 1, \"conceal\", { conceal: \"\" }]],\n  })\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-code-default-conceal\",\n    content: \"```markdown\\n# Hidden heading\\n```\",\n    syntaxStyle,\n    conceal: true,\n    treeSitterClient: mockTreeSitterClient,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n  expect(mockTreeSitterClient.isHighlighting()).toBe(true)\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  const frame = captureFrame()\n  expect(frame).toContain(\"# Hidden heading\")\n})\n\ntest(\"code block concealment can be enabled with concealCode\", async () => {\n  const mockTreeSitterClient = new MockTreeSitterClient()\n  mockTreeSitterClient.setMockResult({\n    highlights: [[0, 1, \"conceal\", { conceal: \"\" }]],\n  })\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-code-conceal-enabled\",\n    content: \"```markdown\\n# Hidden heading\\n```\",\n    syntaxStyle,\n    conceal: true,\n    concealCode: true,\n    treeSitterClient: mockTreeSitterClient,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n  expect(mockTreeSitterClient.isHighlighting()).toBe(true)\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  const frame = captureFrame()\n  expect(frame).not.toContain(\"# Hidden heading\")\n  expect(frame).toContain(\"Hidden heading\")\n})\n\ntest(\"toggling concealCode updates existing code block renderables\", async () => {\n  const mockTreeSitterClient = new MockTreeSitterClient()\n  mockTreeSitterClient.setMockResult({\n    highlights: [[0, 1, \"conceal\", { conceal: \"\" }]],\n  })\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-code-conceal-toggle\",\n    content: \"```markdown\\n# Hidden heading\\n```\",\n    syntaxStyle,\n    conceal: true,\n    concealCode: false,\n    treeSitterClient: mockTreeSitterClient,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n  expect(mockTreeSitterClient.isHighlighting()).toBe(true)\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  const frameBefore = captureFrame()\n  expect(frameBefore).toContain(\"# Hidden heading\")\n\n  md.concealCode = true\n  renderer.requestRender()\n  await renderer.idle()\n  expect(mockTreeSitterClient.isHighlighting()).toBe(true)\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  const frameAfter = captureFrame()\n  expect(frameAfter).not.toContain(\"# Hidden heading\")\n  expect(frameAfter).toContain(\"Hidden heading\")\n})\n\n// Heading tests\n\ntest(\"headings h1 through h3\", async () => {\n  const markdown = `# Heading 1\n\n## Heading 2\n\n### Heading 3`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Heading 1\n\n    Heading 2\n\n    Heading 3\"\n  `)\n})\n\ntest(\"headings with conceal=false show markers\", async () => {\n  const markdown = `# Heading 1\n\n## Heading 2`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    # Heading 1\n\n    ## Heading 2\"\n  `)\n})\n\n// List tests\n\ntest(\"unordered list\", async () => {\n  const markdown = `- Item one\n- Item two\n- Item three`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    - Item one\n    - Item two\n    - Item three\"\n  `)\n})\n\ntest(\"ordered list\", async () => {\n  const markdown = `1. First item\n2. Second item\n3. Third item`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    1. First item\n    2. Second item\n    3. Third item\"\n  `)\n})\n\ntest(\"list with inline formatting\", async () => {\n  const markdown = `- **Bold** item\n- *Italic* item\n- \\`Code\\` item`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    - Bold item\n    - Italic item\n    - Code item\"\n  `)\n})\n\n// Blockquote tests\n\ntest(\"simple blockquote\", async () => {\n  const markdown = `> This is a quote\n> spanning multiple lines`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    > This is a quote\n    > spanning multiple lines\"\n  `)\n})\n\n// Inline formatting tests\n\ntest(\"bold text\", async () => {\n  const markdown = `This has **bold** text in it.`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    This has bold text in it.\"\n  `)\n})\n\ntest(\"italic text\", async () => {\n  const markdown = `This has *italic* text in it.`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    This has italic text in it.\"\n  `)\n})\n\ntest(\"inline code\", async () => {\n  const markdown = `Use \\`console.log()\\` to debug.`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Use console.log() to debug.\"\n  `)\n})\n\ntest(\"mixed inline formatting\", async () => {\n  const markdown = `**Bold**, *italic*, and \\`code\\` together.`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Bold, italic, and code together.\"\n  `)\n})\n\ntest(\"inline formatting with conceal=false\", async () => {\n  const markdown = `**Bold**, *italic*, and \\`code\\` together.`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    **Bold**, *italic*, and \\`code\\` together.\"\n  `)\n})\n\n// Link tests\n\ntest(\"links with conceal mode\", async () => {\n  const markdown = `Check out [OpenTUI](https://github.com/sst/opentui) for more.`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Check out OpenTUI (https://github.com/sst/opentui) for more.\"\n  `)\n})\n\ntest(\"links with conceal=false\", async () => {\n  const markdown = `Check out [OpenTUI](https://github.com/sst/opentui) for more.`\n\n  expect(await renderMarkdown(markdown, false)).toMatchInlineSnapshot(`\n    \"\n    Check out [OpenTUI](https://github.com/sst/opentui) for\n    more.\"\n  `)\n})\n\n// Horizontal rule\n\ntest(\"horizontal rule\", async () => {\n  const markdown = `Before\n\n---\n\nAfter`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Before\n\n    ---\n\n    After\"\n  `)\n})\n\n// Complex document\n\ntest(\"complex markdown document\", async () => {\n  const markdown = `# Project Title\n\nWelcome to **OpenTUI**, a terminal UI library.\n\n## Features\n\n- Automatic table alignment\n- \\`inline code\\` support\n- *Italic* and **bold** text\n\n## Code Example\n\n\\`\\`\\`typescript\nconst md = new MarkdownRenderable(ctx, {\n  content: \"# Hello\",\n})\n\\`\\`\\`\n\n## Links\n\nVisit [GitHub](https://github.com) for more.\n\n---\n\n*Press \\`?\\` for help*`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Project Title\n\n    Welcome to OpenTUI, a terminal UI library.\n\n    Features\n\n    - Automatic table alignment\n    - inline code support\n    - Italic and bold text\n\n    Code Example\n\n    const md = new MarkdownRenderable(ctx, {\n      content: \"# Hello\",\n    })\n\n    Links\n\n    Visit GitHub (https://github.com) for more.\n\n    ---\n\n    Press ? for help\"\n  `)\n})\n\n// Custom renderNode tests\n\ntest(\"custom renderNode can override heading rendering\", async () => {\n  const { TextRenderable } = await import(\"../Text\")\n  const { StyledText } = await import(\"../../lib/styled-text\")\n\n  // Helper to extract text from marked tokens\n  const extractText = (node: any): string => {\n    if (node.type === \"text\") return node.text\n    if (node.tokens) return node.tokens.map(extractText).join(\"\")\n    return \"\"\n  }\n\n  const md = createMarkdownRenderable({\n    id: \"custom-heading\",\n    content: `# Custom Heading\n\nRegular paragraph.`,\n    syntaxStyle,\n    renderNode: (node, ctx) => {\n      if (node.type === \"heading\") {\n        const text = extractText(node)\n        return new TextRenderable(renderer, {\n          id: \"custom\",\n          content: new StyledText([{ __isChunk: true, text: `[CUSTOM] ${text}`, attributes: 0 }]),\n          width: \"100%\",\n        })\n      }\n      return ctx.defaultRender()\n    },\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const lines = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n  expect(\"\\n\" + lines.join(\"\\n\").trimEnd()).toMatchInlineSnapshot(`\n    \"\n    [CUSTOM] Custom Heading\n    Regular paragraph.\"\n  `)\n})\n\ntest(\"custom renderNode can override code block rendering\", async () => {\n  const { BoxRenderable } = await import(\"../Box\")\n  const { TextRenderable } = await import(\"../Text\")\n\n  const md = createMarkdownRenderable({\n    id: \"custom-code\",\n    content: `\\`\\`\\`js\nconst x = 1;\n\\`\\`\\``,\n    syntaxStyle,\n    renderNode: (node, ctx) => {\n      if (node.type === \"code\") {\n        const box = new BoxRenderable(renderer, {\n          id: \"code-box\",\n          border: true,\n          borderStyle: \"single\",\n        })\n        box.add(\n          new TextRenderable(renderer, {\n            id: \"code-text\",\n            content: `CODE: ${(node as any).text}`,\n          }),\n        )\n        return box\n      }\n      return ctx.defaultRender()\n    },\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const lines = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n  expect(\"\\n\" + lines.join(\"\\n\").trimEnd()).toMatchInlineSnapshot(`\n    \"\n    ┌──────────────────────────────────────────────────────────┐\n    │CODE: const x = 1;                                        │\n    └──────────────────────────────────────────────────────────┘\"\n  `)\n})\n\ntest(\"custom renderNode returning null uses default\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"custom-null\",\n    content: `# Heading\n\nParagraph text.`,\n    syntaxStyle,\n    renderNode: () => null,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const lines = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n  expect(\"\\n\" + lines.join(\"\\n\").trimEnd()).toMatchInlineSnapshot(`\n    \"\n    Heading\n\n\n    Paragraph text.\"\n  `)\n})\n\n// Incomplete/invalid markdown tests\n\ntest(\"incomplete code block (no closing fence)\", async () => {\n  const markdown = `Here is some code:\n\n\\`\\`\\`javascript\nconst x = 1;\nconsole.log(x);`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Here is some code:\n    const x = 1;\n    console.log(x);\"\n  `)\n})\n\ntest(\"incomplete bold (no closing **)\", async () => {\n  const markdown = `This has **unclosed bold text`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    This has **unclosed bold text\"\n  `)\n})\n\ntest(\"incomplete italic (no closing *)\", async () => {\n  const markdown = `This has *unclosed italic text`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    This has *unclosed italic text\"\n  `)\n})\n\ntest(\"incomplete link (no closing paren)\", async () => {\n  const markdown = `Check out [this link](https://example.com`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Check out this link(https://example.com\"\n  `)\n})\n\ntest(\"incomplete table (only header)\", async () => {\n  const markdown = `| Header1 | Header2 |`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    | Header1 | Header2 |\"\n  `)\n})\n\ntest(\"incomplete table (header + delimiter, no rows)\", async () => {\n  const markdown = `| Header1 | Header2 |\n|---|---|`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    | Header1 | Header2 |\n    |---|---|\"\n  `)\n})\n\ntest(\"streaming-like content with partial code block\", async () => {\n  const markdown = `# Title\n\nSome text before code.\n\n\\`\\`\\`py`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Title\n\n    Some text before code.\"\n  `)\n})\n\ntest(\"malformed table with missing pipes\", async () => {\n  const markdown = `| A | B\n|---|---\n| 1 | 2`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─┬─┐\n    │A│B│\n    ├─┼─┤\n    │1│2│\n    └─┴─┘\"\n  `)\n})\n\ntest(\"trailing blank lines do not add spacing\", async () => {\n  const markdown = `# Heading\n\nParagraph text.\n\n\n`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Heading\n\n    Paragraph text.\"\n  `)\n})\n\ntest(\"multiple trailing blank lines do not add spacing\", async () => {\n  const markdown = `First paragraph.\n\nSecond paragraph.\n\n\n\n`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    First paragraph.\n\n    Second paragraph.\"\n  `)\n})\n\ntest(\"blank lines between blocks add spacing\", async () => {\n  const markdown = `First\n\nSecond\n\nThird`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    First\n\n    Second\n\n    Third\"\n  `)\n})\n\ntest(\"code block at end with trailing blank lines\", async () => {\n  const markdown = `Text before\n\n\\`\\`\\`js\nconst x = 1;\n\\`\\`\\`\n\n`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    Text before\n    const x = 1;\"\n  `)\n})\n\ntest(\"table at end with trailing blank lines\", async () => {\n  const markdown = `| A | B |\n|---|---|\n| 1 | 2 |\n\n\n`\n\n  expect(await renderMarkdown(markdown)).toMatchInlineSnapshot(`\n    \"\n    ┌─┬─┐\n    │A│B│\n    ├─┼─┤\n    │1│2│\n    └─┴─┘\"\n  `)\n})\n\n// Incremental parsing tests\ntest(\"incremental update reuses unchanged blocks when appending\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"# Hello\\n\\nParagraph 1\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  // Get reference to first block\n  const firstBlockBefore = md._blockStates[0]?.renderable\n\n  // Append content\n  md.content = \"# Hello\\n\\nParagraph 1\\n\\nParagraph 2\"\n  await renderer.idle()\n\n  // First block should be reused (same object reference)\n  const firstBlockAfter = md._blockStates[0]?.renderable\n  expect(firstBlockAfter).toBe(firstBlockBefore)\n})\n\ntest(\"streaming mode keeps trailing tokens unstable\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"# Hello\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const frame1 = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n    .trimEnd()\n  expect(frame1).toContain(\"Hello\")\n\n  // Extend the heading\n  md.content = \"# Hello World\"\n  await renderMarkdownRenderable(md)\n\n  const frame2 = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n    .trimEnd()\n  expect(frame2).toContain(\"Hello World\")\n})\n\ntest(\"streaming code blocks with concealCode=true do not flash unconcealed markdown\", async () => {\n  const mockTreeSitterClient = new MockTreeSitterClient()\n  mockTreeSitterClient.setMockResult({\n    highlights: [[0, 1, \"conceal\", { conceal: \"\" }]],\n  })\n\n  const recorder = new TestRecorder(renderer)\n  recorder.rec()\n\n  const md = createMarkdownRenderable({\n    id: \"markdown-streaming-conceal-flicker\",\n    content: \"# Stream\\n\\n```markdown\\n# Hidden heading\\n```\",\n    syntaxStyle,\n    conceal: true,\n    concealCode: true,\n    streaming: true,\n    treeSitterClient: mockTreeSitterClient,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  expect(mockTreeSitterClient.isHighlighting()).toBe(true)\n\n  mockTreeSitterClient.resolveAllHighlightOnce()\n  await Bun.sleep(10)\n  await renderer.idle()\n\n  recorder.stop()\n\n  const frames = recorder.recordedFrames.map((frame) => frame.frame)\n  const unconcealedFrames = frames.filter((frame) => frame.includes(\"# Hidden heading\"))\n  expect(unconcealedFrames.length).toBe(0)\n})\n\ntest(\"non-streaming mode parses all tokens as stable\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"# Hello\\n\\nPara 1\\n\\nPara 2\",\n    syntaxStyle,\n    streaming: false,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  // Get parse state\n  const parseState = md._parseState\n  expect(parseState).not.toBeNull()\n  expect(parseState!.tokens.length).toBeGreaterThan(0)\n})\n\ntest(\"content update with same text does not rebuild\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"# Hello\",\n    syntaxStyle,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const blockBefore = md._blockStates[0]?.renderable\n\n  // Set same content\n  md.content = \"# Hello\"\n  await renderer.idle()\n\n  const blockAfter = md._blockStates[0]?.renderable\n  expect(blockAfter).toBe(blockBefore)\n})\n\ntest(\"block type change creates new renderable\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"# Hello\",\n    syntaxStyle,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const blockBefore = md._blockStates[0]?.renderable\n\n  // Change from heading to paragraph\n  md.content = \"Hello\"\n  await renderer.idle()\n\n  const blockAfter = md._blockStates[0]?.renderable\n  // Non-special markdown blocks are merged and reused as one markdown code renderable\n  expect(blockAfter).toBe(blockBefore)\n})\n\ntest(\"streaming property can be toggled\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"# Hello\",\n    syntaxStyle,\n    streaming: false,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  expect(md.streaming).toBe(false)\n  const blockBefore = md._blockStates[0]?.renderable\n\n  md.streaming = true\n  expect(md.streaming).toBe(true)\n\n  await renderMarkdownRenderable(md)\n\n  const blockAfter = md._blockStates[0]?.renderable\n  expect(blockAfter).toBe(blockBefore)\n\n  const frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n    .trimEnd()\n  expect(frame).toContain(\"Hello\")\n})\n\ntest(\"clearCache forces full rebuild\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"# Hello\\n\\nWorld\",\n    syntaxStyle,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const parseStateBefore = md._parseState\n\n  md.clearCache()\n  await renderer.idle()\n\n  const parseStateAfter = md._parseState\n  // Parse state should be different (was cleared and rebuilt)\n  expect(parseStateAfter).not.toBe(parseStateBefore)\n})\n\ntest(\"streaming->non-streaming transition keeps final table row visible\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| Value |\\n|---|\\n| first |\\n| second |\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const tableWhileStreaming = md._blockStates[0]?.renderable\n\n  let frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame).toContain(\"first\")\n  expect(frame).toContain(\"second\")\n\n  md.streaming = false\n  await renderer.idle()\n\n  frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame).toContain(\"first\")\n  expect(frame).toContain(\"second\")\n  expect(md._blockStates[0]?.renderable).toBe(tableWhileStreaming)\n})\n\ntest(\"streaming table remains visible when a new block starts\", async () => {\n  const tableMarkdown = \"| Value |\\n|---|\\n| first |\\n| second |\"\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: tableMarkdown,\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const tableWhileTrailing = md._blockStates[0]?.renderable\n\n  let frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame).toContain(\"first\")\n  expect(frame).toContain(\"second\")\n\n  md.content = `${tableMarkdown}\\n\\nAfter table block.`\n  await renderer.idle()\n\n  frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(md.streaming).toBe(true)\n  expect(frame).toContain(\"first\")\n  expect(frame).toContain(\"second\")\n  expect(md._blockStates.length).toBeGreaterThan(1)\n  expect(md._blockStates[0]?.renderable).toBe(tableWhileTrailing)\n})\n\ntest(\"stream end mid-table finalizes full table snapshot\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n\n  md.content = \"| Name | Score |\\n|---|---|\\n\"\n  await renderer.idle()\n\n  md.content = \"| Name | Score |\\n|---|---|\\n| Alpha | 10 |\\n\"\n  await renderer.idle()\n\n  md.content = \"| Name | Score |\\n|---|---|\\n| Alpha | 10 |\\n| Bravo | 20 |\\n\"\n  await renderer.idle()\n\n  md.content = \"| Name | Score |\\n|---|---|\\n| Alpha | 10 |\\n| Bravo | 20 |\\n| Charlie | 30 |\"\n  await renderer.idle()\n\n  let frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame).toContain(\"Charlie\")\n\n  md.streaming = false\n  await renderer.idle()\n\n  frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n    .trimEnd()\n\n  expect(frame).toMatchInlineSnapshot(`\n\"┌──────────────────────────────┬───────────────────────────┐\n│Name                          │Score                      │\n├──────────────────────────────┼───────────────────────────┤\n│Alpha                         │10                         │\n├──────────────────────────────┼───────────────────────────┤\n│Bravo                         │20                         │\n├──────────────────────────────┼───────────────────────────┤\n│Charlie                       │30                         │\n└──────────────────────────────┴───────────────────────────┘\"\n`)\n})\n\ntest(\"ignores content updates after markdown renderable is destroyed during streaming\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n\n  md.content = \"| Name | Score |\\n|---|---|\\n| Alpha | 10 |\\n\"\n  await renderer.idle()\n\n  md.destroyRecursively()\n  expect(md.isDestroyed).toBe(true)\n\n  expect(() => {\n    md.content = \"| Name | Score |\\n|---|---|\\n| Alpha | 10 |\\n| Bravo | 20 |\\n\"\n    md.streaming = false\n  }).not.toThrow()\n\n  await renderer.idle()\n})\n\ntest(\"non-streaming->streaming transition keeps final table row visible\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| Value |\\n|---|\\n| first |\\n| second |\",\n    syntaxStyle,\n    streaming: false,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const tableWhileStable = md._blockStates[0]?.renderable\n\n  let frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame).toContain(\"first\")\n  expect(frame).toContain(\"second\")\n\n  md.streaming = true\n  await renderer.idle()\n\n  frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame).toContain(\"first\")\n  expect(frame).toContain(\"second\")\n  expect(md._blockStates[0]?.renderable).toBe(tableWhileStable)\n})\n\ntest(\"streaming table reuses renderable while updating row content\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| A |\\n|---|\\n| 1 |\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const tableBefore = md._blockStates[0]?.renderable\n\n  md.content = \"| B |\\n|---|\\n| 2 |\"\n  await renderer.idle()\n\n  const tableAfterSameRows = md._blockStates[0]?.renderable\n  expect(tableAfterSameRows).toBe(tableBefore)\n\n  md.content = \"| B |\\n|---|\\n| 2 |\\n| 3 |\"\n  await renderer.idle()\n\n  const tableAfterNewRow = md._blockStates[0]?.renderable\n  expect(tableAfterNewRow).toBe(tableBefore)\n})\n\ntest(\"table shows all rows when streaming is false\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| A |\\n|---|\\n| 1 |\",\n    syntaxStyle,\n    streaming: false,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  // Non-streaming should show all rows including the last\n  const frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n  expect(frame).toContain(\"1\")\n})\n\ntest(\"table updates content when not streaming\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| A |\\n|---|\\n| 1 |\",\n    syntaxStyle,\n    streaming: false,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const frame1 = captureFrame()\n  expect(frame1).toContain(\"1\")\n\n  // Change cell content - should update immediately when not streaming\n  md.content = \"| A |\\n|---|\\n| 2 |\"\n  await renderer.idle()\n\n  const frame2 = captureFrame()\n  expect(frame2).toContain(\"2\")\n  expect(frame2).not.toContain(\"1\")\n})\n\ntest(\"table keeps unchanged cell chunks stable across updates\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| A | B |\\n|---|---|\\n| 1 | 2 |\\n| 3 | 4 |\",\n    syntaxStyle,\n    streaming: false,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const table = md._blockStates[0]?.renderable as TextTableRenderable\n  expect(table).toBeInstanceOf(TextTableRenderable)\n\n  const headerBefore = table.content[0]?.[0]\n  const firstRowBefore = table.content[1]?.[0]\n  const secondRowSecondCellBefore = table.content[2]?.[1]\n  const changedCellBefore = table.content[2]?.[0]\n\n  md.content = \"| A | B |\\n|---|---|\\n| 1 | 2 |\\n| 33 | 4 |\"\n  await renderer.idle()\n\n  const tableAfter = md._blockStates[0]?.renderable as TextTableRenderable\n  expect(tableAfter).toBe(table)\n  expect(tableAfter.content[0]?.[0]).toBe(headerBefore)\n  expect(tableAfter.content[1]?.[0]).toBe(firstRowBefore)\n  expect(tableAfter.content[2]?.[1]).toBe(secondRowSecondCellBefore)\n  expect(tableAfter.content[2]?.[0]).not.toBe(changedCellBefore)\n})\n\ntest(\"streaming table updates trailing row content\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| A |\\n|---|\\n| 1 |\\n| 2 |\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderer.idle()\n\n  const table = md._blockStates[0]?.renderable as TextTableRenderable\n  const contentBefore = table.content\n\n  md.content = \"| A |\\n|---|\\n| 1 |\\n| 200 |\"\n  await renderer.idle()\n\n  const tableAfter = md._blockStates[0]?.renderable as TextTableRenderable\n  const frame = captureFrame()\n  expect(tableAfter).toBe(table)\n  expect(tableAfter.content).not.toBe(contentBefore)\n  expect(frame).toContain(\"200\")\n})\n\ntest(\"streaming complex tables keep final rows visible (issue #15244)\", async () => {\n  const vmHeader = \"| VM | 状态 | Owner | Zone | CPU | Mem(GB) | Disk(GB) | Net | Uptime | Cost/月 | Notes |\"\n  const vmDelimiter = \"|---|---|---|---|---|---|---|---|---|---|---|\"\n  const vmRows = [\n    \"| vm-api-01 | 🟢 运行中 | alice | us-east-1a | 8 | 32 | 500 | 1.2Gbps | 99.99% | 12,345 | 主节点 — steady |\",\n    \"| vm-job-02 | 🟢 运行中 | bob | ap-south-1b | 16 | 64 | 1,024 | 950Mbps | 98.70% | 23,456 | 批处理 — spikes |\",\n    \"| vm-batch-03 | 🟡 维护中 | carol | eu-west-1c | 32 | 128 | 2,048 | 2.4Gbps | 97.10% | 34,567 | 最后一行 — must stay |\",\n  ] as const\n\n  const storageHeader = \"| 存储池 | 状态 | 使用率 | 可用(GB) | 已用(GB) | 冗余 | 备注 |\"\n  const storageDelimiter = \"|---|---|---|---|---|---|---|\"\n  const storageRows = [\n    \"| 热池A | 🟢 正常 | 72% | 12,500 | 32,500 | 3x | 混合负载 |\",\n    \"| 温池B | 🟢 正常 | 81% | 8,250 | 35,750 | 2x | 历史数据 |\",\n    \"| 冷池C | 🟡 告警 | 93% | 2,100 | 27,900 | 2x | 最后一行 — must stay |\",\n  ] as const\n\n  const buildContent = (vmRowCount: number, storageRowCount: number): string =>\n    `### VM details\\n\\n${vmHeader}\\n${vmDelimiter}\\n${vmRows.slice(0, vmRowCount).join(\"\\n\")}\\n\\n### Storage details\\n\\n${storageHeader}\\n${storageDelimiter}\\n${storageRows.slice(0, storageRowCount).join(\"\\n\")}`\n\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n\n  for (const [vmRowCount, storageRowCount] of [\n    [2, 2],\n    [3, 2],\n    [3, 3],\n  ] as const) {\n    md.content = buildContent(vmRowCount, storageRowCount)\n    await renderMarkdownRenderable(md)\n  }\n\n  const tableBlocks = md._blockStates\n    .map((state) => state.renderable)\n    .filter((renderable): renderable is TextTableRenderable => renderable instanceof TextTableRenderable)\n\n  const cellText = (cell: { text: string }[] | null | undefined): string =>\n    cell?.map((chunk) => chunk.text).join(\"\") ?? \"\"\n\n  expect(tableBlocks).toHaveLength(2)\n\n  const vmTable = tableBlocks[0]\n  const storageTable = tableBlocks[1]\n\n  expect(vmTable.content.length).toBe(4)\n  expect(storageTable.content.length).toBe(4)\n  expect(cellText(vmTable.content[3]?.[0])).toContain(\"vm-batch-03\")\n  expect(cellText(storageTable.content[3]?.[0])).toContain(\"冷池C\")\n})\n\ntest(\"streaming table with incomplete first row is rendered with padded cells\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| A |\\n|---|\\n|\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const frame1 = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame1).toMatch(/[┌│└]/)\n  expect(frame1).toContain(\"A\")\n\n  md.content = \"| A |\\n|---|\\n| 1\"\n  await renderMarkdownRenderable(md)\n\n  const frame2 = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame2).toMatch(/[┌│└]/)\n  expect(frame2).toContain(\"1\")\n\n  md.content = \"| A |\\n|---|\\n| 1 |\\n| 2 |\"\n  await renderMarkdownRenderable(md)\n\n  const frame3 = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n\n  expect(frame3).toMatch(/[┌│└]/)\n  expect(frame3).toContain(\"1\")\n  expect(frame3).toContain(\"2\")\n})\n\ntest(\"streaming table transitions from raw text to table once first row appears\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| Header |\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  let frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n  expect(frame).toContain(\"| Header |\")\n  expect(frame).not.toMatch(/[┌│└]/)\n\n  md.content = \"| Header |\\n|---|\"\n  await renderMarkdownRenderable(md)\n\n  frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n  expect(frame).toContain(\"|---|\")\n  expect(frame).not.toMatch(/[┌│└]/)\n\n  md.content = \"| Header |\\n|---|\\n| D\"\n  await renderMarkdownRenderable(md)\n\n  frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n  expect(frame).toMatch(/[┌│└]/)\n  expect(frame).toContain(\"Header\")\n  expect(frame).toContain(\"D\")\n  expect(frame).not.toContain(\"|---|\")\n})\n\ntest(\"streaming table remains rendered when row count decreases\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"| A |\\n|---|\\n| 1 |\\n| 2 |\",\n    syntaxStyle,\n    streaming: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  let frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n  expect(frame).toMatch(/[┌│└]/)\n  expect(frame).toContain(\"1\")\n  expect(frame).toContain(\"2\")\n\n  md.content = \"| A |\\n|---|\\n| 1 |\"\n  await renderMarkdownRenderable(md)\n\n  frame = captureFrame()\n    .split(\"\\n\")\n    .map((line) => line.trimEnd())\n    .join(\"\\n\")\n  expect(frame).toMatch(/[┌│└]/)\n  expect(frame).toContain(\"1\")\n  expect(frame).not.toContain(\"|---|\")\n})\n\ntest(\"conceal change updates rendered content\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"# Hello **bold**\",\n    syntaxStyle,\n    conceal: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const frame1 = captureFrame()\n  expect(frame1).not.toContain(\"**\")\n  expect(frame1).not.toContain(\"#\")\n\n  md.conceal = false\n  renderer.requestRender()\n  await renderMarkdownRenderable(md)\n\n  const frame2 = captureFrame()\n  expect(frame2).toContain(\"**\")\n  expect(frame2).toContain(\"#\")\n})\n\ntest(\"theme switching (syntaxStyle change)\", async () => {\n  const theme1 = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(1, 0, 0, 1) }, // Red\n    \"markup.heading.1\": { fg: RGBA.fromValues(0, 1, 0, 1), bold: true }, // Green\n  })\n\n  const theme2 = SyntaxStyle.fromStyles({\n    default: { fg: RGBA.fromValues(0, 0, 1, 1) }, // Blue\n    \"markup.heading.1\": { fg: RGBA.fromValues(1, 1, 0, 1), bold: true }, // Yellow\n  })\n\n  // Use the EXACT content from markdown-demo.ts to reproduce the issue\n  const content = `# OpenTUI Markdown Demo\n\nWelcome to the **MarkdownRenderable** showcase! This demonstrates automatic table alignment and syntax highlighting.\n\n## Features\n\n- Automatic **table column alignment** based on content width\n- Proper handling of \\`inline code\\`, **bold**, and *italic* in tables\n- Multiple syntax themes to choose from\n- Conceal mode hides formatting markers\n\n## Comparison Table\n\n| Feature | Status | Priority | Notes |\n|---|---|---|---|\n| Table alignment | **Done** | High | Uses \\`marked\\` parser |\n| Conceal mode | *Working* | Medium | Hides \\`**\\`, \\`\\`\\`, etc. |\n| Theme switching | **Done** | Low | 3 themes available |\n| Unicode support | 日本語 | High | CJK characters |\n\n## Code Examples\n\nHere's how to use it:\n\n\\`\\`\\`typescript\nimport { MarkdownRenderable } from \"@opentui/core\"\n\nconst md = createMarkdownRenderable({\n  content: \"# Hello World\",\n  syntaxStyle: mySyntaxStyle,\n  conceal: true, // Hide formatting markers\n})\n\\`\\`\\`\n\n### API Reference\n\n| Method | Parameters | Returns | Description |\n|---|---|---|---|\n| \\`constructor\\` | \\`ctx, options\\` | \\`MarkdownRenderable\\` | Create new instance |\n| \\`clearCache\\` | none | \\`void\\` | Force re-render content |\n\n## Inline Formatting Examples\n\n| Style | Syntax | Rendered |\n|---|---|---|\n| Bold | \\`**text**\\` | **bold text** |\n| Italic | \\`*text*\\` | *italic text* |\n| Code | \\`code\\` | \\`inline code\\` |\n| Link | \\`[text](url)\\` | [OpenTUI](https://github.com) |\n\n## Mixed Content\n\n> **Note**: This blockquote contains **bold** and \\`code\\` formatting.\n> It should render correctly with proper styling.\n\n### Emoji Support\n\n| Emoji | Name | Category |\n|---|---|---|\n| 🚀 | Rocket | Transport |\n| 🎨 | Palette | Art |\n| ⚡ | Lightning | Nature |\n| 🔥 | Fire | Nature |\n\n---\n\n## Alignment Examples\n\n| Left | Center | Right |\n|:---|:---:|---:|\n| L1 | C1 | R1 |\n| Left aligned | Centered text | Right aligned |\n| Short | Medium length | Longer content here |\n\n## Performance\n\nThe table alignment uses:\n1. AST-based parsing with \\`marked\\`\n2. Caching for repeated content\n3. Smart width calculation accounting for concealed chars\n\n---\n\n*Press \\`?\\` for keybindings*\n`\n\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content,\n    syntaxStyle: theme1,\n    conceal: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const frame1 = captureSpans()\n  const headingSpan1 = findSpanContaining(frame1, \"OpenTUI Markdown Demo\")\n  expect(headingSpan1).toBeDefined()\n  expect(headingSpan1!.fg.r).toBe(0)\n  expect(headingSpan1!.fg.g).toBe(1)\n  expect(headingSpan1!.fg.b).toBe(0)\n  expect(headingSpan1!.attributes & TextAttributes.BOLD).toBeTruthy()\n\n  // Switch theme\n  md.syntaxStyle = theme2\n  renderer.requestRender()\n  await renderMarkdownRenderable(md)\n\n  const frame2 = captureSpans()\n  const headingSpan2 = findSpanContaining(frame2, \"OpenTUI Markdown Demo\")\n  expect(headingSpan2).toBeDefined()\n  expect(headingSpan2!.fg.r).toBe(1)\n  expect(headingSpan2!.fg.g).toBe(1)\n  expect(headingSpan2!.fg.b).toBe(0)\n  expect(headingSpan2!.attributes & TextAttributes.BOLD).toBeTruthy()\n})\n\n// Paragraph rendering tests\n\ntest(\"paragraph links are rendered with markdown conceal behavior\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"Check [Google](https://google.com) out\",\n    syntaxStyle,\n    conceal: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const paragraphChildren = md.getChildren()\n  expect(paragraphChildren.length).toBe(1)\n  expect(paragraphChildren[0]).toBeInstanceOf(CodeRenderable)\n  expect(paragraphChildren[0]).not.toBeInstanceOf(TextRenderable)\n\n  const frame = captureFrame()\n  expect(frame).toContain(\"Google\")\n  expect(frame).toContain(\"https://google.com\")\n  expect(frame).not.toContain(\"[Google](https://google.com)\")\n})\n\ntest(\"paragraph initial render does not flash raw markdown markers\", async () => {\n  const recorder = new TestRecorder(renderer)\n  recorder.rec()\n\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"This has **bold** text.\",\n    syntaxStyle,\n    conceal: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n  recorder.stop()\n\n  const paragraphChildren = md.getChildren()\n  expect(paragraphChildren.length).toBe(1)\n  expect(paragraphChildren[0]).toBeInstanceOf(CodeRenderable)\n  expect(paragraphChildren[0]).not.toBeInstanceOf(TextRenderable)\n\n  const rawMarkdownFrames = recorder.recordedFrames.filter((recorded) => recorded.frame.includes(\"**bold**\"))\n  expect(rawMarkdownFrames.length).toBe(0)\n\n  const finalFrame = captureFrame()\n  expect(finalFrame).toContain(\"This has bold text.\")\n})\n\ntest(\"paragraph updates do not flash raw markdown markers\", async () => {\n  const md = createMarkdownRenderable({\n    id: \"markdown\",\n    content: \"**First** value\",\n    syntaxStyle,\n    conceal: true,\n  })\n\n  renderer.root.add(md)\n  await renderMarkdownRenderable(md)\n\n  const paragraphChildrenBefore = md.getChildren()\n  expect(paragraphChildrenBefore.length).toBe(1)\n  expect(paragraphChildrenBefore[0]).toBeInstanceOf(CodeRenderable)\n  expect(paragraphChildrenBefore[0]).not.toBeInstanceOf(TextRenderable)\n\n  const recorder = new TestRecorder(renderer)\n  recorder.rec()\n\n  md.content = \"**Second** value\"\n  await renderMarkdownRenderable(md)\n  recorder.stop()\n\n  const paragraphChildrenAfter = md.getChildren()\n  expect(paragraphChildrenAfter.length).toBe(1)\n  expect(paragraphChildrenAfter[0]).toBeInstanceOf(CodeRenderable)\n  expect(paragraphChildrenAfter[0]).not.toBeInstanceOf(TextRenderable)\n\n  const rawMarkdownFrames = recorder.recordedFrames.filter((recorded) => recorded.frame.includes(\"**Second**\"))\n  expect(rawMarkdownFrames.length).toBe(0)\n\n  const finalFrame = captureFrame()\n  expect(finalFrame).toContain(\"Second value\")\n  expect(finalFrame).not.toContain(\"**Second**\")\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/MultiRenderable.selection.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockMouse } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\nimport { TextRenderable } from \"../Text.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMouse: MockMouse\n\ndescribe(\"Multi-Renderable Selection Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockMouse: currentMouse,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  it(\"should handle selection across Textarea and Text renderable\", async () => {\n    // Create a Textarea with scrolling content\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n      width: 40,\n      height: 5,\n      left: 0,\n      top: 0,\n      selectable: true,\n    })\n\n    // Create a Text renderable below the Textarea\n    const textRenderable = new TextRenderable(currentRenderer, {\n      content: \"Text Below Textarea\",\n      width: 40,\n      height: 1,\n      left: 0,\n      top: 6, // Positioned below the textarea (height 5 + some gap or directly below)\n      selectable: true,\n    })\n    currentRenderer.root.add(textRenderable)\n    await renderOnce()\n\n    // Scroll the Textarea down\n    editor.gotoLine(10)\n    await renderOnce()\n\n    const viewport = editor.editorView.getViewport()\n    expect(viewport.offsetY).toBeGreaterThan(0)\n\n    // Mouse drag from inside the Textarea to the Text renderable\n    // Start: middle of the visible Textarea (relative to scrolled content)\n    // End: inside the Text renderable\n\n    const startX = editor.x + 2\n    const startY = editor.y + 2\n    const endX = textRenderable.x + 5\n    const endY = textRenderable.y\n\n    await currentMouse.drag(startX, startY, endX, endY)\n    await renderOnce()\n\n    expect(editor.hasSelection()).toBe(true)\n    expect(textRenderable.hasSelection()).toBe(true)\n\n    const selectedTextareaText = editor.getSelectedText()\n    const selectedTextText = textRenderable.getSelectedText()\n\n    // Verify selection in Textarea (should be from the scrolled viewport)\n    // The selection starts at column 2 of the line visible at relative row 2\n    // and extends to the end of the visible buffer since we dragged out of it.\n    // Since we scrolled to line 10 with a height of 5 and scroll margin of 0.2 (default),\n    // the viewport logic will position line 10 appropriately.\n    // We check for content that corresponds to this viewport position.\n    expect(selectedTextareaText).toContain(\"ne 9\")\n    expect(selectedTextareaText).toContain(\"Line 10\")\n\n    // Verify selection in Text renderable\n    expect(selectedTextText).toBe(\"Text \")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.buffer.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Buffer Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"getTextRange\", () => {\n    it(\"should get text range by display-width offsets\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello, World!\\nThis is line 2.\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRange(0, 5)\n      expect(range1).toBe(\"Hello\")\n\n      const range2 = editor.getTextRange(7, 12)\n      expect(range2).toBe(\"World\")\n\n      const range3 = editor.getTextRange(0, 13)\n      expect(range3).toBe(\"Hello, World!\")\n\n      const range4 = editor.getTextRange(14, 21)\n      expect(range4).toBe(\"This is\")\n    })\n\n    it(\"should get text range by row/col coordinates\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello, World!\\nThis is line 2.\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRangeByCoords(0, 0, 0, 5)\n      expect(range1).toBe(\"Hello\")\n\n      const range2 = editor.getTextRangeByCoords(0, 7, 0, 12)\n      expect(range2).toBe(\"World\")\n\n      const range3 = editor.getTextRangeByCoords(1, 0, 1, 7)\n      expect(range3).toBe(\"This is\")\n\n      const range4 = editor.getTextRangeByCoords(0, 0, 1, 7)\n      expect(range4).toBe(\"Hello, World!\\nThis is\")\n    })\n\n    it(\"should handle empty ranges with getTextRangeByCoords\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello, World!\",\n        width: 40,\n        height: 10,\n      })\n\n      const rangeEmpty = editor.getTextRangeByCoords(0, 5, 0, 5)\n      expect(rangeEmpty).toBe(\"\")\n\n      const rangeInvalid = editor.getTextRangeByCoords(0, 10, 0, 5)\n      expect(rangeInvalid).toBe(\"\")\n    })\n\n    it(\"should handle ranges spanning multiple lines with getTextRangeByCoords\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRangeByCoords(0, 5, 1, 4)\n      expect(range1).toBe(\"1\\nLine\")\n\n      const range2 = editor.getTextRangeByCoords(0, 0, 2, 6)\n      expect(range2).toBe(\"Line 1\\nLine 2\\nLine 3\")\n\n      const range3 = editor.getTextRangeByCoords(1, 0, 2, 6)\n      expect(range3).toBe(\"Line 2\\nLine 3\")\n    })\n\n    it(\"should handle Unicode characters with getTextRangeByCoords\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello 🌟 World\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRangeByCoords(0, 0, 0, 6)\n      expect(range1).toBe(\"Hello \")\n\n      const range2 = editor.getTextRangeByCoords(0, 6, 0, 8)\n      expect(range2).toBe(\"🌟\")\n\n      const range3 = editor.getTextRangeByCoords(0, 8, 0, 14)\n      expect(range3).toBe(\" World\")\n    })\n\n    it(\"should handle CJK characters with getTextRangeByCoords\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello 世界\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRangeByCoords(0, 0, 0, 6)\n      expect(range1).toBe(\"Hello \")\n\n      const range2 = editor.getTextRangeByCoords(0, 6, 0, 10)\n      expect(range2).toBe(\"世界\")\n    })\n\n    it(\"should get text range by coords after editing operations\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\\nDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      const range1 = editor.getTextRangeByCoords(0, 0, 1, 3)\n      expect(range1).toBe(\"ABC\\nDEF\")\n\n      editor.gotoLine(1)\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEF\")\n\n      const range2 = editor.getTextRangeByCoords(0, 1, 0, 5)\n      expect(range2).toBe(\"BCDE\")\n\n      const range3 = editor.getTextRangeByCoords(0, 0, 0, 6)\n      expect(range3).toBe(\"ABCDEF\")\n    })\n\n    it(\"should handle out-of-bounds coordinates with getTextRangeByCoords\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Short\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRangeByCoords(10, 0, 20, 0)\n      expect(range1).toBe(\"\")\n\n      const range2 = editor.getTextRangeByCoords(0, 0, 0, 5)\n      expect(range2).toBe(\"Short\")\n\n      const range3 = editor.getTextRangeByCoords(0, 100, 0, 200)\n      expect(range3).toBe(\"\")\n    })\n\n    it(\"should match offset-based and coords-based methods\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      const offsetBased = editor.getTextRange(0, 6)\n      const coordsBased = editor.getTextRangeByCoords(0, 0, 0, 6)\n      expect(coordsBased).toBe(offsetBased)\n      expect(coordsBased).toBe(\"Line 1\")\n\n      const offsetBased2 = editor.getTextRange(7, 13)\n      const coordsBased2 = editor.getTextRangeByCoords(1, 0, 1, 6)\n      expect(coordsBased2).toBe(offsetBased2)\n      expect(coordsBased2).toBe(\"Line 2\")\n\n      const offsetBased3 = editor.getTextRange(5, 12)\n      const coordsBased3 = editor.getTextRangeByCoords(0, 5, 1, 5)\n      expect(coordsBased3).toBe(offsetBased3)\n      expect(coordsBased3).toBe(\"1\\nLine \")\n    })\n\n    it(\"should handle empty ranges\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello, World!\",\n        width: 40,\n        height: 10,\n      })\n\n      const rangeEmpty = editor.getTextRange(5, 5)\n      expect(rangeEmpty).toBe(\"\")\n\n      const rangeInvalid = editor.getTextRange(10, 5)\n      expect(rangeInvalid).toBe(\"\")\n    })\n\n    it(\"should handle ranges spanning multiple lines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRange(0, 13)\n      expect(range1).toBe(\"Line 1\\nLine 2\")\n\n      const range2 = editor.getTextRange(5, 12)\n      expect(range2).toBe(\"1\\nLine \")\n    })\n\n    it(\"should handle Unicode characters in ranges\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello 🌟 World\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRange(0, 6)\n      expect(range1).toBe(\"Hello \")\n\n      const range2 = editor.getTextRange(6, 8)\n      expect(range2).toBe(\"🌟\")\n\n      const range3 = editor.getTextRange(8, 14)\n      expect(range3).toBe(\" World\")\n    })\n\n    it(\"should handle CJK characters in ranges\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello 世界\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRange(0, 6)\n      expect(range1).toBe(\"Hello \")\n\n      const range2 = editor.getTextRange(6, 10)\n      expect(range2).toBe(\"世界\")\n    })\n\n    it(\"should get text range after editing operations\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      editor.gotoLine(9999)\n      editor.insertText(\"DEF\")\n      expect(editor.plainText).toBe(\"ABCDEF\")\n\n      const range1 = editor.getTextRange(0, 6)\n      expect(range1).toBe(\"ABCDEF\")\n\n      const range2 = editor.getTextRange(0, 3)\n      expect(range2).toBe(\"ABC\")\n\n      const range3 = editor.getTextRange(3, 6)\n      expect(range3).toBe(\"DEF\")\n    })\n\n    it(\"should get text range across chunk boundaries after line joins\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\\nDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEF\")\n\n      const range1 = editor.getTextRange(1, 5)\n      expect(range1).toBe(\"BCDE\")\n\n      const range2 = editor.getTextRange(0, 6)\n      expect(range2).toBe(\"ABCDEF\")\n    })\n\n    it(\"should handle range at buffer boundaries\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRange(0, 2)\n      expect(range1).toBe(\"Te\")\n\n      const range2 = editor.getTextRange(2, 4)\n      expect(range2).toBe(\"st\")\n\n      const range3 = editor.getTextRange(0, 4)\n      expect(range3).toBe(\"Test\")\n    })\n\n    it(\"should return empty string for out-of-bounds ranges\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Short\",\n        width: 40,\n        height: 10,\n      })\n\n      const range1 = editor.getTextRange(100, 200)\n      expect(range1).toBe(\"\")\n\n      const range2 = editor.getTextRange(0, 1000)\n      expect(range2).toBe(\"Short\")\n    })\n  })\n\n  describe(\"Visual Cursor with Offset\", () => {\n    it(\"should have visualCursor with offset property\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      const visualCursor = editor.visualCursor\n      expect(visualCursor).not.toBe(null)\n      expect(visualCursor!.offset).toBeDefined()\n      expect(visualCursor!.offset).toBe(0)\n    })\n\n    it(\"should update offset after inserting text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      editor.insertText(\"Hello\")\n\n      const visualCursor = editor.visualCursor\n      expect(visualCursor).not.toBe(null)\n      expect(visualCursor!.offset).toBe(5)\n    })\n\n    it(\"should update offset correctly for multi-line content\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\\nDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Cursor at start\n      let visualCursor = editor.visualCursor\n      expect(visualCursor!.offset).toBe(0)\n\n      // Move to end of first line\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight()\n      }\n      visualCursor = editor.visualCursor\n      expect(visualCursor!.offset).toBe(3)\n\n      // Move to second line (across newline)\n      editor.moveCursorRight()\n      visualCursor = editor.visualCursor\n      expect(visualCursor!.offset).toBe(4)\n      expect(visualCursor!.logicalRow).toBe(1)\n      expect(visualCursor!.logicalCol).toBe(0)\n\n      // Move to end of second line\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight()\n      }\n      visualCursor = editor.visualCursor\n      expect(visualCursor!.offset).toBe(7)\n    })\n\n    it(\"should set cursor by offset\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Set cursor to offset 6 (after \"Hello \")\n      editor.editBuffer.setCursorByOffset(6)\n\n      const visualCursor = editor.visualCursor\n      expect(visualCursor).not.toBe(null)\n      expect(visualCursor!.offset).toBe(6)\n      expect(visualCursor!.logicalRow).toBe(0)\n      expect(visualCursor!.logicalCol).toBe(6)\n\n      // Set cursor to offset 2\n      editor.editBuffer.setCursorByOffset(2)\n\n      const newVisualCursor = editor.visualCursor\n      expect(newVisualCursor).not.toBe(null)\n      expect(newVisualCursor!.offset).toBe(2)\n      expect(newVisualCursor!.logicalRow).toBe(0)\n      expect(newVisualCursor!.logicalCol).toBe(2)\n    })\n\n    it(\"should set cursor by offset in multi-line content\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line1\\nLine2\\nLine3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Set cursor to offset 6 (start of \"Line2\")\n      editor.editBuffer.setCursorByOffset(6)\n\n      const visualCursor = editor.visualCursor\n      expect(visualCursor).not.toBe(null)\n      expect(visualCursor!.offset).toBe(6)\n      expect(visualCursor!.logicalRow).toBe(1)\n      expect(visualCursor!.logicalCol).toBe(0)\n\n      // Set cursor to offset 8 (L[i]ne2, at 'n')\n      editor.editBuffer.setCursorByOffset(8)\n\n      const newVisualCursor = editor.visualCursor\n      expect(newVisualCursor).not.toBe(null)\n      expect(newVisualCursor!.offset).toBe(8)\n      expect(newVisualCursor!.logicalRow).toBe(1)\n      expect(newVisualCursor!.logicalCol).toBe(2)\n    })\n\n    it(\"should maintain offset consistency when using editorView.setCursorByOffset\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Use editorView instead of editBuffer\n      editor.editorView.setCursorByOffset(3)\n\n      const visualCursor = editor.visualCursor\n      expect(visualCursor).not.toBe(null)\n      expect(visualCursor!.offset).toBe(3)\n      expect(visualCursor!.logicalRow).toBe(0)\n      expect(visualCursor!.logicalCol).toBe(3)\n    })\n\n    it(\"should set cursor to end of content using cursorOffset setter and Bun.stringWidth\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      const content = \"Hello World\"\n      editor.setText(content)\n      editor.cursorOffset = Bun.stringWidth(content)\n\n      const visualCursor = editor.visualCursor\n      expect(visualCursor).not.toBe(null)\n      expect(visualCursor!.offset).toBe(Bun.stringWidth(content))\n      expect(visualCursor!.logicalRow).toBe(0)\n      expect(visualCursor!.logicalCol).toBe(content.length)\n      expect(visualCursor!.visualCol).toBe(content.length)\n\n      // Verify cursor is at the end\n      expect(editor.cursorOffset).toBe(11)\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n  })\n\n  describe(\"EditBufferRenderable Methods\", () => {\n    describe(\"deleteRange\", () => {\n      it(\"should delete range within a single line\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello World\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.deleteRange(0, 6, 0, 11)\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Hello \")\n      })\n\n      it(\"should delete range across multiple lines\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\\nLine 3\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.deleteRange(0, 5, 2, 5)\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Line 3\")\n      })\n\n      it(\"should delete entire line\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"First\\nSecond\\nThird\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.deleteRange(1, 0, 1, 6)\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"First\\n\\nThird\")\n      })\n\n      it(\"should mark yoga node as dirty and request render\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test text\",\n          width: 40,\n          height: 10,\n        })\n\n        const initialHeight = editor.height\n        editor.deleteRange(0, 0, 0, 5)\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"text\")\n      })\n\n      it(\"should handle empty range deletion\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.deleteRange(0, 2, 0, 2)\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Hello\")\n      })\n    })\n\n    describe(\"insertText\", () => {\n      it(\"should insert text at cursor position\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.insertText(\" World\")\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\" WorldHello\")\n      })\n\n      it(\"should insert text in middle of content\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"HelloWorld\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.editBuffer.setCursor(0, 5)\n        editor.insertText(\" \")\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Hello World\")\n      })\n\n      it(\"should insert multiline text\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Start\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.editBuffer.setCursor(0, 5)\n        editor.insertText(\"\\nEnd\")\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Start\\nEnd\")\n      })\n\n      it(\"should mark yoga node as dirty and request render\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.insertText(\"Test\")\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Test\")\n      })\n\n      it(\"should insert multiline text and update content\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.editBuffer.setCursor(0, 6)\n        editor.insertText(\"\\nLine 2\\nLine 3\")\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n        expect(editor.logicalCursor.row).toBe(2)\n      })\n    })\n\n    describe(\"Combined deleteRange and insertText\", () => {\n      it(\"should replace text by deleting range then inserting\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello World\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.deleteRange(0, 6, 0, 11)\n        editor.editBuffer.setCursor(0, 6)\n        editor.insertText(\"Friend\")\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Hello Friend\")\n      })\n\n      it(\"should handle complex editing operations\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\\nLine 3\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.deleteRange(1, 0, 1, 6)\n        editor.editBuffer.setCursor(1, 0)\n        editor.insertText(\"Modified\")\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\"Line 1\\nModified\\nLine 3\")\n      })\n\n      it(\"should work after multiple operations\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Start\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.editBuffer.setCursor(0, 5)\n        editor.insertText(\" Middle\")\n        editor.editBuffer.setCursor(0, 12)\n        editor.insertText(\" End\")\n        editor.deleteRange(0, 0, 0, 5)\n        await renderOnce()\n\n        expect(editor.plainText).toBe(\" Middle End\")\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.destroyed-events.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Destroyed Renderable Event Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Keypress events on destroyed renderable\", () => {\n    it(\"should not trigger handleKeyPress after destroy is called\", async () => {\n      let keypressCalled = false\n      let handleKeyPressCalled = false\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onKeyDown: () => {\n          keypressCalled = true\n        },\n      })\n\n      // Override handleKeyPress to track calls\n      const originalHandleKeyPress = editor.handleKeyPress.bind(editor)\n      editor.handleKeyPress = (key) => {\n        handleKeyPressCalled = true\n        return originalHandleKeyPress(key)\n      }\n\n      editor.focus()\n      await renderOnce()\n\n      // Destroy the renderable\n      editor.destroy()\n\n      // Reset flags\n      keypressCalled = false\n      handleKeyPressCalled = false\n\n      // Try to send a key event after destruction\n      currentMockInput.pressKey(\"A\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      expect(keypressCalled).toBe(false)\n      expect(handleKeyPressCalled).toBe(false)\n    })\n\n    it(\"should not trigger handleKeyPress when destroyed before blur\", async () => {\n      let keypressCalled = false\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onKeyDown: () => {\n          keypressCalled = true\n        },\n      })\n\n      editor.focus()\n      await renderOnce()\n\n      // Destroy without explicitly blurring first (destroy should handle this)\n      editor.destroy()\n\n      keypressCalled = false\n\n      currentMockInput.pressKey(\"B\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      expect(keypressCalled).toBe(false)\n    })\n\n    it(\"should not trigger keypress during async operations after destroy\", async () => {\n      let keypressCount = 0\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onKeyDown: () => {\n          keypressCount++\n        },\n      })\n\n      editor.focus()\n\n      // Queue multiple key presses\n      currentMockInput.pressKey(\"A\")\n      currentMockInput.pressKey(\"B\")\n\n      // Destroy while events might still be processing\n      editor.destroy()\n\n      // Queue more events after destroy\n      currentMockInput.pressKey(\"C\")\n      currentMockInput.pressKey(\"D\")\n\n      await new Promise((resolve) => setTimeout(resolve, 50))\n\n      // At most the first couple events should have been processed before destroy\n      // After destroy, no new events should be processed\n      expect(keypressCount).toBeLessThanOrEqual(2)\n    })\n\n    it(\"should handle rapid focus/destroy/keypress cycles\", async () => {\n      let errors: Error[] = []\n\n      try {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.focus()\n        currentMockInput.pressKey(\"A\")\n        editor.destroy()\n        currentMockInput.pressKey(\"B\")\n\n        await new Promise((resolve) => setTimeout(resolve, 20))\n\n        // Create and destroy another\n        const { textarea: editor2 } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test2\",\n          width: 40,\n          height: 10,\n        })\n\n        editor2.focus()\n        currentMockInput.pressKey(\"C\")\n        editor2.destroy()\n        currentMockInput.pressKey(\"D\")\n\n        await new Promise((resolve) => setTimeout(resolve, 20))\n      } catch (error) {\n        if (error instanceof Error) {\n          errors.push(error)\n        }\n      }\n\n      expect(errors.length).toBe(0)\n    })\n\n    it(\"should not crash when keypressHandler fires after editBuffer is destroyed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      await renderOnce()\n\n      // Destroy the whole textarea properly (not just editBuffer)\n      // Destroying only editBuffer while textarea is alive is undefined behavior\n      editor.destroy()\n\n      // Try pressing key after destroy - should be safely ignored\n      currentMockInput.pressKey(\"X\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      // Should not crash\n      expect(editor.isDestroyed).toBe(true)\n    })\n  })\n\n  describe(\"Paste events on destroyed renderable\", () => {\n    it(\"should not trigger handlePaste after destroy is called\", async () => {\n      let pasteCalled = false\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onPaste: () => {\n          pasteCalled = true\n        },\n      })\n\n      editor.focus()\n      await renderOnce()\n\n      editor.destroy()\n      pasteCalled = false\n\n      await currentMockInput.pasteBracketedText(\"PastedText\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      expect(pasteCalled).toBe(false)\n    })\n\n    it(\"should not trigger paste during async operations after destroy\", async () => {\n      let pasteCount = 0\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onPaste: () => {\n          pasteCount++\n        },\n      })\n\n      editor.focus()\n\n      // Queue paste operation\n      const pastePromise = currentMockInput.pasteBracketedText(\"Text1\")\n\n      // Destroy while paste might still be processing\n      editor.destroy()\n\n      // Try another paste after destroy\n      await currentMockInput.pasteBracketedText(\"Text2\")\n\n      await pastePromise\n      await new Promise((resolve) => setTimeout(resolve, 50))\n\n      // At most the first paste should have been processed\n      expect(pasteCount).toBeLessThanOrEqual(1)\n    })\n  })\n\n  describe(\"Event handlers cleanup on destroy\", () => {\n    it(\"should remove keypress handler from internal key input on destroy\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      await renderOnce()\n\n      // Check that handlers are set up\n      expect(editor.focused).toBe(true)\n\n      editor.destroy()\n\n      // After destroy, focused should be false and handlers should be removed\n      expect(editor.focused).toBe(false)\n\n      // Verify isDestroyed is true\n      expect(editor.isDestroyed).toBe(true)\n    })\n\n    it(\"should not trigger events when destroyed renderable is still in tree\", async () => {\n      let keypressCount = 0\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onKeyDown: () => {\n          keypressCount++\n        },\n      })\n\n      editor.focus()\n      await renderOnce()\n\n      // Destroy the renderable (this should remove it from parent and clean up handlers)\n      editor.destroy()\n\n      expect(editor.isDestroyed).toBe(true)\n      keypressCount = 0\n\n      // Try to send events\n      currentMockInput.pressKey(\"A\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      expect(keypressCount).toBe(0)\n    })\n\n    it(\"should handle destroy called multiple times\", async () => {\n      let errorOccurred = false\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      try {\n        editor.destroy()\n        editor.destroy()\n        editor.destroy()\n      } catch (error) {\n        errorOccurred = true\n      }\n\n      expect(errorOccurred).toBe(false)\n    })\n\n    it(\"should clean up event listeners when destroyed while handling an event\", async () => {\n      let handlerCallCount = 0\n      let shouldDestroy = false\n      let errorThrown = false\n\n      // Capture console.error to check for error logs\n      const originalConsoleError = console.error\n      console.error = (...args: any[]) => {\n        if (args[0]?.includes?.(\"[KeyHandler] Error in renderable\")) {\n          errorThrown = true\n        }\n        originalConsoleError(...args)\n      }\n\n      try {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          onKeyDown: () => {\n            handlerCallCount++\n            if (shouldDestroy) {\n              editor.destroy()\n            }\n          },\n        })\n\n        editor.focus()\n\n        // First keypress should work\n        currentMockInput.pressKey(\"A\")\n        await new Promise((resolve) => setTimeout(resolve, 20))\n        expect(handlerCallCount).toBe(1)\n\n        // Second keypress destroys the renderable\n        shouldDestroy = true\n        currentMockInput.pressKey(\"B\")\n        await new Promise((resolve) => setTimeout(resolve, 20))\n        expect(handlerCallCount).toBe(2)\n\n        // Third keypress should not trigger anything\n        currentMockInput.pressKey(\"C\")\n        await new Promise((resolve) => setTimeout(resolve, 20))\n        expect(handlerCallCount).toBe(2)\n\n        // CRITICAL: No error should be thrown when destroying during callback\n        expect(errorThrown).toBe(false)\n      } finally {\n        console.error = originalConsoleError\n      }\n    })\n  })\n\n  describe(\"Destroyed renderable with queued operations\", () => {\n    it(\"should not process insertText after destroy\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Initial\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      editor.destroy()\n\n      // Try to call methods on destroyed renderable\n      try {\n        editor.insertText(\"New Text\")\n      } catch (error) {\n        // Expected: operations might throw after destroy\n        expect(error).toBeDefined()\n      }\n\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      // Either the operation threw an error or it was safely ignored\n      expect(true).toBe(true)\n    })\n\n    it(\"should handle events arriving between destroy and cleanup\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Queue several key events\n      currentMockInput.pressKey(\"A\")\n      currentMockInput.pressKey(\"B\")\n      currentMockInput.pressKey(\"C\")\n\n      // Destroy immediately without waiting for events to process\n      editor.destroy()\n\n      // Events might still be in the queue\n      await new Promise((resolve) => setTimeout(resolve, 50))\n\n      // No crashes should occur\n      expect(editor.isDestroyed).toBe(true)\n    })\n\n    it(\"should safely handle focus after destroy\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.focused).toBe(true)\n\n      editor.destroy()\n\n      // Try to focus again after destroy (should be no-op or throw)\n      try {\n        editor.focus()\n      } catch (error) {\n        // May throw, that's fine\n        expect(error).toBeDefined()\n      }\n\n      // Whether it throws or not, it shouldn't crash\n      expect(editor.focused).toBe(false)\n    })\n  })\n\n  describe(\"EditorView and EditBuffer destroyed state\", () => {\n    it(\"should check if editBuffer guard prevents operations after destroy\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Destroy the textarea (which should destroy editBuffer and editorView)\n      editor.destroy()\n\n      // Try to access editBuffer methods that should throw if destroyed\n      let errorThrown = false\n      try {\n        editor.editBuffer.getText()\n      } catch (error) {\n        errorThrown = true\n        expect((error as Error).message).toContain(\"destroyed\")\n      }\n\n      expect(errorThrown).toBe(true)\n    })\n\n    it(\"should check if editorView guard prevents operations after destroy\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Destroy the textarea\n      editor.destroy()\n\n      // Try to access editorView methods that should throw if destroyed\n      let errorThrown = false\n      try {\n        editor.editorView.getCursor()\n      } catch (error) {\n        errorThrown = true\n        expect((error as Error).message).toContain(\"destroyed\")\n      }\n\n      expect(errorThrown).toBe(true)\n    })\n\n    it(\"should not allow keypress after proper destroy\", async () => {\n      let keypressFired = false\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onKeyDown: () => {\n          keypressFired = true\n        },\n      })\n\n      editor.focus()\n      await renderOnce()\n\n      // Properly destroy the whole textarea\n      editor.destroy()\n\n      // Try to trigger a keypress after destroy\n      currentMockInput.pressKey(\"A\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      // Keypress handler should not have fired\n      expect(keypressFired).toBe(false)\n      expect(editor.isDestroyed).toBe(true)\n    })\n  })\n\n  describe(\"Multiple renderables and event routing\", () => {\n    it(\"should not route events to destroyed renderable when multiple exist\", async () => {\n      let editor1KeypressCount = 0\n      let editor2KeypressCount = 0\n\n      const { textarea: editor1 } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Editor 1\",\n        width: 40,\n        height: 10,\n        onKeyDown: () => {\n          editor1KeypressCount++\n        },\n      })\n\n      const { textarea: editor2 } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Editor 2\",\n        width: 40,\n        height: 10,\n        top: 10,\n        onKeyDown: () => {\n          editor2KeypressCount++\n        },\n      })\n\n      // Focus first editor\n      editor1.focus()\n      currentMockInput.pressKey(\"A\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      expect(editor1KeypressCount).toBe(1)\n      expect(editor2KeypressCount).toBe(0)\n\n      // Destroy first editor and focus second\n      editor1.destroy()\n      editor2.focus()\n\n      editor1KeypressCount = 0\n      editor2KeypressCount = 0\n\n      currentMockInput.pressKey(\"B\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      expect(editor1KeypressCount).toBe(0)\n      expect(editor2KeypressCount).toBe(1)\n\n      editor2.destroy()\n    })\n\n    it(\"should handle switching focus between renderables rapidly\", async () => {\n      const { textarea: editor1 } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Editor 1\",\n        width: 40,\n        height: 10,\n      })\n\n      const { textarea: editor2 } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Editor 2\",\n        width: 40,\n        height: 10,\n        top: 10,\n      })\n\n      // Rapidly switch focus and destroy\n      editor1.focus()\n      editor2.focus()\n      editor1.destroy()\n      editor2.blur()\n      editor2.focus()\n      editor2.destroy()\n\n      // Send events after all destroyed\n      currentMockInput.pressKey(\"X\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      // Should not crash\n      expect(true).toBe(true)\n    })\n  })\n\n  describe(\"Renderable destroyed flag checks\", () => {\n    it(\"should prevent handleKeyPress execution when isDestroyed is true\", async () => {\n      let callCount = 0\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      // Wrap handleKeyPress to track calls\n      const originalHandleKeyPress = editor.handleKeyPress.bind(editor)\n      editor.handleKeyPress = (key) => {\n        callCount++\n        if (editor.isDestroyed) {\n          // Should not execute when destroyed\n          throw new Error(\"handleKeyPress called on destroyed renderable\")\n        }\n        return originalHandleKeyPress(key)\n      }\n\n      editor.focus()\n      currentMockInput.pressKey(\"A\")\n      await new Promise((resolve) => setTimeout(resolve, 20))\n\n      expect(callCount).toBe(1)\n\n      // Destroy and try again\n      editor.destroy()\n      callCount = 0\n\n      let errorThrown = false\n      try {\n        currentMockInput.pressKey(\"B\")\n        await new Promise((resolve) => setTimeout(resolve, 20))\n      } catch (error) {\n        errorThrown = true\n      }\n\n      // Should not have called handleKeyPress after destroy\n      expect(callCount).toBe(0)\n      expect(errorThrown).toBe(false)\n    })\n\n    it(\"should check isDestroyed in event handler methods\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      expect(editor.isDestroyed).toBe(false)\n\n      editor.focus()\n      expect(editor.isDestroyed).toBe(false)\n\n      editor.destroy()\n      expect(editor.isDestroyed).toBe(true)\n\n      // After destroy, operations should either fail or be no-ops\n      let errorCount = 0\n      try {\n        editor.focus()\n      } catch {\n        errorCount++\n      }\n\n      try {\n        editor.blur()\n      } catch {\n        errorCount++\n      }\n\n      // Operations after destroy should either throw or be ignored\n      // The important thing is we should be able to detect destroyed state\n      expect(editor.isDestroyed).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.editing.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\nimport { TextareaRenderable } from \"../Textarea.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Editing Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Initialization\", () => {\n    it(\"should initialize with default options\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 40,\n        height: 10,\n      })\n\n      expect(editor.x).toBeDefined()\n      expect(editor.y).toBeDefined()\n      expect(editor.width).toBeGreaterThan(0)\n      expect(editor.height).toBeGreaterThan(0)\n      expect(editor.focusable).toBe(true)\n    })\n\n    it(\"should initialize with content\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should initialize with empty content\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should initialize with multi-line content\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      expect(editor.plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n    })\n  })\n\n  describe(\"Focus Management\", () => {\n    it(\"should handle focus and blur\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"test\",\n        width: 40,\n        height: 10,\n      })\n\n      expect(editor.focused).toBe(false)\n\n      editor.focus()\n      expect(editor.focused).toBe(true)\n\n      editor.blur()\n      expect(editor.focused).toBe(false)\n    })\n  })\n\n  describe(\"Text Insertion via Methods\", () => {\n    it(\"should insert single character\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.gotoLine(9999) // Move to end\n      editor.insertChar(\"!\")\n\n      expect(editor.plainText).toBe(\"Hello!\")\n    })\n\n    it(\"should insert text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.gotoLine(9999) // Move to end\n      editor.insertText(\" World\")\n\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should insert text in middle\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"HelloWorld\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.moveCursorRight()\n      editor.moveCursorRight()\n      editor.moveCursorRight()\n      editor.moveCursorRight()\n      editor.moveCursorRight()\n      editor.insertText(\" \")\n\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n  })\n\n  describe(\"Text Deletion via Methods\", () => {\n    it(\"should delete character at cursor\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      // Move to 'W' and delete it\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n      editor.deleteChar()\n\n      expect(editor.plainText).toBe(\"Hello orld\")\n    })\n\n    it(\"should delete character backward\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.gotoLine(9999) // Move to end\n      editor.deleteCharBackward()\n\n      expect(editor.plainText).toBe(\"Hell\")\n    })\n\n    it(\"should delete entire line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.gotoLine(1)\n      editor.deleteLine()\n\n      expect(editor.plainText).toBe(\"Line 1\\nLine 3\")\n    })\n\n    it(\"should delete to line end\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n      editor.deleteToLineEnd()\n\n      expect(editor.plainText).toBe(\"Hello \")\n    })\n  })\n\n  describe(\"Cursor Movement via Methods\", () => {\n    it(\"should move cursor left and right\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDE\",\n        width: 40,\n        height: 10,\n      })\n\n      const initialCursor = editor.logicalCursor\n      expect(initialCursor.col).toBe(0)\n\n      editor.moveCursorRight()\n      expect(editor.logicalCursor.col).toBe(1)\n\n      editor.moveCursorRight()\n      expect(editor.logicalCursor.col).toBe(2)\n\n      editor.moveCursorLeft()\n      expect(editor.logicalCursor.col).toBe(1)\n    })\n\n    it(\"should move cursor up and down\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      expect(editor.logicalCursor.row).toBe(0)\n\n      editor.moveCursorDown()\n      expect(editor.logicalCursor.row).toBe(1)\n\n      editor.moveCursorDown()\n      expect(editor.logicalCursor.row).toBe(2)\n\n      editor.moveCursorUp()\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should move to line start and end\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      const cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999) // Move to end of line\n      expect(editor.logicalCursor.col).toBe(11)\n\n      editor.editBuffer.setCursor(editor.logicalCursor.row, 0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should move to buffer start and end\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.gotoLine(9999) // Move to end\n      let cursor = editor.logicalCursor\n      expect(cursor.row).toBe(2)\n\n      editor.gotoLine(0) // Move to start\n      cursor = editor.logicalCursor\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should goto specific line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 0\\nLine 1\\nLine 2\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.gotoLine(1)\n      expect(editor.logicalCursor.row).toBe(1)\n\n      editor.gotoLine(2)\n      expect(editor.logicalCursor.row).toBe(2)\n    })\n  })\n\n  describe(\"Keyboard Input - Character Insertion\", () => {\n    it(\"should insert character when key is pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"h\")\n      expect(editor.plainText).toBe(\"h\")\n\n      currentMockInput.pressKey(\"i\")\n      expect(editor.plainText).toBe(\"hi\")\n    })\n\n    it(\"should insert multiple characters in sequence\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"h\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"o\")\n\n      expect(editor.plainText).toBe(\"hello\")\n    })\n\n    it(\"should insert space character\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      currentMockInput.pressKey(\" \")\n      currentMockInput.pressKey(\"W\")\n      currentMockInput.pressKey(\"o\")\n      currentMockInput.pressKey(\"r\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"d\")\n\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should not insert when not focused\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      // Don't focus\n      expect(editor.focused).toBe(false)\n\n      currentMockInput.pressKey(\"a\")\n      expect(editor.plainText).toBe(\"\")\n    })\n  })\n\n  describe(\"Keyboard Input - Arrow Keys\", () => {\n    it(\"should move cursor left with arrow key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressArrow(\"left\")\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressArrow(\"left\")\n      expect(editor.logicalCursor.col).toBe(1)\n    })\n\n    it(\"should move cursor right with arrow key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(2)\n    })\n\n    it(\"should move cursor up and down with arrow keys\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.row).toBe(0)\n\n      currentMockInput.pressArrow(\"down\")\n      expect(editor.logicalCursor.row).toBe(1)\n\n      currentMockInput.pressArrow(\"down\")\n      expect(editor.logicalCursor.row).toBe(2)\n\n      currentMockInput.pressArrow(\"up\")\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should move cursor smoothly from end of one line to start of next\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\\nDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      const cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999) // Move to end of line // End of \"ABC\"\n      expect(editor.logicalCursor.col).toBe(3)\n\n      // Move right should go to start of next line\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      // Move left should go back to end of previous line\n      currentMockInput.pressArrow(\"left\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n  })\n\n  describe(\"Keyboard Input - Backspace and Delete\", () => {\n    it(\"should handle backspace key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Hell\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Hel\")\n    })\n\n    it(\"should handle delete key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      // Cursor at start\n\n      currentMockInput.pressKey(\"DELETE\")\n      expect(editor.plainText).toBe(\"ello\")\n    })\n\n    it(\"should join lines when backspace at start of line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\\nWorld\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to line 2 (0-indexed line 1)\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"HelloWorld\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(5) // Should be at end of \"Hello\"\n    })\n\n    it(\"should remove empty line when backspace at start\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\\n\\nWorld\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to empty line\n      expect(editor.logicalCursor.row).toBe(1)\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Hello\\nWorld\")\n      expect(editor.logicalCursor.row).toBe(0)\n    })\n\n    it(\"should join lines with content when backspace at start\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line1\\nLine2\\nLine3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(2) // Move to \"Line3\"\n      expect(editor.logicalCursor.row).toBe(2)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Line1\\nLine2Line3\")\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(5) // After \"Line2\"\n    })\n\n    it(\"should not do anything when backspace at start of first line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\\nWorld\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Hello\\nWorld\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should handle multiple backspaces joining multiple lines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"A\\nB\\nC\\nD\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(3) // Line \"D\"\n      expect(editor.logicalCursor.row).toBe(3)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"A\\nB\\nCD\")\n      expect(editor.logicalCursor.row).toBe(2)\n      // Cursor should be at the join point (after \"C\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      // Now delete \"C\" by pressing backspace\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"A\\nB\\nD\")\n      expect(editor.logicalCursor.row).toBe(2)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      // Now join line 2 with line 1\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"A\\nBD\")\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(1) // After \"B\"\n\n      // Delete \"B\"\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"A\\nD\")\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      // Now join line 1 with line 0\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"AD\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(1)\n    })\n\n    it(\"should handle backspace after typing on new line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Hello\\n\")\n\n      currentMockInput.pressKey(\"W\")\n      currentMockInput.pressKey(\"o\")\n      currentMockInput.pressKey(\"r\")\n      expect(editor.plainText).toBe(\"Hello\\nWor\")\n\n      // Now backspace to delete \"r\"\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Hello\\nWo\")\n\n      // Move to start of line and backspace to join\n      editor.editBuffer.setCursor(editor.logicalCursor.row, 0)\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"HelloWo\")\n    })\n\n    it(\"should move cursor right after joining lines with backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\\nWorld\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to \"World\"\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      // Join lines with backspace\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"HelloWorld\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(5) // After \"Hello\"\n\n      // Press right repeatedly - should advance one at a time\n      const positions: number[] = [editor.logicalCursor.col]\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\")\n        positions.push(editor.logicalCursor.col)\n      }\n\n      // Should advance one position each time: [5, 6, 7, 8, 9, 10]\n      expect(positions).toEqual([5, 6, 7, 8, 9, 10])\n    })\n\n    it(\"should move right one position after join\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AB\\nCD\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      // Backspace to join\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCD\")\n      expect(editor.logicalCursor.col).toBe(2)\n\n      // Press right - should advance by 1\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should advance cursor by 1 at every position after join\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDE\\nFGHIJ\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      // Join lines\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEFGHIJ\")\n      expect(editor.logicalCursor.col).toBe(5)\n\n      // Each right press should advance by exactly 1\n      const expectedPositions = [5, 6, 7, 8, 9, 10]\n\n      for (let i = 0; i < expectedPositions.length; i++) {\n        expect(editor.logicalCursor.col).toBe(expectedPositions[i])\n        if (i < expectedPositions.length - 1) {\n          currentMockInput.pressArrow(\"right\")\n        }\n      }\n    })\n\n    it(\"should move right after backspace join - setText content\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\\nDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEF\")\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(4)\n    })\n\n    it(\"should move right after backspace join - typed content\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Type \"ABC\", Enter, \"DEF\"\n      currentMockInput.pressKey(\"A\")\n      currentMockInput.pressKey(\"B\")\n      currentMockInput.pressKey(\"C\")\n      currentMockInput.pressEnter()\n      currentMockInput.pressKey(\"D\")\n      currentMockInput.pressKey(\"E\")\n      currentMockInput.pressKey(\"F\")\n\n      // Join and verify cursor advances\n      editor.editBuffer.setCursor(editor.logicalCursor.row, 0)\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEF\")\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(4)\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(5)\n    })\n\n    it(\"should move cursor left after joining lines with backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\\nDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to \"DEF\"\n\n      // Join lines\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEF\")\n      expect(editor.logicalCursor.col).toBe(3) // After \"ABC\"\n\n      // Move right past the boundary\n      currentMockInput.pressArrow(\"right\")\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(5)\n\n      // Now move left - should move smoothly back one at a time\n      const positions: number[] = [editor.logicalCursor.col]\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"left\")\n        positions.push(editor.logicalCursor.col)\n      }\n\n      // Should go back one at a time: [5, 4, 3, 2, 1, 0]\n      expect(positions).toEqual([5, 4, 3, 2, 1, 0])\n    })\n\n    it(\"should move cursor left across chunk boundaries after joining lines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\\nDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to \"DEF\"\n\n      // Join lines\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEF\")\n      expect(editor.logicalCursor.col).toBe(3) // After \"ABC\"\n\n      // Move right to \"D\"\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(4)\n\n      // Move right to \"E\"\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(5)\n\n      // Now move left back across the chunk boundary\n      currentMockInput.pressArrow(\"left\")\n      expect(editor.logicalCursor.col).toBe(4)\n\n      currentMockInput.pressArrow(\"left\")\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressArrow(\"left\")\n      expect(editor.logicalCursor.col).toBe(2)\n    })\n\n    it(\"should handle shift+backspace same as backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"Hello Worl\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"Hello Wor\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Hello Wo\")\n    })\n\n    it(\"should join lines with shift+backspace at start of line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"First\\nSecond\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"FirstSecond\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(5)\n    })\n\n    it(\"should handle shift+backspace with selection\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\" World\")\n      expect(editor.hasSelection()).toBe(false)\n    })\n\n    it(\"should delete characters consistently with shift+backspace after typing\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"T\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"s\")\n      currentMockInput.pressKey(\"t\")\n      expect(editor.plainText).toBe(\"Test\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"Tes\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"Te\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"T\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should not differentiate between backspace and shift+backspace behavior\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDE\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"ABCD\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABC\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"AB\")\n    })\n\n    it(\"should handle shift+backspace at start of buffer\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"Test\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should handle alternating backspace and shift+backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"123456\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n      expect(editor.plainText).toBe(\"123456\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"12345\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"1234\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"123\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"12\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"1\")\n\n      currentMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n  })\n\n  describe(\"Keyboard Input - Kitty Keyboard Protocol\", () => {\n    let kittyRenderer: TestRenderer\n    let kittyRenderOnce: () => Promise<void>\n    let kittyMockInput: MockInput\n\n    beforeEach(async () => {\n      ;({\n        renderer: kittyRenderer,\n        renderOnce: kittyRenderOnce,\n        mockInput: kittyMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        kittyKeyboard: true,\n      }))\n    })\n\n    afterEach(() => {\n      kittyRenderer.destroy()\n    })\n\n    it(\"should handle shift+backspace in kitty mode\", async () => {\n      const textarea = new TextareaRenderable(kittyRenderer, {\n        left: 0,\n        top: 0,\n        width: 40,\n        height: 10,\n        initialValue: \"Hello World\",\n      })\n      kittyRenderer.root.add(textarea)\n      await kittyRenderOnce()\n\n      textarea.focus()\n      textarea.gotoLine(9999)\n\n      kittyMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(textarea.plainText).toBe(\"Hello Worl\")\n\n      kittyMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(textarea.plainText).toBe(\"Hello Wor\")\n    })\n\n    it(\"should handle shift+backspace joining lines in kitty mode\", async () => {\n      const textarea = new TextareaRenderable(kittyRenderer, {\n        left: 0,\n        top: 0,\n        width: 40,\n        height: 10,\n        initialValue: \"Line1\\nLine2\",\n      })\n      kittyRenderer.root.add(textarea)\n      await kittyRenderOnce()\n\n      textarea.focus()\n      textarea.gotoLine(1)\n\n      kittyMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(textarea.plainText).toBe(\"Line1Line2\")\n      expect(textarea.logicalCursor.row).toBe(0)\n      expect(textarea.logicalCursor.col).toBe(5)\n    })\n\n    it(\"should handle shift+backspace with selection in kitty mode\", async () => {\n      const textarea = new TextareaRenderable(kittyRenderer, {\n        left: 0,\n        top: 0,\n        width: 40,\n        height: 10,\n        initialValue: \"Hello World\",\n      })\n      kittyRenderer.root.add(textarea)\n      await kittyRenderOnce()\n\n      textarea.focus()\n\n      for (let i = 0; i < 5; i++) {\n        kittyMockInput.pressArrow(\"right\", { shift: true })\n      }\n      expect(textarea.hasSelection()).toBe(true)\n      expect(textarea.getSelectedText()).toBe(\"Hello\")\n\n      kittyMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(textarea.plainText).toBe(\" World\")\n      expect(textarea.hasSelection()).toBe(false)\n    })\n\n    it(\"should distinguish backspace vs shift+backspace keybindings in kitty mode\", async () => {\n      const textarea = new TextareaRenderable(kittyRenderer, {\n        left: 0,\n        top: 0,\n        width: 40,\n        height: 10,\n        initialValue: \"ABC\",\n      })\n      kittyRenderer.root.add(textarea)\n      await kittyRenderOnce()\n\n      textarea.focus()\n      textarea.gotoLine(9999)\n\n      kittyMockInput.pressBackspace()\n      expect(textarea.plainText).toBe(\"AB\")\n\n      kittyMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      expect(textarea.plainText).toBe(\"A\")\n    })\n\n    it(\"should handle mixed backspace and shift+backspace in kitty mode\", async () => {\n      const textarea = new TextareaRenderable(kittyRenderer, {\n        left: 0,\n        top: 0,\n        width: 40,\n        height: 10,\n        initialValue: \"123456\",\n      })\n      kittyRenderer.root.add(textarea)\n      await kittyRenderOnce()\n\n      textarea.focus()\n      textarea.gotoLine(9999)\n\n      kittyMockInput.pressBackspace()\n      kittyMockInput.pressKey(\"BACKSPACE\", { shift: true })\n      kittyMockInput.pressBackspace()\n      kittyMockInput.pressKey(\"BACKSPACE\", { shift: true })\n\n      expect(textarea.plainText).toBe(\"12\")\n    })\n\n    it(\"should handle shift+delete in kitty mode\", async () => {\n      const textarea = new TextareaRenderable(kittyRenderer, {\n        left: 0,\n        top: 0,\n        width: 40,\n        height: 10,\n        initialValue: \"Hello\",\n      })\n      kittyRenderer.root.add(textarea)\n      await kittyRenderOnce()\n\n      textarea.focus()\n\n      kittyMockInput.pressKey(\"DELETE\", { shift: true })\n      expect(textarea.plainText).toBe(\"ello\")\n    })\n\n    it(\"should handle ctrl+backspace for word deletion in kitty mode\", async () => {\n      const textarea = new TextareaRenderable(kittyRenderer, {\n        left: 0,\n        top: 0,\n        width: 40,\n        height: 10,\n        initialValue: \"hello world test\",\n      })\n      kittyRenderer.root.add(textarea)\n      await kittyRenderOnce()\n\n      textarea.focus()\n      textarea.gotoLine(9999)\n\n      kittyMockInput.pressKey(\"w\", { ctrl: true })\n      expect(textarea.plainText).toBe(\"hello world \")\n    })\n\n    it(\"should handle meta+backspace for word deletion in kitty mode\", async () => {\n      const textarea = new TextareaRenderable(kittyRenderer, {\n        left: 0,\n        top: 0,\n        width: 40,\n        height: 10,\n        initialValue: \"hello world test\",\n      })\n      kittyRenderer.root.add(textarea)\n      await kittyRenderOnce()\n\n      textarea.focus()\n      textarea.gotoLine(9999)\n\n      kittyMockInput.pressBackspace({ meta: true })\n      const text = textarea.plainText\n      expect(text.startsWith(\"hello world\")).toBe(true)\n      expect(text.length).toBeLessThan(16)\n    })\n  })\n\n  describe(\"Keyboard Input - Enter/Return\", () => {\n    it(\"should insert newline with Enter key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"HelloWorld\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Move to middle\n      for (let i = 0; i < 5; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Hello\\nWorld\")\n    })\n\n    it(\"should insert newline at end\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Hello\\n\")\n    })\n\n    it(\"should handle multiple newlines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line1\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      currentMockInput.pressEnter()\n      currentMockInput.pressKey(\"L\")\n      currentMockInput.pressKey(\"i\")\n      currentMockInput.pressKey(\"n\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"2\")\n\n      expect(editor.plainText).toBe(\"Line1\\nLine2\")\n    })\n  })\n\n  describe(\"Keyboard Input - Home and End\", () => {\n    it(\"should move to line start with Home\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n      expect(editor.logicalCursor.col).toBe(11)\n\n      currentMockInput.pressKey(\"HOME\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should move to line end with End\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"END\")\n      expect(editor.logicalCursor.col).toBe(11)\n    })\n  })\n\n  describe(\"Keyboard Input - Control Commands\", () => {\n    it(\"should move to line start with Ctrl+A\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to line 2\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight() // Move to middle of line\n      }\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"a\", { ctrl: true })\n      const cursor = editor.logicalCursor\n      expect(cursor.row).toBe(1) // Should stay on same line\n      expect(cursor.col).toBe(0) // Should move to start of line\n    })\n\n    it(\"should move to line end with Ctrl+E\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to line 2\n\n      currentMockInput.pressKey(\"e\", { ctrl: true })\n      const cursor = editor.logicalCursor\n      expect(cursor.row).toBe(1) // Should stay on same line\n      expect(cursor.col).toBe(6) // \"Line 2\" is 6 chars\n    })\n\n    it(\"should delete character forward with Ctrl+D\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      currentMockInput.pressKey(\"d\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1\\nine 2\\nLine 3\")\n    })\n\n    it(\"should delete to line end with Ctrl+K\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Hello \")\n    })\n\n    it(\"should move to buffer start with Home key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(2) // Move to line 3\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight() // Move to middle of line\n      }\n      expect(editor.logicalCursor.row).toBe(2)\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"HOME\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should move to buffer end with End key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"END\")\n      expect(editor.logicalCursor.row).toBe(2)\n      expect(editor.logicalCursor.col).toBe(6) // \"Line 3\" is 6 chars\n    })\n\n    it(\"should select from cursor to buffer start with Home+Shift\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to line 2\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight() // Move to \"Lin|e 2\"\n      }\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"HOME\", { shift: true })\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      const selection = editor.getSelection()\n      expect(selection).not.toBeNull()\n      expect(selection!.start).toBe(0) // Selection starts at buffer start\n      // Selection should include everything from buffer start to original cursor position\n      // gotoLine(1) positions at end of line, moveCursorRight 3 times goes to col 3 of next line\n      // Selection from buffer start to cursor includes \"Line 1\\nLine\" (one more than \"Lin\" due to cursor position)\n      expect(editor.getSelectedText()).toBe(\"Line 1\\nLine\")\n    })\n\n    it(\"should select from cursor to buffer end with End+Shift\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Move to line 2\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight() // Move to \"Lin|e 2\"\n      }\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"END\", { shift: true })\n      expect(editor.logicalCursor.row).toBe(2)\n      expect(editor.logicalCursor.col).toBe(6)\n\n      const selection = editor.getSelection()\n      expect(selection).not.toBeNull()\n      // Selection should include everything from original cursor position to buffer end\n      expect(editor.getSelectedText()).toBe(\"e 2\\nLine 3\")\n    })\n  })\n\n  describe(\"Word Movement and Deletion\", () => {\n    it(\"should move forward by word with Alt+F\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world foo bar\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(12)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(16)\n    })\n\n    it(\"should move backward by word with Alt+B\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world foo bar\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n      expect(editor.logicalCursor.col).toBe(19)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(16)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(12)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(6)\n    })\n\n    it(\"should move forward by word with Meta+Right\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"one two three\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(4)\n\n      currentMockInput.pressArrow(\"right\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(8)\n    })\n\n    it(\"should move backward by word with Meta+Left\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"one two three\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressArrow(\"left\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(8)\n\n      currentMockInput.pressArrow(\"left\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(4)\n    })\n\n    it(\"should delete word forward with Alt+D\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world foo\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.plainText).toBe(\"hello world foo\")\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText).toBe(\"world foo\")\n    })\n\n    it(\"should delete word backward with Alt+Backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world foo\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressBackspace({ meta: true })\n      const text = editor.plainText\n      expect(text.startsWith(\"hello world\")).toBe(true)\n      expect(text.length).toBeLessThan(15)\n    })\n\n    it(\"should delete word backward with Ctrl+W\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"test string here\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"test string \")\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"test \")\n    })\n\n    it(\"should delete line with Ctrl+Shift+D (requires Kitty keyboard protocol)\", async () => {\n      const {\n        renderer: kittyRenderer,\n        renderOnce: kittyRenderOnce,\n        mockInput: kittyMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        kittyKeyboard: true,\n      })\n\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      kittyMockInput.pressKey(\"d\", { ctrl: true, shift: true })\n      expect(editor.plainText).toBe(\"Line 1\\nLine 3\")\n\n      kittyRenderer.destroy()\n    })\n\n    it(\"should handle word movement across multiple lines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"first line\\nsecond line\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should delete word forward from line start\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello\\nworld test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n      const initialLength = editor.plainText.length\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText.length).toBeLessThan(initialLength)\n      expect(editor.plainText).toContain(\"hello\")\n    })\n\n    it(\"should handle word deletion operations with Alt+D\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText).toBe(\"world test\")\n    })\n\n    it(\"should navigate by words and characters\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"abc def ghi\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      const col1 = editor.logicalCursor.col\n      expect(col1).toBeGreaterThan(0)\n\n      currentMockInput.pressArrow(\"right\")\n      const col2 = editor.logicalCursor.col\n      expect(col2).toBe(col1 + 1)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      const col3 = editor.logicalCursor.col\n      expect(col3).toBeGreaterThan(col2)\n    })\n\n    it(\"should delete word forward even with selection when using meta+d\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world foo\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      expect(editor.hasSelection()).toBe(true)\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText).toBe(\"lo world foo\")\n    })\n  })\n\n  describe(\"Chunk Boundary Navigation\", () => {\n    it(\"should move cursor across chunks created by insertions\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Insert \"Hello\"\n      currentMockInput.pressKey(\"H\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"o\")\n      expect(editor.plainText).toBe(\"Hello\")\n      expect(editor.logicalCursor.col).toBe(5)\n\n      // Move cursor back to position 2\n      for (let i = 0; i < 3; i++) {\n        currentMockInput.pressArrow(\"left\")\n      }\n      expect(editor.logicalCursor.col).toBe(2)\n\n      // Insert \"XXX\" - this creates a new chunk in the middle\n      currentMockInput.pressKey(\"X\")\n      currentMockInput.pressKey(\"X\")\n      currentMockInput.pressKey(\"X\")\n      expect(editor.plainText).toBe(\"HeXXXllo\")\n      expect(editor.logicalCursor.col).toBe(5)\n\n      // Now move right - should move smoothly across chunk boundaries\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(6) // \"l\"\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(7) // \"l\"\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(8) // \"o\"\n    })\n\n    it(\"should move cursor left across multiple chunks\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      // Insert at end\n      currentMockInput.pressKey(\"1\")\n      currentMockInput.pressKey(\"2\")\n      currentMockInput.pressKey(\"3\")\n      expect(editor.plainText).toBe(\"Test123\")\n\n      // Move to middle and insert again\n      editor.gotoLine(0) // Move to start\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressArrow(\"right\")\n      }\n      currentMockInput.pressKey(\"A\")\n      currentMockInput.pressKey(\"B\")\n      expect(editor.plainText).toBe(\"TestAB123\")\n      expect(editor.logicalCursor.col).toBe(6)\n\n      // Now move left across all chunk boundaries\n      for (let i = 6; i > 0; i--) {\n        currentMockInput.pressArrow(\"left\")\n        expect(editor.logicalCursor.col).toBe(i - 1)\n      }\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should move cursor right across all chunks to end\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AB\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      const cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999) // Move to end of line\n      expect(editor.logicalCursor.col).toBe(2)\n\n      // Insert at end\n      currentMockInput.pressKey(\"C\")\n      currentMockInput.pressKey(\"D\")\n      expect(editor.plainText).toBe(\"ABCD\")\n\n      // Move to start\n      editor.gotoLine(0) // Move to start\n      expect(editor.logicalCursor.col).toBe(0)\n\n      // Move right through all characters\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressArrow(\"right\")\n        expect(editor.logicalCursor.col).toBe(i + 1)\n      }\n    })\n\n    it(\"should handle cursor movement after multiple insertions and deletions\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Start\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n      expect(editor.logicalCursor.col).toBe(5)\n\n      // Insert text\n      currentMockInput.pressKey(\"1\")\n      currentMockInput.pressKey(\"2\")\n\n      // Delete one\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Start1\")\n\n      // Insert more\n      currentMockInput.pressKey(\"X\")\n      currentMockInput.pressKey(\"Y\")\n      expect(editor.plainText).toBe(\"Start1XY\")\n\n      // Move back to start\n      editor.gotoLine(0) // Move to start\n\n      // Move right through all characters one by one\n      for (let i = 0; i < 8; i++) {\n        expect(editor.logicalCursor.col).toBe(i)\n        currentMockInput.pressArrow(\"right\")\n      }\n      expect(editor.logicalCursor.col).toBe(8)\n    })\n  })\n\n  describe(\"Complex Editing Scenarios\", () => {\n    it(\"should handle typing, navigation, and deletion\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Type \"Hello\"\n      currentMockInput.pressKey(\"H\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"o\")\n      expect(editor.plainText).toBe(\"Hello\")\n\n      // Add space and \"World\"\n      currentMockInput.pressKey(\" \")\n      currentMockInput.pressKey(\"W\")\n      currentMockInput.pressKey(\"o\")\n      currentMockInput.pressKey(\"r\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"d\")\n      expect(editor.plainText).toBe(\"Hello World\")\n\n      // Backspace a few times\n      currentMockInput.pressBackspace()\n      currentMockInput.pressBackspace()\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Hello Wo\")\n    })\n\n    it(\"should handle newlines and multi-line editing\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"L\")\n      currentMockInput.pressKey(\"i\")\n      currentMockInput.pressKey(\"n\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"1\")\n      currentMockInput.pressEnter()\n      currentMockInput.pressKey(\"L\")\n      currentMockInput.pressKey(\"i\")\n      currentMockInput.pressKey(\"n\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"2\")\n\n      expect(editor.plainText).toBe(\"Line1\\nLine2\")\n    })\n\n    it(\"should handle insert and delete in sequence\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      currentMockInput.pressKey(\"i\")\n      currentMockInput.pressKey(\"n\")\n      currentMockInput.pressKey(\"g\")\n      expect(editor.plainText).toBe(\"Testing\")\n\n      currentMockInput.pressBackspace()\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Testi\")\n    })\n  })\n\n  describe(\"Edit Operations\", () => {\n    it(\"should maintain correct cursor position after join, insert, backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\\nDEF\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      editor.gotoLine(1)\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(0)\n      expect(editor.plainText).toBe(\"ABC\\nDEF\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEF\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"X\")\n      expect(editor.plainText).toBe(\"ABCXDEF\")\n      expect(editor.logicalCursor.col).toBe(4)\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"ABCDEF\")\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should type correctly after backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"h\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"o\")\n\n      expect(editor.plainText).toBe(\"hello\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"hell\")\n\n      currentMockInput.pressKey(\"p\")\n      expect(editor.plainText).toBe(\"hellp\")\n\n      currentMockInput.pressKey(\"!\")\n      expect(editor.plainText).toBe(\"hellp!\")\n    })\n\n    it(\"should type correctly after multiple backspaces\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"t\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"s\")\n      currentMockInput.pressKey(\"t\")\n      currentMockInput.pressKey(\"i\")\n      currentMockInput.pressKey(\"n\")\n      currentMockInput.pressKey(\"g\")\n\n      expect(editor.plainText).toBe(\"testing\")\n\n      currentMockInput.pressBackspace()\n      currentMockInput.pressBackspace()\n      currentMockInput.pressBackspace()\n\n      expect(editor.plainText).toBe(\"test\")\n\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"d\")\n\n      expect(editor.plainText).toBe(\"tested\")\n    })\n\n    it(\"should type correctly after backspacing all text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"w\")\n      currentMockInput.pressKey(\"r\")\n      currentMockInput.pressKey(\"o\")\n      currentMockInput.pressKey(\"n\")\n      currentMockInput.pressKey(\"g\")\n\n      expect(editor.plainText).toBe(\"wrong\")\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressBackspace()\n      }\n\n      expect(editor.plainText).toBe(\"\")\n\n      currentMockInput.pressKey(\"r\")\n      currentMockInput.pressKey(\"i\")\n      currentMockInput.pressKey(\"g\")\n      currentMockInput.pressKey(\"h\")\n      currentMockInput.pressKey(\"t\")\n\n      expect(editor.plainText).toBe(\"right\")\n    })\n  })\n\n  describe(\"Deletion with empty lines\", () => {\n    it(\"should delete selection on line after empty lines correctly\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAA\\n\\nBBBB\\n\\nCCCC\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n      editor.gotoLine(2) // Line with \"BBBB\"\n\n      expect(editor.logicalCursor.row).toBe(2)\n      expect(editor.plainText).toBe(\"AAAA\\n\\nBBBB\\n\\nCCCC\")\n\n      // Select \"BBBB\" by pressing shift+right 4 times\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"BBBB\")\n\n      // Delete the selection\n      currentMockInput.pressKey(\"DELETE\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"AAAA\\n\\n\\n\\nCCCC\")\n      expect(editor.logicalCursor.row).toBe(2)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should delete selection on first line correctly (baseline test)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAA\\n\\nBBBB\\n\\nCCCC\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n      editor.gotoLine(0) // First line with \"AAAA\"\n\n      expect(editor.logicalCursor.row).toBe(0)\n\n      // Select \"AAAA\"\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.getSelectedText()).toBe(\"AAAA\")\n\n      // Delete the selection\n      currentMockInput.pressKey(\"DELETE\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"\\n\\nBBBB\\n\\nCCCC\")\n    })\n\n    it(\"should delete selection on last line after empty lines correctly\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAA\\n\\nBBBB\\n\\nCCCC\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n      editor.gotoLine(4) // Last line with \"CCCC\"\n\n      expect(editor.logicalCursor.row).toBe(4)\n\n      // Select \"CCCC\"\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      const selectedText = editor.getSelectedText()\n      expect(selectedText).toBe(\"CCCC\")\n\n      // Delete the selection\n      currentMockInput.pressKey(\"DELETE\")\n\n      expect(editor.hasSelection()).toBe(false)\n      // After deleting CCCC, we should still have AAAA and BBBB\n      expect(editor.plainText).toContain(\"AAAA\")\n      expect(editor.plainText).toContain(\"BBBB\")\n      expect(editor.plainText).not.toContain(\"CCCC\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.error-handling.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\n\ndescribe(\"Textarea - Error Handling Tests\", () => {\n  beforeEach(async () => {\n    ;({ renderer: currentRenderer, renderOnce } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Error Handling\", () => {\n    it(\"should throw error when using destroyed editor\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.destroy()\n\n      expect(() => editor.plainText).toThrow(\"EditBuffer is destroyed\")\n      expect(() => editor.insertText(\"x\")).toThrow(\"EditorView is destroyed\")\n      expect(() => editor.moveCursorLeft()).toThrow(\"EditorView is destroyed\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.events.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockMouse, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Event Handlers Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Change Events\", () => {\n    describe(\"onCursorChange\", () => {\n      it(\"should fire onCursorChange when cursor moves\", async () => {\n        let cursorChangeCount = 0\n        let lastCursorEvent: { line: number; visualColumn: number } | null = null\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\\nLine 3\",\n          width: 40,\n          height: 10,\n          onCursorChange: (event) => {\n            cursorChangeCount++\n            lastCursorEvent = event\n          },\n        })\n\n        editor.focus()\n        const initialCount = cursorChangeCount\n\n        editor.moveCursorRight()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(cursorChangeCount).toBeGreaterThan(initialCount)\n        expect(lastCursorEvent).not.toBe(null)\n        expect(lastCursorEvent!.line).toBe(0)\n        expect(lastCursorEvent!.visualColumn).toBe(1)\n\n        const prevCount = cursorChangeCount\n\n        editor.moveCursorDown()\n        await new Promise((resolve) => setTimeout(resolve, 20))\n\n        expect(cursorChangeCount).toBeGreaterThanOrEqual(prevCount)\n        expect(lastCursorEvent).not.toBe(null)\n        expect(lastCursorEvent!.line).toBeGreaterThanOrEqual(0)\n      })\n\n      it(\"should fire onCursorChange when typing moves cursor\", async () => {\n        let cursorChangeCount = 0\n        let lastCursorEvent: { line: number; visualColumn: number } | null = null\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onCursorChange: (event) => {\n            cursorChangeCount++\n            lastCursorEvent = event\n          },\n        })\n\n        editor.focus()\n        const initialCount = cursorChangeCount\n\n        currentMockInput.pressKey(\"H\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(cursorChangeCount).toBeGreaterThan(initialCount)\n        expect(lastCursorEvent).not.toBe(null)\n        expect(lastCursorEvent!.line).toBe(0)\n        expect(lastCursorEvent!.visualColumn).toBe(1)\n      })\n\n      it(\"should fire onCursorChange when pressing arrow keys\", async () => {\n        let cursorEventCount = 0\n        let lastCursorEvent: { line: number; visualColumn: number } | null = null\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"ABC\\nDEF\",\n          width: 40,\n          height: 10,\n          onCursorChange: (event) => {\n            cursorEventCount++\n            lastCursorEvent = event\n          },\n        })\n\n        editor.focus()\n        const initialCount = cursorEventCount\n\n        currentMockInput.pressArrow(\"right\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(cursorEventCount).toBeGreaterThan(initialCount)\n        expect(lastCursorEvent).not.toBe(null)\n        expect(lastCursorEvent!.visualColumn).toBe(1)\n\n        const beforeDown = cursorEventCount\n        currentMockInput.pressArrow(\"down\")\n        await new Promise((resolve) => setTimeout(resolve, 20))\n\n        expect(cursorEventCount).toBeGreaterThanOrEqual(beforeDown)\n        expect(lastCursorEvent).not.toBe(null)\n      })\n\n      it(\"should fire onCursorChange when using gotoLine\", async () => {\n        let cursorChangeCount = 0\n        let lastCursorEvent: { line: number; visualColumn: number } | null = null\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 0\\nLine 1\\nLine 2\",\n          width: 40,\n          height: 10,\n          onCursorChange: (event) => {\n            cursorChangeCount++\n            lastCursorEvent = event\n          },\n        })\n\n        editor.focus()\n        const initialCount = cursorChangeCount\n\n        editor.gotoLine(2)\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(cursorChangeCount).toBeGreaterThan(initialCount)\n        expect(lastCursorEvent).not.toBe(null)\n        expect(lastCursorEvent!.line).toBe(2)\n        expect(lastCursorEvent!.visualColumn).toBe(0)\n      })\n\n      it(\"should fire onCursorChange after undo\", async () => {\n        let cursorChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onCursorChange: () => {\n            cursorChangeCount++\n          },\n        })\n\n        editor.focus()\n\n        currentMockInput.pressKey(\"H\")\n        currentMockInput.pressKey(\"i\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        const beforeUndo = cursorChangeCount\n\n        editor.undo()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(cursorChangeCount).toBeGreaterThan(beforeUndo)\n      })\n\n      it(\"should update event handler when set dynamically\", async () => {\n        let firstHandlerCalled = false\n        let secondHandlerCalled = false\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          onCursorChange: () => {\n            firstHandlerCalled = true\n          },\n        })\n\n        editor.focus()\n\n        editor.moveCursorRight()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(firstHandlerCalled).toBe(true)\n\n        editor.onCursorChange = () => {\n          secondHandlerCalled = true\n        }\n\n        editor.moveCursorRight()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(secondHandlerCalled).toBe(true)\n      })\n\n      it(\"should not fire when handler is undefined\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          onCursorChange: undefined,\n        })\n\n        editor.focus()\n\n        editor.moveCursorRight()\n        expect(editor.logicalCursor.col).toBe(1)\n      })\n    })\n\n    describe(\"onContentChange\", () => {\n      it(\"should fire onContentChange when typing\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n        const initialCount = contentChangeCount\n\n        currentMockInput.pressKey(\"H\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(contentChangeCount).toBeGreaterThan(initialCount)\n        expect(editor.plainText).toBe(\"H\")\n      })\n\n      it(\"should fire onContentChange when deleting\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello\",\n          width: 40,\n          height: 10,\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n        editor.gotoLine(9999)\n        const initialCount = contentChangeCount\n\n        currentMockInput.pressBackspace()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(contentChangeCount).toBeGreaterThan(initialCount)\n        expect(editor.plainText).toBe(\"Hell\")\n      })\n\n      it(\"should fire onContentChange when inserting newline\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n        editor.gotoLine(9999)\n        const initialCount = contentChangeCount\n\n        currentMockInput.pressEnter()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(contentChangeCount).toBeGreaterThan(initialCount)\n        expect(editor.plainText).toBe(\"Test\\n\")\n      })\n\n      it(\"should fire onContentChange when pasting\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello\",\n          width: 40,\n          height: 10,\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n        editor.gotoLine(9999)\n\n        const initialCount = contentChangeCount\n\n        await currentMockInput.pasteBracketedText(\" World\")\n\n        expect(contentChangeCount).toBeGreaterThan(initialCount)\n        expect(editor.plainText).toBe(\"Hello World\")\n      })\n\n      it(\"should fire onContentChange after undo\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n\n        currentMockInput.pressKey(\"T\")\n        currentMockInput.pressKey(\"e\")\n        await new Promise((resolve) => setTimeout(resolve, 20))\n\n        const beforeUndo = contentChangeCount\n\n        editor.undo()\n        await new Promise((resolve) => setTimeout(resolve, 20))\n\n        expect(contentChangeCount).toBeGreaterThanOrEqual(beforeUndo)\n        expect(editor.plainText).toBe(\"T\")\n      })\n\n      it(\"should fire onContentChange after redo\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n\n        currentMockInput.pressKey(\"X\")\n        await new Promise((resolve) => setTimeout(resolve, 20))\n        editor.undo()\n        await new Promise((resolve) => setTimeout(resolve, 20))\n\n        const beforeRedo = contentChangeCount\n\n        editor.redo()\n        await new Promise((resolve) => setTimeout(resolve, 20))\n\n        expect(contentChangeCount).toBeGreaterThanOrEqual(beforeRedo)\n        expect(editor.plainText).toBe(\"X\")\n      })\n\n      it(\"should fire onContentChange when setting value programmatically\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Initial\",\n          width: 40,\n          height: 10,\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        const initialCount = contentChangeCount\n\n        editor.setText(\"Updated\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(contentChangeCount).toBeGreaterThan(initialCount)\n        expect(editor.plainText).toBe(\"Updated\")\n      })\n\n      it(\"should fire onContentChange when deleting selection\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello World\",\n          width: 40,\n          height: 10,\n          selectable: true,\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n\n        for (let i = 0; i < 5; i++) {\n          currentMockInput.pressArrow(\"right\", { shift: true })\n        }\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        const beforeDelete = contentChangeCount\n\n        currentMockInput.pressBackspace()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(contentChangeCount).toBeGreaterThan(beforeDelete)\n        expect(editor.plainText).toBe(\" World\")\n      })\n\n      it(\"should update event handler when set dynamically\", async () => {\n        let firstHandlerCalled = false\n        let secondHandlerCalled = false\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onContentChange: () => {\n            firstHandlerCalled = true\n          },\n        })\n\n        editor.focus()\n\n        currentMockInput.pressKey(\"A\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(firstHandlerCalled).toBe(true)\n\n        editor.onContentChange = () => {\n          secondHandlerCalled = true\n        }\n\n        currentMockInput.pressKey(\"B\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(secondHandlerCalled).toBe(true)\n      })\n\n      it(\"should not fire when handler is undefined\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onContentChange: undefined,\n        })\n\n        editor.focus()\n\n        currentMockInput.pressKey(\"X\")\n        expect(editor.plainText).toBe(\"X\")\n      })\n\n      it(\"should fire exactly once when setting via setter and pressing a key\", async () => {\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n        })\n\n        editor.focus()\n\n        editor.onContentChange = () => {\n          contentChangeCount++\n        }\n\n        currentMockInput.pressKey(\"X\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(contentChangeCount).toBe(1)\n        expect(editor.plainText).toBe(\"X\")\n      })\n    })\n\n    describe(\"onSubmit\", () => {\n      it(\"should fire onSubmit with default keybinding (Meta+Enter)\", async () => {\n        let submitCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test content\",\n          width: 40,\n          height: 10,\n          onSubmit: () => {\n            submitCount++\n          },\n        })\n\n        editor.focus()\n        const initialCount = submitCount\n\n        currentMockInput.pressEnter({ meta: true })\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(submitCount).toBe(initialCount + 1)\n        expect(editor.plainText).toBe(\"Test content\")\n      })\n\n      it(\"should fire onSubmit with alternative keybinding (Meta+Return)\", async () => {\n        let submitCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          onSubmit: () => {\n            submitCount++\n          },\n        })\n\n        editor.focus()\n\n        currentMockInput.pressEnter({ meta: true })\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(submitCount).toBe(1)\n      })\n\n      it(\"should not insert newline when submitting\", async () => {\n        let submitCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          onSubmit: () => {\n            submitCount++\n          },\n        })\n\n        editor.focus()\n        editor.gotoLine(9999)\n\n        currentMockInput.pressEnter({ meta: true })\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(submitCount).toBe(1)\n        expect(editor.plainText).toBe(\"Test\")\n      })\n\n      it(\"should update handler via setter\", async () => {\n        let firstHandlerCalled = false\n        let secondHandlerCalled = false\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onSubmit: () => {\n            firstHandlerCalled = true\n          },\n        })\n\n        editor.focus()\n\n        currentMockInput.pressEnter({ meta: true })\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(firstHandlerCalled).toBe(true)\n\n        editor.onSubmit = () => {\n          secondHandlerCalled = true\n        }\n\n        currentMockInput.pressEnter({ meta: true })\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(secondHandlerCalled).toBe(true)\n      })\n\n      it(\"should not fire when handler is undefined\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          onSubmit: undefined,\n        })\n\n        editor.focus()\n\n        currentMockInput.pressEnter({ meta: true })\n        expect(editor.plainText).toBe(\"Test\")\n      })\n\n      it(\"should support custom keybinding for submit\", async () => {\n        let submitCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          keyBindings: [{ name: \"s\", ctrl: true, action: \"submit\" }],\n          onSubmit: () => {\n            submitCount++\n          },\n        })\n\n        editor.focus()\n\n        currentMockInput.pressKey(\"s\", { ctrl: true })\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(submitCount).toBe(1)\n      })\n\n      it(\"should get current handler via getter\", async () => {\n        const handler = () => {}\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onSubmit: handler,\n        })\n\n        expect(editor.onSubmit).toBe(handler)\n      })\n\n      it(\"should allow removing handler by setting to undefined\", async () => {\n        let submitCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onSubmit: () => {\n            submitCount++\n          },\n        })\n\n        editor.focus()\n\n        currentMockInput.pressEnter({ meta: true })\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(submitCount).toBe(1)\n\n        editor.onSubmit = undefined\n\n        currentMockInput.pressEnter({ meta: true })\n        await new Promise((resolve) => setTimeout(resolve, 10))\n        expect(submitCount).toBe(1)\n      })\n    })\n\n    describe(\"Combined cursor and content events\", () => {\n      it(\"should fire both onCursorChange and onContentChange when typing\", async () => {\n        let cursorChangeCount = 0\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onCursorChange: () => {\n            cursorChangeCount++\n          },\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n        const initialCursorCount = cursorChangeCount\n        const initialContentCount = contentChangeCount\n\n        currentMockInput.pressKey(\"H\")\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(cursorChangeCount).toBeGreaterThan(initialCursorCount)\n        expect(contentChangeCount).toBeGreaterThan(initialContentCount)\n      })\n\n      it(\"should fire onCursorChange but not onContentChange when only moving cursor\", async () => {\n        let cursorChangeCount = 0\n        let contentChangeCount = 0\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Test\",\n          width: 40,\n          height: 10,\n          onCursorChange: () => {\n            cursorChangeCount++\n          },\n          onContentChange: () => {\n            contentChangeCount++\n          },\n        })\n\n        editor.focus()\n        const initialCursorCount = cursorChangeCount\n        const initialContentCount = contentChangeCount\n\n        editor.moveCursorRight()\n        await new Promise((resolve) => setTimeout(resolve, 10))\n\n        expect(cursorChangeCount).toBeGreaterThan(initialCursorCount)\n        expect(contentChangeCount).toBe(initialContentCount) // Should not change\n      })\n\n      it(\"should track events through complex editing sequence\", async () => {\n        const events: Array<{ type: \"cursor\" | \"content\"; time: number }> = []\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"\",\n          width: 40,\n          height: 10,\n          onCursorChange: () => {\n            events.push({ type: \"cursor\", time: Date.now() })\n          },\n          onContentChange: () => {\n            events.push({ type: \"content\", time: Date.now() })\n          },\n        })\n\n        editor.focus()\n        events.length = 0 // Clear initial events\n\n        currentMockInput.pressKey(\"H\")\n        currentMockInput.pressKey(\"e\")\n        currentMockInput.pressKey(\"l\")\n        currentMockInput.pressKey(\"l\")\n        currentMockInput.pressKey(\"o\")\n\n        editor.moveCursorLeft()\n        editor.moveCursorLeft()\n\n        currentMockInput.pressBackspace()\n\n        await new Promise((resolve) => setTimeout(resolve, 50))\n\n        const cursorEvents = events.filter((e) => e.type === \"cursor\")\n        const contentEvents = events.filter((e) => e.type === \"content\")\n\n        expect(cursorEvents.length).toBeGreaterThan(0)\n        expect(contentEvents.length).toBeGreaterThan(0)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.highlights.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockMouse, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\nimport { OptimizedBuffer } from \"../../buffer.js\"\nimport { SyntaxStyle } from \"../../syntax-style.js\"\nimport { RGBA } from \"../../lib/index.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Syntax Highlighting Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Syntax Highlighting\", () => {\n    describe(\"SyntaxStyle Management\", () => {\n      it(\"should set syntax style via constructor option\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"keyword\", {\n          fg: RGBA.fromValues(0, 1, 0, 1),\n          bold: true,\n        })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"const x = 5\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        expect(editor.syntaxStyle).toBe(style)\n      })\n\n      it(\"should set syntax style via setter\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"test\",\n          width: 40,\n          height: 10,\n        })\n\n        expect(editor.syntaxStyle).toBe(null)\n\n        const style = SyntaxStyle.create()\n        editor.syntaxStyle = style\n\n        expect(editor.syntaxStyle).toBe(style)\n      })\n\n      it(\"should clear syntax style when set to null\", async () => {\n        const style = SyntaxStyle.create()\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"test\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        expect(editor.syntaxStyle).toBe(style)\n\n        editor.syntaxStyle = null\n\n        expect(editor.syntaxStyle).toBe(null)\n      })\n    })\n\n    describe(\"Highlight Management\", () => {\n      it(\"should add highlight by line and column range\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"highlight\", {\n          fg: RGBA.fromValues(1, 0, 0, 1),\n        })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello World\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 5, styleId: styleId, priority: 0 })\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].start).toBe(0)\n        expect(highlights[0].end).toBe(5)\n        expect(highlights[0].styleId).toBe(styleId)\n        expect(highlights[0].priority).toBe(0)\n        expect(highlights[0].hlRef).toBe(0)\n      })\n\n      it(\"should add multiple highlights to same line\", async () => {\n        const style = SyntaxStyle.create()\n        const keywordId = style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n        const stringId = style.registerStyle(\"string\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"const name = 'value'\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 5, styleId: keywordId, priority: 0 }) // \"const\"\n        editor.addHighlight(0, { start: 13, end: 20, styleId: stringId, priority: 0 }) // \"'value'\"\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(2)\n        expect(highlights[0].start).toBe(0)\n        expect(highlights[0].end).toBe(5)\n        expect(highlights[0].styleId).toBe(keywordId)\n        expect(highlights[1].start).toBe(13)\n        expect(highlights[1].end).toBe(20)\n        expect(highlights[1].styleId).toBe(stringId)\n      })\n\n      it(\"should add highlight by character range\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"highlight\", {\n          fg: RGBA.fromValues(1, 1, 0, 1),\n        })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\\nLine 3\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        // Highlight from \"ine 2\" to \"ine 3\" (char offset 7-13, newlines not counted)\n        // Char positions (excluding newlines): \"Line 1\" = 0-5, \"Line 2\" = 6-11, \"Line 3\" = 12-17\n        // Char 7 = \"i\" in \"Line 2\" (col 1), Char 13 = \"i\" in \"Line 3\" (col 1)\n        editor.addHighlightByCharRange({ start: 7, end: 13, styleId: styleId, priority: 0 })\n\n        const highlights = editor.getLineHighlights(1)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].start).toBe(1) // Second character \"i\" in \"Line 2\"\n        expect(highlights[0].end).toBe(6) // End of \"Line 2\"\n        expect(highlights[0].styleId).toBe(styleId)\n      })\n\n      it(\"should add highlight with custom priority\", async () => {\n        const style = SyntaxStyle.create()\n        const lowPriorityId = style.registerStyle(\"low\", { fg: RGBA.fromValues(0.5, 0.5, 0.5, 1) })\n        const highPriorityId = style.registerStyle(\"high\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"overlapping\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 10, styleId: lowPriorityId, priority: 1 })\n        editor.addHighlight(0, { start: 3, end: 8, styleId: highPriorityId, priority: 10 })\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(2)\n        expect(highlights[0].priority).toBe(1)\n        expect(highlights[1].priority).toBe(10)\n      })\n\n      it(\"should add highlight with reference ID\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"ref-highlight\", {\n          fg: RGBA.fromValues(0, 0, 1, 1),\n        })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"test content\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        const refId = 42\n        editor.addHighlight(0, { start: 0, end: 4, styleId: styleId, priority: 0, hlRef: refId })\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].hlRef).toBe(refId)\n      })\n\n      it(\"should remove highlights by reference ID\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId1 = style.registerStyle(\"style1\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n        const styleId2 = style.registerStyle(\"style2\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"test content here\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 4, styleId: styleId1, priority: 0, hlRef: 1 })\n        editor.addHighlight(0, { start: 5, end: 12, styleId: styleId2, priority: 0, hlRef: 2 })\n        editor.addHighlight(0, { start: 13, end: 17, styleId: styleId1, priority: 0, hlRef: 1 })\n\n        let highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(3)\n\n        editor.removeHighlightsByRef(1)\n\n        highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].start).toBe(5)\n        expect(highlights[0].hlRef).toBe(2)\n      })\n\n      it(\"should clear highlights for specific line\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"style\", { fg: RGBA.fromValues(1, 1, 1, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\\nLine 3\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 6, styleId: styleId, priority: 0 })\n        editor.addHighlight(1, { start: 0, end: 6, styleId: styleId, priority: 0 })\n        editor.addHighlight(2, { start: 0, end: 6, styleId: styleId, priority: 0 })\n\n        expect(editor.getLineHighlights(0).length).toBe(1)\n        expect(editor.getLineHighlights(1).length).toBe(1)\n        expect(editor.getLineHighlights(2).length).toBe(1)\n\n        editor.clearLineHighlights(1)\n\n        expect(editor.getLineHighlights(0).length).toBe(1)\n        expect(editor.getLineHighlights(1).length).toBe(0)\n        expect(editor.getLineHighlights(2).length).toBe(1)\n      })\n\n      it(\"should clear all highlights\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"style\", { fg: RGBA.fromValues(1, 1, 1, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\\nLine 3\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 6, styleId: styleId, priority: 0 })\n        editor.addHighlight(1, { start: 0, end: 6, styleId: styleId, priority: 0 })\n        editor.addHighlight(2, { start: 0, end: 6, styleId: styleId, priority: 0 })\n\n        expect(editor.getLineHighlights(0).length).toBe(1)\n        expect(editor.getLineHighlights(1).length).toBe(1)\n        expect(editor.getLineHighlights(2).length).toBe(1)\n\n        editor.clearAllHighlights()\n\n        expect(editor.getLineHighlights(0).length).toBe(0)\n        expect(editor.getLineHighlights(1).length).toBe(0)\n        expect(editor.getLineHighlights(2).length).toBe(0)\n      })\n\n      it(\"should return empty array for line with no highlights\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\",\n          width: 40,\n          height: 10,\n        })\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights).toEqual([])\n      })\n\n      it(\"should return empty array for line index out of bounds\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Single line\",\n          width: 40,\n          height: 10,\n        })\n\n        const highlights = editor.getLineHighlights(999)\n        expect(highlights).toEqual([])\n      })\n\n      it(\"should handle highlights spanning multiple lines via character range\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"multiline\", {\n          bg: RGBA.fromValues(0.2, 0.2, 0.2, 1),\n        })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"AAAA\\nBBBB\\nCCCC\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        // Highlight from middle of line 0 to all of line 2 (chars 2-12, newlines not counted)\n        // Char positions (excluding newlines): \"AAAA\" = 0-3, \"BBBB\" = 4-7, \"CCCC\" = 8-11\n        // Char 2 = third \"A\", Char 12 = one past end\n        editor.addHighlightByCharRange({ start: 2, end: 12, styleId: styleId, priority: 0 })\n\n        const hl0 = editor.getLineHighlights(0)\n        const hl1 = editor.getLineHighlights(1)\n        const hl2 = editor.getLineHighlights(2)\n\n        expect(hl0.length).toBe(1)\n        expect(hl0[0].start).toBe(2)\n        expect(hl0[0].end).toBe(4)\n\n        expect(hl1.length).toBe(1)\n        expect(hl1[0].start).toBe(0)\n        expect(hl1[0].end).toBe(4)\n\n        expect(hl2.length).toBe(1)\n        expect(hl2[0].start).toBe(0)\n        expect(hl2[0].end).toBe(4) // All of \"CCCC\"\n      })\n\n      it(\"should preserve highlights after text editing when using hlRef\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"persistent\", {\n          fg: RGBA.fromValues(1, 0, 1, 1),\n        })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Hello World\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 5, styleId: styleId, priority: 0, hlRef: 100 })\n\n        let highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].hlRef).toBe(100)\n\n        // Edit text\n        editor.focus()\n        editor.gotoLine(9999)\n        currentMockInput.pressKey(\"!\")\n\n        // Highlight should still exist (this is line-based, not offset-based)\n        highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].hlRef).toBe(100)\n      })\n\n      it(\"should handle multiple highlights with different priorities\", async () => {\n        const style = SyntaxStyle.create()\n        const baseId = style.registerStyle(\"base\", { fg: RGBA.fromValues(0.5, 0.5, 0.5, 1) })\n        const mediumId = style.registerStyle(\"medium\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n        const highId = style.registerStyle(\"high\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"overlapping text\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 15, styleId: baseId, priority: 0 })\n        editor.addHighlight(0, { start: 3, end: 12, styleId: mediumId, priority: 5 })\n        editor.addHighlight(0, { start: 6, end: 9, styleId: highId, priority: 10 })\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(3)\n\n        const sorted = [...highlights].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))\n        expect(sorted[0].priority).toBe(0)\n        expect(sorted[1].priority).toBe(5)\n        expect(sorted[2].priority).toBe(10)\n      })\n\n      it(\"should clear highlights when removing by ref across multiple lines\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"temp\", { bg: RGBA.fromValues(0.1, 0.1, 0.1, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\\nLine 3\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        const refId = 555\n        editor.addHighlight(0, { start: 0, end: 6, styleId: styleId, priority: 0, hlRef: refId })\n        editor.addHighlight(1, { start: 0, end: 6, styleId: styleId, priority: 0, hlRef: refId })\n        editor.addHighlight(2, { start: 0, end: 6, styleId: styleId, priority: 0, hlRef: 999 }) // Different ref\n\n        expect(editor.getLineHighlights(0).length).toBe(1)\n        expect(editor.getLineHighlights(1).length).toBe(1)\n        expect(editor.getLineHighlights(2).length).toBe(1)\n\n        editor.removeHighlightsByRef(refId)\n\n        expect(editor.getLineHighlights(0).length).toBe(0)\n        expect(editor.getLineHighlights(1).length).toBe(0)\n        expect(editor.getLineHighlights(2).length).toBe(1)\n        expect(editor.getLineHighlights(2)[0].hlRef).toBe(999)\n      })\n\n      it(\"should handle empty highlights without hlRef\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"no-ref\", { fg: RGBA.fromValues(1, 1, 1, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"test\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 4, styleId: styleId, priority: 0 })\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].hlRef).toBe(0)\n      })\n\n      it(\"should work without syntax style set\", async () => {\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"test\",\n          width: 40,\n          height: 10,\n        })\n\n        // Can still add highlights even without syntax style (just need style IDs)\n        editor.addHighlight(0, { start: 0, end: 4, styleId: 999, priority: 0 })\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].styleId).toBe(999)\n      })\n\n      it(\"should handle char range spanning entire buffer\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"all\", { bg: RGBA.fromValues(0.1, 0.1, 0.1, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"AAA\\nBBB\\nCCC\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        // Highlight entire content (0 to end)\n        editor.addHighlightByCharRange({ start: 0, end: 11, styleId: styleId, priority: 0 })\n\n        expect(editor.getLineHighlights(0).length).toBeGreaterThan(0)\n        expect(editor.getLineHighlights(1).length).toBeGreaterThan(0)\n        expect(editor.getLineHighlights(2).length).toBeGreaterThan(0)\n      })\n\n      it(\"should handle updating highlights after clearing specific line\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"test\", { fg: RGBA.fromValues(1, 1, 0, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"Line 1\\nLine 2\\nLine 3\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 6, styleId: styleId, priority: 0 })\n        editor.addHighlight(1, { start: 0, end: 6, styleId: styleId, priority: 0 })\n        editor.addHighlight(2, { start: 0, end: 6, styleId: styleId, priority: 0 })\n\n        editor.clearLineHighlights(1)\n\n        // Re-add highlight on line 1\n        editor.addHighlight(1, { start: 2, end: 5, styleId: styleId, priority: 0 })\n\n        const highlights = editor.getLineHighlights(1)\n        expect(highlights.length).toBe(1)\n        expect(highlights[0].start).toBe(2)\n        expect(highlights[0].end).toBe(5)\n      })\n\n      it(\"should handle zero-width highlights (should be ignored)\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"zero\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"test\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        // Add zero-width highlight (start == end)\n        editor.addHighlight(0, { start: 2, end: 2, styleId: styleId, priority: 0 })\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(0) // Should be ignored\n      })\n\n      it(\"should handle multiple reference IDs independently\", async () => {\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"ref-style\", { fg: RGBA.fromValues(1, 1, 1, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"test content for multiple refs\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 4, styleId: styleId, priority: 0, hlRef: 10 })\n        editor.addHighlight(0, { start: 5, end: 12, styleId: styleId, priority: 0, hlRef: 20 })\n        editor.addHighlight(0, { start: 13, end: 16, styleId: styleId, priority: 0, hlRef: 30 })\n\n        let highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(3)\n\n        editor.removeHighlightsByRef(20)\n\n        highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(2)\n        expect(highlights.filter((h) => h.hlRef === 10).length).toBe(1)\n        expect(highlights.filter((h) => h.hlRef === 30).length).toBe(1)\n      })\n    })\n\n    describe(\"Highlight Rendering Integration\", () => {\n      it(\"should render highlighted text without crashing\", async () => {\n        const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n        const style = SyntaxStyle.create()\n        const styleId = style.registerStyle(\"keyword\", {\n          fg: RGBA.fromValues(1, 0, 0, 1),\n          bold: true,\n        })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"const x = 5\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 5, styleId: styleId, priority: 0 })\n\n        // Should render without crashing\n        buffer.drawEditorView(editor.editorView, 0, 0)\n\n        expect(editor.getLineHighlights(0).length).toBe(1)\n      })\n\n      it(\"should handle highlights with overlapping ranges\", async () => {\n        const style = SyntaxStyle.create()\n        const style1 = style.registerStyle(\"style1\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n        const style2 = style.registerStyle(\"style2\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n\n        const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n          initialValue: \"overlapping\",\n          width: 40,\n          height: 10,\n          syntaxStyle: style,\n        })\n\n        editor.addHighlight(0, { start: 0, end: 8, styleId: style1, priority: 0 })\n        editor.addHighlight(0, { start: 4, end: 11, styleId: style2, priority: 5 }) // Higher priority\n\n        const highlights = editor.getLineHighlights(0)\n        expect(highlights.length).toBe(2)\n\n        const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n        // Should render without crashing\n        buffer.drawEditorView(editor.editorView, 0, 0)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.keybinding.test.ts",
    "content": "import { Buffer } from \"node:buffer\"\nimport { describe, expect, it, afterAll, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockMouse, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\nimport { KeyEvent } from \"../../lib/KeyHandler.js\"\n\n// Helper function to create a KeyEvent from a string\nfunction createKeyEvent(\n  input: string | { name: string; shift?: boolean; ctrl?: boolean; meta?: boolean; super?: boolean },\n): KeyEvent {\n  if (typeof input === \"string\") {\n    return new KeyEvent({\n      name: input,\n      sequence: input,\n      ctrl: false,\n      meta: false,\n      shift: false,\n      option: false,\n      number: false,\n      raw: input,\n      eventType: \"press\",\n      source: \"raw\",\n    })\n  } else {\n    return new KeyEvent({\n      name: input.name,\n      sequence: input.name === \"space\" ? \" \" : input.name,\n      ctrl: input.ctrl ?? false,\n      meta: input.meta ?? false,\n      shift: input.shift ?? false,\n      super: input.super ?? false,\n      option: false,\n      number: false,\n      raw: input.name,\n      eventType: \"press\",\n      source: \"raw\",\n    })\n  }\n}\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMouse: MockMouse\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Keybinding Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockMouse: currentMouse,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Keyboard Input - Meta Key Bindings\", () => {\n    it(\"should bind custom action to meta key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"b\", meta: true, action: \"buffer-home\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n\n      const cursor = editor.logicalCursor\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"should bind meta key actions\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"f\", meta: true, action: \"buffer-end\" }],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n\n      const cursor = editor.logicalCursor\n      expect(cursor.row).toBe(0)\n    })\n\n    it(\"should work with meta key for navigation\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"j\", meta: true, action: \"move-down\" }],\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.row).toBe(0)\n\n      currentMockInput.pressKey(\"j\", { meta: true })\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should allow meta key binding override\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"k\", meta: true, action: \"move-up\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(2)\n      expect(editor.logicalCursor.row).toBe(2)\n\n      currentMockInput.pressKey(\"k\", { meta: true })\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should work with Meta+Arrow keys\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABC\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"left\", meta: true, action: \"line-home\" },\n          { name: \"right\", meta: true, action: \"line-end\" },\n        ],\n      })\n\n      editor.focus()\n      for (let i = 0; i < 2; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressArrow(\"left\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressArrow(\"right\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should support meta with shift modifier\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"H\", meta: true, shift: true, action: \"line-home\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n      expect(editor.logicalCursor.col).toBe(11)\n\n      currentMockInput.pressKey(\"h\", { meta: true, shift: true })\n\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should not trigger action without meta when meta binding exists\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"x\", meta: true, action: \"delete-line\" }],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"x\")\n      expect(editor.plainText).toBe(\"xTest\")\n\n      currentMockInput.pressKey(\"x\", { meta: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should update keyBindings dynamically with setter\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      editor.keyBindings = [{ name: \"b\", meta: true, action: \"buffer-end\" }]\n\n      editor.gotoLine(0)\n      expect(editor.logicalCursor.row).toBe(0)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.row).toBe(0)\n    })\n\n    it(\"should merge new keyBindings with defaults\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      editor.keyBindings = [{ name: \"d\", meta: true, action: \"delete-line\" }]\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText).toBe(\"Line 2\")\n    })\n\n    it(\"should override default keyBindings with new bindings\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      editor.keyBindings = [{ name: \"f\", meta: true, action: \"buffer-end\" }]\n\n      editor.gotoLine(0)\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.row).toBe(0)\n    })\n\n    it(\"should override return/enter keys to swap newline and submit actions\", async () => {\n      let submitCalled = false\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\",\n        width: 40,\n        height: 10,\n        onSubmit: () => {\n          submitCalled = true\n        },\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Line 1\\n\")\n      expect(submitCalled).toBe(false)\n\n      currentMockInput.pressEnter({ meta: true })\n      expect(submitCalled).toBe(true)\n      submitCalled = false\n\n      editor.keyBindings = [\n        { name: \"return\", meta: true, action: \"newline\" },\n        { name: \"linefeed\", meta: true, action: \"newline\" },\n        { name: \"return\", action: \"submit\" },\n        { name: \"linefeed\", action: \"submit\" },\n      ]\n\n      currentMockInput.pressEnter()\n      expect(submitCalled).toBe(true)\n      submitCalled = false\n\n      currentMockInput.pressEnter({ meta: true })\n      expect(editor.plainText).toBe(\"Line 1\\n\\n\")\n      expect(submitCalled).toBe(false)\n    })\n  })\n\n  describe(\"Key Event Handling - Modifier Keys\", () => {\n    let kittyRenderer: TestRenderer\n    let kittyRenderOnce: () => Promise<void>\n    let kittyMockInput: MockInput\n\n    beforeEach(async () => {\n      ;({\n        renderer: kittyRenderer,\n        renderOnce: kittyRenderOnce,\n        mockInput: kittyMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        kittyKeyboard: true,\n      }))\n    })\n\n    afterEach(() => {\n      kittyRenderer.destroy()\n    })\n\n    it(\"should not insert text when ctrl modifier is pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Try to type 'a' with ctrl - should not insert\n      kittyMockInput.pressKey(\"a\", { ctrl: true })\n      expect(editor.plainText).toBe(\"\")\n\n      // Try to type 'x' with ctrl - should not insert\n      kittyMockInput.pressKey(\"x\", { ctrl: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should not insert text when meta modifier is pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Try to type 'a' with meta - should not insert\n      kittyMockInput.pressKey(\"a\", { meta: true })\n      expect(editor.plainText).toBe(\"\")\n\n      // Try to type 'x' with meta - should not insert\n      kittyMockInput.pressKey(\"x\", { meta: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should not insert text when super modifier is pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Try to type 'a' with super - should not insert\n      kittyMockInput.pressKey(\"a\", { super: true })\n      expect(editor.plainText).toBe(\"\")\n\n      // Try to type 'x' with super - should not insert\n      kittyMockInput.pressKey(\"x\", { super: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should not insert text when hyper modifier is pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Try to type 'a' with hyper - should not insert\n      kittyMockInput.pressKey(\"a\", { hyper: true })\n      expect(editor.plainText).toBe(\"\")\n\n      // Try to type 'x' with hyper - should not insert\n      kittyMockInput.pressKey(\"x\", { hyper: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should not insert text when multiple modifiers are pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Try to type with ctrl+meta - should not insert\n      kittyMockInput.pressKey(\"a\", { ctrl: true, meta: true })\n      expect(editor.plainText).toBe(\"\")\n\n      // Try to type with ctrl+super - should not insert\n      kittyMockInput.pressKey(\"b\", { ctrl: true, super: true })\n      expect(editor.plainText).toBe(\"\")\n\n      // Try to type with meta+hyper - should not insert\n      kittyMockInput.pressKey(\"c\", { meta: true, hyper: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should insert text when only shift modifier is pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Shift is okay for uppercase letters\n      kittyMockInput.pressKey(\"A\", { shift: true })\n      expect(editor.plainText).toBe(\"A\")\n\n      kittyMockInput.pressKey(\"B\", { shift: true })\n      expect(editor.plainText).toBe(\"AB\")\n    })\n\n    it(\"should not insert text when Caps Lock key is pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      kittyRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[57358u\"))\n      expect(editor.plainText).toBe(\"\")\n    })\n  })\n\n  describe(\"Key Event Handling\", () => {\n    it(\"should only handle KeyEvents, not raw escape sequences\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      const rawEscapeSequence = \"\\x1b[<35;86;19M\"\n      const handled = editor.handleKeyPress(createKeyEvent(rawEscapeSequence))\n\n      expect(handled).toBe(false)\n\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should not insert control sequences into text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Try various control sequences that should NOT be inserted\n      const controlSequences = [\n        \"\\x1b[A\", // Arrow up\n        \"\\x1b[B\", // Arrow down\n        \"\\x1b[C\", // Arrow right\n        \"\\x1b[D\", // Arrow left\n        \"\\x1b[?1004h\", // Focus tracking\n        \"\\x1b[?2004h\", // Bracketed paste\n        \"\\x1b[<0;10;10M\", // Mouse event\n      ]\n\n      for (const seq of controlSequences) {\n        const before = editor.plainText\n        editor.handleKeyPress(createKeyEvent(seq))\n        const after = editor.plainText\n\n        // Content should not change for control sequences\n        expect(after).toBe(before)\n      }\n    })\n\n    it(\"should handle printable characters via handleKeyPress\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // These should be handled\n      const handled1 = editor.handleKeyPress(createKeyEvent(\"a\"))\n      expect(handled1).toBe(true)\n      expect(editor.plainText).toBe(\"a\")\n\n      const handled2 = editor.handleKeyPress(createKeyEvent(\"b\"))\n      expect(handled2).toBe(true)\n      expect(editor.plainText).toBe(\"ab\")\n    })\n\n    it(\"should handle multi-byte Unicode characters (emoji, CJK)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Emoji (multi-byte UTF-8)\n      const emojiHandled = editor.handleKeyPress(createKeyEvent(\"🌟\"))\n      expect(emojiHandled).toBe(true)\n      expect(editor.plainText).toBe(\"🌟\")\n\n      // CJK characters (multi-byte UTF-8)\n      const cjkHandled = editor.handleKeyPress(createKeyEvent(\"世\"))\n      expect(cjkHandled).toBe(true)\n      expect(editor.plainText).toBe(\"🌟世\")\n\n      // Another emoji\n      editor.insertText(\" \")\n      const emoji2Handled = editor.handleKeyPress(createKeyEvent(\"👍\"))\n      expect(emoji2Handled).toBe(true)\n      expect(editor.plainText).toBe(\"🌟世 👍\")\n    })\n\n    it(\"should filter escape sequences when they have non-printable characters\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      // Escape character (0x1b) - should not be inserted\n      const escapeChar = String.fromCharCode(0x1b)\n      const handled = editor.handleKeyPress(createKeyEvent(escapeChar))\n\n      // Should not insert escape character\n      expect(editor.plainText).toBe(\"Test\")\n    })\n  })\n\n  describe(\"Key Bindings\", () => {\n    it(\"should use default keybindings\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressKey(\"HOME\")\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"END\")\n      expect(editor.logicalCursor.col).toBe(11)\n    })\n\n    it(\"should allow custom keybindings to override defaults\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"j\", action: \"move-left\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n      expect(editor.logicalCursor.col).toBe(11)\n\n      currentMockInput.pressKey(\"j\")\n      expect(editor.logicalCursor.col).toBe(10)\n    })\n\n    it(\"should map multiple custom keys to the same action\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"h\", action: \"move-left\" },\n          { name: \"j\", action: \"move-down\" },\n          { name: \"k\", action: \"move-up\" },\n          { name: \"l\", action: \"move-right\" },\n        ],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"l\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressKey(\"l\")\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressKey(\"h\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressKey(\"h\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should support custom keybindings with ctrl modifier\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"g\", ctrl: true, action: \"buffer-home\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n      expect(editor.logicalCursor.row).toBe(2)\n\n      currentMockInput.pressKey(\"g\", { ctrl: true })\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should support custom keybindings with shift modifier\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        keyBindings: [{ name: \"l\", shift: true, action: \"select-right\" }],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"L\", { shift: true })\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"H\")\n\n      currentMockInput.pressKey(\"L\", { shift: true })\n      expect(editor.getSelectedText()).toBe(\"He\")\n    })\n\n    it(\"should support custom keybindings with alt modifier\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"b\", ctrl: true, action: \"buffer-home\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(2)\n\n      currentMockInput.pressKey(\"b\", { ctrl: true })\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should support keybindings with multiple modifiers\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        keyBindings: [{ name: \"right\", ctrl: true, shift: true, action: \"select-line-end\" }],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true, shift: true })\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello World\")\n    })\n\n    it(\"should map newline action to custom key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"n\", ctrl: true, action: \"newline\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressKey(\"n\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Hello\\n\")\n    })\n\n    it(\"should map backspace action to custom key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"h\", ctrl: true, action: \"backspace\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressKey(\"h\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Hell\")\n    })\n\n    it(\"should map delete action to custom key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"d\", ctrl: false, action: \"delete\" }],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"d\")\n      expect(editor.plainText).toBe(\"ello\")\n    })\n\n    it(\"should map line-home and line-end to custom keys\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"a\", action: \"line-home\" },\n          { name: \"e\", action: \"line-end\" },\n        ],\n      })\n\n      editor.focus()\n      editor.moveCursorRight()\n      editor.moveCursorRight()\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressKey(\"a\")\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"e\")\n      expect(editor.logicalCursor.col).toBe(11)\n    })\n\n    it(\"should override default shift+home and shift+end keybindings\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        keyBindings: [\n          { name: \"home\", shift: true, action: \"buffer-home\" },\n          { name: \"end\", shift: true, action: \"buffer-end\" },\n        ],\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"HOME\", { shift: true })\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      editor.moveCursorRight()\n      currentMockInput.pressKey(\"END\", { shift: true })\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.logicalCursor.row).toBe(0)\n    })\n\n    it(\"should map undo and redo actions to custom keys\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"u\", action: \"undo\" },\n          { name: \"r\", action: \"redo\" },\n        ],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"H\")\n      currentMockInput.pressKey(\"i\")\n      expect(editor.plainText).toBe(\"Hi\")\n\n      currentMockInput.pressKey(\"u\")\n      expect(editor.plainText).toBe(\"H\")\n\n      currentMockInput.pressKey(\"r\")\n      expect(editor.plainText).toBe(\"Hi\")\n    })\n\n    it(\"should map delete-line action to custom key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"x\", ctrl: true, action: \"delete-line\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      currentMockInput.pressKey(\"x\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1\\nLine 3\")\n    })\n\n    it(\"should map delete-to-line-end action to custom key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"k\", action: \"delete-to-line-end\" }],\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\")\n      expect(editor.plainText).toBe(\"Hello \")\n    })\n\n    it(\"should delete from cursor to line start with ctrl+u\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n      expect(editor.plainText).toBe(\"World\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should map delete-to-line-start action to custom key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"x\", ctrl: true, action: \"delete-to-line-start\" }],\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"x\", { ctrl: true })\n      expect(editor.plainText).toBe(\"World\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should delete from cursor to line end with ctrl+k in multiline text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1 content\\nLine 2 content\\nLine 3 content\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 7; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1 \\nLine 2 content\\nLine 3 content\")\n      expect(editor.logicalCursor.col).toBe(7)\n      expect(editor.logicalCursor.row).toBe(0)\n    })\n\n    it(\"should delete from cursor to line end with ctrl+k on line 2\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1 content\\nLine 2 content\\nLine 3 content\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n      for (let i = 0; i < 7; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1 content\\nLine 2 \\nLine 3 content\")\n      expect(editor.logicalCursor.col).toBe(7)\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should delete from start to cursor with ctrl+u in multiline text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1 content\\nLine 2 content\\nLine 3 content\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 7; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n      expect(editor.plainText).toBe(\"content\\nLine 2 content\\nLine 3 content\")\n      expect(editor.logicalCursor.col).toBe(0)\n      expect(editor.logicalCursor.row).toBe(0)\n    })\n\n    it(\"should delete from start to cursor with ctrl+u on line 2\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1 content\\nLine 2 content\\nLine 3 content\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n      for (let i = 0; i < 7; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1 content\\ncontent\\nLine 3 content\")\n      expect(editor.logicalCursor.col).toBe(0)\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should do nothing with ctrl+k when cursor is at end of line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1 content\\nLine 2 content\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1 content\\nLine 2 content\")\n      expect(editor.logicalCursor.col).toBe(14)\n    })\n\n    it(\"should do nothing with ctrl+u when cursor is at start of line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1 content\\nLine 2 content\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1 content\\nLine 2 content\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should work with ctrl+k after undo\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"u\", action: \"undo\" }],\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Hello \")\n\n      currentMockInput.pressKey(\"u\")\n      expect(editor.plainText).toBe(\"Hello World\")\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Hello \")\n    })\n\n    it(\"should work with ctrl+u after undo when cursor is repositioned\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"z\", action: \"undo\" }],\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n      expect(editor.plainText).toBe(\"World\")\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"z\")\n      expect(editor.plainText).toBe(\"Hello World\")\n\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n      expect(editor.plainText).toBe(\"World\")\n    })\n\n    it(\"should allow cursor to move right within restored line after undo\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1 content\\nLine 2 content\\nLine 3 content\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"u\", action: \"undo\" }],\n      })\n\n      editor.focus()\n      for (let i = 0; i < 7; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1 \\nLine 2 content\\nLine 3 content\")\n\n      currentMockInput.pressKey(\"u\")\n      expect(editor.plainText).toBe(\"Line 1 content\\nLine 2 content\\nLine 3 content\")\n\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight()\n      }\n\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(10)\n    })\n\n    it(\"should allow ctrl+k to work again after undo\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1 content\\nLine 2\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"u\", action: \"undo\" }],\n      })\n\n      editor.focus()\n      for (let i = 0; i < 7; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1 \\nLine 2\")\n\n      currentMockInput.pressKey(\"u\")\n      expect(editor.plainText).toBe(\"Line 1 content\\nLine 2\")\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n      expect(editor.plainText).toBe(\"Line 1 \\nLine 2\")\n    })\n  })\n\n  describe(\"Wrapped Lines\", () => {\n    it(\"should delete to end of logical line with ctrl+k when wrapping enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"This is a very long line that will wrap when viewport is narrow\\nLine 2 content\",\n        width: 20,\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 30; i++) {\n        editor.moveCursorRight()\n      }\n\n      const visualCursor = editor.editorView.getVisualCursor()\n      expect(visualCursor.logicalRow).toBe(0)\n      expect(visualCursor.logicalCol).toBe(30)\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(\"This is a very long line that \")\n      expect(lines[1]).toBe(\"Line 2 content\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(30)\n    })\n\n    it(\"should delete from start of logical line with ctrl+u when wrapping enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"This is a very long line that will wrap when viewport is narrow\\nLine 2 content\",\n        width: 20,\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n\n      const originalLine0 = editor.plainText.split(\"\\n\")[0]\n\n      for (let i = 0; i < 30; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(originalLine0.substring(30))\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should work on second logical line when wrapped\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Short line 1\\nThis is another very long line that will wrap\\nLine 3\",\n        width: 20,\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      const line1Before = editor.plainText.split(\"\\n\")[1]\n\n      for (let i = 0; i < 25; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(\"Short line 1\")\n      expect(lines[1]).toBe(line1Before.substring(0, 25))\n      expect(lines[2]).toBe(\"Line 3\")\n    })\n\n    it(\"should work after undo with wrapped lines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"This is a very long line that will wrap\\nLine 2\",\n        width: 15,\n        height: 10,\n        wrapMode: \"word\",\n        keyBindings: [{ name: \"z\", action: \"undo\" }],\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 20; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const afterDelete = editor.plainText.split(\"\\n\")[0]\n      expect(afterDelete.length).toBe(20)\n\n      currentMockInput.pressKey(\"z\")\n\n      const afterUndo = editor.plainText.split(\"\\n\")[0]\n      expect(afterUndo.length).toBe(39)\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const afterSecondDelete = editor.plainText.split(\"\\n\")[0]\n      expect(afterSecondDelete.length).toBe(20)\n    })\n\n    it(\"should handle ctrl+k at exact wrap boundary\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAAAAAAAABBBBBBBBBBCCCCCCCCCC\\nLine 2\",\n        width: 10,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 10; i++) {\n        editor.moveCursorRight()\n      }\n\n      const visualCursor = editor.editorView.getVisualCursor()\n      expect(visualCursor.visualRow).toBe(1)\n      expect(visualCursor.logicalCol).toBe(10)\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(\"AAAAAAAAAA\")\n      expect(lines[1]).toBe(\"Line 2\")\n    })\n\n    it(\"should handle ctrl+u on second visual line of first logical line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAAAAAAAABBBBBBBBBBCCCCCCCCCC\\nLine 2\",\n        width: 10,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 15; i++) {\n        editor.moveCursorRight()\n      }\n\n      const visualCursor = editor.editorView.getVisualCursor()\n      expect(visualCursor.visualRow).toBe(1)\n      expect(visualCursor.logicalRow).toBe(0)\n      expect(visualCursor.logicalCol).toBe(15)\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(\"BBBBBCCCCCCCCCC\")\n      expect(lines[0].length).toBe(15)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n  })\n\n  describe(\"Wrapped Lines\", () => {\n    it(\"should delete to end of logical line with ctrl+k when wrapping enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"This is a very long line that will wrap when viewport is narrow\\nLine 2 content\",\n        width: 20,\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 30; i++) {\n        editor.moveCursorRight()\n      }\n\n      const visualCursor = editor.editorView.getVisualCursor()\n      expect(visualCursor.logicalRow).toBe(0)\n      expect(visualCursor.logicalCol).toBe(30)\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(\"This is a very long line that \")\n      expect(lines[1]).toBe(\"Line 2 content\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(30)\n    })\n\n    it(\"should delete from start of logical line with ctrl+u when wrapping enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"This is a very long line that will wrap when viewport is narrow\\nLine 2 content\",\n        width: 20,\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n\n      const originalLine0 = editor.plainText.split(\"\\n\")[0]\n\n      for (let i = 0; i < 30; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(originalLine0.substring(30))\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should work on second logical line when wrapped\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Short line 1\\nThis is another very long line that will wrap\\nLine 3\",\n        width: 20,\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      const line1Before = editor.plainText.split(\"\\n\")[1]\n\n      for (let i = 0; i < 25; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(\"Short line 1\")\n      expect(lines[1]).toBe(line1Before.substring(0, 25))\n      expect(lines[2]).toBe(\"Line 3\")\n    })\n\n    it(\"should work after undo with wrapped lines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"This is a very long line that will wrap\\nLine 2\",\n        width: 15,\n        height: 10,\n        wrapMode: \"word\",\n        keyBindings: [{ name: \"z\", action: \"undo\" }],\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 20; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const afterDelete = editor.plainText.split(\"\\n\")[0]\n      expect(afterDelete.length).toBe(20)\n\n      currentMockInput.pressKey(\"z\")\n\n      const afterUndo = editor.plainText.split(\"\\n\")[0]\n      expect(afterUndo.length).toBe(39)\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const afterSecondDelete = editor.plainText.split(\"\\n\")[0]\n      expect(afterSecondDelete.length).toBe(20)\n    })\n\n    it(\"should handle ctrl+k at exact wrap boundary\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAAAAAAAABBBBBBBBBBCCCCCCCCCC\\nLine 2\",\n        width: 10,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 10; i++) {\n        editor.moveCursorRight()\n      }\n\n      const visualCursor = editor.editorView.getVisualCursor()\n      expect(visualCursor.visualRow).toBe(1)\n      expect(visualCursor.logicalCol).toBe(10)\n\n      currentMockInput.pressKey(\"k\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(\"AAAAAAAAAA\")\n      expect(lines[1]).toBe(\"Line 2\")\n    })\n\n    it(\"should handle ctrl+u on second visual line of first logical line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAAAAAAAABBBBBBBBBBCCCCCCCCCC\\nLine 2\",\n        width: 10,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 15; i++) {\n        editor.moveCursorRight()\n      }\n\n      const visualCursor = editor.editorView.getVisualCursor()\n      expect(visualCursor.visualRow).toBe(1)\n      expect(visualCursor.logicalRow).toBe(0)\n      expect(visualCursor.logicalCol).toBe(15)\n\n      currentMockInput.pressKey(\"u\", { ctrl: true })\n\n      const lines = editor.plainText.split(\"\\n\")\n      expect(lines[0]).toBe(\"BBBBBCCCCCCCCCC\")\n      expect(lines[0].length).toBe(15)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n  })\n\n  describe(\"Key Bindings\", () => {\n    it(\"should use default keybindings\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"g\", action: \"buffer-home\" },\n          { name: \"b\", action: \"buffer-end\" },\n        ],\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n      expect(editor.logicalCursor.row).toBe(2)\n\n      currentMockInput.pressKey(\"g\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"b\")\n      expect(editor.logicalCursor.row).toBe(2)\n    })\n\n    it(\"should map select-up and select-down to custom keys\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        keyBindings: [\n          { name: \"k\", shift: true, action: \"select-up\" },\n          { name: \"j\", shift: true, action: \"select-down\" },\n        ],\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      currentMockInput.pressKey(\"J\", { shift: true })\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n      expect(selectedText.includes(\"Line\")).toBe(true)\n    })\n\n    it(\"should preserve default keybindings when custom bindings don't override them\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"j\", action: \"move-down\" }],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressKey(\"HOME\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should allow remapping default keys to different actions\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"up\", action: \"buffer-home\" }],\n      })\n\n      editor.focus()\n      editor.gotoLine(2)\n\n      currentMockInput.pressArrow(\"up\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should handle complex keybinding scenario with multiple custom mappings\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"h\", action: \"move-left\" },\n          { name: \"j\", action: \"move-down\" },\n          { name: \"k\", action: \"move-up\" },\n          { name: \"l\", action: \"move-right\" },\n          { name: \"i\", action: \"buffer-home\" },\n          { name: \"a\", action: \"line-end\" },\n        ],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"i\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"a\")\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"h\")\n      expect(editor.logicalCursor.col).toBe(5)\n\n      currentMockInput.pressKey(\"j\")\n      expect(editor.logicalCursor.row).toBe(1)\n\n      currentMockInput.pressKey(\"k\")\n      expect(editor.logicalCursor.row).toBe(0)\n\n      currentMockInput.pressKey(\"l\")\n      expect(editor.logicalCursor.col).toBe(6)\n    })\n\n    it(\"should not insert text when key is bound to action\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"x\", action: \"delete\" }],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"x\")\n      expect(editor.plainText).toBe(\"ello\")\n\n      expect(editor.plainText).not.toContain(\"x\")\n    })\n\n    it(\"should still insert unbound keys as text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"j\", action: \"move-down\" }],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"h\")\n      expect(editor.plainText).toBe(\"h\")\n\n      currentMockInput.pressKey(\"i\")\n      expect(editor.plainText).toBe(\"hi\")\n\n      currentMockInput.pressKey(\"j\")\n      expect(editor.plainText).toBe(\"hi\")\n    })\n\n    it(\"should differentiate between key with and without modifiers\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"d\", action: \"delete\" },\n          { name: \"d\", meta: true, action: \"delete-line\" },\n        ],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"d\")\n      expect(editor.plainText).toBe(\"ello\")\n    })\n\n    it(\"should support selection actions with custom keybindings\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        keyBindings: [\n          { name: \"h\", shift: true, action: \"select-left\" },\n          { name: \"l\", shift: true, action: \"select-right\" },\n        ],\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      currentMockInput.pressKey(\"H\", { shift: true })\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"d\")\n\n      currentMockInput.pressKey(\"H\", { shift: true })\n      expect(editor.getSelectedText()).toBe(\"ld\")\n\n      currentMockInput.pressKey(\"L\", { shift: true })\n      expect(editor.getSelectedText()).toBe(\"d\")\n    })\n\n    it(\"should execute correct action when multiple keys map to different actions with same base\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"j\", action: \"move-down\" },\n          { name: \"j\", ctrl: true, action: \"buffer-end\" },\n        ],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"j\")\n      expect(editor.logicalCursor.row).toBe(1)\n\n      editor.gotoLine(0)\n      currentMockInput.pressKey(\"j\", { ctrl: true })\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should handle all action types via custom keybindings\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        keyBindings: [\n          { name: \"1\", action: \"move-left\" },\n          { name: \"2\", action: \"move-right\" },\n          { name: \"3\", action: \"move-up\" },\n          { name: \"4\", action: \"move-down\" },\n          { name: \"5\", shift: true, action: \"select-left\" },\n          { name: \"6\", shift: true, action: \"select-right\" },\n          { name: \"7\", shift: true, action: \"select-up\" },\n          { name: \"8\", shift: true, action: \"select-down\" },\n          { name: \"a\", action: \"line-home\" },\n          { name: \"b\", action: \"line-end\" },\n          { name: \"c\", shift: true, action: \"select-line-home\" },\n          { name: \"d\", shift: true, action: \"select-line-end\" },\n          { name: \"e\", action: \"buffer-home\" },\n          { name: \"f\", action: \"buffer-end\" },\n          { name: \"g\", action: \"delete-line\" },\n          { name: \"h\", action: \"delete-to-line-end\" },\n          { name: \"i\", action: \"backspace\" },\n          { name: \"j\", action: \"delete\" },\n          { name: \"k\", action: \"newline\" },\n          { name: \"u\", action: \"undo\" },\n          { name: \"r\", action: \"redo\" },\n        ],\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n      editor.moveCursorRight()\n      editor.moveCursorRight()\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressKey(\"1\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressKey(\"2\")\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressKey(\"3\")\n      expect(editor.logicalCursor.row).toBe(0)\n\n      currentMockInput.pressKey(\"4\")\n      expect(editor.logicalCursor.row).toBe(1)\n\n      currentMockInput.pressKey(\"a\")\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"b\")\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"e\")\n      expect(editor.logicalCursor.row).toBe(0)\n\n      currentMockInput.pressKey(\"f\")\n      expect(editor.logicalCursor.row).toBe(2)\n    })\n\n    it(\"should not break when empty keyBindings array is provided\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressKey(\"HOME\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should document limitation: bound character keys cannot be typed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"h\", action: \"move-left\" },\n          { name: \"j\", action: \"move-down\" },\n          { name: \"k\", action: \"move-up\" },\n          { name: \"l\", action: \"move-right\" },\n        ],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"h\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"o\")\n\n      expect(editor.plainText).toBe(\"eo\")\n    })\n\n    it(\"should allow typing bound characters when using modifier keys for bindings\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"h\", ctrl: true, action: \"move-left\" },\n          { name: \"j\", ctrl: true, action: \"move-down\" },\n          { name: \"k\", ctrl: true, action: \"move-up\" },\n          { name: \"l\", ctrl: true, action: \"move-right\" },\n        ],\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"h\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"o\")\n\n      expect(editor.plainText).toBe(\"hello\")\n\n      currentMockInput.pressKey(\"h\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(4)\n    })\n  })\n\n  describe(\"Default Word Deletion Keybindings\", () => {\n    it(\"should delete character forward with ctrl+d\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"d\", { ctrl: true })\n      expect(editor.plainText).toBe(\"ello world test\")\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"d\", { ctrl: true })\n      expect(editor.plainText).toBe(\"llo world test\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should delete word backward with ctrl+w\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n      expect(editor.logicalCursor.col).toBe(16)\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"hello world \")\n      expect(editor.logicalCursor.col).toBe(12)\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"hello \")\n      expect(editor.logicalCursor.col).toBe(6)\n    })\n\n    it(\"should stop at CJK-ASCII boundary with ctrl+w\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"日本語abc\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"日本語\")\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should keep Hangul run grouped with ctrl+w\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"테스트test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"테스트\")\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should stop at CJK punctuation before ASCII with ctrl+w\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"日本語。abc\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"日本語。\")\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should stop at compat ideograph boundary with ctrl+w\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"丽abc\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"丽\")\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should delete word forward with meta+d\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText).toBe(\"world test\")\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText).toBe(\"test\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should delete character forward from middle of word with ctrl+d\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"d\", { ctrl: true })\n      expect(editor.plainText).toBe(\"helo world\")\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should delete word backward from middle of word with ctrl+w\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 8; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(8)\n\n      currentMockInput.pressKey(\"w\", { ctrl: true })\n      expect(editor.plainText).toBe(\"hello rld\")\n      expect(editor.logicalCursor.col).toBe(6)\n    })\n\n    it(\"should delete word forward from middle of word with meta+d\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText).toBe(\"helworld\")\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should delete word forward from space with meta+d\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 5; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(5)\n\n      currentMockInput.pressKey(\"d\", { meta: true })\n      expect(editor.plainText).toBe(\"hellotest\")\n      expect(editor.logicalCursor.col).toBe(5)\n    })\n\n    it(\"should delete word forward with meta+delete\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"DELETE\", { meta: true })\n      expect(editor.plainText).toBe(\"world test\")\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"DELETE\", { meta: true })\n      expect(editor.plainText).toBe(\"test\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should delete word forward from middle of word with meta+delete\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"DELETE\", { meta: true })\n      expect(editor.plainText).toBe(\"helworld\")\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should delete word forward from space with meta+delete\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 5; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(5)\n\n      currentMockInput.pressKey(\"DELETE\", { meta: true })\n      expect(editor.plainText).toBe(\"hellotest\")\n      expect(editor.logicalCursor.col).toBe(5)\n    })\n\n    it(\"should delete word forward with ctrl+delete\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"DELETE\", { ctrl: true })\n      expect(editor.plainText).toBe(\"world test\")\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"DELETE\", { ctrl: true })\n      expect(editor.plainText).toBe(\"test\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should delete word forward from middle of word with ctrl+delete\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 3; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressKey(\"DELETE\", { ctrl: true })\n      expect(editor.plainText).toBe(\"helworld\")\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should delete word forward from space with ctrl+delete\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 5; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(5)\n\n      currentMockInput.pressKey(\"DELETE\", { ctrl: true })\n      expect(editor.plainText).toBe(\"hellotest\")\n      expect(editor.logicalCursor.col).toBe(5)\n    })\n\n    it(\"should delete word backward with ctrl+backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n      expect(editor.logicalCursor.col).toBe(16)\n\n      currentMockInput.pressBackspace({ ctrl: true })\n      expect(editor.plainText).toBe(\"hello world \")\n      expect(editor.logicalCursor.col).toBe(12)\n\n      currentMockInput.pressBackspace({ ctrl: true })\n      expect(editor.plainText).toBe(\"hello \")\n      expect(editor.logicalCursor.col).toBe(6)\n    })\n\n    it(\"should delete word backward from middle of word with ctrl+backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 8; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(8)\n\n      currentMockInput.pressBackspace({ ctrl: true })\n      expect(editor.plainText).toBe(\"hello rld\")\n      expect(editor.logicalCursor.col).toBe(6)\n    })\n\n    it(\"should delete word backward from space with ctrl+backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressBackspace({ ctrl: true })\n      expect(editor.plainText).toBe(\"world test\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should delete line with ctrl+shift+d (requires Kitty keyboard protocol)\", async () => {\n      const {\n        renderer: kittyRenderer,\n        renderOnce: kittyRenderOnce,\n        mockInput: kittyMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        kittyKeyboard: true,\n      })\n\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n      expect(editor.logicalCursor.row).toBe(1)\n\n      kittyMockInput.pressKey(\"d\", { ctrl: true, shift: true })\n      expect(editor.plainText).toBe(\"Line 1\\nLine 3\")\n      expect(editor.logicalCursor.row).toBe(1)\n\n      kittyRenderer.destroy()\n    })\n\n    it(\"should delete first line with ctrl+shift+d (requires Kitty keyboard protocol)\", async () => {\n      const {\n        renderer: kittyRenderer,\n        renderOnce: kittyRenderOnce,\n        mockInput: kittyMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        kittyKeyboard: true,\n      })\n\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.row).toBe(0)\n\n      kittyMockInput.pressKey(\"d\", { ctrl: true, shift: true })\n      expect(editor.plainText).toBe(\"Line 2\\nLine 3\")\n      expect(editor.logicalCursor.row).toBe(0)\n\n      kittyRenderer.destroy()\n    })\n\n    it(\"should delete last line with ctrl+shift+d (requires Kitty keyboard protocol)\", async () => {\n      const {\n        renderer: kittyRenderer,\n        renderOnce: kittyRenderOnce,\n        mockInput: kittyMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        kittyKeyboard: true,\n      })\n\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(2)\n      expect(editor.logicalCursor.row).toBe(2)\n\n      kittyMockInput.pressKey(\"d\", { ctrl: true, shift: true })\n      expect(editor.plainText).toBe(\"Line 1\\nLine 2\")\n      expect(editor.logicalCursor.row).toBe(1)\n\n      kittyRenderer.destroy()\n    })\n  })\n\n  describe(\"Default Character and Word Movement Keybindings\", () => {\n    it(\"should move forward one character with ctrl+f\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should move backward one character with ctrl+b\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n      expect(editor.logicalCursor.col).toBe(11)\n\n      currentMockInput.pressKey(\"b\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(10)\n\n      currentMockInput.pressKey(\"b\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(9)\n\n      currentMockInput.pressKey(\"b\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(8)\n    })\n\n    it(\"should move forward one word with meta+f\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(12)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(16)\n    })\n\n    it(\"should move backward one word with meta+b\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n      expect(editor.logicalCursor.col).toBe(16)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(12)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should move forward one word with ctrl+right\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(12)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(16)\n    })\n\n    it(\"should move backward one word with ctrl+left\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello world test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n      expect(editor.logicalCursor.col).toBe(16)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(12)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should move across CJK-ASCII boundary with ctrl+right and ctrl+left\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"日本語abc\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(9)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should move across CJK punctuation boundary with ctrl+right and ctrl+left\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"日本語。abc\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(8)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(11)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(8)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should move across compat ideograph boundary with ctrl+right and ctrl+left\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"丽abc\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(5)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should select words across CJK-ASCII boundary with meta+shift+arrows\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"日本語abc\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\", { meta: true, shift: true })\n      expect(editor.logicalCursor.col).toBe(6)\n      expect(editor.getSelectedText()).toBe(\"日本語\")\n\n      currentMockInput.pressArrow(\"right\", { meta: true, shift: true })\n      expect(editor.logicalCursor.col).toBe(9)\n      expect(editor.getSelectedText()).toBe(\"日本語abc\")\n\n      currentMockInput.pressArrow(\"left\", { meta: true, shift: true })\n      expect(editor.logicalCursor.col).toBe(6)\n      expect(editor.getSelectedText()).toBe(\"日本語\")\n\n      currentMockInput.pressArrow(\"left\", { meta: true, shift: true })\n      expect(editor.logicalCursor.col).toBe(0)\n      expect(editor.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should select words across compat ideograph boundary with meta+shift+arrows\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"丽abc\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\", { meta: true, shift: true })\n      expect(editor.logicalCursor.col).toBe(2)\n      expect(editor.getSelectedText()).toBe(\"丽\")\n\n      currentMockInput.pressArrow(\"right\", { meta: true, shift: true })\n      expect(editor.logicalCursor.col).toBe(5)\n      expect(editor.getSelectedText()).toBe(\"丽abc\")\n\n      currentMockInput.pressArrow(\"left\", { meta: true, shift: true })\n      expect(editor.logicalCursor.col).toBe(2)\n      expect(editor.getSelectedText()).toBe(\"丽\")\n\n      currentMockInput.pressArrow(\"left\", { meta: true, shift: true })\n      expect(editor.logicalCursor.col).toBe(0)\n      expect(editor.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should combine ctrl+left and ctrl+right for word navigation\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"one two three four\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(4)\n\n      currentMockInput.pressArrow(\"right\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(8)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(4)\n\n      currentMockInput.pressArrow(\"left\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should not insert 'f' when using ctrl+f for movement\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      const before = editor.plainText\n\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      expect(editor.plainText).toBe(before)\n      expect(editor.logicalCursor.col).toBe(1)\n    })\n\n    it(\"should not insert 'b' when using ctrl+b for movement\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n      const before = editor.plainText\n\n      currentMockInput.pressKey(\"b\", { ctrl: true })\n      expect(editor.plainText).toBe(before)\n      expect(editor.logicalCursor.col).toBe(3)\n    })\n\n    it(\"should combine ctrl+f and ctrl+b for character navigation\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(2)\n\n      currentMockInput.pressKey(\"b\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      currentMockInput.pressKey(\"f\", { ctrl: true })\n      expect(editor.logicalCursor.col).toBe(4)\n    })\n\n    it(\"should combine meta+f and meta+b for word navigation\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"one two three four\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(4)\n\n      currentMockInput.pressKey(\"f\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(8)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(4)\n\n      currentMockInput.pressKey(\"b\", { meta: true })\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n  })\n\n  describe(\"Shift+Space Key Handling\", () => {\n    let modifierRenderer: TestRenderer\n    let modifierRenderOnce: () => Promise<void>\n    let modifierMockInput: MockInput\n\n    beforeEach(async () => {\n      ;({\n        renderer: modifierRenderer,\n        renderOnce: modifierRenderOnce,\n        mockInput: modifierMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        otherModifiersMode: true,\n      }))\n    })\n\n    afterEach(() => {\n      modifierRenderer.destroy()\n    })\n\n    it(\"should insert a space when shift+space is pressed\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(modifierRenderer, modifierRenderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Type \"hello\"\n      modifierMockInput.pressKey(\"h\")\n      modifierMockInput.pressKey(\"e\")\n      modifierMockInput.pressKey(\"l\")\n      modifierMockInput.pressKey(\"l\")\n      modifierMockInput.pressKey(\"o\")\n      expect(editor.plainText).toBe(\"hello\")\n\n      // Press shift+space - should insert a space\n      modifierMockInput.pressKey(\" \", { shift: true })\n      expect(editor.plainText).toBe(\"hello \")\n      expect(editor.logicalCursor.col).toBe(6)\n\n      // Type \"world\"\n      modifierMockInput.pressKey(\"w\")\n      modifierMockInput.pressKey(\"o\")\n      modifierMockInput.pressKey(\"r\")\n      modifierMockInput.pressKey(\"l\")\n      modifierMockInput.pressKey(\"d\")\n      expect(editor.plainText).toBe(\"hello world\")\n    })\n\n    it(\"should insert multiple spaces with shift+space\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(modifierRenderer, modifierRenderOnce, {\n        initialValue: \"test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLineEnd()\n\n      modifierMockInput.pressKey(\" \", { shift: true })\n      modifierMockInput.pressKey(\" \", { shift: true })\n      modifierMockInput.pressKey(\" \", { shift: true })\n\n      expect(editor.plainText).toBe(\"test   \")\n      expect(editor.logicalCursor.col).toBe(7)\n    })\n\n    it(\"should insert space at middle of text with shift+space\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(modifierRenderer, modifierRenderOnce, {\n        initialValue: \"helloworld\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 5; i++) {\n        editor.moveCursorRight()\n      }\n      expect(editor.logicalCursor.col).toBe(5)\n\n      modifierMockInput.pressKey(\" \", { shift: true })\n\n      expect(editor.plainText).toBe(\"hello world\")\n      expect(editor.logicalCursor.col).toBe(6)\n    })\n  })\n\n  describe(\"Line Home/End Wrap Behavior\", () => {\n    it(\"should wrap to end of previous line when at start of line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n      })\n      editor.focus()\n      editor.gotoLine(1)\n      expect(editor.logicalCursor).toMatchObject({ row: 1, col: 0 })\n      editor.gotoLineHome()\n      expect(editor.logicalCursor).toMatchObject({ row: 0, col: 6 })\n    })\n\n    it(\"should wrap to start of next line when at end of line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n      })\n      editor.focus()\n      editor.gotoLineEnd()\n      expect(editor.logicalCursor).toMatchObject({ row: 0, col: 6 })\n      editor.gotoLineEnd()\n      expect(editor.logicalCursor).toMatchObject({ row: 1, col: 0 })\n    })\n\n    it(\"should stay at buffer boundaries\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n      })\n      editor.focus()\n      editor.gotoLineHome()\n      expect(editor.logicalCursor).toMatchObject({ row: 0, col: 0 })\n      editor.gotoLine(1)\n      editor.gotoLineEnd()\n      editor.gotoLineEnd()\n      expect(editor.logicalCursor).toMatchObject({ row: 1, col: 6 })\n    })\n  })\n\n  describe(\"Key Aliases\", () => {\n    it(\"should support binding 'enter' alias which maps to 'return'\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"enter\", action: \"buffer-home\" }],\n      })\n      editor.focus()\n      editor.gotoLine(9999)\n      // When user binds \"enter\", and \"return\" key is pressed (the actual Enter key)\n      // it should work due to the default alias enter->return\n      currentMockInput.pressEnter()\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should allow binding 'return' directly\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"return\", action: \"buffer-home\" }],\n      })\n      editor.focus()\n      editor.gotoLine(9999)\n      currentMockInput.pressEnter()\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should support custom aliases via keyAliasMap\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"myenter\", action: \"buffer-home\" }],\n        keyAliasMap: { myenter: \"return\" },\n      })\n      editor.focus()\n      editor.gotoLine(9999)\n      // Pressing Enter key (which comes in as \"return\") should trigger buffer-home\n      // because \"myenter\" is aliased to \"return\"\n      currentMockInput.pressEnter()\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should merge custom aliases with defaults\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n        keyBindings: [\n          { name: \"enter\", action: \"buffer-home\" },\n          { name: \"customkey\", action: \"line-end\" },\n        ],\n        keyAliasMap: { customkey: \"e\", enter: \"return\" },\n      })\n      editor.focus()\n      // Default alias should still work (enter -> return)\n      currentMockInput.pressEnter()\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n      // Custom alias should work (customkey -> e)\n      currentMockInput.pressKey(\"e\")\n      expect(editor.logicalCursor.col).toBe(5)\n    })\n\n    it(\"should update aliases dynamically with setter\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"mykey\", action: \"buffer-home\" }],\n      })\n      editor.focus()\n      editor.gotoLine(9999)\n      expect(editor.logicalCursor.row).toBe(1)\n      // Initially \"mykey\" doesn't map to \"return\", so Enter won't trigger buffer-home\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Line 1\\nLine 2\\n\") // newline was inserted\n      // Set alias to map \"mykey\" to \"return\"\n      editor.keyAliasMap = { mykey: \"return\" }\n      // Now remove the newline we just added\n      editor.deleteCharBackward()\n      // Now pressing Enter should trigger buffer-home\n      currentMockInput.pressEnter()\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should handle aliases with modifiers\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n        keyBindings: [{ name: \"enter\", meta: true, action: \"buffer-home\" }],\n      })\n      editor.focus()\n      editor.gotoLine(9999)\n      expect(editor.logicalCursor.row).toBe(1)\n      // Meta+Enter should trigger buffer-home due to alias (enter -> return)\n      currentMockInput.pressEnter({ meta: true })\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n  })\n\n  describe(\"Selection with ctrl+shift+a/e (line home/end)\", () => {\n    let kittyRenderer: TestRenderer\n    let kittyRenderOnce: () => Promise<void>\n    let kittyMockInput: MockInput\n\n    beforeEach(async () => {\n      ;({\n        renderer: kittyRenderer,\n        renderOnce: kittyRenderOnce,\n        mockInput: kittyMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        kittyKeyboard: true,\n      }))\n    })\n\n    afterEach(() => {\n      kittyRenderer.destroy()\n    })\n\n    it(\"should select to line start with ctrl+shift+a\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 11) // End of line\n\n      kittyMockInput.pressKey(\"a\", { ctrl: true, shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      const selection = editor.getSelection()\n      expect(selection).not.toBeNull()\n      expect(selection!.start).toBe(0)\n      expect(selection!.end).toBe(11)\n      expect(editor.getSelectedText()).toBe(\"Hello World\")\n    })\n\n    it(\"should select to line end with ctrl+shift+e\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 0) // Start of line\n\n      kittyMockInput.pressKey(\"e\", { ctrl: true, shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      const selection = editor.getSelection()\n      expect(selection).not.toBeNull()\n      expect(selection!.start).toBe(0)\n      expect(selection!.end).toBe(11)\n      expect(editor.getSelectedText()).toBe(\"Hello World\")\n    })\n\n    it(\"should select to line start from middle with ctrl+shift+a\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 6) // After \"Hello \"\n\n      kittyMockInput.pressKey(\"a\", { ctrl: true, shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello W\")\n    })\n\n    it(\"should select to line end from middle with ctrl+shift+e\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 6) // After \"Hello \"\n\n      kittyMockInput.pressKey(\"e\", { ctrl: true, shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"World\")\n    })\n\n    it(\"should work on multiline text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(1, 4) // Middle of second line\n\n      // Select to start of line 2\n      kittyMockInput.pressKey(\"a\", { ctrl: true, shift: true })\n      expect(editor.getSelectedText()).toBe(\"Line \")\n\n      // Clear selection and move to same position\n      editor.editBuffer.setCursor(1, 4)\n\n      // Select to end of line 2\n      kittyMockInput.pressKey(\"e\", { ctrl: true, shift: true })\n      expect(editor.getSelectedText()).toBe(\" 2\")\n    })\n\n    it(\"should handle line wrapping behavior\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Line 1\\nLine 2\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      // At end of line 1\n      editor.editBuffer.setCursor(0, 6)\n\n      // First ctrl+shift+a from EOL should select entire line\n      kittyMockInput.pressKey(\"a\", { ctrl: true, shift: true })\n      expect(editor.getSelectedText()).toBe(\"Line 1\")\n\n      // Reset\n      editor.editBuffer.setCursor(0, 0)\n\n      // From start, ctrl+shift+e should select line, then wrap to next line\n      kittyMockInput.pressKey(\"e\", { ctrl: true, shift: true })\n      const cursor = editor.editBuffer.getCursorPosition()\n      expect(cursor.col).toBeGreaterThan(0)\n    })\n\n    it(\"should not interfere with ctrl+a (without shift)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 11)\n\n      // ctrl+a (without shift) should just move, not select\n      currentMockInput.pressKey(\"a\", { ctrl: true })\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should not interfere with ctrl+e (without shift)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 0)\n\n      // ctrl+e (without shift) should just move, not select\n      currentMockInput.pressKey(\"e\", { ctrl: true })\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.logicalCursor.col).toBe(11)\n    })\n  })\n\n  describe(\"Visual line navigation with meta+a/e\", () => {\n    it(\"should navigate to visual line start with meta+a (no wrapping)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        wrapMode: \"none\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 6)\n\n      currentMockInput.pressKey(\"a\", { meta: true })\n\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should navigate to visual line end with meta+e (no wrapping)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        wrapMode: \"none\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 6)\n\n      currentMockInput.pressKey(\"e\", { meta: true })\n\n      expect(editor.logicalCursor.col).toBe(11)\n    })\n\n    it(\"should navigate to visual line start with meta+a (with wrapping)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 22) // In second visual line\n\n      currentMockInput.pressKey(\"a\", { meta: true })\n\n      const cursor = editor.logicalCursor\n      expect(cursor.col).toBe(20) // Start of second visual line, not 0\n    })\n\n    it(\"should navigate to visual line end with meta+e (with wrapping)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 5) // In first visual line\n\n      currentMockInput.pressKey(\"e\", { meta: true })\n\n      const cursor = editor.logicalCursor\n      expect(cursor.col).toBe(19)\n    })\n\n    it(\"should differ from ctrl+a/e when wrapping is enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 22)\n\n      // meta+a goes to visual line start (col 20)\n      currentMockInput.pressKey(\"a\", { meta: true })\n      const visualHomeCol = editor.logicalCursor.col\n      expect(visualHomeCol).toBe(20)\n\n      // Reset cursor\n      editor.editBuffer.setCursor(0, 22)\n\n      // ctrl+a goes to logical line start (col 0)\n      currentMockInput.pressKey(\"a\", { ctrl: true })\n      const logicalHomeCol = editor.logicalCursor.col\n      expect(logicalHomeCol).toBe(0)\n\n      expect(visualHomeCol).not.toBe(logicalHomeCol)\n    })\n  })\n\n  describe(\"Visual line selection with meta+shift+a/e\", () => {\n    let kittyRenderer: TestRenderer\n    let kittyRenderOnce: () => Promise<void>\n    let kittyMockInput: MockInput\n\n    beforeEach(async () => {\n      ;({\n        renderer: kittyRenderer,\n        renderOnce: kittyRenderOnce,\n        mockInput: kittyMockInput,\n      } = await createTestRenderer({\n        width: 80,\n        height: 24,\n        kittyKeyboard: true,\n      }))\n    })\n\n    afterEach(() => {\n      kittyRenderer.destroy()\n    })\n\n    it(\"should select to visual line start with meta+shift+a\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 25) // In second visual line\n\n      kittyMockInput.pressKey(\"a\", { meta: true, shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n      expect(selectedText.length).toBe(6) // From col 20 to 26 (includes char at 25)\n    })\n\n    it(\"should select to visual line end with meta+shift+e\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 10) // In first visual line\n\n      kittyMockInput.pressKey(\"e\", { meta: true, shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n      expect(selectedText).toBe(\"KLMNOPQRS\")\n    })\n\n    it(\"should work without wrapping (same as logical)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        wrapMode: \"none\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 6)\n\n      kittyMockInput.pressKey(\"a\", { meta: true, shift: true })\n      expect(editor.getSelectedText()).toBe(\"Hello W\")\n\n      editor.editBuffer.setCursor(0, 6)\n      kittyMockInput.pressKey(\"e\", { meta: true, shift: true })\n      expect(editor.getSelectedText()).toBe(\"World\")\n    })\n\n    it(\"should differ from ctrl+shift+a/e when wrapping is enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(kittyRenderer, kittyRenderOnce, {\n        initialValue: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      editor.focus()\n      editor.editBuffer.setCursor(0, 25) // In second visual line\n\n      // meta+shift+a selects to visual line start\n      kittyMockInput.pressKey(\"a\", { meta: true, shift: true })\n      const visualSelection = editor.getSelectedText()\n      expect(visualSelection.length).toBe(6) // From 20 to 26\n\n      // Reset\n      editor.editBuffer.setCursor(0, 25)\n\n      // ctrl+shift+a selects to logical line start\n      kittyMockInput.pressKey(\"a\", { ctrl: true, shift: true })\n      const logicalSelection = editor.getSelectedText()\n      expect(logicalSelection.length).toBe(26) // From 0 to 26\n\n      expect(visualSelection).not.toBe(logicalSelection)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.paste.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\nimport { decodePasteBytes, PasteEvent } from \"../../lib/index.js\"\nimport { pasteBytes } from \"../../testing/mock-keys.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Paste Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Paste Events\", () => {\n    it(\"should paste text at cursor position\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      await currentMockInput.pasteBracketedText(\" World\")\n\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should paste text in the middle\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"HelloWorld\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 5; i++) {\n        editor.moveCursorRight()\n      }\n\n      await currentMockInput.pasteBracketedText(\" \")\n\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should paste multi-line text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Start\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      await currentMockInput.pasteBracketedText(\"\\nLine 2\\nLine 3\")\n\n      expect(editor.plainText).toBe(\"Start\\nLine 2\\nLine 3\")\n    })\n\n    it(\"should paste text at beginning of buffer\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      // Cursor starts at beginning\n\n      await currentMockInput.pasteBracketedText(\"Hello \")\n\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should replace selected text when pasting\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      // Select \"Hello\" using shift+right\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n\n      // Paste to replace selection\n      await currentMockInput.pasteBracketedText(\"Goodbye\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"Goodbye World\")\n    })\n\n    it(\"should replace multi-line selection when pasting\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      // Select from start through \"Line 1\\nLi\"\n      for (let i = 0; i < 10; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.hasSelection()).toBe(true)\n\n      // Paste replacement text\n      await currentMockInput.pasteBracketedText(\"New\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"Newe 2\\nLine 3\")\n    })\n\n    it(\"should replace selected text with multi-line paste\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      // Select \"Hello\"\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n\n      // Paste multi-line text to replace selection\n      await currentMockInput.pasteBracketedText(\"Line 1\\nLine 2\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"Line 1\\nLine 2 World\")\n    })\n\n    it(\"should paste empty string without error\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      await currentMockInput.pasteBracketedText(\"\")\n\n      expect(editor.plainText).toBe(\"Test\")\n    })\n\n    it(\"should resize viewport when pasting multiline text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        maxHeight: 4,\n        wrapMode: \"none\",\n      })\n\n      editor.focus()\n\n      await renderOnce()\n      expect(editor.height).toBe(1)\n\n      await currentMockInput.pasteBracketedText(\"Line 1\\nLine 2\\nLine 3\")\n      await renderOnce()\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(editor.plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n      expect(viewport.height).toBeGreaterThan(1)\n    })\n\n    it(\"should paste Unicode characters (emoji, CJK)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      await currentMockInput.pasteBracketedText(\" 🌟世界👍\")\n\n      expect(editor.plainText).toBe(\"Hello 🌟世界👍\")\n    })\n\n    it(\"should strip ANSI sequences when inserting pasted text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      await currentMockInput.pasteBracketedText(\"text with \\x1b[31mred\\x1b[0m color\")\n\n      expect(editor.plainText).toBe(\"text with red color\")\n    })\n\n    it(\"should replace entire selection with pasted text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAA\\nBBBB\\nCCCC\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n      editor.gotoLine(1) // Go to BBBB line\n\n      // Select all of BBBB\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.getSelectedText()).toBe(\"BBBB\")\n\n      // Paste replacement\n      await currentMockInput.pasteBracketedText(\"XXXX\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"AAAA\\nXXXX\\nCCCC\")\n    })\n\n    it(\"should handle paste via handlePaste method directly\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      editor.handlePaste(new PasteEvent(pasteBytes(\" Content\")))\n\n      expect(editor.plainText).toBe(\"Test Content\")\n    })\n\n    it(\"should replace selection when using handlePaste directly\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      // Select \"World\"\n      const cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999)\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"left\", { shift: true })\n      }\n\n      expect(editor.getSelectedText()).toBe(\"World\")\n\n      // Use handlePaste directly\n      editor.handlePaste(new PasteEvent(pasteBytes(\"Universe\")))\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"Hello Universe\")\n    })\n\n    it(\"should support preventDefault on paste event\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onPaste: (event) => {\n          event.preventDefault()\n        },\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      await currentMockInput.pasteBracketedText(\" Prevented\")\n\n      expect(editor.plainText).toBe(\"Test\")\n    })\n\n    it(\"should pass full PasteEvent to onPaste handler\", async () => {\n      let receivedEvent: any = null\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onPaste: (event) => {\n          receivedEvent = event\n        },\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      await currentMockInput.pasteBracketedText(\" Event\")\n\n      expect(receivedEvent).not.toBeNull()\n      expect(receivedEvent.bytes).toEqual(pasteBytes(\" Event\"))\n      expect(typeof receivedEvent.preventDefault).toBe(\"function\")\n      expect(receivedEvent.defaultPrevented).toBe(false)\n      expect(editor.plainText).toBe(\"Test Event\")\n    })\n\n    it(\"should allow conditional paste prevention\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        onPaste: (event) => {\n          if (decodePasteBytes(event.bytes).includes(\"blocked\")) {\n            event.preventDefault()\n          }\n        },\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      await currentMockInput.pasteBracketedText(\" allowed\")\n      expect(editor.plainText).toBe(\"Test allowed\")\n\n      await currentMockInput.pasteBracketedText(\" blocked content\")\n      expect(editor.plainText).toBe(\"Test allowed\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.rendering.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\nimport { SyntaxStyle } from \"../../syntax-style.js\"\nimport { OptimizedBuffer } from \"../../buffer.js\"\nimport { fg, t } from \"../../lib/index.js\"\nimport { BoxRenderable, TextareaRenderable, TextRenderable } from \"../index.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\nlet captureFrame: () => string\nlet resize: (width: number, height: number) => void\n\ndescribe(\"Textarea - Rendering Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      captureCharFrame: captureFrame,\n      mockInput: currentMockInput,\n      resize,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Wrapping\", () => {\n    it(\"should move cursor down through all wrapped visual lines at column 0\", async () => {\n      // Create a long line that will wrap into multiple visual lines\n      const longText =\n        \"This is a very long line that will definitely wrap into multiple visual lines when the viewport is small\"\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: longText,\n        width: 20, // Small viewport to force wrapping\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n\n      // Set cursor at the beginning (0, 0) - logical position\n      editor.editBuffer.setCursor(0, 0)\n      await renderOnce()\n\n      // Get initial visual cursor position - should be at visual 0, 0\n      let visualCursor = editor.editorView.getVisualCursor()\n      expect(visualCursor.visualRow).toBe(0)\n      expect(visualCursor.visualCol).toBe(0)\n\n      // Verify we have multiple wrapped lines (should be 7 for this text)\n      const vlineCount = editor.editorView.getVirtualLineCount()\n      expect(vlineCount).toBeGreaterThan(1)\n\n      // Move down through each wrapped line - cursor should stay at column 0\n      for (let i = 1; i < vlineCount; i++) {\n        currentMockInput.pressArrow(\"down\")\n        await renderOnce()\n\n        visualCursor = editor.editorView.getVisualCursor()\n\n        // Cursor should have moved down to the next visual line\n        expect(visualCursor.visualRow).toBe(i)\n\n        // Cursor should be at column 0 (beginning of each wrapped line)\n        expect(visualCursor.visualCol).toBe(0)\n      }\n\n      // After moving through all wrapped lines, we should be at the last wrapped line\n      expect(visualCursor.visualRow).toBe(vlineCount - 1)\n      expect(visualCursor.visualCol).toBe(0)\n    })\n\n    it(\"should move cursor up through all wrapped visual lines at column 0\", async () => {\n      // Create a long line that will wrap into multiple visual lines\n      const longText =\n        \"This is a very long line that will definitely wrap into multiple visual lines when the viewport is small\"\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: longText,\n        width: 20, // Small viewport to force wrapping\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n\n      // Verify we have multiple wrapped lines\n      const vlineCount = editor.editorView.getVirtualLineCount()\n      expect(vlineCount).toBeGreaterThan(1)\n\n      // Start at the END of the line (which will be on the last wrapped visual line)\n      const eol = editor.editBuffer.getEOL()\n      editor.editBuffer.setCursor(eol.row, eol.col)\n      await renderOnce()\n\n      // Move to the beginning of the last wrapped line (column 0 of last visual line)\n      let visualCursor = editor.editorView.getVisualCursor()\n      const lastVisualRow = visualCursor.visualRow\n\n      // Set cursor to column 0 of the last wrapped visual line by finding its logical column\n      // Last visual line starts at a specific logical column - we need to find it\n      const lastVlineStartCol = editor.logicalCursor.col - visualCursor.visualCol\n      editor.editBuffer.setCursor(0, lastVlineStartCol)\n      await renderOnce()\n\n      visualCursor = editor.editorView.getVisualCursor()\n      expect(visualCursor.visualRow).toBe(lastVisualRow)\n      expect(visualCursor.visualCol).toBe(0)\n\n      // Now move UP through each wrapped line - cursor should stay at column 0\n      for (let i = lastVisualRow - 1; i >= 0; i--) {\n        currentMockInput.pressArrow(\"up\")\n        await renderOnce()\n\n        visualCursor = editor.editorView.getVisualCursor()\n\n        // Cursor should have moved up to the previous visual line\n        expect(visualCursor.visualRow).toBe(i)\n\n        // Cursor should be at column 0 (beginning of each wrapped line)\n        expect(visualCursor.visualCol).toBe(0)\n      }\n\n      // After moving through all wrapped lines, we should be at the first wrapped line\n      expect(visualCursor.visualRow).toBe(0)\n      expect(visualCursor.visualCol).toBe(0)\n    })\n\n    it(\"should handle wrap mode property\", async () => {\n      const longText = \"A\".repeat(100)\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: longText,\n        width: 20,\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      expect(editor.wrapMode).toBe(\"word\")\n      const wrappedCount = editor.editorView.getVirtualLineCount()\n      expect(wrappedCount).toBeGreaterThan(1)\n\n      editor.wrapMode = \"none\"\n      expect(editor.wrapMode).toBe(\"none\")\n      const unwrappedCount = editor.editorView.getVirtualLineCount()\n      expect(unwrappedCount).toBe(1)\n    })\n\n    it(\"should handle wrapMode changes\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello wonderful world\",\n        width: 12,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      expect(editor.wrapMode).toBe(\"char\")\n\n      editor.wrapMode = \"word\"\n      expect(editor.wrapMode).toBe(\"word\")\n    })\n\n    it(\"should render with tab indicator correctly\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\tTabbed\\nLine 2\\t\\tDouble tab\",\n        tabIndicator: \"→\",\n        tabIndicatorColor: RGBA.fromValues(0.5, 0.5, 0.5, 1),\n        width: 40,\n        height: 10,\n      })\n\n      await renderOnce()\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Height and Width Measurement\", () => {\n    it(\"should grow height for multiline text without wrapping\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\",\n        wrapMode: \"none\",\n        width: 40,\n      })\n\n      await renderOnce()\n\n      expect(editor.height).toBe(5)\n      expect(editor.width).toBeGreaterThanOrEqual(6)\n    })\n\n    it(\"should grow height for wrapped text when wrapping enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"This is a very long line that will definitely wrap to multiple lines\",\n        wrapMode: \"word\",\n        width: 15,\n      })\n\n      await renderOnce()\n\n      expect(editor.height).toBeGreaterThan(1)\n      expect(editor.width).toBeLessThanOrEqual(15)\n    })\n\n    it(\"should measure full width when wrapping is disabled and not constrained by parent\", async () => {\n      const longLine = \"This is a very long line that would wrap but wrapping is disabled\"\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: longLine,\n        wrapMode: \"none\",\n        position: \"absolute\",\n      })\n\n      await renderOnce()\n\n      expect(editor.height).toBe(1)\n      expect(editor.width).toBe(longLine.length)\n    })\n\n    it(\"should shrink height when deleting lines via value setter\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\",\n        width: 40,\n        wrapMode: \"none\",\n      })\n\n      editor.focus()\n      await renderOnce()\n      expect(editor.height).toBe(5)\n\n      // Remove lines by setting new value\n      editor.setText(\"Line 1\\nLine 2\")\n      await renderOnce()\n\n      expect(editor.height).toBe(2)\n      expect(editor.plainText).toBe(\"Line 1\\nLine 2\")\n    })\n\n    it(\"should update height when content changes from single to multiline\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Single line\",\n        wrapMode: \"none\",\n      })\n\n      await renderOnce()\n      expect(editor.height).toBe(1)\n\n      editor.setText(\"Line 1\\nLine 2\\nLine 3\")\n      await renderOnce()\n\n      expect(editor.height).toBe(3)\n    })\n\n    it(\"should grow height when pressing Enter to add newlines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Single line\",\n        width: 40,\n        wrapMode: \"none\",\n      })\n\n      // Add a second textarea below to verify layout reflow\n      const { textarea: belowEditor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Below\",\n        width: 40,\n      })\n\n      await renderOnce()\n      expect(editor.height).toBe(1)\n      const initialHeight = editor.height\n      const initialBelowY = belowEditor.y\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      // Press Enter 3 times to add 3 newlines\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Single line\\n\")\n      await renderOnce() // Wait for layout recalculation\n\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Single line\\n\\n\")\n      await renderOnce() // Wait for layout recalculation\n\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Single line\\n\\n\\n\")\n      await renderOnce() // Wait for layout recalculation\n\n      // The editor should have grown\n      expect(editor.height).toBeGreaterThan(initialHeight)\n      expect(editor.height).toBe(4) // 1 original line + 3 new lines\n      expect(editor.plainText).toBe(\"Single line\\n\\n\\n\")\n\n      // The element below should have moved down\n      expect(belowEditor.y).toBeGreaterThan(initialBelowY)\n      expect(belowEditor.y).toBe(4) // After the 4-line editor\n    })\n  })\n\n  describe(\"Unicode Support\", () => {\n    it(\"should handle emoji insertion\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n      editor.insertText(\" 🌟\")\n\n      expect(editor.plainText).toBe(\"Hello 🌟\")\n    })\n\n    it(\"should handle CJK characters\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n      editor.insertText(\" 世界\")\n\n      expect(editor.plainText).toBe(\"Hello 世界\")\n    })\n\n    it(\"should handle emoji cursor movement\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"A🌟B\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.logicalCursor.col).toBe(0)\n\n      currentMockInput.pressArrow(\"right\") // Move past A\n      expect(editor.logicalCursor.col).toBe(1)\n\n      currentMockInput.pressArrow(\"right\") // Move past emoji (2 cells)\n      expect(editor.logicalCursor.col).toBe(3)\n\n      currentMockInput.pressArrow(\"right\") // Move past B\n      expect(editor.logicalCursor.col).toBe(4)\n    })\n  })\n\n  describe(\"Content Property\", () => {\n    it(\"should update content programmatically\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Initial\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.setText(\"Updated\")\n      expect(editor.plainText).toBe(\"Updated\")\n      expect(editor.plainText).toBe(\"Updated\")\n    })\n\n    it(\"should reset cursor when content changes\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.gotoLine(9999) // Move to end\n      expect(editor.logicalCursor.col).toBe(11)\n\n      editor.setText(\"New\")\n      // Cursor should reset to start\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should clear text with clear() method\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      expect(editor.plainText).toBe(\"Hello World\")\n\n      editor.clear()\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should clear highlights with clear() method\", async () => {\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"highlight\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        syntaxStyle: style,\n      })\n\n      editor.addHighlightByCharRange({\n        start: 0,\n        end: 5,\n        styleId: styleId,\n        priority: 0,\n      })\n\n      const highlightsBefore = editor.getLineHighlights(0)\n      expect(highlightsBefore.length).toBeGreaterThan(0)\n\n      editor.clear()\n\n      expect(editor.plainText).toBe(\"\")\n      const highlightsAfter = editor.getLineHighlights(0)\n      expect(highlightsAfter.length).toBe(0)\n    })\n\n    it(\"should clear both text and highlights together\", async () => {\n      const style = SyntaxStyle.create()\n      const styleId = style.registerStyle(\"highlight\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        syntaxStyle: style,\n      })\n\n      editor.addHighlight(0, { start: 0, end: 6, styleId: styleId, priority: 0 })\n      editor.addHighlight(1, { start: 0, end: 6, styleId: styleId, priority: 0 })\n\n      expect(editor.plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n      expect(editor.getLineHighlights(0).length).toBe(1)\n      expect(editor.getLineHighlights(1).length).toBe(1)\n\n      editor.clear()\n\n      expect(editor.plainText).toBe(\"\")\n      expect(editor.getLineHighlights(0).length).toBe(0)\n      expect(editor.getLineHighlights(1).length).toBe(0)\n    })\n\n    it(\"should allow typing after clear()\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      expect(editor.plainText).toBe(\"Hello World\")\n\n      currentMockInput.pressKey(\"!\")\n      expect(editor.plainText).toBe(\"!Hello World\")\n\n      editor.clear()\n      expect(editor.plainText).toBe(\"\")\n\n      currentMockInput.pressKey(\"N\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"w\")\n      expect(editor.plainText).toBe(\"New\")\n\n      currentMockInput.pressKey(\" \")\n      currentMockInput.pressKey(\"T\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"x\")\n      currentMockInput.pressKey(\"t\")\n      expect(editor.plainText).toBe(\"New Text\")\n    })\n  })\n\n  describe(\"Rendering After Edits\", () => {\n    it(\"should render correctly after insert text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.insertText(\"x\")\n\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n      buffer.drawEditorView(editor.editorView, 0, 0)\n\n      expect(editor.plainText).toBe(\"xTest\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      buffer.destroy()\n    })\n\n    it(\"should render correctly after rapid edits\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      for (let i = 0; i < 5; i++) {\n        editor.insertText(\"a\")\n        buffer.drawEditorView(editor.editorView, 0, 0)\n      }\n\n      expect(editor.plainText).toBe(\"aaaaa\")\n      expect(editor.logicalCursor.col).toBe(5)\n\n      buffer.destroy()\n    })\n\n    it(\"should render correctly after newline\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      editor.newLine()\n      buffer.drawEditorView(editor.editorView, 0, 0)\n\n      expect(editor.plainText).toBe(\"Hello\\n\")\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(0)\n\n      buffer.destroy()\n    })\n\n    it(\"should render correctly after backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999)\n\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      editor.deleteCharBackward()\n      buffer.drawEditorView(editor.editorView, 0, 0)\n\n      expect(editor.plainText).toBe(\"Hell\")\n      expect(editor.logicalCursor.col).toBe(4)\n\n      buffer.destroy()\n    })\n\n    it(\"should render correctly with draw-edit-draw pattern\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      buffer.drawEditorView(editor.editorView, 0, 0)\n      editor.insertText(\"x\")\n      buffer.drawEditorView(editor.editorView, 0, 0)\n\n      expect(editor.plainText).toBe(\"xTest\")\n      expect(editor.logicalCursor.col).toBe(1)\n\n      buffer.destroy()\n    })\n\n    it(\"should render correctly after multiple text buffer modifications\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line1\\nLine2\\nLine3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      buffer.drawEditorView(editor.editorView, 0, 0)\n\n      editor.insertText(\"X\")\n      buffer.drawEditorView(editor.editorView, 0, 0)\n      expect(editor.plainText).toBe(\"XLine1\\nLine2\\nLine3\")\n\n      editor.newLine()\n      buffer.drawEditorView(editor.editorView, 0, 0)\n      expect(editor.plainText).toBe(\"X\\nLine1\\nLine2\\nLine3\")\n\n      editor.deleteCharBackward()\n      buffer.drawEditorView(editor.editorView, 0, 0)\n      expect(editor.plainText).toBe(\"XLine1\\nLine2\\nLine3\")\n\n      buffer.destroy()\n    })\n  })\n\n  describe(\"Viewport Scrolling\", () => {\n    it(\"should scroll viewport down when cursor moves below visible area\", async () => {\n      // Create editor with small viewport\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\",\n        width: 40,\n        height: 5, // Only 5 lines visible\n      })\n\n      editor.focus()\n\n      // Initial viewport should show lines 0-4\n      let viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBe(0)\n      expect(viewport.height).toBe(5)\n\n      // Move cursor to line 7 (beyond viewport)\n      editor.gotoLine(7)\n\n      // Viewport should have scrolled to keep cursor visible\n      viewport = editor.editorView.getViewport()\n      // With scroll margin of 0.2 (20% = 1 line), viewport should scroll to show line 7\n      // Expected: offsetY should be at least 3 (to show lines 3-7)\n      expect(viewport.offsetY).toBeGreaterThanOrEqual(3)\n    })\n\n    it(\"should scroll viewport up when cursor moves above visible area\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\",\n        width: 40,\n        height: 5,\n      })\n\n      editor.focus()\n\n      // Start at line 8\n      editor.gotoLine(8)\n\n      let viewport = editor.editorView.getViewport()\n      // Viewport should have automatically scrolled to show line 8\n      expect(viewport.offsetY).toBeGreaterThan(0)\n\n      // Now move to line 1 (above viewport)\n      editor.gotoLine(1)\n\n      viewport = editor.editorView.getViewport()\n      // Viewport should have scrolled up to show line 1\n      expect(viewport.offsetY).toBeLessThanOrEqual(1)\n    })\n\n    it(\"should scroll viewport when using arrow keys to move beyond visible area\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n      })\n\n      editor.focus()\n\n      let viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBe(0)\n\n      // Press down arrow 6 times to move beyond initial viewport\n      for (let i = 0; i < 6; i++) {\n        currentMockInput.pressArrow(\"down\")\n      }\n\n      viewport = editor.editorView.getViewport()\n      // Should have scrolled\n      expect(viewport.offsetY).toBeGreaterThan(0)\n    })\n\n    it(\"should maintain scroll margin when moving cursor\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        scrollMargin: 0.2, // 20% = 2 lines margin\n      })\n\n      editor.focus()\n\n      // Move to line 8 (near bottom of initial viewport)\n      editor.gotoLine(8)\n\n      let viewport = editor.editorView.getViewport()\n\n      // With 2-line margin, cursor at line 8 should trigger scroll\n      // so that line 8 is at most at position 8 in viewport\n      expect(viewport.offsetY).toBeGreaterThanOrEqual(0)\n    })\n\n    it(\"should handle viewport scrolling with text wrapping\", async () => {\n      const longLine = \"word \".repeat(50) // Creates line that will wrap\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 10 }, (_, i) => (i === 5 ? longLine : `Line ${i}`)).join(\"\\n\"),\n        width: 20,\n        height: 5,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n\n      // Move to the long line\n      editor.gotoLine(5)\n\n      const vlineCount = editor.editorView.getTotalVirtualLineCount()\n      expect(vlineCount).toBeGreaterThan(10) // Should be more due to wrapping\n\n      // Move to end of long line\n      const cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999) // Move to end of line\n\n      let viewport = editor.editorView.getViewport()\n\n      // Viewport should have scrolled to show cursor\n      // This is complex with wrapping - we need virtual line scrolling\n    })\n\n    it(\"should verify viewport follows cursor to line 10\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 8,\n      })\n\n      editor.focus()\n\n      // Move to line 10\n      editor.gotoLine(10)\n\n      const viewport = editor.editorView.getViewport()\n\n      // Viewport should have scrolled to show line 10\n      // With height=8 and scroll margin, line 10 should be visible\n      expect(viewport.offsetY).toBeGreaterThan(0)\n      expect(viewport.offsetY).toBeLessThanOrEqual(10)\n\n      // Line 10 should be within the viewport range\n      const viewportEnd = viewport.offsetY + viewport.height\n      expect(10).toBeGreaterThanOrEqual(viewport.offsetY)\n      expect(10).toBeLessThan(viewportEnd)\n    })\n\n    it(\"should track viewport offset as cursor moves through document\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 15 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 30,\n        height: 5,\n      })\n\n      editor.focus()\n\n      const viewportOffsets: number[] = []\n\n      // Track viewport offset at different cursor positions\n      for (const line of [0, 2, 4, 6, 8, 10, 12]) {\n        editor.gotoLine(line)\n        const viewport = editor.editorView.getViewport()\n        viewportOffsets.push(viewport.offsetY)\n      }\n\n      // Viewport should generally increase as cursor moves down\n      // (with possible plateaus when cursor is already visible)\n      const lastOffset = viewportOffsets[viewportOffsets.length - 1]\n      const firstOffset = viewportOffsets[0]\n      expect(lastOffset).toBeGreaterThan(firstOffset)\n\n      // At line 0, viewport should be at 0\n      expect(viewportOffsets[0]).toBe(0)\n\n      // At line 12, viewport should have scrolled\n      expect(viewportOffsets[viewportOffsets.length - 1]).toBeGreaterThan(5)\n    })\n\n    it(\"should scroll viewport when cursor moves with Page Up/Page Down\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 30 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      let viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBe(0)\n\n      // Move down 15 lines (more than viewport height)\n      for (let i = 0; i < 15; i++) {\n        editor.moveCursorDown()\n      }\n\n      viewport = editor.editorView.getViewport()\n\n      // Should have scrolled\n      expect(viewport.offsetY).toBeGreaterThan(0)\n      expect(editor.logicalCursor.row).toBe(15)\n    })\n\n    it(\"should scroll viewport down when pressing Enter repeatedly\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Start\",\n        width: 40,\n        height: 5,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      let viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBe(0)\n      expect(editor.logicalCursor.row).toBe(0)\n\n      // Press Enter 8 times to create 8 new lines\n      for (let i = 0; i < 8; i++) {\n        currentMockInput.pressEnter()\n      }\n\n      // After 8 Enters, we should have 9 lines total (0-8)\n      expect(editor.logicalCursor.row).toBe(8)\n\n      // Viewport should have scrolled to keep cursor visible\n      viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(0)\n\n      // Cursor should be visible in viewport\n      const cursorLine = editor.logicalCursor.row\n      expect(cursorLine).toBeGreaterThanOrEqual(viewport.offsetY)\n      expect(cursorLine).toBeLessThan(viewport.offsetY + viewport.height)\n    })\n\n    it(\"should scroll viewport up when pressing Backspace to delete characters and move up\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 15 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n      })\n\n      editor.focus()\n\n      // Start at line 10, move to end so we have characters to delete\n      editor.gotoLine(10)\n      let cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999) // Move to end of line\n\n      let viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(0)\n      const initialOffset = viewport.offsetY\n\n      // Delete all text and move cursor up to line 0\n      // Press Ctrl+A to go to start, then move to line 2, then backspace repeatedly\n      editor.gotoLine(0) // Move to start\n      editor.gotoLine(2)\n      cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999) // Move to end of line\n\n      // Now we're at line 2, and viewport should have scrolled up\n      viewport = editor.editorView.getViewport()\n\n      // Viewport should have scrolled up from initial position\n      expect(viewport.offsetY).toBeLessThan(initialOffset)\n      expect(editor.logicalCursor.row).toBe(2)\n    })\n\n    it(\"should scroll viewport when typing at end creates wrapped lines beyond viewport\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Start\",\n        width: 20,\n        height: 5,\n        wrapMode: \"word\",\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      let viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBe(0)\n\n      // Type enough to create multiple wrapped lines\n      const longText = \" word\".repeat(50)\n      for (const char of longText) {\n        currentMockInput.pressKey(char)\n      }\n\n      viewport = editor.editorView.getViewport()\n      const vlineCount = editor.editorView.getTotalVirtualLineCount()\n\n      // Should have created multiple virtual lines\n      expect(vlineCount).toBeGreaterThan(5)\n\n      // Viewport should have scrolled to keep cursor visible\n      // (This test may fail if virtual line scrolling isn't implemented yet)\n      expect(viewport.offsetY).toBeGreaterThanOrEqual(0)\n    })\n\n    it(\"should scroll viewport when using Enter to add lines, then Backspace to remove them\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 0\\nLine 1\\nLine 2\",\n        width: 40,\n        height: 5,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      let viewport = editor.editorView.getViewport()\n      const initialOffset = viewport.offsetY\n\n      // Add 6 new lines\n      for (let i = 0; i < 6; i++) {\n        currentMockInput.pressEnter()\n        currentMockInput.pressKey(\"X\")\n      }\n\n      // Should have scrolled down\n      viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(initialOffset)\n      const maxOffset = viewport.offsetY\n\n      // Now delete those lines by backspacing\n      for (let i = 0; i < 12; i++) {\n        // 12 backspaces to delete 6 \"X\\n\" pairs\n        currentMockInput.pressBackspace()\n      }\n\n      // Should have scrolled back up\n      viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeLessThan(maxOffset)\n    })\n\n    it(\"should show last line at bottom of viewport with no gap\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 10 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n      })\n\n      editor.focus()\n\n      // Move to last line (line 9)\n      editor.gotoLine(9)\n\n      let viewport = editor.editorView.getViewport()\n\n      // With 10 lines (0-9) and viewport height 5, max offset is 10 - 5 = 5\n      // Viewport should be at offset 5, showing lines 5-9 with line 9 at the bottom\n      expect(viewport.offsetY).toBe(5)\n\n      // Verify cursor line is visible\n      expect(9).toBeGreaterThanOrEqual(viewport.offsetY)\n      expect(9).toBeLessThan(viewport.offsetY + viewport.height)\n\n      // No gap - last visible line should be the last line of content\n      const lastVisibleLine = viewport.offsetY + viewport.height - 1\n      expect(lastVisibleLine).toBe(9)\n    })\n\n    it(\"should not scroll past end when document is smaller than viewport\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 0\\nLine 1\\nLine 2\",\n        width: 40,\n        height: 10, // Viewport bigger than content\n      })\n\n      editor.focus()\n\n      // Move to last line\n      editor.gotoLine(2)\n\n      let viewport = editor.editorView.getViewport()\n\n      // Should NOT scroll at all - content fits in viewport\n      expect(viewport.offsetY).toBe(0)\n    })\n  })\n\n  describe(\"Placeholder Support\", () => {\n    it(\"should display placeholder when empty\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Enter text here...\",\n      })\n\n      // plainText should return empty (placeholder is display-only)\n      expect(editor.plainText).toBe(\"\")\n      expect(editor.placeholder).toBe(\"Enter text here...\")\n    })\n\n    it(\"should hide placeholder when text is inserted\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Type something...\",\n      })\n\n      editor.focus()\n      expect(editor.plainText).toBe(\"\")\n\n      currentMockInput.pressKey(\"H\")\n      currentMockInput.pressKey(\"i\")\n\n      expect(editor.plainText).toBe(\"Hi\")\n    })\n\n    it(\"should reactivate placeholder when all text is deleted\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Test\",\n        width: 40,\n        height: 10,\n        placeholder: \"Empty buffer...\",\n      })\n\n      editor.focus()\n      expect(editor.plainText).toBe(\"Test\")\n\n      // Move to end, then delete all text\n      editor.gotoLine(9999)\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressBackspace()\n      }\n\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should update placeholder text dynamically\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"First placeholder\",\n      })\n\n      expect(editor.placeholder).toBe(\"First placeholder\")\n      expect(editor.plainText).toBe(\"\")\n\n      editor.placeholder = \"Second placeholder\"\n      expect(editor.placeholder).toBe(\"Second placeholder\")\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should update placeholder with styled text dynamically\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Colored placeholder\",\n      })\n\n      expect(editor.plainText).toBe(\"\")\n\n      // Update placeholder with styled text\n      editor.placeholder = t`${fg(\"#FF0000\")(\"Red placeholder\")}`\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should work with value property setter\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Empty state\",\n      })\n\n      expect(editor.plainText).toBe(\"\")\n\n      editor.setText(\"New content\")\n      expect(editor.plainText).toBe(\"New content\")\n\n      editor.setText(\"\")\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should handle placeholder with focus changes\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Click to edit\",\n      })\n\n      // Placeholder should show regardless of focus\n      expect(editor.plainText).toBe(\"\")\n\n      editor.focus()\n      expect(editor.plainText).toBe(\"\")\n\n      editor.blur()\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should handle typing after placeholder is shown\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Start typing...\",\n      })\n\n      editor.focus()\n      expect(editor.plainText).toBe(\"\")\n\n      currentMockInput.pressKey(\"H\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"o\")\n\n      expect(editor.plainText).toBe(\"Hello\")\n    })\n\n    it(\"should show placeholder after deleting all typed text\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Type here\",\n      })\n\n      editor.focus()\n\n      // Type \"Test\"\n      currentMockInput.pressKey(\"T\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"s\")\n      currentMockInput.pressKey(\"t\")\n      expect(editor.plainText).toBe(\"Test\")\n\n      // Backspace all\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressBackspace()\n      }\n\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should handle placeholder with newlines\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Line 1\\nLine 2\",\n      })\n\n      expect(editor.plainText).toBe(\"\")\n\n      editor.insertText(\"Content\")\n      expect(editor.plainText).toBe(\"Content\")\n    })\n\n    it(\"should handle null placeholder (no placeholder)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: null,\n      })\n\n      expect(editor.placeholder).toBe(null)\n      expect(editor.plainText).toBe(\"\")\n\n      editor.insertText(\"Content\")\n      expect(editor.plainText).toBe(\"Content\")\n    })\n\n    it(\"should clear placeholder when set to null\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Initial placeholder\",\n      })\n\n      expect(editor.placeholder).toBe(\"Initial placeholder\")\n      expect(editor.plainText).toBe(\"\")\n\n      editor.placeholder = null\n      expect(editor.placeholder).toBe(null)\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should reset placeholder when set to undefined\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n        placeholder: \"Initial placeholder\",\n      })\n\n      expect(editor.placeholder).toBe(\"Initial placeholder\")\n\n      expect(() => {\n        editor.placeholder = undefined\n      }).not.toThrow()\n\n      expect(editor.placeholder).toBe(null)\n      expect(editor.plainText).toBe(\"\")\n    })\n  })\n\n  describe(\"Textarea Content Snapshots\", () => {\n    it(\"should render basic text content correctly\", async () => {\n      await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        left: 5,\n        top: 3,\n        width: 20,\n        height: 5,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render multiline text content correctly\", async () => {\n      await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1: Hello\\nLine 2: World\\nLine 3: Testing\\nLine 4: Multiline\",\n        left: 1,\n        top: 1,\n        width: 30,\n        height: 10,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text with character wrapping correctly\", async () => {\n      await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"This is a very long text that should wrap to multiple lines when wrap is enabled\",\n        wrapMode: \"char\",\n        width: 15,\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text with word wrapping and punctuation\", async () => {\n      await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello,World.Test-Example/Path with various punctuation marks!\",\n        wrapMode: \"word\",\n        width: 12,\n        left: 0,\n        top: 0,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render placeholder when creating textarea with placeholder directly\", async () => {\n      await createTextareaRenderable(currentRenderer, renderOnce, {\n        placeholder: \"Enter text here...\",\n        left: 1,\n        top: 1,\n        width: 30,\n        height: 5,\n      })\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render placeholder when set programmatically after creation\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        left: 1,\n        top: 1,\n        width: 30,\n        height: 5,\n      })\n\n      textarea.placeholder = \"Type something...\"\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should resize correctly when typing return as first input with placeholder\", async () => {\n      resize(40, 10)\n\n      const container = new BoxRenderable(currentRenderer, {\n        border: true,\n        left: 1,\n        top: 1,\n      })\n      currentRenderer.root.add(container)\n\n      const textarea = new TextareaRenderable(currentRenderer, {\n        placeholder: \"Enter your message...\",\n        width: 30,\n        minHeight: 1,\n        maxHeight: 3,\n      })\n      container.add(textarea)\n\n      textarea.focus()\n      await renderOnce()\n\n      const frameBeforeEnter = captureFrame()\n      expect(textarea.height).toBe(1)\n\n      currentMockInput.pressEnter()\n      await renderOnce()\n      await renderOnce()\n\n      const frameAfterEnter = captureFrame()\n      expect(frameAfterEnter).toMatchSnapshot()\n      expect(textarea.height).toBe(2)\n      expect(textarea.plainText).toBe(\"\\n\")\n    })\n  })\n\n  describe(\"Layout Reflow on Size Change\", () => {\n    it(\"should reflow subsequent elements when textarea grows and shrinks\", async () => {\n      const { textarea: firstEditor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Short\",\n        width: 20,\n        wrapMode: \"word\",\n      })\n\n      const { textarea: secondEditor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"I am below the first textarea\",\n        width: 30,\n      })\n\n      await renderOnce()\n\n      // Initially, first editor is 1 line high\n      expect(firstEditor.height).toBe(1)\n      const initialSecondY = secondEditor.y\n      expect(initialSecondY).toBe(1) // Right after first editor\n\n      // Expand first editor with wrapped content\n      firstEditor.setText(\"This is a very long line that will wrap to multiple lines and push the second textarea down\")\n      await renderOnce()\n\n      // First editor should now be taller\n      expect(firstEditor.height).toBeGreaterThan(1)\n      // Second editor should have moved down\n      expect(secondEditor.y).toBeGreaterThan(initialSecondY)\n      const expandedSecondY = secondEditor.y\n\n      // Shrink first editor back\n      firstEditor.setText(\"Short again\")\n      await renderOnce()\n\n      // First editor should be 1 line again\n      expect(firstEditor.height).toBe(1)\n      // Second editor should have moved back up\n      expect(secondEditor.y).toBeLessThan(expandedSecondY)\n      expect(secondEditor.y).toBe(initialSecondY)\n    })\n  })\n\n  describe(\"Width/Height Setter Layout Tests\", () => {\n    it(\"should not shrink box when width is set via setter\", async () => {\n      resize(40, 10)\n\n      const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })\n      currentRenderer.root.add(container)\n\n      const row = new BoxRenderable(currentRenderer, { flexDirection: \"row\", width: \"100%\" })\n      container.add(row)\n\n      const indicator = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\" })\n      row.add(indicator)\n\n      const indicatorText = new TextRenderable(currentRenderer, { content: \">\" })\n      indicator.add(indicatorText)\n\n      const content = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      row.add(content)\n\n      const contentText = new TextRenderable(currentRenderer, { content: \"Content that takes up space\" })\n      content.add(contentText)\n\n      await renderOnce()\n\n      const initialIndicatorWidth = indicator.width\n\n      indicator.width = 5\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(indicator.width).toBe(5)\n      expect(content.width).toBeGreaterThan(0)\n      expect(content.width).toBeLessThan(30)\n    })\n\n    it(\"should not shrink box when height is set via setter in column layout with textarea\", async () => {\n      resize(30, 15)\n\n      const outerBox = new BoxRenderable(currentRenderer, { border: true, width: 25, height: 10 })\n      currentRenderer.root.add(outerBox)\n\n      const column = new BoxRenderable(currentRenderer, { flexDirection: \"column\", height: \"100%\" })\n      outerBox.add(column)\n\n      const header = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\" })\n      column.add(header)\n\n      const headerText = new TextRenderable(currentRenderer, { content: \"Header\" })\n      header.add(headerText)\n\n      const mainContent = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      column.add(mainContent)\n\n      const { textarea: mainTextarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line1\\nLine2\\nLine3\\nLine4\\nLine5\\nLine6\\nLine7\\nLine8\",\n      })\n      mainContent.add(mainTextarea)\n\n      const footer = new BoxRenderable(currentRenderer, { height: 2, backgroundColor: \"#00f\" })\n      column.add(footer)\n\n      const footerText = new TextRenderable(currentRenderer, { content: \"Footer\" })\n      footer.add(footerText)\n\n      await renderOnce()\n\n      header.height = 3\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(header.height).toBe(3)\n      expect(mainContent.height).toBeGreaterThan(0)\n      expect(footer.height).toBe(2)\n    })\n\n    it(\"should not shrink box when minWidth is set via setter\", async () => {\n      resize(40, 10)\n\n      const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })\n      currentRenderer.root.add(container)\n\n      const row = new BoxRenderable(currentRenderer, { flexDirection: \"row\", width: \"100%\" })\n      container.add(row)\n\n      const indicator = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\", flexShrink: 1 })\n      row.add(indicator)\n\n      const indicatorText = new TextRenderable(currentRenderer, { content: \">\" })\n      indicator.add(indicatorText)\n\n      const content = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      row.add(content)\n\n      const contentText = new TextRenderable(currentRenderer, { content: \"Content that takes up space\" })\n      content.add(contentText)\n\n      await renderOnce()\n\n      indicator.minWidth = 5\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n      expect(indicator.width).toBeGreaterThanOrEqual(5)\n      expect(content.width).toBeGreaterThan(0)\n    })\n\n    it(\"should not shrink box when minHeight is set via setter in column layout with textarea\", async () => {\n      resize(30, 15)\n\n      const outerBox = new BoxRenderable(currentRenderer, { border: true, width: 25, height: 10 })\n      currentRenderer.root.add(outerBox)\n\n      const column = new BoxRenderable(currentRenderer, { flexDirection: \"column\", height: \"100%\" })\n      outerBox.add(column)\n\n      const header = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\", flexShrink: 1 })\n      column.add(header)\n\n      const headerText = new TextRenderable(currentRenderer, { content: \"Header\" })\n      header.add(headerText)\n\n      const mainContent = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      column.add(mainContent)\n\n      const { textarea: mainTextarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line1\\nLine2\\nLine3\\nLine4\\nLine5\\nLine6\\nLine7\\nLine8\",\n      })\n      mainContent.add(mainTextarea)\n\n      const footer = new BoxRenderable(currentRenderer, { height: 2, backgroundColor: \"#00f\" })\n      column.add(footer)\n\n      const footerText = new TextRenderable(currentRenderer, { content: \"Footer\" })\n      footer.add(footerText)\n\n      await renderOnce()\n\n      header.minHeight = 3\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(header.height).toBeGreaterThanOrEqual(3)\n      expect(mainContent.height).toBeGreaterThan(0)\n      expect(footer.height).toBe(2)\n    })\n\n    it(\"should not shrink box when width is set from undefined via setter\", async () => {\n      resize(40, 10)\n\n      const container = new BoxRenderable(currentRenderer, { border: true, width: 30 })\n      currentRenderer.root.add(container)\n\n      const row = new BoxRenderable(currentRenderer, { flexDirection: \"row\", width: \"100%\" })\n      container.add(row)\n\n      const indicator = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\", flexShrink: 1 })\n      row.add(indicator)\n\n      const indicatorText = new TextRenderable(currentRenderer, { content: \">\" })\n      indicator.add(indicatorText)\n\n      const content = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexGrow: 1 })\n      row.add(content)\n\n      const contentText = new TextRenderable(currentRenderer, { content: \"Content that takes up space\" })\n      content.add(contentText)\n\n      await renderOnce()\n\n      indicator.width = 5\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(indicator.width).toBe(5)\n      expect(content.width).toBeGreaterThan(0)\n    })\n\n    it(\"should verify dimensions are actually respected under extreme pressure\", async () => {\n      resize(30, 10)\n\n      const container = new BoxRenderable(currentRenderer, { border: true, width: 20 })\n      currentRenderer.root.add(container)\n\n      const row = new BoxRenderable(currentRenderer, { flexDirection: \"row\", width: \"100%\" })\n      container.add(row)\n\n      const box1 = new BoxRenderable(currentRenderer, { backgroundColor: \"#f00\", flexShrink: 1 })\n      row.add(box1)\n      const text1 = new TextRenderable(currentRenderer, { content: \"AAA\" })\n      box1.add(text1)\n\n      const box2 = new BoxRenderable(currentRenderer, { backgroundColor: \"#0f0\", flexShrink: 1 })\n      row.add(box2)\n      const text2 = new TextRenderable(currentRenderer, { content: \"BBB\" })\n      box2.add(text2)\n\n      const box3 = new BoxRenderable(currentRenderer, { backgroundColor: \"#00f\", flexGrow: 1 })\n      row.add(box3)\n      const text3 = new TextRenderable(currentRenderer, { content: \"CCC\" })\n      box3.add(text3)\n\n      await renderOnce()\n\n      box1.width = 7\n      box2.minWidth = 5\n      await renderOnce()\n\n      expect(box1.width).toBe(7)\n      expect(box2.width).toBeGreaterThanOrEqual(5)\n      expect(box3.width).toBeGreaterThan(0)\n\n      const total = box1.width + box2.width + box3.width\n      expect(total).toBeLessThanOrEqual(18)\n    })\n  })\n\n  describe(\"Absolute Positioned Box with Textarea\", () => {\n    it(\"should render textarea in absolute positioned box with padding and borders correctly\", async () => {\n      resize(80, 20)\n\n      const notificationBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        justifyContent: \"center\",\n        alignItems: \"flex-start\",\n        top: 2,\n        right: 2,\n        maxWidth: Math.min(60, 80 - 6),\n        paddingLeft: 2,\n        paddingRight: 2,\n        paddingTop: 1,\n        paddingBottom: 1,\n        backgroundColor: \"#1e293b\",\n        borderColor: \"#3b82f6\",\n        border: [\"left\", \"right\"],\n      })\n\n      currentRenderer.root.add(notificationBox)\n\n      const outerWrapperBox = new BoxRenderable(currentRenderer, {\n        flexDirection: \"row\",\n        paddingBottom: 1,\n        paddingTop: 1,\n        paddingLeft: 2,\n        paddingRight: 2,\n        gap: 2,\n      })\n      notificationBox.add(outerWrapperBox)\n\n      const innerContentBox = new BoxRenderable(currentRenderer, {\n        flexGrow: 1,\n        gap: 1,\n      })\n      outerWrapperBox.add(innerContentBox)\n\n      const titleText = new TextRenderable(currentRenderer, {\n        content: \"Important Notification\",\n        attributes: 1,\n        marginBottom: 1,\n        fg: \"#f8fafc\",\n      })\n      innerContentBox.add(titleText)\n\n      const { textarea: messageTextarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue:\n          \"This is a longer message that should wrap properly within the absolutely positioned box with appropriate width constraints and padding applied.\",\n        textColor: \"#e2e8f0\",\n        wrapMode: \"word\",\n        width: \"100%\",\n      })\n      innerContentBox.add(messageTextarea)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(notificationBox.x).toBeGreaterThan(0)\n      expect(notificationBox.y).toBe(2)\n      expect(notificationBox.width).toBeGreaterThan(25)\n\n      expect(outerWrapperBox.width).toBeGreaterThan(15)\n      expect(innerContentBox.width).toBeGreaterThan(15)\n\n      expect(titleText.width).toBeGreaterThan(15)\n      expect(titleText.plainText).toBe(\"Important Notification\")\n      expect(titleText.height).toBe(1)\n\n      expect(messageTextarea.width).toBeGreaterThan(15)\n      expect(messageTextarea.height).toBeGreaterThanOrEqual(1)\n      expect(messageTextarea.plainText).toBe(\n        \"This is a longer message that should wrap properly within the absolutely positioned box with appropriate width constraints and padding applied.\",\n      )\n    })\n\n    it(\"should render textarea fully visible in absolute positioned box at various positions\", async () => {\n      resize(100, 25)\n\n      const topRightBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        top: 1,\n        right: 1,\n        maxWidth: 40,\n        paddingLeft: 1,\n        paddingRight: 1,\n        paddingTop: 0,\n        paddingBottom: 0,\n        backgroundColor: \"#fef2f2\",\n        borderColor: \"#ef4444\",\n        border: true,\n      })\n      currentRenderer.root.add(topRightBox)\n\n      const { textarea: topRightTextarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Error: File not found in the specified directory path\",\n        textColor: \"#991b1b\",\n        wrapMode: \"word\",\n        width: \"100%\",\n      })\n      topRightBox.add(topRightTextarea)\n\n      const bottomLeftBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        bottom: 1,\n        left: 1,\n        maxWidth: 35,\n        paddingLeft: 1,\n        paddingRight: 1,\n        backgroundColor: \"#f0fdf4\",\n        borderColor: \"#22c55e\",\n        border: [\"top\", \"bottom\"],\n      })\n      currentRenderer.root.add(bottomLeftBox)\n\n      const { textarea: bottomLeftTextarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Success: Operation completed successfully!\",\n        textColor: \"#166534\",\n        wrapMode: \"word\",\n        width: \"100%\",\n      })\n      bottomLeftBox.add(bottomLeftTextarea)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(topRightBox.y).toBe(1)\n      expect(topRightBox.x).toBeGreaterThan(50)\n      expect(topRightBox.width).toBeGreaterThan(30)\n      expect(topRightBox.width).toBeLessThanOrEqual(40)\n\n      expect(topRightTextarea.plainText).toBe(\"Error: File not found in the specified directory path\")\n      expect(topRightTextarea.width).toBeGreaterThan(25)\n      expect(topRightTextarea.width).toBeLessThanOrEqual(38)\n      expect(topRightTextarea.height).toBeGreaterThan(1)\n\n      expect(bottomLeftBox.x).toBe(1)\n      expect(bottomLeftBox.y).toBeGreaterThan(15)\n      expect(bottomLeftBox.width).toBeGreaterThan(25)\n      expect(bottomLeftBox.width).toBeLessThanOrEqual(35)\n\n      expect(bottomLeftTextarea.plainText).toBe(\"Success: Operation completed successfully!\")\n      expect(bottomLeftTextarea.width).toBeGreaterThan(25)\n      expect(bottomLeftTextarea.width).toBeLessThanOrEqual(33)\n      expect(bottomLeftTextarea.height).toBeGreaterThan(1)\n    })\n\n    it(\"should handle width:100% textarea in absolute positioned box with constrained maxWidth\", async () => {\n      resize(70, 15)\n\n      const constrainedBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        top: 5,\n        left: 10,\n        maxWidth: 50,\n        paddingLeft: 3,\n        paddingRight: 3,\n        paddingTop: 2,\n        paddingBottom: 2,\n        backgroundColor: \"#1e1e2e\",\n      })\n      currentRenderer.root.add(constrainedBox)\n\n      const { textarea: longTextarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue:\n          \"This is an extremely long piece of text that needs to wrap multiple times within the constrained width of the absolutely positioned container box with significant padding on all sides.\",\n        textColor: \"#cdd6f4\",\n        wrapMode: \"word\",\n        width: \"100%\",\n      })\n      constrainedBox.add(longTextarea)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(constrainedBox.width).toBeLessThanOrEqual(50)\n      expect(constrainedBox.width).toBeGreaterThan(40)\n      expect(constrainedBox.x).toBe(10)\n      expect(constrainedBox.y).toBe(5)\n\n      expect(longTextarea.width).toBeGreaterThan(35)\n      expect(longTextarea.width).toBeLessThanOrEqual(44)\n      expect(longTextarea.height).toBeGreaterThanOrEqual(5)\n      expect(longTextarea.plainText).toBe(\n        \"This is an extremely long piece of text that needs to wrap multiple times within the constrained width of the absolutely positioned container box with significant padding on all sides.\",\n      )\n    })\n\n    it(\"should render multiple textarea elements in absolute positioned box with proper spacing\", async () => {\n      resize(90, 20)\n\n      const infoBox = new BoxRenderable(currentRenderer, {\n        position: \"absolute\",\n        justifyContent: \"flex-start\",\n        alignItems: \"flex-start\",\n        top: 3,\n        right: 5,\n        maxWidth: 45,\n        paddingLeft: 2,\n        paddingRight: 2,\n        paddingTop: 1,\n        paddingBottom: 1,\n        backgroundColor: \"#eff6ff\",\n        borderColor: \"#3b82f6\",\n        border: true,\n      })\n      currentRenderer.root.add(infoBox)\n\n      const headerText = new TextRenderable(currentRenderer, {\n        content: \"System Update\",\n        attributes: 1,\n        fg: \"#1e40af\",\n      })\n      infoBox.add(headerText)\n\n      const { textarea: bodyTextarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"A new version is available with bug fixes and performance improvements.\",\n        textColor: \"#1e3a8a\",\n        wrapMode: \"word\",\n        width: \"100%\",\n        marginTop: 1,\n      })\n      infoBox.add(bodyTextarea)\n\n      const footerText = new TextRenderable(currentRenderer, {\n        content: \"Click to install\",\n        fg: \"#60a5fa\",\n        marginTop: 1,\n      })\n      infoBox.add(footerText)\n\n      await renderOnce()\n\n      const frame = captureFrame()\n      expect(frame).toMatchSnapshot()\n\n      expect(headerText.plainText).toBe(\"System Update\")\n      expect(bodyTextarea.plainText).toBe(\"A new version is available with bug fixes and performance improvements.\")\n      expect(footerText.plainText).toBe(\"Click to install\")\n\n      expect(infoBox.width).toBeGreaterThan(35)\n      expect(infoBox.width).toBeLessThanOrEqual(45)\n\n      expect(headerText.width).toBeGreaterThan(10)\n      expect(headerText.height).toBe(1)\n\n      expect(bodyTextarea.width).toBeGreaterThan(30)\n      expect(bodyTextarea.height).toBeGreaterThanOrEqual(2)\n\n      expect(footerText.width).toBeGreaterThan(10)\n      expect(footerText.height).toBe(1)\n\n      expect(bodyTextarea.y).toBeGreaterThan(headerText.y)\n      expect(footerText.y).toBeGreaterThan(bodyTextarea.y)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.scroll.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockMouse } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable, simulateFrames as _simulateFrames } from \"./renderable-test-utils.js\"\nimport { TestRecorder } from \"../../testing/test-recorder.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\nimport { ManualClock } from \"../../testing/manual-clock.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMouse: MockMouse\nlet clock: ManualClock\n\nconst simulateFrames = (ms: number, frameInterval?: number) => _simulateFrames(clock, renderOnce, ms, frameInterval)\n\ndescribe(\"Textarea - Scroll Tests\", () => {\n  beforeEach(async () => {\n    clock = new ManualClock()\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockMouse: currentMouse,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n      clock,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Mouse Selection Auto-Scroll\", () => {\n    it(\"should auto-scroll down when dragging selection below viewport\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      // Position at top\n      editor.editBuffer.gotoLine(0)\n      await renderOnce()\n\n      const viewportBefore = editor.editorView.getViewport()\n      expect(viewportBefore.offsetY).toBe(0)\n\n      // Start dragging from top\n      await currentMouse.pressDown(editor.x, editor.y)\n\n      // Move to bottom edge to trigger auto-scroll (keep button pressed)\n      await currentMouse.moveTo(editor.x + 5, editor.y + editor.height - 1)\n\n      // Simulate 1 second of frames for auto-scroll\n      await simulateFrames(1000)\n\n      const viewportAfter = editor.editorView.getViewport()\n\n      // Release mouse\n      await currentMouse.release(editor.x + 5, editor.y + editor.height - 1)\n\n      // Viewport should have scrolled down significantly\n      expect(viewportAfter.offsetY).toBeGreaterThan(viewportBefore.offsetY)\n\n      editor.destroy()\n    })\n\n    it(\"should set cursor to selection focus when selecting\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.editBuffer.gotoLine(0)\n      await renderOnce()\n\n      const cursorBefore = editor.logicalCursor\n\n      // Start selection and drag\n      await currentMouse.drag(editor.x, editor.y, editor.x + 10, editor.y + 5)\n      await renderOnce()\n\n      const cursorAfter = editor.logicalCursor\n\n      // Cursor should have moved to the selection focus position\n      expect(cursorAfter.row).toBeGreaterThan(cursorBefore.row)\n\n      editor.destroy()\n    })\n\n    it(\"should auto-scroll up when dragging selection above viewport\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 100 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      // Start somewhere in the middle so we can scroll up\n      editor.editBuffer.gotoLine(40)\n      await renderOnce()\n\n      const viewportBefore = editor.editorView.getViewport()\n      expect(viewportBefore.offsetY).toBeGreaterThan(0)\n\n      // Start dragging from within viewport\n      await currentMouse.pressDown(editor.x + 2, editor.y + 5)\n      // Drag to the top edge (within bounds) to trigger upward auto-scroll\n      await currentMouse.moveTo(editor.x + 2, editor.y)\n\n      // Simulate 1 second of frames for auto-scroll\n      await simulateFrames(1000)\n\n      const viewportAfter = editor.editorView.getViewport()\n\n      await currentMouse.release(editor.x + 2, editor.y)\n\n      expect(viewportAfter.offsetY).toBeLessThan(viewportBefore.offsetY)\n\n      editor.destroy()\n    })\n\n    it(\"should stop auto-scroll when selection ends\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 100 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.editBuffer.gotoLine(0)\n      await renderOnce()\n\n      await currentMouse.pressDown(editor.x + 2, editor.y)\n      await currentMouse.moveTo(editor.x + 2, editor.y + editor.height - 1)\n\n      // Simulate 1 second of auto-scroll\n      await simulateFrames(1000)\n\n      // End selection (mouse up) and render a few more frames\n      await currentMouse.release(editor.x + 2, editor.y + editor.height - 1)\n      await simulateFrames(200)\n\n      const viewportAfterRelease = editor.editorView.getViewport()\n\n      // If selection-end notifications work, viewport should remain stable\n      await simulateFrames(1000)\n\n      const viewportFinal = editor.editorView.getViewport()\n\n      expect(viewportFinal.offsetY).toBe(viewportAfterRelease.offsetY)\n\n      editor.destroy()\n    })\n  })\n\n  describe(\"Selection Focus Clamping\", () => {\n    it(\"should clamp cursor when dragging selection focus beyond buffer bounds\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 10 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      // Start selection at the top of the buffer\n      await currentMouse.pressDown(editor.x, editor.y)\n      await renderOnce()\n\n      // Drag selection far below the renderable's bounds (focusY way beyond buffer)\n      await currentMouse.moveTo(editor.x + 2, editor.y + 200)\n      await renderOnce()\n\n      const cursor = editor.logicalCursor\n      expect(cursor.row).toBe(9)\n\n      await currentMouse.release(editor.x + 2, editor.y + 200)\n      await renderOnce()\n\n      editor.destroy()\n    })\n  })\n\n  describe(\"Mouse Click Cursor Positioning\", () => {\n    it(\"should set cursor when clicking without dragging\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.editBuffer.gotoLine(0)\n      await renderOnce()\n\n      const cursorBefore = editor.logicalCursor\n      expect(cursorBefore.row).toBe(0)\n      expect(cursorBefore.col).toBe(0)\n\n      // Click on line 2, column 3\n      await currentMouse.click(editor.x + 3, editor.y + 2)\n      await renderOnce()\n\n      const cursorAfter = editor.logicalCursor\n      expect(cursorAfter.row).toBe(2)\n      expect(cursorAfter.col).toBe(3)\n\n      editor.destroy()\n    })\n\n    it(\"should set cursor when clicking on empty line\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 0\\n\\nLine 2\\n\\nLine 4\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      // Click on empty line 1\n      await currentMouse.click(editor.x + 5, editor.y + 1)\n      await renderOnce()\n\n      const cursor1 = editor.logicalCursor\n      expect(cursor1.row).toBe(1)\n      expect(cursor1.col).toBe(0) // Empty line, cursor at column 0\n\n      // Click on empty line 3\n      await currentMouse.click(editor.x + 10, editor.y + 3)\n      await renderOnce()\n\n      const cursor2 = editor.logicalCursor\n      expect(cursor2.row).toBe(3)\n      expect(cursor2.col).toBe(0)\n\n      editor.destroy()\n    })\n\n    it(\"should clamp cursor when clicking beyond line end\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Short\\nMedium line\\nVery long line here\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      // Click way beyond the end of \"Short\" (5 chars)\n      await currentMouse.click(editor.x + 20, editor.y)\n      await renderOnce()\n\n      const cursor = editor.logicalCursor\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBeLessThanOrEqual(5) // Clamped to line end\n\n      editor.destroy()\n    })\n\n    it(\"should set cursor when clicking with scrolled viewport\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      // Scroll to middle\n      editor.editBuffer.gotoLine(25)\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(10)\n\n      const offsetYBefore = viewport.offsetY\n\n      // Click on first visible line (which is line offsetY in absolute terms)\n      await currentMouse.click(editor.x + 3, editor.y)\n      await renderOnce()\n\n      const cursor = editor.logicalCursor\n      expect(cursor.row).toBe(offsetYBefore) // Should be the first visible line\n      expect(cursor.col).toBe(3)\n\n      editor.destroy()\n    })\n  })\n\n  describe(\"Mouse Wheel Scrolling\", () => {\n    it(\"should scroll down on mouse wheel down\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.editBuffer.gotoLine(0)\n      await renderOnce()\n\n      const viewportBefore = editor.editorView.getViewport()\n      expect(viewportBefore.offsetY).toBe(0)\n\n      // Scroll down by 3 lines\n      for (let i = 0; i < 3; i++) {\n        await currentMouse.scroll(editor.x + 5, editor.y + 5, \"down\")\n      }\n      await renderOnce()\n\n      const viewportAfter = editor.editorView.getViewport()\n      expect(viewportAfter.offsetY).toBe(3)\n\n      editor.destroy()\n    })\n\n    it(\"should move cursor into the viewport when wheel scrolling\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.editBuffer.gotoLine(0)\n      await renderOnce()\n\n      const cursorBefore = editor.logicalCursor\n      expect(cursorBefore.row).toBe(0)\n\n      // Scroll down a few lines\n      for (let i = 0; i < 3; i++) {\n        await currentMouse.scroll(editor.x + 5, editor.y + 5, \"down\")\n      }\n      await renderOnce()\n\n      const viewportAfter = editor.editorView.getViewport()\n      const cursorAfter = editor.logicalCursor\n\n      // Wheel scrolling uses setViewport(..., moveCursor=true), which moves the cursor to stay visible\n      expect(cursorAfter.row).toBeGreaterThan(cursorBefore.row)\n      expect(cursorAfter.row).toBeGreaterThanOrEqual(viewportAfter.offsetY)\n      expect(cursorAfter.row).toBeLessThan(viewportAfter.offsetY + viewportAfter.height)\n\n      editor.destroy()\n    })\n\n    it(\"should scroll up on mouse wheel up\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      // Start at line 20\n      editor.editBuffer.gotoLine(20)\n      await renderOnce()\n\n      const viewportBefore = editor.editorView.getViewport()\n      expect(viewportBefore.offsetY).toBeGreaterThan(10)\n      const offsetBefore = viewportBefore.offsetY\n\n      // Scroll up by 5 lines\n      for (let i = 0; i < 5; i++) {\n        await currentMouse.scroll(editor.x + 5, editor.y + 5, \"up\")\n      }\n      await renderOnce()\n\n      const viewportAfter = editor.editorView.getViewport()\n      expect(viewportAfter.offsetY).toBe(offsetBefore - 5)\n\n      editor.destroy()\n    })\n\n    it(\"should not scroll beyond top\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.editBuffer.gotoLine(2)\n      await renderOnce()\n\n      // Scroll up by 100 lines (should clamp to 0)\n      for (let i = 0; i < 100; i++) {\n        await currentMouse.scroll(editor.x + 5, editor.y + 5, \"up\")\n      }\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBe(0)\n\n      editor.destroy()\n    })\n\n    it(\"should not scroll beyond bottom\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      // Scroll down by 100 lines (should clamp to maxOffsetY = 20 - 10 = 10)\n      for (let i = 0; i < 100; i++) {\n        await currentMouse.scroll(editor.x + 5, editor.y + 5, \"down\")\n      }\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBe(10) // 20 lines - 10 viewport height\n\n      editor.destroy()\n    })\n\n    it(\"should allow mouse wheel scroll after selection auto-scroll\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 100 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      // Position at top\n      editor.editBuffer.gotoLine(0)\n      await renderOnce()\n\n      const viewportInitial = editor.editorView.getViewport()\n      expect(viewportInitial.offsetY).toBe(0)\n\n      // Drag selection from top to way below viewport to trigger auto-scroll to bottom\n      await currentMouse.pressDown(editor.x, editor.y)\n      await currentMouse.moveTo(editor.x + 5, editor.y + editor.height - 1)\n\n      // Simulate 2 seconds for auto-scroll to reach near the end\n      await simulateFrames(2000)\n\n      // Release mouse to complete selection\n      await currentMouse.release(editor.x + 5, editor.y + editor.height - 1)\n\n      const viewportAfterSelection = editor.editorView.getViewport()\n\n      // Should have scrolled down significantly\n      expect(viewportAfterSelection.offsetY).toBeGreaterThan(20)\n\n      // Now use mouse wheel to scroll all the way back up\n      for (let i = 0; i < 100; i++) {\n        await currentMouse.scroll(editor.x + 5, editor.y + 5, \"up\")\n      }\n      await renderOnce()\n\n      const viewportFinal = editor.editorView.getViewport()\n\n      // Should have scrolled all the way back to top\n      expect(viewportFinal.offsetY).toBe(0)\n\n      editor.destroy()\n    })\n  })\n\n  describe(\"Mouse Wheel Horizontal Scrolling\", () => {\n    it(\"should scroll horizontally with wheel when wrapping is disabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"A\".repeat(200),\n        width: 20,\n        height: 5,\n        wrapMode: \"none\",\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      // Keep a selection active so native updateBeforeRender doesn't auto-scroll viewport back to cursor\n      await currentMouse.drag(editor.x, editor.y, editor.x + 1, editor.y)\n      await renderOnce()\n\n      const viewportBefore = editor.editorView.getViewport()\n      expect(viewportBefore.offsetX).toBe(0)\n\n      for (let i = 0; i < 5; i++) {\n        await currentMouse.scroll(editor.x + 2, editor.y + 2, \"right\")\n      }\n      await renderOnce()\n\n      const viewportAfterRight = editor.editorView.getViewport()\n      expect(viewportAfterRight.offsetX).toBe(5)\n\n      for (let i = 0; i < 3; i++) {\n        await currentMouse.scroll(editor.x + 2, editor.y + 2, \"left\")\n      }\n      await renderOnce()\n\n      const viewportAfterLeft = editor.editorView.getViewport()\n      expect(viewportAfterLeft.offsetX).toBe(2)\n\n      editor.destroy()\n    })\n\n    it(\"should not scroll horizontally with wheel when wrapping is enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"A\".repeat(200),\n        width: 20,\n        height: 5,\n        wrapMode: \"word\",\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      // Keep selection active to avoid cursor-driven viewport changes\n      await currentMouse.drag(editor.x, editor.y, editor.x + 1, editor.y)\n      await renderOnce()\n\n      const viewportBefore = editor.editorView.getViewport()\n      expect(viewportBefore.offsetX).toBe(0)\n\n      for (let i = 0; i < 5; i++) {\n        await currentMouse.scroll(editor.x + 2, editor.y + 2, \"right\")\n      }\n      await renderOnce()\n\n      const viewportAfter = editor.editorView.getViewport()\n      expect(viewportAfter.offsetX).toBe(0)\n\n      editor.destroy()\n    })\n  })\n\n  describe(\"Viewport Offset After Resize\", () => {\n    it(\"should keep content at bottom when resizing from narrow wrapped to wide unwrapped\", async () => {\n      const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from(\n          { length: 15 },\n          (_, i) => `This is line ${i.toString().padStart(2, \"0\")} with enough text to wrap when narrow`,\n        ).join(\"\\n\"),\n        width: 10,\n        height: 10,\n        wrapMode: \"word\",\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      editor.focus()\n\n      // Scroll to the very bottom\n      editor.editBuffer.gotoLine(999)\n      await renderOnce()\n\n      const viewportAtBottom = editor.editorView.getViewport()\n      const totalVirtualLinesNarrow = editor.editorView.getTotalVirtualLineCount()\n\n      expect(viewportAtBottom.offsetY).toBeGreaterThan(10)\n\n      // Resize to much wider - this will unwrap most lines\n      editor.width = 80\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      const viewportAfterResize = editor.editorView.getViewport()\n      const totalVirtualLinesWide = editor.editorView.getTotalVirtualLineCount()\n\n      // After unwrapping, total lines should be much less (close to 15 logical lines)\n      expect(totalVirtualLinesWide).toBeLessThan(totalVirtualLinesNarrow)\n\n      // Content should still be at the bottom of the viewport\n      // The last line should be visible at the bottom\n      const maxOffsetYWide = Math.max(0, totalVirtualLinesWide - viewportAfterResize.height)\n      expect(viewportAfterResize.offsetY).toBe(maxOffsetYWide)\n\n      editor.destroy()\n    })\n\n    it(\"should clamp horizontal viewport offset when resizing wider with no wrap\", async () => {\n      const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"A\".repeat(200),\n        width: 20,\n        height: 10,\n        wrapMode: \"none\",\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      // Scroll horizontally to the far right\n      editor.focus()\n      for (let i = 0; i < 100; i++) {\n        editor.moveCursorRight()\n      }\n      await renderOnce()\n\n      const viewportNarrow = editor.editorView.getViewport()\n\n      expect(viewportNarrow.offsetX).toBeGreaterThan(50)\n\n      // Resize to much wider - viewport offsetX might now exceed valid range\n      editor.width = 250\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      const viewportWide = editor.editorView.getViewport()\n      const totalLineWidthWide = editor.lineInfo.lineWidthColsMax\n      const maxOffsetXWide = Math.max(0, totalLineWidthWide - viewportWide.width)\n\n      expect(viewportWide.offsetX).toBeLessThanOrEqual(maxOffsetXWide)\n\n      editor.destroy()\n    })\n\n    it(\"should allow scrolling and selecting last line immediately after resize from wide to narrow\", async () => {\n      const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from(\n          { length: 20 },\n          (_, i) =>\n            `Line ${i.toString().padStart(2, \"0\")} with enough text content to cause wrapping when viewport becomes narrow`,\n        ).join(\"\\n\"),\n        width: 80,\n        height: 10,\n        wrapMode: \"word\",\n        selectable: true,\n      })\n\n      await renderOnce()\n\n      // Resize to very narrow - this will cause heavy wrapping\n      editor.width = 10\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      const viewportAfterResize = editor.editorView.getViewport()\n      const totalVirtualLinesNarrow = editor.editorView.getTotalVirtualLineCount()\n\n      expect(totalVirtualLinesNarrow).toBeGreaterThan(20)\n\n      // Immediately try to scroll down to the bottom with mouse wheel\n      const maxOffsetY = Math.max(0, totalVirtualLinesNarrow - viewportAfterResize.height)\n\n      for (let i = 0; i < maxOffsetY + 20; i++) {\n        await currentMouse.scroll(editor.x + 2, editor.y + 2, \"down\")\n      }\n      await renderOnce()\n\n      const viewportAfterScroll = editor.editorView.getViewport()\n\n      // Should have scrolled close to the bottom (within scroll margin tolerance)\n      expect(viewportAfterScroll.offsetY).toBeGreaterThan(maxOffsetY - 5)\n      expect(viewportAfterScroll.offsetY).toBeLessThanOrEqual(maxOffsetY)\n\n      // Now try to select text on the last visible line\n      await currentMouse.drag(editor.x, editor.y + editor.height - 1, editor.x + 8, editor.y + editor.height - 1)\n      await renderOnce()\n\n      const selectedText = editor.getSelectedText()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(selectedText.length).toBeGreaterThan(0)\n\n      editor.destroy()\n    })\n\n    it(\"should continuously update selection during auto-scroll without mouse movement\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 100 }, (_, i) => `Line ${i.toString().padStart(2, \"0\")}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n        selectionBg: RGBA.fromValues(0, 1, 0, 1), // Bright green for easy detection\n      })\n\n      await renderOnce()\n\n      const recorder = new TestRecorder(currentRenderer, { recordBuffers: { bg: true } })\n\n      editor.editBuffer.gotoLine(0)\n      await renderOnce()\n\n      recorder.rec()\n\n      await currentMouse.pressDown(editor.x + 2, editor.y)\n      await currentMouse.moveTo(editor.x + 2, editor.y + editor.height - 1)\n\n      // Simulate 2 seconds of auto-scroll WITHOUT moving mouse\n      await simulateFrames(2000)\n\n      await currentMouse.release(editor.x + 2, editor.y + editor.height - 1)\n      await renderOnce()\n      recorder.stop()\n\n      const frames = recorder.recordedFrames\n      expect(frames.length).toBeGreaterThan(10)\n\n      const bufferWidth = currentRenderer.width\n      const selectionCellCounts: number[] = []\n\n      for (const frame of frames) {\n        if (!frame.buffers?.bg) continue\n\n        let selectedCells = 0\n        for (let y = editor.y; y < editor.y + editor.height; y++) {\n          for (let x = editor.x; x < editor.x + editor.width; x++) {\n            const bufferIdx = y * bufferWidth + x\n            const bgG = frame.buffers.bg[bufferIdx * 4 + 1]\n            if (Math.abs(bgG - 1.0) < 0.01) {\n              selectedCells++\n            }\n          }\n        }\n        selectionCellCounts.push(selectedCells)\n      }\n\n      const firstFrameSelection = selectionCellCounts[0] || 0\n      const lastFrameSelection = selectionCellCounts[selectionCellCounts.length - 1] || 0\n\n      const framesWithoutSelection = selectionCellCounts.filter((count, i) => i > 0 && count === 0).length\n\n      // Selection should expand and be continuously visible (no flicker)\n      expect(lastFrameSelection).toBeGreaterThan(firstFrameSelection)\n      expect(framesWithoutSelection).toBe(0)\n\n      editor.destroy()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.selection.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockMouse, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\nimport { RGBA } from \"../../lib/RGBA.js\"\nimport { OptimizedBuffer } from \"../../buffer.js\"\nimport { TextRenderable } from \"../Text.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMouse: MockMouse\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Selection Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockMouse: currentMouse,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Selection Support\", () => {\n    it(\"should support selection via mouse drag\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      expect(editor.hasSelection()).toBe(false)\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n\n      const sel = editor.getSelection()\n      expect(sel).not.toBe(null)\n      expect(sel!.start).toBe(0)\n      expect(sel!.end).toBe(5)\n\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n    })\n\n    it(\"should return selected text from multi-line content\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAA\\nBBBB\\nCCCC\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      await currentMouse.drag(editor.x + 2, editor.y, editor.x + 2, editor.y + 2)\n      await renderOnce()\n\n      const selectedText = editor.getSelectedText()\n      expect(selectedText).toBe(\"AA\\nBBBB\\nCC\")\n    })\n\n    it(\"should handle selection with viewport scrolling\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n        selectable: true,\n      })\n\n      editor.gotoLine(10)\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(0)\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 4, editor.y + 2)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n      expect(selectedText.length).toBeGreaterThan(0)\n      expect(selectedText).not.toContain(\"Line 0\")\n      expect(selectedText).not.toContain(\"Line 1\")\n      expect(selectedText).toContain(\"Line\")\n    })\n\n    it(\"should disable selection when selectable is false\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: false,\n      })\n\n      const shouldHandle = editor.shouldStartSelection(editor.x, editor.y)\n      expect(shouldHandle).toBe(false)\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should update selection when selectionBg/selectionFg changes\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        selectionBg: RGBA.fromValues(0, 0, 1, 1),\n      })\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n\n      editor.selectionBg = RGBA.fromValues(1, 0, 0, 1)\n      editor.selectionFg = RGBA.fromValues(1, 1, 1, 1)\n\n      expect(editor.hasSelection()).toBe(true)\n    })\n\n    it(\"should clear selection\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n\n      currentRenderer.clearSelection()\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should handle selection with wrapping enabled\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDEFGHIJKLMNOP\",\n        width: 10,\n        height: 10,\n        wrapMode: \"word\",\n        selectable: true,\n      })\n\n      const vlineCount = editor.editorView.getVirtualLineCount()\n      expect(vlineCount).toBe(2)\n\n      await currentMouse.drag(editor.x + 2, editor.y, editor.x + 3, editor.y + 1)\n      await renderOnce()\n\n      const sel = editor.getSelection()\n      expect(sel).not.toBe(null)\n      expect(sel!.start).toBe(2)\n      expect(sel!.end).toBe(13)\n    })\n\n    it(\"should handle reverse selection (drag from end to start)\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      await currentMouse.drag(editor.x + 11, editor.y, editor.x + 6, editor.y)\n      await renderOnce()\n\n      const sel = editor.getSelection()\n      expect(sel).not.toBe(null)\n      expect(sel!.start).toBe(6)\n      expect(sel!.end).toBe(11)\n\n      expect(editor.getSelectedText()).toBe(\"World\")\n    })\n\n    it(\"should render selection properly when drawing to buffer\", async () => {\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        selectionBg: RGBA.fromValues(0, 0, 1, 1),\n        selectionFg: RGBA.fromValues(1, 1, 1, 1),\n      })\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const sel = editor.getSelection()\n      expect(sel).not.toBe(null)\n      expect(sel!.start).toBe(0)\n      expect(sel!.end).toBe(5)\n\n      buffer.destroy()\n    })\n\n    it(\"should handle viewport-aware selection correctly\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 15 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n        selectable: true,\n        scrollMargin: 0,\n        scrollSpeed: 0,\n      })\n\n      editor.gotoLine(10)\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(0)\n\n      const expectedLineNumber = viewport.offsetY\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 6, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n\n      expect(selectedText).not.toContain(\"Line 0\")\n      expect(selectedText).not.toContain(\"Line 1\")\n      expect(selectedText).toContain(`Line ${expectedLineNumber}`)\n    })\n\n    it(\"should handle multi-line selection with viewport scrolling\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `AAAA${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n        selectable: true,\n      })\n\n      editor.gotoLine(8)\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(0)\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 4, editor.y + 2)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n\n      const line1 = `AAAA${viewport.offsetY}`\n      const line2 = `AAAA${viewport.offsetY + 1}`\n      const line3 = `AAAA${viewport.offsetY + 2}`\n\n      expect(selectedText).toContain(line1)\n      expect(selectedText).toContain(line2)\n      expect(selectedText).toContain(line3.substring(0, 4))\n    })\n\n    it(\"should handle horizontal scrolled selection without wrapping\", async () => {\n      const longLine = \"A\".repeat(100)\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: longLine,\n        width: 20,\n        height: 5,\n        wrapMode: \"none\",\n        selectable: true,\n      })\n\n      for (let i = 0; i < 50; i++) {\n        editor.moveCursorRight()\n      }\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetX).toBeGreaterThan(0)\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 10, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n\n      expect(selectedText).toBe(\"A\".repeat(10))\n\n      const sel = editor.getSelection()\n      expect(sel).not.toBe(null)\n      expect(sel!.start).toBeGreaterThanOrEqual(viewport.offsetX)\n    })\n\n    it(\"should render selection highlighting at correct screen position with viewport scroll\", async () => {\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 15 }, (_, i) => `Line${i}`).join(\"\\n\"),\n        width: 20,\n        height: 5,\n        selectable: true,\n        selectionBg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      editor.gotoLine(8)\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(0)\n\n      // Use manual drag steps instead of the drag helper to avoid timing issues\n      await currentMouse.pressDown(editor.x, editor.y)\n      await currentMouse.emitMouseEvent(\"drag\", editor.x + 5, editor.y)\n      await currentMouse.release(editor.x + 5, editor.y)\n      await renderOnce()\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const selectedText = editor.getSelectedText()\n      expect(selectedText).toBe(`Line${viewport.offsetY}`.substring(0, 5))\n\n      const { bg } = buffer.buffers\n      const bufferWidth = buffer.width\n\n      for (let cellX = editor.x; cellX < editor.x + 5; cellX++) {\n        const bufferIdx = editor.y * bufferWidth + cellX\n        const bgR = bg[bufferIdx * 4 + 0]\n        const bgG = bg[bufferIdx * 4 + 1]\n        const bgB = bg[bufferIdx * 4 + 2]\n\n        expect(Math.abs(bgR - 1.0)).toBeLessThan(0.01)\n        expect(Math.abs(bgG - 0.0)).toBeLessThan(0.01)\n        expect(Math.abs(bgB - 0.0)).toBeLessThan(0.01)\n      }\n\n      buffer.destroy()\n    })\n\n    it(\"should render selection correctly with empty lines between content\", async () => {\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAA\\n\\nBBBB\\n\\nCCCC\",\n        width: 40,\n        height: 10,\n        selectable: true,\n        selectionBg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      editor.focus()\n      editor.gotoLine(2)\n\n      for (let i = 0; i < 4; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.getSelectedText()).toBe(\"BBBB\")\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, 0, 0)\n\n      const { bg } = buffer.buffers\n      const bufferWidth = buffer.width\n\n      for (let cellX = 0; cellX < 4; cellX++) {\n        const bufferIdx = 2 * bufferWidth + cellX\n        const bgR = bg[bufferIdx * 4 + 0]\n        const bgG = bg[bufferIdx * 4 + 1]\n        const bgB = bg[bufferIdx * 4 + 2]\n\n        expect(Math.abs(bgR - 1.0)).toBeLessThan(0.01)\n        expect(Math.abs(bgG - 0.0)).toBeLessThan(0.01)\n        expect(Math.abs(bgB - 0.0)).toBeLessThan(0.01)\n      }\n\n      buffer.destroy()\n    })\n\n    it(\"should handle shift+arrow selection with viewport scrolling\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Line${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      editor.gotoLine(15)\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(10)\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n\n      expect(selectedText).toBe(\"Line1\")\n\n      const sel = editor.getSelection()\n      expect(sel).not.toBe(null)\n      expect(sel!.end - sel!.start).toBe(5)\n    })\n\n    it(\"should handle mouse drag selection with scrolled viewport using correct offset\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 30 }, (_, i) => `AAAA${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n        selectable: true,\n        scrollSpeed: 0,\n      })\n\n      editor.gotoLine(20)\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(15)\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 4, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n\n      expect(selectedText).not.toContain(\"AAAA0\")\n      expect(selectedText).not.toContain(\"AAAA1\")\n\n      const firstVisibleLineIdx = viewport.offsetY\n      const expectedText = `AAAA${firstVisibleLineIdx}`.substring(0, 4)\n      expect(selectedText).toBe(expectedText)\n    })\n\n    it(\"should handle multi-line mouse drag with scrolled viewport\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 30 }, (_, i) => `Line${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n        selectable: true,\n      })\n\n      editor.gotoLine(12)\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(7)\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y + 2)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n\n      expect(selectedText.startsWith(\"Line0\")).toBe(false)\n      expect(selectedText.startsWith(\"Line1\")).toBe(false)\n      expect(selectedText.startsWith(\"Line2\")).toBe(false)\n\n      const line1 = `Line${viewport.offsetY}`\n      const line2 = `Line${viewport.offsetY + 1}`\n      const line3 = `Line${viewport.offsetY + 2}`\n\n      expect(selectedText).toContain(line1)\n      expect(selectedText).toContain(line2)\n      expect(selectedText).toContain(line3.substring(0, 5))\n    })\n  })\n\n  describe(\"Shift+Arrow Key Selection\", () => {\n    it(\"should start selection with shift+right\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n      expect(editor.hasSelection()).toBe(false)\n\n      currentMockInput.pressArrow(\"right\", { shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"H\")\n    })\n\n    it(\"should extend selection with shift+right\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n    })\n\n    it(\"should extend a mouse selection with shift+right\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      await renderOnce()\n\n      expect(editor.getSelectedText()).toBe(\"Hello \")\n    })\n\n    it(\"should handle shift+left selection\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n      const cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999)\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"left\", { shift: true })\n      }\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"World\")\n    })\n\n    it(\"should select with shift+down\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressArrow(\"down\", { shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n      expect(selectedText).toBe(\"Line 1\")\n    })\n\n    it(\"should select with shift+up\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n      editor.gotoLine(2)\n\n      currentMockInput.pressArrow(\"up\", { shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n      expect(selectedText.includes(\"Line 2\")).toBe(true)\n    })\n\n    it(\"should select to line start with shift+home\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n      for (let i = 0; i < 6; i++) {\n        editor.moveCursorRight()\n      }\n\n      currentMockInput.pressKey(\"HOME\", { shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello W\")\n    })\n\n    it(\"should select to line end with shift+end\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"END\", { shift: true })\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello World\")\n    })\n\n    it(\"should clear selection when moving without shift\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.hasSelection()).toBe(true)\n\n      currentMockInput.pressArrow(\"right\")\n\n      expect(editor.hasSelection()).toBe(false)\n    })\n\n    it(\"should delete selected text with backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n      expect(editor.plainText).toBe(\"Hello World\")\n\n      currentMockInput.pressBackspace()\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\" World\")\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should delete selected text with delete key\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World!\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      const cursor = editor.logicalCursor\n      editor.editBuffer.setCursorToLineCol(cursor.row, 9999)\n      for (let i = 0; i < 6; i++) {\n        currentMockInput.pressArrow(\"left\", { shift: true })\n      }\n\n      expect(editor.getSelectedText()).toBe(\"World!\")\n      expect(editor.plainText).toBe(\"Hello World!\")\n\n      currentMockInput.pressKey(\"DELETE\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"Hello \")\n      expect(editor.logicalCursor.col).toBe(6)\n    })\n\n    it(\"should delete multi-line selection with backspace\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 10; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      const selectedText = editor.getSelectedText()\n      expect(editor.plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n\n      currentMockInput.pressBackspace()\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"e 2\\nLine 3\")\n      expect(editor.logicalCursor.col).toBe(0)\n      expect(editor.logicalCursor.row).toBe(0)\n    })\n\n    it(\"should delete entire line when selected with delete\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      currentMockInput.pressArrow(\"down\", { shift: true })\n\n      const selectedText = editor.getSelectedText()\n      expect(selectedText).toBe(\"Line 2\")\n\n      currentMockInput.pressKey(\"DELETE\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"Line 1\\nLine 3\")\n      expect(editor.logicalCursor.row).toBe(1)\n    })\n\n    it(\"should replace selected text when typing\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n\n      currentMockInput.pressKey(\"H\")\n      currentMockInput.pressKey(\"i\")\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.plainText).toBe(\"Hi World\")\n    })\n\n    it(\"should delete selected text via native deleteSelectedText API\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      await currentMouse.drag(editor.x, editor.y, editor.x + 5, editor.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n\n      editor.editorView.deleteSelectedText()\n      currentRenderer.clearSelection()\n      await renderOnce()\n\n      expect(editor.plainText).toBe(\" World\")\n      expect(editor.logicalCursor.row).toBe(0)\n      expect(editor.logicalCursor.col).toBe(0)\n      expect(editor.editorView.hasSelection()).toBe(false)\n    })\n    it(\"should maintain correct selection start when scrolling down with shift+down\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 20,\n        height: 5,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      for (let i = 0; i < 8; i++) {\n        currentMockInput.pressArrow(\"down\", { shift: true })\n        await renderOnce()\n      }\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(0)\n\n      const sel = editor.getSelection()\n      expect(sel).not.toBe(null)\n      expect(sel!.start).toBe(0)\n    })\n\n    it(\"should not start selection in textarea when clicking in text renderable below after scrolling\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Textarea Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n        selectable: true,\n        top: 0,\n      })\n\n      const textBelow = new TextRenderable(currentRenderer, {\n        id: \"text-below\",\n        content: \"This is text below the textarea\",\n        selectable: true,\n        top: 5,\n        left: 0,\n        width: 40,\n        height: 1,\n      })\n      currentRenderer.root.add(textBelow)\n\n      editor.focus()\n\n      editor.gotoBufferEnd()\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(10)\n\n      await currentMouse.drag(textBelow.x, textBelow.y, textBelow.x + 10, textBelow.y)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(false)\n      expect(editor.getSelectedText()).toBe(\"\")\n\n      expect(textBelow.hasSelection()).toBe(true)\n      expect(textBelow.getSelectedText()).toBe(\"This is te\")\n\n      textBelow.destroy()\n    })\n\n    it(\"should maintain selection in both renderables when dragging from text-below up into textarea\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Textarea Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 5,\n        selectable: true,\n        top: 0,\n      })\n\n      const textBelow = new TextRenderable(currentRenderer, {\n        id: \"text-below\",\n        content: \"This is text below the textarea\",\n        selectable: true,\n        top: 5,\n        left: 0,\n        width: 40,\n        height: 1,\n      })\n      currentRenderer.root.add(textBelow)\n\n      editor.focus()\n\n      editor.gotoBufferEnd()\n      await renderOnce()\n\n      const viewport = editor.editorView.getViewport()\n      expect(viewport.offsetY).toBeGreaterThan(10)\n\n      const startX = textBelow.x + 5\n      const startY = textBelow.y\n      const endX = editor.x + 15\n      const endY = editor.y + 3\n\n      await currentMouse.drag(startX, startY, endX, endY)\n      await renderOnce()\n\n      expect(textBelow.hasSelection()).toBe(true)\n      const textBelowSelection = textBelow.getSelectedText()\n      expect(textBelowSelection.length).toBeGreaterThan(0)\n\n      expect(editor.hasSelection()).toBe(true)\n      const textareaSelection = editor.getSelectedText()\n      expect(textareaSelection.length).toBeGreaterThan(0)\n\n      textBelow.destroy()\n    })\n\n    it(\"should handle cross-renderable selection from bottom-left text to top-right text\", async () => {\n      const { BoxRenderable } = await import(\"../Box\")\n\n      const bottomText = new TextRenderable(currentRenderer, {\n        id: \"bottom-instructions\",\n        content: \"Click and drag to select text across any elements\",\n        left: 5,\n        top: 20,\n        width: 50,\n        height: 1,\n        selectable: true,\n      })\n      currentRenderer.root.add(bottomText)\n\n      const rightBox = new BoxRenderable(currentRenderer, {\n        id: \"right-box\",\n        left: 50,\n        top: 5,\n        width: 30,\n        height: 10,\n        padding: 1,\n        flexDirection: \"column\",\n      })\n      currentRenderer.root.add(rightBox)\n\n      const codeText1 = new TextRenderable(currentRenderer, {\n        id: \"code-line-1\",\n        content: \"function handleSelection() {\",\n        selectable: true,\n      })\n      rightBox.add(codeText1)\n\n      const codeText2 = new TextRenderable(currentRenderer, {\n        id: \"code-line-2\",\n        content: \"  const selected = getText()\",\n        selectable: true,\n      })\n      rightBox.add(codeText2)\n\n      const codeText3 = new TextRenderable(currentRenderer, {\n        id: \"code-line-3\",\n        content: \"  console.log(selected)\",\n        selectable: true,\n      })\n      rightBox.add(codeText3)\n\n      const codeText4 = new TextRenderable(currentRenderer, {\n        id: \"code-line-4\",\n        content: \"}\",\n        selectable: true,\n      })\n      rightBox.add(codeText4)\n\n      await renderOnce()\n\n      const startX = bottomText.x + 10\n      const startY = bottomText.y\n      const endX = codeText2.x + 15\n      const endY = codeText2.y\n\n      await currentMouse.drag(startX, startY, endX, endY)\n      await renderOnce()\n\n      expect(bottomText.hasSelection()).toBe(true)\n      const bottomSelected = bottomText.getSelectedText()\n      expect(bottomSelected).toBe(\"Click and d\")\n\n      expect(codeText1.hasSelection()).toBe(false)\n\n      expect(codeText2.hasSelection()).toBe(true)\n      const codeText2Selected = codeText2.getSelectedText()\n      const codeText2Content = \"  const selected = getText()\"\n      expect(codeText2Selected).toBe(codeText2Content.substring(0, 15))\n\n      bottomText.destroy()\n      rightBox.destroy()\n    })\n  })\n\n  describe(\"Selection After Resize\", () => {\n    it(\"should maintain selection correctly after resize - same text selected and rendered properly\", async () => {\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 30 }, (_, i) => `Line ${i.toString().padStart(2, \"0\")}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n        selectionBg: RGBA.fromValues(0, 1, 0, 1),\n        selectionFg: RGBA.fromValues(0, 0, 0, 1),\n      })\n\n      editor.gotoLine(5)\n      await renderOnce()\n\n      await currentMouse.drag(editor.x + 5, editor.y + 2, editor.x + 10, editor.y + 4)\n      await renderOnce()\n\n      const selectedTextBefore = editor.getSelectedText()\n      const selectionBefore = editor.getSelection()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(selectedTextBefore).toBeTruthy()\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const { bg: bgBefore } = buffer.buffers\n      const bufferWidth = buffer.width\n\n      const selectedCellsBefore: Array<{ x: number; y: number }> = []\n      for (let y = 0; y < editor.height; y++) {\n        for (let x = 0; x < editor.width; x++) {\n          const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)\n          const bgG = bgBefore[bufferIdx * 4 + 1]\n          if (Math.abs(bgG - 1.0) < 0.01) {\n            selectedCellsBefore.push({ x, y })\n          }\n        }\n      }\n\n      expect(selectedCellsBefore.length).toBeGreaterThan(0)\n\n      editor.width = 50\n      editor.height = 15\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      const selectedTextAfter = editor.getSelectedText()\n      const selectionAfter = editor.getSelection()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(selectedTextAfter).toBe(selectedTextBefore)\n      expect(selectionAfter?.start).toBe(selectionBefore?.start)\n      expect(selectionAfter?.end).toBe(selectionBefore?.end)\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const { bg: bgAfter } = buffer.buffers\n\n      const selectedCellsAfter: Array<{ x: number; y: number }> = []\n      for (let y = 0; y < editor.height; y++) {\n        for (let x = 0; x < editor.width; x++) {\n          const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)\n          const bgG = bgAfter[bufferIdx * 4 + 1]\n          if (Math.abs(bgG - 1.0) < 0.01) {\n            selectedCellsAfter.push({ x, y })\n          }\n        }\n      }\n\n      expect(selectedCellsAfter.length).toBeGreaterThan(0)\n      expect(selectedCellsAfter.length).toBe(selectedCellsBefore.length)\n\n      buffer.destroy()\n      editor.destroy()\n    })\n\n    it(\"should maintain exact same text selected after wrap width changes\", async () => {\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"AAAAA BBBBB CCCCC DDDDD EEEEE FFFFF GGGGG HHHHH\",\n        width: 50,\n        height: 10,\n        wrapMode: \"word\",\n        selectable: true,\n        selectionBg: RGBA.fromValues(1, 0, 1, 1),\n        selectionFg: RGBA.fromValues(1, 1, 1, 1),\n      })\n\n      await renderOnce()\n\n      await currentMouse.drag(editor.x + 6, editor.y, editor.x + 17, editor.y)\n      await renderOnce()\n\n      const selectedTextBefore = editor.getSelectedText()\n      const selectionBefore = editor.getSelection()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(selectedTextBefore).toBe(\"BBBBB CCCCC\")\n\n      editor.width = 15\n      editor.height = 15\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      const selectedTextAfterNarrow = editor.getSelectedText()\n      const selectionAfterNarrow = editor.getSelection()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(selectedTextAfterNarrow).toBe(\"BBBBB CCCCC\")\n      expect(selectionAfterNarrow?.start).toBe(selectionBefore?.start)\n      expect(selectionAfterNarrow?.end).toBe(selectionBefore?.end)\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const { bg: bgNarrow } = buffer.buffers\n      const bufferWidth = buffer.width\n\n      let selectedCellsNarrow = 0\n      for (let y = 0; y < editor.height; y++) {\n        for (let x = 0; x < editor.width; x++) {\n          const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)\n          const bgR = bgNarrow[bufferIdx * 4 + 0]\n          const bgB = bgNarrow[bufferIdx * 4 + 2]\n          if (Math.abs(bgR - 1.0) < 0.01 && Math.abs(bgB - 1.0) < 0.01) {\n            selectedCellsNarrow++\n          }\n        }\n      }\n\n      expect(selectedCellsNarrow).toBe(11)\n\n      editor.width = 50\n      editor.height = 10\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      const selectedTextAfterWide = editor.getSelectedText()\n      const selectionAfterWide = editor.getSelection()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(selectedTextAfterWide).toBe(\"BBBBB CCCCC\")\n      expect(selectionAfterWide?.start).toBe(selectionBefore?.start)\n      expect(selectionAfterWide?.end).toBe(selectionBefore?.end)\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const { bg: bgWide } = buffer.buffers\n\n      let selectedCellsWide = 0\n      for (let y = 0; y < editor.height; y++) {\n        for (let x = 0; x < editor.width; x++) {\n          const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)\n          const bgR = bgWide[bufferIdx * 4 + 0]\n          const bgB = bgWide[bufferIdx * 4 + 2]\n          if (Math.abs(bgR - 1.0) < 0.01 && Math.abs(bgB - 1.0) < 0.01) {\n            selectedCellsWide++\n          }\n        }\n      }\n\n      expect(selectedCellsWide).toBe(11)\n\n      buffer.destroy()\n      editor.destroy()\n    })\n\n    it(\"should handle resize during active mouse selection drag\", async () => {\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n        selectionBg: RGBA.fromValues(0, 1, 1, 1),\n      })\n\n      await renderOnce()\n\n      await currentMouse.pressDown(editor.x + 2, editor.y + 1)\n      await currentMouse.moveTo(editor.x + 8, editor.y + 3)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedBeforeResize = editor.getSelectedText()\n\n      editor.width = 30\n      editor.height = 8\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      await currentMouse.moveTo(editor.x + 10, editor.y + 2)\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n\n      await currentMouse.release(editor.x + 10, editor.y + 2)\n      await renderOnce()\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const { bg: bgAfterResize } = buffer.buffers\n      const bufferWidth = buffer.width\n\n      let selectedCellsAfterResize = 0\n      for (let y = 0; y < editor.height; y++) {\n        for (let x = 0; x < editor.width; x++) {\n          const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)\n          const bgG = bgAfterResize[bufferIdx * 4 + 1]\n          const bgB = bgAfterResize[bufferIdx * 4 + 2]\n          if (Math.abs(bgG - 1.0) < 0.01 && Math.abs(bgB - 1.0) < 0.01) {\n            selectedCellsAfterResize++\n          }\n        }\n      }\n\n      expect(selectedCellsAfterResize).toBeGreaterThan(0)\n\n      buffer.destroy()\n      editor.destroy()\n    })\n\n    it(\"should maintain selection correctly when renderable position changes during resize\", async () => {\n      const buffer = OptimizedBuffer.create(80, 24, \"wcwidth\")\n\n      const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 20 }, (_, i) => `Line ${i.toString().padStart(2, \"0\")}`).join(\"\\n\"),\n        left: 10,\n        top: 5,\n        width: 40,\n        height: 10,\n        selectable: true,\n        selectionBg: RGBA.fromValues(1, 1, 0, 1),\n        selectionFg: RGBA.fromValues(0, 0, 0, 1),\n      })\n\n      await renderOnce()\n\n      const initialX = editor.x\n      const initialY = editor.y\n\n      await currentMouse.drag(editor.x + 5, editor.y + 2, editor.x + 10, editor.y + 4)\n      await renderOnce()\n\n      const selectedTextBefore = editor.getSelectedText()\n      const selectionBefore = editor.getSelection()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(selectedTextBefore).toBeTruthy()\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const { bg: bgBefore } = buffer.buffers\n      const bufferWidth = buffer.width\n\n      const selectedCellsBeforeCount = countSelectedCells(bgBefore, bufferWidth, editor, 1, 1, 0)\n      expect(selectedCellsBeforeCount).toBeGreaterThan(0)\n\n      editor.left = 20\n      editor.top = 10\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      const newX = editor.x\n      const newY = editor.y\n\n      expect(newX).not.toBe(initialX)\n      expect(newY).not.toBe(initialY)\n\n      const selectedTextAfter = editor.getSelectedText()\n      const selectionAfter = editor.getSelection()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(selectedTextAfter).toBe(selectedTextBefore)\n      expect(selectionAfter?.start).toBe(selectionBefore?.start)\n      expect(selectionAfter?.end).toBe(selectionBefore?.end)\n\n      buffer.clear(RGBA.fromValues(0, 0, 0, 1))\n      buffer.drawEditorView(editor.editorView, editor.x, editor.y)\n\n      const { bg: bgAfter } = buffer.buffers\n      const selectedCellsAfterCount = countSelectedCells(bgAfter, bufferWidth, editor, 1, 1, 0)\n\n      expect(selectedCellsAfterCount).toBe(selectedCellsBeforeCount)\n      expect(selectedCellsAfterCount).toBeGreaterThan(0)\n\n      buffer.destroy()\n      editor.destroy()\n    })\n\n    it(\"should keep cursor within textarea bounds after resize causes wrapping with scrolled selection\", async () => {\n      const { textarea: editor, root } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from(\n          { length: 50 },\n          (_, i) =>\n            `This is a long line ${i.toString().padStart(2, \"0\")} with enough text to cause wrapping when narrow`,\n        ).join(\"\\n\"),\n        width: 60,\n        height: 10,\n        top: 0,\n        wrapMode: \"word\",\n        selectable: true,\n        showCursor: true,\n      })\n\n      const textBelow = new TextRenderable(currentRenderer, {\n        id: \"text-below\",\n        content: \"Element below textarea\",\n        top: 10,\n        left: 0,\n      })\n      currentRenderer.root.add(textBelow)\n\n      await renderOnce()\n\n      editor.focus()\n      editor.gotoLine(15)\n      await renderOnce()\n\n      await currentMouse.drag(editor.x + 5, editor.y + 3, editor.x + 10, editor.y + 9)\n      await renderOnce()\n\n      const viewportAfterSelection = editor.editorView.getViewport()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(viewportAfterSelection.offsetY).toBeGreaterThan(0)\n\n      editor.width = 8\n      root.yogaNode.calculateLayout(80, 24)\n      await renderOnce()\n\n      const viewportAfterResize = editor.editorView.getViewport()\n      const cursorAfterResize = editor.visualCursor\n\n      expect(cursorAfterResize.visualRow).toBeGreaterThanOrEqual(0)\n      expect(cursorAfterResize.visualRow).toBeLessThan(editor.height)\n      expect(cursorAfterResize.visualCol).toBeGreaterThanOrEqual(0)\n      expect(cursorAfterResize.visualCol).toBeLessThan(editor.width)\n\n      textBelow.destroy()\n      editor.destroy()\n    })\n  })\n\n  describe(\"Selection Preserved on Viewport Scroll\", () => {\n    it(\"should preserve selection when scrolling viewport\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: Array.from({ length: 50 }, (_, i) => `Line ${i}`).join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n      await renderOnce()\n\n      // Select all text using keyboard (Cmd+Shift+Down)\n      currentMockInput.pressKey(\"ARROW_DOWN\", { shift: true, super: true })\n      await renderOnce()\n\n      const selectionBefore = editor.getSelection()\n      const selectedTextBefore = editor.getSelectedText()\n\n      expect(selectionBefore).not.toBeNull()\n      expect(selectedTextBefore).toContain(\"Line 0\")\n      expect(selectedTextBefore).toContain(\"Line 49\")\n\n      // Start renderer to simulate real app with continuous render loop\n      currentRenderer.start()\n\n      // Scroll up with mouse wheel\n      await currentMouse.scroll(editor.x, editor.y + 1, \"up\")\n      await Bun.sleep(100)\n\n      const selectionAfter = editor.getSelection()\n      const selectedTextAfter = editor.getSelectedText()\n\n      currentRenderer.pause()\n\n      // Selection should not change when scrolling viewport\n      expect(selectionAfter).not.toBeNull()\n      expect(selectionAfter!.start).toBe(selectionBefore!.start)\n      expect(selectionAfter!.end).toBe(selectionBefore!.end)\n      expect(selectedTextAfter).toBe(selectedTextBefore)\n\n      editor.destroy()\n    })\n  })\n\n  describe(\"Keyboard Selection with Viewport Scrolling\", () => {\n    it(\"should select to buffer home after shift+end then shift+home when scrolled\", async () => {\n      const lines = Array.from({ length: 30 }, (_, i) => `Line ${i.toString().padStart(2, \"0\")}`)\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: lines.join(\"\\n\"),\n        width: 40,\n        height: 6,\n        selectable: true,\n      })\n\n      editor.focus()\n      await renderOnce()\n\n      for (let i = 0; i < 3; i++) {\n        await currentMouse.scroll(editor.x + 2, editor.y + 2, \"down\")\n      }\n      await renderOnce()\n\n      const viewportAfterScroll = editor.editorView.getViewport()\n      expect(viewportAfterScroll.offsetY).toBeGreaterThan(0)\n      expect(editor.logicalCursor.row).toBeGreaterThan(0)\n\n      currentMockInput.pressKey(\"END\", { shift: true })\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n\n      currentMockInput.pressKey(\"HOME\", { shift: true })\n      await renderOnce()\n\n      const selection = editor.getSelection()\n      expect(selection).not.toBeNull()\n      expect(selection!.start).toBe(0)\n\n      const selectedText = editor.getSelectedText()\n      expect(selectedText.startsWith(\"Line 00\")).toBe(true)\n      expect(selectedText).not.toContain(\"Line 29\")\n    })\n\n    it(\"should allow shift+end after shift+home from a mid-buffer cursor\", async () => {\n      const lines = Array.from({ length: 30 }, (_, i) => `Line ${i.toString().padStart(2, \"0\")}`)\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: lines.join(\"\\n\"),\n        width: 40,\n        height: 6,\n        selectable: true,\n      })\n\n      editor.focus()\n      editor.gotoLine(10)\n      await renderOnce()\n\n      currentMockInput.pressKey(\"END\", { shift: true })\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n\n      currentMockInput.pressKey(\"HOME\", { shift: true })\n      await renderOnce()\n\n      currentMockInput.pressKey(\"END\", { shift: true })\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toContain(\"Line 29\")\n    })\n\n    it(\"should select to buffer home with shift+super+up in scrollable textarea\", async () => {\n      // Create textarea with content taller than visible area\n      const lines = Array.from({ length: 50 }, (_, i) => `Line ${i.toString().padStart(2, \"0\")}`)\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: lines.join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      // Move cursor to middle of content (line 25)\n      editor.focus()\n      editor.gotoLine(25)\n      await renderOnce()\n\n      // Verify viewport has scrolled\n      const viewportBefore = editor.editorView.getViewport()\n      expect(viewportBefore.offsetY).toBeGreaterThan(0)\n\n      // Select to buffer home (shift+super+up)\n      currentMockInput.pressKey(\"ARROW_UP\", { shift: true, super: true })\n      await renderOnce()\n\n      // Should have selection\n      expect(editor.hasSelection()).toBe(true)\n\n      // Selection should include content from line 0 to line 25\n      const selectedText = editor.getSelectedText()\n      expect(selectedText).toContain(\"Line 00\")\n      expect(selectedText).toContain(\"Line 24\")\n      expect(selectedText.split(\"\\n\").length).toBeGreaterThanOrEqual(25)\n\n      const viewportAfter = editor.editorView.getViewport()\n      expect(viewportAfter.offsetY).toBe(0)\n    })\n\n    it(\"should select to buffer end with shift+super+down in scrollable textarea\", async () => {\n      // Create textarea with content taller than visible area\n      const lines = Array.from({ length: 50 }, (_, i) => `Line ${i.toString().padStart(2, \"0\")}`)\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: lines.join(\"\\n\"),\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      // Move cursor to line 20\n      editor.focus()\n      editor.gotoLine(20)\n      await renderOnce()\n\n      const viewportBefore = editor.editorView.getViewport()\n      expect(viewportBefore.offsetY).toBeGreaterThan(0)\n\n      // Select to buffer end (shift+super+down)\n      currentMockInput.pressKey(\"ARROW_DOWN\", { shift: true, super: true })\n      await renderOnce()\n\n      // Should have selection\n      expect(editor.hasSelection()).toBe(true)\n\n      // Selection should include content from line 20 to line 49\n      const selectedText = editor.getSelectedText()\n      expect(selectedText).toContain(\"Line 20\")\n      expect(selectedText).toContain(\"Line 49\")\n      expect(selectedText.split(\"\\n\").length).toBeGreaterThanOrEqual(29)\n\n      const viewportAfter = editor.editorView.getViewport()\n      const totalLines = editor.editorView.getTotalVirtualLineCount()\n      const maxOffsetY = Math.max(0, totalLines - viewportBefore.height)\n      expect(viewportAfter.offsetY).toBe(maxOffsetY)\n    })\n\n    it(\"should handle selection across viewport boundaries correctly\", async () => {\n      // Create textarea with content taller than visible area\n      const lines = Array.from({ length: 30 }, (_, i) => `Line ${i.toString().padStart(2, \"0\")}`)\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: lines.join(\"\\n\"),\n        width: 40,\n        height: 5, // Small viewport\n        selectable: true,\n      })\n\n      // Move cursor to middle (line 15)\n      editor.focus()\n      editor.gotoLine(15)\n      // Move to column 5\n      for (let i = 0; i < 5; i++) {\n        editor.moveCursorRight()\n      }\n      await renderOnce()\n\n      const cursorBefore = editor.editorView.getVisualCursor()\n      expect(cursorBefore.logicalRow).toBe(15)\n      expect(cursorBefore.logicalCol).toBe(5)\n\n      // Select to buffer home\n      currentMockInput.pressKey(\"ARROW_UP\", { shift: true, super: true })\n      await renderOnce()\n\n      expect(editor.hasSelection()).toBe(true)\n      const selectedText = editor.getSelectedText()\n\n      // Should select from (15, 5) to (0, 0)\n      // First line should be complete, last line should be partial\n      expect(selectedText.startsWith(\"Line 00\")).toBe(true)\n      expect(selectedText).toContain(\"Line 14\")\n    })\n  })\n})\n\nfunction countSelectedCells(\n  bg: Float32Array,\n  bufferWidth: number,\n  editor: { x: number; y: number; height: number; width: number },\n  r: number,\n  g: number,\n  b: number,\n): number {\n  let count = 0\n  for (let y = 0; y < editor.height; y++) {\n    for (let x = 0; x < editor.width; x++) {\n      const bufferIdx = (editor.y + y) * bufferWidth + (editor.x + x)\n      const bgR = bg[bufferIdx * 4 + 0]\n      const bgG = bg[bufferIdx * 4 + 1]\n      const bgB = bg[bufferIdx * 4 + 2]\n      if (Math.abs(bgR - r) < 0.01 && Math.abs(bgG - g) < 0.01 && Math.abs(bgB - b) < 0.01) {\n        count++\n      }\n    }\n  }\n  return count\n}\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.stress.test.ts",
    "content": "import { describe, expect, it, afterAll, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockMouse, type MockInput } from \"../../testing/test-renderer.js\"\nimport { ManualClock } from \"../../testing/manual-clock.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMouse: MockMouse\nlet currentMockInput: MockInput\nlet currentClock: ManualClock\n\ndescribe(\"Textarea - Stress Tests\", () => {\n  beforeEach(async () => {\n    currentClock = new ManualClock()\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockMouse: currentMouse,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n      clock: currentClock,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  it(\"STRESS TEST: should not process raw mouse bytes in textarea buffer with hundreds of rapid movements\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Initial text content\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Send 500 rapid mouse movement events across the screen\n    for (let i = 0; i < 500; i++) {\n      const x = i % 40\n      const y = i % 10\n      await currentMouse.moveTo(x, y)\n    }\n\n    // The text content should remain unchanged - no raw mouse bytes should appear\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: thousands of mouse events per second should not corrupt textarea\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Test content\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Send 2000 mouse movement events as fast as possible\n    for (let i = 0; i < 2000; i++) {\n      const x = (i * 7) % 40\n      const y = (i * 3) % 10\n      await currentMouse.moveTo(x, y)\n    }\n\n    // Text should be unchanged\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toMatch(/\\x1b|\\[<|[0-9]+;[0-9]+/)\n  })\n\n  it(\"STRESS TEST: mouse movements while typing should not inject mouse bytes\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n\n    // Interleave typing and mouse movements\n    for (let i = 0; i < 100; i++) {\n      currentMockInput.pressKey(\"a\")\n      await currentMouse.moveTo(i % 40, i % 10)\n      currentMockInput.pressKey(\"b\")\n      await currentMouse.moveTo((i + 5) % 40, (i + 5) % 10)\n    }\n\n    // Should only contain the typed characters\n    expect(editor.plainText).toMatch(/^[ab]+$/)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: rapid mouse drags should not leak bytes into buffer\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Original\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Perform 50 rapid drag operations (reduced to avoid timeouts)\n    for (let i = 0; i < 10; i++) {\n      const startX = i % 20\n      const startY = i % 5\n      const endX = (i + 10) % 30\n      const endY = (i + 3) % 8\n      await currentMouse.drag(startX, startY, endX, endY, 0, { delayMs: 0 })\n    }\n\n    // Text should remain unchanged or only contain valid selections/edits\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toMatch(/[0-9]+;[0-9]+/)\n  }, 10000) // 10 second timeout for drag operations\n\n  it(\"STRESS TEST: mouse clicks during rapid typing should not corrupt buffer\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Start\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n\n    // Type while clicking randomly (reduced to 50 iterations to avoid timeout)\n    for (let i = 0; i < 10; i++) {\n      if (i % 3 === 0) {\n        currentMockInput.pressKey(\"x\")\n      }\n      await currentMouse.click(i % 40, i % 10, 0, { delayMs: 0 })\n      if (i % 5 === 0) {\n        currentMockInput.pressKey(\"y\")\n      }\n    }\n\n    // Should not contain any mouse escape sequences\n    expect(editor.plainText).not.toContain(\"\\x1b[<\")\n    expect(editor.plainText).not.toMatch(/[0-9]+;[0-9]+;[0-9]+/)\n  }, 10000) // 10 second timeout for click operations\n\n  it(\"STRESS TEST: high-frequency mouse scroll should not inject bytes\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\",\n      width: 40,\n      height: 5,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Rapid scroll events\n    for (let i = 0; i < 500; i++) {\n      const direction = i % 2 === 0 ? \"down\" : \"up\"\n      await currentMouse.scroll(20, 3, direction as \"up\" | \"down\")\n    }\n\n    // Text should be unchanged\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n  })\n\n  it(\"STRESS TEST: raw stdin with mouse SGR sequences should be filtered\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Clean text\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Directly inject raw mouse SGR sequences into stdin\n    const rawMouseSequences = [\n      \"\\x1b[<35;20;5m\", // mouse move\n      \"\\x1b[<0;10;3M\", // left button press\n      \"\\x1b[<0;10;3m\", // left button release\n      \"\\x1b[<35;25;7m\", // mouse move\n      \"\\x1b[<64;15;2M\", // scroll up\n      \"\\x1b[<65;15;2M\", // scroll down\n    ]\n\n    for (let i = 0; i < 10; i++) {\n      for (const seq of rawMouseSequences) {\n        currentRenderer.stdin.emit(\"data\", Buffer.from(seq))\n      }\n    }\n\n    // Text should remain unchanged\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n    expect(editor.plainText).not.toMatch(/[0-9]+;[0-9]+/)\n  })\n\n  it(\"STRESS TEST: simultaneous typing and mouse flood\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n\n    const typedText = \"hello world\"\n    let typeIndex = 0\n\n    // Type text while flooding with mouse events\n    for (let i = 0; i < 1000; i++) {\n      // Every 100 mouse events, type one character\n      if (i % 100 === 0 && typeIndex < typedText.length) {\n        currentMockInput.pressKey(typedText[typeIndex])\n        typeIndex++\n      }\n\n      // Flood with mouse movements\n      await currentMouse.moveTo(i % 40, i % 10)\n    }\n\n    // Type remaining characters\n    while (typeIndex < typedText.length) {\n      currentMockInput.pressKey(typedText[typeIndex])\n      typeIndex++\n    }\n\n    // Should only contain the typed text\n    expect(editor.plainText).toBe(typedText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toMatch(/[0-9]+;[0-9]+/)\n  })\n\n  it(\"STRESS TEST: mouse events during multi-line editing\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Line1\\nLine2\\nLine3\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n\n    // Navigate and edit while sending mouse events\n    for (let i = 0; i < 500; i++) {\n      if (i % 100 === 0) {\n        currentMockInput.pressArrow(\"down\")\n      }\n      if (i % 150 === 0) {\n        currentMockInput.pressKey(\"X\")\n      }\n\n      await currentMouse.moveTo(i % 40, i % 10)\n    }\n\n    // Text should only contain edits from keyboard, not mouse bytes\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n    expect(editor.plainText).not.toMatch(/35;[0-9]+;[0-9]+/)\n  })\n\n  it(\"STRESS TEST: 10000 raw mouse byte injections without delay\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Protected\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Inject 10000 raw mouse movement sequences as fast as possible\n    for (let i = 0; i < 10000; i++) {\n      const x = (i % 40) + 1\n      const y = (i % 10) + 1\n      const rawSeq = `\\x1b[<35;${x};${y}m`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n    }\n\n    // Verify no corruption\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n    expect(editor.plainText).not.toContain(\"35;\")\n  })\n\n  it(\"STRESS TEST: inject mouse bytes between every character typed\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n\n    const toType = \"HelloWorld\"\n    for (let i = 0; i < toType.length; i++) {\n      // Inject 100 mouse sequences before each character\n      for (let j = 0; j < 100; j++) {\n        const rawSeq = `\\x1b[<35;${(j % 40) + 1};${(j % 10) + 1}m`\n        currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n      }\n\n      currentMockInput.pressKey(toType[i])\n\n      // Inject 100 mouse sequences after each character\n      for (let j = 0; j < 100; j++) {\n        const rawSeq = `\\x1b[<35;${(j % 40) + 1};${(j % 10) + 1}m`\n        currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n      }\n    }\n\n    // Should only contain the typed text\n    expect(editor.plainText).toBe(toType)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n    expect(editor.plainText).not.toMatch(/[0-9]+;[0-9]+/)\n  })\n\n  it(\"STRESS TEST: extreme burst - 50000 mouse events in rapid succession\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Stable content\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Massive burst of mouse events\n    for (let i = 0; i < 50000; i++) {\n      const x = ((i * 17) % 40) + 1\n      const y = ((i * 11) % 10) + 1\n      const buttonCode = 35 + (i % 4) // Vary the button codes\n      const rawSeq = `\\x1b[<${buttonCode};${x};${y}m`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n    }\n\n    // Verify integrity\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: partial/malformed mouse sequences\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Clean\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Send partial sequences that might confuse the parser\n    const partialSequences = [\n      \"\\x1b[<35;\",\n      \"\\x1b[<35;20\",\n      \"\\x1b[<35;20;\",\n      \"\\x1b[<35;20;5\",\n      \"\\x1b\",\n      \"\\x1b[\",\n      \"\\x1b[<\",\n      \"\\x1b[<35;20;5m\\x1b[<35;\", // Complete + incomplete\n    ]\n\n    for (let i = 0; i < 1000; i++) {\n      const seq = partialSequences[i % partialSequences.length]\n      currentRenderer.stdin.emit(\"data\", Buffer.from(seq))\n    }\n\n    // Text should remain unchanged\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: mouse events mixed with paste operations\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n\n    // Simulate paste with mouse flood\n    for (let i = 0; i < 100; i++) {\n      // Inject mouse bytes\n      for (let j = 0; j < 50; j++) {\n        const rawSeq = `\\x1b[<35;${(j % 40) + 1};${(j % 10) + 1}m`\n        currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n      }\n\n      // Paste some text\n      const pasteText = `Paste${i}`\n      editor.insertText(pasteText)\n\n      // More mouse bytes\n      for (let j = 0; j < 50; j++) {\n        const rawSeq = `\\x1b[<0;${(j % 40) + 1};${(j % 10) + 1}M`\n        currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n      }\n    }\n\n    // Should not contain escape sequences\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n    expect(editor.plainText).toContain(\"Paste\")\n  })\n\n  it(\"STRESS TEST: focused vs unfocused with mouse flood\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Content\",\n      width: 40,\n      height: 10,\n    })\n\n    const initialText = editor.plainText\n\n    // Flood while unfocused\n    for (let i = 0; i < 5000; i++) {\n      const rawSeq = `\\x1b[<35;${(i % 40) + 1};${(i % 10) + 1}m`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n    }\n\n    expect(editor.plainText).toBe(initialText)\n\n    // Focus and flood\n    editor.focus()\n    for (let i = 0; i < 5000; i++) {\n      const rawSeq = `\\x1b[<35;${(i % 40) + 1};${(i % 10) + 1}m`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n    }\n\n    expect(editor.plainText).toBe(initialText)\n\n    // Blur and flood again\n    editor.blur()\n    for (let i = 0; i < 5000; i++) {\n      const rawSeq = `\\x1b[<35;${(i % 40) + 1};${(i % 10) + 1}m`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n    }\n\n    // Final check - no corruption at any stage\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: all mouse button types with modifiers\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Test\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Test all button codes with all modifier combinations\n    const buttonCodes = [0, 1, 2, 3, 32, 33, 34, 35, 36, 37, 38, 39, 64, 65, 66, 67]\n    const modifiers = [0, 4, 8, 12, 16, 20, 24, 28] // shift, alt, ctrl combinations\n\n    for (let i = 0; i < 10000; i++) {\n      const button = buttonCodes[i % buttonCodes.length]\n      const modifier = modifiers[(i / buttonCodes.length) % modifiers.length | 0]\n      const code = button | modifier\n      const x = (i % 40) + 1\n      const y = (i % 10) + 1\n      const suffix = i % 2 === 0 ? \"M\" : \"m\"\n      const rawSeq = `\\x1b[<${code};${x};${y}${suffix}`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n    }\n\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n  })\n\n  it(\"STRESS TEST: mouse data split across multiple buffers\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Original\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // When mouse SGR sequences like \\x1b[<35;20;5m are split across multiple\n    // stdin data events (as happens in real terminals), the partial sequences\n    // bypass the mouse event filter in parseKeypress and get inserted as text!\n\n    // Send just ONE mouse sequence split across multiple emit calls\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b\"))\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"[\"))\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"<\"))\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"35\"))\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\";\"))\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"20\"))\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\";\"))\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"5\"))\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"m\"))\n\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: delayed split SGR mouse sequence should not leak into textarea\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Original\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Simulate ESC and continuation arriving in separate chunks where the ESC is\n    // timeout-flushed before the continuation arrives.\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b\"))\n    currentClock.advance(1000)\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"[<35;20;5m\"))\n\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: alternating mouse and keyboard at high frequency\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n\n    // Alternate between mouse and keyboard events very rapidly\n    const chars = \"abcdefghij\"\n    for (let i = 0; i < 1000; i++) {\n      // Mouse event\n      const rawSeq = `\\x1b[<35;${(i % 40) + 1};${(i % 10) + 1}m`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n\n      // Keyboard event\n      if (i % 100 === 0) {\n        currentMockInput.pressKey(chars[(i / 100) % chars.length])\n      }\n\n      // Another mouse event\n      const rawSeq2 = `\\x1b[<0;${(i % 20) + 1};${(i % 5) + 1}M`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq2))\n    }\n\n    // Should only contain the typed characters\n    expect(editor.plainText).toMatch(/^[a-j]*$/)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: mouse during undo/redo operations\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Start\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n\n    // Make some edits with mouse flood\n    for (let i = 0; i < 100; i++) {\n      currentMockInput.pressKey(\"x\")\n\n      // Flood with mouse\n      for (let j = 0; j < 50; j++) {\n        const rawSeq = `\\x1b[<35;${(j % 40) + 1};${(j % 10) + 1}m`\n        currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n      }\n    }\n\n    // Undo with mouse flood\n    for (let i = 0; i < 50; i++) {\n      currentMockInput.pressKey(\"z\", { ctrl: true })\n\n      for (let j = 0; j < 100; j++) {\n        const rawSeq = `\\x1b[<35;${(j % 40) + 1};${(j % 10) + 1}m`\n        currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n      }\n    }\n\n    // Should not contain mouse bytes\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n  })\n\n  it(\"STRESS TEST: 100000 mouse events - ultimate stress\", async () => {\n    const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Extreme test\",\n      width: 40,\n      height: 10,\n    })\n\n    editor.focus()\n    const initialText = editor.plainText\n\n    // Send 100,000 mouse events as fast as possible\n    for (let i = 0; i < 100000; i++) {\n      const x = ((i * 19) % 40) + 1\n      const y = ((i * 13) % 10) + 1\n      const code = 32 + (i % 8)\n      const rawSeq = `\\x1b[<${code};${x};${y}m`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n    }\n\n    // After this extreme flood, text should be intact\n    expect(editor.plainText).toBe(initialText)\n    expect(editor.plainText).not.toContain(\"\\x1b\")\n    expect(editor.plainText).not.toContain(\"[<\")\n\n    // Also check the raw bytes haven't corrupted the cursor position\n    const cursor = editor.logicalCursor\n    expect(typeof cursor.row).toBe(\"number\")\n    expect(typeof cursor.col).toBe(\"number\")\n  })\n\n  it(\"STRESS TEST: concurrent mouse events on multiple textareas\", async () => {\n    const { textarea: editor1 } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Editor 1\",\n      width: 40,\n      height: 5,\n    })\n\n    const { textarea: editor2 } = await createTextareaRenderable(currentRenderer, renderOnce, {\n      initialValue: \"Editor 2\",\n      width: 40,\n      height: 5,\n    })\n\n    editor1.focus()\n    const text1 = editor1.plainText\n    const text2 = editor2.plainText\n\n    // Flood both editors with mouse events\n    for (let i = 0; i < 10000; i++) {\n      const x = (i % 40) + 1\n      const y = (i % 10) + 1\n      const rawSeq = `\\x1b[<35;${x};${y}m`\n      currentRenderer.stdin.emit(\"data\", Buffer.from(rawSeq))\n\n      // Switch focus occasionally\n      if (i % 500 === 0) {\n        if (i % 1000 === 0) {\n          editor1.focus()\n        } else {\n          editor2.focus()\n        }\n      }\n    }\n\n    // Both should be intact\n    expect(editor1.plainText).toBe(text1)\n    expect(editor2.plainText).toBe(text2)\n    expect(editor1.plainText).not.toContain(\"\\x1b\")\n    expect(editor2.plainText).not.toContain(\"\\x1b\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.undo-redo.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\n\ndescribe(\"Textarea - Undo/Redo Tests\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n      otherModifiersMode: true,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"Undo/Redo\", () => {\n    it(\"should delete multiple selected ranges and restore with undo\", async () => {\n      const initialText = \"Hello World Test\"\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: initialText,\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      editor.editBuffer.setCursor(0, 0)\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\"Hello\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\" World Test\")\n      expect(editor.hasSelection()).toBe(false)\n\n      editor.editBuffer.setCursor(0, 0)\n      for (let i = 0; i < 6; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\" World\")\n\n      currentMockInput.pressKey(\"DELETE\")\n      expect(editor.plainText).toBe(\" Test\")\n      expect(editor.hasSelection()).toBe(false)\n\n      editor.editBuffer.setCursor(0, 0)\n      for (let i = 0; i < 5; i++) {\n        currentMockInput.pressArrow(\"right\", { shift: true })\n      }\n      expect(editor.hasSelection()).toBe(true)\n      expect(editor.getSelectedText()).toBe(\" Test\")\n\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"\")\n      expect(editor.hasSelection()).toBe(false)\n\n      currentMockInput.pressKey(\"-\", { ctrl: true })\n      expect(editor.plainText).toBe(\" Test\")\n\n      currentMockInput.pressKey(\"-\", { ctrl: true })\n      expect(editor.plainText).toBe(\" World Test\")\n\n      currentMockInput.pressKey(\"-\", { ctrl: true })\n      expect(editor.plainText).toBe(initialText)\n    })\n  })\n\n  describe(\"History - Undo/Redo\", () => {\n    it(\"should undo text insertion\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Type \"Hello\"\n      currentMockInput.pressKey(\"H\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"l\")\n      currentMockInput.pressKey(\"o\")\n      expect(editor.plainText).toBe(\"Hello\")\n\n      // Undo\n      editor.undo()\n      expect(editor.plainText).toBe(\"Hell\")\n    })\n\n    it(\"should redo after undo\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Type text\n      currentMockInput.pressKey(\"T\")\n      currentMockInput.pressKey(\"e\")\n      currentMockInput.pressKey(\"s\")\n      currentMockInput.pressKey(\"t\")\n      expect(editor.plainText).toBe(\"Test\")\n\n      // Undo\n      editor.undo()\n      expect(editor.plainText).toBe(\"Tes\")\n\n      // Redo\n      editor.redo()\n      expect(editor.plainText).toBe(\"Test\")\n    })\n\n    it(\"should handle multiple undo operations\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Type characters one by one\n      currentMockInput.pressKey(\"A\")\n      currentMockInput.pressKey(\"B\")\n      currentMockInput.pressKey(\"C\")\n      expect(editor.plainText).toBe(\"ABC\")\n\n      // Undo 3 times\n      editor.undo()\n      expect(editor.plainText).toBe(\"AB\")\n\n      editor.undo()\n      expect(editor.plainText).toBe(\"A\")\n\n      editor.undo()\n      expect(editor.plainText).toBe(\"\")\n    })\n\n    it(\"should handle Ctrl+- for undo\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"H\")\n      currentMockInput.pressKey(\"i\")\n      expect(editor.plainText).toBe(\"Hi\")\n\n      // Ctrl+- to undo\n      currentMockInput.pressKey(\"-\", { ctrl: true })\n      expect(editor.plainText).toBe(\"H\")\n    })\n\n    it(\"should handle Ctrl+. for redo\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"X\")\n      expect(editor.plainText).toBe(\"X\")\n\n      // Undo\n      currentMockInput.pressKey(\"-\", { ctrl: true })\n      expect(editor.plainText).toBe(\"\")\n\n      // Ctrl+. to redo\n      currentMockInput.pressKey(\".\", { ctrl: true })\n      expect(editor.plainText).toBe(\"X\")\n    })\n\n    it(\"should handle redo programmatically\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      currentMockInput.pressKey(\"Y\")\n      expect(editor.plainText).toBe(\"Y\")\n\n      editor.undo()\n      expect(editor.plainText).toBe(\"\")\n\n      // Programmatic redo\n      editor.redo()\n      expect(editor.plainText).toBe(\"Y\")\n    })\n\n    it(\"should undo deletion\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      // Delete backward\n      currentMockInput.pressBackspace()\n      expect(editor.plainText).toBe(\"Hello Worl\")\n\n      // Undo\n      editor.undo()\n      expect(editor.plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should undo newline insertion\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n\n      currentMockInput.pressEnter()\n      expect(editor.plainText).toBe(\"Hello\\n\")\n\n      // Undo\n      editor.undo()\n      expect(editor.plainText).toBe(\"Hello\")\n    })\n\n    it(\"should restore cursor position after undo\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(9999) // Move to end\n      expect(editor.logicalCursor.col).toBe(6)\n\n      currentMockInput.pressEnter()\n      currentMockInput.pressKey(\"L\")\n      currentMockInput.pressKey(\"i\")\n      expect(editor.plainText).toBe(\"Line 1\\nLi\")\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(2)\n\n      // Undo last character \"i\"\n      editor.undo()\n      expect(editor.plainText).toBe(\"Line 1\\nL\")\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(1)\n\n      // Undo \"L\"\n      editor.undo()\n      expect(editor.plainText).toBe(\"Line 1\\n\")\n      expect(editor.logicalCursor.row).toBe(1)\n      expect(editor.logicalCursor.col).toBe(0)\n    })\n\n    it(\"should handle undo/redo chain\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Build up edits\n      currentMockInput.pressKey(\"1\")\n      currentMockInput.pressKey(\"2\")\n      currentMockInput.pressKey(\"3\")\n      expect(editor.plainText).toBe(\"123\")\n\n      // Undo all\n      editor.undo()\n      expect(editor.plainText).toBe(\"12\")\n      editor.undo()\n      expect(editor.plainText).toBe(\"1\")\n      editor.undo()\n      expect(editor.plainText).toBe(\"\")\n\n      // Redo all\n      editor.redo()\n      expect(editor.plainText).toBe(\"1\")\n      editor.redo()\n      expect(editor.plainText).toBe(\"12\")\n      editor.redo()\n      expect(editor.plainText).toBe(\"123\")\n    })\n\n    it(\"should handle undo after deleteChar\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"ABCDE\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n\n      // Delete \"A\"\n      currentMockInput.pressKey(\"DELETE\")\n      expect(editor.plainText).toBe(\"BCDE\")\n\n      // Undo\n      editor.undo()\n      expect(editor.plainText).toBe(\"ABCDE\")\n    })\n\n    it(\"should handle undo after deleteLine\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Line 1\\nLine 2\\nLine 3\",\n        width: 40,\n        height: 10,\n      })\n\n      editor.focus()\n      editor.gotoLine(1)\n\n      const beforeDelete = editor.plainText\n\n      // Delete line 2\n      currentMockInput.pressKey(\"d\", { ctrl: true })\n      const afterDelete = editor.plainText\n\n      // Verify delete happened\n      expect(afterDelete).not.toBe(beforeDelete)\n\n      // Undo\n      editor.undo()\n      expect(editor.plainText).toBe(beforeDelete)\n    })\n\n    it(\"should clear selection on undo\", async () => {\n      const { textarea: editor } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        initialValue: \"Hello World\",\n        width: 40,\n        height: 10,\n        selectable: true,\n      })\n\n      editor.focus()\n\n      // Type a character first\n      currentMockInput.pressKey(\"A\")\n      expect(editor.plainText).toBe(\"AHello World\")\n\n      // Undo to get back to original\n      editor.undo()\n      expect(editor.plainText).toBe(\"Hello World\")\n\n      // Make a selection\n      currentMockInput.pressArrow(\"right\", { shift: true })\n      expect(editor.hasSelection()).toBe(true)\n\n      // Undo should clear selection (even though there's nothing to undo now)\n      editor.undo()\n      expect(editor.hasSelection()).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/Textarea.visual-lines.test.ts",
    "content": "import { describe, it, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockInput } from \"../../testing/test-renderer.js\"\nimport { createTextareaRenderable } from \"./renderable-test-utils.js\"\n\nlet currentRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet currentMockInput: MockInput\n\ndescribe(\"TextareaRenderable - Visual Line Navigation\", () => {\n  beforeEach(async () => {\n    ;({\n      renderer: currentRenderer,\n      renderOnce,\n      mockInput: currentMockInput,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n  })\n\n  afterEach(() => {\n    currentRenderer.destroy()\n  })\n\n  describe(\"without wrapping\", () => {\n    it(\"gotoVisualLineHome should go to start of line\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 40,\n        height: 10,\n        wrapMode: \"none\",\n      })\n\n      textarea.setText(\"Hello World\")\n      textarea.editBuffer.setCursor(0, 6)\n\n      textarea.gotoVisualLineHome()\n\n      const cursor = textarea.editBuffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(0)\n    })\n\n    it(\"gotoVisualLineEnd should go to end of line\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 40,\n        height: 10,\n        wrapMode: \"none\",\n      })\n\n      textarea.setText(\"Hello World\")\n      textarea.editBuffer.setCursor(0, 6)\n\n      textarea.gotoVisualLineEnd()\n\n      const cursor = textarea.editBuffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(11)\n    })\n\n    it(\"should support selection with visual line home\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 40,\n        height: 10,\n      })\n\n      textarea.setText(\"Hello World\")\n      textarea.editBuffer.setCursor(0, 11)\n\n      textarea.gotoVisualLineHome({ select: true })\n\n      const selection = textarea.getSelection()\n      expect(selection).not.toBeNull()\n      expect(selection!.start).toBe(0)\n      expect(selection!.end).toBe(11)\n      expect(textarea.getSelectedText()).toBe(\"Hello World\")\n    })\n\n    it(\"should support selection with visual line end\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 40,\n        height: 10,\n      })\n\n      textarea.setText(\"Hello World\")\n      textarea.editBuffer.setCursor(0, 0)\n\n      textarea.gotoVisualLineEnd({ select: true })\n\n      const selection = textarea.getSelection()\n      expect(selection).not.toBeNull()\n      expect(selection!.start).toBe(0)\n      expect(selection!.end).toBe(11)\n      expect(textarea.getSelectedText()).toBe(\"Hello World\")\n    })\n  })\n\n  describe(\"with wrapping\", () => {\n    it(\"gotoVisualLineHome should go to start of visual line, not logical line\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      textarea.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n      textarea.editBuffer.setCursor(0, 22)\n\n      textarea.gotoVisualLineHome()\n\n      const cursor = textarea.editBuffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(20)\n    })\n\n    it(\"gotoVisualLineEnd should go to end of visual line, not logical line\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      textarea.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n      textarea.editBuffer.setCursor(0, 5)\n\n      textarea.gotoVisualLineEnd()\n\n      const cursor = textarea.editBuffer.getCursorPosition()\n      expect(cursor.row).toBe(0)\n      expect(cursor.col).toBe(19)\n    })\n\n    it(\"should navigate between visual lines correctly\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      textarea.setText(\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\n      // First visual line\n      textarea.editBuffer.setCursor(0, 10)\n      textarea.gotoVisualLineHome()\n      expect(textarea.editBuffer.getCursorPosition().col).toBe(0)\n\n      textarea.gotoVisualLineEnd()\n      expect(textarea.editBuffer.getCursorPosition().col).toBe(19)\n\n      // Move to second visual line\n      textarea.editBuffer.moveCursorRight()\n\n      textarea.gotoVisualLineHome()\n      expect(textarea.editBuffer.getCursorPosition().col).toBe(20)\n\n      textarea.gotoVisualLineEnd()\n      const cursor = textarea.editBuffer.getCursorPosition()\n      expect(cursor.col).toBeGreaterThan(20)\n    })\n\n    it(\"should handle word wrapping correctly\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 20,\n        height: 10,\n        wrapMode: \"word\",\n      })\n\n      textarea.setText(\"Hello wonderful world of wrapped text\")\n      textarea.editBuffer.setCursor(0, 25)\n\n      const vcursor = textarea.editorView.getVisualCursor()\n      expect(vcursor.visualRow).toBeGreaterThan(0)\n\n      textarea.gotoVisualLineHome()\n      const solCursor = textarea.editBuffer.getCursorPosition()\n      expect(solCursor.col).toBeGreaterThan(0)\n\n      textarea.gotoVisualLineEnd()\n      const eolCursor = textarea.editBuffer.getCursorPosition()\n      expect(eolCursor.col).toBeLessThan(37)\n    })\n\n    it(\"should select within visual line boundaries\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      textarea.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n      textarea.editBuffer.setCursor(0, 10)\n\n      textarea.gotoVisualLineEnd({ select: true })\n\n      const selectedText = textarea.getSelectedText()\n      expect(selectedText).toBe(\"KLMNOPQRS\")\n      expect(selectedText.length).toBe(9)\n    })\n  })\n\n  describe(\"with multi-byte characters\", () => {\n    it(\"should handle wrapped emoji correctly\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 15,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      textarea.setText(\"🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟\")\n\n      // First visual line\n      textarea.editBuffer.setCursor(0, 2)\n      textarea.gotoVisualLineHome()\n      expect(textarea.editBuffer.getCursorPosition().col).toBe(0)\n\n      textarea.gotoVisualLineEnd()\n      const firstLineEnd = textarea.editBuffer.getCursorPosition().col\n      expect(firstLineEnd).toBeGreaterThan(0)\n      expect(firstLineEnd).toBeLessThan(20)\n\n      // Move to second visual line - need to move far enough\n      textarea.editBuffer.setCursor(0, 16)\n      const vcursor = textarea.editorView.getVisualCursor()\n\n      // Only test visual line navigation if we actually moved to second visual line\n      if (vcursor.visualRow > 0) {\n        textarea.gotoVisualLineHome()\n        const secondLineStart = textarea.editBuffer.getCursorPosition().col\n        expect(secondLineStart).toBeGreaterThan(firstLineEnd - 1)\n      }\n    })\n  })\n\n  describe(\"comparison with logical line navigation\", () => {\n    it(\"visual home should differ from logical home when wrapped\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      textarea.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n      textarea.editBuffer.setCursor(0, 22)\n\n      textarea.gotoVisualLineHome()\n      const visualHomeCol = textarea.editBuffer.getCursorPosition().col\n      expect(visualHomeCol).toBe(20)\n\n      textarea.editBuffer.setCursor(0, 22)\n      textarea.gotoLineHome()\n      const logicalHomeCol = textarea.editBuffer.getCursorPosition().col\n      expect(logicalHomeCol).toBe(0)\n\n      expect(visualHomeCol).not.toBe(logicalHomeCol)\n    })\n\n    it(\"visual end should differ from logical end when wrapped\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 20,\n        height: 10,\n        wrapMode: \"char\",\n      })\n\n      textarea.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n      textarea.editBuffer.setCursor(0, 5)\n\n      textarea.gotoVisualLineEnd()\n      const visualEndCol = textarea.editBuffer.getCursorPosition().col\n      expect(visualEndCol).toBe(19)\n\n      textarea.editBuffer.setCursor(0, 5)\n      textarea.gotoLineEnd()\n      const logicalEndCol = textarea.editBuffer.getCursorPosition().col\n      expect(logicalEndCol).toBe(26)\n\n      expect(visualEndCol).not.toBe(logicalEndCol)\n    })\n\n    it(\"without wrapping, visual and logical should be the same\", async () => {\n      const { textarea } = await createTextareaRenderable(currentRenderer, renderOnce, {\n        width: 40,\n        height: 10,\n        wrapMode: \"none\",\n      })\n\n      textarea.setText(\"Hello World\")\n\n      // Test home\n      textarea.editBuffer.setCursor(0, 6)\n      textarea.gotoVisualLineHome()\n      const visualHomeCol = textarea.editBuffer.getCursorPosition().col\n\n      textarea.editBuffer.setCursor(0, 6)\n      textarea.gotoLineHome()\n      const logicalHomeCol = textarea.editBuffer.getCursorPosition().col\n\n      expect(visualHomeCol).toBe(logicalHomeCol)\n\n      // Test end\n      textarea.editBuffer.setCursor(0, 6)\n      textarea.gotoVisualLineEnd()\n      const visualEndCol = textarea.editBuffer.getCursorPosition().col\n\n      textarea.editBuffer.setCursor(0, 6)\n      textarea.gotoLineEnd()\n      const logicalEndCol = textarea.editBuffer.getCursorPosition().col\n\n      expect(visualEndCol).toBe(logicalEndCol)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`LineNumber with Code - Core Tests LineNumber with Code should have correct height - no scrollbox 1`] = `\n\" 1 function hello() {                             \n 2   console.log(\"Hello\");                        \n 3   return 42;                                   \n 4 }                                              \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`LineNumber with Code - Core Tests LineNumber with Code in ScrollBox - check height 1`] = `\n\" 1 function test() {                              \n 2   return true;                                 \n 3 }                                              \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`LineNumber with Code - Core Tests Multiple LineNumber blocks in ScrollBox 1`] = `\n\"                                                 ▀\n 1 const x = 1;                                   \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`LineNumber with Code - Core Tests LineNumber with Code - check actual rendered height vs yoga height 1`] = `\n\" 1 line1                                          \n 2 line2                                          \n 3 line3                                          \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`LineNumber with Code - Core Tests LineNumber with height=100% in ScrollBox 1`] = `\n\" 1 const x = 1;                                   \n 2 const y = 2;                                   \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`LineNumber in ScrollBox - Simple Core Test LineNumber with Code in ScrollBox should wrap content height 1`] = `\n\" 1 function test() {                              \n 2   return true;                                 \n 3 }                                              \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Simple Core Test Multiple LineNumber blocks in ScrollBox should each wrap content 1`] = `\n\" 1 const x = 1;                                   \n 1 const y = 2;                                   \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`LineNumberRenderable in ScrollBox single Code renderable with line numbers in ScrollBox - correct dimensions 1`] = `\n\"┌────────────────────────────┐                              \n│   1 function test1() {     │                              \n│   2   console.log(\"Line    │                              \n│     1\");                   │                              \n│   3   return 1;            │                              \n│   4 }                      │                              \n│   5 function test2() {     │                              \n│   6   console.log(\"Line    │                              \n│     2\");                   │                              \n└────────────────────────────┘                              \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox single Code renderable in ScrollBox - scroll and verify dimensions 1`] = `\n\"┌──────────────────────────────────────┐                    \n│   1 function test1() {               │                    \n│   2   console.log(\"Line 1\");         │                    \n│   3   return 1;                      │                    \n│   4 }                                │                    \n│   5 function test2() {               │                    \n│   6   console.log(\"Line 2\");         │                    \n│   7   return 2;                      │                    \n│   8 }                                │                    \n│   9 function test3() {               │                    \n│  10   console.log(\"Line 3\");         │                    \n└──────────────────────────────────────┘                    \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox single Code renderable in ScrollBox - scroll and verify dimensions 2`] = `\n\"┌──────────────────────────────────────┐                    \n│   1 function test1() {               │                    \n│   2   console.log(\"Line 1\");         │                    \n│   3   return 1;                      │                    \n│   4 }                                │                    \n│   5 function test2() {               │                    \n│   6   console.log(\"Line 2\");         │                    \n│   7   return 2;                      │                    \n│   8 }                                │                    \n│   9 function test3() {               │                    \n│  10   console.log(\"Line 3\");         │                    \n└──────────────────────────────────────┘                    \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox single Code renderable in ScrollBox - scroll and verify dimensions 3`] = `\n\"┌──────────────────────────────────────┐                    \n│   1 function test1() {               │                    \n│   2   console.log(\"Line 1\");         │                    \n│   3   return 1;                      │                    \n│   4 }                                │                    \n│   5 function test2() {               │                    \n│   6   console.log(\"Line 2\");         │                    \n│   7   return 2;                      │                    \n│   8 }                                │                    \n│   9 function test3() {               │                    \n│  10   console.log(\"Line 3\");         │                    \n└──────────────────────────────────────┘                    \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox multiple Code renderables with line numbers in ScrollBox - correct dimensions 1`] = `\n\"┌────────────────────────────────────────────────┐         █\n│  1 function test1() {                          │         █\n│  2   console.log(\"Line 1\");                    │         █\n│  3   return 1;                                 │         █\n│  4 }                                           │         █\n│  5 function test2() {                          │         █\n│  6   console.log(\"Line 2\");                    │         █\n└────────────────────────────────────────────────┘         █\n                                                           █\n                                                           █\n┌────────────────────────────────────────────────┐          \n│  1 function test1() {                          │          \n│  2   console.log(\"Line 1\");                    │          \n│  3   return 1;                                 │          \n│  4 }                                           │          \n│  5 function test2() {                          │          \n│  6   console.log(\"Line 2\");                    │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox multiple Code renderables with line numbers in ScrollBox - correct dimensions 2`] = `\n\"│  5 function test2() {                          │          \n│  6   console.log(\"Line 2\");                    │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n┌────────────────────────────────────────────────┐         █\n│  1 function test1() {                          │         █\n│  2   console.log(\"Line 1\");                    │         █\n│  3   return 1;                                 │         █\n│  4 }                                           │         █\n│  5 function test2() {                          │         █\n│  6   console.log(\"Line 2\");                    │         █\n└────────────────────────────────────────────────┘         █\n                                                           █\n                                                           █\n┌────────────────────────────────────────────────┐          \n│  1 function test1() {                          │          \n│  2   console.log(\"Line 1\");                    │          \n│  3   return 1;                                 │          \n│  4 }                                           │          \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox nested boxes with different border styles - dimensions correct 1`] = `\n\"╔═════════════════════════════════════════════════════╗     \n║                                                     ║     \n║                                                     ║     \n║  ╭───────────────────────────────────────────╮      ║     \n║  │                                           │      ║     \n║  │    1  function test1() {                  │      ║     \n║  │    2    console.log(\"Line 1\");            │      ║     \n║  │    3    return 1;                         │      ║     \n║  │    4  }                                   │      ║     \n║  │    5  function test2() {                  │      ║     \n║  │    6    console.log(\"Line 2\");            │      ║     \n║  │    7    return 2;                         │      ║     \n║  │    8  }                                   │      ║     \n║  │    9  function test3() {                  │      ║     \n║  │   10    console.log(\"Line 3\");            │      ║     \n║  │   11    return 3;                         │      ║     \n║  │                                           │      ║     \n║  ╰───────────────────────────────────────────╯      ║     \n╚═════════════════════════════════════════════════════╝     \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox nested boxes with different border styles - dimensions correct 2`] = `\n\"╔═════════════════════════════════════════════════════╗     \n║                                                     ║     \n║                                                     ║     \n║  ╭───────────────────────────────────────────╮      ║     \n║  │                                           │      ║     \n║  │    1  function test1() {                  │      ║     \n║  │    2    console.log(\"Line 1\");            │      ║     \n║  │    3    return 1;                         │      ║     \n║  │    4  }                                   │      ║     \n║  │    5  function test2() {                  │      ║     \n║  │    6    console.log(\"Line 2\");            │      ║     \n║  │    7    return 2;                         │      ║     \n║  │    8  }                                   │      ║     \n║  │    9  function test3() {                  │      ║     \n║  │   10    console.log(\"Line 3\");            │      ║     \n║  │   11    return 3;                         │      ║     \n║  │                                           │      ║     \n║  ╰───────────────────────────────────────────╯      ║     \n╚═════════════════════════════════════════════════════╝     \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox ScrollBox with horizontal and vertical scrolling - dimensions stable 1`] = `\n\"┌────────────────────────────────────────────────┐          \n│   1 const veryLongVariableName1 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   2 const veryLongVariableName2 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   3 const veryLongVariableName3 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   4 const veryLongVariableName4 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   5 const veryLongVariableName5 = \"This is a   │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox ScrollBox with horizontal and vertical scrolling - dimensions stable 2`] = `\n\"┌────────────────────────────────────────────────┐          \n│   1 const veryLongVariableName1 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   2 const veryLongVariableName2 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   3 const veryLongVariableName3 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   4 const veryLongVariableName4 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   5 const veryLongVariableName5 = \"This is a   │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox ScrollBox with horizontal and vertical scrolling - dimensions stable 3`] = `\n\"┌────────────────────────────────────────────────┐          \n│   1 const veryLongVariableName1 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   2 const veryLongVariableName2 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   3 const veryLongVariableName3 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   4 const veryLongVariableName4 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   5 const veryLongVariableName5 = \"This is a   │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox ScrollBox with horizontal and vertical scrolling - dimensions stable 4`] = `\n\"┌────────────────────────────────────────────────┐          \n│   1 const veryLongVariableName1 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   2 const veryLongVariableName2 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   3 const veryLongVariableName3 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   4 const veryLongVariableName4 = \"This is a   │          \n│     very long line that should require         │          \n│     horizontal scrolling to view completely\";  │          \n│   5 const veryLongVariableName5 = \"This is a   │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox gutter width changes with line count - verify remeasure 1`] = `\n\"┌──────────────────────────────────────┐                    \n│ 1 function test1() {                 │                    \n│ 2   console.log(\"Line 1\");           │                    \n│ 3   return 1;                        │                    \n│ 4 }                                  │                    \n│ 5 function test2() {                 │                    \n│ 6   console.log(\"Line 2\");           │                    \n│ 7   return 2;                        │                    \n│ 8 }                                  │                    \n│                                      │                    \n│                                      │                    \n└──────────────────────────────────────┘                    \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox gutter width changes with line count - verify remeasure 2`] = `\n\"┌──────────────────────────────────────┐                    \n│  1 function test1() {                │                    \n│  2   console.log(\"Line 1\");          │                    \n│  3   return 1;                       │                    \n│  4 }                                 │                    \n│  5 function test2() {                │                    \n│  6   console.log(\"Line 2\");          │                    \n│  7   return 2;                       │                    \n│  8 }                                 │                    \n│  9 function test3() {                │                    \n│ 10   console.log(\"Line 3\");          │                    \n└──────────────────────────────────────┘                    \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox gutter width changes with line count - verify remeasure 3`] = `\n\"┌──────────────────────────────────────┐                    \n│   1 function test1() {               │                    \n│   2   console.log(\"Line 1\");         │                    \n│   3   return 1;                      │                    \n│   4 }                                │                    \n│   5 function test2() {               │                    \n│   6   console.log(\"Line 2\");         │                    \n│   7   return 2;                      │                    \n│   8 }                                │                    \n│   9 function test3() {               │                    \n│  10   console.log(\"Line 3\");         │                    \n└──────────────────────────────────────┘                    \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox line colors span full width in ScrollBox 1`] = `\n\"┌────────────────────────────────────────────────┐          \n│  1 function test1() {                          │          \n│  2   console.log(\"Line 1\");                    │          \n│  3   return 1;                                 │          \n│  4 }                                           │          \n│  5 function test2() {                          │          \n│  6   console.log(\"Line 2\");                    │          \n│  7   return 2;                                 │          \n│  8 }                                           │          \n│  9 function test3() {                          │          \n│ 10   console.log(\"Line 3\");                    │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox line colors span full width in ScrollBox 2`] = `\n\"┌────────────────────────────────────────────────┐          \n│  1 function test1() {                          │          \n│  2   console.log(\"Line 1\");                    │          \n│  3   return 1;                                 │          \n│  4 }                                           │          \n│  5 function test2() {                          │          \n│  6   console.log(\"Line 2\");                    │          \n│  7   return 2;                                 │          \n│  8 }                                           │          \n│  9 function test3() {                          │          \n│ 10   console.log(\"Line 3\");                    │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox viewport culling with line numbers - dimensions stable 1`] = `\n\"┌───────────────────────────────────────────┐              ▀\n│  1 function test1() {                     │               \n│  2   console.log(\"Line 1\");               │               \n│  3   return 1;                            │               \n│  4 }                                      │               \n└───────────────────────────────────────────┘               \n                                                            \n┌───────────────────────────────────────────┐               \n│  1 function test1() {                     │               \n│  2   console.log(\"Line 1\");               │               \n│  3   return 1;                            │               \n│  4 }                                      │               \n└───────────────────────────────────────────┘               \n                                                            \n┌───────────────────────────────────────────┐               \n│  1 function test1() {                     │               \n│  2   console.log(\"Line 1\");               │               \n│  3   return 1;                            │               \n│  4 }                                      │               \n└───────────────────────────────────────────┘               \n\"\n`;\n\nexports[`LineNumberRenderable in ScrollBox viewport culling with line numbers - dimensions stable 2`] = `\n\"│  2   console.log(\"Line 1\");               │               \n│  3   return 1;                            │               \n│  4 }                                      │               \n└───────────────────────────────────────────┘               \n                                                            \n┌───────────────────────────────────────────┐               \n│  1 function test1() {                     │               \n│  2   console.log(\"Line 1\");               │               \n│  3   return 1;                            │               \n│  4 }                                      │               \n└───────────────────────────────────────────┘               \n                                                            \n┌───────────────────────────────────────────┐               \n│  1 function test1() {                     │               \n│  2   console.log(\"Line 1\");               │               \n│  3   return 1;                            │               \n│  4 }                                      │              ▄\n└───────────────────────────────────────────┘               \n                                                            \n┌───────────────────────────────────────────┐               \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`LineNumberRenderable renders line numbers correctly 1`] = `\n\" 1 Line 1           \n 2 Line 2           \n 3 Line 3           \n                    \n                    \n                    \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`LineNumberRenderable renders line numbers for wrapping text 1`] = `\n\" 1 Line 1 is very l \n   ong and should w \n   rap around multi \n   ple lines        \n                    \n                    \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`LineNumberRenderable renders line numbers with offset 1`] = `\n\" 42 Line 1          \n 43 Line 2          \n 44 Line 3          \n                    \n                    \n                    \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`LineNumberRenderable hides line numbers for specific lines 1`] = `\n\" 1 Line 1           \n   Line 2           \n 3 Line 3           \n   Line 4           \n 5 Line 5           \n                    \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`LineNumberRenderable combines line number offset with hidden line numbers 1`] = `\n\" 42 Line 1          \n    Line 2          \n 44 Line 3          \n    Line 4          \n 46 Line 5          \n                    \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`LineNumberRenderable maintains consistent left padding for all line numbers 1`] = `\n\"  1 Line 1                    \n  2 Line 2                    \n  3 Line 3                    \n  4 Line 4                    \n  5 Line 5                    \n  6 Line 6                    \n  7 Line 7                    \n  8 Line 8                    \n  9 Line 9                    \n 10 Line 10                   \n 11 Line 11                   \n 12 Line 12                   \n                              \n                              \n                              \n\"\n`;\n\nexports[`LineNumberRenderable maintains stable visual line count when scrolling and typing with word wrap 1`] = `\n\"                                   \n ┌───────────────────────────────┐ \n │     Ctrl+Y to redo            │ \n │  36                           │ \n │  37 VIEW:                     │ \n │  38   • Shift+W to toggle     │ \n │     wrap mode (word/char/     │ \n │     none)                     │ \n │  39   • Shift+L to toggle     │ \n │     line numbers              │ \n │  40                           │ \n │  41 FEATURES:                 │ \n │  42   ✓ Grapheme-aware        │ \n │     cursor movement           │ \n │  43   ✓ Unicode (emoji 🌟     │ \n │     and CJK 世界, 你好世界,   │ \n │     中文, 한글)               │ \n │  44   ✓ Incremental editing   │ \n │  45   ✓ Text wrapping and     │ \n │     viewport management       │ \n │  46   ✓ Undo/redo support     │ \n │  47   ✓ Word-based            │ \n │     navigation and deletion   │ \n │  48   ✓ Text selection with   │ \n │     shift keys                │ \n │  49                           │ \n │  50 Press ESC to return to    │ \n │     main menu                 │ \n └───────────────────────────────┘ \n                                   \n\"\n`;\n\nexports[`LineNumberRenderable maintains stable visual line count when scrolling and typing with word wrap 2`] = `\n\"                                   \n ┌───────────────────────────────┐ \n │     Ctrl+Y to redo            │ \n │  36                           │ \n │  37 VIEW:                     │ \n │  38   • Shift+W to toggle     │ \n │     wrap mode (word/char/     │ \n │     none)                     │ \n │  39   • Shift+L to toggle     │ \n │     line numbers              │ \n │  40                           │ \n │  41 FEATURES:                 │ \n │  42   ✓ Grapheme-aware        │ \n │     cursor movement           │ \n │  43   ✓ Unicode (emoji 🌟     │ \n │     and CJK 世界, 你好世界,   │ \n │     中文, 한글)               │ \n │  44   ✓ Incremental editing   │ \n │  45   ✓ Text wrapping and     │ \n │     viewport management       │ \n │  46   ✓ Undo/redo support     │ \n │  47   ✓ Word-based            │ \n │     navigation and deletion   │ \n │  48   ✓ Text selection with   │ \n │     shift keys                │ \n │  49 a                         │ \n │  50 Press ESC to return to    │ \n │     main menu                 │ \n └───────────────────────────────┘ \n                                   \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`Textarea - Rendering Tests Wrapping should render with tab indicator correctly 1`] = `\n\"Line 1→ Tabbed                                                                  \nLine 2→ → Double tab                                                            \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`Textarea - Rendering Tests Textarea Content Snapshots should render basic text content correctly 1`] = `\n\"                                                                                \n                                                                                \n                                                                                \n     Hello World                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`Textarea - Rendering Tests Textarea Content Snapshots should render multiline text content correctly 1`] = `\n\"                                                                                \n Line 1: Hello                                                                  \n Line 2: World                                                                  \n Line 3: Testing                                                                \n Line 4: Multiline                                                              \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`Textarea - Rendering Tests Textarea Content Snapshots should render text with character wrapping correctly 1`] = `\n\"This is a very                                                                  \nlong text that                                                                  \nshould wrap to                                                                  \nmultiple lines                                                                  \nwhen wrap is en                                                                 \nabled                                                                           \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`Textarea - Rendering Tests Textarea Content Snapshots should render text with word wrapping and punctuation 1`] = `\n\"Hello,World.                                                                    \nTest-                                                                           \nExample/                                                                        \nPath with                                                                       \nvarious                                                                         \npunctuation                                                                     \nmarks!                                                                          \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`Textarea - Rendering Tests Textarea Content Snapshots should render placeholder when creating textarea with placeholder directly 1`] = `\n\"                                                                                \n Enter text here...                                                             \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`Textarea - Rendering Tests Textarea Content Snapshots should render placeholder when set programmatically after creation 1`] = `\n\"                                                                                \n Type something...                                                              \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`Textarea - Rendering Tests Textarea Content Snapshots should resize correctly when typing return as first input with placeholder 1`] = `\n\"                                        \n ┌──────────────────────────────────────\n │                                      \n │                                      \n └──────────────────────────────────────\n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea - Rendering Tests Width/Height Setter Layout Tests should not shrink box when width is set via setter 1`] = `\n\"┌────────────────────────────┐          \n│>    Content that takes up  │          \n│     space                  │          \n└────────────────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea - Rendering Tests Width/Height Setter Layout Tests should not shrink box when height is set via setter in column layout with textarea 1`] = `\n\"┌───────────────────────┐     \n│Header                 │     \n│                       │     \n│                       │     \n│Line1                  │     \n│Line2                  │     \n│Line3                  │     \n│Footer                 │     \n│                       │     \n└───────────────────────┘     \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n\nexports[`Textarea - Rendering Tests Width/Height Setter Layout Tests should not shrink box when minWidth is set via setter 1`] = `\n\"┌────────────────────────────┐          \n│>    Content that takes up  │          \n│     space                  │          \n└────────────────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea - Rendering Tests Width/Height Setter Layout Tests should not shrink box when minHeight is set via setter in column layout with textarea 1`] = `\n\"┌───────────────────────┐     \n│Header                 │     \n│                       │     \n│                       │     \n│Line1                  │     \n│Line2                  │     \n│Line3                  │     \n│Footer                 │     \n│                       │     \n└───────────────────────┘     \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n\nexports[`Textarea - Rendering Tests Width/Height Setter Layout Tests should not shrink box when width is set from undefined via setter 1`] = `\n\"┌────────────────────────────┐          \n│>    Content that takes up  │          \n│     space                  │          \n└────────────────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea - Rendering Tests Absolute Positioned Box with Textarea should render textarea in absolute positioned box with padding and borders correctly 1`] = `\n\"                                                                                \n                                                                                \n                  │                                                          │  \n                  │                                                          │  \n                  │    Important Notification                                │  \n                  │                                                          │  \n                  │                                                          │  \n                  │    This is a longer message that should wrap properly within\n                  │                                                          │  \n                  │                                                          │  \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n\nexports[`Textarea - Rendering Tests Absolute Positioned Box with Textarea should render textarea fully visible in absolute positioned box at various positions 1`] = `\n\"                                                                                                    \n                                                           ┌──────────────────────────────────────┐ \n                                                           │ Error: File not found in the         │ \n                                                           │ specified directory path             │ \n                                                           └──────────────────────────────────────┘ \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n                                                                                                    \n ───────────────────────────────────                                                                \n  Success: Operation completed                                                                      \n  successfully!                                                                                     \n ───────────────────────────────────                                                                \n                                                                                                    \n\"\n`;\n\nexports[`Textarea - Rendering Tests Absolute Positioned Box with Textarea should handle width:100% textarea in absolute positioned box with constrained maxWidth 1`] = `\n\"                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n             This is an extremely long piece of text                  \n             that needs to wrap multiple times within                 \n             the constrained width of the absolutely                  \n             positioned container box with significant                \n             padding on all sides.                                    \n                                                                      \n                                                                      \n                                                                      \n\"\n`;\n\nexports[`Textarea - Rendering Tests Absolute Positioned Box with Textarea should render multiple textarea elements in absolute positioned box with proper spacing 1`] = `\n\"                                                                                          \n                                                                                          \n                                                                                          \n                                        ┌───────────────────────────────────────────┐     \n                                        │                                           │     \n                                        │  System Update                            │     \n                                        │                                           │     \n                                        │  A new version is available with bug      │     \n                                        │  fixes and performance improvements.      │     \n                                        │                                           │     \n                                        │  Click to install                         │     \n                                        │                                           │     \n                                        └───────────────────────────────────────────┘     \n                                                                                          \n                                                                                          \n                                                                                          \n                                                                                          \n                                                                                          \n                                                                                          \n                                                                                          \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/markdown-parser.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { Lexer } from \"marked\"\nimport { parseMarkdownIncremental, type ParseState } from \"../markdown-parser.js\"\n\ntest(\"first parse returns all tokens\", () => {\n  const state = parseMarkdownIncremental(\"# Hello\\n\\nParagraph\", null)\n\n  expect(state.content).toBe(\"# Hello\\n\\nParagraph\")\n  expect(state.tokens.length).toBeGreaterThan(0)\n  expect(state.tokens[0].type).toBe(\"heading\")\n})\n\ntest(\"reuses unchanged tokens when appending content\", () => {\n  const state1 = parseMarkdownIncremental(\"# Hello\\n\\nPara 1\\n\\n\", null)\n  const state2 = parseMarkdownIncremental(\"# Hello\\n\\nPara 1\\n\\nPara 2\", state1, 0) // No trailing unstable\n\n  // First tokens should be same object reference (reused)\n  expect(state2.tokens[0]).toBe(state1.tokens[0]) // heading\n  expect(state2.tokens[1]).toBe(state1.tokens[1]) // paragraph\n})\n\ntest(\"trailing unstable tokens are re-parsed\", () => {\n  const state1 = parseMarkdownIncremental(\"# Hello\\n\\nPara 1\\n\\n\", null)\n  const state2 = parseMarkdownIncremental(\"# Hello\\n\\nPara 1\\n\\nPara 2\", state1, 2)\n\n  // With trailingUnstable=2, last 2 tokens from state1 should be re-parsed\n  // state1 has: heading, paragraph, space (3 tokens)\n  // With trailing=2, only first token (heading) is stable\n  // So heading token should NOT be reused (since we only have 3 tokens and skip last 2)\n  // Actually with 3 tokens and trailingUnstable=2, we keep 1 token stable\n  expect(state2.tokens.length).toBeGreaterThan(0)\n  // The new tokens are re-parsed versions\n  expect(state2.tokens[0].type).toBe(\"heading\")\n})\n\ntest(\"handles content that diverges from start\", () => {\n  const state1 = parseMarkdownIncremental(\"# Hello\", null)\n  const state2 = parseMarkdownIncremental(\"## World\", state1)\n\n  // Content changed from start, no tokens can be reused\n  expect(state2.tokens[0]).not.toBe(state1.tokens[0])\n  expect(state2.tokens[0].type).toBe(\"heading\")\n})\n\ntest(\"handles empty content\", () => {\n  const state = parseMarkdownIncremental(\"\", null)\n\n  expect(state.content).toBe(\"\")\n  expect(state.tokens).toEqual([])\n})\n\ntest(\"handles empty previous state\", () => {\n  const prevState: ParseState = { content: \"\", tokens: [] }\n  const state = parseMarkdownIncremental(\"# Hello\", prevState)\n\n  expect(state.tokens.length).toBeGreaterThan(0)\n  expect(state.tokens[0].type).toBe(\"heading\")\n})\n\ntest(\"handles content truncation\", () => {\n  const state1 = parseMarkdownIncremental(\"# Hello\\n\\nPara 1\\n\\nPara 2\", null)\n  const state2 = parseMarkdownIncremental(\"# Hello\", state1)\n\n  expect(state2.tokens.length).toBe(1)\n  expect(state2.tokens[0].type).toBe(\"heading\")\n})\n\ntest(\"handles partial token match\", () => {\n  const state1 = parseMarkdownIncremental(\"# Hello World\", null)\n  const state2 = parseMarkdownIncremental(\"# Hello\", state1)\n\n  // Token at start doesn't match exactly, so it's re-parsed\n  expect(state2.tokens[0]).not.toBe(state1.tokens[0])\n})\n\ntest(\"handles multiple stable tokens with explicit boundaries\", () => {\n  // Use content with clear token boundaries that won't change\n  const content1 = \"Para 1\\n\\nPara 2\\n\\nPara 3\\n\\n\"\n  const state1 = parseMarkdownIncremental(content1, null)\n\n  const content2 = content1 + \"Para 4\"\n  const state2 = parseMarkdownIncremental(content2, state1, 0)\n\n  // All original tokens should be reused (same object reference)\n  for (let i = 0; i < state1.tokens.length; i++) {\n    expect(state2.tokens[i]).toBe(state1.tokens[i])\n  }\n  // And there should be a new token at the end\n  expect(state2.tokens.length).toBe(state1.tokens.length + 1)\n})\n\ntest(\"code blocks are parsed correctly\", () => {\n  const state = parseMarkdownIncremental(\"```js\\nconst x = 1;\\n```\", null)\n\n  const codeToken = state.tokens.find((t) => t.type === \"code\")\n  expect(codeToken).toBeDefined()\n  expect((codeToken as any).lang).toBe(\"js\")\n})\n\ntest(\"streaming scenario with incremental typing\", () => {\n  let state: ParseState | null = null\n\n  // Simulate typing character by character\n  state = parseMarkdownIncremental(\"#\", state, 2)\n  expect(state.tokens.length).toBe(1)\n\n  state = parseMarkdownIncremental(\"# \", state, 2)\n  state = parseMarkdownIncremental(\"# H\", state, 2)\n  state = parseMarkdownIncremental(\"# He\", state, 2)\n  state = parseMarkdownIncremental(\"# Hel\", state, 2)\n  state = parseMarkdownIncremental(\"# Hell\", state, 2)\n  state = parseMarkdownIncremental(\"# Hello\", state, 2)\n\n  expect(state.tokens[0].type).toBe(\"heading\")\n  expect((state.tokens[0] as any).text).toBe(\"Hello\")\n})\n\ntest(\"token identity is preserved for stable tokens\", () => {\n  // Create initial state with multiple paragraphs\n  const state1 = parseMarkdownIncremental(\"A\\n\\nB\\n\\nC\\n\\n\", null)\n\n  // Append content - with trailingUnstable=0, all tokens should be reused\n  const state2 = parseMarkdownIncremental(\"A\\n\\nB\\n\\nC\\n\\nD\", state1, 0)\n\n  // Verify token identity (same object reference)\n  expect(state2.tokens[0]).toBe(state1.tokens[0])\n  expect(state2.tokens[1]).toBe(state1.tokens[1])\n  expect(state2.tokens[2]).toBe(state1.tokens[2])\n})\n\ntest(\"trailingUnstable re-parses trailing table when new rows are appended\", () => {\n  const content1 = \"| A |\\n|---|\\n| 1 |\"\n  const state1 = parseMarkdownIncremental(content1, null, 2)\n  const table1 = state1.tokens.find((token) => token.type === \"table\") as any\n\n  expect(table1).toBeDefined()\n  expect(table1.rows.length).toBe(1)\n\n  const content2 = \"| A |\\n|---|\\n| 1 |\\n| 2 |\"\n  const state2 = parseMarkdownIncremental(content2, state1, 2)\n  const table2 = state2.tokens.find((token) => token.type === \"table\") as any\n\n  expect(table2).toBeDefined()\n  expect(table2.rows.length).toBe(2)\n  expect(table2).not.toBe(table1)\n})\n\ntest(\"trailingUnstable updates trailing table rows in multi-table markdown\", () => {\n  const table1Markdown = \"| T1 |\\n|---|\\n| a |\\n| b |\"\n  const table2Markdown = \"| T2 |\\n|---|\\n| 1 |\\n| 2 |\"\n\n  const content1 = `${table1Markdown}\\n\\n${table2Markdown}`\n  const state1 = parseMarkdownIncremental(content1, null, 2)\n  const tables1 = state1.tokens.filter((token) => token.type === \"table\") as any[]\n\n  expect(tables1.length).toBe(2)\n  expect(tables1[0].rows.length).toBe(2)\n  expect(tables1[1].rows.length).toBe(2)\n\n  const content2 = `${table1Markdown}\\n\\n${table2Markdown}\\n| 3 |`\n  const state2 = parseMarkdownIncremental(content2, state1, 2)\n  const tables2 = state2.tokens.filter((token) => token.type === \"table\") as any[]\n\n  expect(tables2.length).toBe(2)\n  expect(tables2[0].rows.length).toBe(2)\n  expect(tables2[1]).not.toBe(tables1[1])\n  expect(tables2[1].rows.length).toBe(3)\n})\n\ntest(\"falls back to full re-parse when incremental tail parse fails\", () => {\n  const content1 = \"| A |\\n|---|\\n| 1 |\"\n  const content2 = \"| A |\\n|---|\\n| 1 |\\n| 2 |\"\n  const state1 = parseMarkdownIncremental(content1, null, 2)\n\n  const lexerRef = Lexer as unknown as { lex: typeof Lexer.lex }\n  const originalLex = lexerRef.lex\n  let lexCalls = 0\n\n  lexerRef.lex = ((src, options) => {\n    lexCalls += 1\n    if (lexCalls === 1) {\n      throw new Error(\"incremental tail parse failed\")\n    }\n    return originalLex(src, options)\n  }) as typeof Lexer.lex\n\n  try {\n    const state2 = parseMarkdownIncremental(content2, state1, 2)\n    const table = state2.tokens.find((token) => token.type === \"table\") as any\n\n    expect(lexCalls).toBeGreaterThanOrEqual(2)\n    expect(table).toBeDefined()\n    expect(table.rows.length).toBe(2)\n  } finally {\n    lexerRef.lex = originalLex\n  }\n})\n\ntest(\"returns empty token list when both incremental and full parse fail\", () => {\n  const content1 = \"| A |\\n|---|\\n| 1 |\"\n  const content2 = \"| A |\\n|---|\\n| 1 |\\n| 2 |\"\n  const state1 = parseMarkdownIncremental(content1, null, 2)\n\n  const lexerRef = Lexer as unknown as { lex: typeof Lexer.lex }\n  const originalLex = lexerRef.lex\n\n  lexerRef.lex = (() => {\n    throw new Error(\"parse failed\")\n  }) as typeof Lexer.lex\n\n  try {\n    const state2 = parseMarkdownIncremental(content2, state1, 2)\n    expect(state2.tokens).toEqual([])\n  } finally {\n    lexerRef.lex = originalLex\n  }\n})\n"
  },
  {
    "path": "packages/core/src/renderables/__tests__/renderable-test-utils.ts",
    "content": "import { TextareaRenderable } from \"../Textarea.js\"\nimport { type TestRenderer } from \"../../testing/test-renderer.js\"\nimport { type TextareaOptions } from \"../Textarea.js\"\nimport type { DiffRenderable } from \"../Diff.js\"\nimport type { CodeRenderable } from \"../Code.js\"\nimport type { MockTreeSitterClient } from \"../../testing/mock-tree-sitter-client.js\"\nimport type { ManualClock } from \"../../testing/manual-clock.js\"\n\nexport async function createTextareaRenderable(\n  renderer: TestRenderer,\n  renderOnce: () => Promise<void>,\n  options: TextareaOptions,\n): Promise<{ textarea: TextareaRenderable; root: any }> {\n  const textareaRenderable = new TextareaRenderable(renderer, { left: 0, top: 0, ...options })\n  renderer.root.add(textareaRenderable)\n  await renderOnce()\n\n  return { textarea: textareaRenderable, root: renderer.root }\n}\n\n// Settle Diff highlighting deterministically. Each iteration:\n// 1. Render twice — the first render may trigger Diff.requestRebuild via microtask\n//    (runs during renderOnce's internal awaits), which calls requestRender while\n//    rendering=true, setting immediateRerenderRequested. The resulting re-render\n//    is scheduled via clock.setTimeout (ManualClock), so needs a second renderOnce.\n// 2. Resolve all pending highlights (proper signal via mock)\n// 3. Await Code.highlightingDone on both sides (proper signal from Code)\n// Loop exits when mock has no more pending requests (state-based, not count-based).\nexport async function settleDiffHighlighting(\n  diff: DiffRenderable,\n  client: MockTreeSitterClient,\n  render: () => Promise<void>,\n) {\n  const MAX = 15\n  for (let i = 0; i < MAX; i++) {\n    await render()\n    await render()\n    if (!client.isHighlighting()) break\n    client.resolveAllHighlightOnce()\n    const left: CodeRenderable | null = (diff as any).leftCodeRenderable\n    const right: CodeRenderable | null = (diff as any).rightCodeRenderable\n    if (left) await left.highlightingDone\n    if (right) await right.highlightingDone\n  }\n}\n\n// Simulate the passage of time by advancing a ManualClock and rendering frames.\n// Useful for testing animations, scroll momentum, and other time-dependent behavior.\nexport async function simulateFrames(\n  clock: ManualClock,\n  renderOnce: () => Promise<void>,\n  ms: number,\n  frameInterval: number = 50,\n): Promise<void> {\n  const frames = Math.ceil(ms / frameInterval)\n  for (let i = 0; i < frames; i++) {\n    clock.advance(frameInterval)\n    await renderOnce()\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/composition/README.md",
    "content": "# Composition Exploration\n\nThis is a simple exploration of how to compose renderables into a tree.\n\nIt's not react and not reactive, it's just a way to compose renderables\nand mount them into a parent container.\n\nIt's a work in progress and not in any way meant to be a replacement for react.\n"
  },
  {
    "path": "packages/core/src/renderables/composition/VRenderable.ts",
    "content": "import { Renderable, type RenderableOptions } from \"../../Renderable.js\"\nimport type { OptimizedBuffer } from \"../../buffer.js\"\nimport type { RenderContext } from \"../../types.js\"\n\nexport interface VRenderableOptions extends RenderableOptions<VRenderable> {\n  render?: (\n    this: VRenderable | VRenderableOptions,\n    buffer: OptimizedBuffer,\n    deltaTime: number,\n    renderable: VRenderable,\n  ) => void\n}\n\n/**\n * A generic renderable that accepts a custom render function as a prop.\n * This allows functional constructs to specify custom rendering behavior\n * without needing to subclass Renderable.\n */\nexport class VRenderable extends Renderable {\n  private options: VRenderableOptions\n\n  constructor(ctx: RenderContext, options: VRenderableOptions) {\n    super(ctx, options)\n    this.options = options\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    if (this.options.render) {\n      this.options.render.call(this.options, buffer, deltaTime, this)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderables/composition/constructs.ts",
    "content": "import {\n  ASCIIFontRenderable,\n  BoxRenderable,\n  CodeRenderable,\n  InputRenderable,\n  ScrollBoxRenderable,\n  SelectRenderable,\n  TabSelectRenderable,\n  TextRenderable,\n  VRenderable,\n  FrameBufferRenderable,\n  type ASCIIFontOptions,\n  type BoxOptions,\n  type CodeOptions,\n  type TextOptions,\n  type VRenderableOptions,\n  type InputRenderableOptions,\n  type ScrollBoxOptions,\n  type SelectRenderableOptions,\n  type TabSelectRenderableOptions,\n  type FrameBufferOptions,\n} from \"..//index.js\"\nimport { TextNodeRenderable, type TextNodeOptions } from \"../TextNode.js\"\nimport { h, type VChild } from \"./vnode.js\"\nimport { TextAttributes } from \"../../types.js\"\nimport type { RGBA } from \"../../lib/RGBA.js\"\n\nexport function Generic(props?: VRenderableOptions, ...children: VChild[]) {\n  return h(VRenderable, props || {}, ...children)\n}\n\nexport function Box(props?: BoxOptions, ...children: VChild[]) {\n  return h(BoxRenderable, props || {}, ...children)\n}\n\nexport function Text(props?: TextOptions & { content?: any }, ...children: VChild[] | TextNodeRenderable[]) {\n  return h(TextRenderable, props || {}, ...(children as VChild[]))\n}\n\nexport function ASCIIFont(props?: ASCIIFontOptions, ...children: VChild[]) {\n  return h(ASCIIFontRenderable, props || {}, ...children)\n}\n\nexport function Input(props?: InputRenderableOptions, ...children: VChild[]) {\n  return h(InputRenderable, props || {}, ...children)\n}\n\nexport function Select(props?: SelectRenderableOptions, ...children: VChild[]) {\n  return h(SelectRenderable, props || {}, ...children)\n}\n\nexport function TabSelect(props?: TabSelectRenderableOptions, ...children: VChild[]) {\n  return h(TabSelectRenderable, props || {}, ...children)\n}\n\nexport function FrameBuffer(props: FrameBufferOptions, ...children: VChild[]) {\n  return h(FrameBufferRenderable, props, ...children)\n}\n\nexport function Code(props: CodeOptions, ...children: VChild[]) {\n  return h(CodeRenderable, props, ...children)\n}\n\nexport function ScrollBox(props?: ScrollBoxOptions, ...children: VChild[]) {\n  return h(ScrollBoxRenderable, props || {}, ...children)\n}\n\ninterface StyledTextProps extends Omit<TextNodeOptions, \"attributes\"> {\n  attributes?: number\n}\n\nfunction StyledText(props?: StyledTextProps, ...children: (string | TextNodeRenderable)[]): TextNodeRenderable {\n  const styledProps = props as StyledTextProps\n  const textNodeOptions: TextNodeOptions = {\n    ...styledProps,\n    attributes: styledProps?.attributes ?? 0,\n  }\n\n  const textNode = new TextNodeRenderable(textNodeOptions)\n\n  for (const child of children) {\n    textNode.add(child)\n  }\n\n  return textNode\n}\n\n// Text styling convenience functions - these create TextNodeRenderable instances that can be nested and stacked\nexport const vstyles = {\n  // Basic text styles\n  bold: (...children: (string | TextNodeRenderable)[]) => StyledText({ attributes: TextAttributes.BOLD }, ...children),\n  italic: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.ITALIC }, ...children),\n  underline: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.UNDERLINE }, ...children),\n  dim: (...children: (string | TextNodeRenderable)[]) => StyledText({ attributes: TextAttributes.DIM }, ...children),\n  blink: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.BLINK }, ...children),\n  inverse: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.INVERSE }, ...children),\n  hidden: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.HIDDEN }, ...children),\n  strikethrough: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.STRIKETHROUGH }, ...children),\n\n  // Combined styles\n  boldItalic: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.BOLD | TextAttributes.ITALIC }, ...children),\n  boldUnderline: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE }, ...children),\n  italicUnderline: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.ITALIC | TextAttributes.UNDERLINE }, ...children),\n  boldItalicUnderline: (...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes: TextAttributes.BOLD | TextAttributes.ITALIC | TextAttributes.UNDERLINE }, ...children),\n\n  // Color helpers\n  color: (color: string | RGBA, ...children: (string | TextNodeRenderable)[]) => StyledText({ fg: color }, ...children),\n  bgColor: (bgColor: string | RGBA, ...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ bg: bgColor }, ...children),\n  fg: (color: string | RGBA, ...children: (string | TextNodeRenderable)[]) => StyledText({ fg: color }, ...children),\n  bg: (bgColor: string | RGBA, ...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ bg: bgColor }, ...children),\n\n  // Custom styling function\n  styled: (attributes: number = 0, ...children: (string | TextNodeRenderable)[]) =>\n    StyledText({ attributes }, ...children),\n}\n"
  },
  {
    "path": "packages/core/src/renderables/composition/vnode.ts",
    "content": "import { isRenderable, Renderable, type RenderableOptions } from \"../../Renderable.js\"\nimport type { RenderContext } from \"../../types.js\"\nimport util from \"node:util\"\n\nexport type VChild = VNode | Renderable | VChild[] | null | undefined | false\n\nexport interface PendingCall {\n  method: string\n  args: any[]\n  isProperty?: boolean\n}\n\nconst BrandedVNode: unique symbol = Symbol.for(\"@opentui/core/VNode\")\n\nexport interface VNode<P = any, C = VChild[]> {\n  [BrandedVNode]: true\n  type: Construct<P>\n  props?: P\n  children?: C\n  __delegateMap?: Record<string, string>\n  __pendingCalls?: PendingCall[]\n}\n\n// Type that represents a VNode with Renderable methods available for chaining\nexport type ProxiedVNode<TCtor extends RenderableConstructor<any>> = VNode<\n  TCtor extends RenderableConstructor<infer P> ? P : any\n> & {\n  [K in keyof InstanceType<TCtor>]: InstanceType<TCtor>[K] extends (...args: infer Args) => any\n    ? (...args: Args) => ProxiedVNode<TCtor>\n    : InstanceType<TCtor>[K]\n}\n\nexport interface RenderableConstructor<P extends RenderableOptions<any> = RenderableOptions<any>> {\n  new (ctx: RenderContext, options: P): Renderable\n}\n\nexport type FunctionalConstruct<P = any> = (props: P, children?: VChild[]) => VNode\n\nexport type Construct<P = any> =\n  | RenderableConstructor<P extends RenderableOptions<any> ? P : never>\n  | FunctionalConstruct<P>\n\nfunction isRenderableConstructor<P extends RenderableOptions<any> = RenderableOptions<any>>(\n  value: any,\n): value is RenderableConstructor<P> {\n  return typeof value === \"function\" && value.prototype && Renderable.prototype.isPrototypeOf(value.prototype)\n}\n\nfunction flattenChildren(children: VChild[]): VChild[] {\n  const result: VChild[] = []\n  for (const child of children) {\n    if (Array.isArray(child)) {\n      result.push(...flattenChildren(child))\n    } else if (child !== null && child !== undefined && child !== false) {\n      result.push(child)\n    }\n  }\n  return result\n}\n\n// Overloads for proper typing\nexport function h<TCtor extends RenderableConstructor<any>>(\n  type: TCtor,\n  props?: TCtor extends RenderableConstructor<infer P> ? P : never,\n  ...children: VChild[]\n): ProxiedVNode<TCtor>\nexport function h<P>(type: FunctionalConstruct<P>, props?: P, ...children: VChild[]): VNode<P>\nexport function h<P>(type: Construct<P>, props?: P, ...children: VChild[]): VNode<P> | ProxiedVNode<any>\nexport function h<P>(type: Construct<P>, props?: P, ...children: VChild[]): any {\n  if (typeof type !== \"function\") {\n    throw new TypeError(\"h() received an invalid vnode type\")\n  }\n\n  const vnode: VNode<P> = {\n    [BrandedVNode]: true,\n    type,\n    props,\n    children: flattenChildren(children),\n    __pendingCalls: [],\n  }\n\n  if (isRenderableConstructor(type)) {\n    return new Proxy(vnode, {\n      get(target, prop, receiver) {\n        // Return VNode properties directly\n        if (prop in target) {\n          return Reflect.get(target, prop, receiver)\n        }\n\n        if (typeof prop === \"string\") {\n          const prototype = type.prototype\n          const hasMethod =\n            prototype &&\n            (typeof prototype[prop] === \"function\" ||\n              Object.getOwnPropertyDescriptor(prototype, prop) ||\n              Object.getOwnPropertyDescriptor(Object.getPrototypeOf(prototype), prop))\n\n          if (hasMethod) {\n            return (...args: any[]) => {\n              target.__pendingCalls = target.__pendingCalls || []\n              target.__pendingCalls.push({ method: prop, args })\n              return target\n            }\n          }\n        }\n\n        return Reflect.get(target, prop, receiver)\n      },\n\n      set(target, prop, value, receiver) {\n        if (typeof prop === \"string\" && isRenderableConstructor(type)) {\n          const prototype = type.prototype\n          const descriptor =\n            Object.getOwnPropertyDescriptor(prototype, prop) ||\n            Object.getOwnPropertyDescriptor(Object.getPrototypeOf(prototype), prop)\n\n          if (descriptor && descriptor.set) {\n            target.__pendingCalls = target.__pendingCalls || []\n            target.__pendingCalls.push({ method: prop, args: [value], isProperty: true })\n            return true\n          }\n        }\n\n        return Reflect.set(target, prop, value, receiver)\n      },\n    })\n  }\n\n  return vnode\n}\n\nexport function isVNode(node: any): node is VNode {\n  return node && node[BrandedVNode]\n}\n\nexport function maybeMakeRenderable(\n  ctx: RenderContext,\n  node: Renderable | VNode<any, any[]> | unknown,\n): Renderable | null {\n  if (isRenderable(node)) return node\n  if (isVNode(node)) return instantiate(ctx, node)\n  if (process.env.NODE_ENV !== \"production\") {\n    console.warn(\"maybeMakeRenderable received an invalid node\", util.inspect(node, { depth: 2 }))\n  }\n  return null\n}\n\nexport function wrapWithDelegates<T extends InstanceType<RenderableConstructor>>(\n  instance: T,\n  delegateMap: Record<string, string> | undefined,\n): T {\n  if (!delegateMap || Object.keys(delegateMap).length === 0) return instance\n\n  const descendantCache = new Map<string, Renderable | undefined>()\n\n  const getDescendant = (id: string): Renderable | undefined => {\n    if (descendantCache.has(id)) {\n      const cached = descendantCache.get(id)\n      if (cached !== undefined) {\n        return cached\n      }\n    }\n    const descendant = (instance as Renderable).findDescendantById(id)\n    if (descendant) {\n      descendantCache.set(id, descendant)\n    }\n    return descendant\n  }\n\n  const proxy = new Proxy(instance as any, {\n    get(target, prop, receiver) {\n      if (typeof prop === \"string\" && delegateMap[prop]) {\n        const host = getDescendant(delegateMap[prop])\n        if (host) {\n          const value = (host as any)[prop]\n          if (typeof value === \"function\") {\n            return value.bind(host)\n          }\n          return value\n        }\n      }\n      return Reflect.get(target, prop, receiver)\n    },\n    set(target, prop, value, receiver) {\n      if (typeof prop === \"string\" && delegateMap[prop]) {\n        const host = getDescendant(delegateMap[prop])\n        if (host) {\n          return Reflect.set(host as any, prop, value)\n        }\n      }\n      return Reflect.set(target, prop, value, receiver)\n    },\n  })\n  return proxy\n}\n\nexport type InstantiateFn<NodeType extends VNode | Renderable> = Renderable & { __node?: NodeType }\n\nexport function instantiate<NodeType extends VNode | Renderable>(\n  ctx: RenderContext,\n  node: NodeType,\n): InstantiateFn<NodeType> {\n  if (isRenderable(node)) return node\n\n  if (!node || typeof node !== \"object\") {\n    throw new TypeError(\"mount() received an invalid vnode\")\n  }\n\n  const vnode = node as VNode\n  const { type, props } = vnode\n  const children = flattenChildren(vnode.children || [])\n  const delegateMap = (vnode as any).__delegateMap as Record<string, string> | undefined\n\n  if (isRenderableConstructor(type)) {\n    const instance = new type(ctx, (props || {}) as any)\n\n    for (const child of children) {\n      if (isRenderable(child)) {\n        instance.add(child)\n      } else {\n        const mounted = instantiate(ctx, child as NodeType)\n        instance.add(mounted)\n      }\n    }\n\n    const delegatedInstance = wrapWithDelegates(instance, delegateMap)\n\n    const pendingCalls = (vnode as any).__pendingCalls as PendingCall[] | undefined\n    if (pendingCalls) {\n      for (const call of pendingCalls) {\n        if (call.isProperty) {\n          ;(delegatedInstance as any)[call.method] = call.args[0]\n        } else {\n          ;(delegatedInstance as any)[call.method].apply(delegatedInstance, call.args)\n        }\n      }\n    }\n\n    return delegatedInstance\n  }\n\n  // Functional construct: resolve to a concrete vnode and mount it\n  const resolved = (type as FunctionalConstruct)(props || ({} as any), children)\n  const inst = instantiate(ctx, resolved)\n\n  return wrapWithDelegates(inst, delegateMap) as InstantiateFn<NodeType>\n}\n\nexport type DelegateMap<T> = Partial<Record<keyof T, string>>\n\nexport type ValidateShape<Given, AllowedKeys> = {\n  [K in keyof Given]: K extends keyof AllowedKeys ? NonNullable<Given[K]> : never\n}\n\ntype InferNode<T> = T extends InstantiateFn<infer U> ? U : never\n\nexport function delegate<\n  Factory extends InstantiateFn<any>,\n  InnerNode extends InferNode<Factory>,\n  TargetMap extends Record<keyof InnerNode, string>,\n  const Mapping extends Partial<TargetMap>,\n>(mapping: ValidateShape<Mapping, TargetMap>, vnode: Factory): Renderable\n\nexport function delegate<\n  ConstructorType extends RenderableConstructor<any>,\n  TargetMap extends Record<keyof InstanceType<ConstructorType>, string>,\n  const Mapping extends Partial<TargetMap>,\n>(mapping: ValidateShape<Mapping, TargetMap>, vnode: ProxiedVNode<ConstructorType>): ProxiedVNode<ConstructorType>\n\nexport function delegate<\n  ConstructorType extends RenderableConstructor<any>,\n  const Mapping extends DelegateMap<InstanceType<ConstructorType>>,\n>(mapping: ValidateShape<Mapping, string>, vnode: VNode & { type: ConstructorType }): VNode\n\n/**\n * Controlled delegation that routes selected properties/methods\n * to a descendant renderable identified by ID.\n */\nexport function delegate<NodeType extends VNode | Renderable | InstantiateFn<any>>(\n  mapping: Record<string, string>,\n  vnode: NodeType,\n): VNode | Renderable {\n  if (isRenderable(vnode)) {\n    return wrapWithDelegates(vnode, mapping)\n  }\n  if (!vnode || typeof vnode !== \"object\") return vnode\n  vnode.__delegateMap = { ...(vnode.__delegateMap || {}), ...mapping }\n  return vnode\n}\n"
  },
  {
    "path": "packages/core/src/renderables/index.ts",
    "content": "export * from \"./ASCIIFont.js\"\nexport * from \"./Box.js\"\nexport * from \"./Code.js\"\nexport * from \"./composition/constructs.js\"\nexport * from \"./composition/VRenderable.js\"\nexport * from \"./composition/vnode.js\"\nexport * from \"./Diff.js\"\nexport * from \"./FrameBuffer.js\"\nexport * from \"./Input.js\"\nexport * from \"./LineNumberRenderable.js\"\nexport * from \"./Markdown.js\"\nexport * from \"./ScrollBar.js\"\nexport * from \"./ScrollBox.js\"\nexport * from \"./Select.js\"\nexport * from \"./Slider.js\"\nexport * from \"./TextTable.js\"\nexport * from \"./TabSelect.js\"\nexport * from \"./Text.js\"\nexport * from \"./TimeToFirstDraw.js\"\nexport * from \"./TextBufferRenderable.js\"\nexport * from \"./TextNode.js\"\nexport * from \"./Textarea.js\"\n"
  },
  {
    "path": "packages/core/src/renderables/markdown-parser.ts",
    "content": "import { Lexer, type MarkedToken } from \"marked\"\n\nexport interface ParseState {\n  content: string\n  tokens: MarkedToken[]\n}\n\n/**\n * Incrementally parse markdown, reusing unchanged tokens from previous parse.\n * Compares token.raw at each offset - matching tokens keep same object reference.\n */\nexport function parseMarkdownIncremental(\n  newContent: string,\n  prevState: ParseState | null,\n  trailingUnstable: number = 2,\n): ParseState {\n  if (!prevState || prevState.tokens.length === 0) {\n    try {\n      const tokens = Lexer.lex(newContent, { gfm: true }) as MarkedToken[]\n      return { content: newContent, tokens }\n    } catch {\n      return { content: newContent, tokens: [] }\n    }\n  }\n\n  // Find how many tokens from start are unchanged\n  let offset = 0\n  let reuseCount = 0\n\n  for (const token of prevState.tokens) {\n    const tokenLength = token.raw.length\n    if (offset + tokenLength <= newContent.length && newContent.startsWith(token.raw, offset)) {\n      reuseCount++\n      offset += tokenLength\n    } else {\n      break\n    }\n  }\n\n  // Keep last N tokens unstable (e.g. \"# Hello\" might become \"# Hello World\")\n  reuseCount = Math.max(0, reuseCount - trailingUnstable)\n\n  offset = 0\n  for (let i = 0; i < reuseCount; i++) {\n    offset += prevState.tokens[i].raw.length\n  }\n\n  const stableTokens = prevState.tokens.slice(0, reuseCount)\n  const remainingContent = newContent.slice(offset)\n\n  if (!remainingContent) {\n    return { content: newContent, tokens: stableTokens }\n  }\n\n  try {\n    const newTokens = Lexer.lex(remainingContent, { gfm: true }) as MarkedToken[]\n    return { content: newContent, tokens: [...stableTokens, ...newTokens] }\n  } catch {\n    try {\n      const fullTokens = Lexer.lex(newContent, { gfm: true }) as MarkedToken[]\n      return { content: newContent, tokens: fullTokens }\n    } catch {\n      return { content: newContent, tokens: [] }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderer.ts",
    "content": "import { ANSI } from \"./ansi.js\"\nimport { Renderable, RootRenderable } from \"./Renderable.js\"\nimport {\n  DebugOverlayCorner,\n  type CursorStyleOptions,\n  type MousePointerStyle,\n  type RenderContext,\n  type ThemeMode,\n  type ViewportBounds,\n  type WidthMethod,\n} from \"./types.js\"\nimport { RGBA, parseColor, type ColorInput } from \"./lib/RGBA.js\"\nimport type { Pointer } from \"bun:ffi\"\nimport { OptimizedBuffer } from \"./buffer.js\"\nimport { resolveRenderLib, type RenderLib } from \"./zig.js\"\nimport { TerminalConsole, type ConsoleOptions, capture } from \"./console.js\"\nimport { type MouseEventType, type RawMouseEvent, type ScrollInfo } from \"./lib/parse.mouse.js\"\nimport { Selection } from \"./lib/selection.js\"\nimport { Clipboard, type ClipboardTarget } from \"./lib/clipboard.js\"\nimport { EventEmitter } from \"events\"\nimport { destroySingleton, hasSingleton, singleton } from \"./lib/singleton.js\"\nimport { getObjectsInViewport } from \"./lib/objects-in-viewport.js\"\nimport { KeyHandler, InternalKeyHandler } from \"./lib/KeyHandler.js\"\nimport { env, registerEnvVar } from \"./lib/env.js\"\nimport { getTreeSitterClient } from \"./lib/tree-sitter/index.js\"\nimport {\n  createTerminalPalette,\n  type TerminalPaletteDetector,\n  type TerminalColors,\n  type GetPaletteOptions,\n} from \"./lib/terminal-palette.js\"\nimport {\n  isCapabilityResponse,\n  isPixelResolutionResponse,\n  parsePixelResolution,\n} from \"./lib/terminal-capability-detection.js\"\nimport { type Clock, type TimerHandle, SystemClock } from \"./lib/clock.js\"\nimport { StdinParser, type StdinEvent, type StdinParserProtocolContext } from \"./lib/stdin-parser.js\"\n\nregisterEnvVar({\n  name: \"OTUI_DUMP_CAPTURES\",\n  description: \"Dump captured output when the renderer exits.\",\n  type: \"boolean\",\n  default: false,\n})\n\nregisterEnvVar({\n  name: \"OTUI_NO_NATIVE_RENDER\",\n  description: \"Disable native rendering. This will not actually output ansi and is useful for debugging.\",\n  type: \"boolean\",\n  default: false,\n})\n\nregisterEnvVar({\n  name: \"OTUI_USE_ALTERNATE_SCREEN\",\n  description: \"Use the terminal alternate screen buffer.\",\n  type: \"boolean\",\n  default: true,\n})\n\nregisterEnvVar({\n  name: \"OTUI_OVERRIDE_STDOUT\",\n  description: \"Override the stdout stream. This is useful for debugging.\",\n  type: \"boolean\",\n  default: true,\n})\n\nregisterEnvVar({\n  name: \"OTUI_DEBUG\",\n  description: \"Enable debug mode to capture all raw input for debugging purposes.\",\n  type: \"boolean\",\n  default: false,\n})\n\nregisterEnvVar({\n  name: \"OTUI_SHOW_STATS\",\n  description: \"Show the debug overlay at startup.\",\n  type: \"boolean\",\n  default: false,\n})\n\nexport interface CliRendererConfig {\n  stdin?: NodeJS.ReadStream\n  stdout?: NodeJS.WriteStream\n  remote?: boolean\n  testing?: boolean\n  exitOnCtrlC?: boolean\n  exitSignals?: NodeJS.Signals[]\n  forwardEnvKeys?: string[]\n  debounceDelay?: number\n  targetFps?: number\n  maxFps?: number\n  memorySnapshotInterval?: number\n  useThread?: boolean\n  gatherStats?: boolean\n  maxStatSamples?: number\n  consoleOptions?: Omit<ConsoleOptions, \"clock\">\n  postProcessFns?: ((buffer: OptimizedBuffer, deltaTime: number) => void)[]\n  enableMouseMovement?: boolean\n  useMouse?: boolean\n  autoFocus?: boolean\n  useAlternateScreen?: boolean\n  useConsole?: boolean\n  experimental_splitHeight?: number\n  useKittyKeyboard?: KittyKeyboardOptions | null\n  backgroundColor?: ColorInput\n  openConsoleOnError?: boolean\n  prependInputHandlers?: ((sequence: string) => boolean)[]\n  stdinParserMaxBufferBytes?: number\n  clock?: Clock\n  onDestroy?: () => void\n}\n\nexport type PixelResolution = {\n  width: number\n  height: number\n}\n\nconst DEFAULT_FORWARDED_ENV_KEYS = [\n  \"TMUX\",\n  \"TERM\",\n  \"OPENTUI_GRAPHICS\",\n  \"TERM_PROGRAM\",\n  \"TERM_PROGRAM_VERSION\",\n  \"ALACRITTY_SOCKET\",\n  \"ALACRITTY_LOG\",\n  \"COLORTERM\",\n  \"TERMUX_VERSION\",\n  \"VHS_RECORD\",\n  \"OPENTUI_FORCE_WCWIDTH\",\n  \"OPENTUI_FORCE_UNICODE\",\n  \"OPENTUI_FORCE_NOZWJ\",\n  \"OPENTUI_FORCE_EXPLICIT_WIDTH\",\n  \"WT_SESSION\",\n  \"STY\",\n] as const\n\n// Kitty keyboard protocol flags\n// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/\nconst KITTY_FLAG_DISAMBIGUATE = 0b1 // Report disambiguated escape codes\nconst KITTY_FLAG_EVENT_TYPES = 0b10 // Report event types (press/repeat/release)\nconst KITTY_FLAG_ALTERNATE_KEYS = 0b100 // Report alternate keys (e.g., numpad vs regular)\nconst KITTY_FLAG_ALL_KEYS_AS_ESCAPES = 0b1000 // Report all keys as escape codes\nconst KITTY_FLAG_REPORT_TEXT = 0b10000 // Report text associated with key events\n\nconst DEFAULT_STDIN_PARSER_MAX_BUFFER_BYTES = 64 * 1024 * 1024\n\n/**\n * Kitty Keyboard Protocol configuration options\n * See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement\n */\nexport interface KittyKeyboardOptions {\n  /** Disambiguate escape codes (fixes ESC timing, alt+key ambiguity, ctrl+c as event). Default: true */\n  disambiguate?: boolean\n  /** Report alternate keys (numpad, shifted, base layout) for cross-keyboard shortcuts. Default: true */\n  alternateKeys?: boolean\n  /** Report event types (press/repeat/release). Default: false */\n  events?: boolean\n  /** Report all keys as escape codes. Default: false */\n  allKeysAsEscapes?: boolean\n  /** Report text associated with key events. Default: false */\n  reportText?: boolean\n}\n\n/**\n * Build kitty keyboard protocol flags based on configuration\n * @param config Kitty keyboard configuration object (null/undefined = disabled)\n * @returns The combined flags value (0 = disabled, >0 = enabled)\n * @internal Exported for testing\n */\nexport function buildKittyKeyboardFlags(config: KittyKeyboardOptions | null | undefined): number {\n  if (!config) {\n    return 0\n  }\n\n  let flags = 0\n\n  // Default: disambiguate + alternate keys (both default to true)\n  // - Disambiguate (0b1): Fixes ESC timing issues, alt+key ambiguity, makes ctrl+c a key event\n  // - Alternate keys (0b100): Reports shifted/base-layout keys for cross-keyboard shortcuts\n\n  // disambiguate defaults to true unless explicitly set to false\n  if (config.disambiguate !== false) {\n    flags |= KITTY_FLAG_DISAMBIGUATE\n  }\n\n  // alternateKeys defaults to true unless explicitly set to false\n  if (config.alternateKeys !== false) {\n    flags |= KITTY_FLAG_ALTERNATE_KEYS\n  }\n\n  // Optional flags (default to false, only enabled when explicitly true)\n  if (config.events === true) {\n    flags |= KITTY_FLAG_EVENT_TYPES\n  }\n\n  if (config.allKeysAsEscapes === true) {\n    flags |= KITTY_FLAG_ALL_KEYS_AS_ESCAPES\n  }\n\n  if (config.reportText === true) {\n    flags |= KITTY_FLAG_REPORT_TEXT\n  }\n\n  return flags\n}\n\nexport class MouseEvent {\n  public readonly type: MouseEventType\n  public readonly button: number\n  public readonly x: number\n  public readonly y: number\n  public readonly source?: Renderable\n  public readonly modifiers: {\n    shift: boolean\n    alt: boolean\n    ctrl: boolean\n  }\n  public readonly scroll?: ScrollInfo\n  public readonly target: Renderable | null\n  public readonly isDragging?: boolean\n  private _propagationStopped: boolean = false\n  private _defaultPrevented: boolean = false\n\n  public get propagationStopped(): boolean {\n    return this._propagationStopped\n  }\n\n  public get defaultPrevented(): boolean {\n    return this._defaultPrevented\n  }\n\n  constructor(target: Renderable | null, attributes: RawMouseEvent & { source?: Renderable; isDragging?: boolean }) {\n    this.target = target\n    this.type = attributes.type\n    this.button = attributes.button\n    this.x = attributes.x\n    this.y = attributes.y\n    this.modifiers = attributes.modifiers\n    this.scroll = attributes.scroll\n    this.source = attributes.source\n    this.isDragging = attributes.isDragging\n  }\n\n  public stopPropagation(): void {\n    this._propagationStopped = true\n  }\n\n  public preventDefault(): void {\n    this._defaultPrevented = true\n  }\n}\n\nexport enum MouseButton {\n  LEFT = 0,\n  MIDDLE = 1,\n  RIGHT = 2,\n  WHEEL_UP = 4,\n  WHEEL_DOWN = 5,\n}\n\nconst rendererTracker = singleton(\"RendererTracker\", () => {\n  const renderers = new Set<CliRenderer>()\n  return {\n    addRenderer: (renderer: CliRenderer) => {\n      renderers.add(renderer)\n    },\n    removeRenderer: (renderer: CliRenderer) => {\n      renderers.delete(renderer)\n      if (renderers.size === 0) {\n        process.stdin.pause()\n\n        if (hasSingleton(\"tree-sitter-client\")) {\n          getTreeSitterClient().destroy()\n          destroySingleton(\"tree-sitter-client\")\n        }\n      }\n    },\n  }\n})\n\nexport async function createCliRenderer(config: CliRendererConfig = {}): Promise<CliRenderer> {\n  if (process.argv.includes(\"--delay-start\")) {\n    await new Promise((resolve) => setTimeout(resolve, 5000))\n  }\n  const stdin = config.stdin || process.stdin\n  const stdout = config.stdout || process.stdout\n\n  const width = stdout.columns || 80\n  const height = stdout.rows || 24\n  const renderHeight =\n    config.experimental_splitHeight && config.experimental_splitHeight > 0 ? config.experimental_splitHeight : height\n\n  const ziglib = resolveRenderLib()\n  const rendererPtr = ziglib.createRenderer(width, renderHeight, {\n    remote: config.remote ?? false,\n    testing: config.testing ?? false,\n  })\n  if (!rendererPtr) {\n    throw new Error(\"Failed to create renderer\")\n  }\n  if (config.useThread === undefined) {\n    config.useThread = true\n  }\n\n  // Disable threading on linux because there currently is currently an issue\n  // might be just a missing dependency for the build or something, but threads crash on linux\n  if (process.platform === \"linux\") {\n    config.useThread = false\n  }\n  ziglib.setUseThread(rendererPtr, config.useThread)\n\n  const kittyConfig = config.useKittyKeyboard ?? {}\n  const kittyFlags = buildKittyKeyboardFlags(kittyConfig)\n\n  ziglib.setKittyKeyboardFlags(rendererPtr, kittyFlags)\n\n  const renderer = new CliRenderer(ziglib, rendererPtr, stdin, stdout, width, height, config)\n  if (!config.testing) {\n    await renderer.setupTerminal()\n  }\n  return renderer\n}\n\nexport enum CliRenderEvents {\n  RESIZE = \"resize\",\n  FOCUS = \"focus\",\n  BLUR = \"blur\",\n  THEME_MODE = \"theme_mode\",\n  CAPABILITIES = \"capabilities\",\n  SELECTION = \"selection\",\n  DEBUG_OVERLAY_TOGGLE = \"debugOverlay:toggle\",\n  DESTROY = \"destroy\",\n  MEMORY_SNAPSHOT = \"memory:snapshot\",\n}\n\nexport enum RendererControlState {\n  IDLE = \"idle\",\n  AUTO_STARTED = \"auto_started\",\n  EXPLICIT_STARTED = \"explicit_started\",\n  EXPLICIT_PAUSED = \"explicit_paused\",\n  EXPLICIT_SUSPENDED = \"explicit_suspended\",\n  EXPLICIT_STOPPED = \"explicit_stopped\",\n}\n\nexport class CliRenderer extends EventEmitter implements RenderContext {\n  private static animationFrameId = 0\n  private lib: RenderLib\n  public rendererPtr: Pointer\n  public stdin: NodeJS.ReadStream\n  private stdout: NodeJS.WriteStream\n  private exitOnCtrlC: boolean\n  private exitSignals: NodeJS.Signals[]\n  private _exitListenersAdded: boolean = false\n  private _isDestroyed: boolean = false\n  private _destroyPending: boolean = false\n  private _destroyFinalized: boolean = false\n  public nextRenderBuffer: OptimizedBuffer\n  public currentRenderBuffer: OptimizedBuffer\n  private _isRunning: boolean = false\n  private targetFps: number = 30\n  private maxFps: number = 60\n  private automaticMemorySnapshot: boolean = false\n  private memorySnapshotInterval: number\n  private memorySnapshotTimer: TimerHandle | null = null\n  private lastMemorySnapshot: { heapUsed: number; heapTotal: number; arrayBuffers: number } = {\n    heapUsed: 0,\n    heapTotal: 0,\n    arrayBuffers: 0,\n  }\n  public readonly root: RootRenderable\n  public width: number\n  public height: number\n  private _useThread: boolean = false\n  private gatherStats: boolean = false\n  private frameTimes: number[] = []\n  private maxStatSamples: number = 300\n  private postProcessFns: ((buffer: OptimizedBuffer, deltaTime: number) => void)[] = []\n  private backgroundColor: RGBA = RGBA.fromInts(0, 0, 0, 0)\n  private waitingForPixelResolution: boolean = false\n  private readonly clock: Clock\n\n  private rendering: boolean = false\n  private renderingNative: boolean = false\n  private renderTimeout: TimerHandle | null = null\n  private lastTime: number = 0\n  private frameCount: number = 0\n  private lastFpsTime: number = 0\n  private currentFps: number = 0\n  private targetFrameTime: number = 1000 / this.targetFps\n  private minTargetFrameTime: number = 1000 / this.maxFps\n  private immediateRerenderRequested: boolean = false\n  private updateScheduled: boolean = false\n\n  private liveRequestCounter: number = 0\n  private _controlState: RendererControlState = RendererControlState.IDLE\n\n  private frameCallbacks: ((deltaTime: number) => Promise<void>)[] = []\n  private renderStats: {\n    frameCount: number\n    fps: number\n    renderTime?: number\n    frameCallbackTime: number\n  } = {\n    frameCount: 0,\n    fps: 0,\n    renderTime: 0,\n    frameCallbackTime: 0,\n  }\n  public debugOverlay = {\n    enabled: env.OTUI_SHOW_STATS,\n    corner: DebugOverlayCorner.bottomRight,\n  }\n\n  private _console: TerminalConsole\n  private _resolution: PixelResolution | null = null\n  private _keyHandler: InternalKeyHandler\n  private stdinParser: StdinParser | null = null\n  private readonly oscSubscribers = new Set<(sequence: string) => void>()\n  private hasLoggedStdinParserError = false\n\n  private animationRequest: Map<number, FrameRequestCallback> = new Map()\n\n  private resizeTimeoutId: TimerHandle | null = null\n  private capabilityTimeoutId: TimerHandle | null = null\n  private resizeDebounceDelay: number = 100\n\n  private enableMouseMovement: boolean = false\n  private _useMouse: boolean = true\n  private autoFocus: boolean = true\n  private _useAlternateScreen: boolean = env.OTUI_USE_ALTERNATE_SCREEN\n  private _suspendedMouseEnabled: boolean = false\n  private _previousControlState: RendererControlState = RendererControlState.IDLE\n  private capturedRenderable?: Renderable\n  private lastOverRenderableNum: number = 0\n  private lastOverRenderable?: Renderable\n\n  private currentSelection: Selection | null = null\n  private selectionContainers: Renderable[] = []\n  private clipboard: Clipboard\n\n  private _splitHeight: number = 0\n  private renderOffset: number = 0\n\n  private _terminalWidth: number = 0\n  private _terminalHeight: number = 0\n  private _terminalIsSetup: boolean = false\n\n  private realStdoutWrite: (chunk: any, encoding?: any, callback?: any) => boolean\n  private captureCallback: () => void = () => {\n    if (this._splitHeight > 0) {\n      this.requestRender()\n    }\n  }\n\n  private _useConsole: boolean = true\n  private sigwinchHandler: () => void = (() => {\n    const width = this.stdout.columns || 80\n    const height = this.stdout.rows || 24\n    this.handleResize(width, height)\n  }).bind(this)\n  private _capabilities: any | null = null\n  private _latestPointer: { x: number; y: number } = { x: 0, y: 0 }\n  private _hasPointer: boolean = false\n  private _lastPointerModifiers: RawMouseEvent[\"modifiers\"] = { shift: false, alt: false, ctrl: false }\n  private _currentMousePointerStyle: MousePointerStyle | undefined = undefined\n\n  private _currentFocusedRenderable: Renderable | null = null\n  private lifecyclePasses: Set<Renderable> = new Set()\n  private _openConsoleOnError: boolean = true\n  private _paletteDetector: TerminalPaletteDetector | null = null\n  private _cachedPalette: TerminalColors | null = null\n  private _paletteDetectionPromise: Promise<TerminalColors> | null = null\n  private _onDestroy?: () => void\n  private _themeMode: ThemeMode | null = null\n  private _terminalFocusState: boolean | null = null\n\n  private sequenceHandlers: ((sequence: string) => boolean)[] = []\n  private prependedInputHandlers: ((sequence: string) => boolean)[] = []\n  private shouldRestoreModesOnNextFocus: boolean = false\n\n  private idleResolvers: (() => void)[] = []\n\n  private _debugInputs: Array<{ timestamp: string; sequence: string }> = []\n  private _debugModeEnabled: boolean = env.OTUI_DEBUG\n\n  private handleError: (error: Error) => void = ((error: Error) => {\n    console.error(error)\n\n    if (this._openConsoleOnError) {\n      this.console.show()\n    }\n  }).bind(this)\n\n  private dumpOutputCache(optionalMessage: string = \"\"): void {\n    const cachedLogs = this.console.getCachedLogs()\n    const capturedOutput = capture.claimOutput()\n\n    if (capturedOutput.length > 0 || cachedLogs.length > 0) {\n      this.realStdoutWrite.call(this.stdout, optionalMessage)\n    }\n\n    if (cachedLogs.length > 0) {\n      this.realStdoutWrite.call(this.stdout, \"Console cache:\\n\")\n      this.realStdoutWrite.call(this.stdout, cachedLogs)\n    }\n\n    if (capturedOutput.length > 0) {\n      this.realStdoutWrite.call(this.stdout, \"\\nCaptured output:\\n\")\n      this.realStdoutWrite.call(this.stdout, capturedOutput + \"\\n\")\n    }\n\n    this.realStdoutWrite.call(this.stdout, ANSI.reset)\n  }\n\n  private exitHandler: () => void = (() => {\n    this.destroy()\n    if (env.OTUI_DUMP_CAPTURES) {\n      Bun.sleep(100).then(() => {\n        this.dumpOutputCache(\"=== CAPTURED OUTPUT ===\\n\")\n      })\n    }\n  }).bind(this)\n\n  private warningHandler: (warning: any) => void = ((warning: any) => {\n    console.warn(JSON.stringify(warning.message, null, 2))\n  }).bind(this)\n\n  public get controlState(): RendererControlState {\n    return this._controlState\n  }\n\n  constructor(\n    lib: RenderLib,\n    rendererPtr: Pointer,\n    stdin: NodeJS.ReadStream,\n    stdout: NodeJS.WriteStream,\n    width: number,\n    height: number,\n    config: CliRendererConfig = {},\n  ) {\n    super()\n\n    rendererTracker.addRenderer(this)\n\n    this.stdin = stdin\n    this.stdout = stdout\n    this.realStdoutWrite = stdout.write\n    this.lib = lib\n    this._terminalWidth = stdout.columns ?? width\n    this._terminalHeight = stdout.rows ?? height\n    this.width = width\n    this.height = height\n    this._useThread = config.useThread === undefined ? false : config.useThread\n    this._splitHeight = config.experimental_splitHeight || 0\n\n    if (this._splitHeight > 0) {\n      capture.on(\"write\", this.captureCallback)\n      this.renderOffset = height - this._splitHeight\n      this.height = this._splitHeight\n      lib.setRenderOffset(rendererPtr, this.renderOffset)\n    }\n\n    this.rendererPtr = rendererPtr\n\n    const forwardEnvKeys = config.forwardEnvKeys ?? [...DEFAULT_FORWARDED_ENV_KEYS]\n    for (const key of forwardEnvKeys) {\n      const value = process.env[key]\n      if (value === undefined) continue\n      this.lib.setTerminalEnvVar(this.rendererPtr, key, value)\n    }\n\n    this.exitOnCtrlC = config.exitOnCtrlC === undefined ? true : config.exitOnCtrlC\n    this.exitSignals = config.exitSignals || [\n      \"SIGINT\", // Ctrl+C\n      \"SIGTERM\", // Termination signal\n      \"SIGQUIT\", // Ctrl+\\\n      \"SIGABRT\", // Abort signal\n      \"SIGHUP\", // Hangup (terminal closed)\n      \"SIGBREAK\", // Ctrl+Break on Windows\n      \"SIGPIPE\", // Broken pipe\n      \"SIGBUS\", // Bus error\n      \"SIGFPE\", // Floating point exception\n    ]\n\n    this.clipboard = new Clipboard(this.lib, this.rendererPtr)\n    this.resizeDebounceDelay = config.debounceDelay || 100\n    this.targetFps = config.targetFps || 30\n    this.maxFps = config.maxFps || 60\n    this.targetFrameTime = 1000 / this.targetFps\n    this.minTargetFrameTime = 1000 / this.maxFps\n    this.memorySnapshotInterval = config.memorySnapshotInterval ?? 0\n    this.gatherStats = config.gatherStats || false\n    this.maxStatSamples = config.maxStatSamples || 300\n    this.enableMouseMovement = config.enableMouseMovement ?? true\n    this._useMouse = config.useMouse ?? true\n    this.autoFocus = config.autoFocus ?? true\n    this._useAlternateScreen = config.useAlternateScreen ?? env.OTUI_USE_ALTERNATE_SCREEN\n    this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)\n    this.currentRenderBuffer = this.lib.getCurrentBuffer(this.rendererPtr)\n    this.postProcessFns = config.postProcessFns || []\n    this.prependedInputHandlers = config.prependInputHandlers || []\n\n    this.root = new RootRenderable(this)\n\n    if (this.memorySnapshotInterval > 0) {\n      this.startMemorySnapshotTimer()\n    }\n\n    if (env.OTUI_OVERRIDE_STDOUT) {\n      this.stdout.write = this.interceptStdoutWrite.bind(this)\n    }\n\n    // Handle terminal resize\n    process.on(\"SIGWINCH\", this.sigwinchHandler)\n\n    process.on(\"warning\", this.warningHandler)\n\n    process.on(\"uncaughtException\", this.handleError)\n    process.on(\"unhandledRejection\", this.handleError)\n    process.on(\"beforeExit\", this.exitHandler)\n\n    const kittyConfig = config.useKittyKeyboard ?? {}\n    const useKittyForParsing = kittyConfig !== null\n    this._keyHandler = new InternalKeyHandler()\n    this._keyHandler.on(\"keypress\", (event) => {\n      if (this.exitOnCtrlC && event.name === \"c\" && event.ctrl) {\n        process.nextTick(() => {\n          this.destroy()\n        })\n        return\n      }\n    })\n\n    this.addExitListeners()\n\n    this.clock = config.clock ?? new SystemClock()\n\n    const stdinParserMaxBufferBytes = config.stdinParserMaxBufferBytes ?? DEFAULT_STDIN_PARSER_MAX_BUFFER_BYTES\n    this.stdinParser = new StdinParser({\n      timeoutMs: 10,\n      maxPendingBytes: stdinParserMaxBufferBytes,\n      armTimeouts: true,\n      onTimeoutFlush: () => {\n        this.drainStdinParser()\n      },\n      useKittyKeyboard: useKittyForParsing,\n      protocolContext: {\n        kittyKeyboardEnabled: useKittyForParsing,\n        privateCapabilityRepliesActive: false,\n        pixelResolutionQueryActive: false,\n        explicitWidthCprActive: false,\n      },\n      clock: this.clock,\n    })\n\n    this._console = new TerminalConsole(this, {\n      ...(config.consoleOptions ?? {}),\n      clock: this.clock,\n    })\n    this.useConsole = config.useConsole ?? true\n    this._openConsoleOnError = config.openConsoleOnError ?? process.env.NODE_ENV !== \"production\"\n    this._onDestroy = config.onDestroy\n\n    global.requestAnimationFrame = (callback: FrameRequestCallback) => {\n      const id = CliRenderer.animationFrameId++\n      this.animationRequest.set(id, callback)\n      this.requestLive()\n      return id\n    }\n    global.cancelAnimationFrame = (handle: number) => {\n      this.animationRequest.delete(handle)\n    }\n\n    const window = global.window\n    if (!window) {\n      global.window = {} as Window & typeof globalThis\n    }\n    global.window.requestAnimationFrame = requestAnimationFrame\n\n    // Prevents output from being written to the terminal, useful for debugging\n    if (env.OTUI_NO_NATIVE_RENDER) {\n      this.renderNative = () => {\n        if (this._splitHeight > 0) {\n          this.flushStdoutCache(this._splitHeight)\n        }\n      }\n    }\n\n    this.setupInput()\n  }\n\n  private addExitListeners(): void {\n    if (this._exitListenersAdded || this.exitSignals.length === 0) return\n\n    this.exitSignals.forEach((signal) => {\n      process.addListener(signal, this.exitHandler)\n    })\n\n    this._exitListenersAdded = true\n  }\n\n  private removeExitListeners(): void {\n    if (!this._exitListenersAdded || this.exitSignals.length === 0) return\n\n    this.exitSignals.forEach((signal) => {\n      process.removeListener(signal, this.exitHandler)\n    })\n\n    this._exitListenersAdded = false\n  }\n\n  public get isDestroyed(): boolean {\n    return this._isDestroyed\n  }\n\n  public registerLifecyclePass(renderable: Renderable) {\n    this.lifecyclePasses.add(renderable)\n  }\n\n  public unregisterLifecyclePass(renderable: Renderable) {\n    this.lifecyclePasses.delete(renderable)\n  }\n\n  public getLifecyclePasses() {\n    return this.lifecyclePasses\n  }\n\n  public get currentFocusedRenderable(): Renderable | null {\n    return this._currentFocusedRenderable\n  }\n\n  private normalizeClockTime(now: number, fallback: number): number {\n    if (Number.isFinite(now)) {\n      return now\n    }\n\n    return Number.isFinite(fallback) ? fallback : 0\n  }\n\n  private getElapsedMs(now: number, then: number): number {\n    if (!Number.isFinite(now) || !Number.isFinite(then)) {\n      return 0\n    }\n\n    return Math.max(now - then, 0)\n  }\n\n  public focusRenderable(renderable: Renderable) {\n    if (this._currentFocusedRenderable === renderable) return\n\n    if (this._currentFocusedRenderable) {\n      this._currentFocusedRenderable.blur()\n    }\n\n    this._currentFocusedRenderable = renderable\n  }\n\n  private setCapturedRenderable(renderable: Renderable | undefined): void {\n    if (this.capturedRenderable === renderable) {\n      return\n    }\n    this.capturedRenderable = renderable\n  }\n\n  public addToHitGrid(x: number, y: number, width: number, height: number, id: number) {\n    if (id !== this.capturedRenderable?.num) {\n      this.lib.addToHitGrid(this.rendererPtr, x, y, width, height, id)\n    }\n  }\n\n  public pushHitGridScissorRect(x: number, y: number, width: number, height: number): void {\n    this.lib.hitGridPushScissorRect(this.rendererPtr, x, y, width, height)\n  }\n\n  public popHitGridScissorRect(): void {\n    this.lib.hitGridPopScissorRect(this.rendererPtr)\n  }\n\n  public clearHitGridScissorRects(): void {\n    this.lib.hitGridClearScissorRects(this.rendererPtr)\n  }\n\n  public get widthMethod(): WidthMethod {\n    const caps = this.capabilities\n    return caps?.unicode === \"wcwidth\" ? \"wcwidth\" : \"unicode\"\n  }\n\n  private writeOut(chunk: any, encoding?: any, callback?: any): boolean {\n    if (this.rendererPtr && this._useThread) {\n      const data = typeof chunk === \"string\" ? chunk : (chunk?.toString() ?? \"\")\n      this.lib.writeOut(this.rendererPtr, data)\n      if (typeof callback === \"function\") {\n        process.nextTick(callback)\n      }\n      return true\n    }\n\n    return this.realStdoutWrite.call(this.stdout, chunk, encoding, callback)\n  }\n\n  public requestRender() {\n    if (this._controlState === RendererControlState.EXPLICIT_SUSPENDED) {\n      return\n    }\n\n    if (this._isRunning) {\n      return\n    }\n\n    // NOTE: Using a frame callback that causes a re-render while already rendering\n    // leads to a continuous loop of renders.\n    if (this.rendering) {\n      this.immediateRerenderRequested = true\n      return\n    }\n\n    if (!this.updateScheduled && !this.renderTimeout) {\n      this.updateScheduled = true\n      const now = this.normalizeClockTime(this.clock.now(), this.lastTime)\n      const elapsed = this.getElapsedMs(now, this.lastTime)\n      const delay = Math.max(this.minTargetFrameTime - elapsed, 0)\n\n      if (delay === 0) {\n        process.nextTick(() => this.activateFrame())\n        return\n      }\n\n      this.clock.setTimeout(() => this.activateFrame(), delay)\n    }\n  }\n\n  private async activateFrame() {\n    await this.loop()\n    this.updateScheduled = false\n    this.resolveIdleIfNeeded()\n  }\n\n  public get useConsole(): boolean {\n    return this._useConsole\n  }\n\n  public set useConsole(value: boolean) {\n    this._useConsole = value\n    if (value) {\n      this.console.activate()\n    } else {\n      this.console.deactivate()\n    }\n  }\n\n  public get isRunning(): boolean {\n    return this._isRunning\n  }\n\n  private isIdleNow(): boolean {\n    return (\n      !this._isRunning &&\n      !this.rendering &&\n      !this.renderTimeout &&\n      !this.updateScheduled &&\n      !this.immediateRerenderRequested\n    )\n  }\n\n  private resolveIdleIfNeeded(): void {\n    if (!this.isIdleNow()) return\n    const resolvers = this.idleResolvers.splice(0)\n    for (const resolve of resolvers) {\n      resolve()\n    }\n  }\n\n  public idle(): Promise<void> {\n    if (this._isDestroyed) return Promise.resolve()\n    if (this.isIdleNow()) return Promise.resolve()\n    return new Promise<void>((resolve) => {\n      this.idleResolvers.push(resolve)\n    })\n  }\n\n  public get resolution(): PixelResolution | null {\n    return this._resolution\n  }\n\n  public get console(): TerminalConsole {\n    return this._console\n  }\n\n  public get keyInput(): KeyHandler {\n    return this._keyHandler\n  }\n\n  public get _internalKeyInput(): InternalKeyHandler {\n    return this._keyHandler\n  }\n\n  public get terminalWidth(): number {\n    return this._terminalWidth\n  }\n\n  public get terminalHeight(): number {\n    return this._terminalHeight\n  }\n\n  public get useThread(): boolean {\n    return this._useThread\n  }\n\n  public get useMouse(): boolean {\n    return this._useMouse\n  }\n\n  public set useMouse(useMouse: boolean) {\n    if (this._useMouse === useMouse) return // No change needed\n\n    this._useMouse = useMouse\n\n    if (useMouse) {\n      this.enableMouse()\n    } else {\n      this.disableMouse()\n    }\n  }\n\n  public get experimental_splitHeight(): number {\n    return this._splitHeight\n  }\n\n  public get liveRequestCount(): number {\n    return this.liveRequestCounter\n  }\n\n  public get currentControlState(): string {\n    return this._controlState\n  }\n\n  public get capabilities(): any | null {\n    return this._capabilities\n  }\n\n  public get themeMode(): ThemeMode | null {\n    return this._themeMode\n  }\n\n  public getDebugInputs(): Array<{ timestamp: string; sequence: string }> {\n    return [...this._debugInputs]\n  }\n\n  public get useKittyKeyboard(): boolean {\n    return this.lib.getKittyKeyboardFlags(this.rendererPtr) > 0\n  }\n\n  public set useKittyKeyboard(use: boolean) {\n    const flags = use ? KITTY_FLAG_DISAMBIGUATE | KITTY_FLAG_ALTERNATE_KEYS : 0\n    this.lib.setKittyKeyboardFlags(this.rendererPtr, flags)\n  }\n\n  public set experimental_splitHeight(splitHeight: number) {\n    if (splitHeight < 0) splitHeight = 0\n\n    const prevSplitHeight = this._splitHeight\n\n    if (splitHeight > 0) {\n      this._splitHeight = splitHeight\n      this.renderOffset = this._terminalHeight - this._splitHeight\n      this.height = this._splitHeight\n\n      if (prevSplitHeight === 0) {\n        this.useConsole = false\n        capture.on(\"write\", this.captureCallback)\n        const freedLines = this._terminalHeight - this._splitHeight\n        const scrollDown = ANSI.scrollDown(freedLines)\n        this.writeOut(scrollDown)\n      } else if (prevSplitHeight > this._splitHeight) {\n        const freedLines = prevSplitHeight - this._splitHeight\n        const scrollDown = ANSI.scrollDown(freedLines)\n        this.writeOut(scrollDown)\n      } else if (prevSplitHeight < this._splitHeight) {\n        const additionalLines = this._splitHeight - prevSplitHeight\n        const scrollUp = ANSI.scrollUp(additionalLines)\n        this.writeOut(scrollUp)\n      }\n    } else {\n      if (prevSplitHeight > 0) {\n        this.flushStdoutCache(this._terminalHeight, true)\n\n        capture.off(\"write\", this.captureCallback)\n        this.useConsole = true\n      }\n\n      this._splitHeight = 0\n      this.renderOffset = 0\n      this.height = this._terminalHeight\n    }\n\n    this.width = this._terminalWidth\n    this.lib.setRenderOffset(this.rendererPtr, this.renderOffset)\n    this.lib.resizeRenderer(this.rendererPtr, this.width, this.height)\n    this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)\n\n    this._console.resize(this.width, this.height)\n    this.root.resize(this.width, this.height)\n    this.emit(CliRenderEvents.RESIZE, this.width, this.height)\n    this.requestRender()\n  }\n\n  private interceptStdoutWrite = (chunk: any, encoding?: any, callback?: any): boolean => {\n    const text = chunk.toString()\n\n    capture.write(\"stdout\", text)\n    if (this._splitHeight > 0) {\n      this.requestRender()\n    }\n\n    if (typeof callback === \"function\") {\n      process.nextTick(callback)\n    }\n\n    return true\n  }\n\n  public disableStdoutInterception(): void {\n    this.stdout.write = this.realStdoutWrite\n  }\n\n  // TODO: Move this to native\n  private flushStdoutCache(space: number, force: boolean = false): boolean {\n    if (capture.size === 0 && !force) return false\n\n    const output = capture.claimOutput()\n\n    const rendererStartLine = this._terminalHeight - this._splitHeight\n    const flush = ANSI.moveCursorAndClear(rendererStartLine, 1)\n\n    const outputLine = this._terminalHeight - this._splitHeight\n    const move = ANSI.moveCursor(outputLine, 1)\n\n    let clear = \"\"\n    if (space > 0) {\n      const backgroundColor = this.backgroundColor.toInts()\n      const newlines = \" \".repeat(this.width) + \"\\n\".repeat(space)\n      // Check if background is transparent (alpha = 0)\n      if (backgroundColor[3] === 0) {\n        clear = newlines\n      } else {\n        clear =\n          ANSI.setRgbBackground(backgroundColor[0], backgroundColor[1], backgroundColor[2]) +\n          newlines +\n          ANSI.resetBackground\n      }\n    }\n\n    this.writeOut(flush + move + output + clear)\n\n    return true\n  }\n\n  private enableMouse(): void {\n    this._useMouse = true\n    this.lib.enableMouse(this.rendererPtr, this.enableMouseMovement)\n  }\n\n  private disableMouse(): void {\n    this._useMouse = false\n    this.setCapturedRenderable(undefined)\n    this.stdinParser?.resetMouseState()\n    this.lib.disableMouse(this.rendererPtr)\n  }\n\n  public enableKittyKeyboard(flags: number = 0b00011): void {\n    this.lib.enableKittyKeyboard(this.rendererPtr, flags)\n    this.updateStdinParserProtocolContext({ kittyKeyboardEnabled: true })\n  }\n\n  public disableKittyKeyboard(): void {\n    this.lib.disableKittyKeyboard(this.rendererPtr)\n    this.updateStdinParserProtocolContext({ kittyKeyboardEnabled: false }, true)\n  }\n\n  public set useThread(useThread: boolean) {\n    this._useThread = useThread\n    this.lib.setUseThread(this.rendererPtr, useThread)\n  }\n\n  // TODO: All input management may move to native when zig finally has async io support again,\n  // without rolling a full event loop\n  public async setupTerminal(): Promise<void> {\n    if (this._terminalIsSetup) return\n    this._terminalIsSetup = true\n\n    this.updateStdinParserProtocolContext({\n      privateCapabilityRepliesActive: true,\n      explicitWidthCprActive: true,\n    })\n    this.lib.setupTerminal(this.rendererPtr, this._useAlternateScreen)\n    this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr)\n\n    if (this.debugOverlay.enabled) {\n      this.lib.setDebugOverlay(this.rendererPtr, true, this.debugOverlay.corner)\n      if (!this.memorySnapshotInterval) {\n        this.memorySnapshotInterval = 3000\n        this.startMemorySnapshotTimer()\n        this.automaticMemorySnapshot = true\n      }\n    }\n\n    this.capabilityTimeoutId = this.clock.setTimeout(() => {\n      this.capabilityTimeoutId = null\n      this.removeInputHandler(this.capabilityHandler)\n      this.updateStdinParserProtocolContext(\n        {\n          privateCapabilityRepliesActive: false,\n          explicitWidthCprActive: false,\n        },\n        true,\n      )\n    }, 5000)\n\n    if (this._useMouse) {\n      this.enableMouse()\n    }\n\n    this.queryPixelResolution()\n  }\n\n  private stdinListener: (chunk: Buffer | string) => void = ((chunk: Buffer | string) => {\n    const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n    if (!this.stdinParser) return\n\n    try {\n      this.stdinParser.push(data)\n      this.drainStdinParser()\n    } catch (error) {\n      this.handleStdinParserFailure(error)\n    }\n  }).bind(this)\n\n  public addInputHandler(handler: (sequence: string) => boolean): void {\n    this.sequenceHandlers.push(handler)\n  }\n\n  public prependInputHandler(handler: (sequence: string) => boolean): void {\n    this.sequenceHandlers.unshift(handler)\n  }\n\n  public removeInputHandler(handler: (sequence: string) => boolean): void {\n    this.sequenceHandlers = this.sequenceHandlers.filter((candidate) => candidate !== handler)\n  }\n\n  private updateStdinParserProtocolContext(patch: Partial<StdinParserProtocolContext>, drain = false): void {\n    if (!this.stdinParser) return\n    this.stdinParser.updateProtocolContext(patch)\n    if (drain) this.drainStdinParser()\n  }\n\n  public subscribeOsc(handler: (sequence: string) => void): () => void {\n    this.oscSubscribers.add(handler)\n    return () => {\n      this.oscSubscribers.delete(handler)\n    }\n  }\n\n  private capabilityHandler: (sequence: string) => boolean = ((sequence: string) => {\n    if (isCapabilityResponse(sequence)) {\n      this.lib.processCapabilityResponse(this.rendererPtr, sequence)\n      this._capabilities = this.lib.getTerminalCapabilities(this.rendererPtr)\n      this.emit(CliRenderEvents.CAPABILITIES, this._capabilities)\n      return true\n    }\n    return false\n  }).bind(this)\n\n  private focusHandler: (sequence: string) => boolean = ((sequence: string) => {\n    if (sequence === \"\\x1b[I\") {\n      // When the terminal regains focus, some terminal emulators (notably\n      // Windows Terminal / ConPTY) may have stripped DEC private modes like\n      // mouse tracking, bracketed paste, and focus tracking itself while the\n      // window was unfocused.\n      if (this.shouldRestoreModesOnNextFocus) {\n        this.lib.restoreTerminalModes(this.rendererPtr)\n        this.shouldRestoreModesOnNextFocus = false\n      }\n      if (this._terminalFocusState !== true) {\n        this._terminalFocusState = true\n        this.emit(CliRenderEvents.FOCUS)\n      }\n      return true\n    }\n    if (sequence === \"\\x1b[O\") {\n      this.shouldRestoreModesOnNextFocus = true\n      if (this._terminalFocusState !== false) {\n        this._terminalFocusState = false\n        this.emit(CliRenderEvents.BLUR)\n      }\n      return true\n    }\n    return false\n  }).bind(this)\n\n  private themeModeHandler: (sequence: string) => boolean = ((sequence: string) => {\n    if (sequence === \"\\x1b[?997;1n\") {\n      if (this._themeMode !== \"dark\") {\n        this._themeMode = \"dark\"\n        this.emit(CliRenderEvents.THEME_MODE, \"dark\")\n      }\n      return true\n    }\n    if (sequence === \"\\x1b[?997;2n\") {\n      if (this._themeMode !== \"light\") {\n        this._themeMode = \"light\"\n        this.emit(CliRenderEvents.THEME_MODE, \"light\")\n      }\n      return true\n    }\n    return false\n  }).bind(this)\n\n  private dispatchSequenceHandlers(sequence: string): boolean {\n    if (this._debugModeEnabled) {\n      this._debugInputs.push({\n        timestamp: new Date().toISOString(),\n        sequence,\n      })\n    }\n\n    for (const handler of this.sequenceHandlers) {\n      if (handler(sequence)) {\n        return true\n      }\n    }\n\n    return false\n  }\n\n  private drainStdinParser(): void {\n    if (!this.stdinParser) return\n\n    this.stdinParser.drain((event) => {\n      this.handleStdinEvent(event)\n    })\n  }\n\n  private handleStdinEvent(event: StdinEvent): void {\n    switch (event.type) {\n      case \"key\":\n        if (this.dispatchSequenceHandlers(event.raw)) {\n          return\n        }\n\n        this._keyHandler.processParsedKey(event.key)\n        return\n      case \"mouse\":\n        if (this._useMouse && this.processSingleMouseEvent(event.event)) {\n          return\n        }\n\n        this.dispatchSequenceHandlers(event.raw)\n        return\n      case \"paste\":\n        this._keyHandler.processPaste(event.bytes, event.metadata)\n        return\n      case \"response\":\n        if (event.protocol === \"osc\") {\n          for (const subscriber of this.oscSubscribers) {\n            subscriber(event.sequence)\n          }\n        }\n\n        this.dispatchSequenceHandlers(event.sequence)\n        return\n    }\n  }\n\n  private handleStdinParserFailure(error: unknown): void {\n    if (!this.hasLoggedStdinParserError) {\n      this.hasLoggedStdinParserError = true\n      if (process.env.NODE_ENV !== \"test\") {\n        console.error(\"[stdin-parser-error] parser failure, resetting parser\", error)\n      }\n    }\n\n    try {\n      this.stdinParser?.reset()\n    } catch (resetError) {\n      console.error(\"stdin parser reset failed after parser error\", resetError)\n    }\n  }\n\n  private setupInput(): void {\n    for (const handler of this.prependedInputHandlers) {\n      this.addInputHandler(handler)\n    }\n\n    this.addInputHandler((sequence: string) => {\n      if (isPixelResolutionResponse(sequence) && this.waitingForPixelResolution) {\n        const resolution = parsePixelResolution(sequence)\n        if (resolution) {\n          this._resolution = resolution\n        }\n        this.waitingForPixelResolution = false\n        this.updateStdinParserProtocolContext({ pixelResolutionQueryActive: false }, true)\n        return true\n      }\n      return false\n    })\n    this.addInputHandler(this.capabilityHandler)\n    this.addInputHandler(this.focusHandler)\n    this.addInputHandler(this.themeModeHandler)\n\n    if (this.stdin.setRawMode) {\n      this.stdin.setRawMode(true)\n    }\n\n    this.stdin.resume()\n    this.stdin.on(\"data\", this.stdinListener)\n  }\n\n  private dispatchMouseEvent(\n    target: Renderable,\n    attributes: RawMouseEvent & { source?: Renderable; isDragging?: boolean },\n  ): MouseEvent {\n    const event = new MouseEvent(target, attributes)\n    target.processMouseEvent(event)\n\n    if (this.autoFocus && event.type === \"down\" && event.button === MouseButton.LEFT && !event.defaultPrevented) {\n      let current: Renderable | null = target\n      while (current) {\n        if (current.focusable) {\n          current.focus()\n          break\n        }\n        current = current.parent\n      }\n    }\n\n    return event\n  }\n\n  private processSingleMouseEvent(mouseEvent: RawMouseEvent): boolean {\n    if (this._splitHeight > 0) {\n      if (mouseEvent.y < this.renderOffset) {\n        return false\n      }\n      mouseEvent.y -= this.renderOffset\n    }\n\n    this._latestPointer.x = mouseEvent.x\n    this._latestPointer.y = mouseEvent.y\n    this._hasPointer = true\n    this._lastPointerModifiers = mouseEvent.modifiers\n\n    if (this._console.visible) {\n      const consoleBounds = this._console.bounds\n      if (\n        mouseEvent.x >= consoleBounds.x &&\n        mouseEvent.x < consoleBounds.x + consoleBounds.width &&\n        mouseEvent.y >= consoleBounds.y &&\n        mouseEvent.y < consoleBounds.y + consoleBounds.height\n      ) {\n        const event = new MouseEvent(null, mouseEvent)\n        const handled = this._console.handleMouse(event)\n        if (handled) return true\n      }\n    }\n\n    if (mouseEvent.type === \"scroll\") {\n      const maybeRenderableId = this.hitTest(mouseEvent.x, mouseEvent.y)\n      const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)\n      const fallbackTarget =\n        this._currentFocusedRenderable &&\n        !this._currentFocusedRenderable.isDestroyed &&\n        this._currentFocusedRenderable.focused\n          ? this._currentFocusedRenderable\n          : null\n      const scrollTarget = maybeRenderable ?? fallbackTarget\n\n      if (scrollTarget) {\n        const event = new MouseEvent(scrollTarget, mouseEvent)\n        scrollTarget.processMouseEvent(event)\n      }\n      return true\n    }\n\n    const maybeRenderableId = this.hitTest(mouseEvent.x, mouseEvent.y)\n    const sameElement = maybeRenderableId === this.lastOverRenderableNum\n    this.lastOverRenderableNum = maybeRenderableId\n    const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)\n\n    if (\n      mouseEvent.type === \"down\" &&\n      mouseEvent.button === MouseButton.LEFT &&\n      !this.currentSelection?.isDragging &&\n      !mouseEvent.modifiers.ctrl\n    ) {\n      const canStartSelection = Boolean(\n        maybeRenderable &&\n          maybeRenderable.selectable &&\n          !maybeRenderable.isDestroyed &&\n          maybeRenderable.shouldStartSelection(mouseEvent.x, mouseEvent.y),\n      )\n\n      if (canStartSelection && maybeRenderable) {\n        this.startSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)\n        this.dispatchMouseEvent(maybeRenderable, mouseEvent)\n        return true\n      }\n    }\n\n    if (mouseEvent.type === \"drag\" && this.currentSelection?.isDragging) {\n      this.updateSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)\n\n      if (maybeRenderable) {\n        const event = new MouseEvent(maybeRenderable, { ...mouseEvent, isDragging: true })\n        maybeRenderable.processMouseEvent(event)\n      }\n\n      return true\n    }\n\n    if (mouseEvent.type === \"up\" && this.currentSelection?.isDragging) {\n      if (maybeRenderable) {\n        const event = new MouseEvent(maybeRenderable, { ...mouseEvent, isDragging: true })\n        maybeRenderable.processMouseEvent(event)\n      }\n\n      this.finishSelection()\n      return true\n    }\n\n    if (mouseEvent.type === \"down\" && mouseEvent.button === MouseButton.LEFT && this.currentSelection) {\n      if (mouseEvent.modifiers.ctrl) {\n        this.currentSelection.isDragging = true\n        this.updateSelection(maybeRenderable, mouseEvent.x, mouseEvent.y)\n        return true\n      }\n    }\n\n    if (!sameElement && (mouseEvent.type === \"drag\" || mouseEvent.type === \"move\")) {\n      if (\n        this.lastOverRenderable &&\n        this.lastOverRenderable !== this.capturedRenderable &&\n        !this.lastOverRenderable.isDestroyed\n      ) {\n        const event = new MouseEvent(this.lastOverRenderable, { ...mouseEvent, type: \"out\" })\n        this.lastOverRenderable.processMouseEvent(event)\n      }\n      this.lastOverRenderable = maybeRenderable\n      if (maybeRenderable) {\n        const event = new MouseEvent(maybeRenderable, {\n          ...mouseEvent,\n          type: \"over\",\n          source: this.capturedRenderable,\n        })\n        maybeRenderable.processMouseEvent(event)\n      }\n    }\n\n    if (this.capturedRenderable && mouseEvent.type !== \"up\") {\n      const event = new MouseEvent(this.capturedRenderable, mouseEvent)\n      this.capturedRenderable.processMouseEvent(event)\n      return true\n    }\n\n    if (this.capturedRenderable && mouseEvent.type === \"up\") {\n      const event = new MouseEvent(this.capturedRenderable, { ...mouseEvent, type: \"drag-end\" })\n      this.capturedRenderable.processMouseEvent(event)\n      this.capturedRenderable.processMouseEvent(new MouseEvent(this.capturedRenderable, mouseEvent))\n      if (maybeRenderable) {\n        const event = new MouseEvent(maybeRenderable, {\n          ...mouseEvent,\n          type: \"drop\",\n          source: this.capturedRenderable,\n        })\n        maybeRenderable.processMouseEvent(event)\n      }\n      this.lastOverRenderable = this.capturedRenderable\n      this.lastOverRenderableNum = this.capturedRenderable.num\n      this.setCapturedRenderable(undefined)\n      // Dropping the renderable needs to push another frame when the renderer is not live\n      // to update the hit grid, otherwise capturedRenderable won't be in the hit grid and will not receive mouse events\n      this.requestRender()\n    }\n\n    let event: MouseEvent | undefined\n    if (maybeRenderable) {\n      if (mouseEvent.type === \"drag\" && mouseEvent.button === MouseButton.LEFT) {\n        this.setCapturedRenderable(maybeRenderable)\n      } else {\n        this.setCapturedRenderable(undefined)\n      }\n      event = this.dispatchMouseEvent(maybeRenderable, mouseEvent)\n    } else {\n      this.setCapturedRenderable(undefined)\n      this.lastOverRenderable = undefined\n    }\n\n    if (!event?.defaultPrevented && mouseEvent.type === \"down\" && this.currentSelection) {\n      this.clearSelection()\n    }\n\n    return true\n  }\n\n  /**\n   * Recheck hover state after hit grid changes.\n   * Called after render when native code detects the hit grid changed.\n   * Fires out/over events if the element under the cursor changed.\n   */\n  private recheckHoverState(): void {\n    if (this._isDestroyed || !this._hasPointer) return\n    if (this.capturedRenderable) return\n\n    const hitId = this.hitTest(this._latestPointer.x, this._latestPointer.y)\n    const hitRenderable = Renderable.renderablesByNumber.get(hitId)\n    const lastOver = this.lastOverRenderable\n\n    // No change\n    if (lastOver?.num === hitId) {\n      this.lastOverRenderableNum = hitId\n      return\n    }\n\n    const baseEvent: RawMouseEvent = {\n      type: \"move\",\n      button: 0,\n      x: this._latestPointer.x,\n      y: this._latestPointer.y,\n      modifiers: this._lastPointerModifiers,\n    }\n\n    // Fire out on old element\n    if (lastOver && !lastOver.isDestroyed) {\n      const event = new MouseEvent(lastOver, { ...baseEvent, type: \"out\" })\n      lastOver.processMouseEvent(event)\n    }\n\n    this.lastOverRenderable = hitRenderable\n    this.lastOverRenderableNum = hitId\n\n    // Fire over on new element\n    if (hitRenderable) {\n      const event = new MouseEvent(hitRenderable, {\n        ...baseEvent,\n        type: \"over\",\n      })\n      hitRenderable.processMouseEvent(event)\n    }\n  }\n  public setMousePointer(style: MousePointerStyle): void {\n    this._currentMousePointerStyle = style\n    this.lib.setCursorStyleOptions(this.rendererPtr, { cursor: style })\n  }\n\n  public hitTest(x: number, y: number): number {\n    return this.lib.checkHit(this.rendererPtr, x, y)\n  }\n\n  private takeMemorySnapshot(): void {\n    if (this._isDestroyed) return\n\n    const memoryUsage = process.memoryUsage()\n    this.lastMemorySnapshot = {\n      heapUsed: memoryUsage.heapUsed,\n      heapTotal: memoryUsage.heapTotal,\n      arrayBuffers: memoryUsage.arrayBuffers,\n    }\n\n    this.lib.updateMemoryStats(\n      this.rendererPtr,\n      this.lastMemorySnapshot.heapUsed,\n      this.lastMemorySnapshot.heapTotal,\n      this.lastMemorySnapshot.arrayBuffers,\n    )\n\n    this.emit(CliRenderEvents.MEMORY_SNAPSHOT, this.lastMemorySnapshot)\n  }\n\n  private startMemorySnapshotTimer(): void {\n    this.stopMemorySnapshotTimer()\n\n    this.memorySnapshotTimer = this.clock.setInterval(() => {\n      this.takeMemorySnapshot()\n    }, this.memorySnapshotInterval)\n  }\n\n  private stopMemorySnapshotTimer(): void {\n    if (this.memorySnapshotTimer) {\n      this.clock.clearInterval(this.memorySnapshotTimer)\n      this.memorySnapshotTimer = null\n    }\n  }\n\n  public setMemorySnapshotInterval(interval: number): void {\n    this.memorySnapshotInterval = interval\n\n    if (this._isRunning && interval > 0) {\n      this.startMemorySnapshotTimer()\n    } else if (interval <= 0 && this.memorySnapshotTimer) {\n      this.clock.clearInterval(this.memorySnapshotTimer)\n      this.memorySnapshotTimer = null\n    }\n  }\n\n  private handleResize(width: number, height: number): void {\n    if (this._isDestroyed) return\n    if (this._splitHeight > 0) {\n      this.processResize(width, height)\n      return\n    }\n\n    if (this.resizeTimeoutId !== null) {\n      this.clock.clearTimeout(this.resizeTimeoutId)\n      this.resizeTimeoutId = null\n    }\n\n    this.resizeTimeoutId = this.clock.setTimeout(() => {\n      this.resizeTimeoutId = null\n      this.processResize(width, height)\n    }, this.resizeDebounceDelay)\n  }\n\n  private queryPixelResolution() {\n    this.waitingForPixelResolution = true\n    this.updateStdinParserProtocolContext({ pixelResolutionQueryActive: true })\n    this.lib.queryPixelResolution(this.rendererPtr)\n  }\n\n  private processResize(width: number, height: number): void {\n    if (width === this._terminalWidth && height === this._terminalHeight) return\n\n    const prevWidth = this._terminalWidth\n\n    this._terminalWidth = width\n    this._terminalHeight = height\n    this.queryPixelResolution()\n\n    this.setCapturedRenderable(undefined)\n    this.stdinParser?.resetMouseState()\n\n    if (this._splitHeight > 0) {\n      // TODO: Handle resizing split mode properly\n      if (width < prevWidth) {\n        const start = this._terminalHeight - this._splitHeight * 2\n        const flush = ANSI.moveCursorAndClear(start, 1)\n        this.writeOut(flush)\n      }\n      this.renderOffset = height - this._splitHeight\n      this.width = width\n      this.height = this._splitHeight\n      this.currentRenderBuffer.clear(this.backgroundColor)\n      this.lib.setRenderOffset(this.rendererPtr, this.renderOffset)\n    } else {\n      this.width = width\n      this.height = height\n    }\n\n    this.lib.resizeRenderer(this.rendererPtr, this.width, this.height)\n    this.nextRenderBuffer = this.lib.getNextBuffer(this.rendererPtr)\n    this.currentRenderBuffer = this.lib.getCurrentBuffer(this.rendererPtr)\n    this._console.resize(this.width, this.height)\n    this.root.resize(this.width, this.height)\n    this.emit(CliRenderEvents.RESIZE, this.width, this.height)\n    this.requestRender()\n  }\n\n  public setBackgroundColor(color: ColorInput): void {\n    const parsedColor = parseColor(color)\n    this.lib.setBackgroundColor(this.rendererPtr, parsedColor as RGBA)\n    this.backgroundColor = parsedColor as RGBA\n    this.nextRenderBuffer.clear(parsedColor as RGBA)\n    this.requestRender()\n  }\n\n  public toggleDebugOverlay(): void {\n    const willBeEnabled = !this.debugOverlay.enabled\n\n    if (willBeEnabled && !this.memorySnapshotInterval) {\n      this.memorySnapshotInterval = 3000\n      this.startMemorySnapshotTimer()\n      this.automaticMemorySnapshot = true\n    } else if (!willBeEnabled && this.automaticMemorySnapshot) {\n      this.stopMemorySnapshotTimer()\n      this.memorySnapshotInterval = 0\n      this.automaticMemorySnapshot = false\n    }\n\n    this.debugOverlay.enabled = !this.debugOverlay.enabled\n    this.lib.setDebugOverlay(this.rendererPtr, this.debugOverlay.enabled, this.debugOverlay.corner)\n    this.emit(CliRenderEvents.DEBUG_OVERLAY_TOGGLE, this.debugOverlay.enabled)\n    this.requestRender()\n  }\n\n  public configureDebugOverlay(options: { enabled?: boolean; corner?: DebugOverlayCorner }): void {\n    this.debugOverlay.enabled = options.enabled ?? this.debugOverlay.enabled\n    this.debugOverlay.corner = options.corner ?? this.debugOverlay.corner\n    this.lib.setDebugOverlay(this.rendererPtr, this.debugOverlay.enabled, this.debugOverlay.corner)\n    this.requestRender()\n  }\n\n  public setTerminalTitle(title: string): void {\n    this.lib.setTerminalTitle(this.rendererPtr, title)\n  }\n\n  public copyToClipboardOSC52(text: string, target?: ClipboardTarget): boolean {\n    return this.clipboard.copyToClipboardOSC52(text, target)\n  }\n\n  public clearClipboardOSC52(target?: ClipboardTarget): boolean {\n    return this.clipboard.clearClipboardOSC52(target)\n  }\n\n  public isOsc52Supported(): boolean {\n    return this._capabilities?.osc52 ?? this.clipboard.isOsc52Supported()\n  }\n\n  public dumpHitGrid(): void {\n    this.lib.dumpHitGrid(this.rendererPtr)\n  }\n\n  public dumpBuffers(timestamp?: number): void {\n    this.lib.dumpBuffers(this.rendererPtr, timestamp)\n  }\n\n  public dumpStdoutBuffer(timestamp?: number): void {\n    this.lib.dumpStdoutBuffer(this.rendererPtr, timestamp)\n  }\n\n  public static setCursorPosition(renderer: CliRenderer, x: number, y: number, visible: boolean = true): void {\n    const lib = resolveRenderLib()\n    lib.setCursorPosition(renderer.rendererPtr, x, y, visible)\n  }\n\n  public static setCursorStyle(renderer: CliRenderer, options: CursorStyleOptions): void {\n    const lib = resolveRenderLib()\n    lib.setCursorStyleOptions(renderer.rendererPtr, options)\n    if (options.cursor !== undefined) {\n      renderer._currentMousePointerStyle = options.cursor\n    }\n  }\n\n  public static setCursorColor(renderer: CliRenderer, color: RGBA): void {\n    const lib = resolveRenderLib()\n    lib.setCursorColor(renderer.rendererPtr, color)\n  }\n\n  public setCursorPosition(x: number, y: number, visible: boolean = true): void {\n    this.lib.setCursorPosition(this.rendererPtr, x, y, visible)\n  }\n\n  public setCursorStyle(options: CursorStyleOptions): void {\n    this.lib.setCursorStyleOptions(this.rendererPtr, options)\n    if (options.cursor !== undefined) {\n      this._currentMousePointerStyle = options.cursor\n    }\n  }\n\n  public setCursorColor(color: RGBA): void {\n    this.lib.setCursorColor(this.rendererPtr, color)\n  }\n\n  public getCursorState() {\n    return this.lib.getCursorState(this.rendererPtr)\n  }\n\n  public addPostProcessFn(processFn: (buffer: OptimizedBuffer, deltaTime: number) => void): void {\n    this.postProcessFns.push(processFn)\n  }\n\n  public removePostProcessFn(processFn: (buffer: OptimizedBuffer, deltaTime: number) => void): void {\n    this.postProcessFns = this.postProcessFns.filter((fn) => fn !== processFn)\n  }\n\n  public clearPostProcessFns(): void {\n    this.postProcessFns = []\n  }\n\n  public setFrameCallback(callback: (deltaTime: number) => Promise<void>): void {\n    this.frameCallbacks.push(callback)\n  }\n\n  public removeFrameCallback(callback: (deltaTime: number) => Promise<void>): void {\n    this.frameCallbacks = this.frameCallbacks.filter((cb) => cb !== callback)\n  }\n\n  public clearFrameCallbacks(): void {\n    this.frameCallbacks = []\n  }\n\n  public requestLive(): void {\n    this.liveRequestCounter++\n\n    if (this._controlState === RendererControlState.IDLE && this.liveRequestCounter > 0) {\n      this._controlState = RendererControlState.AUTO_STARTED\n      this.internalStart()\n    }\n  }\n\n  public dropLive(): void {\n    this.liveRequestCounter = Math.max(0, this.liveRequestCounter - 1)\n\n    if (this._controlState === RendererControlState.AUTO_STARTED && this.liveRequestCounter === 0) {\n      this._controlState = RendererControlState.IDLE\n      this.internalPause()\n    }\n  }\n\n  public start(): void {\n    this._controlState = RendererControlState.EXPLICIT_STARTED\n    this.internalStart()\n  }\n\n  public auto(): void {\n    this._controlState = this._isRunning ? RendererControlState.AUTO_STARTED : RendererControlState.IDLE\n  }\n\n  private internalStart(): void {\n    if (!this._isRunning && !this._isDestroyed) {\n      this._isRunning = true\n\n      if (this.memorySnapshotInterval > 0) {\n        this.startMemorySnapshotTimer()\n      }\n\n      this.startRenderLoop()\n    }\n  }\n\n  public pause(): void {\n    this._controlState = RendererControlState.EXPLICIT_PAUSED\n    this.internalPause()\n  }\n\n  public suspend(): void {\n    this._previousControlState = this._controlState\n\n    this._controlState = RendererControlState.EXPLICIT_SUSPENDED\n    this.internalPause()\n\n    this._suspendedMouseEnabled = this._useMouse\n\n    this.disableMouse()\n    this.removeExitListeners()\n    this.waitingForPixelResolution = false\n    this.updateStdinParserProtocolContext({\n      privateCapabilityRepliesActive: false,\n      pixelResolutionQueryActive: false,\n      explicitWidthCprActive: false,\n    })\n    this.stdinParser?.reset()\n    this.stdin.removeListener(\"data\", this.stdinListener)\n    this.lib.suspendRenderer(this.rendererPtr)\n\n    if (this.stdin.setRawMode) {\n      this.stdin.setRawMode(false)\n    }\n\n    this.stdin.pause()\n  }\n\n  public resume(): void {\n    if (this.stdin.setRawMode) {\n      this.stdin.setRawMode(true)\n    }\n\n    this.stdin.resume()\n    this.addExitListeners()\n\n    setImmediate(() => {\n      // Consume any existing stdin data to avoid processing stale input\n      while (this.stdin.read() !== null) {}\n      this.stdin.on(\"data\", this.stdinListener)\n    })\n\n    this.lib.resumeRenderer(this.rendererPtr)\n\n    if (this._suspendedMouseEnabled) {\n      this.enableMouse()\n    }\n\n    this.currentRenderBuffer.clear(this.backgroundColor)\n    this._controlState = this._previousControlState\n\n    if (\n      this._previousControlState === RendererControlState.AUTO_STARTED ||\n      this._previousControlState === RendererControlState.EXPLICIT_STARTED\n    ) {\n      this.internalStart()\n    } else {\n      this.requestRender()\n    }\n  }\n\n  private internalPause(): void {\n    this._isRunning = false\n\n    if (this.renderTimeout) {\n      this.clock.clearTimeout(this.renderTimeout)\n      this.renderTimeout = null\n    }\n\n    if (!this.rendering) {\n      this.resolveIdleIfNeeded()\n    }\n  }\n\n  public stop(): void {\n    this._controlState = RendererControlState.EXPLICIT_STOPPED\n    this.internalStop()\n  }\n\n  private internalStop(): void {\n    if (this.isRunning && !this._isDestroyed) {\n      this._isRunning = false\n\n      if (this.memorySnapshotTimer) {\n        this.clock.clearInterval(this.memorySnapshotTimer)\n        this.memorySnapshotTimer = null\n      }\n\n      if (this.renderTimeout) {\n        this.clock.clearTimeout(this.renderTimeout)\n        this.renderTimeout = null\n      }\n\n      // If we're currently rendering, the frame will resolve idle when it completes\n      // Otherwise, resolve immediately\n      if (!this.rendering) {\n        this.resolveIdleIfNeeded()\n      }\n    }\n  }\n\n  public destroy(): void {\n    if (this._isDestroyed) return\n    this._isDestroyed = true\n    this._destroyPending = true\n\n    if (this.rendering) {\n      // Defer teardown until the active frame completes to avoid freeing native resources mid-render.\n      return\n    }\n\n    this.finalizeDestroy()\n  }\n\n  private finalizeDestroy(): void {\n    if (this._destroyFinalized) return\n    this._destroyFinalized = true\n    this._destroyPending = false\n\n    process.removeListener(\"SIGWINCH\", this.sigwinchHandler)\n    process.removeListener(\"uncaughtException\", this.handleError)\n    process.removeListener(\"unhandledRejection\", this.handleError)\n    process.removeListener(\"warning\", this.warningHandler)\n    process.removeListener(\"beforeExit\", this.exitHandler)\n    capture.removeListener(\"write\", this.captureCallback)\n    this.removeExitListeners()\n\n    if (this.resizeTimeoutId !== null) {\n      this.clock.clearTimeout(this.resizeTimeoutId)\n      this.resizeTimeoutId = null\n    }\n\n    if (this.capabilityTimeoutId !== null) {\n      this.clock.clearTimeout(this.capabilityTimeoutId)\n      this.capabilityTimeoutId = null\n    }\n\n    if (this.memorySnapshotTimer) {\n      this.clock.clearInterval(this.memorySnapshotTimer)\n    }\n\n    // Clean up palette detector\n    if (this._paletteDetector) {\n      this._paletteDetector.cleanup()\n      this._paletteDetector = null\n    }\n    this._paletteDetectionPromise = null\n    this._cachedPalette = null\n\n    this.emit(CliRenderEvents.DESTROY)\n\n    if (this.renderTimeout) {\n      this.clock.clearTimeout(this.renderTimeout)\n      this.renderTimeout = null\n    }\n    this._isRunning = false\n\n    this.waitingForPixelResolution = false\n    this.updateStdinParserProtocolContext(\n      {\n        privateCapabilityRepliesActive: false,\n        pixelResolutionQueryActive: false,\n        explicitWidthCprActive: false,\n      },\n      true,\n    )\n    this.setCapturedRenderable(undefined)\n\n    try {\n      this.root.destroyRecursively()\n    } catch (e) {\n      console.error(\"Error destroying root renderable:\", e instanceof Error ? e.stack : String(e))\n    }\n\n    // Remove listener before destroying parser\n    this.stdin.removeListener(\"data\", this.stdinListener)\n    if (this.stdin.setRawMode) {\n      this.stdin.setRawMode(false)\n    }\n\n    this.stdinParser?.destroy()\n    this.stdinParser = null\n    this.oscSubscribers.clear()\n    this._console.destroy()\n    this.disableStdoutInterception()\n\n    if (this._splitHeight > 0) {\n      this.flushStdoutCache(this._splitHeight, true)\n    }\n\n    this.lib.destroyRenderer(this.rendererPtr)\n    rendererTracker.removeRenderer(this)\n\n    if (this._onDestroy) {\n      try {\n        this._onDestroy()\n      } catch (e) {\n        console.error(\"Error in onDestroy callback:\", e instanceof Error ? e.stack : String(e))\n      }\n    }\n\n    // Resolve any pending idle() calls\n    this.resolveIdleIfNeeded()\n  }\n\n  private startRenderLoop(): void {\n    if (!this._isRunning) return\n\n    this.lastTime = this.normalizeClockTime(this.clock.now(), 0)\n    this.frameCount = 0\n    this.lastFpsTime = this.lastTime\n    this.currentFps = 0\n\n    this.loop()\n  }\n\n  private async loop(): Promise<void> {\n    if (this.rendering || this._isDestroyed) return\n    this.renderTimeout = null\n\n    this.rendering = true\n    if (this.renderTimeout) {\n      this.clock.clearTimeout(this.renderTimeout)\n      this.renderTimeout = null\n    }\n    try {\n      const now = this.normalizeClockTime(this.clock.now(), this.lastTime)\n      const elapsed = this.getElapsedMs(now, this.lastTime)\n\n      const deltaTime = elapsed\n      this.lastTime = now\n\n      this.frameCount++\n      if (this.getElapsedMs(now, this.lastFpsTime) >= 1000) {\n        this.currentFps = this.frameCount\n        this.frameCount = 0\n        this.lastFpsTime = now\n      }\n\n      this.renderStats.frameCount++\n      this.renderStats.fps = this.currentFps\n      const overallStart = performance.now()\n\n      const frameRequests = Array.from(this.animationRequest.values())\n      this.animationRequest.clear()\n      const animationRequestStart = performance.now()\n      for (const callback of frameRequests) {\n        callback(deltaTime)\n        this.dropLive()\n      }\n      const animationRequestEnd = performance.now()\n      const animationRequestTime = animationRequestEnd - animationRequestStart\n\n      const start = performance.now()\n      for (const frameCallback of this.frameCallbacks) {\n        try {\n          await frameCallback(deltaTime)\n        } catch (error) {\n          console.error(\"Error in frame callback:\", error)\n        }\n      }\n      const end = performance.now()\n      this.renderStats.frameCallbackTime = end - start\n\n      this.root.render(this.nextRenderBuffer, deltaTime)\n\n      for (const postProcessFn of this.postProcessFns) {\n        postProcessFn(this.nextRenderBuffer, deltaTime)\n      }\n\n      this._console.renderToBuffer(this.nextRenderBuffer)\n\n      // If destroy() was requested during this frame, skip native work and scheduling.\n      if (!this._isDestroyed) {\n        this.renderNative()\n\n        // Check if hit grid changed and recheck hover state if needed\n        if (this._useMouse && this.lib.getHitGridDirty(this.rendererPtr)) {\n          this.recheckHoverState()\n        }\n\n        const overallFrameTime = performance.now() - overallStart\n\n        // TODO: Add animationRequestTime to stats\n        this.lib.updateStats(\n          this.rendererPtr,\n          overallFrameTime,\n          this.renderStats.fps,\n          this.renderStats.frameCallbackTime,\n        )\n\n        if (this.gatherStats) {\n          this.collectStatSample(overallFrameTime)\n        }\n\n        if (this._isRunning || this.immediateRerenderRequested) {\n          const targetFrameTime = this.immediateRerenderRequested ? this.minTargetFrameTime : this.targetFrameTime\n          const delay = Math.max(1, targetFrameTime - Math.floor(overallFrameTime))\n          this.immediateRerenderRequested = false\n          this.renderTimeout = this.clock.setTimeout(() => {\n            this.renderTimeout = null\n            this.loop()\n          }, delay)\n        } else {\n          this.clock.clearTimeout(this.renderTimeout!)\n          this.renderTimeout = null\n        }\n      }\n    } finally {\n      this.rendering = false\n      if (this._destroyPending) {\n        this.finalizeDestroy()\n      }\n      this.resolveIdleIfNeeded()\n    }\n  }\n\n  public intermediateRender(): void {\n    this.immediateRerenderRequested = true\n    this.loop()\n  }\n\n  private renderNative(): void {\n    if (this.renderingNative) {\n      console.error(\"Rendering called concurrently\")\n      throw new Error(\"Rendering called concurrently\")\n    }\n\n    let force = false\n    if (this._splitHeight > 0) {\n      // TODO: Flickering could maybe be even more reduced by moving the flush to the native layer,\n      // to output the flush with the buffered writer, after the render is done.\n      force = this.flushStdoutCache(this._splitHeight)\n    }\n\n    this.renderingNative = true\n    this.lib.render(this.rendererPtr, force)\n    // this.dumpStdoutBuffer(Date.now())\n    this.renderingNative = false\n  }\n\n  private collectStatSample(frameTime: number): void {\n    this.frameTimes.push(frameTime)\n    if (this.frameTimes.length > this.maxStatSamples) {\n      this.frameTimes.shift()\n    }\n  }\n\n  public getStats(): {\n    fps: number\n    frameCount: number\n    frameTimes: number[]\n    averageFrameTime: number\n    minFrameTime: number\n    maxFrameTime: number\n  } {\n    const frameTimes = [...this.frameTimes]\n    const sum = frameTimes.reduce((acc, time) => acc + time, 0)\n    const avg = frameTimes.length ? sum / frameTimes.length : 0\n    const min = frameTimes.length ? Math.min(...frameTimes) : 0\n    const max = frameTimes.length ? Math.max(...frameTimes) : 0\n\n    return {\n      fps: this.renderStats.fps,\n      frameCount: this.renderStats.frameCount,\n      frameTimes,\n      averageFrameTime: avg,\n      minFrameTime: min,\n      maxFrameTime: max,\n    }\n  }\n\n  public resetStats(): void {\n    this.frameTimes = []\n    this.renderStats.frameCount = 0\n  }\n\n  public setGatherStats(enabled: boolean): void {\n    this.gatherStats = enabled\n    if (!enabled) {\n      this.frameTimes = []\n    }\n  }\n\n  public getSelection(): Selection | null {\n    return this.currentSelection\n  }\n\n  public get hasSelection(): boolean {\n    return !!this.currentSelection\n  }\n\n  public getSelectionContainer(): Renderable | null {\n    return this.selectionContainers.length > 0 ? this.selectionContainers[this.selectionContainers.length - 1] : null\n  }\n\n  public clearSelection(): void {\n    if (this.currentSelection) {\n      for (const renderable of this.currentSelection.touchedRenderables) {\n        if (renderable.selectable && !renderable.isDestroyed) {\n          renderable.onSelectionChanged(null)\n        }\n      }\n      this.currentSelection = null\n    }\n    this.selectionContainers = []\n  }\n\n  /**\n   * Start a new selection at the given coordinates.\n   * Used by both mouse and keyboard selection.\n   */\n  public startSelection(renderable: Renderable, x: number, y: number): void {\n    if (!renderable.selectable) return\n\n    this.clearSelection()\n    this.selectionContainers.push(renderable.parent || this.root)\n    this.currentSelection = new Selection(renderable, { x, y }, { x, y })\n    this.currentSelection.isStart = true\n\n    this.notifySelectablesOfSelectionChange()\n  }\n\n  public updateSelection(\n    currentRenderable: Renderable | undefined,\n    x: number,\n    y: number,\n    options?: { finishDragging?: boolean },\n  ): void {\n    if (this.currentSelection) {\n      this.currentSelection.isStart = false\n      this.currentSelection.focus = { x, y }\n\n      if (options?.finishDragging) {\n        this.currentSelection.isDragging = false\n      }\n\n      if (this.selectionContainers.length > 0) {\n        const currentContainer = this.selectionContainers[this.selectionContainers.length - 1]\n\n        if (!currentRenderable || !this.isWithinContainer(currentRenderable, currentContainer)) {\n          const parentContainer = currentContainer.parent || this.root\n          this.selectionContainers.push(parentContainer)\n        } else if (currentRenderable && this.selectionContainers.length > 1) {\n          let containerIndex = this.selectionContainers.indexOf(currentRenderable)\n\n          if (containerIndex === -1) {\n            const immediateParent = currentRenderable.parent || this.root\n            containerIndex = this.selectionContainers.indexOf(immediateParent)\n          }\n\n          if (containerIndex !== -1 && containerIndex < this.selectionContainers.length - 1) {\n            this.selectionContainers = this.selectionContainers.slice(0, containerIndex + 1)\n          }\n        }\n      }\n\n      this.notifySelectablesOfSelectionChange()\n    }\n  }\n\n  public requestSelectionUpdate(): void {\n    if (this.currentSelection?.isDragging) {\n      const pointer = this._latestPointer\n\n      const maybeRenderableId = this.hitTest(pointer.x, pointer.y)\n      const maybeRenderable = Renderable.renderablesByNumber.get(maybeRenderableId)\n\n      this.updateSelection(maybeRenderable, pointer.x, pointer.y)\n    }\n  }\n\n  private isWithinContainer(renderable: Renderable, container: Renderable): boolean {\n    let current: Renderable | null = renderable\n    while (current) {\n      if (current === container) return true\n      current = current.parent\n    }\n    return false\n  }\n\n  private finishSelection(): void {\n    if (this.currentSelection) {\n      this.currentSelection.isDragging = false\n      this.emit(CliRenderEvents.SELECTION, this.currentSelection)\n      this.notifySelectablesOfSelectionChange()\n    }\n  }\n\n  private notifySelectablesOfSelectionChange(): void {\n    const selectedRenderables: Renderable[] = []\n    const touchedRenderables: Renderable[] = []\n    const currentContainer =\n      this.selectionContainers.length > 0 ? this.selectionContainers[this.selectionContainers.length - 1] : this.root\n\n    if (this.currentSelection) {\n      this.walkSelectableRenderables(\n        currentContainer,\n        this.currentSelection.bounds,\n        selectedRenderables,\n        touchedRenderables,\n      )\n\n      for (const renderable of this.currentSelection.touchedRenderables) {\n        if (!touchedRenderables.includes(renderable) && !renderable.isDestroyed) {\n          renderable.onSelectionChanged(null)\n        }\n      }\n\n      this.currentSelection.updateSelectedRenderables(selectedRenderables)\n      this.currentSelection.updateTouchedRenderables(touchedRenderables)\n    }\n  }\n\n  private walkSelectableRenderables(\n    container: Renderable,\n    selectionBounds: ViewportBounds,\n    selectedRenderables: Renderable[],\n    touchedRenderables: Renderable[],\n  ): void {\n    const children = getObjectsInViewport<Renderable>(\n      selectionBounds,\n      container.getChildrenSortedByPrimaryAxis(),\n      container.primaryAxis,\n      0, // padding\n      0, // minTriggerSize - always perform overlap checks for selection\n    )\n\n    for (const child of children) {\n      if (child.selectable) {\n        const hasSelection = child.onSelectionChanged(this.currentSelection)\n        if (hasSelection) {\n          selectedRenderables.push(child)\n        }\n        touchedRenderables.push(child)\n      }\n      if (child.getChildrenCount() > 0) {\n        this.walkSelectableRenderables(child, selectionBounds, selectedRenderables, touchedRenderables)\n      }\n    }\n  }\n\n  public get paletteDetectionStatus(): \"idle\" | \"detecting\" | \"cached\" {\n    if (this._cachedPalette) return \"cached\"\n    if (this._paletteDetectionPromise) return \"detecting\"\n    return \"idle\"\n  }\n\n  public clearPaletteCache(): void {\n    this._cachedPalette = null\n  }\n\n  /**\n   * Detects the terminal's color palette\n   *\n   * @returns Promise resolving to TerminalColors object containing palette and special colors\n   * @throws Error if renderer is suspended\n   */\n  public async getPalette(options?: GetPaletteOptions): Promise<TerminalColors> {\n    if (this._controlState === RendererControlState.EXPLICIT_SUSPENDED) {\n      throw new Error(\"Cannot detect palette while renderer is suspended\")\n    }\n\n    const requestedSize = options?.size ?? 16\n\n    if (this._cachedPalette && this._cachedPalette.palette.length !== requestedSize) {\n      this._cachedPalette = null\n    }\n\n    if (this._cachedPalette) {\n      return this._cachedPalette\n    }\n\n    if (this._paletteDetectionPromise) {\n      return this._paletteDetectionPromise\n    }\n\n    if (!this._paletteDetector) {\n      const isLegacyTmux =\n        this.capabilities?.terminal?.name?.toLowerCase()?.includes(\"tmux\") &&\n        this.capabilities?.terminal?.version?.localeCompare(\"3.6\") < 0\n      this._paletteDetector = createTerminalPalette(\n        this.stdin,\n        this.stdout,\n        this.writeOut.bind(this),\n        isLegacyTmux,\n        {\n          subscribeOsc: this.subscribeOsc.bind(this),\n        },\n        this.clock,\n      )\n    }\n\n    this._paletteDetectionPromise = this._paletteDetector.detect(options).then((result) => {\n      this._cachedPalette = result\n      this._paletteDetectionPromise = null\n      return result\n    })\n\n    return this._paletteDetectionPromise\n  }\n}\n"
  },
  {
    "path": "packages/core/src/runtime-plugin-support.ts",
    "content": "import { plugin as registerBunPlugin } from \"bun\"\nimport {\n  createRuntimePlugin,\n  runtimeModuleIdForSpecifier,\n  type CreateRuntimePluginOptions,\n  type RuntimeModuleEntry,\n  type RuntimeModuleExports,\n  type RuntimeModuleLoader,\n} from \"./runtime-plugin\"\n\nconst runtimePluginSupportInstalledKey = \"__opentuiCoreRuntimePluginSupportInstalled__\"\n\ntype RuntimePluginSupportState = typeof globalThis & {\n  [runtimePluginSupportInstalledKey]?: boolean\n}\n\nexport function ensureRuntimePluginSupport(options: CreateRuntimePluginOptions = {}): boolean {\n  const state = globalThis as RuntimePluginSupportState\n\n  if (state[runtimePluginSupportInstalledKey]) {\n    return false\n  }\n\n  registerBunPlugin(createRuntimePlugin(options))\n\n  state[runtimePluginSupportInstalledKey] = true\n  return true\n}\n\nensureRuntimePluginSupport()\n\nexport {\n  createRuntimePlugin,\n  runtimeModuleIdForSpecifier,\n  type CreateRuntimePluginOptions,\n  type RuntimeModuleEntry,\n  type RuntimeModuleExports,\n  type RuntimeModuleLoader,\n}\n"
  },
  {
    "path": "packages/core/src/runtime-plugin.ts",
    "content": "import { type BunPlugin } from \"bun\"\nimport * as coreRuntime from \"./index\"\n\nexport type RuntimeModuleExports = Record<string, unknown>\nexport type RuntimeModuleLoader = () => RuntimeModuleExports | Promise<RuntimeModuleExports>\nexport type RuntimeModuleEntry = RuntimeModuleExports | RuntimeModuleLoader\n\nexport interface CreateRuntimePluginOptions {\n  core?: RuntimeModuleEntry\n  additional?: Record<string, RuntimeModuleEntry>\n}\n\nconst CORE_RUNTIME_SPECIFIER = \"@opentui/core\"\nconst CORE_TESTING_RUNTIME_SPECIFIER = \"@opentui/core/testing\"\nconst RUNTIME_MODULE_PREFIX = \"opentui:runtime-module:\"\nconst MAX_RUNTIME_RESOLVE_PARENTS = 64\n\nconst DEFAULT_CORE_RUNTIME_MODULE_SPECIFIERS = [CORE_RUNTIME_SPECIFIER, CORE_TESTING_RUNTIME_SPECIFIER] as const\n\nconst DEFAULT_CORE_RUNTIME_MODULE_SPECIFIER_SET = new Set<string>(DEFAULT_CORE_RUNTIME_MODULE_SPECIFIERS)\n\nexport const isCoreRuntimeModuleSpecifier = (specifier: string): boolean => {\n  return DEFAULT_CORE_RUNTIME_MODULE_SPECIFIER_SET.has(specifier)\n}\n\nconst loadCoreTestingRuntimeModule = async (): Promise<RuntimeModuleExports> => {\n  return (await import(\"./testing\")) as RuntimeModuleExports\n}\n\nconst escapeRegExp = (value: string): string => {\n  return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n}\n\nconst exactSpecifierFilter = (specifier: string): RegExp => {\n  return new RegExp(`^${escapeRegExp(specifier)}$`)\n}\n\nexport const runtimeModuleIdForSpecifier = (specifier: string): string => {\n  return `${RUNTIME_MODULE_PREFIX}${encodeURIComponent(specifier)}`\n}\n\nconst resolveRuntimeModuleExports = async (moduleEntry: RuntimeModuleEntry): Promise<RuntimeModuleExports> => {\n  if (typeof moduleEntry === \"function\") {\n    return await moduleEntry()\n  }\n\n  return moduleEntry\n}\n\nconst sourcePath = (path: string): string => {\n  const searchIndex = path.indexOf(\"?\")\n  const hashIndex = path.indexOf(\"#\")\n  const end = [searchIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0]\n  return end === undefined ? path : path.slice(0, end)\n}\n\nconst runtimeLoaderForPath = (path: string): \"js\" | \"ts\" | \"jsx\" | \"tsx\" | null => {\n  const cleanPath = sourcePath(path)\n\n  if (cleanPath.endsWith(\".tsx\")) {\n    return \"tsx\"\n  }\n\n  if (cleanPath.endsWith(\".jsx\")) {\n    return \"jsx\"\n  }\n\n  if (cleanPath.endsWith(\".ts\") || cleanPath.endsWith(\".mts\") || cleanPath.endsWith(\".cts\")) {\n    return \"ts\"\n  }\n\n  if (cleanPath.endsWith(\".js\") || cleanPath.endsWith(\".mjs\") || cleanPath.endsWith(\".cjs\")) {\n    return \"js\"\n  }\n\n  return null\n}\n\nconst runtimeSourceFilter = /^(?!.*(?:\\/|\\\\)node_modules(?:\\/|\\\\)).*\\.(?:[cm]?js|[cm]?ts|jsx|tsx)(?:[?#].*)?$/\n\nconst resolveImportSpecifierPatterns = [\n  /(from\\s+[\"'])([^\"']+)([\"'])/g,\n  /(import\\s+[\"'])([^\"']+)([\"'])/g,\n  /(import\\s*\\(\\s*[\"'])([^\"']+)([\"']\\s*\\))/g,\n  /(require\\s*\\(\\s*[\"'])([^\"']+)([\"']\\s*\\))/g,\n] as const\n\nconst isBareSpecifier = (specifier: string): boolean => {\n  if (specifier.startsWith(\".\") || specifier.startsWith(\"/\") || specifier.startsWith(\"\\\\\")) {\n    return false\n  }\n\n  if (\n    specifier.startsWith(\"node:\") ||\n    specifier.startsWith(\"bun:\") ||\n    specifier.startsWith(\"http:\") ||\n    specifier.startsWith(\"https:\") ||\n    specifier.startsWith(\"file:\") ||\n    specifier.startsWith(\"data:\")\n  ) {\n    return false\n  }\n\n  if (specifier.startsWith(RUNTIME_MODULE_PREFIX)) {\n    return false\n  }\n\n  return true\n}\n\nconst registerResolveParent = (resolveParentsByRecency: string[], resolveParent: string): void => {\n  const existingIndex = resolveParentsByRecency.indexOf(resolveParent)\n  if (existingIndex >= 0) {\n    resolveParentsByRecency.splice(existingIndex, 1)\n  }\n\n  resolveParentsByRecency.push(resolveParent)\n\n  if (resolveParentsByRecency.length > MAX_RUNTIME_RESOLVE_PARENTS) {\n    resolveParentsByRecency.shift()\n  }\n}\n\nconst rewriteImportSpecifiers = (code: string, resolveReplacement: (specifier: string) => string | null): string => {\n  let transformedCode = code\n\n  for (const pattern of resolveImportSpecifierPatterns) {\n    transformedCode = transformedCode.replace(pattern, (fullMatch, prefix, specifier, suffix) => {\n      const replacement = resolveReplacement(specifier)\n      if (!replacement || replacement === specifier) {\n        return fullMatch\n      }\n\n      return `${prefix}${replacement}${suffix}`\n    })\n  }\n\n  return transformedCode\n}\n\nconst resolveFromParent = (specifier: string, parent: string): string | null => {\n  try {\n    const resolvedSpecifier = import.meta.resolve(specifier, parent)\n    if (\n      resolvedSpecifier === specifier ||\n      resolvedSpecifier.startsWith(\"node:\") ||\n      resolvedSpecifier.startsWith(\"bun:\")\n    ) {\n      return null\n    }\n\n    return resolvedSpecifier\n  } catch {\n    return null\n  }\n}\n\nconst rewriteImportsFromResolveParents = (code: string, resolveParentsByRecency: string[]): string => {\n  if (resolveParentsByRecency.length === 0) {\n    return code\n  }\n\n  const resolveFromParents = (specifier: string): string | null => {\n    if (!isBareSpecifier(specifier)) {\n      return null\n    }\n\n    for (let index = resolveParentsByRecency.length - 1; index >= 0; index -= 1) {\n      const resolveParent = resolveParentsByRecency[index]\n      const resolvedSpecifier = resolveFromParent(specifier, resolveParent)\n      if (resolvedSpecifier) {\n        return resolvedSpecifier\n      }\n    }\n\n    return null\n  }\n\n  return rewriteImportSpecifiers(code, resolveFromParents)\n}\n\nconst rewriteRuntimeSpecifiers = (code: string, runtimeModuleIdsBySpecifier: Map<string, string>): string => {\n  return rewriteImportSpecifiers(code, (specifier) => {\n    const runtimeModuleId = runtimeModuleIdsBySpecifier.get(specifier)\n    return runtimeModuleId ?? null\n  })\n}\n\nexport function createRuntimePlugin(input: CreateRuntimePluginOptions = {}): BunPlugin {\n  const runtimeModules = new Map<string, RuntimeModuleEntry>()\n  runtimeModules.set(CORE_RUNTIME_SPECIFIER, input.core ?? (coreRuntime as RuntimeModuleExports))\n  runtimeModules.set(CORE_TESTING_RUNTIME_SPECIFIER, loadCoreTestingRuntimeModule)\n\n  for (const [specifier, moduleEntry] of Object.entries(input.additional ?? {})) {\n    runtimeModules.set(specifier, moduleEntry)\n  }\n\n  const runtimeModuleIdsBySpecifier = new Map<string, string>()\n  for (const specifier of runtimeModules.keys()) {\n    runtimeModuleIdsBySpecifier.set(specifier, runtimeModuleIdForSpecifier(specifier))\n  }\n\n  return {\n    name: \"bun-plugin-opentui-runtime-modules\",\n    setup: (build) => {\n      const resolveParentsByRecency: string[] = []\n\n      for (const [specifier, moduleEntry] of runtimeModules.entries()) {\n        const moduleId = runtimeModuleIdsBySpecifier.get(specifier)\n\n        if (!moduleId) {\n          continue\n        }\n\n        build.module(moduleId, async () => ({\n          exports: await resolveRuntimeModuleExports(moduleEntry),\n          loader: \"object\",\n        }))\n\n        build.onResolve({ filter: exactSpecifierFilter(specifier) }, () => ({ path: moduleId }))\n      }\n\n      build.onLoad({ filter: runtimeSourceFilter }, async (args) => {\n        const path = sourcePath(args.path)\n        const loader = runtimeLoaderForPath(args.path)\n        if (!loader) {\n          throw new Error(`Unable to determine runtime loader for path: ${args.path}`)\n        }\n\n        const file = Bun.file(path)\n        const contents = await file.text()\n        const runtimeRewrittenContents = rewriteRuntimeSpecifiers(contents, runtimeModuleIdsBySpecifier)\n\n        if (runtimeRewrittenContents !== contents) {\n          registerResolveParent(resolveParentsByRecency, path)\n        }\n\n        const transformedContents = rewriteImportsFromResolveParents(runtimeRewrittenContents, resolveParentsByRecency)\n\n        return {\n          contents: transformedContents,\n          loader,\n        }\n      })\n    },\n  }\n}\n"
  },
  {
    "path": "packages/core/src/syntax-style.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { SyntaxStyle } from \"./syntax-style.js\"\nimport { RGBA } from \"./lib/RGBA.js\"\nimport type { StyleDefinition, ThemeTokenStyle } from \"./syntax-style.js\"\n\ndescribe(\"NativeSyntaxStyle\", () => {\n  let style: SyntaxStyle\n\n  beforeEach(() => {\n    style = SyntaxStyle.create()\n  })\n\n  afterEach(() => {\n    style.destroy()\n  })\n\n  describe(\"create\", () => {\n    it(\"should create a new NativeSyntaxStyle instance\", () => {\n      const newStyle = SyntaxStyle.create()\n      expect(newStyle).toBeDefined()\n      expect(newStyle.getStyleCount()).toBe(0)\n      newStyle.destroy()\n    })\n\n    it(\"should create multiple independent instances\", () => {\n      const style1 = SyntaxStyle.create()\n      const style2 = SyntaxStyle.create()\n\n      style1.registerStyle(\"test\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n\n      expect(style1.getStyleCount()).toBe(1)\n      expect(style2.getStyleCount()).toBe(0)\n\n      style1.destroy()\n      style2.destroy()\n    })\n  })\n\n  describe(\"registerStyle\", () => {\n    it(\"should register a simple style and return an ID\", () => {\n      const id = style.registerStyle(\"keyword\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should register style with both fg and bg colors\", () => {\n      const id = style.registerStyle(\"string\", {\n        fg: RGBA.fromValues(0, 1, 0, 1),\n        bg: RGBA.fromValues(0, 0, 0, 1),\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should register style with attributes\", () => {\n      const id = style.registerStyle(\"bold-keyword\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n        bold: true,\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should register style with multiple attributes\", () => {\n      const id = style.registerStyle(\"styled\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n        bold: true,\n        italic: true,\n        underline: true,\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should register multiple different styles\", () => {\n      const id1 = style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      const id2 = style.registerStyle(\"string\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n      const id3 = style.registerStyle(\"comment\", { fg: RGBA.fromValues(0.5, 0.5, 0.5, 1) })\n\n      expect(id1).not.toBe(id2)\n      expect(id2).not.toBe(id3)\n      expect(id1).not.toBe(id3)\n      expect(style.getStyleCount()).toBe(3)\n    })\n\n    it(\"should return existing ID when registering same style name\", () => {\n      const id1 = style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      const id2 = style.registerStyle(\"keyword\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n\n      expect(id1).toBe(id2)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should handle style without colors\", () => {\n      const id = style.registerStyle(\"plain\", {})\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should handle style with only background color\", () => {\n      const id = style.registerStyle(\"highlighted\", {\n        bg: RGBA.fromValues(1, 1, 0, 1),\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should handle style with only attributes\", () => {\n      const id = style.registerStyle(\"bold-only\", {\n        bold: true,\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should handle style with dim attribute\", () => {\n      const id = style.registerStyle(\"dimmed\", {\n        fg: RGBA.fromValues(1, 1, 1, 1),\n        dim: true,\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should register styles with special characters in names\", () => {\n      const id1 = style.registerStyle(\"keyword.control\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      const id2 = style.registerStyle(\"variable.parameter\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n\n      expect(id1).toBeGreaterThan(0)\n      expect(id2).toBeGreaterThan(0)\n      expect(id1).not.toBe(id2)\n      expect(style.getStyleCount()).toBe(2)\n    })\n\n    it(\"should register many styles without issue\", () => {\n      const ids: number[] = []\n      for (let i = 0; i < 100; i++) {\n        const id = style.registerStyle(`style-${i}`, {\n          fg: RGBA.fromValues(i / 100, 0, 0, 1),\n        })\n        ids.push(id)\n      }\n\n      expect(style.getStyleCount()).toBe(100)\n      const uniqueIds = new Set(ids)\n      expect(uniqueIds.size).toBe(100)\n    })\n  })\n\n  describe(\"resolveStyleId\", () => {\n    it(\"should resolve registered style name to ID\", () => {\n      const registeredId = style.registerStyle(\"keyword\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      const resolvedId = style.resolveStyleId(\"keyword\")\n      expect(resolvedId).toBe(registeredId)\n    })\n\n    it(\"should return null for unregistered style\", () => {\n      const id = style.resolveStyleId(\"nonexistent\")\n      expect(id).toBeNull()\n    })\n\n    it(\"should resolve multiple styles correctly\", () => {\n      const id1 = style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      const id2 = style.registerStyle(\"string\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n\n      expect(style.resolveStyleId(\"keyword\")).toBe(id1)\n      expect(style.resolveStyleId(\"string\")).toBe(id2)\n    })\n\n    it(\"should cache resolved style IDs\", () => {\n      const registeredId = style.registerStyle(\"keyword\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      const resolvedId1 = style.resolveStyleId(\"keyword\")\n      const resolvedId2 = style.resolveStyleId(\"keyword\")\n\n      expect(resolvedId1).toBe(registeredId)\n      expect(resolvedId2).toBe(registeredId)\n    })\n\n    it(\"should handle empty string style name\", () => {\n      const id = style.registerStyle(\"\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      expect(style.resolveStyleId(\"\")).toBe(id)\n    })\n\n    it(\"should be case-sensitive\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n\n      expect(style.resolveStyleId(\"keyword\")).not.toBeNull()\n      expect(style.resolveStyleId(\"Keyword\")).toBeNull()\n      expect(style.resolveStyleId(\"KEYWORD\")).toBeNull()\n    })\n  })\n\n  describe(\"getStyleId\", () => {\n    it(\"should return style ID for exact match\", () => {\n      const id = style.registerStyle(\"keyword\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      expect(style.getStyleId(\"keyword\")).toBe(id)\n    })\n\n    it(\"should fall back to base scope for dotted names\", () => {\n      const baseId = style.registerStyle(\"keyword\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      expect(style.getStyleId(\"keyword.control\")).toBe(baseId)\n      expect(style.getStyleId(\"keyword.operator\")).toBe(baseId)\n    })\n\n    it(\"should prefer exact match over base scope\", () => {\n      const baseId = style.registerStyle(\"keyword\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n      const specificId = style.registerStyle(\"keyword.control\", {\n        fg: RGBA.fromValues(0, 1, 0, 1),\n      })\n\n      expect(style.getStyleId(\"keyword\")).toBe(baseId)\n      expect(style.getStyleId(\"keyword.control\")).toBe(specificId)\n      expect(style.getStyleId(\"keyword.operator\")).toBe(baseId)\n    })\n\n    it(\"should return null for non-existent style without fallback\", () => {\n      expect(style.getStyleId(\"nonexistent\")).toBeNull()\n      expect(style.getStyleId(\"nonexistent.scope\")).toBeNull()\n    })\n\n    it(\"should handle multiple dot levels\", () => {\n      const baseId = style.registerStyle(\"meta\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      expect(style.getStyleId(\"meta.tag.xml\")).toBe(baseId)\n    })\n\n    it(\"should handle names without dots\", () => {\n      const id = style.registerStyle(\"comment\", {\n        fg: RGBA.fromValues(0.5, 0.5, 0.5, 1),\n      })\n\n      expect(style.getStyleId(\"comment\")).toBe(id)\n    })\n  })\n\n  describe(\"getStyleCount\", () => {\n    it(\"should return 0 for empty style registry\", () => {\n      expect(style.getStyleCount()).toBe(0)\n    })\n\n    it(\"should return correct count after registering styles\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      expect(style.getStyleCount()).toBe(1)\n\n      style.registerStyle(\"string\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n      expect(style.getStyleCount()).toBe(2)\n\n      style.registerStyle(\"comment\", { fg: RGBA.fromValues(0.5, 0.5, 0.5, 1) })\n      expect(style.getStyleCount()).toBe(3)\n    })\n\n    it(\"should not increment count for duplicate registrations\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      expect(style.getStyleCount()).toBe(1)\n\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n      expect(style.getStyleCount()).toBe(1)\n    })\n  })\n\n  describe(\"clearNameCache\", () => {\n    it(\"should clear the name-to-ID cache\", () => {\n      const id = style.registerStyle(\"keyword\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      style.resolveStyleId(\"keyword\")\n      style.clearNameCache()\n\n      // Should still work after clearing cache\n      expect(style.resolveStyleId(\"keyword\")).toBe(id)\n    })\n\n    it(\"should not affect registered styles\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      style.registerStyle(\"string\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n\n      style.clearNameCache()\n\n      expect(style.getStyleCount()).toBe(2)\n    })\n  })\n\n  describe(\"ptr getter\", () => {\n    it(\"should return a valid pointer\", () => {\n      const ptr = style.ptr\n      expect(ptr).toBeDefined()\n      expect(typeof ptr).toBe(\"number\")\n    })\n\n    it(\"should return same pointer for same instance\", () => {\n      const ptr1 = style.ptr\n      const ptr2 = style.ptr\n      expect(ptr1).toBe(ptr2)\n    })\n\n    it(\"should return different pointers for different instances\", () => {\n      const style2 = SyntaxStyle.create()\n      const ptr1 = style.ptr\n      const ptr2 = style2.ptr\n\n      expect(ptr1).not.toBe(ptr2)\n\n      style2.destroy()\n    })\n  })\n\n  describe(\"destroy\", () => {\n    it(\"should destroy the style instance\", () => {\n      const testStyle = SyntaxStyle.create()\n      testStyle.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n\n      testStyle.destroy()\n\n      expect(() => testStyle.getStyleCount()).toThrow(\"NativeSyntaxStyle is destroyed\")\n    })\n\n    it(\"should be safe to call destroy multiple times\", () => {\n      const testStyle = SyntaxStyle.create()\n\n      testStyle.destroy()\n      expect(() => testStyle.destroy()).not.toThrow()\n    })\n\n    it(\"should throw error when using destroyed instance\", () => {\n      const testStyle = SyntaxStyle.create()\n      testStyle.destroy()\n\n      expect(() => testStyle.registerStyle(\"test\", {})).toThrow(\"NativeSyntaxStyle is destroyed\")\n      expect(() => testStyle.resolveStyleId(\"test\")).toThrow(\"NativeSyntaxStyle is destroyed\")\n      expect(() => testStyle.getStyleId(\"test\")).toThrow(\"NativeSyntaxStyle is destroyed\")\n      expect(() => testStyle.getStyleCount()).toThrow(\"NativeSyntaxStyle is destroyed\")\n      expect(() => testStyle.ptr).toThrow(\"NativeSyntaxStyle is destroyed\")\n    })\n  })\n\n  describe(\"fromStyles\", () => {\n    it(\"should create style from styles object\", () => {\n      const styles: Record<string, StyleDefinition> = {\n        keyword: { fg: RGBA.fromValues(1, 0, 0, 1), bold: true },\n        string: { fg: RGBA.fromValues(0, 1, 0, 1) },\n        comment: { fg: RGBA.fromValues(0.5, 0.5, 0.5, 1), italic: true },\n      }\n\n      const newStyle = SyntaxStyle.fromStyles(styles)\n\n      expect(newStyle.getStyleCount()).toBe(3)\n      expect(newStyle.resolveStyleId(\"keyword\")).not.toBeNull()\n      expect(newStyle.resolveStyleId(\"string\")).not.toBeNull()\n      expect(newStyle.resolveStyleId(\"comment\")).not.toBeNull()\n\n      newStyle.destroy()\n    })\n\n    it(\"should handle empty styles object\", () => {\n      const newStyle = SyntaxStyle.fromStyles({})\n\n      expect(newStyle.getStyleCount()).toBe(0)\n\n      newStyle.destroy()\n    })\n\n    it(\"should preserve style definitions\", () => {\n      const styles: Record<string, StyleDefinition> = {\n        keyword: {\n          fg: RGBA.fromValues(1, 0, 0, 1),\n          bold: true,\n          italic: true,\n        },\n      }\n\n      const newStyle = SyntaxStyle.fromStyles(styles)\n      const id = newStyle.resolveStyleId(\"keyword\")\n\n      expect(id).not.toBeNull()\n\n      newStyle.destroy()\n    })\n  })\n\n  describe(\"fromTheme\", () => {\n    it(\"should create style from theme\", () => {\n      const theme: ThemeTokenStyle[] = [\n        {\n          scope: [\"keyword\", \"keyword.control\"],\n          style: {\n            foreground: \"#ff0000\",\n            bold: true,\n          },\n        },\n        {\n          scope: [\"string\"],\n          style: {\n            foreground: \"#00ff00\",\n          },\n        },\n      ]\n\n      const newStyle = SyntaxStyle.fromTheme(theme)\n\n      expect(newStyle.getStyleCount()).toBe(3) // keyword, keyword.control, string\n      expect(newStyle.resolveStyleId(\"keyword\")).not.toBeNull()\n      expect(newStyle.resolveStyleId(\"keyword.control\")).not.toBeNull()\n      expect(newStyle.resolveStyleId(\"string\")).not.toBeNull()\n\n      newStyle.destroy()\n    })\n\n    it(\"should handle empty theme\", () => {\n      const newStyle = SyntaxStyle.fromTheme([])\n\n      expect(newStyle.getStyleCount()).toBe(0)\n\n      newStyle.destroy()\n    })\n\n    it(\"should handle theme with multiple scopes\", () => {\n      const theme: ThemeTokenStyle[] = [\n        {\n          scope: [\"comment\", \"comment.line\", \"comment.block\"],\n          style: {\n            foreground: \"#808080\",\n            italic: true,\n          },\n        },\n      ]\n\n      const newStyle = SyntaxStyle.fromTheme(theme)\n\n      expect(newStyle.getStyleCount()).toBe(3)\n      expect(newStyle.resolveStyleId(\"comment\")).not.toBeNull()\n      expect(newStyle.resolveStyleId(\"comment.line\")).not.toBeNull()\n      expect(newStyle.resolveStyleId(\"comment.block\")).not.toBeNull()\n\n      newStyle.destroy()\n    })\n\n    it(\"should handle theme with all style properties\", () => {\n      const theme: ThemeTokenStyle[] = [\n        {\n          scope: [\"styled\"],\n          style: {\n            foreground: \"#ff0000\",\n            background: \"#000000\",\n            bold: true,\n            italic: true,\n            underline: true,\n            dim: true,\n          },\n        },\n      ]\n\n      const newStyle = SyntaxStyle.fromTheme(theme)\n\n      expect(newStyle.getStyleCount()).toBe(1)\n      expect(newStyle.resolveStyleId(\"styled\")).not.toBeNull()\n\n      newStyle.destroy()\n    })\n\n    it(\"should handle theme with rgb color format\", () => {\n      const theme: ThemeTokenStyle[] = [\n        {\n          scope: [\"keyword\"],\n          style: {\n            foreground: \"rgb(255, 0, 0)\",\n          },\n        },\n      ]\n\n      const newStyle = SyntaxStyle.fromTheme(theme)\n\n      expect(newStyle.resolveStyleId(\"keyword\")).not.toBeNull()\n\n      newStyle.destroy()\n    })\n  })\n\n  describe(\"integration tests\", () => {\n    it(\"should handle complex syntax highlighting scenario\", () => {\n      const theme: ThemeTokenStyle[] = [\n        { scope: [\"keyword\"], style: { foreground: \"#569cd6\", bold: true } },\n        { scope: [\"string\"], style: { foreground: \"#ce9178\" } },\n        { scope: [\"comment\"], style: { foreground: \"#6a9955\", italic: true } },\n        { scope: [\"variable\"], style: { foreground: \"#9cdcfe\" } },\n        { scope: [\"function\"], style: { foreground: \"#dcdcaa\" } },\n        { scope: [\"operator\"], style: { foreground: \"#d4d4d4\" } },\n      ]\n\n      const syntaxStyle = SyntaxStyle.fromTheme(theme)\n\n      expect(syntaxStyle.getStyleCount()).toBe(6)\n\n      const keywordId = syntaxStyle.getStyleId(\"keyword\")\n      const stringId = syntaxStyle.getStyleId(\"string\")\n      const commentId = syntaxStyle.getStyleId(\"comment\")\n\n      expect(keywordId).not.toBeNull()\n      expect(stringId).not.toBeNull()\n      expect(commentId).not.toBeNull()\n\n      expect(keywordId).not.toBe(stringId)\n      expect(stringId).not.toBe(commentId)\n\n      syntaxStyle.destroy()\n    })\n\n    it(\"should handle registering and resolving many styles efficiently\", () => {\n      const start = Date.now()\n\n      for (let i = 0; i < 1000; i++) {\n        style.registerStyle(`style-${i}`, {\n          fg: RGBA.fromValues(Math.random(), Math.random(), Math.random(), 1),\n        })\n      }\n\n      const registerTime = Date.now() - start\n\n      const resolveStart = Date.now()\n      for (let i = 0; i < 1000; i++) {\n        style.resolveStyleId(`style-${i}`)\n      }\n      const resolveTime = Date.now() - resolveStart\n\n      expect(registerTime).toBeLessThan(1000) // Should register 1000 styles in < 1s\n      expect(resolveTime).toBeLessThan(100) // Should resolve 1000 styles in < 100ms\n\n      expect(style.getStyleCount()).toBe(1000)\n    })\n\n    it(\"should handle style name collisions correctly\", () => {\n      const id1 = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      const id2 = style.registerStyle(\"test\", {\n        fg: RGBA.fromValues(0, 1, 0, 1),\n        bold: true,\n      })\n\n      expect(id1).toBe(id2)\n      expect(style.getStyleCount()).toBe(1)\n    })\n\n    it(\"should maintain style registry across cache clears\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      style.registerStyle(\"string\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n      style.registerStyle(\"comment\", { fg: RGBA.fromValues(0.5, 0.5, 0.5, 1) })\n\n      const count1 = style.getStyleCount()\n      style.clearNameCache()\n      const count2 = style.getStyleCount()\n\n      expect(count1).toBe(count2)\n      expect(count1).toBe(3)\n    })\n  })\n\n  describe(\"edge cases\", () => {\n    it(\"should handle very long style names\", () => {\n      const longName = \"a\".repeat(1000)\n      const id = style.registerStyle(longName, {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.resolveStyleId(longName)).toBe(id)\n    })\n\n    it(\"should handle style names with unicode characters\", () => {\n      const id = style.registerStyle(\"关键字\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      expect(id).toBeGreaterThan(0)\n      expect(style.resolveStyleId(\"关键字\")).toBe(id)\n    })\n\n    it(\"should handle style names with special characters\", () => {\n      const specialNames = [\n        \"style-with-dashes\",\n        \"style_with_underscores\",\n        \"style.with.dots\",\n        \"style:with:colons\",\n        \"style/with/slashes\",\n      ]\n\n      for (const name of specialNames) {\n        const id = style.registerStyle(name, {\n          fg: RGBA.fromValues(1, 0, 0, 1),\n        })\n        expect(id).toBeGreaterThan(0)\n        expect(style.resolveStyleId(name)).toBe(id)\n      }\n    })\n\n    it(\"should handle colors with full alpha range\", () => {\n      const id1 = style.registerStyle(\"transparent\", {\n        fg: RGBA.fromValues(1, 0, 0, 0),\n      })\n      const id2 = style.registerStyle(\"semi-transparent\", {\n        fg: RGBA.fromValues(1, 0, 0, 0.5),\n      })\n      const id3 = style.registerStyle(\"opaque\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      })\n\n      expect(id1).toBeGreaterThan(0)\n      expect(id2).toBeGreaterThan(0)\n      expect(id3).toBeGreaterThan(0)\n    })\n\n    it(\"should handle all attribute combinations\", () => {\n      const combinations = [\n        { bold: true },\n        { italic: true },\n        { underline: true },\n        { dim: true },\n        { bold: true, italic: true },\n        { bold: true, underline: true },\n        { bold: true, dim: true },\n        { italic: true, underline: true },\n        { italic: true, dim: true },\n        { underline: true, dim: true },\n        { bold: true, italic: true, underline: true },\n        { bold: true, italic: true, dim: true },\n        { bold: true, underline: true, dim: true },\n        { italic: true, underline: true, dim: true },\n        { bold: true, italic: true, underline: true, dim: true },\n      ]\n\n      for (let i = 0; i < combinations.length; i++) {\n        const id = style.registerStyle(`combo-${i}`, {\n          fg: RGBA.fromValues(1, 0, 0, 1),\n          ...combinations[i],\n        })\n        expect(id).toBeGreaterThan(0)\n      }\n\n      expect(style.getStyleCount()).toBe(combinations.length)\n    })\n  })\n\n  describe(\"getStyle\", () => {\n    it(\"should retrieve registered style definition\", () => {\n      const styleDef = { fg: RGBA.fromValues(1, 0, 0, 1), bold: true }\n      style.registerStyle(\"keyword\", styleDef)\n\n      const retrieved = style.getStyle(\"keyword\")\n      expect(retrieved).toBeDefined()\n      expect(retrieved?.fg).toEqual(styleDef.fg)\n      expect(retrieved?.bold).toBe(true)\n    })\n\n    it(\"should return undefined for unregistered style\", () => {\n      expect(style.getStyle(\"nonexistent\")).toBeUndefined()\n    })\n\n    it(\"should fall back to base scope for dotted names\", () => {\n      const baseDef = { fg: RGBA.fromValues(1, 0, 0, 1), bold: true }\n      style.registerStyle(\"keyword\", baseDef)\n\n      const retrieved = style.getStyle(\"keyword.control\")\n      expect(retrieved).toBeDefined()\n      expect(retrieved?.fg).toEqual(baseDef.fg)\n      expect(retrieved?.bold).toBe(true)\n    })\n\n    it(\"should prefer exact match over base scope\", () => {\n      const baseDef = { fg: RGBA.fromValues(1, 0, 0, 1) }\n      const specificDef = { fg: RGBA.fromValues(0, 1, 0, 1), bold: true }\n\n      style.registerStyle(\"keyword\", baseDef)\n      style.registerStyle(\"keyword.control\", specificDef)\n\n      const exactMatch = style.getStyle(\"keyword.control\")\n      expect(exactMatch?.fg).toEqual(specificDef.fg)\n      expect(exactMatch?.bold).toBe(true)\n\n      const baseMatch = style.getStyle(\"keyword.operator\")\n      expect(baseMatch?.fg).toEqual(baseDef.fg)\n    })\n\n    it(\"should not return Object prototype properties\", () => {\n      expect(style.getStyle(\"constructor\")).toBeUndefined()\n      expect(style.getStyle(\"toString\")).toBeUndefined()\n      expect(style.getStyle(\"hasOwnProperty\")).toBeUndefined()\n    })\n\n    it(\"should handle style named constructor correctly\", () => {\n      const constructorDef = { fg: RGBA.fromValues(1, 0.5, 0, 1), bold: true }\n      style.registerStyle(\"constructor\", constructorDef)\n\n      const retrieved = style.getStyle(\"constructor\")\n      expect(retrieved).toBeDefined()\n      expect(retrieved?.fg).toEqual(constructorDef.fg)\n      expect(retrieved?.bold).toBe(true)\n    })\n\n    it(\"should handle multiple dot levels\", () => {\n      const baseDef = { fg: RGBA.fromValues(1, 0, 0, 1) }\n      style.registerStyle(\"meta\", baseDef)\n\n      const retrieved = style.getStyle(\"meta.tag.xml\")\n      expect(retrieved).toBeDefined()\n      expect(retrieved?.fg).toEqual(baseDef.fg)\n    })\n  })\n\n  describe(\"mergeStyles\", () => {\n    it(\"should merge single style correctly\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1), bold: true })\n\n      const merged = style.mergeStyles(\"keyword\")\n      expect(merged.fg).toEqual(RGBA.fromValues(1, 0, 0, 1))\n      expect(merged.attributes).toBeGreaterThan(0)\n    })\n\n    it(\"should merge multiple styles with later taking precedence\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1), bold: true })\n      style.registerStyle(\"emphasis\", { italic: true })\n      style.registerStyle(\"override\", { fg: RGBA.fromValues(0, 1, 0, 1) })\n\n      const merged = style.mergeStyles(\"keyword\", \"emphasis\", \"override\")\n      expect(merged.fg).toEqual(RGBA.fromValues(0, 1, 0, 1))\n      expect(merged.attributes).toBeGreaterThan(0)\n    })\n\n    it(\"should handle dotted style names with fallback\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1), bold: true })\n\n      const merged = style.mergeStyles(\"keyword.operator\")\n      expect(merged.fg).toEqual(RGBA.fromValues(1, 0, 0, 1))\n      expect(merged.attributes).toBeGreaterThan(0)\n    })\n\n    it(\"should return empty merge for non-existent styles\", () => {\n      const merged = style.mergeStyles(\"nonexistent\")\n      expect(merged.fg).toBeUndefined()\n      expect(merged.bg).toBeUndefined()\n      expect(merged.attributes).toBe(0)\n    })\n\n    it(\"should cache merged results\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n\n      expect(style.getCacheSize()).toBe(0)\n\n      const result1 = style.mergeStyles(\"keyword.operator\")\n      expect(style.getCacheSize()).toBe(1)\n\n      const result2 = style.mergeStyles(\"keyword.operator\")\n      expect(style.getCacheSize()).toBe(1)\n\n      expect(result1).toBe(result2)\n    })\n\n    it(\"should handle all style attributes correctly\", () => {\n      style.registerStyle(\"complex\", {\n        fg: RGBA.fromValues(1, 0, 0, 1),\n        bg: RGBA.fromValues(0.2, 0.2, 0.2, 1),\n        bold: true,\n        italic: true,\n        underline: true,\n        dim: true,\n      })\n\n      const merged = style.mergeStyles(\"complex\")\n      expect(merged.fg).toEqual(RGBA.fromValues(1, 0, 0, 1))\n      expect(merged.bg).toEqual(RGBA.fromValues(0.2, 0.2, 0.2, 1))\n      expect(merged.attributes).toBeGreaterThan(0)\n    })\n\n    it(\"should handle empty style names\", () => {\n      const merged = style.mergeStyles()\n      expect(merged.fg).toBeUndefined()\n      expect(merged.bg).toBeUndefined()\n      expect(merged.attributes).toBe(0)\n    })\n  })\n\n  describe(\"clearCache and getCacheSize\", () => {\n    it(\"should clear merged style cache\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      style.mergeStyles(\"keyword\")\n      style.mergeStyles(\"keyword.operator\")\n\n      expect(style.getCacheSize()).toBe(2)\n\n      style.clearCache()\n      expect(style.getCacheSize()).toBe(0)\n    })\n\n    it(\"should not affect registered styles when clearing cache\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n      style.mergeStyles(\"keyword\")\n\n      style.clearCache()\n\n      expect(style.getStyleCount()).toBe(1)\n      expect(style.resolveStyleId(\"keyword\")).not.toBeNull()\n    })\n\n    it(\"should allow re-merging after cache clear\", () => {\n      style.registerStyle(\"keyword\", { fg: RGBA.fromValues(1, 0, 0, 1) })\n\n      const result1 = style.mergeStyles(\"keyword\")\n      style.clearCache()\n      const result2 = style.mergeStyles(\"keyword\")\n\n      expect(result1.fg).toEqual(result2.fg)\n      expect(result1.attributes).toBe(result2.attributes)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/syntax-style.ts",
    "content": "import { RGBA, parseColor, type ColorInput } from \"./lib/RGBA.js\"\nimport { resolveRenderLib, type RenderLib } from \"./zig.js\"\nimport { type Pointer } from \"bun:ffi\"\nimport { createTextAttributes } from \"./utils.js\"\n\nexport interface StyleDefinition {\n  fg?: RGBA\n  bg?: RGBA\n  bold?: boolean\n  italic?: boolean\n  underline?: boolean\n  dim?: boolean\n}\n\nexport interface MergedStyle {\n  fg?: RGBA\n  bg?: RGBA\n  attributes: number\n}\n\nexport interface ThemeTokenStyle {\n  scope: string[]\n  style: {\n    foreground?: ColorInput\n    background?: ColorInput\n    bold?: boolean\n    italic?: boolean\n    underline?: boolean\n    dim?: boolean\n  }\n}\n\nexport function convertThemeToStyles(theme: ThemeTokenStyle[]): Record<string, StyleDefinition> {\n  const flatStyles: Record<string, StyleDefinition> = {}\n\n  for (const tokenStyle of theme) {\n    const styleDefinition: StyleDefinition = {}\n\n    if (tokenStyle.style.foreground) {\n      styleDefinition.fg = parseColor(tokenStyle.style.foreground)\n    }\n    if (tokenStyle.style.background) {\n      styleDefinition.bg = parseColor(tokenStyle.style.background)\n    }\n\n    if (tokenStyle.style.bold !== undefined) {\n      styleDefinition.bold = tokenStyle.style.bold\n    }\n    if (tokenStyle.style.italic !== undefined) {\n      styleDefinition.italic = tokenStyle.style.italic\n    }\n    if (tokenStyle.style.underline !== undefined) {\n      styleDefinition.underline = tokenStyle.style.underline\n    }\n    if (tokenStyle.style.dim !== undefined) {\n      styleDefinition.dim = tokenStyle.style.dim\n    }\n\n    // Apply the same style to all scopes\n    for (const scope of tokenStyle.scope) {\n      flatStyles[scope] = styleDefinition\n    }\n  }\n\n  return flatStyles\n}\n\nexport class SyntaxStyle {\n  private lib: RenderLib\n  private stylePtr: Pointer\n  private _destroyed: boolean = false\n  private nameCache: Map<string, number> = new Map()\n  private styleDefs: Map<string, StyleDefinition> = new Map()\n  private mergedCache: Map<string, MergedStyle> = new Map()\n\n  constructor(lib: RenderLib, ptr: Pointer) {\n    this.lib = lib\n    this.stylePtr = ptr\n  }\n\n  static create(): SyntaxStyle {\n    const lib = resolveRenderLib()\n    const ptr = lib.createSyntaxStyle()\n    return new SyntaxStyle(lib, ptr)\n  }\n\n  static fromTheme(theme: ThemeTokenStyle[]): SyntaxStyle {\n    const style = SyntaxStyle.create()\n    const flatStyles = convertThemeToStyles(theme)\n\n    for (const [name, styleDef] of Object.entries(flatStyles)) {\n      style.registerStyle(name, styleDef)\n    }\n\n    return style\n  }\n\n  static fromStyles(styles: Record<string, StyleDefinition>): SyntaxStyle {\n    const style = SyntaxStyle.create()\n\n    for (const [name, styleDef] of Object.entries(styles)) {\n      style.registerStyle(name, styleDef)\n    }\n\n    return style\n  }\n\n  private guard(): void {\n    if (this._destroyed) throw new Error(\"NativeSyntaxStyle is destroyed\")\n  }\n\n  public registerStyle(name: string, style: StyleDefinition): number {\n    this.guard()\n\n    const attributes = createTextAttributes({\n      bold: style.bold,\n      italic: style.italic,\n      underline: style.underline,\n      dim: style.dim,\n    })\n\n    const id = this.lib.syntaxStyleRegister(this.stylePtr, name, style.fg || null, style.bg || null, attributes)\n\n    this.nameCache.set(name, id)\n    this.styleDefs.set(name, style)\n\n    return id\n  }\n\n  public resolveStyleId(name: string): number | null {\n    this.guard()\n\n    // Check cache first\n    const cached = this.nameCache.get(name)\n    if (cached !== undefined) return cached\n\n    const id = this.lib.syntaxStyleResolveByName(this.stylePtr, name)\n\n    if (id !== null) {\n      this.nameCache.set(name, id)\n    }\n\n    return id\n  }\n\n  public getStyleId(name: string): number | null {\n    this.guard()\n\n    const id = this.resolveStyleId(name)\n    if (id !== null) return id\n\n    // Try base name if it's a scoped style\n    if (name.includes(\".\")) {\n      const baseName = name.split(\".\")[0]\n      return this.resolveStyleId(baseName)\n    }\n\n    return null\n  }\n\n  public get ptr(): Pointer {\n    this.guard()\n    return this.stylePtr\n  }\n\n  public getStyleCount(): number {\n    this.guard()\n    return this.lib.syntaxStyleGetStyleCount(this.stylePtr)\n  }\n\n  public clearNameCache(): void {\n    this.nameCache.clear()\n  }\n\n  public getStyle(name: string): StyleDefinition | undefined {\n    this.guard()\n\n    if (Object.prototype.hasOwnProperty.call(this.styleDefs, name)) {\n      return undefined\n    }\n\n    const style = this.styleDefs.get(name)\n    if (style) return style\n\n    if (name.includes(\".\")) {\n      const baseName = name.split(\".\")[0]\n      if (Object.prototype.hasOwnProperty.call(this.styleDefs, baseName)) {\n        return undefined\n      }\n      return this.styleDefs.get(baseName)\n    }\n\n    return undefined\n  }\n\n  public mergeStyles(...styleNames: string[]): MergedStyle {\n    this.guard()\n\n    const cacheKey = styleNames.join(\":\")\n    const cached = this.mergedCache.get(cacheKey)\n    if (cached) return cached\n\n    const styleDefinition: StyleDefinition = {}\n\n    for (const name of styleNames) {\n      const style = this.getStyle(name)\n\n      if (!style) continue\n\n      if (style.fg) styleDefinition.fg = style.fg\n      if (style.bg) styleDefinition.bg = style.bg\n      if (style.bold !== undefined) styleDefinition.bold = style.bold\n      if (style.italic !== undefined) styleDefinition.italic = style.italic\n      if (style.underline !== undefined) styleDefinition.underline = style.underline\n      if (style.dim !== undefined) styleDefinition.dim = style.dim\n    }\n\n    const attributes = createTextAttributes({\n      bold: styleDefinition.bold,\n      italic: styleDefinition.italic,\n      underline: styleDefinition.underline,\n      dim: styleDefinition.dim,\n    })\n\n    const merged: MergedStyle = {\n      fg: styleDefinition.fg,\n      bg: styleDefinition.bg,\n      attributes,\n    }\n\n    this.mergedCache.set(cacheKey, merged)\n\n    return merged\n  }\n\n  public clearCache(): void {\n    this.guard()\n    this.mergedCache.clear()\n  }\n\n  public getCacheSize(): number {\n    this.guard()\n    return this.mergedCache.size\n  }\n\n  public getAllStyles(): Map<string, StyleDefinition> {\n    this.guard()\n    return new Map(this.styleDefs)\n  }\n\n  public getRegisteredNames(): string[] {\n    this.guard()\n    return Array.from(this.styleDefs.keys())\n  }\n\n  public destroy(): void {\n    if (this._destroyed) return\n    this._destroyed = true\n    this.nameCache.clear()\n    this.styleDefs.clear()\n    this.mergedCache.clear()\n    this.lib.destroySyntaxStyle(this.stylePtr)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/testing/README.md",
    "content": "# Testing Utilities\n\nTest utilities for opentui terminal UI testing.\n\n## Test Renderer\n\n```ts\nimport { createTestRenderer } from \"@opentui/core/testing\"\n\nconst { renderer, mockInput, mockMouse, renderOnce, captureCharFrame, resize } = await createTestRenderer({\n  width: 80,\n  height: 24,\n})\n\n// Render once and capture output\nawait renderOnce()\nconst output = captureCharFrame()\n\n// Resize terminal\nresize(100, 30)\n```\n\n## Mock Keyboard Input\n\n```ts\nimport { createMockKeys, KeyCodes } from \"@opentui/core/testing\"\n\nconst mockInput = createMockKeys(renderer)\n\n// Type text\nmockInput.typeText(\"hello world\")\nawait mockInput.typeText(\"hello\", 10) // 10ms delay between keys\n\n// Press single keys\nmockInput.pressKey(\"a\")\nmockInput.pressKey(KeyCodes.ENTER)\n\n// Press keys with modifiers\nmockInput.pressKey(\"a\", { ctrl: true })\nmockInput.pressKey(\"f\", { meta: true })\nmockInput.pressKey(\"z\", { ctrl: true, shift: true })\nmockInput.pressKey(KeyCodes.ARROW_LEFT, { meta: true })\n\n// Press multiple keys\nmockInput.pressKeys([\"h\", \"e\", \"l\", \"l\", \"o\"])\nawait mockInput.pressKeys([\"a\", \"b\"], 10) // with delay\n\n// Convenience methods\nmockInput.pressEnter()\nmockInput.pressEnter({ meta: true })\nmockInput.pressEscape()\nmockInput.pressTab()\nmockInput.pressBackspace()\nmockInput.pressArrow(\"up\" | \"down\" | \"left\" | \"right\")\nmockInput.pressArrow(\"left\", { meta: true })\nmockInput.pressCtrlC()\nmockInput.pasteBracketedText(\"paste content\")\n```\n\n### KeyCodes\n\nSpecial keycodes available: `RETURN`, `LINEFEED`, `TAB`, `BACKSPACE`, `DELETE`, `HOME`, `END`, `ESCAPE`, `ARROW_UP`, `ARROW_DOWN`, `ARROW_LEFT`, `ARROW_RIGHT`, `F1`-`F12`\n\n### Modifiers\n\nAll `pressKey()`, `pressEnter()`, `pressEscape()`, `pressTab()`, `pressBackspace()`, and `pressArrow()` methods support an optional modifiers object:\n\n```ts\n{ ctrl?: boolean; shift?: boolean; meta?: boolean }\n```\n\n## Mock Mouse Input\n\n```ts\nimport { createMockMouse, MouseButtons } from \"@opentui/core/testing\"\n\nconst mockMouse = createMockMouse(renderer)\n\n// Click\nawait mockMouse.click(x, y)\nawait mockMouse.click(x, y, MouseButtons.RIGHT)\nawait mockMouse.click(x, y, MouseButtons.LEFT, {\n  modifiers: { ctrl: true, shift: true, alt: true },\n  delayMs: 10,\n})\n\n// Double click\nawait mockMouse.doubleClick(x, y)\n\n// Press and release\nawait mockMouse.pressDown(x, y, MouseButtons.MIDDLE)\nawait mockMouse.release(x, y, MouseButtons.MIDDLE)\n\n// Move\nawait mockMouse.moveTo(x, y)\nawait mockMouse.moveTo(x, y, { modifiers: { shift: true } })\n\n// Drag\nawait mockMouse.drag(startX, startY, endX, endY)\nawait mockMouse.drag(startX, startY, endX, endY, MouseButtons.RIGHT, {\n  modifiers: { alt: true },\n})\n\n// Scroll\nawait mockMouse.scroll(x, y, \"up\" | \"down\" | \"left\" | \"right\")\nawait mockMouse.scroll(x, y, \"up\", { modifiers: { shift: true } })\n\n// State\nconst pos = mockMouse.getCurrentPosition() // { x, y }\nconst buttons = mockMouse.getPressedButtons() // MouseButton[]\n```\n\n### MouseButtons\n\n`LEFT` (0), `MIDDLE` (1), `RIGHT` (2)\n\n`WHEEL_UP` (64), `WHEEL_DOWN` (65), `WHEEL_LEFT` (66), `WHEEL_RIGHT` (67)\n\n## Spy\n\nSimple function spy for testing callbacks.\n\n```ts\nimport { createSpy } from \"@opentui/core/testing\"\n\nconst spy = createSpy()\n\n// Use as callback\nsomeFunction(spy)\n\n// Assertions\nspy.callCount() // number\nspy.calledWith(arg1, arg2) // boolean\nspy.calls // any[][]\nspy.reset()\n```\n\n## Test Recorder\n\nRecord frames during rendering for testing or analysis.\n\n```ts\nimport { TestRecorder } from \"@opentui/core/testing\"\n\nconst { renderer, renderOnce } = await createTestRenderer({ width: 80, height: 24 })\nconst recorder = new TestRecorder(renderer)\n\n// Start recording\nrecorder.rec()\n\n// Add content and trigger renders\nconst text = new TextRenderable(renderer, { content: \"Hello\" })\nrenderer.root.add(text)\nawait Bun.sleep(1) // Wait for automatic render from add()\n\ntext.content = \"World\"\nawait Bun.sleep(1) // Wait for automatic render from content change\n\n// Stop recording\nrecorder.stop()\n\n// Access recorded frames\nconst frames = recorder.recordedFrames\nconsole.log(`Recorded ${frames.length} frames`)\n\nframes.forEach((frame) => {\n  console.log(`Frame ${frame.frameNumber} at ${frame.timestamp}ms:`)\n  console.log(frame.frame)\n})\n\n// Clear and start new recording\nrecorder.clear()\nrecorder.rec()\n```\n\n### TestRecorder API\n\n- `rec()` - Start recording frames\n- `stop()` - Stop recording frames\n- `clear()` - Clear all recorded frames\n- `isRecording` - Check if currently recording\n- `recordedFrames` - Get array of recorded frames (returns a copy)\n\n### RecordedFrame\n\nEach frame contains:\n\n- `frame: string` - The captured frame content\n- `timestamp: number` - Time in milliseconds since recording started\n- `frameNumber: number` - Sequential frame number (0-indexed)\n\n## Example\n\n```ts\nimport { test, expect } from \"bun:test\"\nimport { createTestRenderer } from \"@opentui/core/testing\"\n\ntest(\"button click\", async () => {\n  const { renderer, mockMouse, renderOnce, captureCharFrame } = await createTestRenderer({ width: 80, height: 24 })\n\n  const clicked = createSpy()\n  const button = new Button(\"btn\", { text: \"Click me\", onClick: clicked })\n\n  renderer.add(button)\n  await renderOnce()\n\n  await mockMouse.click(10, 5)\n  expect(clicked.callCount()).toBe(1)\n})\n```\n"
  },
  {
    "path": "packages/core/src/testing/capture-spans.test.ts",
    "content": "import { describe, test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"./test-renderer.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\nimport { BoxRenderable } from \"../renderables/Box.js\"\nimport { TextAttributes, type CapturedFrame } from \"../types.js\"\nimport { RGBA } from \"../lib/index.js\"\n\ndescribe(\"captureSpans\", () => {\n  let renderer: TestRenderer\n  let renderOnce: () => Promise<void>\n  let captureSpans: () => CapturedFrame\n\n  beforeEach(async () => {\n    const setup = await createTestRenderer({ width: 40, height: 10 })\n    renderer = setup.renderer\n    renderOnce = setup.renderOnce\n    captureSpans = setup.captureSpans\n  })\n\n  afterEach(() => {\n    renderer.destroy()\n  })\n\n  test(\"returns correct dimensions and line count\", async () => {\n    await renderOnce()\n    const data = captureSpans()\n\n    expect(data.cols).toBe(40)\n    expect(data.rows).toBe(10)\n    expect(data.lines.length).toBe(10)\n  })\n\n  test(\"captures text content in spans\", async () => {\n    const text = new TextRenderable(renderer, { content: \"Hello World\" })\n    renderer.root.add(text)\n    await renderOnce()\n\n    const data = captureSpans()\n    const firstLine = data.lines[0]\n    const textContent = firstLine.spans.map((s) => s.text).join(\"\")\n\n    expect(textContent).toContain(\"Hello World\")\n  })\n\n  test(\"groups consecutive cells with same styling into single span\", async () => {\n    const text = new TextRenderable(renderer, { content: \"AAAA\" })\n    renderer.root.add(text)\n    await renderOnce()\n\n    const data = captureSpans()\n    const firstLine = data.lines[0]\n    const aaaSpan = firstLine.spans.find((s) => s.text.includes(\"AAAA\"))\n\n    expect(aaaSpan).toBeDefined()\n    expect(aaaSpan!.width).toBeGreaterThanOrEqual(4)\n  })\n\n  test(\"captures foreground color\", async () => {\n    const text = new TextRenderable(renderer, {\n      content: \"Red Text\",\n      fg: RGBA.fromHex(\"#ff0000\"),\n    })\n    renderer.root.add(text)\n    await renderOnce()\n\n    const data = captureSpans()\n    const firstLine = data.lines[0]\n    const redSpan = firstLine.spans.find((s) => s.text.includes(\"Red\"))\n\n    expect(redSpan).toBeDefined()\n    expect(redSpan!.fg.r).toBe(1)\n    expect(redSpan!.fg.g).toBe(0)\n    expect(redSpan!.fg.b).toBe(0)\n  })\n\n  test(\"captures background color\", async () => {\n    const box = new BoxRenderable(renderer, {\n      width: 10,\n      height: 3,\n      backgroundColor: RGBA.fromHex(\"#00ff00\"),\n    })\n    renderer.root.add(box)\n    await renderOnce()\n\n    const data = captureSpans()\n    const secondLine = data.lines[1]\n    const greenSpan = secondLine.spans.find((s) => s.bg.g === 1 && s.bg.r === 0 && s.bg.b === 0)\n\n    expect(greenSpan).toBeDefined()\n  })\n\n  test(\"returns alpha 0 for transparent colors\", async () => {\n    await renderOnce()\n\n    const data = captureSpans()\n    const firstLine = data.lines[0]\n    const transparentSpan = firstLine.spans.find((s) => s.bg.a === 0)\n\n    expect(transparentSpan).toBeDefined()\n  })\n\n  test(\"captures text attributes\", async () => {\n    const text = new TextRenderable(renderer, {\n      content: \"Styled\",\n      attributes: TextAttributes.BOLD | TextAttributes.ITALIC | TextAttributes.UNDERLINE | TextAttributes.DIM,\n    })\n    renderer.root.add(text)\n    await renderOnce()\n\n    const data = captureSpans()\n    const firstLine = data.lines[0]\n    const styledSpan = firstLine.spans.find((s) => s.text.includes(\"Styled\"))\n\n    expect(styledSpan).toBeDefined()\n    expect(styledSpan!.attributes & TextAttributes.BOLD).toBeTruthy()\n    expect(styledSpan!.attributes & TextAttributes.ITALIC).toBeTruthy()\n    expect(styledSpan!.attributes & TextAttributes.UNDERLINE).toBeTruthy()\n    expect(styledSpan!.attributes & TextAttributes.DIM).toBeTruthy()\n  })\n\n  test(\"includes cursor position\", async () => {\n    await renderOnce()\n    const data = captureSpans()\n\n    expect(data.cursor).toEqual([expect.any(Number), expect.any(Number)])\n  })\n\n  test(\"splits spans when styling changes\", async () => {\n    const text1 = new TextRenderable(renderer, {\n      content: \"AAA\",\n      fg: RGBA.fromHex(\"#ff0000\"),\n    })\n    const text2 = new TextRenderable(renderer, {\n      content: \"BBB\",\n      fg: RGBA.fromHex(\"#00ff00\"),\n    })\n    renderer.root.add(text1)\n    renderer.root.add(text2)\n    await renderOnce()\n\n    const data = captureSpans()\n    const allSpans = data.lines.flatMap((l) => l.spans)\n\n    expect(allSpans.some((s) => s.fg.r === 1 && s.fg.g === 0)).toBe(true)\n    expect(allSpans.some((s) => s.fg.g === 1 && s.fg.r === 0)).toBe(true)\n  })\n\n  test(\"handles box-drawing characters without crashing\", async () => {\n    const text = new TextRenderable(renderer, {\n      content: \"├── folder\",\n    })\n    renderer.root.add(text)\n    await renderOnce()\n\n    const data = captureSpans()\n    const firstLine = data.lines[0]\n    const textContent = firstLine.spans.map((s) => s.text).join(\"\")\n\n    expect(textContent).toContain(\"├── folder\")\n  })\n\n  test(\"handles box borders without crashing\", async () => {\n    const box = new BoxRenderable(renderer, {\n      width: 10,\n      height: 4,\n      border: true,\n      borderStyle: \"single\",\n      borderColor: RGBA.fromHex(\"#ffffff\"),\n    })\n    renderer.root.add(box)\n    await renderOnce()\n\n    const data = captureSpans()\n    expect(data.lines.length).toBe(10)\n\n    const firstLine = data.lines[0]\n    const textContent = firstLine.spans.map((s) => s.text).join(\"\")\n    expect(textContent.includes(\"┌\") || textContent.includes(\"─\")).toBe(true)\n  })\n\n  test(\"handles multi-width characters correctly\", async () => {\n    const text = new TextRenderable(renderer, {\n      content: \"A🌟B\",\n    })\n    renderer.root.add(text)\n    await renderOnce()\n\n    const data = captureSpans()\n    const firstLine = data.lines[0]\n    const textContent = firstLine.spans.map((s) => s.text).join(\"\")\n\n    expect(textContent).toContain(\"A🌟B\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/testing/integration.test.ts",
    "content": "import { describe, test, expect } from \"bun:test\"\nimport { createMockMouse, MouseButtons } from \"./mock-mouse.js\"\nimport { MouseParser } from \"../lib/parse.mouse.js\"\n\nclass MockRenderer {\n  public stdin: { emit: (event: string, data: Buffer) => void }\n  public emittedData: Buffer[] = []\n\n  constructor() {\n    this.stdin = {\n      emit: (event: string, chunk: Buffer) => {\n        this.emittedData.push(chunk)\n      },\n    }\n  }\n\n  getEmittedData(): Buffer {\n    return Buffer.concat(this.emittedData)\n  }\n}\n\n// Helper function to parse all events from buffer\nfunction parseAllEvents(emittedData: Buffer, parser: MouseParser) {\n  return parser.parseAllMouseEvents(emittedData)\n}\n\ndescribe(\"mock-mouse + parser integration\", () => {\n  test(\"simple click is correctly parsed\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.click(10, 5)\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents).toHaveLength(2)\n    expect(parsedEvents[0]).toEqual({\n      type: \"down\",\n      button: 0,\n      x: 10,\n      y: 5,\n      modifiers: { shift: false, alt: false, ctrl: false },\n      scroll: undefined,\n    })\n    expect(parsedEvents[1]).toEqual({\n      type: \"up\",\n      button: 0,\n      x: 10,\n      y: 5,\n      modifiers: { shift: false, alt: false, ctrl: false },\n      scroll: undefined,\n    })\n  })\n\n  test(\"double click is correctly parsed\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.doubleClick(10, 5)\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents).toHaveLength(4)\n    // All events should be at the same position with LEFT button\n    parsedEvents.forEach((event) => {\n      expect(event.x).toBe(10)\n      expect(event.y).toBe(5)\n      expect(event.button).toBe(0)\n    })\n    // Should alternate down/up\n    expect(parsedEvents.map((e) => e.type)).toEqual([\"down\", \"up\", \"down\", \"up\"])\n  })\n\n  test(\"pressDown and release separately are correctly parsed\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.pressDown(10, 5, MouseButtons.MIDDLE)\n    await mockMouse.release(10, 5, MouseButtons.MIDDLE)\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents).toHaveLength(2)\n    expect(parsedEvents[0]).toEqual({\n      type: \"down\",\n      button: 1, // MIDDLE button\n      x: 10,\n      y: 5,\n      modifiers: { shift: false, alt: false, ctrl: false },\n      scroll: undefined,\n    })\n    expect(parsedEvents[1]).toEqual({\n      type: \"up\",\n      button: 1,\n      x: 10,\n      y: 5,\n      modifiers: { shift: false, alt: false, ctrl: false },\n      scroll: undefined,\n    })\n  })\n\n  test(\"different buttons work correctly\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    // Test RIGHT button\n    await mockMouse.click(10, 5, MouseButtons.RIGHT)\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents).toHaveLength(2)\n    parsedEvents.forEach((event) => {\n      expect(event.button).toBe(2) // RIGHT button\n      expect(event.x).toBe(10)\n      expect(event.y).toBe(5)\n    })\n  })\n\n  test(\"all scroll directions are correctly parsed\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.scroll(15, 8, \"up\")\n    await mockMouse.scroll(15, 8, \"down\")\n    await mockMouse.scroll(15, 8, \"left\")\n    await mockMouse.scroll(15, 8, \"right\")\n\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents).toHaveLength(4)\n    const expectedDirections: (\"up\" | \"down\" | \"left\" | \"right\")[] = [\"up\", \"down\", \"left\", \"right\"]\n    const expectedButtons = [0, 1, 2, 0] // Based on parser logic: button & 3, with button=3 becoming 0\n    parsedEvents.forEach((event, index) => {\n      expect(event.type).toBe(\"scroll\")\n      expect(event.button).toBe(expectedButtons[index])\n      expect(event.x).toBe(15)\n      expect(event.y).toBe(8)\n      expect(event.scroll).toEqual({\n        direction: expectedDirections[index],\n        delta: 1,\n      })\n    })\n  })\n\n  test(\"scroll with modifiers is correctly parsed\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.scroll(15, 8, \"left\", { modifiers: { shift: true } })\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents).toHaveLength(1)\n    expect(parsedEvents[0]).toEqual({\n      type: \"scroll\",\n      button: 2, // WHEEL_LEFT (66) & 3 = 2\n      x: 15,\n      y: 8,\n      modifiers: { shift: true, alt: false, ctrl: false },\n      scroll: { direction: \"left\", delta: 1 },\n    })\n  })\n\n  test(\"drag events are correctly parsed\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.drag(5, 5, 15, 10)\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    // Should have down, several drag events, and up\n    expect(parsedEvents.length).toBeGreaterThan(3)\n    expect(parsedEvents[0].type).toBe(\"down\")\n    expect(parsedEvents[0].button).toBe(0)\n    expect(parsedEvents[0].x).toBe(5)\n    expect(parsedEvents[0].y).toBe(5)\n\n    // Check that intermediate events are drag events\n    for (let i = 1; i < parsedEvents.length - 1; i++) {\n      expect(parsedEvents[i].type).toBe(\"drag\")\n      expect(parsedEvents[i].button).toBe(0)\n    }\n\n    const lastEvent = parsedEvents[parsedEvents.length - 1]\n    expect(lastEvent.type).toBe(\"up\")\n    expect(lastEvent.x).toBe(15)\n    expect(lastEvent.y).toBe(10)\n  })\n\n  test(\"moveTo without button press generates move events\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.moveTo(15, 8)\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents).toHaveLength(1)\n    expect(parsedEvents[0]).toEqual({\n      type: \"move\",\n      button: 0,\n      x: 15,\n      y: 8,\n      modifiers: { shift: false, alt: false, ctrl: false },\n      scroll: undefined,\n    })\n  })\n\n  test(\"moveTo with button press generates drag events\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.pressDown(5, 5)\n    await mockMouse.moveTo(15, 8)\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents).toHaveLength(2)\n    expect(parsedEvents[0].type).toBe(\"down\")\n    expect(parsedEvents[1].type).toBe(\"drag\")\n    expect(parsedEvents[1].x).toBe(15)\n    expect(parsedEvents[1].y).toBe(8)\n  })\n\n  test(\"all modifier combinations work\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    const modifierCombos = [\n      { shift: true },\n      { alt: true },\n      { ctrl: true },\n      { shift: true, alt: true },\n      { shift: true, ctrl: true },\n      { alt: true, ctrl: true },\n      { shift: true, alt: true, ctrl: true },\n    ]\n\n    for (const modifiers of modifierCombos) {\n      const testRenderer = new MockRenderer()\n      const testMouse = createMockMouse(testRenderer as any)\n      const testParser = new MouseParser()\n\n      await testMouse.click(10, 5, MouseButtons.LEFT, { modifiers })\n      const parsedEvents = parseAllEvents(testRenderer.getEmittedData(), testParser)\n\n      expect(parsedEvents).toHaveLength(2)\n      parsedEvents.forEach((event) => {\n        expect(event.modifiers).toEqual({\n          ...modifiers,\n          shift: modifiers.shift || false,\n          alt: modifiers.alt || false,\n          ctrl: modifiers.ctrl || false,\n        })\n      })\n    }\n  })\n\n  test(\"drag with different button and modifiers\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n    const parser = new MouseParser()\n\n    await mockMouse.drag(5, 5, 15, 10, MouseButtons.RIGHT, { modifiers: { alt: true } })\n    const parsedEvents = parseAllEvents(mockRenderer.getEmittedData(), parser)\n\n    expect(parsedEvents.length).toBeGreaterThan(3)\n    parsedEvents.forEach((event) => {\n      expect(event.button).toBe(2) // RIGHT button\n      expect(event.modifiers.alt).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/testing/manual-clock.ts",
    "content": "import type { Clock, TimerHandle } from \"../lib/clock\"\n\ninterface ScheduledTimer {\n  id: number\n  fireAt: number\n  order: number\n  delayMs: number\n  repeat: boolean\n  fn: () => void\n}\n\nfunction compareTimers(left: ScheduledTimer, right: ScheduledTimer): number {\n  if (left.fireAt !== right.fireAt) {\n    return left.fireAt - right.fireAt\n  }\n\n  return left.order - right.order\n}\n\nexport class ManualClock implements Clock {\n  private time = 0\n  private nextId = 1\n  private nextOrder = 0\n  private readonly timers = new Map<number, ScheduledTimer>()\n\n  public now(): number {\n    return this.time\n  }\n\n  public setTime(time: number): void {\n    const targetTime = Math.floor(time)\n\n    if (targetTime >= this.time) {\n      this.advance(targetTime - this.time)\n      return\n    }\n\n    this.time = targetTime\n  }\n\n  public setTimeout(fn: () => void, delayMs: number): TimerHandle {\n    return this.schedule(fn, delayMs, false)\n  }\n\n  public clearTimeout(handle: TimerHandle): void {\n    this.timers.delete(Number(handle))\n  }\n\n  public setInterval(fn: () => void, delayMs: number): TimerHandle {\n    return this.schedule(fn, delayMs, true)\n  }\n\n  public clearInterval(handle: TimerHandle): void {\n    this.clearTimeout(handle)\n  }\n\n  public advance(delayMs: number): void {\n    const targetTime = this.time + Math.max(0, Math.floor(delayMs))\n\n    while (true) {\n      const nextTimer = this.peekNextTimer()\n      if (!nextTimer || nextTimer.fireAt > targetTime) {\n        break\n      }\n\n      this.timers.delete(nextTimer.id)\n      this.time = nextTimer.fireAt\n      nextTimer.fn()\n\n      if (nextTimer.repeat && !this.timers.has(nextTimer.id)) {\n        this.timers.set(nextTimer.id, {\n          ...nextTimer,\n          fireAt: this.time + nextTimer.delayMs,\n          order: this.nextOrder++,\n        })\n      }\n    }\n\n    this.time = targetTime\n  }\n\n  public runAll(): void {\n    while (true) {\n      const nextTimer = this.peekNextTimer()\n      if (!nextTimer) {\n        return\n      }\n\n      this.advance(nextTimer.fireAt - this.time)\n    }\n  }\n\n  private schedule(fn: () => void, delayMs: number, repeat: boolean): number {\n    const id = this.nextId++\n    const normalizedDelay = Math.max(0, Math.floor(delayMs))\n    this.timers.set(id, {\n      id,\n      fireAt: this.time + normalizedDelay,\n      order: this.nextOrder++,\n      delayMs: normalizedDelay,\n      repeat,\n      fn,\n    })\n    return id\n  }\n\n  private peekNextTimer(): ScheduledTimer | null {\n    let nextTimer: ScheduledTimer | null = null\n    for (const timer of this.timers.values()) {\n      if (!nextTimer || compareTimers(timer, nextTimer) < 0) {\n        nextTimer = timer\n      }\n    }\n\n    return nextTimer\n  }\n}\n"
  },
  {
    "path": "packages/core/src/testing/mock-keys.test.ts",
    "content": "import { describe, test, expect } from \"bun:test\"\nimport { createMockKeys, KeyCodes } from \"./mock-keys.js\"\nimport { PassThrough } from \"stream\"\n\nclass MockRenderer {\n  public stdin: PassThrough\n  public emittedData: Buffer[] = []\n\n  constructor() {\n    this.stdin = new PassThrough()\n\n    this.stdin.on(\"data\", (chunk: Buffer) => {\n      this.emittedData.push(chunk)\n    })\n  }\n\n  getEmittedData(): string {\n    return Buffer.concat(this.emittedData).toString()\n  }\n}\n\ndescribe(\"mock-keys\", () => {\n  test(\"pressKeys with string keys\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKeys([\"h\", \"e\", \"l\", \"l\", \"o\"])\n\n    expect(mockRenderer.getEmittedData()).toBe(\"hello\")\n  })\n\n  test(\"pressKeys with KeyCodes\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKeys([KeyCodes.RETURN, KeyCodes.TAB])\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\r\\t\")\n  })\n\n  test(\"pressKey with string\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"a\")\n\n    expect(mockRenderer.getEmittedData()).toBe(\"a\")\n  })\n\n  test(\"pressKey with KeyCode\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ESCAPE)\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b\")\n  })\n\n  test(\"typeText\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.typeText(\"hello world\")\n\n    expect(mockRenderer.getEmittedData()).toBe(\"hello world\")\n  })\n\n  test(\"convenience methods\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressEnter()\n    mockKeys.pressEscape()\n    mockKeys.pressTab()\n    mockKeys.pressBackspace()\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\r\\x1b\\t\\b\")\n  })\n\n  test(\"pressArrow\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressArrow(\"up\")\n    mockKeys.pressArrow(\"down\")\n    mockKeys.pressArrow(\"left\")\n    mockKeys.pressArrow(\"right\")\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[A\\x1b[B\\x1b[D\\x1b[C\")\n  })\n\n  test(\"pressCtrlC\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressCtrlC()\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x03\")\n  })\n\n  test(\"arbitrary string keys work\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"x\")\n    mockKeys.pressKey(\"y\")\n    mockKeys.pressKey(\"z\")\n\n    expect(mockRenderer.getEmittedData()).toBe(\"xyz\")\n  })\n\n  test(\"KeyCodes enum values work\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.RETURN)\n    mockKeys.pressKey(KeyCodes.TAB)\n    mockKeys.pressKey(KeyCodes.ESCAPE)\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\r\\t\\x1b\")\n  })\n\n  test(\"data events are properly emitted\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    const receivedData: Buffer[] = []\n    mockRenderer.stdin.on(\"data\", (chunk: Buffer) => {\n      receivedData.push(chunk)\n    })\n\n    mockKeys.pressKey(\"a\")\n    mockKeys.pressKey(KeyCodes.RETURN)\n\n    expect(receivedData).toHaveLength(2)\n    expect(receivedData[0].toString()).toBe(\"a\")\n    expect(receivedData[1].toString()).toBe(\"\\r\")\n  })\n\n  test(\"multiple data events accumulate correctly\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    const receivedData: string[] = []\n    mockRenderer.stdin.on(\"data\", (chunk: Buffer) => {\n      receivedData.push(chunk.toString())\n    })\n\n    mockKeys.typeText(\"hello\")\n    mockKeys.pressEnter()\n\n    expect(receivedData).toEqual([\"h\", \"e\", \"l\", \"l\", \"o\", \"\\r\"])\n  })\n\n  test(\"stream write method emits data events correctly\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    const emittedChunks: Buffer[] = []\n    mockRenderer.stdin.on(\"data\", (chunk: Buffer) => {\n      emittedChunks.push(chunk)\n    })\n\n    // Directly test the stream write method that mock-keys uses\n    mockRenderer.stdin.write(\"test\")\n    mockRenderer.stdin.write(KeyCodes.RETURN)\n\n    expect(emittedChunks).toHaveLength(2)\n    expect(emittedChunks[0].toString()).toBe(\"test\")\n    expect(emittedChunks[1].toString()).toBe(\"\\r\")\n  })\n\n  test(\"pressKeys with delay works\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    const timestamps: number[] = []\n    mockRenderer.stdin.on(\"data\", () => {\n      timestamps.push(Date.now())\n    })\n\n    const startTime = Date.now()\n    await mockKeys.pressKeys([\"a\", \"b\"], 10) // 10ms delay between keys\n    const totalElapsed = Date.now() - startTime\n\n    expect(timestamps).toHaveLength(2)\n    expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(8) // Allow some tolerance\n    expect(totalElapsed).toBeGreaterThanOrEqual(16)\n  })\n\n  test(\"pressKey with shift modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_RIGHT, { shift: true })\n\n    // Arrow right with shift: \\x1b[1;2C\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;2C\")\n  })\n\n  test(\"pressKey with ctrl modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_LEFT, { ctrl: true })\n\n    // Arrow left with ctrl: \\x1b[1;5D (1 base + 4 ctrl = 5)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;5D\")\n  })\n\n  test(\"pressKey with shift+ctrl modifiers\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_UP, { shift: true, ctrl: true })\n\n    // Arrow up with shift+ctrl: \\x1b[1;6A (1 base + 1 shift + 4 ctrl = 6)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;6A\")\n  })\n\n  test(\"pressKey with meta modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_DOWN, { meta: true })\n\n    // Arrow down with meta: \\x1b[1;3B (1 base + 2 meta = 3)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;3B\")\n  })\n\n  test(\"pressKey with super modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_UP, { super: true })\n\n    // Arrow up with super: \\x1b[1;9A (1 base + 8 super = 9)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;9A\")\n  })\n\n  test(\"pressKey with hyper modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_LEFT, { hyper: true })\n\n    // Arrow left with hyper: \\x1b[1;17D (1 base + 16 hyper = 17)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;17D\")\n  })\n\n  test(\"pressKey with super+hyper modifiers\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_RIGHT, { super: true, hyper: true })\n\n    // Arrow right with super+hyper: \\x1b[1;25C (1 base + 8 super + 16 hyper = 25)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;25C\")\n  })\n\n  test(\"pressArrow with shift modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressArrow(\"right\", { shift: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;2C\")\n  })\n\n  test(\"pressArrow without modifiers still works\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressArrow(\"left\")\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[D\")\n  })\n\n  test(\"pressKey with modifiers on HOME key\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.HOME, { shift: true })\n\n    // HOME with shift: \\x1b[1;2H\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;2H\")\n  })\n\n  test(\"pressKey with modifiers on END key\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.END, { shift: true })\n\n    // END with shift: \\x1b[1;2F\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;2F\")\n  })\n\n  test(\"pressKey with meta on regular character\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"a\", { meta: true })\n\n    // Meta+a: \\x1ba (escape + a)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1ba\")\n  })\n\n  test(\"pressKey with meta+shift on character\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"a\", { meta: true, shift: true })\n\n    // Meta+Shift+a: \\x1bA (escape + uppercase A)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1bA\")\n  })\n\n  test(\"pressKey with meta+ctrl on arrow\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_RIGHT, { meta: true, ctrl: true })\n\n    // Arrow right with meta+ctrl: \\x1b[1;7C (1 base + 2 meta + 4 ctrl = 7)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;7C\")\n  })\n\n  test(\"pressKey with meta+shift+ctrl on arrow\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(KeyCodes.ARROW_UP, { meta: true, shift: true, ctrl: true })\n\n    // Arrow up with all modifiers: \\x1b[1;8A (1 base + 1 shift + 2 meta + 4 ctrl = 8)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;8A\")\n  })\n\n  test(\"pressArrow with meta modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressArrow(\"left\", { meta: true })\n\n    // Arrow left with meta: \\x1b[1;3D\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;3D\")\n  })\n\n  test(\"pressArrow with meta+shift modifiers\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressArrow(\"down\", { meta: true, shift: true })\n\n    // Arrow down with meta+shift: \\x1b[1;4B (1 base + 1 shift + 2 meta = 4)\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;4B\")\n  })\n\n  test(\"meta modifier produces escape sequences\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"a\", { meta: true })\n    mockKeys.pressKey(\"z\", { meta: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1ba\\x1bz\")\n  })\n\n  test(\"pressEnter with modifiers\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressEnter({ meta: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b\\r\")\n  })\n\n  test(\"pressTab with shift modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressTab({ shift: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\t\")\n  })\n\n  test(\"pressEscape with ctrl modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressEscape({ ctrl: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b\")\n  })\n\n  test(\"pressBackspace with meta modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressBackspace({ meta: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b\\b\")\n  })\n\n  test(\"pressKey with ctrl on letter produces control code\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"a\", { ctrl: true })\n    mockKeys.pressKey(\"z\", { ctrl: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x01\\x1a\")\n  })\n\n  test(\"pressKey with ctrl on uppercase letter\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"A\", { ctrl: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x01\")\n  })\n\n  test(\"pressKey with ctrl+meta combination\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"a\", { ctrl: true, meta: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b\\x01\")\n  })\n\n  test(\"ctrl modifier produces control codes\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"a\", { ctrl: true })\n    mockKeys.pressKey(\"c\", { ctrl: true })\n    mockKeys.pressKey(\"d\", { ctrl: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x01\\x03\\x04\")\n  })\n\n  test(\"meta modifier produces escape sequences\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    mockKeys.pressKey(\"a\", { meta: true })\n    mockKeys.pressKey(\"z\", { meta: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1ba\\x1bz\")\n  })\n\n  test(\"all CTRL_* letters produce correct control codes\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    const letters = \"abcdefghijklmnopqrstuvwxyz\"\n    for (const letter of letters) {\n      mockKeys.pressKey(letter, { ctrl: true })\n    }\n\n    const expected = letters\n      .split(\"\")\n      .map((c) => String.fromCharCode(c.charCodeAt(0) - 96))\n      .join(\"\")\n    expect(mockRenderer.getEmittedData()).toBe(expected)\n  })\n\n  test(\"pressKey with ctrl modifier produces control code\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n    mockKeys.pressKey(\"c\", { ctrl: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x03\")\n  })\n\n  test(\"pressKey with meta modifier on letters produces escape sequences\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n    mockKeys.pressKey(\"x\", { meta: true })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1bx\")\n  })\n\n  test(\"pressKey with ctrl modifier on special characters\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    // Ctrl+- should produce \\u001f (ASCII 31, Unit Separator)\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\"-\", { ctrl: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\u001f\")\n\n    // Ctrl+. should produce \\u001e (ASCII 30, Record Separator)\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\".\", { ctrl: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\u001e\")\n\n    // Ctrl+, should produce \\u001c (ASCII 28, File Separator)\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\",\", { ctrl: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\u001c\")\n\n    // Ctrl+] should produce \\u001d (ASCII 29, Group Separator)\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\"]\", { ctrl: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\u001d\")\n\n    // Ctrl+[ should produce \\x1b (ASCII 27, Escape)\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\"[\", { ctrl: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b\")\n\n    // Ctrl+/ should produce \\u001f (ASCII 31, same as Ctrl+-)\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\"/\", { ctrl: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\u001f\")\n\n    // Ctrl+_ should also produce \\u001f (ASCII 31)\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\"_\", { ctrl: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\u001f\")\n  })\n\n  test(\"pressKey with ctrl modifier on all special control characters\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    // Test all standard control character mappings\n    const tests = [\n      { key: \"[\", expected: \"\\x1b\" }, // ESC\n      { key: \"\\\\\", expected: \"\\x1c\" }, // FS\n      { key: \"]\", expected: \"\\x1d\" }, // GS\n      { key: \"^\", expected: \"\\x1e\" }, // RS\n      { key: \"_\", expected: \"\\x1f\" }, // US\n      { key: \"?\", expected: \"\\x7f\" }, // DEL\n      { key: \"@\", expected: \"\\x00\" }, // NUL\n      { key: \" \", expected: \"\\x00\" }, // NUL (Ctrl+Space)\n    ]\n\n    for (const { key, expected } of tests) {\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(key, { ctrl: true })\n      expect(mockRenderer.getEmittedData()).toBe(expected)\n    }\n  })\n\n  test(\"pressKey with ctrl+meta on special characters\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    // Ctrl+Meta+- should produce ESC + \\u001f\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\"-\", { ctrl: true, meta: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b\\u001f\")\n\n    // Ctrl+Meta+] should produce ESC + \\u001d\n    mockRenderer.emittedData = []\n    mockKeys.pressKey(\"]\", { ctrl: true, meta: true })\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b\\u001d\")\n  })\n\n  test(\"pressKey with ctrl on special chars does NOT use kitty keyboard\", () => {\n    const mockRenderer = new MockRenderer()\n    // Explicitly use non-kitty mode\n    const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: false })\n\n    mockKeys.pressKey(\"-\", { ctrl: true })\n\n    // Should produce raw control sequence, NOT kitty CSI u sequence\n    const data = mockRenderer.getEmittedData()\n    expect(data).toBe(\"\\u001f\")\n    expect(data).not.toContain(\"[\") // Should not contain CSI\n    expect(data).not.toContain(\"u\") // Should not contain kitty 'u' ending\n  })\n\n  test(\"comprehensive test: all punctuation keys work with ctrl modifier\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    // Test multiple punctuation keys in sequence\n    mockKeys.pressKey(\"-\", { ctrl: true })\n    mockKeys.pressKey(\".\", { ctrl: true })\n    mockKeys.pressKey(\",\", { ctrl: true })\n    mockKeys.pressKey(\"]\", { ctrl: true })\n    mockKeys.pressKey(\"[\", { ctrl: true })\n\n    const expected = \"\\u001f\\u001e\\u001c\\u001d\\x1b\"\n    expect(mockRenderer.getEmittedData()).toBe(expected)\n  })\n\n  test(\"ctrl modifier with non-mapped characters preserves original\", () => {\n    const mockRenderer = new MockRenderer()\n    const mockKeys = createMockKeys(mockRenderer as any)\n\n    // Characters without specific ctrl mappings should be preserved\n    // (though in real terminals they might not do anything)\n    mockKeys.pressKey(\"(\", { ctrl: true })\n\n    // Should preserve the character since it's not in the mapping\n    expect(mockRenderer.getEmittedData()).toBe(\"(\")\n  })\n\n  describe(\"Kitty Keyboard Protocol Mode\", () => {\n    test(\"basic character in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"a\")\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97u\")\n    })\n\n    test(\"backspace without modifiers in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressBackspace()\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[127u\")\n    })\n\n    test(\"backspace with shift in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressBackspace({ shift: true })\n\n      // Kitty protocol: backspace(127) with shift modifier (1+1=2)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[127;2u\")\n    })\n\n    test(\"backspace with ctrl in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressBackspace({ ctrl: true })\n\n      // Kitty protocol: backspace(127) with ctrl modifier (4+1=5)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[127;5u\")\n    })\n\n    test(\"backspace with meta in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressBackspace({ meta: true })\n\n      // Kitty protocol: backspace(127) with meta modifier (2+1=3)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[127;3u\")\n    })\n\n    test(\"backspace with shift+meta in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressBackspace({ shift: true, meta: true })\n\n      // Kitty protocol: backspace(127) with shift+meta (1+2+1=4)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[127;4u\")\n    })\n\n    test(\"delete key in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"DELETE\")\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57349u\")\n    })\n\n    test(\"arrow keys in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressArrow(\"up\")\n      mockKeys.pressArrow(\"down\")\n      mockKeys.pressArrow(\"left\")\n      mockKeys.pressArrow(\"right\")\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57352u\\x1b[57353u\\x1b[57350u\\x1b[57351u\")\n    })\n\n    test(\"arrow key with shift in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressArrow(\"right\", { shift: true })\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57351;2u\")\n    })\n\n    test(\"arrow key with ctrl in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressArrow(\"left\", { ctrl: true })\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57350;5u\")\n    })\n\n    test(\"arrow key with meta in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressArrow(\"down\", { meta: true })\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57353;3u\")\n    })\n\n    test(\"arrow key with shift+ctrl+meta in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressArrow(\"up\", { shift: true, ctrl: true, meta: true })\n\n      // shift(1) + meta(2) + ctrl(4) = 7, plus 1 = 8\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57352;8u\")\n    })\n\n    test(\"enter/return in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressEnter()\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[13u\")\n    })\n\n    test(\"enter with meta in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressEnter({ meta: true })\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[13;3u\")\n    })\n\n    test(\"tab in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressTab()\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[9u\")\n    })\n\n    test(\"tab with shift in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressTab({ shift: true })\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[9;2u\")\n    })\n\n    test(\"escape in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressEscape()\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27u\")\n    })\n\n    test(\"home key in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"HOME\")\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57356u\")\n    })\n\n    test(\"home with shift in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"HOME\", { shift: true })\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57356;2u\")\n    })\n\n    test(\"end key in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"END\")\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57357u\")\n    })\n\n    test(\"function keys in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"F1\")\n      mockKeys.pressKey(\"F2\")\n      mockKeys.pressKey(\"F12\")\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[57364u\\x1b[57365u\\x1b[57375u\")\n    })\n\n    test(\"regular characters with shift in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"a\", { shift: true })\n\n      // 'a' (97) with shift modifier\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97;2u\")\n    })\n\n    test(\"regular characters with ctrl in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"c\", { ctrl: true })\n\n      // 'c' (99) with ctrl modifier (4+1=5)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[99;5u\")\n    })\n\n    test(\"regular characters with meta in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"x\", { meta: true })\n\n      // 'x' (120) with meta modifier (2+1=3)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[120;3u\")\n    })\n\n    test(\"multiple keys in sequence in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"h\")\n      mockKeys.pressKey(\"i\")\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[104u\\x1b[105u\")\n    })\n\n    test(\"mixed modifier combinations in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"a\")\n      mockKeys.pressKey(\"a\", { shift: true })\n      mockKeys.pressKey(\"a\", { ctrl: true })\n      mockKeys.pressKey(\"a\", { meta: true })\n      mockKeys.pressKey(\"a\", { shift: true, ctrl: true })\n\n      expect(mockRenderer.getEmittedData()).toBe(\n        \"\\x1b[97u\" + // no mods\n          \"\\x1b[97;2u\" + // shift\n          \"\\x1b[97;5u\" + // ctrl\n          \"\\x1b[97;3u\" + // meta\n          \"\\x1b[97;6u\", // shift+ctrl\n      )\n    })\n\n    test(\"character with super modifier in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"a\", { super: true })\n\n      // 'a' (97) with super modifier (8+1=9)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97;9u\")\n    })\n\n    test(\"character with hyper modifier in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"a\", { hyper: true })\n\n      // 'a' (97) with hyper modifier (16+1=17)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97;17u\")\n    })\n\n    test(\"character with super+hyper modifiers in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"a\", { super: true, hyper: true })\n\n      // 'a' (97) with super+hyper (8+16+1=25)\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97;25u\")\n    })\n\n    test(\"character with all modifiers in kitty mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"a\", { shift: true, ctrl: true, meta: true, super: true, hyper: true })\n\n      // 'a' (97) with all modifiers: shift(1) + meta(2) + ctrl(4) + super(8) + hyper(16) = 31, +1 = 32\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97;32u\")\n    })\n\n    test(\"kitty mode vs regular mode comparison\", () => {\n      const kittyRenderer = new MockRenderer()\n      const regularRenderer = new MockRenderer()\n      const kittyKeys = createMockKeys(kittyRenderer as any, { kittyKeyboard: true })\n      const regularKeys = createMockKeys(regularRenderer as any, { kittyKeyboard: false })\n\n      kittyKeys.pressBackspace({ shift: true })\n      regularKeys.pressBackspace({ shift: true })\n\n      // Kitty should send the protocol sequence with modifier\n      expect(kittyRenderer.getEmittedData()).toBe(\"\\x1b[127;2u\")\n      // Regular should just send backspace (shift is ignored)\n      expect(regularRenderer.getEmittedData()).toBe(\"\\b\")\n    })\n\n    test(\"special characters with ctrl in kitty mode\", () => {\n      const kittyRenderer = new MockRenderer()\n      const regularRenderer = new MockRenderer()\n      const kittyKeys = createMockKeys(kittyRenderer as any, { kittyKeyboard: true })\n      const regularKeys = createMockKeys(regularRenderer as any, { kittyKeyboard: false })\n\n      // Test Ctrl+- in both modes\n      kittyKeys.pressKey(\"-\", { ctrl: true })\n      regularKeys.pressKey(\"-\", { ctrl: true })\n\n      // Kitty should send the protocol sequence: '-' is codepoint 45, ctrl modifier is 4+1=5\n      expect(kittyRenderer.getEmittedData()).toBe(\"\\x1b[45;5u\")\n      // Regular should send raw control sequence \\u001f\n      expect(regularRenderer.getEmittedData()).toBe(\"\\u001f\")\n    })\n\n    test(\"various special characters with ctrl in kitty mode\", () => {\n      const kittyRenderer = new MockRenderer()\n      const kittyKeys = createMockKeys(kittyRenderer as any, { kittyKeyboard: true })\n\n      // Test multiple special characters\n      kittyRenderer.emittedData = []\n      kittyKeys.pressKey(\".\", { ctrl: true })\n      expect(kittyRenderer.getEmittedData()).toBe(\"\\x1b[46;5u\") // '.' is codepoint 46\n\n      kittyRenderer.emittedData = []\n      kittyKeys.pressKey(\",\", { ctrl: true })\n      expect(kittyRenderer.getEmittedData()).toBe(\"\\x1b[44;5u\") // ',' is codepoint 44\n\n      kittyRenderer.emittedData = []\n      kittyKeys.pressKey(\"]\", { ctrl: true })\n      expect(kittyRenderer.getEmittedData()).toBe(\"\\x1b[93;5u\") // ']' is codepoint 93\n    })\n  })\n\n  describe(\"modifyOtherKeys Mode (CSI u variant)\", () => {\n    test(\"modifyOtherKeys sequences can be parsed by parseKeypress\", async () => {\n      const { parseKeypress } = await import(\"../lib/parse.keypress\")\n\n      // Test that our generated sequences can be parsed correctly\n      const tests = [\n        { seq: \"\\x1b[27;5;97~\", expectedName: \"a\", expectedCtrl: true },\n        { seq: \"\\x1b[27;2;13~\", expectedName: \"return\", expectedShift: true },\n        { seq: \"\\x1b[27;5;27~\", expectedName: \"escape\", expectedCtrl: true },\n        { seq: \"\\x1b[27;2;9~\", expectedName: \"tab\", expectedShift: true },\n        { seq: \"\\x1b[27;5;32~\", expectedName: \"space\", expectedCtrl: true },\n        { seq: \"\\x1b[27;6;97~\", expectedName: \"a\", expectedShift: true, expectedCtrl: true },\n      ]\n\n      for (const test of tests) {\n        const result = parseKeypress(test.seq)\n        expect(result).not.toBeNull()\n        expect(result?.name).toBe(test.expectedName)\n        if (test.expectedCtrl !== undefined) {\n          expect(result?.ctrl).toBe(test.expectedCtrl)\n        }\n        if (test.expectedShift !== undefined) {\n          expect(result?.shift).toBe(test.expectedShift)\n        }\n      }\n    })\n\n    test(\"basic character without modifiers in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\")\n\n      // Without modifiers, should send plain character\n      expect(mockRenderer.getEmittedData()).toBe(\"a\")\n    })\n\n    test(\"character with ctrl in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\", { ctrl: true })\n\n      // modifyOtherKeys format: CSI 27 ; modifier ; code ~\n      // 'a' is charCode 97, ctrl is 4, modifier is 4+1=5\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;97~\")\n    })\n\n    test(\"character with shift in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\", { shift: true })\n\n      // 'a' is charCode 97, shift is 1, modifier is 1+1=2\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;2;97~\")\n    })\n\n    test(\"character with meta in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\", { meta: true })\n\n      // 'a' is charCode 97, meta is 2, modifier is 2+1=3\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;3;97~\")\n    })\n\n    test(\"return/enter with ctrl in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressEnter({ ctrl: true })\n\n      // return is charCode 13, ctrl is 4, modifier is 4+1=5\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;13~\")\n    })\n\n    test(\"return with shift in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressEnter({ shift: true })\n\n      // return is charCode 13, shift is 1, modifier is 1+1=2\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;2;13~\")\n    })\n\n    test(\"escape with ctrl in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressEscape({ ctrl: true })\n\n      // escape is charCode 27, ctrl is 4, modifier is 4+1=5\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;27~\")\n    })\n\n    test(\"tab with ctrl in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressTab({ ctrl: true })\n\n      // tab is charCode 9, ctrl is 4, modifier is 4+1=5\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;9~\")\n    })\n\n    test(\"tab with shift in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressTab({ shift: true })\n\n      // tab is charCode 9, shift is 1, modifier is 1+1=2\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;2;9~\")\n    })\n\n    test(\"backspace with ctrl in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressBackspace({ ctrl: true })\n\n      // backspace is charCode 127, ctrl is 4, modifier is 4+1=5\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;127~\")\n    })\n\n    test(\"backspace with meta in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressBackspace({ meta: true })\n\n      // backspace is charCode 127, meta is 2, modifier is 2+1=3\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;3;127~\")\n    })\n\n    test(\"space with ctrl in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\" \", { ctrl: true })\n\n      // space is charCode 32, ctrl is 4, modifier is 4+1=5\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;32~\")\n    })\n\n    test(\"special characters with ctrl in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      // Ctrl+- should use modifyOtherKeys format\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(\"-\", { ctrl: true })\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;45~\") // '-' is charCode 45\n\n      // Ctrl+. should use modifyOtherKeys format\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(\".\", { ctrl: true })\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;46~\") // '.' is charCode 46\n\n      // Ctrl+, should use modifyOtherKeys format\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(\",\", { ctrl: true })\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;44~\") // ',' is charCode 44\n\n      // Ctrl+] should use modifyOtherKeys format\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(\"]\", { ctrl: true })\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;93~\") // ']' is charCode 93\n    })\n\n    test(\"multiple modifier combinations in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      // shift + ctrl: 1 + 4 = 5, + 1 = 6\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(\"a\", { shift: true, ctrl: true })\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;6;97~\")\n\n      // shift + meta: 1 + 2 = 3, + 1 = 4\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(\"a\", { shift: true, meta: true })\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;4;97~\")\n\n      // ctrl + meta: 4 + 2 = 6, + 1 = 7\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(\"a\", { ctrl: true, meta: true })\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;7;97~\")\n\n      // shift + ctrl + meta: 1 + 4 + 2 = 7, + 1 = 8\n      mockRenderer.emittedData = []\n      mockKeys.pressKey(\"a\", { shift: true, ctrl: true, meta: true })\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;8;97~\")\n    })\n\n    test(\"character with super modifier in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\", { super: true })\n\n      // 'a' is charCode 97, super is 8, modifier is 8+1=9\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;9;97~\")\n    })\n\n    test(\"character with hyper modifier in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\", { hyper: true })\n\n      // 'a' is charCode 97, hyper is 16, modifier is 16+1=17\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;17;97~\")\n    })\n\n    test(\"character with super+hyper modifiers in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\", { super: true, hyper: true })\n\n      // super(8) + hyper(16) = 24, +1 = 25\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;25;97~\")\n    })\n\n    test(\"character with all modifiers in modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\", { shift: true, ctrl: true, meta: true, super: true, hyper: true })\n\n      // shift(1) + meta(2) + ctrl(4) + super(8) + hyper(16) = 31, +1 = 32\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;32;97~\")\n    })\n\n    test(\"arrow keys with modifiers fall through to regular mode in modifyOtherKeys\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      // Arrow keys should still use the standard CSI sequence with modifiers\n      // not the modifyOtherKeys format\n      mockKeys.pressArrow(\"right\", { shift: true })\n\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[1;2C\")\n    })\n\n    test(\"kitty mode takes precedence over modifyOtherKeys mode\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, {\n        kittyKeyboard: true,\n        otherModifiersMode: true,\n      })\n\n      mockKeys.pressKey(\"a\", { ctrl: true })\n\n      // Should use kitty format, not modifyOtherKeys\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97;5u\")\n    })\n\n    test(\"modifyOtherKeys vs regular mode comparison\", () => {\n      const modifyOtherKeysRenderer = new MockRenderer()\n      const regularRenderer = new MockRenderer()\n      const modifyOtherKeysKeys = createMockKeys(modifyOtherKeysRenderer as any, { otherModifiersMode: true })\n      const regularKeys = createMockKeys(regularRenderer as any, { otherModifiersMode: false })\n\n      modifyOtherKeysKeys.pressKey(\"-\", { ctrl: true })\n      regularKeys.pressKey(\"-\", { ctrl: true })\n\n      // modifyOtherKeys should send CSI 27 format\n      expect(modifyOtherKeysRenderer.getEmittedData()).toBe(\"\\x1b[27;5;45~\")\n      // Regular should send raw control sequence\n      expect(regularRenderer.getEmittedData()).toBe(\"\\u001f\")\n    })\n\n    test(\"characters without modifiers don't use modifyOtherKeys format\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      // Without modifiers, should send plain characters\n      mockKeys.pressKey(\"a\")\n      mockKeys.pressKey(\"b\")\n      mockKeys.pressEnter()\n\n      expect(mockRenderer.getEmittedData()).toBe(\"ab\\r\")\n    })\n\n    test(\"modifyOtherKeys with all printable characters\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      const chars = \"abcdefghijklmnopqrstuvwxyz0123456789-=[]\\\\;',./`\"\n\n      for (const char of chars) {\n        mockRenderer.emittedData = []\n        mockKeys.pressKey(char, { ctrl: true })\n        const charCode = char.charCodeAt(0)\n        expect(mockRenderer.getEmittedData()).toBe(`\\x1b[27;5;${charCode}~`)\n      }\n    })\n\n    test(\"modifyOtherKeys mode can be parsed back correctly\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      // Test various keys with modifiers\n      const tests = [\n        { key: \"a\", mods: { ctrl: true }, expectedSeq: \"\\x1b[27;5;97~\" },\n        { key: \"-\", mods: { ctrl: true }, expectedSeq: \"\\x1b[27;5;45~\" },\n        { key: KeyCodes.RETURN, mods: { shift: true }, expectedSeq: \"\\x1b[27;2;13~\" },\n        { key: KeyCodes.ESCAPE, mods: { ctrl: true }, expectedSeq: \"\\x1b[27;5;27~\" },\n        { key: KeyCodes.TAB, mods: { shift: true }, expectedSeq: \"\\x1b[27;2;9~\" },\n        { key: \" \", mods: { ctrl: true }, expectedSeq: \"\\x1b[27;5;32~\" },\n      ]\n\n      for (const { key, mods, expectedSeq } of tests) {\n        mockRenderer.emittedData = []\n        mockKeys.pressKey(key, mods)\n        expect(mockRenderer.getEmittedData()).toBe(expectedSeq)\n      }\n    })\n\n    test(\"comprehensive three-mode comparison: regular vs modifyOtherKeys vs kitty\", () => {\n      const regularRenderer = new MockRenderer()\n      const modifyOtherKeysRenderer = new MockRenderer()\n      const kittyRenderer = new MockRenderer()\n\n      const regularKeys = createMockKeys(regularRenderer as any, { kittyKeyboard: false, otherModifiersMode: false })\n      const modifyOtherKeysKeys = createMockKeys(modifyOtherKeysRenderer as any, { otherModifiersMode: true })\n      const kittyKeys = createMockKeys(kittyRenderer as any, { kittyKeyboard: true })\n\n      // Test Ctrl+- in all three modes\n      regularKeys.pressKey(\"-\", { ctrl: true })\n      modifyOtherKeysKeys.pressKey(\"-\", { ctrl: true })\n      kittyKeys.pressKey(\"-\", { ctrl: true })\n\n      expect(regularRenderer.getEmittedData()).toBe(\"\\u001f\") // Raw control sequence\n      expect(modifyOtherKeysRenderer.getEmittedData()).toBe(\"\\x1b[27;5;45~\") // modifyOtherKeys format\n      expect(kittyRenderer.getEmittedData()).toBe(\"\\x1b[45;5u\") // Kitty format\n\n      // Test Shift+Enter in all three modes\n      regularRenderer.emittedData = []\n      modifyOtherKeysRenderer.emittedData = []\n      kittyRenderer.emittedData = []\n\n      regularKeys.pressEnter({ shift: true })\n      modifyOtherKeysKeys.pressEnter({ shift: true })\n      kittyKeys.pressEnter({ shift: true })\n\n      expect(regularRenderer.getEmittedData()).toBe(\"\\r\") // Regular mode ignores shift on Enter\n      expect(modifyOtherKeysRenderer.getEmittedData()).toBe(\"\\x1b[27;2;13~\") // modifyOtherKeys format\n      expect(kittyRenderer.getEmittedData()).toBe(\"\\x1b[13;2u\") // Kitty format\n    })\n  })\n\n  describe(\"Mode selection and precedence\", () => {\n    test(\"default mode (no options)\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any)\n\n      mockKeys.pressKey(\"-\", { ctrl: true })\n\n      // Default should use raw control sequences\n      expect(mockRenderer.getEmittedData()).toBe(\"\\u001f\")\n    })\n\n    test(\"only kittyKeyboard enabled\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { kittyKeyboard: true })\n\n      mockKeys.pressKey(\"a\", { ctrl: true })\n\n      // Should use kitty format\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97;5u\")\n    })\n\n    test(\"only otherModifiersMode enabled\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, { otherModifiersMode: true })\n\n      mockKeys.pressKey(\"a\", { ctrl: true })\n\n      // Should use modifyOtherKeys format\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[27;5;97~\")\n    })\n\n    test(\"both kittyKeyboard and otherModifiersMode enabled (kitty wins)\", () => {\n      const mockRenderer = new MockRenderer()\n      const mockKeys = createMockKeys(mockRenderer as any, {\n        kittyKeyboard: true,\n        otherModifiersMode: true,\n      })\n\n      mockKeys.pressKey(\"a\", { ctrl: true })\n\n      // Kitty should take precedence\n      expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[97;5u\")\n      expect(mockRenderer.getEmittedData()).not.toContain(\"27;5;97~\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/testing/mock-keys.ts",
    "content": "import { Buffer } from \"node:buffer\"\nimport type { CliRenderer } from \"../renderer.js\"\nimport { ANSI } from \"../ansi.js\"\n\nexport function pasteBytes(text: string): Uint8Array {\n  return Uint8Array.from(Buffer.from(text))\n}\n\nexport const KeyCodes = {\n  // Control keys\n  RETURN: \"\\r\",\n  LINEFEED: \"\\n\",\n  TAB: \"\\t\",\n  BACKSPACE: \"\\b\",\n  // NOTE: This may depend on the platform and terminals\n  DELETE: \"\\x1b[3~\",\n  HOME: \"\\x1b[H\",\n  END: \"\\x1b[F\",\n  ESCAPE: \"\\x1b\",\n\n  // Arrow keys\n  ARROW_UP: \"\\x1b[A\",\n  ARROW_DOWN: \"\\x1b[B\",\n  ARROW_RIGHT: \"\\x1b[C\",\n  ARROW_LEFT: \"\\x1b[D\",\n\n  // Function keys\n  F1: \"\\x1bOP\",\n  F2: \"\\x1bOQ\",\n  F3: \"\\x1bOR\",\n  F4: \"\\x1bOS\",\n  F5: \"\\x1b[15~\",\n  F6: \"\\x1b[17~\",\n  F7: \"\\x1b[18~\",\n  F8: \"\\x1b[19~\",\n  F9: \"\\x1b[20~\",\n  F10: \"\\x1b[21~\",\n  F11: \"\\x1b[23~\",\n  F12: \"\\x1b[24~\",\n} as const\n\nexport type KeyInput = string | keyof typeof KeyCodes\n\nexport interface MockKeysOptions {\n  kittyKeyboard?: boolean\n  otherModifiersMode?: boolean\n}\n\n// Kitty keyboard protocol key mappings\nconst kittyKeyCodeMap: Record<string, number> = {\n  escape: 27,\n  tab: 9,\n  return: 13,\n  backspace: 127,\n  insert: 57348,\n  delete: 57349,\n  left: 57350,\n  right: 57351,\n  up: 57352,\n  down: 57353,\n  pageup: 57354,\n  pagedown: 57355,\n  home: 57356,\n  end: 57357,\n  f1: 57364,\n  f2: 57365,\n  f3: 57366,\n  f4: 57367,\n  f5: 57368,\n  f6: 57369,\n  f7: 57370,\n  f8: 57371,\n  f9: 57372,\n  f10: 57373,\n  f11: 57374,\n  f12: 57375,\n}\n\nfunction encodeKittySequence(\n  codepoint: number,\n  modifiers?: { shift?: boolean; ctrl?: boolean; meta?: boolean; super?: boolean; hyper?: boolean },\n): string {\n  // Kitty keyboard protocol: CSI unicode-key-code ; modifiers u\n  // Modifier encoding: shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, caps=64, num=128\n  let modMask = 0\n  if (modifiers?.shift) modMask |= 1\n  if (modifiers?.meta) modMask |= 2 // alt/meta\n  if (modifiers?.ctrl) modMask |= 4\n  if (modifiers?.super) modMask |= 8\n  if (modifiers?.hyper) modMask |= 16\n\n  if (modMask === 0) {\n    // No modifiers\n    return `\\x1b[${codepoint}u`\n  } else {\n    // With modifiers (kitty uses 1-based, so add 1)\n    return `\\x1b[${codepoint};${modMask + 1}u`\n  }\n}\n\nfunction encodeModifyOtherKeysSequence(\n  charCode: number,\n  modifiers?: { shift?: boolean; ctrl?: boolean; meta?: boolean; super?: boolean; hyper?: boolean },\n): string {\n  // modifyOtherKeys protocol: CSI 27 ; modifier ; code ~\n  // This is the format used by xterm, iTerm2, Ghostty with modifyOtherKeys enabled\n  // Modifier encoding: shift=1, alt/option=2, ctrl=4, super=8, hyper=16 (1-based, so add 1)\n  let modMask = 0\n  if (modifiers?.shift) modMask |= 1\n  if (modifiers?.meta) modMask |= 2 // alt/option/meta\n  if (modifiers?.ctrl) modMask |= 4\n  if (modifiers?.super) modMask |= 8\n  if (modifiers?.hyper) modMask |= 16\n\n  // modifyOtherKeys is only used when modifiers are present\n  // Without modifiers, use the standard key sequence\n  if (modMask === 0) {\n    return String.fromCharCode(charCode)\n  }\n\n  // With modifiers, use CSI 27 ; modifier ; code ~\n  return `\\x1b[27;${modMask + 1};${charCode}~`\n}\n\ninterface ResolvedKey {\n  keyValue: string\n  keyName: string | undefined\n}\n\nfunction resolveKeyInput(key: KeyInput): ResolvedKey {\n  let keyValue: string\n  let keyName: string | undefined\n\n  if (typeof key === \"string\") {\n    if (key in KeyCodes) {\n      // It's a KeyCode name like \"BACKSPACE\", \"ARROW_UP\", etc.\n      keyValue = KeyCodes[key as keyof typeof KeyCodes]\n      keyName = key.toLowerCase()\n    } else {\n      // It's a regular character\n      keyValue = key\n      keyName = undefined\n    }\n  } else {\n    // It's already a keycode enum value\n    keyValue = KeyCodes[key]\n    if (!keyValue) {\n      throw new Error(`Unknown key: ${key}`)\n    }\n    keyName = String(key).toLowerCase()\n  }\n\n  return { keyValue, keyName }\n}\n\nexport function createMockKeys(renderer: CliRenderer, options?: MockKeysOptions) {\n  const useKittyKeyboard = options?.kittyKeyboard ?? false\n  const useOtherModifiersMode = options?.otherModifiersMode ?? false\n\n  // Kitty keyboard takes precedence over otherModifiersMode\n  const effectiveOtherModifiersMode = useOtherModifiersMode && !useKittyKeyboard\n\n  const pressKeys = async (keys: KeyInput[], delayMs: number = 0): Promise<void> => {\n    for (const key of keys) {\n      const { keyValue: keyCode } = resolveKeyInput(key)\n\n      renderer.stdin.emit(\"data\", Buffer.from(keyCode))\n\n      if (delayMs > 0) {\n        await new Promise((resolve) => setTimeout(resolve, delayMs))\n      }\n    }\n  }\n\n  const pressKey = (\n    key: KeyInput,\n    modifiers?: { shift?: boolean; ctrl?: boolean; meta?: boolean; super?: boolean; hyper?: boolean },\n  ): void => {\n    // Handle Kitty keyboard protocol mode\n    if (useKittyKeyboard) {\n      // Resolve the key to its string representation or keycode value\n      let { keyValue, keyName } = resolveKeyInput(key)\n\n      // Map control characters and escape sequences to their kitty key names\n      const valueToKeyNameMap: Record<string, string> = {\n        \"\\b\": \"backspace\",\n        \"\\r\": \"return\",\n        \"\\n\": \"return\",\n        \"\\t\": \"tab\",\n        \"\\x1b\": \"escape\",\n        \"\\x1b[A\": \"up\",\n        \"\\x1b[B\": \"down\",\n        \"\\x1b[C\": \"right\",\n        \"\\x1b[D\": \"left\",\n        \"\\x1b[H\": \"home\",\n        \"\\x1b[F\": \"end\",\n        \"\\x1b[3~\": \"delete\",\n      }\n\n      // Check value mapping\n      if (keyValue && valueToKeyNameMap[keyValue]) {\n        keyName = valueToKeyNameMap[keyValue]\n      }\n\n      // Also check for ARROW_ prefix\n      if (keyName && keyName.startsWith(\"arrow_\")) {\n        keyName = keyName.substring(6) // Remove \"arrow_\" prefix\n      }\n\n      // Check if we have a direct kitty code mapping\n      if (keyName && kittyKeyCodeMap[keyName]) {\n        const kittyCode = kittyKeyCodeMap[keyName]\n        const sequence = encodeKittySequence(kittyCode, modifiers)\n        renderer.stdin.emit(\"data\", Buffer.from(sequence))\n        return\n      }\n\n      // For regular characters, get the codepoint\n      if (keyValue && keyValue.length === 1 && !keyValue.startsWith(\"\\x1b\")) {\n        const codepoint = keyValue.codePointAt(0)\n        if (codepoint) {\n          const sequence = encodeKittySequence(codepoint, modifiers)\n          renderer.stdin.emit(\"data\", Buffer.from(sequence))\n          return\n        }\n      }\n\n      // Fall through to regular mode for unknown keys\n    }\n\n    // Handle modifyOtherKeys mode (CSI u protocol variant)\n    // Used by xterm, iTerm2, Ghostty with modifyOtherKeys enabled\n    if (effectiveOtherModifiersMode && modifiers) {\n      // Resolve the key to its string representation or keycode value\n      let { keyValue, keyName } = resolveKeyInput(key)\n\n      // Map control characters and escape sequences to their char codes\n      const valueToCharCodeMap: Record<string, number> = {\n        \"\\b\": 127, // backspace (or 8, but 127 is more common)\n        \"\\r\": 13, // return\n        \"\\n\": 13, // linefeed -> return\n        \"\\t\": 9, // tab\n        \"\\x1b\": 27, // escape\n        \" \": 32, // space\n      }\n\n      // Check if we have a control character that needs modifyOtherKeys encoding\n      let charCode: number | undefined\n\n      if (keyValue && valueToCharCodeMap[keyValue] !== undefined) {\n        charCode = valueToCharCodeMap[keyValue]\n      } else if (keyValue && keyValue.length === 1 && !keyValue.startsWith(\"\\x1b\")) {\n        // For regular single characters\n        charCode = keyValue.charCodeAt(0)\n      }\n\n      // If we have a char code and modifiers, use modifyOtherKeys format\n      if (charCode !== undefined) {\n        const sequence = encodeModifyOtherKeysSequence(charCode, modifiers)\n        renderer.stdin.emit(\"data\", Buffer.from(sequence))\n        return\n      }\n\n      // For other keys (like arrow keys with modifiers), fall through to regular mode\n    }\n\n    // Regular (non-Kitty, non-modifyOtherKeys) mode\n    let keyCode = resolveKeyInput(key).keyValue\n\n    // Apply modifiers if present\n    if (modifiers) {\n      // For arrow keys and special keys, modify the escape sequence\n      if (keyCode.startsWith(\"\\x1b[\") && keyCode.length > 2) {\n        // Arrow keys: \\x1b[A, \\x1b[B, \\x1b[C, \\x1b[D\n        // With shift modifier: \\x1b[1;2A, \\x1b[1;2B, \\x1b[1;2C, \\x1b[1;2D\n        // Special keys like delete: \\x1b[3~ becomes \\x1b[3;2~ with meta\n        const modifier =\n          1 +\n          (modifiers.shift ? 1 : 0) +\n          (modifiers.meta ? 2 : 0) +\n          (modifiers.ctrl ? 4 : 0) +\n          (modifiers.super ? 8 : 0) +\n          (modifiers.hyper ? 16 : 0)\n        if (modifier > 1) {\n          // Check if it's a sequence like \\x1b[3~ (delete, insert, pageup, etc.)\n          const tildeMatch = keyCode.match(/^\\x1b\\[(\\d+)~$/)\n          if (tildeMatch) {\n            // Format: \\x1b[number;modifier~\n            keyCode = `\\x1b[${tildeMatch[1]};${modifier}~`\n          } else {\n            // Arrow keys and other single-letter endings\n            // Insert modifier into sequence\n            const ending = keyCode.slice(-1)\n            keyCode = `\\x1b[1;${modifier}${ending}`\n          }\n        }\n      } else if (keyCode.length === 1) {\n        // For regular characters and single-char control codes with modifiers\n        let char = keyCode\n\n        // Special handling for backspace with modifiers - use modifyOtherKeys format\n        // Terminals send Ctrl+Backspace as CSI 27;5;127~ (or CSI 27;5;8~)\n        // Only use modifyOtherKeys for ctrl, super, or hyper (not shift or meta alone)\n        if (char === \"\\b\" && (modifiers.ctrl || modifiers.super || modifiers.hyper)) {\n          const modifier =\n            1 +\n            (modifiers.shift ? 1 : 0) +\n            (modifiers.meta ? 2 : 0) +\n            (modifiers.ctrl ? 4 : 0) +\n            (modifiers.super ? 8 : 0) +\n            (modifiers.hyper ? 16 : 0)\n          // Use charcode 127 for backspace (DEL)\n          keyCode = `\\x1b[27;${modifier};127~`\n        } else if (modifiers.ctrl) {\n          // Handle ctrl modifier for characters\n          // Ctrl+letter produces control codes (0x01-0x1a for a-z)\n          if (char >= \"a\" && char <= \"z\") {\n            keyCode = String.fromCharCode(char.charCodeAt(0) - 96)\n          } else if (char >= \"A\" && char <= \"Z\") {\n            keyCode = String.fromCharCode(char.charCodeAt(0) - 64)\n          } else {\n            // Handle special characters with ctrl modifier\n            // These produce ASCII control codes\n            const specialCtrlMap: Record<string, string> = {\n              \"[\": \"\\x1b\", // Ctrl+[ = ESC (ASCII 27)\n              \"\\\\\": \"\\x1c\", // Ctrl+\\ = FS (ASCII 28)\n              \"]\": \"\\x1d\", // Ctrl+] = GS (ASCII 29)\n              \"^\": \"\\x1e\", // Ctrl+^ = RS (ASCII 30)\n              _: \"\\x1f\", // Ctrl+_ = US (ASCII 31)\n              \"?\": \"\\x7f\", // Ctrl+? = DEL (ASCII 127)\n              // Common aliases\n              \"/\": \"\\x1f\", // Ctrl+/ = US (ASCII 31, same as Ctrl+_)\n              \"-\": \"\\x1f\", // Ctrl+- = US (ASCII 31, same as Ctrl+_)\n              \".\": \"\\x1e\", // Ctrl+. = RS (ASCII 30, same as Ctrl+^)\n              \",\": \"\\x1c\", // Ctrl+, = FS (ASCII 28, same as Ctrl+\\)\n              \"@\": \"\\x00\", // Ctrl+@ = NUL (ASCII 0)\n              \" \": \"\\x00\", // Ctrl+Space = NUL (ASCII 0)\n            }\n\n            if (char in specialCtrlMap) {\n              keyCode = specialCtrlMap[char]\n            }\n            // If no mapping found, keep the original character\n          }\n          // If meta is also pressed, prefix with escape\n          if (modifiers.meta) {\n            keyCode = `\\x1b${keyCode}`\n          }\n        } else {\n          // Handle shift+meta or just meta\n          if (modifiers.shift && char >= \"a\" && char <= \"z\") {\n            char = char.toUpperCase()\n          }\n          if (modifiers.meta) {\n            // For meta+character (including control codes), prefix with escape\n            keyCode = `\\x1b${char}`\n          } else {\n            keyCode = char\n          }\n        }\n      } else if (modifiers.meta && !keyCode.startsWith(\"\\x1b\")) {\n        // For multi-char sequences that aren't escape sequences (like simple control codes)\n        // just prefix with escape for meta\n        keyCode = `\\x1b${keyCode}`\n      }\n    }\n\n    renderer.stdin.emit(\"data\", Buffer.from(keyCode))\n  }\n\n  const typeText = async (text: string, delayMs: number = 0): Promise<void> => {\n    const keys = text.split(\"\")\n    await pressKeys(keys, delayMs)\n  }\n\n  const pressReturn = (modifiers?: {\n    shift?: boolean\n    ctrl?: boolean\n    meta?: boolean\n    super?: boolean\n    hyper?: boolean\n  }): void => {\n    pressKey(KeyCodes.RETURN, modifiers)\n  }\n\n  const pressEscape = (modifiers?: {\n    shift?: boolean\n    ctrl?: boolean\n    meta?: boolean\n    super?: boolean\n    hyper?: boolean\n  }): void => {\n    pressKey(KeyCodes.ESCAPE, modifiers)\n  }\n\n  const pressTab = (modifiers?: {\n    shift?: boolean\n    ctrl?: boolean\n    meta?: boolean\n    super?: boolean\n    hyper?: boolean\n  }): void => {\n    pressKey(KeyCodes.TAB, modifiers)\n  }\n\n  const pressBackspace = (modifiers?: {\n    shift?: boolean\n    ctrl?: boolean\n    meta?: boolean\n    super?: boolean\n    hyper?: boolean\n  }): void => {\n    pressKey(KeyCodes.BACKSPACE, modifiers)\n  }\n\n  const pressArrow = (\n    direction: \"up\" | \"down\" | \"left\" | \"right\",\n    modifiers?: { shift?: boolean; ctrl?: boolean; meta?: boolean; super?: boolean; hyper?: boolean },\n  ): void => {\n    const keyMap = {\n      up: KeyCodes.ARROW_UP,\n      down: KeyCodes.ARROW_DOWN,\n      left: KeyCodes.ARROW_LEFT,\n      right: KeyCodes.ARROW_RIGHT,\n    }\n    pressKey(keyMap[direction], modifiers)\n  }\n\n  const pressCtrlC = (): void => {\n    pressKey(\"c\", { ctrl: true })\n  }\n\n  const pasteBracketedText = (text: string): Promise<void> => {\n    return pressKeys([ANSI.bracketedPasteStart, text, ANSI.bracketedPasteEnd])\n  }\n\n  return {\n    pressKeys,\n    pressKey,\n    typeText,\n    pressEnter: pressReturn,\n    pressEscape,\n    pressTab,\n    pressBackspace,\n    pressArrow,\n    pressCtrlC,\n    pasteBracketedText,\n  }\n}\n"
  },
  {
    "path": "packages/core/src/testing/mock-mouse.test.ts",
    "content": "import { describe, test, expect } from \"bun:test\"\nimport { createMockMouse, MouseButtons } from \"./mock-mouse.js\"\nimport { PassThrough } from \"stream\"\n\nclass MockRenderer {\n  public stdin: PassThrough\n  public emittedData: Buffer[] = []\n\n  constructor() {\n    this.stdin = new PassThrough()\n\n    this.stdin.on(\"data\", (chunk: Buffer) => {\n      this.emittedData.push(chunk)\n    })\n  }\n\n  getEmittedData(): string {\n    return Buffer.concat(this.emittedData).toString()\n  }\n\n  getLastEmittedData(): string {\n    return this.emittedData.length > 0 ? this.emittedData[this.emittedData.length - 1].toString() : \"\"\n  }\n}\n\ndescribe(\"mock-mouse\", () => {\n  test(\"click generates correct mouse events\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.click(10, 5)\n\n    expect(mockRenderer.emittedData).toHaveLength(2)\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<0;11;6M\") // down event\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<0;11;6m\") // up event\n  })\n\n  test(\"click with different button\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.click(10, 5, MouseButtons.RIGHT)\n\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<2;11;6M\") // right button down\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<2;11;6m\") // right button up\n  })\n\n  test(\"click with modifiers\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.click(10, 5, MouseButtons.LEFT, { modifiers: { ctrl: true, shift: true } })\n\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<20;11;6M\") // 0 + 16 (ctrl) + 4 (shift) = 20\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<20;11;6m\")\n  })\n\n  test(\"moveTo generates move event\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.moveTo(15, 8)\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[<35;16;9m\") // 32 (motion) + 3 (button 3 for move) = 35\n  })\n\n  test(\"moveTo with modifiers\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.moveTo(15, 8, { modifiers: { alt: true } })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[<43;16;9m\") // 32 + 3 + 8 (alt) = 43\n  })\n\n  test(\"doubleClick generates four events\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.doubleClick(10, 5)\n\n    expect(mockRenderer.emittedData).toHaveLength(4)\n    // Two down events and two up events\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<0;11;6M\")\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<0;11;6m\")\n    expect(mockRenderer.emittedData[2].toString()).toBe(\"\\x1b[<0;11;6M\")\n    expect(mockRenderer.emittedData[3].toString()).toBe(\"\\x1b[<0;11;6m\")\n  })\n\n  test(\"pressDown and release work separately\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.pressDown(10, 5, MouseButtons.MIDDLE)\n    await mockMouse.release(10, 5, MouseButtons.MIDDLE)\n\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<1;11;6M\") // middle button down\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<1;11;6m\") // middle button up\n  })\n\n  test(\"drag generates drag events\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.drag(10, 5, 20, 10)\n\n    // Should have: down, several drag events, up\n    expect(mockRenderer.emittedData.length).toBeGreaterThan(3)\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<0;11;6M\") // initial down\n\n    // Check that drag events have the motion flag (32)\n    for (let i = 1; i < mockRenderer.emittedData.length - 1; i++) {\n      const event = mockRenderer.emittedData[i].toString()\n      expect(event).toMatch(/\\x1b\\[<32;\\d+;\\d+m/) // Should have motion flag (32) and release (m)\n    }\n\n    const lastEvent = mockRenderer.emittedData[mockRenderer.emittedData.length - 1].toString()\n    expect(lastEvent).toBe(\"\\x1b[<0;21;11m\") // final up\n  })\n\n  test(\"scroll events work\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.scroll(10, 5, \"up\")\n    await mockMouse.scroll(10, 5, \"down\")\n\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<64;11;6M\") // wheel up (64 = scroll flag)\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<65;11;6M\") // wheel down (64 + 1)\n  })\n\n  test(\"scroll with modifiers\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.scroll(10, 5, \"left\", { modifiers: { shift: true } })\n\n    expect(mockRenderer.getEmittedData()).toBe(\"\\x1b[<70;11;6M\") // 66 (wheel left) + 4 (shift) = 70\n  })\n\n  test(\"moveTo becomes drag when button is pressed\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.pressDown(5, 5)\n    await mockMouse.moveTo(15, 8)\n\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<0;6;6M\") // down\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<32;16;9m\") // drag (32 = motion flag, no button 3)\n  })\n\n  test(\"getCurrentPosition tracks position\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    expect(mockMouse.getCurrentPosition()).toEqual({ x: 0, y: 0 })\n\n    await mockMouse.moveTo(10, 5)\n    expect(mockMouse.getCurrentPosition()).toEqual({ x: 10, y: 5 })\n\n    await mockMouse.click(15, 8)\n    expect(mockMouse.getCurrentPosition()).toEqual({ x: 15, y: 8 })\n  })\n\n  test(\"getPressedButtons tracks button state\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    expect(mockMouse.getPressedButtons()).toEqual([])\n\n    await mockMouse.pressDown(10, 5, MouseButtons.LEFT)\n    expect(mockMouse.getPressedButtons()).toEqual([MouseButtons.LEFT])\n\n    await mockMouse.pressDown(10, 5, MouseButtons.RIGHT)\n    expect(mockMouse.getPressedButtons()).toEqual([MouseButtons.LEFT, MouseButtons.RIGHT])\n\n    await mockMouse.release(10, 5, MouseButtons.LEFT)\n    expect(mockMouse.getPressedButtons()).toEqual([MouseButtons.RIGHT])\n\n    await mockMouse.release(10, 5, MouseButtons.RIGHT)\n    expect(mockMouse.getPressedButtons()).toEqual([])\n  })\n\n  test(\"delay works correctly\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    const startTime = Date.now()\n    await mockMouse.click(10, 5, MouseButtons.LEFT, { delayMs: 20 })\n\n    expect(Date.now() - startTime).toBeGreaterThanOrEqual(15) // Allow some tolerance\n  })\n\n  test(\"coordinates are 1-based in ANSI output\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.click(0, 0) // 0-based coordinates\n\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<0;1;1M\") // 1-based in ANSI\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<0;1;1m\")\n  })\n\n  test(\"all scroll directions work\", async () => {\n    const mockRenderer = new MockRenderer()\n    const mockMouse = createMockMouse(mockRenderer as any)\n\n    await mockMouse.scroll(10, 5, \"up\")\n    await mockMouse.scroll(10, 5, \"down\")\n    await mockMouse.scroll(10, 5, \"left\")\n    await mockMouse.scroll(10, 5, \"right\")\n\n    expect(mockRenderer.emittedData[0].toString()).toBe(\"\\x1b[<64;11;6M\") // up\n    expect(mockRenderer.emittedData[1].toString()).toBe(\"\\x1b[<65;11;6M\") // down\n    expect(mockRenderer.emittedData[2].toString()).toBe(\"\\x1b[<66;11;6M\") // left\n    expect(mockRenderer.emittedData[3].toString()).toBe(\"\\x1b[<67;11;6M\") // right\n  })\n})\n"
  },
  {
    "path": "packages/core/src/testing/mock-mouse.ts",
    "content": "import type { CliRenderer } from \"../renderer.js\"\n\nexport const MouseButtons = {\n  LEFT: 0,\n  MIDDLE: 1,\n  RIGHT: 2,\n\n  WHEEL_UP: 64, // 64 = scroll flag + 0\n  WHEEL_DOWN: 65, // 64 + 1\n  WHEEL_LEFT: 66, // 64 + 2\n  WHEEL_RIGHT: 67, // 64 + 3\n} as const\n\nexport type MouseButton = (typeof MouseButtons)[keyof typeof MouseButtons]\n\nexport interface MousePosition {\n  x: number\n  y: number\n}\n\nexport interface MouseModifiers {\n  shift?: boolean\n  alt?: boolean\n  ctrl?: boolean\n}\n\nexport type MouseEventType = \"down\" | \"up\" | \"move\" | \"drag\" | \"scroll\"\n\nexport interface MouseEventOptions {\n  button?: MouseButton\n  modifiers?: MouseModifiers\n  delayMs?: number\n}\n\nexport function createMockMouse(renderer: CliRenderer) {\n  let currentPosition: MousePosition = { x: 0, y: 0 }\n  let buttonsPressed = new Set<MouseButton>()\n\n  // Generate SGR mouse event sequence\n  const generateMouseEvent = (\n    type: MouseEventType,\n    x: number,\n    y: number,\n    button: MouseButton = MouseButtons.LEFT,\n    modifiers: MouseModifiers = {},\n  ): string => {\n    // SGR format: \\x1b[<b;x;yM or \\x1b[<b;x;ym\n    // where b = button code + modifier flags + motion/scroll flags\n\n    let buttonCode: number = button\n\n    // Add modifier flags\n    if (modifiers.shift) buttonCode |= 4\n    if (modifiers.alt) buttonCode |= 8\n    if (modifiers.ctrl) buttonCode |= 16\n\n    switch (type) {\n      case \"move\":\n        buttonCode = 32 | 3 // motion flag (32) + button 3 for motion without button press\n        if (modifiers.shift) buttonCode |= 4\n        if (modifiers.alt) buttonCode |= 8\n        if (modifiers.ctrl) buttonCode |= 16\n        break\n      case \"drag\":\n        buttonCode = (buttonsPressed.size > 0 ? Array.from(buttonsPressed)[0] : button) | 32\n        if (modifiers.shift) buttonCode |= 4\n        if (modifiers.alt) buttonCode |= 8\n        if (modifiers.ctrl) buttonCode |= 16\n        break\n      case \"scroll\":\n        // Scroll events already have the scroll flag set in the button code\n        break\n    }\n\n    // Convert to 1-based coordinates for ANSI\n    const ansiX = x + 1\n    const ansiY = y + 1\n\n    let pressRelease = \"M\" // Default to press\n    if (type === \"up\" || type === \"move\" || type === \"drag\") {\n      pressRelease = \"m\"\n    }\n\n    return `\\x1b[<${buttonCode};${ansiX};${ansiY}${pressRelease}`\n  }\n\n  const emitMouseEvent = async (\n    type: MouseEventType,\n    x: number,\n    y: number,\n    button: MouseButton = MouseButtons.LEFT,\n    options: Omit<MouseEventOptions, \"button\"> = {},\n  ): Promise<void> => {\n    const { modifiers = {}, delayMs = 0 } = options\n\n    const eventSequence = generateMouseEvent(type, x, y, button, modifiers)\n    renderer.stdin.emit(\"data\", Buffer.from(eventSequence))\n\n    currentPosition = { x, y }\n\n    if (type === \"down\" && button < 64) {\n      buttonsPressed.add(button)\n    } else if (type === \"up\") {\n      buttonsPressed.delete(button)\n    }\n\n    if (delayMs > 0) {\n      await new Promise((resolve) => setTimeout(resolve, delayMs))\n    }\n  }\n\n  const moveTo = async (x: number, y: number, options: MouseEventOptions = {}): Promise<void> => {\n    const { button = MouseButtons.LEFT, delayMs = 0, modifiers = {} } = options\n\n    if (buttonsPressed.size > 0) {\n      await emitMouseEvent(\"drag\", x, y, Array.from(buttonsPressed)[0], { modifiers, delayMs })\n    } else {\n      await emitMouseEvent(\"move\", x, y, button, { modifiers, delayMs })\n    }\n\n    currentPosition = { x, y }\n  }\n\n  const click = async (\n    x: number,\n    y: number,\n    button: MouseButton = MouseButtons.LEFT,\n    options: MouseEventOptions = {},\n  ): Promise<void> => {\n    const { delayMs = 10, modifiers = {} } = options\n\n    await emitMouseEvent(\"down\", x, y, button, { modifiers, delayMs })\n    await new Promise((resolve) => setTimeout(resolve, delayMs))\n    await emitMouseEvent(\"up\", x, y, button, { modifiers, delayMs })\n  }\n\n  const doubleClick = async (\n    x: number,\n    y: number,\n    button: MouseButton = MouseButtons.LEFT,\n    options: MouseEventOptions = {},\n  ): Promise<void> => {\n    const { delayMs = 10, modifiers = {} } = options\n\n    await click(x, y, button, { modifiers, delayMs })\n    await new Promise((resolve) => setTimeout(resolve, delayMs))\n    await click(x, y, button, { modifiers, delayMs })\n  }\n\n  const pressDown = async (\n    x: number,\n    y: number,\n    button: MouseButton = MouseButtons.LEFT,\n    options: MouseEventOptions = {},\n  ): Promise<void> => {\n    const { modifiers = {}, delayMs = 0 } = options\n    await emitMouseEvent(\"down\", x, y, button, { modifiers, delayMs })\n  }\n\n  const release = async (\n    x: number,\n    y: number,\n    button: MouseButton = MouseButtons.LEFT,\n    options: MouseEventOptions = {},\n  ): Promise<void> => {\n    const { modifiers = {}, delayMs = 0 } = options\n    await emitMouseEvent(\"up\", x, y, button, { modifiers, delayMs })\n  }\n\n  const drag = async (\n    startX: number,\n    startY: number,\n    endX: number,\n    endY: number,\n    button: MouseButton = MouseButtons.LEFT,\n    options: MouseEventOptions = {},\n  ): Promise<void> => {\n    const { delayMs = 10, modifiers = {} } = options\n\n    await pressDown(startX, startY, button, { modifiers })\n\n    const steps = 5\n    const dx = (endX - startX) / steps\n    const dy = (endY - startY) / steps\n\n    for (let i = 1; i <= steps; i++) {\n      const currentX = Math.round(startX + dx * i)\n      const currentY = Math.round(startY + dy * i)\n      await emitMouseEvent(\"drag\", currentX, currentY, button, { modifiers, delayMs })\n    }\n\n    await release(endX, endY, button, { modifiers })\n  }\n\n  const scroll = async (\n    x: number,\n    y: number,\n    direction: \"up\" | \"down\" | \"left\" | \"right\",\n    options: MouseEventOptions = {},\n  ): Promise<void> => {\n    const { modifiers = {}, delayMs = 0 } = options\n\n    let button: MouseButton\n    switch (direction) {\n      case \"up\":\n        button = MouseButtons.WHEEL_UP\n        break\n      case \"down\":\n        button = MouseButtons.WHEEL_DOWN\n        break\n      case \"left\":\n        button = MouseButtons.WHEEL_LEFT\n        break\n      case \"right\":\n        button = MouseButtons.WHEEL_RIGHT\n        break\n    }\n\n    await emitMouseEvent(\"scroll\", x, y, button, { modifiers, delayMs })\n  }\n\n  const getCurrentPosition = (): MousePosition => {\n    return { ...currentPosition }\n  }\n\n  const getPressedButtons = (): MouseButton[] => {\n    return Array.from(buttonsPressed)\n  }\n\n  return {\n    // Core interaction methods\n    moveTo,\n    click,\n    doubleClick,\n    pressDown,\n    release,\n    drag,\n    scroll,\n\n    // State getters\n    getCurrentPosition,\n    getPressedButtons,\n\n    // Low-level event emission (for advanced use cases)\n    emitMouseEvent,\n  }\n}\n"
  },
  {
    "path": "packages/core/src/testing/mock-tree-sitter-client.ts",
    "content": "import { TreeSitterClient } from \"../lib/tree-sitter/index.js\"\nimport type { SimpleHighlight } from \"../lib/tree-sitter/types.js\"\n\nexport class MockTreeSitterClient extends TreeSitterClient {\n  private _highlightPromises: Array<{\n    promise: Promise<{ highlights?: SimpleHighlight[]; warning?: string; error?: string }>\n    resolve: (result: { highlights?: SimpleHighlight[]; warning?: string; error?: string }) => void\n    timeout?: ReturnType<typeof setTimeout>\n  }> = []\n  private _mockResult: { highlights?: SimpleHighlight[]; warning?: string; error?: string } = { highlights: [] }\n  private _autoResolveTimeout?: number\n\n  constructor(options?: { autoResolveTimeout?: number }) {\n    super({ dataPath: \"/tmp/mock\" })\n    this._autoResolveTimeout = options?.autoResolveTimeout\n  }\n\n  async highlightOnce(\n    content: string,\n    filetype: string,\n  ): Promise<{ highlights?: SimpleHighlight[]; warning?: string; error?: string }> {\n    const { promise, resolve } = Promise.withResolvers<{\n      highlights?: SimpleHighlight[]\n      warning?: string\n      error?: string\n    }>()\n\n    let timeout: ReturnType<typeof setTimeout> | undefined\n\n    if (this._autoResolveTimeout !== undefined) {\n      timeout = setTimeout(() => {\n        const index = this._highlightPromises.findIndex((p) => p.promise === promise)\n        if (index !== -1) {\n          resolve(this._mockResult)\n          this._highlightPromises.splice(index, 1)\n        }\n      }, this._autoResolveTimeout)\n    }\n\n    this._highlightPromises.push({ promise, resolve, timeout })\n\n    return promise\n  }\n\n  setMockResult(result: { highlights?: SimpleHighlight[]; warning?: string; error?: string }) {\n    this._mockResult = result\n  }\n\n  resolveHighlightOnce(index: number = 0) {\n    if (index >= 0 && index < this._highlightPromises.length) {\n      const item = this._highlightPromises[index]\n      if (item.timeout) {\n        clearTimeout(item.timeout)\n      }\n      item.resolve(this._mockResult)\n      this._highlightPromises.splice(index, 1)\n    }\n  }\n\n  resolveAllHighlightOnce() {\n    for (const { resolve, timeout } of this._highlightPromises) {\n      if (timeout) {\n        clearTimeout(timeout)\n      }\n      resolve(this._mockResult)\n    }\n    this._highlightPromises = []\n  }\n\n  isHighlighting(): boolean {\n    return this._highlightPromises.length > 0\n  }\n}\n"
  },
  {
    "path": "packages/core/src/testing/spy.ts",
    "content": "export function createSpy() {\n  const calls: any[][] = []\n  const spy = (...args: any[]) => {\n    calls.push(args)\n  }\n  spy.calls = calls\n  spy.callCount = () => calls.length\n  spy.calledWith = (...expected: any[]) => {\n    return calls.some((call) => JSON.stringify(call) === JSON.stringify(expected))\n  }\n  spy.reset = () => (calls.length = 0)\n  return spy\n}\n"
  },
  {
    "path": "packages/core/src/testing/test-recorder.test.ts",
    "content": "import { describe, test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"./test-renderer.js\"\nimport { TestRecorder } from \"./test-recorder.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\ndescribe(\"TestRecorder\", () => {\n  let renderer: TestRenderer\n  let recorder: TestRecorder\n  let renderOnce: () => Promise<void>\n\n  beforeEach(async () => {\n    const setup = await createTestRenderer({ width: 80, height: 24 })\n    renderer = setup.renderer\n    renderOnce = setup.renderOnce\n    recorder = new TestRecorder(renderer)\n  })\n\n  afterEach(() => {\n    recorder.stop()\n    renderer.destroy()\n  })\n\n  test(\"should initialize with empty frames\", () => {\n    expect(recorder.recordedFrames).toEqual([])\n    expect(recorder.isRecording).toBe(false)\n  })\n\n  test(\"should start recording\", () => {\n    recorder.rec()\n    expect(recorder.isRecording).toBe(true)\n  })\n\n  test(\"should stop recording\", () => {\n    recorder.rec()\n    expect(recorder.isRecording).toBe(true)\n    recorder.stop()\n    expect(recorder.isRecording).toBe(false)\n  })\n\n  test(\"should record frames during rendering\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Hello World\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    expect(recorder.recordedFrames.length).toBe(1)\n\n    await renderOnce()\n    expect(recorder.recordedFrames.length).toBe(2)\n\n    recorder.stop()\n  })\n\n  test(\"should capture frame content correctly\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Test Content\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames = recorder.recordedFrames\n    expect(frames.length).toBe(1)\n    expect(frames[0].frame).toContain(\"Test Content\")\n\n    recorder.stop()\n  })\n\n  test(\"should include frame metadata\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Frame Metadata\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames = recorder.recordedFrames\n    expect(frames.length).toBe(1)\n    expect(frames[0].timestamp).toBeGreaterThanOrEqual(0)\n    expect(frames[0].frameNumber).toBe(0)\n\n    recorder.stop()\n  })\n\n  test(\"should increment frame numbers\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Multiple Frames\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    await renderOnce()\n    await renderOnce()\n\n    const frames = recorder.recordedFrames\n    expect(frames.length).toBe(3)\n    expect(frames[0].frameNumber).toBe(0)\n    expect(frames[1].frameNumber).toBe(1)\n    expect(frames[2].frameNumber).toBe(2)\n\n    recorder.stop()\n  })\n\n  test(\"should capture changing content across frames\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Initial\" })\n    renderer.root.add(text)\n    await Bun.sleep(10)\n\n    text.content = \"Changed\"\n    await Bun.sleep(10)\n    recorder.stop()\n\n    // NOTE: Should this fail, make sure the Bun.sleeps are in sync with maxFps of the renderer\n    const frame1 = recorder.recordedFrames[0].frame\n    const frame2 = recorder.recordedFrames[1].frame\n\n    expect(frame1).toContain(\"Initial\")\n    expect(frame2).toContain(\"Changed\")\n    expect(frame1).not.toEqual(frame2)\n  })\n\n  test(\"should not record when not started\", async () => {\n    const text = new TextRenderable(renderer, { content: \"Not Recording\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    expect(recorder.recordedFrames.length).toBe(0)\n  })\n\n  test(\"should not record after stopped\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Stopped\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    expect(recorder.recordedFrames.length).toBe(1)\n\n    recorder.stop()\n    await renderOnce()\n    expect(recorder.recordedFrames.length).toBe(1)\n  })\n\n  test(\"should clear recorded frames\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Clear Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    await renderOnce()\n\n    expect(recorder.recordedFrames.length).toBe(2)\n    recorder.clear()\n    expect(recorder.recordedFrames.length).toBe(0)\n\n    recorder.stop()\n  })\n\n  test(\"should handle multiple rec/stop cycles\", async () => {\n    const text = new TextRenderable(renderer, { content: \"Cycle Test\" })\n\n    recorder.rec()\n    renderer.root.add(text)\n    await Bun.sleep(1)\n    recorder.stop()\n    expect(recorder.recordedFrames.length).toBe(1)\n\n    recorder.clear()\n    recorder.rec()\n    await renderOnce()\n    await renderOnce()\n    recorder.stop()\n    expect(recorder.recordedFrames.length).toBe(2)\n  })\n\n  test(\"should not duplicate frames when rec is called multiple times\", async () => {\n    recorder.rec()\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Duplicate Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    recorder.stop()\n\n    expect(recorder.recordedFrames.length).toBe(1)\n  })\n\n  test(\"should restore original renderNative after stop\", async () => {\n    const text = new TextRenderable(renderer, { content: \"Restore Test\" })\n\n    recorder.rec()\n    renderer.root.add(text)\n    await Bun.sleep(1)\n    recorder.stop()\n\n    recorder.clear()\n    await renderOnce()\n    expect(recorder.recordedFrames.length).toBe(0)\n\n    recorder.rec()\n    await renderOnce()\n    recorder.stop()\n    expect(recorder.recordedFrames.length).toBe(1)\n  })\n\n  test(\"should capture timestamps in increasing order\", async () => {\n    let time = 0\n    recorder = new TestRecorder(renderer, { now: () => time })\n    recorder.rec()\n\n    await renderOnce()\n    time += 10\n    await renderOnce()\n\n    const frames = recorder.recordedFrames\n    expect(frames.length).toBe(2)\n    expect(frames[1].timestamp).toBeGreaterThan(frames[0].timestamp)\n    expect(frames[1].timestamp - frames[0].timestamp).toBe(10)\n\n    recorder.stop()\n  })\n\n  test(\"should return a copy of recorded frames\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Copy Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames1 = recorder.recordedFrames\n    const frames2 = recorder.recordedFrames\n\n    expect(frames1).toEqual(frames2)\n    expect(frames1).not.toBe(frames2)\n\n    recorder.stop()\n  })\n\n  test(\"should handle empty renders\", async () => {\n    recorder.rec()\n    await renderOnce()\n\n    expect(recorder.recordedFrames.length).toBe(1)\n    expect(recorder.recordedFrames[0].frame).toBeDefined()\n\n    recorder.stop()\n  })\n\n  test(\"should capture complex content\", async () => {\n    recorder.rec()\n\n    const text1 = new TextRenderable(renderer, { content: \"Line 1\" })\n    const text2 = new TextRenderable(renderer, { content: \"Line 2\" })\n    renderer.root.add(text1)\n    renderer.root.add(text2)\n    await Bun.sleep(1)\n\n    const frame = recorder.recordedFrames[0].frame\n    expect(frame).toContain(\"Line 1\")\n    expect(frame).toContain(\"Line 2\")\n\n    recorder.stop()\n  })\n\n  test(\"should handle rapid render calls\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Rapid Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    for (let i = 0; i < 4; i++) {\n      await renderOnce()\n    }\n\n    expect(recorder.recordedFrames.length).toBe(5)\n\n    recorder.stop()\n  })\n\n  test(\"should optionally record fg buffer\", async () => {\n    const recorderWithFg = new TestRecorder(renderer, { recordBuffers: { fg: true } })\n    recorderWithFg.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Buffer Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames = recorderWithFg.recordedFrames\n    expect(frames.length).toBe(1)\n    expect(frames[0].buffers).toBeDefined()\n    expect(frames[0].buffers?.fg).toBeInstanceOf(Float32Array)\n    expect(frames[0].buffers?.bg).toBeUndefined()\n    expect(frames[0].buffers?.attributes).toBeUndefined()\n\n    recorderWithFg.stop()\n  })\n\n  test(\"should optionally record bg buffer\", async () => {\n    const recorderWithBg = new TestRecorder(renderer, { recordBuffers: { bg: true } })\n    recorderWithBg.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Buffer Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames = recorderWithBg.recordedFrames\n    expect(frames.length).toBe(1)\n    expect(frames[0].buffers).toBeDefined()\n    expect(frames[0].buffers?.bg).toBeInstanceOf(Float32Array)\n    expect(frames[0].buffers?.fg).toBeUndefined()\n    expect(frames[0].buffers?.attributes).toBeUndefined()\n\n    recorderWithBg.stop()\n  })\n\n  test(\"should optionally record attributes buffer\", async () => {\n    const recorderWithAttrs = new TestRecorder(renderer, { recordBuffers: { attributes: true } })\n    recorderWithAttrs.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Buffer Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames = recorderWithAttrs.recordedFrames\n    expect(frames.length).toBe(1)\n    expect(frames[0].buffers).toBeDefined()\n    expect(frames[0].buffers?.attributes).toBeInstanceOf(Uint8Array)\n    expect(frames[0].buffers?.fg).toBeUndefined()\n    expect(frames[0].buffers?.bg).toBeUndefined()\n\n    recorderWithAttrs.stop()\n  })\n\n  test(\"should record multiple buffers when requested\", async () => {\n    const recorderWithAll = new TestRecorder(renderer, {\n      recordBuffers: { fg: true, bg: true, attributes: true },\n    })\n    recorderWithAll.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Buffer Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames = recorderWithAll.recordedFrames\n    expect(frames.length).toBe(1)\n    expect(frames[0].buffers).toBeDefined()\n    expect(frames[0].buffers?.fg).toBeInstanceOf(Float32Array)\n    expect(frames[0].buffers?.bg).toBeInstanceOf(Float32Array)\n    expect(frames[0].buffers?.attributes).toBeInstanceOf(Uint8Array)\n\n    recorderWithAll.stop()\n  })\n\n  test(\"should not record buffers when not requested\", async () => {\n    recorder.rec()\n\n    const text = new TextRenderable(renderer, { content: \"No Buffer Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames = recorder.recordedFrames\n    expect(frames.length).toBe(1)\n    expect(frames[0].buffers).toBeUndefined()\n\n    recorder.stop()\n  })\n\n  test(\"should record independent buffer copies\", async () => {\n    const recorderWithBuffers = new TestRecorder(renderer, { recordBuffers: { fg: true } })\n    recorderWithBuffers.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Copy Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    await renderOnce()\n\n    const frames = recorderWithBuffers.recordedFrames\n    expect(frames.length).toBe(2)\n\n    const frame1Fg = frames[0].buffers?.fg\n    const frame2Fg = frames[1].buffers?.fg\n\n    expect(frame1Fg).toBeDefined()\n    expect(frame2Fg).toBeDefined()\n    expect(frame1Fg).not.toBe(frame2Fg)\n\n    recorderWithBuffers.stop()\n  })\n\n  test(\"should have correct buffer sizes\", async () => {\n    const recorderWithAll = new TestRecorder(renderer, {\n      recordBuffers: { fg: true, bg: true, attributes: true },\n    })\n    recorderWithAll.rec()\n\n    const text = new TextRenderable(renderer, { content: \"Size Test\" })\n    renderer.root.add(text)\n    await Bun.sleep(1)\n\n    const frames = recorderWithAll.recordedFrames\n    expect(frames.length).toBe(1)\n\n    const expectedSize = renderer.width * renderer.height\n    expect(frames[0].buffers?.fg?.length).toBe(expectedSize * 4)\n    expect(frames[0].buffers?.bg?.length).toBe(expectedSize * 4)\n    expect(frames[0].buffers?.attributes?.length).toBe(expectedSize)\n\n    recorderWithAll.stop()\n  })\n})\n"
  },
  {
    "path": "packages/core/src/testing/test-recorder.ts",
    "content": "import type { TestRenderer } from \"./test-renderer.js\"\n\nexport interface RecordBuffersOptions {\n  fg?: boolean\n  bg?: boolean\n  attributes?: boolean\n}\n\nexport interface RecordedBuffers {\n  fg?: Float32Array\n  bg?: Float32Array\n  attributes?: Uint8Array\n}\n\nexport interface RecordedFrame {\n  frame: string\n  timestamp: number\n  frameNumber: number\n  buffers?: RecordedBuffers\n}\n\nexport interface TestRecorderOptions {\n  recordBuffers?: RecordBuffersOptions\n  now?: () => number\n}\n\n/**\n * TestRecorder records frames from a TestRenderer by hooking into the render pipeline.\n * It captures the character frame after each native render pass.\n */\nexport class TestRecorder {\n  private renderer: TestRenderer\n  private frames: RecordedFrame[] = []\n  private recording: boolean = false\n  private frameNumber: number = 0\n  private startTime: number = 0\n  private originalRenderNative?: () => void\n  private decoder = new TextDecoder()\n  private recordBuffers: RecordBuffersOptions\n  private now: () => number\n\n  constructor(renderer: TestRenderer, options?: TestRecorderOptions) {\n    this.renderer = renderer\n    this.recordBuffers = options?.recordBuffers || {}\n    this.now = options?.now ?? (() => performance.now())\n  }\n\n  /**\n   * Start recording frames. This hooks into the renderer's renderNative method.\n   */\n  public rec(): void {\n    if (this.recording) {\n      return\n    }\n\n    this.recording = true\n    this.frames = []\n    this.frameNumber = 0\n    this.startTime = this.now()\n\n    // Store the original renderNative method\n    this.originalRenderNative = this.renderer[\"renderNative\"].bind(this.renderer)\n\n    // Override renderNative to capture frames after each render\n    this.renderer[\"renderNative\"] = () => {\n      // Call the original renderNative\n      this.originalRenderNative!()\n\n      // Capture the frame after rendering\n      this.captureFrame()\n    }\n  }\n\n  /**\n   * Stop recording frames and restore the original renderNative method.\n   */\n  public stop(): void {\n    if (!this.recording) {\n      return\n    }\n\n    this.recording = false\n\n    // Restore the original renderNative method\n    if (this.originalRenderNative) {\n      this.renderer[\"renderNative\"] = this.originalRenderNative\n      this.originalRenderNative = undefined\n    }\n  }\n\n  /**\n   * Get the recorded frames.\n   */\n  public get recordedFrames(): RecordedFrame[] {\n    return [...this.frames]\n  }\n\n  /**\n   * Clear all recorded frames.\n   */\n  public clear(): void {\n    this.frames = []\n    this.frameNumber = 0\n  }\n\n  /**\n   * Check if currently recording.\n   */\n  public get isRecording(): boolean {\n    return this.recording\n  }\n\n  /**\n   * Capture the current frame from the renderer's buffer.\n   */\n  private captureFrame(): void {\n    const currentBuffer = this.renderer.currentRenderBuffer\n    const frameBytes = currentBuffer.getRealCharBytes(true)\n    const frame = this.decoder.decode(frameBytes)\n\n    const recordedFrame: RecordedFrame = {\n      frame,\n      timestamp: this.now() - this.startTime,\n      frameNumber: this.frameNumber++,\n    }\n\n    // Optionally record buffer data from currentRenderBuffer\n    if (this.recordBuffers.fg || this.recordBuffers.bg || this.recordBuffers.attributes) {\n      const buffers = currentBuffer.buffers\n      recordedFrame.buffers = {}\n\n      if (this.recordBuffers.fg) {\n        recordedFrame.buffers.fg = new Float32Array(buffers.fg)\n      }\n      if (this.recordBuffers.bg) {\n        recordedFrame.buffers.bg = new Float32Array(buffers.bg)\n      }\n      if (this.recordBuffers.attributes) {\n        recordedFrame.buffers.attributes = new Uint8Array(buffers.attributes)\n      }\n    }\n\n    this.frames.push(recordedFrame)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/testing/test-renderer.ts",
    "content": "import { Readable } from \"stream\"\nimport { CliRenderer, type CliRendererConfig } from \"../renderer.js\"\nimport { resolveRenderLib } from \"../zig.js\"\nimport { createMockKeys } from \"./mock-keys.js\"\nimport { createMockMouse } from \"./mock-mouse.js\"\nimport type { CapturedFrame } from \"../types.js\"\n\nexport interface TestRendererOptions extends CliRendererConfig {\n  width?: number\n  height?: number\n  kittyKeyboard?: boolean\n  otherModifiersMode?: boolean\n}\nexport interface TestRenderer extends CliRenderer {}\nexport type MockInput = ReturnType<typeof createMockKeys>\nexport type MockMouse = ReturnType<typeof createMockMouse>\n\nconst decoder = new TextDecoder()\n\nexport async function createTestRenderer(options: TestRendererOptions): Promise<{\n  renderer: TestRenderer\n  mockInput: MockInput\n  mockMouse: MockMouse\n  renderOnce: () => Promise<void>\n  captureCharFrame: () => string\n  captureSpans: () => CapturedFrame\n  resize: (width: number, height: number) => void\n}> {\n  process.env.OTUI_USE_CONSOLE = \"false\"\n\n  // Convert legacy kittyKeyboard boolean to new format\n  const useKittyKeyboard = options.kittyKeyboard ? { events: true } : options.useKittyKeyboard\n\n  const renderer = await setupTestRenderer({\n    ...options,\n    useKittyKeyboard,\n    useAlternateScreen: false,\n    useConsole: false,\n  })\n\n  renderer.disableStdoutInterception()\n\n  const mockInput = createMockKeys(renderer, {\n    kittyKeyboard: options.kittyKeyboard,\n    otherModifiersMode: options.otherModifiersMode,\n  })\n  const mockMouse = createMockMouse(renderer)\n\n  const renderOnce = async () => {\n    //@ts-expect-error - this is a test renderer\n    await renderer.loop()\n  }\n\n  return {\n    renderer,\n    mockInput,\n    mockMouse,\n    renderOnce,\n    captureCharFrame: () => {\n      const currentBuffer = renderer.currentRenderBuffer\n      const frameBytes = currentBuffer.getRealCharBytes(true)\n      return decoder.decode(frameBytes)\n    },\n    captureSpans: () => {\n      const currentBuffer = renderer.currentRenderBuffer\n      const lines = currentBuffer.getSpanLines()\n      const cursorState = renderer.getCursorState()\n      return {\n        cols: currentBuffer.width,\n        rows: currentBuffer.height,\n        cursor: [cursorState.x, cursorState.y] as [number, number],\n        lines,\n      }\n    },\n    resize: (width: number, height: number) => {\n      //@ts-expect-error - this is a test renderer\n      renderer.processResize(width, height)\n    },\n  }\n}\n\nasync function setupTestRenderer(config: TestRendererOptions) {\n  const stdin = config.stdin || (new Readable({ read() {} }) as NodeJS.ReadStream)\n  const stdout = config.stdout || process.stdout\n\n  const width = config.width || stdout.columns || 80\n  const height = config.height || stdout.rows || 24\n  const renderHeight =\n    config.experimental_splitHeight && config.experimental_splitHeight > 0 ? config.experimental_splitHeight : height\n\n  const ziglib = resolveRenderLib()\n  const rendererPtr = ziglib.createRenderer(width, renderHeight, {\n    testing: true,\n    remote: config.remote ?? false,\n  })\n  if (!rendererPtr) {\n    throw new Error(\"Failed to create test renderer\")\n  }\n  if (config.useThread === undefined) {\n    config.useThread = true\n  }\n\n  if (process.platform === \"linux\") {\n    config.useThread = false\n  }\n  ziglib.setUseThread(rendererPtr, config.useThread)\n\n  const renderer = new CliRenderer(ziglib, rendererPtr, stdin, stdout, width, height, config)\n\n  process.off(\"SIGWINCH\", renderer[\"sigwinchHandler\"])\n\n  // Do not setup the terminal for testing as we will not actually output anything to the terminal\n  // await renderer.setupTerminal()\n\n  return renderer\n}\n"
  },
  {
    "path": "packages/core/src/testing.ts",
    "content": "// Testing utilities module exports\nexport * from \"./testing/test-renderer.js\"\nexport * from \"./testing/mock-keys.js\"\nexport * from \"./testing/mock-mouse.js\"\nexport * from \"./testing/mock-tree-sitter-client.js\"\nexport * from \"./testing/spy.js\"\nexport { TestRecorder, type RecordedFrame } from \"./testing/test-recorder.js\"\n"
  },
  {
    "path": "packages/core/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`Absolute Positioning - Snapshot Tests Basic absolute positioning absolute positioned box at top-left: absolute positioned box at top-left 1`] = `\n\"┌─────────────┐                         \n│Top Left     │                         \n│             │                         \n│             │                         \n└─────────────┘                         \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Basic absolute positioning absolute positioned box at bottom-right using right/bottom: absolute positioned box at bottom-right 1`] = `\n\"                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                         ┌─────────────┐\n                         │Bottom Right │\n                         │             │\n                         │             │\n                         └─────────────┘\n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Basic absolute positioning absolute positioned box centered with left/top: absolute positioned box centered 1`] = `\n\"                                        \n                                        \n                                        \n                                        \n                                        \n          ┌──────────────────┐          \n          │Centered          │          \n          │                  │          \n          │                  │          \n          │                  │          \n          │                  │          \n          │                  │          \n          └──────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Nested absolute positioning absolute child inside absolute parent - basic: nested absolute - child inside parent at left/top 1`] = `\n\"                                        \n                                        \n                                        \n     ┌────────────────────────────┐     \n     │                            │     \n     │  ┌──────────┐              │     \n     │  │Nested    │              │     \n     │  │          │              │     \n     │  └──────────┘              │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     └────────────────────────────┘     \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Nested absolute positioning absolute child at bottom:0 inside absolute parent (issue #406 fix): nested absolute - child at bottom:0 of parent 1`] = `\n\"                                        \n                                        \n     ┌────────────────────────────┐     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │  ┌─────────────┐           │     \n     │  │At Bottom    │           │     \n     │  └─────────────┘           │     \n     └────────────────────────────┘     \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Nested absolute positioning absolute child at right:0 inside absolute parent: nested absolute - child at right:0 of parent 1`] = `\n\"                                        \n                                        \n  ┌─────────────────────────────────┐   \n  │                                 │   \n  │                     ┌──────────┐│   \n  │                     │At Right  ││   \n  │                     │          ││   \n  │                     └──────────┘│   \n  │                                 │   \n  │                                 │   \n  │                                 │   \n  │                                 │   \n  │                                 │   \n  └─────────────────────────────────┘   \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Nested absolute positioning absolute child at bottom-right corner inside absolute parent: nested absolute - child at bottom-right corner 1`] = `\n\"                                        \n   ┌────────────────────────────────┐   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                 ┌────────────┐ │   \n   │                 │Corner      │ │   \n   │                 │            │ │   \n   │                 └────────────┘ │   \n   │                                │   \n   └────────────────────────────────┘   \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Nested absolute positioning multiple absolute children inside absolute parent at different positions: nested absolute - four corners inside parent 1`] = `\n\"                                        \n  ┌──────────────────────────────────┐  \n  │                                  │  \n  │ ┌────────┐            ┌────────┐ │  \n  │ │TL      │            │TR      │ │  \n  │ └────────┘            └────────┘ │  \n  │                                  │  \n  │                                  │  \n  │                                  │  \n  │                                  │  \n  │                                  │  \n  │                                  │  \n  │                                  │  \n  │ ┌────────┐            ┌────────┐ │  \n  │ │BL      │            │BR      │ │  \n  │ └────────┘            └────────┘ │  \n  │                                  │  \n  └──────────────────────────────────┘  \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Three-level nesting deeply nested absolute positioning - grandchild at bottom: three-level nested absolute - grandchild at bottom 1`] = `\n\"                                        \n ┌────────────────────────────────────┐ \n │                                    │ \n │                                    │ \n │  ┌──────────────────────────────┐  │ \n │  │                              │  │ \n │  │                              │  │ \n │  │                              │  │ \n │  │                              │  │ \n │  │                              │  │ \n │  │                              │  │ \n │  │  ┌─────────────┐             │  │ \n │  │  │Deep         │             │  │ \n │  │  └─────────────┘             │  │ \n │  │                              │  │ \n │  └──────────────────────────────┘  │ \n │                                    │ \n │                                    │ \n └────────────────────────────────────┘ \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Mixed positioning absolute child inside relative parent: absolute child inside relative parent 1`] = `\n\"                                        \n                                        \n   ┌────────────────────────────┐       \n   │                            │       \n   │                            │       \n   │                            │       \n   │                            │       \n   │                            │       \n   │                            │       \n   │                            │       \n   │               ┌──────────┐ │       \n   │               │Absolute  │ │       \n   │               │          │ │       \n   │               └──────────┘ │       \n   │                            │       \n   └────────────────────────────┘       \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Mixed positioning sibling absolute elements at same level: sibling absolute elements overlapping 1`] = `\n\"┌─────────────┐                         \n│Box 1        │                         \n│             │                         \n│             │                         \n│           ┌─────────────┐             \n└───────────│Box 2        │             \n            │             │             \n            │             │             \n            │           ┌─────────────┐ \n            └───────────│Box 3        │ \n                        │             │ \n                        │             │ \n                        │             │ \n                        └─────────────┘ \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Edge cases absolute positioned box with negative coordinates (partially off-screen): absolute box with negative coordinates 1`] = `\n\"              │                         \n              │                         \n              │                         \n              │                         \n              │                         \n──────────────┘                         \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Edge cases absolute positioned box extending beyond viewport: absolute box extending beyond viewport 1`] = `\n\"                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                              ┌─────────\n                              │Overflow \n                              │         \n                              │         \n                              │         \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Edge cases absolute child fills parent completely: absolute child fills parent with inset 0 1`] = `\n\"                                        \n                                        \n                                        \n     ┌────────────────────────────┐     \n     │╔══════════════════════════╗│     \n     │║Full                      ║│     \n     │║                          ║│     \n     │║                          ║│     \n     │║                          ║│     \n     │║                          ║│     \n     │║                          ║│     \n     │║                          ║│     \n     │║                          ║│     \n     │╚══════════════════════════╝│     \n     └────────────────────────────┘     \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Edge cases absolute positioned box with percentage width inside absolute parent: absolute child with percentage width 1`] = `\n\"                                                  \n                                                  \n     ┌──────────────────────────────────────┐     \n     │                                      │     \n     │                                      │     \n     │                                      │     \n     │                                      │     \n     │                                      │     \n     │                                      │     \n     │                                      │     \n     │                                      │     \n     │  ┌─────────────────┐                 │     \n     │  │50%              │                 │     \n     │  │                 │                 │     \n     │  └─────────────────┘                 │     \n     │                                      │     \n     └──────────────────────────────────────┘     \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Edge cases absolute positioned box with percentage height inside absolute parent: absolute child with percentage height 1`] = `\n\"                                        \n                                        \n     ┌────────────────────────────┐     \n     │                            │     \n     │  ┌─────────────┐           │     \n     │  │50% H        │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  └─────────────┘           │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     │                            │     \n     └────────────────────────────┘     \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Edge cases absolute child with conflicting insets (left and right without explicit width): absolute child with left and right insets (no explicit width) 1`] = `\n\"                                        \n                                        \n   ┌────────────────────────────────┐   \n   │                                │   \n   │                                │   \n   │  ┌──────────────────────────┐  │   \n   │  │Stretch                   │  │   \n   │  │                          │  │   \n   │  │                          │  │   \n   │  └──────────────────────────┘  │   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                                │   \n   │                                │   \n   └────────────────────────────────┘   \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Edge cases absolute child with conflicting insets (top and bottom without explicit height): absolute child with top and bottom insets (no explicit height) 1`] = `\n\"                                        \n     ┌────────────────────────────┐     \n     │                            │     \n     │  ┌─────────────┐           │     \n     │  │VStretch     │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  │             │           │     \n     │  └─────────────┘           │     \n     │                            │     \n     └────────────────────────────┘     \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Complex hierarchies relative parent with absolute child containing absolute grandchild: relative -> absolute -> absolute hierarchy 1`] = `\n\"                                        \n  ┌─────────────────────────────────┐   \n  │                                 │   \n  │  ┌──────────────────────────┐   │   \n  │  │                          │   │   \n  │  │                          │   │   \n  │  │                          │   │   \n  │  │                          │   │   \n  │  │                          │   │   \n  │  │             ┌──────────┐ │   │   \n  │  │             │Grand     │ │   │   \n  │  │             │          │ │   │   \n  │  │             └──────────┘ │   │   \n  │  │                          │   │   \n  │  └──────────────────────────┘   │   \n  │                                 │   \n  └─────────────────────────────────┘   \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Absolute Positioning - Snapshot Tests Complex hierarchies multiple nested relative and absolute layers: relative -> absolute -> relative -> absolute hierarchy 1`] = `\n\"┌────────────────────────────────────┐  \n│                                    │  \n│  ┌──────────────────────────────┐  │  \n│  │                              │  │  \n│  │ ┌──────────────────────────┐ │  │  \n│  │ │                          │ │  │  \n│  │ │                          │ │  │  \n│  │ │                          │ │  │  \n│  │ │                          │ │  │  \n│  │ │               ┌────────┐ │ │  │  \n│  │ │               │Deep    │ │ │  │  \n│  │ │               └────────┘ │ │  │  \n│  │ │                          │ │  │  \n│  │ └──────────────────────────┘ │  │  \n│  │                              │  │  \n│  └──────────────────────────────┘  │  \n│                                    │  \n└────────────────────────────────────┘  \n                                        \n                                        \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/tests/__snapshots__/renderable.snapshot.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`Renderable - insertBefore reproduces insertBefore behavior with state change after timeout: insertBefore initial state 1`] = `\n\"banana    \napple     \npear      \n          \n          \n\"\n`;\n\nexports[`Renderable - insertBefore reproduces insertBefore behavior with state change after timeout: insertBefore reordered state 1`] = `\n\"banana    \npear      \napple     \n          \n          \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/tests/__snapshots__/scrollbox.test.ts.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`ScrollBoxRenderable - Content Visibility scrolls CodeRenderable with LineNumberRenderable using mouse wheel 1`] = `\n\" 22 Line 22                                                                     \n 23 Line 23                                                                     \n 24 Line 24                                                                     \n 25 Line 25                                                                     \n 26 Line 26                                                                     \n 27 Line 27                                                                     \n 28 Line 28                                                                     \n 29 Line 29                                                                     \n 30 Line 30                                                                     \n 31                                    ▄                                        \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n                                                                                \n\"\n`;\n"
  },
  {
    "path": "packages/core/src/tests/absolute-positioning.snapshot.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { BoxRenderable } from \"../renderables/Box.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\nlet testRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet captureFrame: () => string\nlet resize: (width: number, height: number) => void\n\nbeforeEach(async () => {\n  ;({\n    renderer: testRenderer,\n    renderOnce,\n    captureCharFrame: captureFrame,\n    resize,\n  } = await createTestRenderer({\n    width: 40,\n    height: 20,\n  }))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ndescribe(\"Absolute Positioning - Snapshot Tests\", () => {\n  describe(\"Basic absolute positioning\", () => {\n    test(\"absolute positioned box at top-left\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: 15,\n        height: 5,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"Top Left\" })\n      box.add(text)\n      testRenderer.root.add(box)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute positioned box at top-left\")\n    })\n\n    test(\"absolute positioned box at bottom-right using right/bottom\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        right: 0,\n        bottom: 0,\n        width: 15,\n        height: 5,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"Bottom Right\" })\n      box.add(text)\n      testRenderer.root.add(box)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute positioned box at bottom-right\")\n    })\n\n    test(\"absolute positioned box centered with left/top\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 10,\n        top: 5,\n        width: 20,\n        height: 8,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"Centered\" })\n      box.add(text)\n      testRenderer.root.add(box)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute positioned box centered\")\n    })\n  })\n\n  describe(\"Nested absolute positioning\", () => {\n    test(\"absolute child inside absolute parent - basic\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 5,\n        top: 3,\n        width: 30,\n        height: 12,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        top: 1,\n        width: 12,\n        height: 4,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"Nested\" })\n      child.add(text)\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"nested absolute - child inside parent at left/top\")\n    })\n\n    test(\"absolute child at bottom:0 inside absolute parent (issue #406 fix)\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 5,\n        top: 2,\n        width: 30,\n        height: 14,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        bottom: 0,\n        left: 2,\n        width: 15,\n        height: 3,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"At Bottom\" })\n      child.add(text)\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"nested absolute - child at bottom:0 of parent\")\n    })\n\n    test(\"absolute child at right:0 inside absolute parent\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 35,\n        height: 12,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        right: 0,\n        top: 1,\n        width: 12,\n        height: 4,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"At Right\" })\n      child.add(text)\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"nested absolute - child at right:0 of parent\")\n    })\n\n    test(\"absolute child at bottom-right corner inside absolute parent\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 3,\n        top: 1,\n        width: 34,\n        height: 16,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        right: 1,\n        bottom: 1,\n        width: 14,\n        height: 4,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"Corner\" })\n      child.add(text)\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"nested absolute - child at bottom-right corner\")\n    })\n\n    test(\"multiple absolute children inside absolute parent at different positions\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        top: 1,\n        width: 36,\n        height: 17,\n        border: true,\n      })\n\n      const topLeftChild = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 10,\n        height: 3,\n        border: true,\n      })\n\n      const topRightChild = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        right: 1,\n        top: 1,\n        width: 10,\n        height: 3,\n        border: true,\n      })\n\n      const bottomLeftChild = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 1,\n        bottom: 1,\n        width: 10,\n        height: 3,\n        border: true,\n      })\n\n      const bottomRightChild = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        right: 1,\n        bottom: 1,\n        width: 10,\n        height: 3,\n        border: true,\n      })\n\n      topLeftChild.add(new TextRenderable(testRenderer, { content: \"TL\" }))\n      topRightChild.add(new TextRenderable(testRenderer, { content: \"TR\" }))\n      bottomLeftChild.add(new TextRenderable(testRenderer, { content: \"BL\" }))\n      bottomRightChild.add(new TextRenderable(testRenderer, { content: \"BR\" }))\n\n      parent.add(topLeftChild)\n      parent.add(topRightChild)\n      parent.add(bottomLeftChild)\n      parent.add(bottomRightChild)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"nested absolute - four corners inside parent\")\n    })\n  })\n\n  describe(\"Three-level nesting\", () => {\n    test(\"deeply nested absolute positioning - grandchild at bottom\", async () => {\n      const grandparent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 38,\n        height: 18,\n        border: true,\n      })\n\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 32,\n        height: 12,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        bottom: 1,\n        left: 2,\n        width: 15,\n        height: 3,\n        border: true,\n      })\n\n      child.add(new TextRenderable(testRenderer, { content: \"Deep\" }))\n      parent.add(child)\n      grandparent.add(parent)\n      testRenderer.root.add(grandparent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"three-level nested absolute - grandchild at bottom\")\n    })\n  })\n\n  describe(\"Mixed positioning\", () => {\n    test(\"absolute child inside relative parent\", async () => {\n      const container = new BoxRenderable(testRenderer, {\n        width: 40,\n        height: 20,\n        paddingTop: 2,\n        paddingLeft: 3,\n      })\n\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"relative\",\n        width: 30,\n        height: 14,\n        border: true,\n      })\n\n      const absoluteChild = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        bottom: 1,\n        right: 1,\n        width: 12,\n        height: 4,\n        border: true,\n      })\n\n      absoluteChild.add(new TextRenderable(testRenderer, { content: \"Absolute\" }))\n      parent.add(absoluteChild)\n      container.add(parent)\n      testRenderer.root.add(container)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute child inside relative parent\")\n    })\n\n    test(\"sibling absolute elements at same level\", async () => {\n      const box1 = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: 15,\n        height: 6,\n        border: true,\n      })\n\n      const box2 = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 12,\n        top: 4,\n        width: 15,\n        height: 6,\n        border: true,\n      })\n\n      const box3 = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 24,\n        top: 8,\n        width: 15,\n        height: 6,\n        border: true,\n      })\n\n      box1.add(new TextRenderable(testRenderer, { content: \"Box 1\" }))\n      box2.add(new TextRenderable(testRenderer, { content: \"Box 2\" }))\n      box3.add(new TextRenderable(testRenderer, { content: \"Box 3\" }))\n\n      testRenderer.root.add(box1)\n      testRenderer.root.add(box2)\n      testRenderer.root.add(box3)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"sibling absolute elements overlapping\")\n    })\n  })\n\n  describe(\"Edge cases\", () => {\n    test(\"absolute positioned box with negative coordinates (partially off-screen)\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: -5,\n        top: -2,\n        width: 20,\n        height: 8,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"Partial\" })\n      box.add(text)\n      testRenderer.root.add(box)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute box with negative coordinates\")\n    })\n\n    test(\"absolute positioned box extending beyond viewport\", async () => {\n      const box = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 30,\n        top: 15,\n        width: 20,\n        height: 10,\n        border: true,\n      })\n\n      const text = new TextRenderable(testRenderer, { content: \"Overflow\" })\n      box.add(text)\n      testRenderer.root.add(box)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute box extending beyond viewport\")\n    })\n\n    test(\"absolute child fills parent completely\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 5,\n        top: 3,\n        width: 30,\n        height: 12,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        right: 0,\n        bottom: 0,\n        border: true,\n        borderStyle: \"double\",\n      })\n\n      child.add(new TextRenderable(testRenderer, { content: \"Full\" }))\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute child fills parent with inset 0\")\n    })\n\n    test(\"absolute positioned box with percentage width inside absolute parent\", async () => {\n      resize(50, 20)\n\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 5,\n        top: 2,\n        width: 40,\n        height: 15,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        bottom: 1,\n        width: \"50%\",\n        height: 4,\n        border: true,\n      })\n\n      child.add(new TextRenderable(testRenderer, { content: \"50%\" }))\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute child with percentage width\")\n    })\n\n    test(\"absolute positioned box with percentage height inside absolute parent\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 5,\n        top: 2,\n        width: 30,\n        height: 16,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        top: 1,\n        width: 15,\n        height: \"50%\",\n        border: true,\n      })\n\n      child.add(new TextRenderable(testRenderer, { content: \"50% H\" }))\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute child with percentage height\")\n    })\n\n    test(\"absolute child with conflicting insets (left and right without explicit width)\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 3,\n        top: 2,\n        width: 34,\n        height: 14,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        right: 2,\n        top: 2,\n        height: 5,\n        border: true,\n      })\n\n      child.add(new TextRenderable(testRenderer, { content: \"Stretch\" }))\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute child with left and right insets (no explicit width)\")\n    })\n\n    test(\"absolute child with conflicting insets (top and bottom without explicit height)\", async () => {\n      const parent = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 5,\n        top: 1,\n        width: 30,\n        height: 16,\n        border: true,\n      })\n\n      const child = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        top: 1,\n        bottom: 1,\n        left: 2,\n        width: 15,\n        border: true,\n      })\n\n      child.add(new TextRenderable(testRenderer, { content: \"VStretch\" }))\n      parent.add(child)\n      testRenderer.root.add(parent)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"absolute child with top and bottom insets (no explicit height)\")\n    })\n  })\n\n  describe(\"Complex hierarchies\", () => {\n    test(\"relative parent with absolute child containing absolute grandchild\", async () => {\n      const container = new BoxRenderable(testRenderer, {\n        width: 40,\n        height: 20,\n        paddingTop: 1,\n        paddingLeft: 2,\n      })\n\n      const relativeParent = new BoxRenderable(testRenderer, {\n        position: \"relative\",\n        width: 35,\n        height: 16,\n        border: true,\n      })\n\n      const absoluteChild = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        top: 1,\n        width: 28,\n        height: 12,\n        border: true,\n      })\n\n      const absoluteGrandchild = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        right: 1,\n        bottom: 1,\n        width: 12,\n        height: 4,\n        border: true,\n      })\n\n      absoluteGrandchild.add(new TextRenderable(testRenderer, { content: \"Grand\" }))\n      absoluteChild.add(absoluteGrandchild)\n      relativeParent.add(absoluteChild)\n      container.add(relativeParent)\n      testRenderer.root.add(container)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"relative -> absolute -> absolute hierarchy\")\n    })\n\n    test(\"multiple nested relative and absolute layers\", async () => {\n      const root = new BoxRenderable(testRenderer, {\n        position: \"relative\",\n        width: 38,\n        height: 18,\n        border: true,\n      })\n\n      const absoluteLayer1 = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        left: 2,\n        top: 1,\n        width: 32,\n        height: 14,\n        border: true,\n      })\n\n      const relativeLayer2 = new BoxRenderable(testRenderer, {\n        position: \"relative\",\n        width: 28,\n        height: 10,\n        marginLeft: 1,\n        marginTop: 1,\n        border: true,\n      })\n\n      const absoluteLayer3 = new BoxRenderable(testRenderer, {\n        position: \"absolute\",\n        right: 1,\n        bottom: 1,\n        width: 10,\n        height: 3,\n        border: true,\n      })\n\n      absoluteLayer3.add(new TextRenderable(testRenderer, { content: \"Deep\" }))\n      relativeLayer2.add(absoluteLayer3)\n      absoluteLayer1.add(relativeLayer2)\n      root.add(absoluteLayer1)\n      testRenderer.root.add(root)\n\n      await renderOnce()\n      expect(captureFrame()).toMatchSnapshot(\"relative -> absolute -> relative -> absolute hierarchy\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/allocator-stats.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { resolveRenderLib } from \"../zig\"\n\nconst lib = resolveRenderLib()\n\nfunction expectValidAllocatorStats(stats: ReturnType<typeof lib.getAllocatorStats>): void {\n  expect(Number.isFinite(stats.totalRequestedBytes)).toBe(true)\n  expect(Number.isFinite(stats.activeAllocations)).toBe(true)\n  expect(Number.isFinite(stats.smallAllocations)).toBe(true)\n  expect(Number.isFinite(stats.largeAllocations)).toBe(true)\n  expect(typeof stats.requestedBytesValid).toBe(\"boolean\")\n\n  expect(stats.totalRequestedBytes).toBeGreaterThanOrEqual(0)\n  expect(stats.activeAllocations).toBeGreaterThanOrEqual(0)\n  expect(stats.smallAllocations).toBeGreaterThanOrEqual(0)\n  expect(stats.largeAllocations).toBeGreaterThanOrEqual(0)\n  expect(stats.activeAllocations).toBe(stats.smallAllocations + stats.largeAllocations)\n}\n\ntest(\"getBuildOptions exposes native build flags\", () => {\n  const buildOptions = lib.getBuildOptions()\n  expect(typeof buildOptions.gpaSafeStats).toBe(\"boolean\")\n  expect(typeof buildOptions.gpaMemoryLimitTracking).toBe(\"boolean\")\n  expect(buildOptions.gpaMemoryLimitTracking).toBe(buildOptions.gpaSafeStats)\n})\n\ntest(\"getAllocatorStats returns allocator stats\", () => {\n  const before = lib.getAllocatorStats()\n  expectValidAllocatorStats(before)\n\n  const textBuffer = lib.createTextBuffer(\"unicode\")\n  textBuffer.append(\"allocator stats smoke test\")\n\n  const after = lib.getAllocatorStats()\n  expectValidAllocatorStats(after)\n\n  textBuffer.destroy()\n})\n"
  },
  {
    "path": "packages/core/src/tests/destroy-during-render.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe } from \"bun:test\"\nimport { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport type { RenderContext } from \"../types.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\n\nclass TestRenderable extends Renderable {\n  public renderSelfCalled = false\n  public customOnUpdate?: () => void\n\n  constructor(ctx: RenderContext, options: RenderableOptions) {\n    super(ctx, options)\n  }\n\n  public onUpdate(deltaTime: number): void {\n    if (this.customOnUpdate) {\n      this.customOnUpdate()\n    }\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    this.renderSelfCalled = true\n  }\n}\n\nlet testRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\n\nbeforeEach(async () => {\n  ;({ renderer: testRenderer, renderOnce } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ndescribe(\"Destroy During Render - Actual Bugs\", () => {\n  test(\"BUG: destroying self in onUpdate still calls renderSelf\", async () => {\n    const renderable = new TestRenderable(testRenderer, {\n      id: \"test\",\n      width: 100,\n      height: 100,\n    })\n\n    renderable.customOnUpdate = () => {\n      renderable.destroy()\n    }\n\n    testRenderer.root.add(renderable)\n    await renderOnce()\n\n    expect(renderable.isDestroyed).toBe(true)\n    // BUG: renderSelf should NOT be called after destroy in onUpdate\n    expect(renderable.renderSelfCalled).toBe(false)\n  })\n\n  test(\"BUG: destroying child in parent's onUpdate, child still renders\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n    })\n    const child = new TestRenderable(testRenderer, {\n      id: \"child\",\n      width: 50,\n      height: 50,\n    })\n\n    parent.add(child)\n    testRenderer.root.add(parent)\n\n    parent.customOnUpdate = () => {\n      child.destroy()\n    }\n\n    await renderOnce()\n\n    expect(child.isDestroyed).toBe(true)\n    // BUG: Child should not render if destroyed in parent's onUpdate\n    expect(child.renderSelfCalled).toBe(false)\n  })\n\n  test(\"BUG: destroying sibling in onUpdate, sibling still renders\", async () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      width: 50,\n      height: 50,\n    })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      width: 50,\n      height: 50,\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n    testRenderer.root.add(parent)\n\n    child1.customOnUpdate = () => {\n      child2.destroy()\n    }\n\n    await renderOnce()\n\n    expect(child2.isDestroyed).toBe(true)\n    // BUG: child2 should not render if destroyed by sibling's onUpdate\n    expect(child2.renderSelfCalled).toBe(false)\n  })\n\n  test(\"BUG: destroying sibling in renderBefore, sibling (later in render list) still renders\", async () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      width: 50,\n      height: 50,\n      zIndex: 2,\n    })\n\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      width: 50,\n      height: 50,\n      zIndex: 1,\n      renderBefore: function () {\n        child2.destroy()\n      },\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n    testRenderer.root.add(parent)\n\n    await renderOnce()\n\n    expect(child2.isDestroyed).toBe(true)\n    // BUG: child2 should not render since it was destroyed before its turn\n    expect(child2.renderSelfCalled).toBe(false)\n  })\n\n  test(\"BUG: onLifecyclePass not called (registration issue)\", async () => {\n    let lifecyclePassCalled = false\n\n    const renderable = new TestRenderable(testRenderer, { id: \"test\" })\n    renderable.onLifecyclePass = () => {\n      lifecyclePassCalled = true\n    }\n\n    testRenderer.root.add(renderable)\n    await renderOnce()\n\n    // BUG: Lifecycle pass should be called but isn't\n    expect(lifecyclePassCalled).toBe(true)\n  })\n})\n\ndescribe(\"Destroy During Render - Working Cases (for documentation)\", () => {\n  test(\"WORKS: destroying self in renderAfter\", async () => {\n    const renderable = new TestRenderable(testRenderer, {\n      id: \"test\",\n      width: 100,\n      height: 100,\n      renderAfter: function () {\n        this.destroy()\n      },\n    })\n\n    testRenderer.root.add(renderable)\n    await renderOnce()\n\n    expect(renderable.isDestroyed).toBe(true)\n    // renderSelf was already called by this point, which is fine\n    expect(renderable.renderSelfCalled).toBe(true)\n  })\n\n  test(\"WORKS: destroying child in renderAfter\", async () => {\n    const child = new TestRenderable(testRenderer, {\n      id: \"child\",\n      width: 50,\n      height: 50,\n    })\n\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n      renderAfter: function () {\n        child.destroy()\n      },\n    })\n\n    parent.add(child)\n    testRenderer.root.add(parent)\n\n    await renderOnce()\n\n    expect(child.isDestroyed).toBe(true)\n    // Child already rendered before parent's renderAfter, which is expected\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/hover-cursor.test.ts",
    "content": "import { beforeEach, describe, expect, test, afterEach } from \"bun:test\"\nimport { createTestRenderer, MouseButtons, type MockMouse, type TestRenderer } from \"../testing\"\nimport { BoxRenderable } from \"../renderables\"\nimport type { MousePointerStyle } from \"../types\"\n\ndescribe(\"mouse pointer style\", () => {\n  let renderer: TestRenderer\n  let mockMouse: MockMouse\n  let renderOnce: () => Promise<void>\n\n  beforeEach(async () => {\n    ;({ renderer, mockMouse, renderOnce } = await createTestRenderer({ width: 40, height: 20 }))\n  })\n\n  afterEach(() => {\n    renderer.destroy()\n  })\n\n  test(\"setMousePointer sets style\", async () => {\n    renderer.setMousePointer(\"pointer\")\n    expect((renderer as any)._currentMousePointerStyle).toBe(\"pointer\")\n  })\n\n  test(\"setMousePointer with 'default' clears style\", async () => {\n    renderer.setMousePointer(\"pointer\")\n    renderer.setMousePointer(\"default\")\n    expect((renderer as any)._currentMousePointerStyle).toBe(\"default\")\n  })\n\n  test(\"setMousePointer supports all style types\", async () => {\n    const styles: MousePointerStyle[] = [\"default\", \"pointer\", \"text\", \"crosshair\", \"move\", \"not-allowed\"]\n    for (const style of styles) {\n      renderer.setMousePointer(style)\n      expect((renderer as any)._currentMousePointerStyle).toBe(style)\n    }\n  })\n\n  test(\"onMouseOver callback can set mouse pointer\", async () => {\n    let pointerSet = false\n    const box = new BoxRenderable(renderer, {\n      position: \"absolute\",\n      left: 5,\n      top: 5,\n      width: 10,\n      height: 5,\n      onMouseOver() {\n        this.ctx.setMousePointer(\"pointer\")\n        pointerSet = true\n      },\n    })\n    renderer.root.add(box)\n    await renderOnce()\n\n    await mockMouse.moveTo(10, 7)\n    await renderOnce()\n\n    expect(pointerSet).toBe(true)\n    expect((renderer as any)._currentMousePointerStyle).toBe(\"pointer\")\n  })\n\n  test(\"onMouseOut callback can reset mouse pointer\", async () => {\n    let pointerReset = false\n    const box = new BoxRenderable(renderer, {\n      position: \"absolute\",\n      left: 5,\n      top: 5,\n      width: 10,\n      height: 5,\n      onMouseOver() {\n        this.ctx.setMousePointer(\"pointer\")\n      },\n      onMouseOut() {\n        this.ctx.setMousePointer(\"default\")\n        pointerReset = true\n      },\n    })\n    renderer.root.add(box)\n    await renderOnce()\n\n    // Move into box\n    await mockMouse.moveTo(10, 7)\n    await renderOnce()\n    expect((renderer as any)._currentMousePointerStyle).toBe(\"pointer\")\n\n    // Move out of box\n    await mockMouse.moveTo(1, 1)\n    await renderOnce()\n\n    expect(pointerReset).toBe(true)\n    expect((renderer as any)._currentMousePointerStyle).toBe(\"default\")\n  })\n\n  test(\"pointer resets on renderer destroy\", async () => {\n    renderer.setMousePointer(\"pointer\")\n    renderer.destroy()\n    // After destroy, the reset is called internally - just verify no error\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/native-span-feed-async.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { NativeSpanFeed } from \"../NativeSpanFeed\"\nimport { resolveRenderLib } from \"../zig\"\n\nconst lib = resolveRenderLib()\n\nfunction writeAndCommit(stream: NativeSpanFeed, data: Uint8Array): void {\n  lib.streamWrite(stream.streamPtr, data)\n  lib.streamCommit(stream.streamPtr)\n}\n\ntest(\"async handler keeps chunk pinned until Promise resolves\", async () => {\n  // Single chunk forces reuse; async handlers must keep data pinned.\n  const stream = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n\n  let resolveHandler!: () => void\n  const handlerDone = new Promise<void>((r) => {\n    resolveHandler = r\n  })\n\n  let capturedData: Uint8Array | null = null\n  let dataValidAtResolve = false\n\n  stream.onData(async (data) => {\n    capturedData = data\n    const originalBytes = new Uint8Array(data)\n    await handlerDone\n    dataValidAtResolve = capturedData.every((b, i) => b === originalBytes[i])\n  })\n  const original = new Uint8Array(64)\n  for (let i = 0; i < 64; i++) original[i] = i\n  writeAndCommit(stream, original)\n  const overwrite = new Uint8Array(64).fill(0xff)\n  writeAndCommit(stream, overwrite)\n  stream.drainAll()\n  resolveHandler()\n  await new Promise((r) => setTimeout(r, 10))\n\n  expect(capturedData).not.toBeNull()\n  expect(dataValidAtResolve).toBe(true)\n\n  stream.close()\n})\n\ntest(\"mixed sync and async handlers on same stream\", async () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const syncReceived: string[] = []\n  let asyncReceived: string[] = []\n  let resolveAsync!: () => void\n  const asyncDone = new Promise<void>((r) => {\n    resolveAsync = r\n  })\n\n  stream.onData((data) => {\n    syncReceived.push(new TextDecoder().decode(data))\n  })\n  stream.onData(async (data) => {\n    const text = new TextDecoder().decode(data)\n    await asyncDone\n    asyncReceived.push(text)\n  })\n\n  const msg = new TextEncoder().encode(\"hello\")\n  writeAndCommit(stream, msg)\n  stream.drainAll()\n\n  expect(syncReceived).toEqual([\"hello\"])\n  expect(asyncReceived).toEqual([])\n  resolveAsync()\n  await new Promise((r) => setTimeout(r, 10))\n\n  expect(asyncReceived).toEqual([\"hello\"])\n\n  stream.close()\n})\n\ntest(\"async handler rejection still decrements refcount\", async () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n\n  stream.onData(async () => {\n    throw new Error(\"async failure\")\n  })\n\n  const data = new Uint8Array(64).fill(0xaa)\n  writeAndCommit(stream, data)\n  stream.drainAll()\n\n  await new Promise((r) => setTimeout(r, 10))\n  const received: Uint8Array[] = []\n  stream.onData((d) => {\n    received.push(new Uint8Array(d))\n  })\n\n  const data2 = new Uint8Array(64).fill(0xbb)\n  writeAndCommit(stream, data2)\n  stream.drainAll()\n\n  expect(received.length).toBe(1)\n  expect(received[0][0]).toBe(0xbb)\n\n  stream.close()\n})\n\ntest(\"sync-only handlers decrement refcount immediately (no regression)\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n\n  const received: string[] = []\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n  })\n\n  const msg1 = new TextEncoder().encode(\"A\".repeat(64))\n  writeAndCommit(stream, msg1)\n  stream.drainAll()\n  const msg2 = new TextEncoder().encode(\"B\".repeat(64))\n  writeAndCommit(stream, msg2)\n  stream.drainAll()\n\n  expect(received.length).toBe(2)\n  expect(received[1]).toBe(\"B\".repeat(64))\n\n  stream.close()\n})\n\ntest(\"multiple async handlers all settle before refcount decrement\", async () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n\n  let resolve1!: () => void\n  let resolve2!: () => void\n  const done1 = new Promise<void>((r) => {\n    resolve1 = r\n  })\n  const done2 = new Promise<void>((r) => {\n    resolve2 = r\n  })\n\n  const order: string[] = []\n\n  stream.onData(async (_data) => {\n    await done1\n    order.push(\"handler1\")\n  })\n\n  stream.onData(async (_data) => {\n    await done2\n    order.push(\"handler2\")\n  })\n\n  const data = new Uint8Array(64).fill(0xcc)\n  writeAndCommit(stream, data)\n  stream.drainAll()\n\n  resolve1()\n  await new Promise((r) => setTimeout(r, 10))\n  resolve2()\n  await new Promise((r) => setTimeout(r, 10))\n\n  expect(order).toEqual([\"handler1\", \"handler2\"])\n\n  const received: number[] = []\n  stream.onData((d) => {\n    received.push(d[0])\n  })\n\n  const data2 = new Uint8Array(64).fill(0xdd)\n  writeAndCommit(stream, data2)\n  stream.drainAll()\n\n  expect(received).toContain(0xdd)\n\n  stream.close()\n})\n"
  },
  {
    "path": "packages/core/src/tests/native-span-feed-close.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { NativeSpanFeed } from \"../NativeSpanFeed\"\nimport { resolveRenderLib } from \"../zig\"\n\nconst lib = resolveRenderLib()\n\nfunction nextTick(): Promise<void> {\n  // Use a timer turn instead of process.nextTick so Promise/microtask work\n  // from async handlers and close deferral can settle before assertions.\n  return new Promise((resolve) => setTimeout(resolve, 0))\n}\n\nconst enum EventId {\n  Closed = 5,\n}\n\ntest(\"streamClose emits Closed once\", () => {\n  const events: number[] = []\n\n  const streamPtr = lib.createNativeSpanFeed(null)\n  expect(streamPtr).not.toBe(0)\n  expect(streamPtr).not.toBeNull()\n  lib.registerNativeSpanFeedStream(streamPtr!, (eventId) => {\n    events.push(Number(eventId))\n  })\n  expect(lib.attachNativeSpanFeed(streamPtr!)).toBe(0)\n\n  expect(lib.streamClose(streamPtr!)).toBe(0)\n  expect(lib.streamClose(streamPtr!)).toBe(0)\n  lib.unregisterNativeSpanFeedStream(streamPtr!)\n  lib.destroyNativeSpanFeed(streamPtr!)\n\n  const closedEvents = events.filter((id) => id === EventId.Closed).length\n  expect(closedEvents).toBe(1)\n})\n\ntest(\"destroyNativeSpanFeed emits Closed when needed\", () => {\n  const events: number[] = []\n\n  const streamPtr = lib.createNativeSpanFeed(null)\n  expect(streamPtr).not.toBe(0)\n  expect(streamPtr).not.toBeNull()\n  lib.registerNativeSpanFeedStream(streamPtr!, (eventId) => {\n    events.push(Number(eventId))\n  })\n  expect(lib.attachNativeSpanFeed(streamPtr!)).toBe(0)\n  lib.destroyNativeSpanFeed(streamPtr!)\n\n  const closedEvents = events.filter((id) => id === EventId.Closed).length\n  expect(closedEvents).toBe(1)\n})\n\ntest(\"close should not destroy immediately while async handler is still pending\", async () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n  const ptr = stream.streamPtr\n\n  let release!: () => void\n  const gate = new Promise<void>((resolve) => {\n    release = resolve\n  })\n\n  let handlerStarted = false\n  let handlerSettled = false\n\n  stream.onData(async (_data) => {\n    handlerStarted = true\n    await gate\n    handlerSettled = true\n  })\n\n  const payload = new Uint8Array(64).fill(0xaa)\n  lib.streamWrite(ptr, payload)\n  lib.streamCommit(ptr)\n  stream.drainAll()\n\n  expect(handlerStarted).toBe(true)\n\n  stream.close()\n\n  const destroyedImmediately = (stream as any).destroyed === true\n\n  release()\n  await nextTick()\n\n  expect(handlerSettled).toBe(true)\n\n  try {\n    expect(destroyedImmediately).toBe(false)\n  } finally {\n    if (!(stream as any).destroyed) {\n      lib.destroyNativeSpanFeed(ptr)\n    }\n  }\n})\n\ntest(\"close should not destroy when native close reports Busy\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1, autoCommitOnFull: false })\n  const ptr = stream.streamPtr\n\n  const reserve = lib.streamReserve(ptr, 1)\n  expect(reserve.status).toBe(0)\n\n  try {\n    stream.close()\n  } catch {\n    // If close starts throwing on Busy, that's acceptable for this assertion.\n  }\n\n  const destroyedAfterBusyClose = (stream as any).destroyed === true\n\n  try {\n    expect(destroyedAfterBusyClose).toBe(false)\n  } finally {\n    if (!(stream as any).destroyed) {\n      lib.streamCommitReserved(ptr, 0)\n      lib.streamClose(ptr)\n      lib.destroyNativeSpanFeed(ptr)\n    }\n  }\n})\n"
  },
  {
    "path": "packages/core/src/tests/native-span-feed-coverage.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { NativeSpanFeed } from \"../NativeSpanFeed\"\nimport { resolveRenderLib } from \"../zig\"\n\nconst lib = resolveRenderLib()\n\nfunction writeData(stream: NativeSpanFeed, text: string): void {\n  const data = new TextEncoder().encode(text)\n  lib.streamWrite(stream.streamPtr, data)\n}\n\nfunction commitData(stream: NativeSpanFeed): void {\n  lib.streamCommit(stream.streamPtr)\n}\n\nfunction produceData(stream: NativeSpanFeed, text: string): void {\n  writeData(stream, text)\n  commitData(stream)\n}\n\ntest(\"attach replays ChunkAdded and receives subsequent data\", () => {\n  // Pre-queued spans should not drain until an onData handler exists.\n  // Create+close a dummy stream so ensureCallback() runs.\n  const dummy = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n  dummy.close()\n\n  const rawPtr = lib.createNativeSpanFeed({\n    chunkSize: 256,\n    initialChunks: 1,\n    autoCommitOnFull: true,\n  })\n  expect(rawPtr).not.toBe(0)\n  expect(rawPtr).not.toBeNull()\n\n  const msg1 = new TextEncoder().encode(\"pre-queued-1\")\n  lib.streamWrite(rawPtr!, msg1)\n  lib.streamCommit(rawPtr!)\n  const stream = NativeSpanFeed.attach(rawPtr!)\n\n  const received: string[] = []\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n  })\n\n  stream.drainAll()\n  expect(received).toEqual([\"pre-queued-1\"])\n  const post1 = new TextEncoder().encode(\"post-attach-1\")\n  lib.streamWrite(rawPtr!, post1)\n  lib.streamCommit(rawPtr!)\n  stream.drainAll()\n\n  expect(received).toEqual([\"pre-queued-1\", \"post-attach-1\"])\n\n  const post2 = new TextEncoder().encode(\"post-attach-2\")\n  lib.streamWrite(rawPtr!, post2)\n  lib.streamCommit(rawPtr!)\n  stream.drainAll()\n\n  expect(received).toEqual([\"pre-queued-1\", \"post-attach-1\", \"post-attach-2\"])\n\n  stream.close()\n})\n\ntest(\"multiple concurrent streams operate independently\", () => {\n  const streamA = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n  const streamB = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const receivedA: string[] = []\n  const receivedB: string[] = []\n\n  streamA.onData((data) => {\n    receivedA.push(new TextDecoder().decode(data))\n  })\n  streamB.onData((data) => {\n    receivedB.push(new TextDecoder().decode(data))\n  })\n\n  produceData(streamA, \"alpha\")\n  streamA.drainAll()\n  produceData(streamB, \"beta\")\n  streamB.drainAll()\n  produceData(streamA, \"gamma\")\n  produceData(streamB, \"delta\")\n  streamA.drainAll()\n  streamB.drainAll()\n\n  expect(receivedA).toEqual([\"alpha\", \"gamma\"])\n  expect(receivedB).toEqual([\"beta\", \"delta\"])\n  streamA.close()\n\n  produceData(streamB, \"epsilon\")\n  streamB.drainAll()\n\n  expect(receivedB).toEqual([\"beta\", \"delta\", \"epsilon\"])\n  expect(receivedA).toEqual([\"alpha\", \"gamma\"])\n\n  streamB.close()\n})\n\ntest(\"onError handler fires when Error event is received\", () => {\n  // Zig doesn't emit Error yet; this verifies the handler plumbing.\n\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const errors1: number[] = []\n  const errors2: number[] = []\n\n  const unsub1 = stream.onError((code) => {\n    errors1.push(code)\n  })\n  const unsub2 = stream.onError((code) => {\n    errors2.push(code)\n  })\n\n  expect(typeof unsub1).toBe(\"function\")\n  expect(typeof unsub2).toBe(\"function\")\n  unsub1()\n  const received: string[] = []\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n  })\n\n  produceData(stream, \"hello\")\n  stream.drainAll()\n  expect(received).toContain(\"hello\")\n\n  unsub2()\n  stream.close()\n})\n\ntest(\"handler calling close() during drain does not crash\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const received: string[] = []\n  let closeCalled = false\n\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n    if (!closeCalled) {\n      closeCalled = true\n      stream.close()\n    }\n  })\n\n  stream.onData((data) => {\n    received.push(\"B:\" + new TextDecoder().decode(data))\n  })\n\n  produceData(stream, \"trigger\")\n  expect(received).toContain(\"trigger\")\n  stream.drainAll()\n})\n\ntest(\"handler calling close() during drain silently drops remaining spans\", () => {\n  // After close, dropping remaining spans is safe because the stream is destroyed.\n\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const received: string[] = []\n  let closeCalled = false\n\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n    if (!closeCalled && received.length >= 2) {\n      closeCalled = true\n      stream.close()\n    }\n  })\n\n  for (let i = 0; i < 5; i++) {\n    const msg = new TextEncoder().encode(`msg${i}`)\n    lib.streamWrite(stream.streamPtr, msg)\n    lib.streamCommit(stream.streamPtr)\n  }\n\n  stream.drainAll()\n  expect(received).toEqual([\"msg0\", \"msg1\"])\n  stream.drainAll()\n  expect(received).toEqual([\"msg0\", \"msg1\"])\n})\ntest(\"draining more than 256 spans works correctly\", () => {\n  // drainBuffer holds 256 spans, so drainAll must loop.\n  const stream = NativeSpanFeed.create({ chunkSize: 4096, initialChunks: 1 })\n\n  const received: Uint8Array[] = []\n  stream.onData((data) => {\n    received.push(new Uint8Array(data))\n  })\n\n  const totalSpans = 400\n\n  for (let i = 0; i < totalSpans; i++) {\n    const byte = new Uint8Array([i & 0xff])\n    lib.streamWrite(stream.streamPtr, byte)\n    lib.streamCommit(stream.streamPtr)\n  }\n\n  stream.drainAll()\n  expect(received.length).toBe(totalSpans)\n  for (let i = 0; i < totalSpans; i++) {\n    expect(received[i].length).toBe(1)\n    expect(received[i][0]).toBe(i & 0xff)\n  }\n\n  stream.close()\n})\n\ntest(\"draining exactly 256 spans works correctly\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 4096, initialChunks: 1 })\n\n  let totalBytes = 0\n  stream.onData((data) => {\n    totalBytes += data.byteLength\n  })\n\n  for (let i = 0; i < 256; i++) {\n    const byte = new Uint8Array([0xaa])\n    lib.streamWrite(stream.streamPtr, byte)\n    lib.streamCommit(stream.streamPtr)\n  }\n\n  stream.drainAll()\n\n  expect(totalBytes).toBe(256)\n\n  stream.close()\n})\n"
  },
  {
    "path": "packages/core/src/tests/native-span-feed-edge-cases.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { NativeSpanFeed } from \"../NativeSpanFeed\"\nimport { resolveRenderLib } from \"../zig\"\n\nconst lib = resolveRenderLib()\n\nfunction writeData(stream: NativeSpanFeed, text: string): void {\n  const data = new TextEncoder().encode(text)\n  lib.streamWrite(stream.streamPtr, data)\n}\n\nfunction commitData(stream: NativeSpanFeed): void {\n  lib.streamCommit(stream.streamPtr)\n}\n\nfunction produceData(stream: NativeSpanFeed, text: string): void {\n  writeData(stream, text)\n  commitData(stream)\n}\n\ntest(\"throwing handler does not prevent state buffer decrements\", () => {\n  // Decrement must happen even if a handler throws.\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const received: string[] = []\n  let shouldThrow = true\n\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n    if (shouldThrow) {\n      throw new Error(\"handler error\")\n    }\n  })\n\n  try {\n    produceData(stream, \"first\")\n  } catch {}\n\n  try {\n    produceData(stream, \"second\")\n  } catch {}\n\n  expect(received).toContain(\"first\")\n  expect(received).toContain(\"second\")\n  shouldThrow = false\n  produceData(stream, \"third\")\n\n  try {\n    stream.drainAll()\n  } catch {}\n\n  expect(received).toContain(\"third\")\n\n  stream.close()\n})\n\ntest(\"attach with pre-queued data waits for onData\", () => {\n  const bootstrap = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n  bootstrap.close()\n  const rawPtr = lib.createNativeSpanFeed({\n    chunkSize: 128,\n    initialChunks: 1,\n    autoCommitOnFull: true,\n  })\n  expect(rawPtr).toBeTruthy()\n\n  const pre = new TextEncoder().encode(\"pre-attach\")\n  lib.streamWrite(rawPtr!, pre)\n  lib.streamCommit(rawPtr!)\n\n  const stream = NativeSpanFeed.attach(rawPtr!)\n\n  const received: string[] = []\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n  })\n\n  stream.drainAll()\n  expect(received).toEqual([\"pre-attach\"])\n\n  produceData(stream, \"post-attach\")\n  stream.drainAll()\n  expect(received).toEqual([\"pre-attach\", \"post-attach\"])\n\n  stream.close()\n})\n\ntest(\"decrementRefcount with out-of-bounds chunkIndex does not crash or corrupt\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n\n  const received: string[] = []\n  let closedOnSpan = -1\n\n  stream.onData((data) => {\n    const text = new TextDecoder().decode(data)\n    received.push(text)\n    if (closedOnSpan < 0) {\n      closedOnSpan = 0\n      // Force an empty state buffer to exercise the guard.\n      ;(stream as any).stateBuffer = new Uint8Array(0)\n    }\n  })\n  for (let i = 0; i < 5; i++) {\n    const msg = new TextEncoder().encode(`s${i}`)\n    lib.streamWrite(stream.streamPtr, msg)\n    lib.streamCommit(stream.streamPtr)\n  }\n\n  stream.drainAll()\n  expect(received).toEqual([\"s0\", \"s1\", \"s2\", \"s3\", \"s4\"])\n\n  stream.close()\n})\n\ntest(\"toArrayBuffer aliases Zig-owned chunk memory\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const received: Uint8Array[] = []\n  stream.onData((data) => {\n    received.push(data)\n  })\n\n  produceData(stream, \"hello\")\n  stream.drainAll()\n\n  expect(received.length).toBe(1)\n  const view = received[0]\n\n  const original = view[0]\n  const sentinel = original ^ 0xff\n  view[0] = sentinel\n  expect(view[0]).toBe(sentinel)\n  // Restore original value so the chunk data isn't corrupted.\n  view[0] = original\n\n  stream.close()\n})\n\ntest(\"state buffer view stays current across chunk growth\", () => {\n  // StateBuffer events must keep the TS view in sync after growth.\n  const stream = NativeSpanFeed.create({\n    chunkSize: 32,\n    initialChunks: 1,\n  })\n\n  const allData: string[] = []\n  stream.onData((data) => {\n    allData.push(new TextDecoder().decode(data))\n  })\n\n  for (let i = 0; i < 20; i++) {\n    const msg = new TextEncoder().encode(`msg${i.toString().padStart(2, \"0\")}`)\n    lib.streamWrite(stream.streamPtr, msg)\n    lib.streamCommit(stream.streamPtr)\n  }\n\n  stream.drainAll()\n\n  const allContent = allData.join(\"\")\n  for (let i = 0; i < 20; i++) {\n    const expected = `msg${i.toString().padStart(2, \"0\")}`\n    expect(allContent).toContain(expected)\n  }\n  expect(allContent.length).toBe(20 * 5)\n\n  stream.close()\n})\n\ntest(\"state buffer view stays current when writes span multiple chunks\", () => {\n  const chunkSize = 32\n  const stream = NativeSpanFeed.create({ chunkSize, initialChunks: 1 })\n\n  const allData: Uint8Array[] = []\n  stream.onData((data) => {\n    allData.push(new Uint8Array(data)) // copy to avoid aliasing\n  })\n\n  const bigWrite = new Uint8Array(256)\n  for (let i = 0; i < 256; i++) bigWrite[i] = i & 0xff\n  lib.streamWrite(stream.streamPtr, bigWrite)\n\n  lib.streamCommit(stream.streamPtr)\n  stream.drainAll()\n  const received = new Uint8Array(allData.reduce((sum, d) => sum + d.length, 0))\n  let offset = 0\n  for (const chunk of allData) {\n    received.set(chunk, offset)\n    offset += chunk.length\n  }\n\n  expect(received.length).toBe(256)\n  for (let i = 0; i < 256; i++) {\n    expect(received[i]).toBe(i & 0xff)\n  }\n\n  stream.close()\n})\n\ntest(\"unsubscribing self during onData iteration does not affect other handlers\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const calls: string[] = []\n\n  const unsubA = stream.onData(() => {\n    calls.push(\"A\")\n    unsubA()\n  })\n  stream.onData(() => {\n    calls.push(\"B\")\n  })\n\n  produceData(stream, \"msg1\")\n  stream.drainAll()\n\n  expect(calls).toEqual([\"A\", \"B\"])\n  produceData(stream, \"msg2\")\n  stream.drainAll()\n\n  expect(calls).toEqual([\"A\", \"B\", \"B\"])\n\n  stream.close()\n})\n\ntest(\"unsubscribing a later handler during iteration skips it\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const calls: string[] = []\n  let unsubB: (() => void) | null = null\n\n  stream.onData(() => {\n    calls.push(\"A\")\n    if (unsubB) {\n      unsubB()\n      unsubB = null\n    }\n  })\n\n  unsubB = stream.onData(() => {\n    calls.push(\"B\")\n  })\n\n  produceData(stream, \"msg1\")\n  stream.drainAll()\n\n  expect(calls).toEqual([\"A\"])\n  produceData(stream, \"msg2\")\n  stream.drainAll()\n\n  expect(calls).toEqual([\"A\", \"A\"])\n\n  stream.close()\n})\n\ntest(\"handler adding a new handler during iteration includes it per Set semantics\", () => {\n  // Set iteration visits handlers added before they're reached.\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const calls: string[] = []\n\n  let added = false\n  stream.onData(() => {\n    calls.push(\"A\")\n    if (!added) {\n      added = true\n      stream.onData(() => {\n        calls.push(\"B\")\n      })\n    }\n  })\n\n  produceData(stream, \"msg1\")\n  stream.drainAll()\n\n  expect(calls).toEqual([\"A\", \"B\"])\n  produceData(stream, \"msg2\")\n  stream.drainAll()\n\n  expect(calls).toEqual([\"A\", \"B\", \"A\", \"B\"])\n\n  stream.close()\n})\n\ntest(\"throwing handler does not skip remaining handlers for the same span\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const calls: string[] = []\n\n  stream.onData(() => {\n    calls.push(\"A\")\n    throw new Error(\"handler A error\")\n  })\n  stream.onData((data) => {\n    calls.push(\"B:\" + new TextDecoder().decode(data))\n  })\n\n  try {\n    produceData(stream, \"msg1\")\n    stream.drainAll()\n  } catch {}\n  expect(calls).toEqual([\"A\", \"B:msg1\"])\n\n  stream.close()\n})\n\ntest(\"reentrant drainAll from handler is safely ignored\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const received: string[] = []\n  let reentrantCallCount = 0\n\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n    reentrantCallCount++\n    stream.drainAll()\n  })\n\n  produceData(stream, \"msg1\")\n  produceData(stream, \"msg2\")\n  stream.drainAll()\n\n  expect(received).toContain(\"msg1\")\n  expect(received).toContain(\"msg2\")\n  expect(received.length).toBe(2)\n\n  stream.close()\n})\n\ntest(\"committing during drain does not drop pending spans\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 64, initialChunks: 1 })\n\n  const received: string[] = []\n  let injected = false\n\n  stream.onData((data) => {\n    received.push(new TextDecoder().decode(data))\n    if (!injected) {\n      injected = true\n      const next = new TextEncoder().encode(\"inner\")\n      lib.streamWrite(stream.streamPtr, next)\n      lib.streamCommit(stream.streamPtr)\n    }\n  })\n\n  const first = new TextEncoder().encode(\"outer\")\n  lib.streamWrite(stream.streamPtr, first)\n  lib.streamCommit(stream.streamPtr)\n  stream.drainAll()\n\n  expect(received).toEqual([\"outer\", \"inner\"])\n\n  stream.close()\n})\n"
  },
  {
    "path": "packages/core/src/tests/native-span-feed-use-after-free.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { NativeSpanFeed } from \"../NativeSpanFeed\"\nimport { resolveRenderLib } from \"../zig\"\n\nconst lib = resolveRenderLib()\n\ntest(\"close clears chunkMap and internal state\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  const retained: Uint8Array[] = []\n  stream.onData((data) => {\n    retained.push(data)\n  })\n\n  const data = new TextEncoder().encode(\"hello world\")\n  lib.streamWrite(stream.streamPtr, data)\n  lib.streamCommit(stream.streamPtr)\n  stream.drainAll()\n\n  expect(retained.length).toBe(1)\n  expect(new TextDecoder().decode(retained[0])).toBe(\"hello world\")\n\n  stream.close()\n  stream.drainAll()\n  expect(retained.length).toBe(1)\n})\n\ntest(\"onData handlers are cleared on close\", () => {\n  const stream = NativeSpanFeed.create({ chunkSize: 256, initialChunks: 1 })\n\n  let callCount = 0\n  stream.onData(() => {\n    callCount++\n  })\n\n  const data = new TextEncoder().encode(\"before close\")\n  lib.streamWrite(stream.streamPtr, data)\n  lib.streamCommit(stream.streamPtr)\n  stream.drainAll()\n  expect(callCount).toBe(1)\n\n  stream.close()\n\n  expect(callCount).toBe(1)\n})\n"
  },
  {
    "path": "packages/core/src/tests/opacity.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe } from \"bun:test\"\nimport { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport type { RenderContext } from \"../types.js\"\n\nclass TestRenderable extends Renderable {\n  constructor(ctx: RenderContext, options: RenderableOptions) {\n    super(ctx, options)\n  }\n}\n\nlet testRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\n\nbeforeEach(async () => {\n  ;({ renderer: testRenderer, renderOnce } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ndescribe(\"Renderable - Opacity\", () => {\n  test(\"defaults to 1.0\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-opacity\" })\n    expect(renderable.opacity).toBe(1.0)\n  })\n\n  test(\"accepts opacity in constructor options\", () => {\n    const renderable = new TestRenderable(testRenderer, {\n      id: \"test-opacity-options\",\n      opacity: 0.5,\n    })\n    expect(renderable.opacity).toBe(0.5)\n  })\n\n  test(\"clamps opacity to 0-1 range via setter\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-clamp\" })\n\n    renderable.opacity = 1.5\n    expect(renderable.opacity).toBe(1.0)\n\n    renderable.opacity = -0.5\n    expect(renderable.opacity).toBe(0.0)\n\n    renderable.opacity = 0.7\n    expect(renderable.opacity).toBe(0.7)\n  })\n\n  test(\"clamps opacity from constructor options\", () => {\n    const r1 = new TestRenderable(testRenderer, {\n      id: \"test-clamp-high\",\n      opacity: 2.0,\n    })\n    expect(r1.opacity).toBe(1.0)\n\n    const r2 = new TestRenderable(testRenderer, {\n      id: \"test-clamp-low\",\n      opacity: -1,\n    })\n    expect(r2.opacity).toBe(0.0)\n  })\n\n  test(\"handles opacity of 0 (fully transparent)\", async () => {\n    const renderable = new TestRenderable(testRenderer, {\n      id: \"invisible\",\n      width: 10,\n      height: 5,\n      opacity: 0,\n    })\n    testRenderer.root.add(renderable)\n\n    expect(renderable.opacity).toBe(0)\n\n    // Render should not crash with zero opacity\n    await renderOnce()\n  })\n\n  test(\"nested renderables maintain independent opacity values\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 20,\n      height: 10,\n      opacity: 0.5,\n    })\n\n    const child = new TestRenderable(testRenderer, {\n      id: \"child\",\n      width: 10,\n      height: 5,\n      opacity: 0.8,\n    })\n\n    parent.add(child)\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    expect(parent.opacity).toBe(0.5)\n    expect(child.opacity).toBe(0.8)\n  })\n\n  test(\"opacity changes trigger render request\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-render\" })\n    testRenderer.root.add(renderable)\n\n    const initialOpacity = renderable.opacity\n    renderable.opacity = 0.3\n\n    expect(renderable.opacity).not.toBe(initialOpacity)\n    expect(renderable.opacity).toBe(0.3)\n  })\n\n  test(\"setting same opacity value does not update\", () => {\n    const renderable = new TestRenderable(testRenderer, {\n      id: \"test-same\",\n      opacity: 0.5,\n    })\n\n    // Set to same value - should not trigger change\n    renderable.opacity = 0.5\n    expect(renderable.opacity).toBe(0.5)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderable.snapshot.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { BoxRenderable } from \"../renderables/Box.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\nlet testRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet captureFrame: () => string\n\nbeforeEach(async () => {\n  ;({\n    renderer: testRenderer,\n    renderOnce,\n    captureCharFrame: captureFrame,\n  } = await createTestRenderer({\n    width: 10,\n    height: 5,\n  }))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ndescribe(\"Renderable - insertBefore\", () => {\n  test(\"reproduces insertBefore behavior with state change after timeout\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 5,\n    })\n\n    const bananaText = new TextRenderable(testRenderer, {\n      id: \"banana\",\n      content: \"banana\",\n    })\n\n    const appleText = new TextRenderable(testRenderer, {\n      id: \"apple\",\n      content: \"apple\",\n    })\n\n    const pearText = new TextRenderable(testRenderer, {\n      id: \"pear\",\n      content: \"pear\",\n    })\n\n    const separator = new BoxRenderable(testRenderer, {\n      id: \"separator\",\n      width: 20,\n      height: 1,\n    })\n\n    container.add(bananaText)\n    container.add(appleText)\n    container.add(pearText)\n    container.add(separator)\n\n    testRenderer.root.add(container)\n    await renderOnce()\n\n    const initialFrame = captureFrame()\n    expect(initialFrame).toMatchSnapshot(\"insertBefore initial state\")\n\n    await new Promise((resolve) => setTimeout(resolve, 100))\n\n    container.insertBefore(appleText, separator)\n\n    await renderOnce()\n\n    const reorderedFrame = captureFrame()\n    expect(reorderedFrame).toMatchSnapshot(\"insertBefore reordered state\")\n  })\n\n  test(\"ensure .add with index works correctly\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 20,\n      height: 10,\n    })\n\n    // Create 5 text renderables in order\n    const items = [\n      new TextRenderable(testRenderer, { id: \"order-1\", content: \"First\" }),\n      new TextRenderable(testRenderer, { id: \"order-2\", content: \"Second\" }),\n      new TextRenderable(testRenderer, { id: \"order-3\", content: \"Third\" }),\n      new TextRenderable(testRenderer, { id: \"order-4\", content: \"Fourth\" }),\n      new TextRenderable(testRenderer, { id: \"order-5\", content: \"Fifth\" }),\n    ]\n\n    // Add items in initial order [1, 2, 3, 4, 5]\n    for (const item of items) {\n      container.add(item)\n    }\n\n    testRenderer.root.add(container)\n    await renderOnce()\n\n    let children = container.getChildren()\n\n    expect(children.length).toBe(5)\n    expect(children[0]?.id).toBe(\"order-1\")\n    expect(children[1]?.id).toBe(\"order-2\")\n    expect(children[2]?.id).toBe(\"order-3\")\n    expect(children[3]?.id).toBe(\"order-4\")\n    expect(children[4]?.id).toBe(\"order-5\")\n\n    // Reproduce the EXACT sequence from SolidJS reconciler output:\n    container.add(items[4]!, 1) // order-5 at index 1\n    container.add(items[0]!) // order-1 at index undefined\n    container.add(items[3]!, 2) // order-4 at index 2\n    container.add(items[1]!, 4) // order-2 at index 4\n\n    await renderOnce()\n\n    children = container.getChildren()\n\n    // Expected: [5, 4, 3, 2, 1]\n    expect(children.length).toBe(5)\n    expect(children[0]?.id).toBe(\"order-5\")\n    expect(children[1]?.id).toBe(\"order-4\")\n    expect(children[2]?.id).toBe(\"order-3\")\n    expect(children[3]?.id).toBe(\"order-2\")\n    expect(children[4]?.id).toBe(\"order-1\")\n  })\n})\n\ndescribe(\"Renderable - add method\", () => {\n  test(\"basic add appends to end\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item2)\n    container.add(item3)\n\n    const children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-1\")\n    expect(children[1]?.id).toBe(\"item-2\")\n    expect(children[2]?.id).toBe(\"item-3\")\n  })\n\n  test(\"add with index 0 inserts at beginning\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item2)\n    container.add(item3, 0) // Insert at beginning\n\n    const children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-3\")\n    expect(children[1]?.id).toBe(\"item-1\")\n    expect(children[2]?.id).toBe(\"item-2\")\n  })\n\n  test(\"add with middle index inserts correctly\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item2)\n    container.add(item3, 1) // Insert in middle\n\n    const children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-1\")\n    expect(children[1]?.id).toBe(\"item-3\")\n    expect(children[2]?.id).toBe(\"item-2\")\n  })\n\n  test(\"add with large index appends to end\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item2)\n    container.add(item3, 999) // Out of bounds index\n\n    const children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-1\")\n    expect(children[1]?.id).toBe(\"item-2\")\n    expect(children[2]?.id).toBe(\"item-3\")\n  })\n\n  test(\"add returns correct index\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    const idx1 = container.add(item1)\n    const idx2 = container.add(item2)\n    const idx3 = container.add(item3, 1)\n\n    expect(idx1).toBe(0)\n    expect(idx2).toBe(1)\n    expect(idx3).toBe(1) // Inserted at index 1\n  })\n\n  test(\"add null/undefined returns -1\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const idx1 = container.add(null as any)\n    const idx2 = container.add(undefined as any)\n\n    expect(idx1).toBe(-1)\n    expect(idx2).toBe(-1)\n    expect(container.getChildrenCount()).toBe(0)\n  })\n\n  test(\"re-adding existing child moves it\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item2)\n    container.add(item3)\n\n    // Re-add item1 to end\n    container.add(item1)\n\n    let children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-2\")\n    expect(children[1]?.id).toBe(\"item-3\")\n    expect(children[2]?.id).toBe(\"item-1\")\n\n    // Re-add item3 to beginning\n    container.add(item3, 0)\n\n    children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-3\")\n    expect(children[1]?.id).toBe(\"item-2\")\n    expect(children[2]?.id).toBe(\"item-1\")\n  })\n\n  test(\"adding child from another parent removes it from old parent\", async () => {\n    const container1 = new BoxRenderable(testRenderer, {\n      id: \"container-1\",\n      width: 10,\n      height: 10,\n    })\n\n    const container2 = new BoxRenderable(testRenderer, {\n      id: \"container-2\",\n      width: 10,\n      height: 10,\n    })\n\n    const item = new TextRenderable(testRenderer, { id: \"item\", content: \"A\" })\n\n    container1.add(item)\n    expect(container1.getChildrenCount()).toBe(1)\n    expect(item.parent).toBe(container1)\n\n    container2.add(item)\n    expect(container1.getChildrenCount()).toBe(0)\n    expect(container2.getChildrenCount()).toBe(1)\n    expect(item.parent).toBe(container2)\n  })\n})\n\ndescribe(\"Renderable - insertBefore method\", () => {\n  test(\"insertBefore with null anchor appends to end\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item2)\n    container.insertBefore(item3, null as any)\n\n    const children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[2]?.id).toBe(\"item-3\")\n  })\n\n  test(\"insertBefore inserts at correct position\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item3)\n    container.insertBefore(item2, item3) // Insert item2 before item3\n\n    const children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-1\")\n    expect(children[1]?.id).toBe(\"item-2\")\n    expect(children[2]?.id).toBe(\"item-3\")\n  })\n\n  test(\"insertBefore at beginning\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item2)\n    container.insertBefore(item3, item1) // Insert before first item\n\n    const children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-3\")\n    expect(children[1]?.id).toBe(\"item-1\")\n    expect(children[2]?.id).toBe(\"item-2\")\n  })\n\n  test(\"insertBefore moves existing child\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item2)\n    container.add(item3)\n\n    // Move item3 before item1\n    container.insertBefore(item3, item1)\n\n    let children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-3\")\n    expect(children[1]?.id).toBe(\"item-1\")\n    expect(children[2]?.id).toBe(\"item-2\")\n\n    // Move item1 before item2\n    container.insertBefore(item1, item2)\n\n    children = container.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0]?.id).toBe(\"item-3\")\n    expect(children[1]?.id).toBe(\"item-1\")\n    expect(children[2]?.id).toBe(\"item-2\")\n  })\n\n  test(\"insertBefore with invalid anchor returns -1\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const notAChild = new TextRenderable(testRenderer, { id: \"not-child\", content: \"X\" })\n\n    container.add(item1)\n\n    expect(container.insertBefore(item2, notAChild)).toBe(-1)\n  })\n\n  test(\"insertBefore returns correct index\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const item1 = new TextRenderable(testRenderer, { id: \"item-1\", content: \"A\" })\n    const item2 = new TextRenderable(testRenderer, { id: \"item-2\", content: \"B\" })\n    const item3 = new TextRenderable(testRenderer, { id: \"item-3\", content: \"C\" })\n\n    container.add(item1)\n    container.add(item3)\n\n    const idx = container.insertBefore(item2, item3)\n    expect(idx).toBe(1)\n  })\n\n  test(\"insertBefore with null object returns -1\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const anchor = new TextRenderable(testRenderer, { id: \"anchor\", content: \"A\" })\n    container.add(anchor)\n\n    const idx = container.insertBefore(null as any, anchor)\n    expect(idx).toBe(-1)\n  })\n\n  test(\"complex reordering scenario\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const items = [\n      new TextRenderable(testRenderer, { id: \"A\", content: \"A\" }),\n      new TextRenderable(testRenderer, { id: \"B\", content: \"B\" }),\n      new TextRenderable(testRenderer, { id: \"C\", content: \"C\" }),\n      new TextRenderable(testRenderer, { id: \"D\", content: \"D\" }),\n      new TextRenderable(testRenderer, { id: \"E\", content: \"E\" }),\n    ]\n\n    // Initial: A, B, C, D, E\n    items.forEach((item) => container.add(item))\n\n    let children = container.getChildren()\n    expect(children.map((c) => c.id)).toEqual([\"A\", \"B\", \"C\", \"D\", \"E\"])\n\n    // Move E before B: A, E, B, C, D\n    container.insertBefore(items[4]!, items[1]!)\n    children = container.getChildren()\n    expect(children.map((c) => c.id)).toEqual([\"A\", \"E\", \"B\", \"C\", \"D\"])\n\n    // Move A before D: E, B, C, A, D\n    container.insertBefore(items[0]!, items[3]!)\n    children = container.getChildren()\n    expect(children.map((c) => c.id)).toEqual([\"E\", \"B\", \"C\", \"A\", \"D\"])\n\n    // Move C before E: C, E, B, A, D\n    container.insertBefore(items[2]!, items[4]!)\n    children = container.getChildren()\n    expect(children.map((c) => c.id)).toEqual([\"C\", \"E\", \"B\", \"A\", \"D\"])\n  })\n\n  test(\"multiple sequential adds and inserts\", async () => {\n    const container = new BoxRenderable(testRenderer, {\n      id: \"container\",\n      width: 10,\n      height: 10,\n    })\n\n    const items = [\n      new TextRenderable(testRenderer, { id: \"1\", content: \"1\" }),\n      new TextRenderable(testRenderer, { id: \"2\", content: \"2\" }),\n      new TextRenderable(testRenderer, { id: \"3\", content: \"3\" }),\n      new TextRenderable(testRenderer, { id: \"4\", content: \"4\" }),\n    ]\n\n    container.add(items[0]!)\n    container.add(items[1]!)\n    expect(container.getChildren().map((c) => c.id)).toEqual([\"1\", \"2\"])\n\n    container.insertBefore(items[2]!, items[1]!)\n    expect(container.getChildren().map((c) => c.id)).toEqual([\"1\", \"3\", \"2\"])\n\n    container.add(items[3]!, 0)\n    expect(container.getChildren().map((c) => c.id)).toEqual([\"4\", \"1\", \"3\", \"2\"])\n\n    // Move \"2\" before \"4\"\n    container.insertBefore(items[1]!, items[3]!)\n    expect(container.getChildren().map((c) => c.id)).toEqual([\"2\", \"4\", \"1\", \"3\"])\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderable.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe, spyOn } from \"bun:test\"\nimport { decodePasteBytes } from \"../lib/paste\"\nimport {\n  Renderable,\n  BaseRenderable,\n  RootRenderable,\n  RenderableEvents,\n  type BaseRenderableOptions,\n  type RenderableOptions,\n} from \"../Renderable.js\"\nimport { createTestRenderer, type TestRenderer, type MockMouse, type MockInput } from \"../testing/test-renderer.js\"\nimport type { RenderContext } from \"../types.js\"\nimport { TextNodeRenderable } from \"../renderables/TextNode.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\nexport class TestBaseRenderable extends BaseRenderable {\n  constructor(options: BaseRenderableOptions) {\n    super(options)\n  }\n\n  add(obj: BaseRenderable | unknown, index?: number): number {\n    throw new Error(\"Method not implemented.\")\n  }\n  remove(id: string): void {\n    throw new Error(\"Method not implemented.\")\n  }\n  insertBefore(obj: BaseRenderable | unknown, anchor: BaseRenderable | unknown): void {\n    throw new Error(\"Method not implemented.\")\n  }\n  getChildren(): BaseRenderable[] {\n    throw new Error(\"Method not implemented.\")\n  }\n  getChildrenCount(): number {\n    throw new Error(\"Method not implemented.\")\n  }\n  getRenderable(id: string): BaseRenderable | undefined {\n    throw new Error(\"Method not implemented.\")\n  }\n  requestRender(): void {\n    throw new Error(\"Method not implemented.\")\n  }\n  findDescendantById(id: string): BaseRenderable | undefined {\n    throw new Error(\"Method not implemented.\")\n  }\n}\n\nclass TestRenderable extends Renderable {\n  constructor(ctx: RenderContext, options: RenderableOptions) {\n    super(ctx, options)\n  }\n}\n\nclass TestFocusableRenderable extends Renderable {\n  _focusable = true\n\n  constructor(ctx: RenderContext, options: RenderableOptions) {\n    super(ctx, options)\n  }\n}\n\nlet testRenderer: TestRenderer\nlet testMockMouse: MockMouse\nlet testMockInput: MockInput\nlet renderOnce: () => Promise<void>\n\nbeforeEach(async () => {\n  ;({\n    renderer: testRenderer,\n    mockMouse: testMockMouse,\n    mockInput: testMockInput,\n    renderOnce,\n  } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ndescribe(\"BaseRenderable\", () => {\n  test(\"creates with default id\", () => {\n    const renderable = new TestBaseRenderable({})\n    expect(renderable.id).toMatch(/^renderable-\\d+$/)\n    expect(typeof renderable.num).toBe(\"number\")\n    expect(renderable.num).toBeGreaterThan(0)\n  })\n\n  test(\"creates with custom id\", () => {\n    const renderable = new TestBaseRenderable({ id: \"custom-id\" })\n    expect(renderable.id).toBe(\"custom-id\")\n  })\n\n  test(\"has unique numbers\", () => {\n    const r1 = new TestBaseRenderable({})\n    const r2 = new TestBaseRenderable({})\n    expect(r1.num).not.toBe(r2.num)\n  })\n\n  test(\"initial visibility state\", () => {\n    const renderable = new TestBaseRenderable({})\n    expect(renderable.visible).toBe(true)\n  })\n\n  test(\"can set visibility\", () => {\n    const renderable = new TestBaseRenderable({})\n    renderable.visible = false\n    expect(renderable.visible).toBe(false)\n  })\n})\n\ndescribe(\"Renderable\", () => {\n  test(\"creates with basic options\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-renderable\" })\n    expect(renderable.id).toBe(\"test-renderable\")\n    expect(renderable.visible).toBe(true)\n    expect(renderable.focusable).toBe(false)\n    expect(renderable.zIndex).toBe(0)\n    expect(renderable.live).toBe(false)\n    expect(renderable.liveCount).toBe(0)\n  })\n\n  test(\"isRenderable\", () => {\n    const { isRenderable } = require(\"../Renderable\")\n    const renderable = new TestBaseRenderable({})\n    expect(isRenderable(renderable)).toBe(true)\n    expect(isRenderable({})).toBe(false)\n    expect(isRenderable(null)).toBe(false)\n    expect(isRenderable(undefined)).toBe(false)\n  })\n\n  test(\"creates with width and height\", () => {\n    const renderable = new TestRenderable(testRenderer, {\n      id: \"test-size\",\n      width: 100,\n      height: 50,\n    })\n    expect(renderable.width).toBe(100)\n    expect(renderable.height).toBe(50)\n  })\n\n  test(\"throws on invalid width\", () => {\n    expect(() => {\n      new TestRenderable(testRenderer, { width: -10 })\n    }).toThrow(TypeError)\n  })\n\n  test(\"throws on invalid height\", () => {\n    expect(() => {\n      new TestRenderable(testRenderer, { width: 100, height: -5 })\n    }).toThrow(TypeError)\n  })\n\n  test(\"handles visibility changes\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-visible\" })\n    expect(renderable.visible).toBe(true)\n\n    renderable.visible = false\n    expect(renderable.visible).toBe(false)\n\n    renderable.visible = true\n    expect(renderable.visible).toBe(true)\n  })\n\n  test(\"handles live mode\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-live\", live: true })\n    expect(renderable.live).toBe(true)\n    expect(renderable.liveCount).toBe(1)\n  })\n})\n\ndescribe(\"Renderable - Child Management\", () => {\n  test(\"can add and remove children\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n\n    const index1 = parent.add(child1)\n    expect(index1).toBe(0)\n    expect(parent.getChildrenCount()).toBe(1)\n    expect(parent.getRenderable(\"child1\")).toBe(child1)\n\n    const index2 = parent.add(child2)\n    expect(index2).toBe(1)\n    expect(parent.getChildrenCount()).toBe(2)\n\n    parent.remove(\"child1\")\n    expect(parent.getChildrenCount()).toBe(1)\n    expect(parent.getRenderable(\"child1\")).toBeUndefined()\n    expect(parent.getRenderable(\"child2\")).toBe(child2)\n  })\n\n  test(\"can insert child at specific index\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n    const child3 = new TestRenderable(testRenderer, { id: \"child3\" })\n\n    parent.add(child1)\n    parent.add(child2)\n    parent.insertBefore(child3, child2)\n\n    const children = parent.getChildren()\n    expect(children[0].id).toBe(\"child1\")\n    expect(children[1].id).toBe(\"child3\")\n    expect(children[2].id).toBe(\"child2\")\n  })\n\n  test(\"insertBefore makes new child accessible\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n    const newChild = new TestRenderable(testRenderer, { id: \"newChild\" })\n\n    parent.add(child1)\n    parent.add(child2)\n    parent.insertBefore(newChild, child2)\n\n    expect(parent.getRenderable(\"newChild\")).toBe(newChild)\n  })\n\n  test(\"insertBefore with same node as anchor should not change order\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n    const child3 = new TestRenderable(testRenderer, { id: \"child3\" })\n\n    parent.add(child1)\n    parent.add(child2)\n    parent.add(child3)\n\n    const childrenBefore = parent.getChildren()\n    expect(childrenBefore[0].id).toBe(\"child1\")\n    expect(childrenBefore[1].id).toBe(\"child2\")\n    expect(childrenBefore[2].id).toBe(\"child3\")\n\n    // Call insertBefore with child2 as both the node and anchor\n    // This should be a no-op\n    parent.insertBefore(child3, child3)\n    parent.insertBefore(child2, child2)\n    parent.insertBefore(child1, child1)\n\n    const childrenAfter = parent.getChildren()\n    expect(childrenAfter[0].id).toBe(\"child1\")\n    expect(childrenAfter[1].id).toBe(\"child2\")\n    expect(childrenAfter[2].id).toBe(\"child3\")\n    expect(parent.getChildrenCount()).toBe(3)\n  })\n\n  test(\"handles adding destroyed renderable\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child = new TestRenderable(testRenderer, { id: \"child\" })\n    child.destroy()\n\n    const result = parent.add(child)\n    expect(result).toBe(-1)\n    expect(parent.getChildrenCount()).toBe(0)\n  })\n\n  test(\"can change renderable id and updates parent mapping\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child = new TestRenderable(testRenderer, { id: \"child\" })\n\n    parent.add(child)\n    expect(parent.getRenderable(\"child\")).toBe(child)\n\n    child.id = \"new-child-id\"\n    expect(child.id).toBe(\"new-child-id\")\n\n    expect(parent.getRenderable(\"child\")).toBeUndefined()\n    expect(parent.getRenderable(\"new-child-id\")).toBe(child)\n  })\n\n  test(\"findDescendantById finds direct children\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n\n    parent.add(child1)\n    parent.add(child2)\n\n    expect(parent.findDescendantById(\"child1\")).toBe(child1)\n    expect(parent.findDescendantById(\"child2\")).toBe(child2)\n    expect(parent.findDescendantById(\"nonexistent\")).toBeUndefined()\n  })\n\n  test(\"findDescendantById finds nested descendants\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n    const grandchild = new TestRenderable(testRenderer, { id: \"grandchild\" })\n\n    parent.add(child1)\n    parent.add(child2)\n    child1.add(grandchild)\n\n    expect(parent.findDescendantById(\"grandchild\")).toBe(grandchild)\n    expect(parent.findDescendantById(\"child1\")).toBe(child1)\n    expect(parent.findDescendantById(\"child2\")).toBe(child2)\n  })\n\n  test(\"findDescendantById handles TextNodeRenderable children without crashing\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n    const child3 = new TextRenderable(testRenderer, { id: \"child3\" })\n    const textNode = new TextNodeRenderable({ id: \"text-node\" })\n\n    parent.add(child1)\n    child1.add(child2)\n    child2.add(child3)\n    child3.add(textNode)\n\n    expect(parent.findDescendantById(\"child1\")).toBe(child1)\n    expect(parent.findDescendantById(\"child2\")).toBe(child2)\n    expect(parent.findDescendantById(\"text-node\")).toBeUndefined()\n  })\n\n  test(\"destroyRecursively destroys nested children recursively\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child = new TestRenderable(testRenderer, { id: \"child\" })\n    const grandchild = new TestRenderable(testRenderer, { id: \"grandchild\" })\n    const greatGrandchild = new TestRenderable(testRenderer, { id: \"greatGrandchild\" })\n\n    parent.add(child)\n    child.add(grandchild)\n    grandchild.add(greatGrandchild)\n\n    expect(parent.isDestroyed).toBe(false)\n    expect(child.isDestroyed).toBe(false)\n    expect(grandchild.isDestroyed).toBe(false)\n    expect(greatGrandchild.isDestroyed).toBe(false)\n\n    parent.destroyRecursively()\n\n    expect(parent.isDestroyed).toBe(true)\n    expect(child.isDestroyed).toBe(true)\n    expect(grandchild.isDestroyed).toBe(true)\n    expect(greatGrandchild.isDestroyed).toBe(true)\n  })\n\n  test(\"destroyRecursively handles empty renderable without errors\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"empty-parent\" })\n\n    expect(parent.isDestroyed).toBe(false)\n    expect(() => parent.destroyRecursively()).not.toThrow()\n    expect(parent.isDestroyed).toBe(true)\n  })\n\n  test(\"destroyRecursively destroys all children correctly with multiple children\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child1 = new TestRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n    const child3 = new TestRenderable(testRenderer, { id: \"child3\" })\n\n    parent.add(child1)\n    parent.add(child2)\n    parent.add(child3)\n\n    parent.destroyRecursively()\n\n    expect(parent.isDestroyed).toBe(true)\n    expect(child1.isDestroyed).toBe(true)\n    expect(child2.isDestroyed).toBe(true)\n    expect(child3.isDestroyed).toBe(true)\n  })\n\n  test(\"handles immediate add and destroy before render tick\", async () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const children = []\n    for (let i = 0; i < 10; i++) {\n      children.push(new TestRenderable(testRenderer, { id: `child-${i}` }))\n    }\n\n    for (const child of children) {\n      parent.add(child)\n    }\n\n    testRenderer.root.add(parent)\n\n    parent.destroyRecursively()\n\n    await renderOnce()\n    expect(parent.getChildrenCount()).toBe(0)\n  })\n\n  test(\"newly added child should not have layout updated if destroyed before render\", async () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent\" })\n    const child = new TestRenderable(testRenderer, { id: \"child\" })\n\n    parent.add(child)\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    const child2 = new TestRenderable(testRenderer, { id: \"child2\" })\n    parent.add(child2)\n\n    const spy = spyOn(child2, \"updateFromLayout\")\n\n    child2.destroy()\n\n    await renderOnce()\n\n    expect(spy).not.toHaveBeenCalled()\n  })\n\n  test(\"newly added children receive correct layout dimensions on first render\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n      flexDirection: \"column\",\n    })\n\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    // Add children after parent has been rendered\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      height: 30,\n      flexGrow: 0,\n    })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      height: 20,\n      flexGrow: 0,\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n\n    expect(child1.width).toBe(0)\n    expect(child2.width).toBe(0)\n\n    await renderOnce()\n\n    expect(child1.width).toBe(100)\n    expect(child1.height).toBe(30)\n    expect(child2.width).toBe(100)\n    expect(child2.height).toBe(20)\n  })\n\n  test(\"newly added children with nested children receive correct layout\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n    })\n\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    const child = new TestRenderable(testRenderer, {\n      id: \"child\",\n      width: 50,\n      height: 50,\n    })\n    const grandchild = new TestRenderable(testRenderer, {\n      id: \"grandchild\",\n      flexGrow: 1,\n    })\n\n    child.add(grandchild)\n    parent.add(child)\n\n    await renderOnce()\n\n    expect(child.width).toBe(50)\n    expect(child.height).toBe(50)\n\n    expect(grandchild.width).toBeGreaterThan(0)\n    expect(grandchild.height).toBeGreaterThan(0)\n  })\n\n  test(\"children added via insertBefore receive correct layout on first render\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n      flexDirection: \"column\",\n    })\n\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      height: 20,\n      flexGrow: 0,\n    })\n    const child3 = new TestRenderable(testRenderer, {\n      id: \"child3\",\n      height: 20,\n      flexGrow: 0,\n    })\n\n    parent.add(child1)\n    parent.add(child3)\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    // Insert child2 between child1 and child3\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      height: 15,\n      flexGrow: 0,\n    })\n\n    parent.insertBefore(child2, child3)\n\n    expect(child2.width).toBe(0)\n\n    await renderOnce()\n\n    expect(child2.width).toBe(100)\n    expect(child2.height).toBe(15)\n\n    expect(child1.y).toBe(0)\n    expect(child2.y).toBe(20)\n    expect(child3.y).toBe(35)\n  })\n\n  test(\"children after insertBefore anchor maintain correct layout\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n      flexDirection: \"row\",\n    })\n\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      width: 20,\n      flexGrow: 0,\n    })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      width: 25,\n      flexGrow: 0,\n    })\n    const child3 = new TestRenderable(testRenderer, {\n      id: \"child3\",\n      width: 30,\n      flexGrow: 0,\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n    parent.add(child3)\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    const child1InitialX = child1.x\n    const child2InitialX = child2.x\n    const child3InitialX = child3.x\n\n    const newChild = new TestRenderable(testRenderer, {\n      id: \"newChild\",\n      width: 10,\n      flexGrow: 0,\n    })\n\n    parent.insertBefore(newChild, child2)\n    await renderOnce()\n\n    expect(child1.x).toBe(child1InitialX)\n    expect(newChild.x).toBe(child1InitialX + 20)\n    expect(child2.x).toBe(child1InitialX + 30)\n    expect(child3.x).toBe(child1InitialX + 55)\n\n    expect(child1.width).toBe(20)\n    expect(newChild.width).toBe(10)\n    expect(child2.width).toBe(25)\n    expect(child3.width).toBe(30)\n  })\n\n  test(\"multiple children inserted in sequence receive correct layout\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 200,\n      height: 100,\n      flexDirection: \"column\",\n    })\n\n    const anchor = new TestRenderable(testRenderer, {\n      id: \"anchor\",\n      height: 10,\n      flexGrow: 0,\n    })\n\n    parent.add(anchor)\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    // Insert multiple children before the anchor in sequence\n    const newChild1 = new TestRenderable(testRenderer, {\n      id: \"new1\",\n      height: 15,\n      flexGrow: 0,\n    })\n    const newChild2 = new TestRenderable(testRenderer, {\n      id: \"new2\",\n      height: 20,\n      flexGrow: 0,\n    })\n    const newChild3 = new TestRenderable(testRenderer, {\n      id: \"new3\",\n      height: 25,\n      flexGrow: 0,\n    })\n\n    parent.insertBefore(newChild1, anchor)\n    parent.insertBefore(newChild2, anchor)\n    parent.insertBefore(newChild3, anchor)\n\n    await renderOnce()\n\n    expect(newChild1.width).toBe(200)\n    expect(newChild1.height).toBe(15)\n    expect(newChild2.width).toBe(200)\n    expect(newChild2.height).toBe(20)\n    expect(newChild3.width).toBe(200)\n    expect(newChild3.height).toBe(25)\n\n    expect(newChild1.y).toBe(0)\n    expect(newChild2.y).toBe(15)\n    expect(newChild3.y).toBe(35)\n    expect(anchor.y).toBe(60)\n  })\n\n  test(\"existing child moved via insertBefore maintains layout integrity\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n      flexDirection: \"column\",\n    })\n\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      height: 10,\n      flexGrow: 0,\n    })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      height: 20,\n      flexGrow: 0,\n    })\n    const child3 = new TestRenderable(testRenderer, {\n      id: \"child3\",\n      height: 30,\n      flexGrow: 0,\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n    parent.add(child3)\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    parent.insertBefore(child3, child1)\n    await renderOnce()\n\n    expect(child3.y).toBe(0)\n    expect(child1.y).toBe(30)\n    expect(child2.y).toBe(40)\n\n    expect(child1.width).toBe(100)\n    expect(child1.height).toBe(10)\n    expect(child2.width).toBe(100)\n    expect(child2.height).toBe(20)\n    expect(child3.width).toBe(100)\n    expect(child3.height).toBe(30)\n  })\n})\n\ndescribe(\"Renderable - Events\", () => {\n  test(\"handles mouse events\", async () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-mouse\", left: 0, top: 0, width: 10, height: 10 })\n    let mouseCalled = false\n\n    renderable.onMouse = () => {\n      mouseCalled = true\n    }\n\n    testRenderer.root.add(renderable)\n    await renderOnce()\n\n    testMockMouse.click(5, 5)\n    expect(mouseCalled).toBe(true)\n  })\n\n  test(\"handles mouse event types\", async () => {\n    const renderable = new TestRenderable(testRenderer, {\n      id: \"test-mouse-types\",\n      left: 0,\n      top: 0,\n      width: 10,\n      height: 10,\n    })\n    let downCalled = false\n    let upCalled = false\n\n    renderable.onMouseDown = () => {\n      downCalled = true\n    }\n    renderable.onMouseUp = () => {\n      upCalled = true\n    }\n\n    testRenderer.root.add(renderable)\n    await renderOnce()\n\n    testMockMouse.pressDown(5, 5)\n    expect(downCalled).toBe(true)\n\n    testMockMouse.release(5, 5)\n    expect(upCalled).toBe(true)\n  })\n})\n\ndescribe(\"Renderable - Focus\", () => {\n  test(\"handles focus when not focusable\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-focus\" })\n    expect(renderable.focusable).toBe(false)\n    expect(renderable.focused).toBe(false)\n\n    renderable.focus()\n    expect(renderable.focused).toBe(false)\n  })\n\n  test(\"handles focus when focusable\", () => {\n    const renderable = new TestFocusableRenderable(testRenderer, { id: \"test-focusable\" })\n\n    expect(renderable.focusable).toBe(true)\n    expect(renderable.focused).toBe(false)\n\n    renderable.focus()\n    expect(renderable.focused).toBe(true)\n\n    renderable.blur()\n    expect(renderable.focused).toBe(false)\n  })\n\n  test(\"emits focus events\", () => {\n    const renderable = new TestFocusableRenderable(testRenderer, { id: \"test-focus-events\" })\n\n    let focused = false\n    let blurred = false\n\n    renderable.on(RenderableEvents.FOCUSED, () => {\n      focused = true\n    })\n    renderable.on(RenderableEvents.BLURRED, () => {\n      blurred = true\n    })\n\n    renderable.focus()\n    expect(focused).toBe(true)\n\n    renderable.blur()\n    expect(blurred).toBe(true)\n  })\n\n  test(\"onPaste receives full paste event with preventDefault\", async () => {\n    const renderable = new TestFocusableRenderable(testRenderer, { id: \"test-paste\" })\n    let receivedEvent: any = null\n    let handlePasteCalled = false\n\n    renderable.handlePaste = (event) => {\n      handlePasteCalled = true\n    }\n\n    renderable.onPaste = (event) => {\n      receivedEvent = event\n      event.preventDefault()\n    }\n\n    renderable.focus()\n    await testMockInput.pasteBracketedText(\"test text\")\n\n    expect(receivedEvent).not.toBeNull()\n    expect(decodePasteBytes(receivedEvent.bytes)).toBe(\"test text\")\n    expect(receivedEvent.defaultPrevented).toBe(true)\n    expect(handlePasteCalled).toBe(false)\n  })\n\n  test(\"handlePaste receives full paste event\", async () => {\n    const renderable = new TestFocusableRenderable(testRenderer, { id: \"test-paste-handler\" })\n    let receivedEvent: any = null\n\n    renderable.handlePaste = (event) => {\n      receivedEvent = event\n    }\n\n    renderable.focus()\n    await testMockInput.pasteBracketedText(\"handler text\")\n\n    expect(receivedEvent).not.toBeNull()\n    expect(decodePasteBytes(receivedEvent.bytes)).toBe(\"handler text\")\n    expect(typeof receivedEvent.preventDefault).toBe(\"function\")\n  })\n\n  test(\"preventDefault in onPaste prevents handlePaste\", async () => {\n    const renderable = new TestFocusableRenderable(testRenderer, { id: \"test-prevent\" })\n    let onPasteCalled = false\n    let handlePasteCalled = false\n\n    renderable.onPaste = (event) => {\n      onPasteCalled = true\n      event.preventDefault()\n    }\n\n    renderable.handlePaste = () => {\n      handlePasteCalled = true\n    }\n\n    renderable.focus()\n    await testMockInput.pasteBracketedText(\"prevented\")\n\n    expect(onPasteCalled).toBe(true)\n    expect(handlePasteCalled).toBe(false)\n  })\n})\n\ndescribe(\"Renderable - Lifecycle\", () => {\n  test(\"handles destroy\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-destroy\" })\n    expect(renderable.isDestroyed).toBe(false)\n\n    renderable.destroy()\n    expect(renderable.isDestroyed).toBe(true)\n  })\n\n  test(\"prevents double destroy\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-double-destroy\" })\n    renderable.destroy()\n    expect(renderable.isDestroyed).toBe(true)\n\n    // Should not throw or cause issues\n    renderable.destroy()\n    expect(renderable.isDestroyed).toBe(true)\n  })\n\n  test(\"handles recursive destroy\", () => {\n    const parent = new TestRenderable(testRenderer, { id: \"parent-destroy\" })\n    const child = new TestRenderable(testRenderer, { id: \"child-destroy\" })\n    parent.add(child)\n\n    parent.destroyRecursively()\n    expect(parent.isDestroyed).toBe(true)\n    expect(child.isDestroyed).toBe(true)\n  })\n})\n\ndescribe(\"Renderable - Layout with Viewport Filtering\", () => {\n  // Create a test renderable that filters visible children like ScrollBox does\n  class ViewportFilteringRenderable extends Renderable {\n    private _filterEnabled = false\n\n    constructor(ctx: RenderContext, options: RenderableOptions) {\n      super(ctx, options)\n    }\n\n    enableFiltering() {\n      this._filterEnabled = true\n    }\n\n    protected _getVisibleChildren(): number[] {\n      if (!this._filterEnabled) {\n        return super._getVisibleChildren()\n      }\n      const children = this._childrenInZIndexOrder.slice(0, 2)\n      return children.map((c) => c.num)\n    }\n  }\n\n  test(\"newly added children receive layout even when filtered from viewport\", async () => {\n    const parent = new ViewportFilteringRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n      flexDirection: \"column\",\n    })\n\n    // Add initial children\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      height: 30,\n      flexGrow: 0,\n    })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      height: 30,\n      flexGrow: 0,\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n    testRenderer.root.add(parent)\n    parent.enableFiltering()\n    await renderOnce()\n\n    // Add a third child that will be filtered out\n    const child3 = new TestRenderable(testRenderer, {\n      id: \"child3\",\n      height: 25,\n      flexGrow: 0,\n    })\n\n    parent.add(child3)\n\n    expect(child3.width).toBe(0)\n\n    await renderOnce()\n\n    expect(child3.width).toBe(100)\n    expect(child3.height).toBe(25)\n    expect(child3.y).toBe(60)\n  })\n\n  test(\"child inserted before visible children receives layout when filtered\", async () => {\n    const parent = new ViewportFilteringRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n      flexDirection: \"column\",\n    })\n\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      height: 20,\n      flexGrow: 0,\n    })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      height: 20,\n      flexGrow: 0,\n    })\n    const child3 = new TestRenderable(testRenderer, {\n      id: \"child3\",\n      height: 20,\n      flexGrow: 0,\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n    parent.add(child3)\n    testRenderer.root.add(parent)\n    parent.enableFiltering()\n    await renderOnce()\n\n    // Insert a new child that pushes child3 further down (outside viewport filter)\n    const newChild = new TestRenderable(testRenderer, {\n      id: \"newChild\",\n      height: 15,\n      flexGrow: 0,\n    })\n\n    parent.insertBefore(newChild, child2)\n\n    await renderOnce()\n\n    expect(newChild.width).toBe(100)\n    expect(newChild.height).toBe(15)\n    expect(child3.width).toBe(100)\n    expect(child3.height).toBe(20)\n\n    expect(child1.y).toBe(0)\n    expect(newChild.y).toBe(20)\n    expect(child2.y).toBe(35)\n    expect(child3.y).toBe(55)\n  })\n})\n\ndescribe(\"Renderable - Nested Children Layout\", () => {\n  test(\"newly added parent with deeply nested children all receive layout\", async () => {\n    const root = new TestRenderable(testRenderer, {\n      id: \"root\",\n      width: 200,\n      height: 200,\n    })\n\n    testRenderer.root.add(root)\n    await renderOnce()\n\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 150,\n      height: 150,\n    })\n    const child = new TestRenderable(testRenderer, {\n      id: \"child\",\n      width: 100,\n      height: 100,\n    })\n    const grandchild = new TestRenderable(testRenderer, {\n      id: \"grandchild\",\n      width: 50,\n      height: 50,\n    })\n    const greatGrandchild = new TestRenderable(testRenderer, {\n      id: \"greatGrandchild\",\n      flexGrow: 1,\n    })\n\n    grandchild.add(greatGrandchild)\n    child.add(grandchild)\n    parent.add(child)\n\n    root.add(parent)\n\n    await renderOnce()\n\n    expect(parent.width).toBe(150)\n    expect(parent.height).toBe(150)\n\n    expect(child.width).toBe(100)\n    expect(child.height).toBe(100)\n\n    expect(grandchild.width).toBeGreaterThan(0)\n    expect(grandchild.height).toBeGreaterThan(0)\n  })\n\n  test(\"insertBefore with nested children updates all descendants correctly\", async () => {\n    const root = new TestRenderable(testRenderer, {\n      id: \"root\",\n      width: 200,\n      height: 200,\n      flexDirection: \"column\",\n    })\n\n    const existingChild = new TestRenderable(testRenderer, {\n      id: \"existing\",\n      height: 50,\n      flexGrow: 0,\n    })\n\n    root.add(existingChild)\n    testRenderer.root.add(root)\n    await renderOnce()\n\n    const newParent = new TestRenderable(testRenderer, {\n      id: \"newParent\",\n      height: 80,\n      flexGrow: 0,\n    })\n    const nestedChild = new TestRenderable(testRenderer, {\n      id: \"nested\",\n      flexGrow: 1,\n    })\n\n    newParent.add(nestedChild)\n    root.insertBefore(newParent, existingChild)\n\n    await renderOnce()\n\n    expect(newParent.width).toBe(200)\n    expect(newParent.height).toBe(80)\n    expect(newParent.y).toBe(0)\n\n    expect(nestedChild.width).toBeGreaterThan(0)\n    expect(nestedChild.height).toBeGreaterThan(0)\n\n    expect(existingChild.y).toBe(80)\n  })\n})\n\ndescribe(\"Renderable - Complex Layout Update Scenarios\", () => {\n  test(\"multiple rapid add operations before render complete correctly\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 200,\n      flexDirection: \"column\",\n    })\n\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    const children: TestRenderable[] = []\n    for (let i = 0; i < 5; i++) {\n      const child = new TestRenderable(testRenderer, {\n        id: `child-${i}`,\n        height: 20,\n        flexGrow: 0,\n      })\n      children.push(child)\n      parent.add(child)\n    }\n\n    for (const child of children) {\n      expect(child.width).toBe(0)\n    }\n\n    await renderOnce()\n\n    let expectedY = 0\n    for (const child of children) {\n      expect(child.width).toBe(100)\n      expect(child.height).toBe(20)\n      expect(child.y).toBe(expectedY)\n      expectedY += 20\n    }\n  })\n\n  test(\"insertBefore at different positions updates subsequent children correctly\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 300,\n      flexDirection: \"column\",\n    })\n\n    const children: TestRenderable[] = []\n    for (let i = 0; i < 5; i++) {\n      const child = new TestRenderable(testRenderer, {\n        id: `child-${i}`,\n        height: 20,\n        flexGrow: 0,\n      })\n      children.push(child)\n      parent.add(child)\n    }\n\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    const insert1 = new TestRenderable(testRenderer, {\n      id: \"insert1\",\n      height: 15,\n      flexGrow: 0,\n    })\n    parent.insertBefore(insert1, children[2]!)\n\n    await renderOnce()\n\n    expect(children[0]!.y).toBe(0)\n    expect(children[1]!.y).toBe(20)\n    expect(insert1.y).toBe(40)\n    expect(children[2]!.y).toBe(55)\n    expect(children[3]!.y).toBe(75)\n    expect(children[4]!.y).toBe(95)\n\n    const insert2 = new TestRenderable(testRenderer, {\n      id: \"insert2\",\n      height: 10,\n      flexGrow: 0,\n    })\n    parent.insertBefore(insert2, children[4]!)\n\n    await renderOnce()\n\n    expect(children[0]!.y).toBe(0)\n    expect(children[1]!.y).toBe(20)\n    expect(insert1.y).toBe(40)\n    expect(children[2]!.y).toBe(55)\n    expect(children[3]!.y).toBe(75)\n    expect(insert2.y).toBe(95)\n    expect(children[4]!.y).toBe(105)\n  })\n\n  test(\"add and insertBefore mixed operations maintain layout integrity\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 200,\n      flexDirection: \"column\",\n    })\n\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      height: 10,\n      flexGrow: 0,\n    })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      height: 20,\n      flexGrow: 0,\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n\n    const child3 = new TestRenderable(testRenderer, {\n      id: \"child3\",\n      height: 15,\n      flexGrow: 0,\n    })\n    parent.insertBefore(child3, child2)\n\n    const child4 = new TestRenderable(testRenderer, {\n      id: \"child4\",\n      height: 25,\n      flexGrow: 0,\n    })\n    parent.add(child4)\n\n    await renderOnce()\n\n    expect(child1.y).toBe(0)\n    expect(child3.y).toBe(10)\n    expect(child2.y).toBe(25)\n    expect(child4.y).toBe(45)\n\n    expect(child1.width).toBe(100)\n    expect(child2.width).toBe(100)\n    expect(child3.width).toBe(100)\n    expect(child4.width).toBe(100)\n  })\n\n  test(\"children removed and re-added receive fresh layout\", async () => {\n    const parent = new TestRenderable(testRenderer, {\n      id: \"parent\",\n      width: 100,\n      height: 100,\n      flexDirection: \"column\",\n    })\n\n    const child1 = new TestRenderable(testRenderer, {\n      id: \"child1\",\n      height: 30,\n      flexGrow: 0,\n    })\n    const child2 = new TestRenderable(testRenderer, {\n      id: \"child2\",\n      height: 40,\n      flexGrow: 0,\n    })\n\n    parent.add(child1)\n    parent.add(child2)\n    testRenderer.root.add(parent)\n    await renderOnce()\n\n    const child1InitialY = child1.y\n    const child2InitialY = child2.y\n\n    expect(child1InitialY).toBe(0)\n    expect(child2InitialY).toBe(30)\n\n    parent.remove(child1.id)\n    await renderOnce()\n\n    expect(child2.y).toBe(0)\n\n    parent.add(child1)\n    await renderOnce()\n\n    expect(child2.y).toBe(0)\n    expect(child1.y).toBe(40)\n    expect(child1.width).toBe(100)\n    expect(child1.height).toBe(30)\n  })\n})\n\ndescribe(\"RootRenderable\", () => {\n  test(\"creates with proper setup\", () => {\n    const root = new RootRenderable(testRenderer)\n    expect(root.id).toBe(\"__root__\")\n    expect(root.visible).toBe(true)\n    expect(root.width).toBe(testRenderer.width)\n    expect(root.height).toBe(testRenderer.height)\n  })\n\n  test(\"handles layout calculation\", () => {\n    const root = new RootRenderable(testRenderer)\n    expect(() => root.calculateLayout()).not.toThrow()\n  })\n\n  test(\"handles resize\", async () => {\n    const root = testRenderer.root\n    const newWidth = 70\n    const newHeight = 50\n\n    root.resize(newWidth, newHeight)\n    await renderOnce()\n\n    expect(root.width).toBe(newWidth)\n    expect(root.height).toBe(newHeight)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.clock.test.ts",
    "content": "import { afterEach, beforeEach, expect, test } from \"bun:test\"\nimport { SystemClock } from \"../lib/clock.js\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { ManualClock } from \"../testing/manual-clock.js\"\n\nlet clock: ManualClock\nlet renderer: TestRenderer\nlet renderOnce: () => Promise<void>\n\nbeforeEach(async () => {\n  clock = new ManualClock()\n  ;({ renderer, renderOnce } = await createTestRenderer({ clock, maxFps: 60 }))\n})\n\nafterEach(() => {\n  renderer.destroy()\n})\n\ntest(\"requestRender() does not stall after a backward clock jump\", async () => {\n  clock.setTime(10_000)\n  // @ts-expect-error - inspect private renderer timing state in regression test\n  renderer.lastTime = 10_000\n  clock.setTime(8_000)\n\n  let renderCalled = false\n  // @ts-expect-error - intercept private render method in regression test\n  renderer.renderNative = () => {\n    renderCalled = true\n  }\n\n  renderer.requestRender()\n  clock.advance(20)\n  await Promise.resolve()\n\n  expect(renderCalled).toBe(true)\n})\n\ntest(\"requestRender() uses SystemClock by default when no clock is injected\", async () => {\n  const originalNow = globalThis.performance.now\n  let nowValue = 10_000\n  let defaultRenderer: TestRenderer | null = null\n\n  globalThis.performance.now = () => nowValue\n\n  try {\n    ;({ renderer: defaultRenderer } = await createTestRenderer({ maxFps: 60 }))\n\n    // @ts-expect-error - inspect private renderer clock in regression test\n    expect(defaultRenderer.clock).toBeInstanceOf(SystemClock)\n\n    // @ts-expect-error - inspect private renderer timing state in regression test\n    defaultRenderer.lastTime = 10_000\n    nowValue = 8_000\n\n    let renderCalled = false\n    // @ts-expect-error - intercept private render method in regression test\n    defaultRenderer.renderNative = () => {\n      renderCalled = true\n    }\n\n    defaultRenderer.requestRender()\n    await Bun.sleep(20)\n\n    expect(renderCalled).toBe(true)\n  } finally {\n    defaultRenderer?.destroy()\n    globalThis.performance.now = originalNow\n  }\n})\n\ntest(\"loop() clamps negative deltaTime after a backward clock jump\", async () => {\n  const deltas: number[] = []\n\n  renderer.setFrameCallback(async (deltaTime) => {\n    deltas.push(deltaTime)\n  })\n\n  clock.setTime(10_000)\n  // @ts-expect-error - inspect private renderer timing state in regression test\n  renderer.lastTime = 10_000\n  // @ts-expect-error - inspect private renderer timing state in regression test\n  renderer.lastFpsTime = 10_000\n  clock.setTime(8_000)\n\n  await renderOnce()\n\n  expect(deltas).toEqual([0])\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.console-startup.test.ts",
    "content": "import { afterEach, beforeEach, expect, test } from \"bun:test\"\n\nimport { clearEnvCache } from \"../lib/env.ts\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer\"\nimport { ManualClock } from \"../testing/manual-clock\"\n\nlet renderer: TestRenderer | null = null\nlet previousShowConsole: string | undefined\n\nbeforeEach(() => {\n  previousShowConsole = process.env.SHOW_CONSOLE\n  delete process.env.SHOW_CONSOLE\n  clearEnvCache()\n})\n\nafterEach(() => {\n  renderer?.destroy()\n  renderer = null\n\n  if (previousShowConsole === undefined) {\n    delete process.env.SHOW_CONSOLE\n  } else {\n    process.env.SHOW_CONSOLE = previousShowConsole\n  }\n\n  clearEnvCache()\n})\n\ntest(\"CliRenderer initializes its clock before SHOW_CONSOLE triggers a render\", async () => {\n  process.env.SHOW_CONSOLE = \"true\"\n  clearEnvCache()\n\n  const result = await createTestRenderer({\n    clock: new ManualClock(),\n  })\n\n  renderer = result.renderer\n\n  expect(renderer).toBeDefined()\n})\n\ntest(\"CliRenderer uses its shared clock for debounced resize\", async () => {\n  const clock = new ManualClock()\n  const result = await createTestRenderer({\n    width: 40,\n    height: 20,\n    clock,\n  })\n\n  renderer = result.renderer\n  ;(renderer as any).handleResize(70, 30)\n\n  expect(renderer.width).toBe(40)\n  expect(renderer.height).toBe(20)\n\n  clock.advance(99)\n\n  expect(renderer.width).toBe(40)\n  expect(renderer.height).toBe(20)\n\n  clock.advance(1)\n\n  expect(renderer.width).toBe(70)\n  expect(renderer.height).toBe(30)\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.control.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockInput, type MockMouse } from \"../testing/test-renderer.js\"\nimport { RendererControlState } from \"../renderer.js\"\nimport { Renderable } from \"../Renderable.js\"\n\nclass TestRenderable extends Renderable {\n  constructor(renderer: TestRenderer, options: any) {\n    super(renderer, options)\n  }\n}\n\nlet renderer: TestRenderer\nlet mockInput: MockInput\nlet mockMouse: MockMouse\nlet renderOnce: () => Promise<void>\n\nbeforeEach(async () => {\n  ;({ renderer, mockInput, mockMouse, renderOnce } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  renderer.destroy()\n})\n\ntest(\"initial renderer state is IDLE\", () => {\n  expect(renderer.controlState).toBe(RendererControlState.IDLE)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"start() transitions to EXPLICIT_STARTED and starts rendering\", () => {\n  renderer.start()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n  expect(renderer.isRunning).toBe(true)\n})\n\ntest(\"pause() transitions to EXPLICIT_PAUSED and stops rendering\", () => {\n  renderer.start()\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.pause()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_PAUSED)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"suspend() transitions to EXPLICIT_SUSPENDED and stops rendering\", () => {\n  renderer.start()\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"suspend() disables mouse and keyboard input\", () => {\n  renderer.start()\n  expect(renderer.useMouse).toBe(true)\n\n  renderer.suspend()\n  expect(renderer.useMouse).toBe(false)\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n})\n\ntest(\"resume() restores previous EXPLICIT_STARTED state and restarts rendering\", () => {\n  renderer.start()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n  expect(renderer.isRunning).toBe(true)\n})\n\ntest(\"resume() restores previous IDLE state without starting rendering\", () => {\n  expect(renderer.controlState).toBe(RendererControlState.IDLE)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.IDLE)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"resume() restores previous EXPLICIT_PAUSED state without starting rendering\", () => {\n  renderer.start()\n  renderer.pause()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_PAUSED)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_PAUSED)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"resume() restores previous AUTO_STARTED state and restarts rendering\", () => {\n  renderer.requestLive()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n})\n\ntest(\"stop() transitions to EXPLICIT_STOPPED and stops rendering\", () => {\n  renderer.start()\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.stop()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STOPPED)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"requestRender() does not trigger when renderer is suspended\", async () => {\n  renderer.start()\n  renderer.suspend()\n\n  let renderCalled = false\n  // @ts-expect-error - renderNative is private\n  const originalRender = renderer.renderNative.bind(renderer)\n  // @ts-expect-error - renderNative is private\n  renderer.renderNative = () => {\n    renderCalled = true\n    return originalRender()\n  }\n\n  renderer.requestRender()\n  await new Promise((resolve) => setTimeout(resolve, 0))\n\n  expect(renderCalled).toBe(false)\n\n  // @ts-expect-error - renderNative is private\n  renderer.renderNative = originalRender\n})\n\ntest(\"requestRender() does trigger when renderer is paused\", async () => {\n  renderer.start()\n  await Bun.sleep(20)\n  renderer.pause()\n\n  let renderCalled = false\n  // @ts-expect-error - renderNative is private\n  const originalRender = renderer.renderNative.bind(renderer)\n  // @ts-expect-error - renderNative is private\n  renderer.renderNative = () => {\n    renderCalled = true\n    return originalRender()\n  }\n\n  renderer.requestRender()\n  await Bun.sleep(20)\n\n  expect(renderCalled).toBe(true)\n\n  // @ts-expect-error - renderNative is private\n  renderer.renderNative = originalRender\n})\n\ntest(\"auto() transitions running renderer to AUTO_STARTED state\", () => {\n  renderer.start()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n\n  renderer.auto()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n})\n\ntest(\"requestLive() auto-starts idle renderer\", () => {\n  expect(renderer.controlState).toBe(RendererControlState.IDLE)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.requestLive()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n})\n\ntest(\"dropLive() stops auto-started renderer when no live requests remain\", () => {\n  renderer.requestLive()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.dropLive()\n  expect(renderer.controlState).toBe(RendererControlState.IDLE)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"dropLive() does not stop explicitly started renderer\", () => {\n  renderer.start()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.requestLive()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n\n  renderer.dropLive()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n  expect(renderer.isRunning).toBe(true)\n})\n\ntest(\"suspend() preserves live request state for resume\", () => {\n  renderer.requestLive()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n})\n\ntest(\"control state transitions maintain consistency\", () => {\n  renderer.start()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.pause()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_PAUSED)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.start()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.auto()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  renderer.stop()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STOPPED)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"multiple suspend/resume cycles work correctly\", () => {\n  renderer.start()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_STARTED)\n\n  renderer.pause()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_PAUSED)\n  renderer.suspend()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_SUSPENDED)\n  renderer.resume()\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_PAUSED)\n})\n\ntest(\"keyboard input is suspended when renderer is suspended\", async () => {\n  renderer.start()\n\n  let keyEventReceived = false\n  const onKeypress = () => {\n    keyEventReceived = true\n  }\n  renderer.keyInput.on(\"keypress\", onKeypress)\n\n  mockInput.pressKey(\"a\")\n  expect(keyEventReceived).toBe(true)\n\n  keyEventReceived = false\n  renderer.suspend()\n\n  mockInput.pressKey(\"b\")\n  expect(keyEventReceived).toBe(false)\n  renderer.resume()\n  // Wait for renderer to consume stale input and re-register listeners\n  await new Promise((r) => setImmediate(r))\n  mockInput.pressKey(\"c\")\n  expect(keyEventReceived).toBe(true)\n  renderer.keyInput.off(\"keypress\", onKeypress)\n})\n\ntest(\"mouse input is suspended when renderer is suspended\", async () => {\n  renderer.start()\n\n  const testRenderable = new TestRenderable(renderer, {\n    x: 0,\n    y: 0,\n    width: renderer.width,\n    height: renderer.height,\n  })\n  renderer.root.add(testRenderable)\n  await renderOnce()\n\n  let mouseEventReceived = false\n  testRenderable.onMouse = () => {\n    mouseEventReceived = true\n  }\n\n  await mockMouse.click(0, 0)\n  expect(mouseEventReceived).toBe(true)\n\n  mouseEventReceived = false\n  renderer.suspend()\n\n  await mockMouse.click(0, 0)\n  expect(mouseEventReceived).toBe(false)\n\n  renderer.resume()\n  await mockMouse.click(0, 0)\n  expect(mouseEventReceived).toBe(true)\n\n  renderer.root.remove(testRenderable.id)\n})\n\ntest(\"paste input is suspended when renderer is suspended\", async () => {\n  renderer.start()\n\n  let pasteEventReceived = false\n  const onPaste = () => {\n    pasteEventReceived = true\n  }\n  renderer.keyInput.on(\"paste\", onPaste)\n\n  mockInput.pasteBracketedText(\"pasted text\")\n  expect(pasteEventReceived).toBe(true)\n\n  pasteEventReceived = false\n  renderer.suspend()\n\n  mockInput.pasteBracketedText(\"pasted text 2\")\n  expect(pasteEventReceived).toBe(false)\n\n  renderer.resume()\n  // Wait for renderer to consume stale input and re-register listeners\n  await new Promise((r) => setImmediate(r))\n\n  mockInput.pasteBracketedText(\"pasted text 3\")\n  expect(pasteEventReceived).toBe(true)\n\n  renderer.keyInput.off(\"paste\", onPaste)\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.core-slot-binding.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, test } from \"bun:test\"\nimport { Renderable } from \"../Renderable\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer\"\nimport { createCoreSlotRegistry, registerCorePlugin, SlotRenderable } from \"../plugins/core-slot\"\n\ntype AppSlot = \"statusbar\"\ntype AppContext = { appName: string; version: string }\ntype AppData = { label: string }\n\nclass TestRenderable extends Renderable {\n  constructor(renderer: TestRenderer, id: string) {\n    super(renderer, { id })\n  }\n}\n\nlet renderer: TestRenderer\n\nbeforeEach(async () => {\n  ;({ renderer } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  renderer.destroy()\n})\n\ndescribe(\"Core slot binding\", () => {\n  test(\"creates renderer-scoped registry by default\", () => {\n    const context = { appName: \"core-only\", version: \"1.0.0\" }\n    const first = createCoreSlotRegistry<AppSlot, AppContext>(renderer, context)\n    const second = createCoreSlotRegistry<AppSlot, AppContext>(renderer, context)\n\n    expect(first).toBe(second)\n    expect(first.context.appName).toBe(\"core-only\")\n\n    expect(() => {\n      createCoreSlotRegistry<AppSlot, AppContext>(renderer, { appName: \"other\", version: \"2.0.0\" })\n    }).toThrow(\"different context\")\n  })\n\n  test(\"uses fallback when no plugin is registered\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let fallbackCreateCount = 0\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      fallback: () => {\n        fallbackCreateCount++\n        return new TestRenderable(renderer, \"fallback\")\n      },\n    })\n    renderer.root.add(slot)\n\n    expect(fallbackCreateCount).toBe(1)\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\"])\n\n    slot.refresh()\n\n    expect(fallbackCreateCount).toBe(1)\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\"])\n\n    slot.destroy()\n  })\n\n  test(\"passes slot data to plugin renderers and updates on data change\", () => {\n    const registry = createCoreSlotRegistry<AppSlot, AppContext, AppData>(renderer, {\n      appName: \"core-only\",\n      version: \"1.0.0\",\n    })\n    const receivedLabels: string[] = []\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      slots: {\n        statusbar(_ctx, data) {\n          receivedLabels.push(data.label)\n          return new TestRenderable(renderer, `plugin-${data.label}`)\n        },\n      },\n    })\n\n    const slot = new SlotRenderable<AppSlot, AppContext, AppData>(renderer, {\n      registry,\n      name: \"statusbar\",\n      data: { label: \"initial\" },\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-initial\"])\n    expect(receivedLabels).toEqual([\"initial\"])\n\n    slot.data = { label: \"updated\" }\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-updated\"])\n    expect(receivedLabels).toEqual([\"initial\", \"updated\"])\n\n    slot.destroy()\n  })\n\n  test(\"creates plugin node once and reuses it on refresh\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let pluginCreateCount = 0\n    let pluginNode: TestRenderable | null = null\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      slots: {\n        statusbar() {\n          pluginCreateCount++\n          pluginNode = new TestRenderable(renderer, `plugin-a-${pluginCreateCount}`)\n          return pluginNode\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n    })\n    renderer.root.add(slot)\n\n    expect(pluginCreateCount).toBe(1)\n    expect(slot.getChildren()[0]).toBe(pluginNode)\n\n    slot.refresh()\n    registry.updateOrder(\"plugin-a\", 10)\n\n    expect(pluginCreateCount).toBe(1)\n    expect(slot.getChildren()[0]).toBe(pluginNode)\n\n    slot.destroy()\n  })\n\n  test(\"single_winner mode recreates plugins that re-enter as winner\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let pluginACreateCount = 0\n    let pluginBCreateCount = 0\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      order: 0,\n      slots: {\n        statusbar() {\n          pluginACreateCount++\n          return new TestRenderable(renderer, `plugin-a-${pluginACreateCount}`)\n        },\n      },\n    })\n\n    registerCorePlugin(registry, {\n      id: \"plugin-b\",\n      order: 10,\n      slots: {\n        statusbar() {\n          pluginBCreateCount++\n          return new TestRenderable(renderer, `plugin-b-${pluginBCreateCount}`)\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"single_winner\",\n    })\n    renderer.root.add(slot)\n\n    expect(pluginACreateCount).toBe(1)\n    expect(pluginBCreateCount).toBe(0)\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-1\"])\n\n    registry.updateOrder(\"plugin-b\", -1)\n\n    expect(pluginACreateCount).toBe(1)\n    expect(pluginBCreateCount).toBe(1)\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-b-1\"])\n\n    registry.updateOrder(\"plugin-a\", -2)\n\n    expect(pluginACreateCount).toBe(2)\n    expect(pluginBCreateCount).toBe(1)\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-2\"])\n\n    slot.destroy()\n  })\n\n  test(\"single_winner destroys non-winning plugin nodes when winner changes\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let pluginACreateCount = 0\n    let pluginBCreateCount = 0\n    const pluginANodes: TestRenderable[] = []\n    const pluginBNodes: TestRenderable[] = []\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      order: 0,\n      slots: {\n        statusbar() {\n          pluginACreateCount++\n          const node = new TestRenderable(renderer, `plugin-a-${pluginACreateCount}`)\n          pluginANodes.push(node)\n          return node\n        },\n      },\n    })\n\n    registerCorePlugin(registry, {\n      id: \"plugin-b\",\n      order: 10,\n      slots: {\n        statusbar() {\n          pluginBCreateCount++\n          const node = new TestRenderable(renderer, `plugin-b-${pluginBCreateCount}`)\n          pluginBNodes.push(node)\n          return node\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"single_winner\",\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-1\"])\n    expect(pluginANodes[0]?.isDestroyed).toBe(false)\n\n    registry.updateOrder(\"plugin-b\", -1)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-b-1\"])\n    expect(pluginANodes[0]?.isDestroyed).toBe(true)\n    expect(pluginBNodes[0]?.isDestroyed).toBe(false)\n\n    registry.updateOrder(\"plugin-a\", -2)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-2\"])\n    expect(pluginACreateCount).toBe(2)\n    expect(pluginBCreateCount).toBe(1)\n    expect(pluginANodes[1]?.isDestroyed).toBe(false)\n    expect(pluginBNodes[0]?.isDestroyed).toBe(true)\n\n    slot.destroy()\n\n    expect(pluginANodes[1]?.isDestroyed).toBe(true)\n    expect(pluginBNodes[0]?.isDestroyed).toBe(true)\n  })\n\n  test(\"single_winner object slots use activate/deactivate lifecycle and are not host-destroyed\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    const lifecycleEvents: string[] = []\n    let pluginARenderCount = 0\n    let pluginBRenderCount = 0\n    let pluginANode: TestRenderable | null = null\n    let pluginBNode: TestRenderable | null = null\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      order: 0,\n      slots: {\n        statusbar: {\n          render() {\n            pluginARenderCount++\n            if (!pluginANode) {\n              pluginANode = new TestRenderable(renderer, \"plugin-a-object\")\n            }\n            return pluginANode\n          },\n          onActivate() {\n            lifecycleEvents.push(\"a:activate\")\n          },\n          onDeactivate() {\n            lifecycleEvents.push(\"a:deactivate\")\n          },\n          onDispose() {\n            lifecycleEvents.push(\"a:dispose\")\n          },\n        },\n      },\n    })\n\n    registerCorePlugin(registry, {\n      id: \"plugin-b\",\n      order: 10,\n      slots: {\n        statusbar: {\n          render() {\n            pluginBRenderCount++\n            if (!pluginBNode) {\n              pluginBNode = new TestRenderable(renderer, \"plugin-b-object\")\n            }\n            return pluginBNode\n          },\n          onActivate() {\n            lifecycleEvents.push(\"b:activate\")\n          },\n          onDeactivate() {\n            lifecycleEvents.push(\"b:deactivate\")\n          },\n          onDispose() {\n            lifecycleEvents.push(\"b:dispose\")\n          },\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"single_winner\",\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-object\"])\n\n    registry.updateOrder(\"plugin-b\", -1)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-b-object\"])\n    expect(pluginANode?.isDestroyed).toBe(false)\n\n    registry.updateOrder(\"plugin-a\", -2)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-object\"])\n    expect(pluginARenderCount).toBe(2)\n    expect(pluginBRenderCount).toBe(1)\n\n    slot.destroy()\n\n    expect(pluginANode?.isDestroyed).toBe(false)\n    expect(pluginBNode?.isDestroyed).toBe(false)\n    expect(lifecycleEvents).toEqual([\n      \"a:activate\",\n      \"a:deactivate\",\n      \"b:activate\",\n      \"b:deactivate\",\n      \"a:activate\",\n      \"a:deactivate\",\n      \"a:dispose\",\n      \"b:dispose\",\n    ])\n  })\n\n  test(\"single_winner object slot dispose runs on unregister\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    const lifecycleEvents: string[] = []\n    let pluginNode: TestRenderable | null = null\n\n    registerCorePlugin(registry, {\n      id: \"plugin-object\",\n      slots: {\n        statusbar: {\n          render() {\n            if (!pluginNode) {\n              pluginNode = new TestRenderable(renderer, \"plugin-object\")\n            }\n            return pluginNode\n          },\n          onActivate() {\n            lifecycleEvents.push(\"activate\")\n          },\n          onDeactivate() {\n            lifecycleEvents.push(\"deactivate\")\n          },\n          onDispose() {\n            lifecycleEvents.push(\"dispose\")\n          },\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"single_winner\",\n    })\n    renderer.root.add(slot)\n\n    registry.unregister(\"plugin-object\")\n\n    expect(slot.getChildren()).toEqual([])\n    expect(pluginNode?.isDestroyed).toBe(false)\n    expect(lifecycleEvents).toEqual([\"activate\", \"deactivate\", \"dispose\"])\n\n    slot.destroy()\n  })\n\n  test(\"reports managed slot lifecycle hook failures without crashing\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    const lifecycleErrors: string[] = []\n\n    registry.onPluginError((event) => {\n      lifecycleErrors.push(`${event.phase}:${event.error.message}`)\n    })\n\n    registerCorePlugin(registry, {\n      id: \"managed-errors\",\n      slots: {\n        statusbar: {\n          render() {\n            return new TestRenderable(renderer, \"managed-errors\")\n          },\n          onActivate() {\n            throw new Error(\"activate failed\")\n          },\n          onDeactivate() {\n            throw new Error(\"deactivate failed\")\n          },\n          onDispose() {\n            throw new Error(\"dispose failed\")\n          },\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"single_winner\",\n    })\n    renderer.root.add(slot)\n\n    registry.unregister(\"managed-errors\")\n\n    expect(slot.getChildren()).toEqual([])\n    expect(lifecycleErrors).toEqual([\"setup:activate failed\", \"dispose:deactivate failed\", \"dispose:dispose failed\"])\n\n    slot.destroy()\n  })\n\n  test(\"replace mode hides fallback and renders all ordered plugins\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n\n    registerCorePlugin(registry, {\n      id: \"late\",\n      order: 10,\n      slots: {\n        statusbar() {\n          return new TestRenderable(renderer, \"late-plugin\")\n        },\n      },\n    })\n\n    registerCorePlugin(registry, {\n      id: \"early\",\n      order: 0,\n      slots: {\n        statusbar() {\n          return new TestRenderable(renderer, \"early-plugin\")\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"replace\",\n      fallback: () => new TestRenderable(renderer, \"replace-fallback\"),\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"early-plugin\", \"late-plugin\"])\n\n    slot.destroy()\n  })\n\n  test(\"replace mode keeps healthy plugins when one plugin render fails\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n\n    registerCorePlugin(registry, {\n      id: \"broken-plugin\",\n      order: 0,\n      slots: {\n        statusbar() {\n          throw new Error(\"broken render\")\n        },\n      },\n    })\n\n    registerCorePlugin(registry, {\n      id: \"healthy-plugin\",\n      order: 10,\n      slots: {\n        statusbar() {\n          return new TestRenderable(renderer, \"healthy-plugin\")\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"replace\",\n      fallback: () => new TestRenderable(renderer, \"replace-fallback\"),\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"healthy-plugin\"])\n\n    slot.destroy()\n  })\n\n  test(\"single_winner mode falls back when winning plugin fails\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n\n    registerCorePlugin(registry, {\n      id: \"broken-winner\",\n      order: 0,\n      slots: {\n        statusbar() {\n          throw new Error(\"winner failed\")\n        },\n      },\n    })\n\n    registerCorePlugin(registry, {\n      id: \"healthy-second\",\n      order: 10,\n      slots: {\n        statusbar() {\n          return new TestRenderable(renderer, \"healthy-second\")\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"single_winner\",\n      fallback: () => new TestRenderable(renderer, \"single-fallback\"),\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"single-fallback\"])\n\n    slot.destroy()\n  })\n\n  test(\"unregister removes and destroys plugin node while keeping fallback\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let fallbackCreateCount = 0\n    let pluginNode: TestRenderable | null = null\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      slots: {\n        statusbar() {\n          pluginNode = new TestRenderable(renderer, \"plugin-a\")\n          return pluginNode\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"append\",\n      fallback: () => {\n        fallbackCreateCount++\n        return new TestRenderable(renderer, \"fallback\")\n      },\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\", \"plugin-a\"])\n\n    registry.unregister(\"plugin-a\")\n\n    expect(fallbackCreateCount).toBe(1)\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\"])\n    expect(pluginNode?.isDestroyed).toBe(true)\n\n    slot.destroy()\n  })\n\n  test(\"clear removes mounted plugin nodes and restores fallback\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let fallbackCreateCount = 0\n    let pluginNode: TestRenderable | null = null\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      slots: {\n        statusbar() {\n          pluginNode = new TestRenderable(renderer, \"plugin-a\")\n          return pluginNode\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"replace\",\n      fallback: () => {\n        fallbackCreateCount++\n        return new TestRenderable(renderer, \"fallback\")\n      },\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a\"])\n\n    registry.clear()\n\n    expect(fallbackCreateCount).toBe(1)\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\"])\n    expect(pluginNode?.isDestroyed).toBe(true)\n\n    slot.destroy()\n  })\n\n  test(\"mode setter transitions reconcile mounted output\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let pluginACreateCount = 0\n    let pluginBCreateCount = 0\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      order: 0,\n      slots: {\n        statusbar() {\n          pluginACreateCount++\n          return new TestRenderable(renderer, `plugin-a-${pluginACreateCount}`)\n        },\n      },\n    })\n\n    registerCorePlugin(registry, {\n      id: \"plugin-b\",\n      order: 10,\n      slots: {\n        statusbar() {\n          pluginBCreateCount++\n          return new TestRenderable(renderer, `plugin-b-${pluginBCreateCount}`)\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"append\",\n      fallback: () => new TestRenderable(renderer, \"fallback\"),\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\", \"plugin-a-1\", \"plugin-b-1\"])\n\n    slot.mode = \"replace\"\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-1\", \"plugin-b-1\"])\n\n    slot.mode = \"single_winner\"\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-1\"])\n\n    slot.mode = \"replace\"\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"plugin-a-1\", \"plugin-b-2\"])\n\n    slot.mode = \"append\"\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\", \"plugin-a-1\", \"plugin-b-2\"])\n\n    slot.destroy()\n  })\n\n  test(\"destroy clears mounted nodes and unsubscribes from registry updates\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let fallbackNode: TestRenderable | null = null\n    let pluginNode: TestRenderable | null = null\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      slots: {\n        statusbar() {\n          pluginNode = new TestRenderable(renderer, \"plugin-a\")\n          return pluginNode\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      fallback: () => {\n        fallbackNode = new TestRenderable(renderer, \"fallback\")\n        return fallbackNode\n      },\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().length).toBe(2)\n\n    slot.destroy()\n\n    expect(fallbackNode?.isDestroyed).toBe(true)\n    expect(pluginNode?.isDestroyed).toBe(true)\n\n    registerCorePlugin(registry, {\n      id: \"plugin-b\",\n      slots: {\n        statusbar() {\n          return new TestRenderable(renderer, \"plugin-b\")\n        },\n      },\n    })\n\n    // SlotRenderable is destroyed, so no new children should appear\n    // (it's been removed from tree by destroy())\n  })\n\n  test(\"captures async plugin renderer failures without crashing\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    const errors: string[] = []\n    registry.onPluginError((event) => {\n      errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.error.message}`)\n    })\n\n    registerCorePlugin(registry, {\n      id: \"plugin-async\",\n      slots: {\n        statusbar() {\n          return Promise.resolve(new TestRenderable(renderer, \"plugin-async\")) as unknown as TestRenderable\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      fallback: () => new TestRenderable(renderer, \"fallback\"),\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\"])\n    expect(errors.length).toBe(1)\n    expect(errors[0]).toContain(\"plugin-async:statusbar:render\")\n    expect(errors[0]).toContain(\"async value\")\n\n    slot.refresh()\n    expect(errors.length).toBe(1)\n\n    slot.destroy()\n  })\n\n  test(\"captures non-renderable plugin values without crashing\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    const errors: string[] = []\n    registry.onPluginError((event) => {\n      errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.error.message}`)\n    })\n\n    registerCorePlugin(registry, {\n      id: \"plugin-invalid\",\n      slots: {\n        statusbar() {\n          return \"not-a-renderable\" as unknown as TestRenderable\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren()).toEqual([])\n    expect(errors).toEqual(['plugin-invalid:statusbar:render:Plugin \"plugin-invalid\" must return a BaseRenderable'])\n\n    slot.destroy()\n  })\n\n  test(\"captures plugin self-mount failures\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    const errors: string[] = []\n    registry.onPluginError((event) => {\n      errors.push(event.error.message)\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n    })\n    renderer.root.add(slot)\n\n    registerCorePlugin(registry, {\n      id: \"plugin-self\",\n      slots: {\n        statusbar() {\n          return slot\n        },\n      },\n    })\n\n    expect(slot.getChildren()).toEqual([])\n    expect(errors[0]).toContain(\"mount container\")\n\n    slot.destroy()\n  })\n\n  test(\"captures failures when plugin returns node attached to another parent\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    const errors: string[] = []\n    registry.onPluginError((event) => {\n      errors.push(event.error.message)\n    })\n    const otherParent = new TestRenderable(renderer, \"other-parent\")\n    const attachedNode = new TestRenderable(renderer, \"attached-node\")\n    renderer.root.add(otherParent)\n    otherParent.add(attachedNode)\n\n    registerCorePlugin(registry, {\n      id: \"plugin-attached\",\n      slots: {\n        statusbar() {\n          return attachedNode\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n    })\n    renderer.root.add(slot)\n\n    expect(attachedNode.parent).toBe(otherParent)\n    expect(slot.getChildren()).toEqual([])\n    expect(errors[0]).toContain(\"already attached to another parent\")\n\n    slot.destroy()\n  })\n\n  test(\"renders plugin failure placeholder only when configured\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n\n    registerCorePlugin(registry, {\n      id: \"broken-plugin\",\n      slots: {\n        statusbar() {\n          throw new Error(\"plugin exploded\")\n        },\n      },\n    })\n\n    const slotWithoutPlaceholder = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      fallback: () => new TestRenderable(renderer, \"fallback\"),\n    })\n    renderer.root.add(slotWithoutPlaceholder)\n\n    expect(slotWithoutPlaceholder.getChildren().map((child) => child.id)).toEqual([\"fallback\"])\n    slotWithoutPlaceholder.destroy()\n\n    const slotWithPlaceholder = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      fallback: () => new TestRenderable(renderer, \"fallback\"),\n      pluginFailurePlaceholder(failure) {\n        return new TestRenderable(renderer, `error-${failure.pluginId}`)\n      },\n    })\n    renderer.root.add(slotWithPlaceholder)\n\n    expect(slotWithPlaceholder.getChildren().map((child) => child.id)).toEqual([\"fallback\", \"error-broken-plugin\"])\n    slotWithPlaceholder.destroy()\n  })\n\n  test(\"replace mode uses fallback when plugin fails and no placeholder is configured\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n\n    registerCorePlugin(registry, {\n      id: \"broken-plugin\",\n      slots: {\n        statusbar() {\n          throw new Error(\"plugin exploded\")\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      mode: \"replace\",\n      fallback: () => new TestRenderable(renderer, \"fallback\"),\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\"])\n    slot.destroy()\n  })\n\n  test(\"reports placeholder renderer failures separately\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    const errors: string[] = []\n    registry.onPluginError((event) => {\n      errors.push(`${event.phase}:${event.error.message}`)\n    })\n\n    registerCorePlugin(registry, {\n      id: \"broken-plugin\",\n      slots: {\n        statusbar() {\n          throw new Error(\"plugin render failed\")\n        },\n      },\n    })\n\n    const slot = new SlotRenderable(renderer, {\n      registry,\n      name: \"statusbar\",\n      fallback: () => new TestRenderable(renderer, \"fallback\"),\n      pluginFailurePlaceholder() {\n        throw new Error(\"placeholder failed\")\n      },\n    })\n    renderer.root.add(slot)\n\n    expect(slot.getChildren().map((child) => child.id)).toEqual([\"fallback\"])\n    expect(errors).toEqual([\"render:plugin render failed\", \"error_placeholder:placeholder failed\"])\n\n    slot.destroy()\n  })\n\n  test(\"cleans up plugin nodes when fallback renderer fails\", () => {\n    const registry = createCoreSlotRegistry<AppSlot>(renderer, { appName: \"core-only\", version: \"1.0.0\" })\n    let pluginNode: TestRenderable | null = null\n\n    registerCorePlugin(registry, {\n      id: \"plugin-a\",\n      slots: {\n        statusbar() {\n          pluginNode = new TestRenderable(renderer, \"plugin-a\")\n          return pluginNode\n        },\n      },\n    })\n\n    expect(() => {\n      new SlotRenderable(renderer, {\n        registry,\n        name: \"statusbar\",\n        mode: \"append\",\n        fallback: () => {\n          return Promise.resolve(new TestRenderable(renderer, \"fallback\")) as unknown as TestRenderable\n        },\n      })\n    }).toThrow(\"async value\")\n\n    expect(pluginNode?.isDestroyed).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.cursor.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, test } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../testing\"\n\ndescribe(\"renderer cursor state\", () => {\n  let renderer: TestRenderer\n\n  beforeEach(async () => {\n    ;({ renderer } = await createTestRenderer({ width: 20, height: 10 }))\n  })\n\n  afterEach(() => {\n    renderer.destroy()\n  })\n\n  test(\"setCursorPosition preserves the terminal cursor style by default\", () => {\n    expect(renderer.getCursorState().style).toBe(\"default\")\n\n    renderer.setCursorPosition(4, 2)\n\n    const cursorState = renderer.getCursorState()\n    expect(cursorState.x).toBe(4)\n    expect(cursorState.y).toBe(2)\n    expect(cursorState.visible).toBe(true)\n    expect(cursorState.style).toBe(\"default\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.destroy-during-render.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { Renderable } from \"../Renderable.js\"\nimport type { OptimizedBuffer } from \"../buffer.js\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\n\nclass DestroyingRenderable extends Renderable {\n  protected renderSelf(_buffer: OptimizedBuffer, _deltaTime: number): void {}\n}\n\ntest(\"destroying renderer during frame callback should not crash\", async () => {\n  const { renderer } = await createTestRenderer({})\n\n  let destroyedDuringRender = false\n\n  renderer.setFrameCallback(async () => {\n    destroyedDuringRender = true\n    renderer.destroy()\n  })\n\n  renderer.start()\n\n  await new Promise((resolve) => setTimeout(resolve, 100))\n\n  expect(destroyedDuringRender).toBe(true)\n\n  // If we got here without a segfault, the test passes\n})\n\ntest(\"destroying renderer during post-process should not crash\", async () => {\n  const { renderer } = await createTestRenderer({})\n\n  let destroyedDuringPostProcess = false\n\n  renderer.addPostProcessFn(() => {\n    destroyedDuringPostProcess = true\n    renderer.destroy()\n  })\n\n  renderer.start()\n\n  await new Promise((resolve) => setTimeout(resolve, 100))\n\n  expect(destroyedDuringPostProcess).toBe(true)\n\n  // If we got here without a segfault, the test passes\n})\n\ntest(\"destroying renderer during root render should not crash\", async () => {\n  const { renderer } = await createTestRenderer({})\n\n  let destroyedDuringRender = false\n\n  // Override the root's render method to destroy the renderer\n  const originalRender = renderer.root.render.bind(renderer.root)\n  renderer.root.render = (buffer, deltaTime) => {\n    originalRender(buffer, deltaTime)\n    if (!destroyedDuringRender) {\n      destroyedDuringRender = true\n      renderer.destroy()\n    }\n  }\n\n  renderer.start()\n\n  await new Promise((resolve) => setTimeout(resolve, 100))\n\n  expect(destroyedDuringRender).toBe(true)\n\n  // If we got here without a segfault, the test passes\n})\n\ntest(\"destroying renderer during requestAnimationFrame should not crash\", async () => {\n  const { renderer } = await createTestRenderer({})\n\n  let destroyedDuringAnimationFrame = false\n\n  requestAnimationFrame(() => {\n    destroyedDuringAnimationFrame = true\n    renderer.destroy()\n  })\n\n  await new Promise((resolve) => setTimeout(resolve, 100))\n\n  expect(destroyedDuringAnimationFrame).toBe(true)\n})\n\ntest(\"destroying renderer during renderBefore should not crash\", async () => {\n  const { renderer } = await createTestRenderer({})\n\n  let destroyedDuringRenderBefore = false\n\n  const renderable = new DestroyingRenderable(renderer, {\n    id: \"destroy-render-before\",\n    width: 10,\n    height: 1,\n    renderBefore() {\n      if (!destroyedDuringRenderBefore) {\n        destroyedDuringRenderBefore = true\n        renderer.destroy()\n      }\n    },\n  })\n\n  renderer.root.add(renderable)\n  renderer.start()\n\n  await new Promise((resolve) => setTimeout(resolve, 100))\n\n  expect(destroyedDuringRenderBefore).toBe(true)\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.focus-restore.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe, spyOn } from \"bun:test\"\nimport { Buffer } from \"node:buffer\"\nimport { createTestRenderer, type TestRenderer, type MockInput, type MockMouse } from \"../testing/test-renderer\"\nimport { Renderable } from \"../Renderable\"\nimport { ManualClock } from \"../testing/manual-clock\"\n\nclass TestRenderable extends Renderable {\n  constructor(renderer: TestRenderer, options: any) {\n    super(renderer, options)\n  }\n}\n\nlet renderer: TestRenderer\nlet mockInput: MockInput\nlet mockMouse: MockMouse\nlet renderOnce: () => Promise<void>\nlet restoreSpy: ReturnType<typeof spyOn>\nlet clock: ManualClock\n\nbeforeEach(async () => {\n  clock = new ManualClock()\n  ;({ renderer, mockInput, mockMouse, renderOnce } = await createTestRenderer({\n    useMouse: true,\n    clock,\n  }))\n\n  // @ts-expect-error - testing private renderer internals\n  restoreSpy = spyOn(renderer.lib, \"restoreTerminalModes\")\n})\n\nafterEach(() => {\n  restoreSpy.mockRestore()\n  renderer.destroy()\n})\n\ndescribe(\"focus restore - terminal mode re-enable on focus-in\", () => {\n  test(\"restoreTerminalModes is NOT called on focus-in without prior blur\", async () => {\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    expect(restoreSpy).toHaveBeenCalledTimes(0)\n  })\n\n  test(\"restoreTerminalModes is called once after blur then focus-in\", async () => {\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    expect(restoreSpy).toHaveBeenCalledTimes(1)\n  })\n\n  test(\"restoreTerminalModes is NOT called on blur event\", async () => {\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    expect(restoreSpy).toHaveBeenCalledTimes(0)\n  })\n\n  test(\"restoreTerminalModes is called before focus event is emitted after blur\", async () => {\n    const callOrder: string[] = []\n\n    restoreSpy.mockImplementation(() => {\n      callOrder.push(\"restoreTerminalModes\")\n    })\n\n    renderer.on(\"focus\", () => {\n      callOrder.push(\"focus-event\")\n    })\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    expect(callOrder).toEqual([\"restoreTerminalModes\", \"focus-event\"])\n  })\n\n  test(\"repeated focus-in events only restore once per blur cycle\", async () => {\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    expect(restoreSpy).toHaveBeenCalledTimes(1)\n  })\n\n  test(\"multiple blur/focus cycles each trigger one restore\", async () => {\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    expect(restoreSpy).toHaveBeenCalledTimes(2)\n  })\n\n  test(\"focus-in emits focus event on the renderer\", async () => {\n    const events: string[] = []\n\n    renderer.on(\"focus\", () => {\n      events.push(\"focus\")\n    })\n\n    renderer.on(\"blur\", () => {\n      events.push(\"blur\")\n    })\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    expect(events).toEqual([\"focus\", \"blur\"])\n  })\n\n  test(\"duplicate focus and blur sequences only emit transitions once\", async () => {\n    const events: string[] = []\n\n    renderer.on(\"focus\", () => {\n      events.push(\"focus\")\n    })\n\n    renderer.on(\"blur\", () => {\n      events.push(\"blur\")\n    })\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    expect(events).toEqual([\"blur\", \"focus\", \"blur\"])\n  })\n\n  test(\"focus events do not trigger keypress events\", async () => {\n    const keypresses: any[] = []\n\n    renderer.keyInput.on(\"keypress\", (event) => {\n      keypresses.push(event)\n    })\n\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n\n    expect(keypresses).toHaveLength(0)\n  })\n\n  test(\"mouse events work after focus restore cycle\", async () => {\n    renderer.start()\n\n    const target = new TestRenderable(renderer, {\n      position: \"absolute\",\n      left: 0,\n      top: 0,\n      width: renderer.width,\n      height: renderer.height,\n    })\n    renderer.root.add(target)\n    await renderOnce()\n\n    let mouseEventCount = 0\n    target.onMouse = () => {\n      mouseEventCount++\n    }\n\n    // Verify mouse works initially\n    await mockMouse.click(5, 5)\n    expect(mouseEventCount).toBeGreaterThan(0)\n\n    const countBefore = mouseEventCount\n\n    // Simulate focus loss and regain\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    // Verify restoreTerminalModes was called\n    expect(restoreSpy).toHaveBeenCalledTimes(1)\n\n    // Verify mouse still works after focus restore\n    await mockMouse.click(5, 5)\n    expect(mouseEventCount).toBeGreaterThan(countBefore)\n\n    renderer.root.remove(target.id)\n  })\n\n  test(\"keyboard input works after focus restore cycle\", async () => {\n    renderer.start()\n\n    let keyEventCount = 0\n    const onKeypress = () => {\n      keyEventCount++\n    }\n    renderer.keyInput.on(\"keypress\", onKeypress)\n\n    // Verify keyboard works initially\n    mockInput.pressKey(\"a\")\n    clock.advance(15)\n    expect(keyEventCount).toBeGreaterThan(0)\n\n    const countBefore = keyEventCount\n\n    // Simulate focus loss and regain\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n    clock.advance(15)\n    renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    clock.advance(15)\n\n    // Verify keyboard still works after focus restore\n    mockInput.pressKey(\"b\")\n    clock.advance(15)\n    expect(keyEventCount).toBeGreaterThan(countBefore)\n\n    renderer.keyInput.off(\"keypress\", onKeypress)\n  })\n\n  test(\"rapid focus toggle does not cause issues\", async () => {\n    // Simulate rapid alt-tab back and forth\n    for (let i = 0; i < 10; i++) {\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n    }\n    clock.advance(15)\n\n    expect(restoreSpy).toHaveBeenCalledTimes(10)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.focus.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, MouseButtons, type MockMouse, type TestRenderer } from \"../testing.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { BoxRenderable } from \"../renderables/Box.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\nlet testRenderer: TestRenderer\nlet mockMouse: MockMouse\n\nbeforeEach(async () => {\n  ;({ renderer: testRenderer, mockMouse } = await createTestRenderer({\n    width: 50,\n    height: 30,\n  }))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ntest(\"click on focusable element focuses it\", async () => {\n  const scrollbox = new ScrollBoxRenderable(testRenderer, {\n    id: \"focusable-box\",\n    width: 20,\n    height: 10,\n  })\n  testRenderer.root.add(scrollbox)\n  await testRenderer.idle()\n\n  expect(scrollbox.focused).toBe(false)\n\n  await mockMouse.click(scrollbox.x + 1, scrollbox.y + 1)\n\n  expect(scrollbox.focused).toBe(true)\n})\n\ntest(\"click on child bubbles up to focusable parent\", async () => {\n  const scrollbox = new ScrollBoxRenderable(testRenderer, {\n    id: \"parent-box\",\n    width: 20,\n    height: 10,\n  })\n  testRenderer.root.add(scrollbox)\n\n  const text = new TextRenderable(testRenderer, {\n    id: \"child-text\",\n    content: \"Click me\",\n  })\n  scrollbox.add(text)\n  await testRenderer.idle()\n\n  expect(scrollbox.focused).toBe(false)\n\n  await mockMouse.click(text.x + 1, text.y)\n\n  expect(scrollbox.focused).toBe(true)\n})\n\ntest(\"click on non-focusable with no focusable parent does nothing\", async () => {\n  const box = new BoxRenderable(testRenderer, {\n    id: \"plain-box\",\n    width: 20,\n    height: 10,\n  })\n  testRenderer.root.add(box)\n  await testRenderer.idle()\n\n  expect(box.focusable).toBe(false)\n\n  await mockMouse.click(box.x + 1, box.y + 1)\n\n  expect(box.focused).toBe(false)\n})\n\ntest(\"preventDefault on mousedown prevents auto-focus\", async () => {\n  const scrollbox = new ScrollBoxRenderable(testRenderer, {\n    id: \"focusable-box\",\n    width: 20,\n    height: 10,\n    onMouseDown: (event) => {\n      event.preventDefault()\n    },\n  })\n  testRenderer.root.add(scrollbox)\n  await testRenderer.idle()\n\n  expect(scrollbox.focused).toBe(false)\n\n  await mockMouse.click(scrollbox.x + 1, scrollbox.y + 1)\n\n  expect(scrollbox.focused).toBe(false)\n})\n\ntest(\"mousedown handler is only called once per click\", async () => {\n  let mouseDownCount = 0\n  const box = new BoxRenderable(testRenderer, {\n    id: \"click-box\",\n    width: 20,\n    height: 10,\n    onMouseDown: () => {\n      mouseDownCount++\n    },\n  })\n  testRenderer.root.add(box)\n  await testRenderer.idle()\n\n  await mockMouse.click(box.x + 1, box.y + 1)\n\n  expect(mouseDownCount).toBe(1)\n})\n\ntest(\"non-left click does not auto-focus\", async () => {\n  const scrollbox = new ScrollBoxRenderable(testRenderer, {\n    id: \"focusable-box\",\n    width: 20,\n    height: 10,\n  })\n  testRenderer.root.add(scrollbox)\n  await testRenderer.idle()\n\n  await mockMouse.click(scrollbox.x + 1, scrollbox.y + 1, MouseButtons.RIGHT)\n  expect(scrollbox.focused).toBe(false)\n\n  await mockMouse.click(scrollbox.x + 2, scrollbox.y + 2, MouseButtons.MIDDLE)\n  expect(scrollbox.focused).toBe(false)\n})\n\ntest(\"preventDefault on ancestor blocks auto-focus\", async () => {\n  let childDown = false\n  const parent = new BoxRenderable(testRenderer, {\n    id: \"focus-parent\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    width: 20,\n    height: 10,\n    focusable: true,\n    onMouseDown: (event) => {\n      event.preventDefault()\n    },\n  })\n  const child = new BoxRenderable(testRenderer, {\n    id: \"focus-child\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    width: 6,\n    height: 3,\n    onMouseDown: () => {\n      childDown = true\n    },\n  })\n  parent.add(child)\n  testRenderer.root.add(parent)\n  await testRenderer.idle()\n\n  await mockMouse.click(child.x + 1, child.y + 1)\n\n  expect(childDown).toBe(true)\n  expect(parent.focused).toBe(false)\n  expect(child.focused).toBe(false)\n})\n\ntest(\"dragging over focusable target does not auto-focus\", async () => {\n  const start = new BoxRenderable(testRenderer, {\n    id: \"drag-start\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    width: 6,\n    height: 4,\n  })\n  const focusable = new BoxRenderable(testRenderer, {\n    id: \"drag-focusable\",\n    position: \"absolute\",\n    left: 12,\n    top: 1,\n    width: 6,\n    height: 4,\n    focusable: true,\n  })\n  testRenderer.root.add(start)\n  testRenderer.root.add(focusable)\n  await testRenderer.idle()\n\n  await mockMouse.pressDown(start.x + 1, start.y + 1)\n  await mockMouse.moveTo(focusable.x + 1, focusable.y + 1)\n  await mockMouse.release(focusable.x + 1, focusable.y + 1)\n\n  expect(focusable.focused).toBe(false)\n})\n\ntest(\"clicking empty space does not auto-focus\", async () => {\n  const box = new BoxRenderable(testRenderer, {\n    id: \"focusable-box\",\n    position: \"absolute\",\n    left: 1,\n    top: 1,\n    width: 8,\n    height: 4,\n    focusable: true,\n  })\n  testRenderer.root.add(box)\n  await testRenderer.idle()\n\n  await mockMouse.click(testRenderer.width - 1, testRenderer.height - 1)\n\n  expect(box.focused).toBe(false)\n})\n\ntest(\"autoFocus=false prevents click focus changes\", async () => {\n  const { renderer, mockMouse } = await createTestRenderer({\n    width: 50,\n    height: 30,\n    autoFocus: false,\n  })\n\n  try {\n    const first = new BoxRenderable(renderer, {\n      id: \"focus-first\",\n      position: \"absolute\",\n      left: 1,\n      top: 1,\n      width: 8,\n      height: 4,\n      focusable: true,\n    })\n    const second = new BoxRenderable(renderer, {\n      id: \"focus-second\",\n      position: \"absolute\",\n      left: 12,\n      top: 1,\n      width: 8,\n      height: 4,\n      focusable: true,\n    })\n    renderer.root.add(first)\n    renderer.root.add(second)\n    await renderer.idle()\n\n    first.focus()\n    expect(first.focused).toBe(true)\n\n    await mockMouse.click(second.x + 1, second.y + 1)\n\n    expect(first.focused).toBe(true)\n    expect(second.focused).toBe(false)\n  } finally {\n    renderer.destroy()\n  }\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.idle.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { RendererControlState } from \"../renderer.js\"\n\nlet renderer: TestRenderer\nlet renderOnce: () => Promise<void>\n\nasync function expectIdleToResolveImmediately(renderer: TestRenderer): Promise<void> {\n  const idlePromise = renderer.idle()\n  let resolved = false\n\n  idlePromise.then(() => {\n    resolved = true\n  })\n\n  await Promise.resolve()\n\n  expect(resolved).toBe(true)\n  await idlePromise\n}\n\nbeforeEach(async () => {\n  ;({ renderer, renderOnce } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  renderer.destroy()\n})\n\ntest(\"idle() resolves immediately when renderer is already idle\", async () => {\n  expect(renderer.controlState).toBe(RendererControlState.IDLE)\n  expect(renderer.isRunning).toBe(false)\n\n  await expectIdleToResolveImmediately(renderer)\n})\n\ntest(\"idle() waits for running renderer to stop\", async () => {\n  renderer.start()\n  expect(renderer.isRunning).toBe(true)\n\n  const idlePromise = renderer.idle()\n\n  await new Promise((resolve) => setTimeout(resolve, 50))\n\n  renderer.stop()\n\n  await idlePromise\n\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"idle() waits for paused renderer after requestRender()\", async () => {\n  renderer.pause()\n  expect(renderer.isRunning).toBe(false)\n\n  renderer.requestRender()\n\n  const idlePromise = renderer.idle()\n\n  await idlePromise\n\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"idle() resolves immediately after requestRender() completes\", async () => {\n  renderer.requestRender()\n\n  await renderer.idle()\n\n  await expectIdleToResolveImmediately(renderer)\n})\n\ntest(\"multiple idle() calls all resolve when renderer becomes idle\", async () => {\n  renderer.start()\n\n  const idlePromise1 = renderer.idle()\n  const idlePromise2 = renderer.idle()\n  const idlePromise3 = renderer.idle()\n\n  await new Promise((resolve) => setTimeout(resolve, 50))\n\n  renderer.stop()\n\n  await Promise.all([idlePromise1, idlePromise2, idlePromise3])\n\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"idle() resolves when AUTO_STARTED renderer drops all live requests\", async () => {\n  renderer.requestLive()\n  expect(renderer.controlState).toBe(RendererControlState.AUTO_STARTED)\n  expect(renderer.isRunning).toBe(true)\n\n  const idlePromise = renderer.idle()\n\n  renderer.dropLive()\n\n  await idlePromise\n\n  expect(renderer.controlState).toBe(RendererControlState.IDLE)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"idle() resolves after explicit pause\", async () => {\n  renderer.start()\n  expect(renderer.isRunning).toBe(true)\n\n  const idlePromise = renderer.idle()\n\n  renderer.pause()\n\n  await idlePromise\n\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_PAUSED)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"idle() resolves immediately when called on paused renderer\", async () => {\n  renderer.start()\n  renderer.pause()\n\n  await expectIdleToResolveImmediately(renderer)\n})\n\ntest(\"idle() resolves when renderer is destroyed\", async () => {\n  renderer.start()\n\n  const idlePromise = renderer.idle()\n\n  renderer.destroy()\n\n  await idlePromise\n})\n\ntest(\"idle() resolves immediately when called on destroyed renderer\", async () => {\n  renderer.destroy()\n\n  await expectIdleToResolveImmediately(renderer)\n})\n\ntest(\"idle() waits through multiple requestRender() calls\", async () => {\n  renderer.requestRender()\n  renderer.requestRender()\n\n  await renderer.idle()\n\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"idle() works correctly with stop() called during rendering\", async () => {\n  renderer.start()\n\n  await new Promise((resolve) => setTimeout(resolve, 50))\n\n  const idlePromise = renderer.idle()\n\n  renderer.stop()\n\n  await idlePromise\n\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"idle() resolves after pause() called during rendering\", async () => {\n  renderer.start()\n\n  await new Promise((resolve) => setTimeout(resolve, 50))\n\n  const idlePromise = renderer.idle()\n\n  renderer.pause()\n\n  await idlePromise\n\n  expect(renderer.controlState).toBe(RendererControlState.EXPLICIT_PAUSED)\n  expect(renderer.isRunning).toBe(false)\n})\n\ntest(\"idle() can be used in a loop to wait between operations\", async () => {\n  const operations: string[] = []\n\n  operations.push(\"start\")\n  renderer.requestRender()\n  await renderer.idle()\n  operations.push(\"rendered\")\n\n  renderer.requestRender()\n  await renderer.idle()\n  operations.push(\"rendered again\")\n\n  expect(operations).toEqual([\"start\", \"rendered\", \"rendered again\"])\n})\n\ntest(\"idle() works with requestAnimationFrame\", async () => {\n  let frameCallbackExecuted = false\n\n  requestAnimationFrame(() => {\n    frameCallbackExecuted = true\n  })\n\n  await renderer.idle()\n\n  expect(frameCallbackExecuted).toBe(true)\n})\n\ntest(\"idle() waits for all animation frames to complete\", async () => {\n  let count = 0\n\n  requestAnimationFrame(() => {\n    count++\n    requestAnimationFrame(() => {\n      count++\n    })\n  })\n\n  await renderer.idle()\n\n  expect(count).toBe(2)\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.input.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe } from \"bun:test\"\nimport { decodePasteBytes } from \"../lib/paste.js\"\nimport { nonAlphanumericKeys, type KeyEventType, type ParsedKey } from \"../lib/parse.keypress.js\"\nimport { type KeyEvent } from \"../lib/KeyHandler.js\"\nimport { Buffer } from \"node:buffer\"\nimport { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { createTestRenderer, type TestRenderer, type TestRendererOptions } from \"../testing/test-renderer.js\"\nimport { ManualClock } from \"../testing/manual-clock.js\"\nimport type { RenderContext } from \"../types.js\"\n\nlet currentRenderer: TestRenderer\nlet kittyRenderer: TestRenderer\nlet mockProcessCapabilityResponse: any\nlet mockGetTerminalCapabilities: any\nlet currentClock: ManualClock\nlet kittyClock: ManualClock\n\nbeforeEach(async () => {\n  currentClock = new ManualClock()\n  kittyClock = new ManualClock()\n  ;({ renderer: currentRenderer } = await createTestRenderer({ clock: currentClock }))\n  ;({ renderer: kittyRenderer } = await createTestRenderer({ kittyKeyboard: true, clock: kittyClock }))\n\n  // Mock native capability functions to avoid interfering with the test terminal\n  // @ts-expect-error - mocking for test\n  mockProcessCapabilityResponse = currentRenderer.lib.processCapabilityResponse\n  // @ts-expect-error - mocking for test\n  mockGetTerminalCapabilities = currentRenderer.lib.getTerminalCapabilities\n\n  // @ts-expect-error - mocking for test\n  currentRenderer.lib.processCapabilityResponse = () => {}\n  // @ts-expect-error - mocking for test\n  currentRenderer.lib.getTerminalCapabilities = () => ({ unicode: \"unicode\" })\n\n  // @ts-expect-error - mocking for test\n  kittyRenderer.lib.processCapabilityResponse = () => {}\n  // @ts-expect-error - mocking for test\n  kittyRenderer.lib.getTerminalCapabilities = () => ({ unicode: \"unicode\" })\n})\n\nafterEach(() => {\n  // Restore mocks\n  // @ts-expect-error - restore mock\n  currentRenderer.lib.processCapabilityResponse = mockProcessCapabilityResponse\n  // @ts-expect-error - restore mock\n  currentRenderer.lib.getTerminalCapabilities = mockGetTerminalCapabilities\n  // @ts-expect-error - restore mock\n  kittyRenderer.lib.processCapabilityResponse = mockProcessCapabilityResponse\n  // @ts-expect-error - restore mock\n  kittyRenderer.lib.getTerminalCapabilities = mockGetTerminalCapabilities\n\n  currentRenderer.destroy()\n  kittyRenderer.destroy()\n})\n\nasync function triggerInput(sequence: string): Promise<KeyEvent> {\n  return new Promise((resolve) => {\n    const onKeypress = (parsedKey: KeyEvent) => {\n      currentRenderer.keyInput.removeListener(\"keypress\", onKeypress)\n      resolve(parsedKey)\n    }\n\n    currentRenderer.keyInput.once(\"keypress\", onKeypress)\n\n    currentRenderer.stdin.emit(\"data\", Buffer.from(sequence))\n    advanceCurrentClock()\n  })\n}\n\nasync function triggerKittyInput(sequence: string): Promise<KeyEvent> {\n  return new Promise((resolve) => {\n    const onKeypress = (parsedKey: KeyEvent) => {\n      kittyRenderer.keyInput.removeListener(\"keypress\", onKeypress)\n      kittyRenderer.keyInput.removeListener(\"keyrelease\", onKeypress)\n      resolve(parsedKey)\n    }\n\n    kittyRenderer.keyInput.on(\"keypress\", onKeypress)\n    kittyRenderer.keyInput.on(\"keyrelease\", onKeypress)\n\n    kittyRenderer.stdin.emit(\"data\", Buffer.from(sequence))\n    advanceKittyClock()\n  })\n}\n\nfunction advanceCurrentClock(ms: number = 10): void {\n  currentClock.advance(ms)\n}\n\nfunction advanceKittyClock(ms: number = 10): void {\n  kittyClock.advance(ms)\n}\n\nclass MouseTarget extends Renderable {\n  constructor(context: RenderContext, options: RenderableOptions) {\n    super(context, options)\n  }\n}\n\nfunction advanceClock(clock: ManualClock, ms: number = 10): void {\n  clock.advance(ms)\n}\n\nasync function createRoutingRenderer(options: Partial<TestRendererOptions> = {}): Promise<{\n  renderer: TestRenderer\n  renderOnce: () => Promise<void>\n  resize: (width: number, height: number) => void\n  clock: ManualClock\n}> {\n  const clock = new ManualClock()\n  const { renderer, renderOnce, resize } = await createTestRenderer({\n    width: 40,\n    height: 20,\n    useMouse: true,\n    clock,\n    ...options,\n  })\n\n  return { renderer, renderOnce, resize, clock }\n}\n\ntest(\"basic letters via keyInput events\", async () => {\n  const result = await triggerInput(\"a\")\n  expect(result).toMatchObject({\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"a\",\n    eventType: \"press\",\n  })\n\n  const resultShift = await triggerInput(\"A\")\n  expect(resultShift).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"A\",\n    raw: \"A\",\n  })\n})\n\ntest(\"numbers via keyInput events\", async () => {\n  const result = await triggerInput(\"1\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"1\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: true,\n    sequence: \"1\",\n    raw: \"1\",\n  })\n})\n\ntest(\"special keys via keyInput events\", async () => {\n  const resultReturn = await triggerInput(\"\\r\")\n  expect(resultReturn).toMatchObject({\n    eventType: \"press\",\n    name: \"return\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\r\",\n    raw: \"\\r\",\n  })\n\n  const resultEnter = await triggerInput(\"\\n\")\n  expect(resultEnter).toMatchObject({\n    eventType: \"press\",\n    name: \"linefeed\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\n\",\n    raw: \"\\n\",\n  })\n\n  const resultTab = await triggerInput(\"\\t\")\n  expect(resultTab).toMatchObject({\n    eventType: \"press\",\n    name: \"tab\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\t\",\n    raw: \"\\t\",\n  })\n\n  const resultBackspace = await triggerInput(\"\\b\")\n  expect(resultBackspace).toMatchObject({\n    eventType: \"press\",\n    name: \"backspace\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\b\",\n    raw: \"\\b\",\n  })\n\n  const resultEscape = await triggerInput(\"\\x1b\")\n  expect(resultEscape).toMatchObject({\n    name: \"escape\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b\",\n    raw: \"\\x1b\",\n    eventType: \"press\",\n  })\n\n  const resultSpace = await triggerInput(\" \")\n  expect(resultSpace).toMatchObject({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \" \",\n    raw: \" \",\n  })\n})\n\ntest(\"ctrl+letter combinations via keyInput events\", async () => {\n  const resultCtrlA = await triggerInput(\"\\x01\")\n  expect(resultCtrlA).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x01\",\n    raw: \"\\x01\",\n  })\n\n  const resultCtrlZ = await triggerInput(\"\\x1a\")\n  expect(resultCtrlZ).toMatchObject({\n    eventType: \"press\",\n    name: \"z\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1a\",\n    raw: \"\\x1a\",\n  })\n})\n\ntest(\"meta+character combinations via keyInput events\", async () => {\n  const resultMetaA = await triggerInput(\"\\x1ba\")\n  expect(resultMetaA).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1ba\",\n    raw: \"\\x1ba\",\n  })\n\n  const resultMetaShiftA = await triggerInput(\"\\x1bA\")\n  expect(resultMetaShiftA).toMatchObject({\n    eventType: \"press\",\n    name: \"A\",\n    ctrl: false,\n    meta: true,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"\\x1bA\",\n    raw: \"\\x1bA\",\n  })\n})\n\ntest(\"function keys via keyInput events\", async () => {\n  const resultF1 = await triggerInput(\"\\x1bOP\")\n  expect(resultF1).toMatchObject({\n    eventType: \"press\",\n    name: \"f1\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1bOP\",\n    raw: \"\\x1bOP\",\n    code: \"OP\",\n  })\n\n  const resultF1Alt = await triggerInput(\"\\x1b[11~\")\n  expect(resultF1Alt).toMatchObject({\n    eventType: \"press\",\n    name: \"f1\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[11~\",\n    raw: \"\\x1b[11~\",\n    code: \"[11~\",\n  })\n\n  const resultF12 = await triggerInput(\"\\x1b[24~\")\n  expect(resultF12).toMatchObject({\n    eventType: \"press\",\n    name: \"f12\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[24~\",\n    raw: \"\\x1b[24~\",\n    code: \"[24~\",\n  })\n})\n\ntest(\"arrow keys via keyInput events\", async () => {\n  const resultUp = await triggerInput(\"\\x1b[A\")\n  expect(resultUp).toMatchObject({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[A\",\n    raw: \"\\x1b[A\",\n    code: \"[A\",\n  })\n\n  const resultDown = await triggerInput(\"\\x1b[B\")\n  expect(resultDown).toMatchObject({\n    eventType: \"press\",\n    name: \"down\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[B\",\n    raw: \"\\x1b[B\",\n    code: \"[B\",\n  })\n\n  const resultRight = await triggerInput(\"\\x1b[C\")\n  expect(resultRight).toMatchObject({\n    eventType: \"press\",\n    name: \"right\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[C\",\n    raw: \"\\x1b[C\",\n    code: \"[C\",\n  })\n\n  const resultLeft = await triggerInput(\"\\x1b[D\")\n  expect(resultLeft).toMatchObject({\n    eventType: \"press\",\n    name: \"left\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[D\",\n    raw: \"\\x1b[D\",\n    code: \"[D\",\n  })\n})\n\ntest(\"navigation keys via keyInput events\", async () => {\n  const resultHome = await triggerInput(\"\\x1b[H\")\n  expect(resultHome).toMatchObject({\n    eventType: \"press\",\n    name: \"home\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[H\",\n    raw: \"\\x1b[H\",\n    code: \"[H\",\n  })\n\n  const resultEnd = await triggerInput(\"\\x1b[F\")\n  expect(resultEnd).toMatchObject({\n    eventType: \"press\",\n    name: \"end\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[F\",\n    raw: \"\\x1b[F\",\n    code: \"[F\",\n  })\n\n  const resultPageUp = await triggerInput(\"\\x1b[5~\")\n  expect(resultPageUp).toMatchObject({\n    eventType: \"press\",\n    name: \"pageup\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[5~\",\n    raw: \"\\x1b[5~\",\n    code: \"[5~\",\n  })\n\n  const resultPageDown = await triggerInput(\"\\x1b[6~\")\n  expect(resultPageDown).toMatchObject({\n    eventType: \"press\",\n    name: \"pagedown\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[6~\",\n    raw: \"\\x1b[6~\",\n    code: \"[6~\",\n  })\n})\n\ntest(\"modifier combinations via keyInput events\", async () => {\n  const resultShiftUp = await triggerInput(\"\\x1b[1;2A\")\n  expect(resultShiftUp).toMatchObject({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[1;2A\",\n    raw: \"\\x1b[1;2A\",\n    code: \"[A\",\n  })\n\n  const resultMetaAltUp = await triggerInput(\"\\x1b[1;4A\")\n  expect(resultMetaAltUp).toMatchObject({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: true,\n    shift: true,\n    option: true,\n    number: false,\n    sequence: \"\\x1b[1;4A\",\n    raw: \"\\x1b[1;4A\",\n    code: \"[A\",\n  })\n\n  const resultAllModsUp = await triggerInput(\"\\x1b[1;8A\")\n  expect(resultAllModsUp).toMatchObject({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: true,\n    meta: true,\n    shift: true,\n    option: true,\n    number: false,\n    sequence: \"\\x1b[1;8A\",\n    raw: \"\\x1b[1;8A\",\n    code: \"[A\",\n  })\n})\n\ntest(\"delete key via keyInput events\", async () => {\n  const resultDelete = await triggerInput(\"\\x1b[3~\")\n  expect(resultDelete).toMatchObject({\n    eventType: \"press\",\n    name: \"delete\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[3~\",\n    raw: \"\\x1b[3~\",\n    code: \"[3~\",\n  })\n})\n\ntest(\"Buffer input via keyInput events\", async () => {\n  // Test with Buffer input by emitting buffer data directly\n  const result = await new Promise<KeyEvent>((resolve) => {\n    const onKeypress = (parsedKey: KeyEvent) => {\n      currentRenderer.keyInput.removeListener(\"keypress\", onKeypress)\n      resolve(parsedKey)\n    }\n\n    currentRenderer.keyInput.on(\"keypress\", onKeypress)\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"a\"))\n  })\n\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"a\",\n  })\n})\n\ntest(\"special characters via keyInput events\", async () => {\n  const resultExclamation = await triggerInput(\"!\")\n  expect(resultExclamation).toMatchObject({\n    eventType: \"press\",\n    name: \"!\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"!\",\n    raw: \"!\",\n  })\n\n  const resultAt = await triggerInput(\"@\")\n  expect(resultAt).toMatchObject({\n    eventType: \"press\",\n    name: \"@\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"@\",\n    raw: \"@\",\n  })\n})\n\ntest(\"meta space and escape combinations via keyInput events\", async () => {\n  const resultMetaSpace = await triggerInput(\"\\x1b \")\n  expect(resultMetaSpace).toMatchObject({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b \",\n    raw: \"\\x1b \",\n  })\n\n  const resultDoubleEscape = await triggerInput(\"\\x1b\\x1b\")\n  expect(resultDoubleEscape).toMatchObject({\n    eventType: \"press\",\n    name: \"escape\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b\\x1b\",\n    raw: \"\\x1b\\x1b\",\n  })\n})\n\n// ===== KITTY KEYBOARD PROTOCOL INTEGRATION TESTS =====\n\ntest(\"Kitty keyboard basic key via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard shift+a via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97:65;2u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"A\",\n    raw: \"\\x1b[97:65;2u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard ctrl+a via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;5u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;5u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard alt+a via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;3u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: true,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;3u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard function key via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[57364u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"f1\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[57364u\",\n    raw: \"\\x1b[57364u\",\n    code: \"[57364u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard arrow key via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[57352u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[57352u\",\n    raw: \"\\x1b[57352u\",\n    code: \"[57352u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard shift+space via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[32;2u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \" \",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \" \",\n    raw: \"\\x1b[32;2u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard event types via keyInput events\", async () => {\n  // Press event (explicit)\n  const pressExplicit = await triggerKittyInput(\"\\x1b[97;1:1u\")\n  expect(pressExplicit).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;1:1u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n\n  // Press event (default when no event type specified)\n  const pressDefault = await triggerKittyInput(\"\\x1b[97u\")\n  expect(pressDefault).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n\n  // Press event (modifier without event type)\n  const pressWithModifier = await triggerKittyInput(\"\\x1b[97;5u\") // Ctrl+a\n  expect(pressWithModifier).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;5u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n\n  // Repeat event (emitted as press with repeated=true)\n  const repeat = await triggerKittyInput(\"\\x1b[97;1:2u\")\n  expect(repeat).toMatchObject({\n    eventType: \"press\",\n    repeated: true,\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;1:2u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n\n  // Release event\n  const release = await triggerKittyInput(\"\\x1b[97;1:3u\")\n  expect(release).toMatchObject({\n    eventType: \"release\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;1:3u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n\n  // Repeat event with modifier (emitted as press with repeated=true)\n  const repeatWithCtrl = await triggerKittyInput(\"\\x1b[97;5:2u\")\n  expect(repeatWithCtrl).toMatchObject({\n    eventType: \"press\",\n    repeated: true,\n    name: \"a\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;5:2u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n\n  // Release event with modifier\n  const releaseWithShift = await triggerKittyInput(\"\\x1b[97;2:3u\")\n  expect(releaseWithShift).toMatchObject({\n    eventType: \"release\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"A\",\n    raw: \"\\x1b[97;2:3u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard with text via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;1;97u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;1;97u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard ctrl+shift+a via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;6u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: true,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"A\",\n    raw: \"\\x1b[97;6u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard alt+shift+a via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;4u\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: true,\n    shift: true,\n    option: true,\n    number: false,\n    sequence: \"A\",\n    raw: \"\\x1b[97;4u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard super+a via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;9u\") // modifier 9 - 1 = 8 = super\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;9u\",\n    super: true,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard hyper+a via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;17u\") // modifier 17 - 1 = 16 = hyper\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;17u\",\n    super: false,\n    hyper: true,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard caps lock via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;65u\") // modifier 65 - 1 = 64 = caps lock\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;65u\",\n    super: false,\n    hyper: false,\n    capsLock: true,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard num lock via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[97;129u\") // modifier 129 - 1 = 128 = num lock\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"a\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"a\",\n    raw: \"\\x1b[97;129u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: true,\n  })\n})\n\ntest(\"Kitty keyboard unicode character via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[233u\") // é\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"é\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"é\",\n    raw: \"\\x1b[233u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard emoji via keyInput events\", async () => {\n  const result = await triggerKittyInput(\"\\x1b[128512u\") // 😀\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"😀\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"😀\",\n    raw: \"\\x1b[128512u\",\n    super: false,\n    hyper: false,\n    capsLock: false,\n    numLock: false,\n  })\n})\n\ntest(\"Kitty keyboard keypad keys via keyInput events\", async () => {\n  const kp0 = await triggerKittyInput(\"\\x1b[57399u\")\n  expect(kp0?.name).toBe(\"kp0\")\n\n  const kpEnter = await triggerKittyInput(\"\\x1b[57414u\")\n  expect(kpEnter?.name).toBe(\"kpenter\")\n})\n\ntest(\"Kitty keyboard media keys via keyInput events\", async () => {\n  const play = await triggerKittyInput(\"\\x1b[57428u\")\n  expect(play?.name).toBe(\"mediaplay\")\n\n  const volumeUp = await triggerKittyInput(\"\\x1b[57439u\")\n  expect(volumeUp?.name).toBe(\"volumeup\")\n})\n\ntest(\"Kitty keyboard modifier keys via keyInput events\", async () => {\n  const leftShift = await triggerKittyInput(\"\\x1b[57441u\")\n  expect(leftShift?.name).toBe(\"leftshift\")\n  expect(leftShift?.eventType).toBe(\"press\")\n\n  const rightCtrl = await triggerKittyInput(\"\\x1b[57448u\")\n  expect(rightCtrl?.name).toBe(\"rightctrl\")\n  expect(rightCtrl?.eventType).toBe(\"press\")\n})\n\ntest(\"Kitty keyboard function keys with event types via keyInput events\", async () => {\n  // F1 press\n  const f1Press = await triggerKittyInput(\"\\x1b[57364u\")\n  expect(f1Press.name).toBe(\"f1\")\n  expect(f1Press.eventType).toBe(\"press\")\n  expect(f1Press.super ?? false).toBe(false)\n  expect(f1Press.hyper ?? false).toBe(false)\n  expect(f1Press.capsLock ?? false).toBe(false)\n  expect(f1Press.numLock ?? false).toBe(false)\n\n  // F1 repeat (emitted as press with repeated=true)\n  const f1Repeat = await triggerKittyInput(\"\\x1b[57364;1:2u\")\n  expect(f1Repeat.name).toBe(\"f1\")\n  expect(f1Repeat.eventType).toBe(\"press\")\n  expect((f1Repeat as any).repeated).toBe(true)\n  expect(f1Repeat.super ?? false).toBe(false)\n  expect(f1Repeat.hyper ?? false).toBe(false)\n  expect(f1Repeat.capsLock ?? false).toBe(false)\n  expect(f1Repeat.numLock ?? false).toBe(false)\n\n  // F1 release\n  const f1Release = await triggerKittyInput(\"\\x1b[57364;1:3u\")\n  expect(f1Release.name).toBe(\"f1\")\n  expect(f1Release.eventType).toBe(\"release\")\n  expect(f1Release.super ?? false).toBe(false)\n  expect(f1Release.hyper ?? false).toBe(false)\n  expect(f1Release.capsLock ?? false).toBe(false)\n  expect(f1Release.numLock ?? false).toBe(false)\n})\n\ntest(\"Kitty keyboard arrow keys with event types via keyInput events\", async () => {\n  // Up arrow press\n  const upPress = await triggerKittyInput(\"\\x1b[57352u\")\n  expect(upPress.name).toBe(\"up\")\n  expect(upPress.eventType).toBe(\"press\")\n  expect(upPress.super ?? false).toBe(false)\n  expect(upPress.hyper ?? false).toBe(false)\n  expect(upPress.capsLock ?? false).toBe(false)\n  expect(upPress.numLock ?? false).toBe(false)\n\n  // Up arrow repeat with Ctrl (emitted as press with repeated=true)\n  const upRepeatCtrl = await triggerKittyInput(\"\\x1b[57352;5:2u\")\n  expect(upRepeatCtrl.name).toBe(\"up\")\n  expect(upRepeatCtrl.ctrl).toBe(true)\n  expect(upRepeatCtrl.eventType).toBe(\"press\")\n  expect((upRepeatCtrl as any).repeated).toBe(true)\n  expect(upRepeatCtrl.super).toBe(false)\n  expect(upRepeatCtrl.hyper).toBe(false)\n  expect(upRepeatCtrl.capsLock).toBe(false)\n  expect(upRepeatCtrl.numLock).toBe(false)\n\n  // Down arrow release\n  const downRelease = await triggerKittyInput(\"\\x1b[57353;1:3u\")\n  expect(downRelease.name).toBe(\"down\")\n  expect(downRelease.eventType).toBe(\"release\")\n  expect(downRelease.super).toBe(false)\n  expect(downRelease.hyper).toBe(false)\n  expect(downRelease.capsLock).toBe(false)\n  expect(downRelease.numLock).toBe(false)\n})\n\n// ===== MISSING UNIT TEST CASES INTEGRATION TESTS =====\n\ntest(\"high byte buffer handling via keyInput events\", async () => {\n  // Test with Buffer input by emitting buffer data directly\n  const result = await new Promise<KeyEvent>((resolve) => {\n    const onKeypress = (parsedKey: KeyEvent) => {\n      currentRenderer.keyInput.removeListener(\"keypress\", onKeypress)\n      resolve(parsedKey)\n    }\n\n    currentRenderer.keyInput.on(\"keypress\", onKeypress)\n    // 128 + 32 = 160, should become \\x1b + \" \"\n    currentRenderer.stdin.emit(\"data\", Buffer.from([160]))\n    advanceCurrentClock()\n  })\n\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"space\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b \",\n    raw: \"\\x1b \",\n  })\n})\n\ntest(\"high byte UTF-8 lead byte does not stall indefinitely\", async () => {\n  const result = await new Promise<KeyEvent>((resolve, reject) => {\n    const timeout = setTimeout(() => {\n      currentRenderer.keyInput.removeListener(\"keypress\", onKeypress)\n      reject(new Error(\"timed out waiting for high-byte keypress\"))\n    }, 300)\n\n    const onKeypress = (parsedKey: KeyEvent) => {\n      clearTimeout(timeout)\n      currentRenderer.keyInput.removeListener(\"keypress\", onKeypress)\n      resolve(parsedKey)\n    }\n\n    currentRenderer.keyInput.on(\"keypress\", onKeypress)\n    // 128 + 105 = 233, should become \\x1b + \"i\"\n    currentRenderer.stdin.emit(\"data\", Buffer.from([233]))\n    advanceCurrentClock()\n  })\n\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"i\",\n    ctrl: false,\n    meta: true,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1bi\",\n    raw: \"\\x1bi\",\n  })\n})\n\ntest(\"empty input via keyInput events\", async () => {\n  const result = await triggerInput(\"\")\n  expect(result).toMatchObject({\n    eventType: \"press\",\n    name: \"\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\",\n    raw: \"\",\n  })\n})\n\ntest(\"rxvt style arrow keys with modifiers via keyInput events\", async () => {\n  const resultShiftUp = await triggerInput(\"\\x1b[a\")\n  expect(resultShiftUp).toMatchObject({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[a\",\n    raw: \"\\x1b[a\",\n    code: \"[a\",\n  })\n\n  const resultShiftInsert = await triggerInput(\"\\x1b[2$\")\n  expect(resultShiftInsert).toMatchObject({\n    eventType: \"press\",\n    name: \"insert\",\n    ctrl: false,\n    meta: false,\n    shift: true,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[2$\",\n    raw: \"\\x1b[2$\",\n    code: \"[2$\",\n  })\n})\n\ntest(\"ctrl modifier keys via keyInput events\", async () => {\n  const resultCtrlUp = await triggerInput(\"\\x1bOa\")\n  expect(resultCtrlUp).toMatchObject({\n    eventType: \"press\",\n    name: \"up\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1bOa\",\n    raw: \"\\x1bOa\",\n    code: \"Oa\",\n  })\n\n  const resultCtrlInsert = await triggerInput(\"\\x1b[2^\")\n  expect(resultCtrlInsert).toMatchObject({\n    eventType: \"press\",\n    name: \"insert\",\n    ctrl: true,\n    meta: false,\n    shift: false,\n    option: false,\n    number: false,\n    sequence: \"\\x1b[2^\",\n    raw: \"\\x1b[2^\",\n    code: \"[2^\",\n  })\n})\n\ntest(\"modifier bit calculations and meta/alt relationship via keyInput events\", async () => {\n  // Super modifier is bit 8, so modifier value 9 = 8 + 1 (base)\n  const superOnly = await triggerInput(\"\\x1b[1;9A\")\n  expect(superOnly.name).toBe(\"up\")\n  expect(superOnly.meta).toBe(false)\n  expect(superOnly.ctrl).toBe(false)\n  expect(superOnly.shift).toBe(false)\n  expect(superOnly.option).toBe(false)\n  expect((superOnly as any).super).toBe(true)\n  expect((superOnly as any).hyper).toBe(false)\n\n  // Alt/Option modifier is bit 1 (value 2), so modifier value 3 = 2 + 1\n  const altOnly = await triggerInput(\"\\x1b[1;3A\")\n  expect(altOnly.name).toBe(\"up\")\n  expect(altOnly.meta).toBe(true) // Alt sets meta flag (by design)\n  expect(altOnly.option).toBe(true)\n  expect(altOnly.ctrl).toBe(false)\n  expect(altOnly.shift).toBe(false)\n\n  // Ctrl modifier is bit 2 (value 4), so modifier value 5 = 4 + 1\n  const ctrlOnly = await triggerInput(\"\\x1b[1;5A\")\n  expect(ctrlOnly.name).toBe(\"up\")\n  expect(ctrlOnly.ctrl).toBe(true)\n  expect(ctrlOnly.meta).toBe(false)\n  expect(ctrlOnly.shift).toBe(false)\n  expect(ctrlOnly.option).toBe(false)\n\n  // Shift modifier is bit 0 (value 1), so modifier value 2 = 1 + 1\n  const shiftOnly = await triggerInput(\"\\x1b[1;2A\")\n  expect(shiftOnly.name).toBe(\"up\")\n  expect(shiftOnly.shift).toBe(true)\n  expect(shiftOnly.ctrl).toBe(false)\n  expect(shiftOnly.meta).toBe(false)\n  expect(shiftOnly.option).toBe(false)\n\n  // Combined modifiers\n  // Ctrl+Super = 4 + 8 = 12, so modifier value 13 = 12 + 1\n  const ctrlSuper = await triggerInput(\"\\x1b[1;13A\")\n  expect(ctrlSuper.name).toBe(\"up\")\n  expect(ctrlSuper.ctrl).toBe(true)\n  expect(ctrlSuper.meta).toBe(false)\n  expect(ctrlSuper.shift).toBe(false)\n  expect(ctrlSuper.option).toBe(false)\n  expect((ctrlSuper as any).super).toBe(true)\n  expect((ctrlSuper as any).hyper).toBe(false)\n\n  // Shift+Alt = 1 + 2 = 3, so modifier value 4 = 3 + 1\n  const shiftAlt = await triggerInput(\"\\x1b[1;4A\")\n  expect(shiftAlt.name).toBe(\"up\")\n  expect(shiftAlt.shift).toBe(true)\n  expect(shiftAlt.option).toBe(true)\n  expect(shiftAlt.meta).toBe(true) // Alt sets meta flag\n  expect(shiftAlt.ctrl).toBe(false)\n\n  // All modifiers: Shift(1) + Alt(2) + Ctrl(4) + Meta(8) = 15, so modifier value 16 = 15 + 1\n  const allMods = await triggerInput(\"\\x1b[1;16A\")\n  expect(allMods.name).toBe(\"up\")\n  expect(allMods.shift).toBe(true)\n  expect(allMods.option).toBe(true)\n  expect(allMods.ctrl).toBe(true)\n  expect(allMods.meta).toBe(true)\n})\n\ntest(\"modifier combinations with function keys via keyInput events\", async () => {\n  // Ctrl+F1\n  const ctrlF1 = await triggerInput(\"\\x1b[11;5~\")\n  expect(ctrlF1.name).toBe(\"f1\")\n  expect(ctrlF1.ctrl).toBe(true)\n  expect(ctrlF1.meta).toBe(false)\n  expect(ctrlF1.eventType).toBe(\"press\")\n\n  // Super+F1\n  const superF1 = await triggerInput(\"\\x1b[11;9~\")\n  expect(superF1.name).toBe(\"f1\")\n  expect(superF1.meta).toBe(false)\n  expect(superF1.ctrl).toBe(false)\n  expect(superF1.super).toBe(true)\n  expect(superF1.hyper).toBe(false)\n  expect(superF1.eventType).toBe(\"press\")\n\n  // Shift+Ctrl+F1\n  const shiftCtrlF1 = await triggerInput(\"\\x1b[11;6~\")\n  expect(shiftCtrlF1.name).toBe(\"f1\")\n  expect(shiftCtrlF1.shift).toBe(true)\n  expect(shiftCtrlF1.ctrl).toBe(true)\n  expect(shiftCtrlF1.meta).toBe(false)\n  expect(shiftCtrlF1.eventType).toBe(\"press\")\n})\n\ntest(\"regular parsing always defaults to press event type via keyInput events\", async () => {\n  // Test various regular key sequences to ensure they all default to \"press\"\n  const keys = [\n    \"a\",\n    \"A\",\n    \"1\",\n    \"!\",\n    \"\\t\",\n    \"\\r\",\n    \"\\n\",\n    \" \",\n    \"\\x1b\",\n    \"\\x01\", // Ctrl+A\n    \"\\x1ba\", // Alt+A\n    \"\\x1b[A\", // Up arrow\n    \"\\x1b[11~\", // F1\n    \"\\x1b[1;2A\", // Shift+Up\n    \"\\x1b[3~\", // Delete\n  ]\n\n  for (const keySeq of keys) {\n    const result = await triggerInput(keySeq)\n    expect(result.eventType).toBe(\"press\")\n  }\n\n  // Test with Buffer input too\n  const bufResult = await new Promise<KeyEvent>((resolve) => {\n    const onKeypress = (parsedKey: KeyEvent) => {\n      currentRenderer.keyInput.removeListener(\"keypress\", onKeypress)\n      resolve(parsedKey)\n    }\n\n    currentRenderer.keyInput.once(\"keypress\", onKeypress)\n    currentRenderer.stdin.emit(\"data\", Buffer.from(\"x\"))\n  })\n  expect(bufResult.eventType).toBe(\"press\")\n})\n\ntest(\"nonAlphanumericKeys export validation\", async () => {\n  expect(Array.isArray(nonAlphanumericKeys)).toBe(true)\n  expect(nonAlphanumericKeys.length).toBeGreaterThan(0)\n  expect(nonAlphanumericKeys).toContain(\"up\")\n  expect(nonAlphanumericKeys).toContain(\"down\")\n  expect(nonAlphanumericKeys).toContain(\"f1\")\n  expect(nonAlphanumericKeys).toContain(\"backspace\")\n  expect(nonAlphanumericKeys).toContain(\"tab\")\n  expect(nonAlphanumericKeys).toContain(\"left\")\n  expect(nonAlphanumericKeys).toContain(\"right\")\n})\n\ntest(\"ParsedKey type structure validation\", async () => {\n  const key: ParsedKey = {\n    name: \"test\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    sequence: \"test\",\n    raw: \"test\",\n    number: false,\n    eventType: \"press\",\n    source: \"raw\",\n  }\n\n  expect(key).toHaveProperty(\"name\")\n  expect(key).toHaveProperty(\"ctrl\")\n  expect(key).toHaveProperty(\"meta\")\n  expect(key).toHaveProperty(\"shift\")\n  expect(key).toHaveProperty(\"option\")\n  expect(key).toHaveProperty(\"sequence\")\n  expect(key).toHaveProperty(\"raw\")\n  expect(key).toHaveProperty(\"number\")\n\n  // Test that a key with code property works\n  const keyWithCode: ParsedKey = {\n    name: \"up\",\n    ctrl: false,\n    meta: false,\n    shift: false,\n    option: false,\n    sequence: \"\\x1b[A\",\n    raw: \"\\x1b[A\",\n    number: false,\n    code: \"[A\",\n    eventType: \"press\",\n    source: \"raw\",\n  }\n\n  expect(keyWithCode).toHaveProperty(\"code\")\n  expect(keyWithCode.code).toBe(\"[A\")\n})\n\ntest(\"KeyEventType type validation\", async () => {\n  // Test that KeyEventType only allows valid values\n  const validEventTypes: KeyEventType[] = [\"press\", \"repeat\", \"release\"]\n\n  for (const eventType of validEventTypes) {\n    // This should compile without errors\n    const mockKey: ParsedKey = {\n      name: \"test\",\n      ctrl: false,\n      meta: false,\n      shift: false,\n      option: false,\n      sequence: \"test\",\n      raw: \"test\",\n      number: false,\n      eventType: eventType,\n      source: \"raw\",\n    }\n    expect(mockKey.eventType).toBe(eventType)\n  }\n})\n\n// ===== CAPABILITY RESPONSE HANDLING TESTS =====\n\ntest(\"capability responses should not trigger keypress events\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Send various capability responses - none should trigger keypresses\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?1016;2$y\")) // DECRPM\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[1;2R\")) // CPR\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?62;c\")) // DA1\n\n  // Wait for stdin parser timeout\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"capability response followed by keypress\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Send capability response followed by 'a'\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?1016;2$ya\"))\n\n  // Wait for processing\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(1)\n  expect(keypresses[0].name).toBe(\"a\")\n})\n\ntest(\"partial SGR mouse stays pending on timeout, completes when rest arrives\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Incomplete SGR mouse sequence; stays pending (not flushed on timeout).\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[<35;20\"))\n\n  // Wait past native stdin parser timeout (10ms)\n  advanceCurrentClock()\n  expect(keypresses).toHaveLength(0)\n\n  // Completing the mouse sequence should not trigger keypress either\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\";5m\"))\n  advanceCurrentClock()\n  expect(keypresses).toHaveLength(0)\n\n  // Normal key input still works after\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"x\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(1)\n  expect(keypresses[0].name).toBe(\"x\")\n})\n\ntest(\"partial OSC flushed on timeout should not block later text\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b]52;c;\"))\n  advanceCurrentClock()\n  expect(keypresses).toHaveLength(0)\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"abc\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(3)\n  expect(keypresses.map((event) => event.name)).toEqual([\"a\", \"b\", \"c\"])\n})\n\ntest(\"partial OSC flushed on timeout should not block later escape sequences\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b]52;c;\"))\n  advanceCurrentClock()\n  expect(keypresses).toHaveLength(0)\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[A\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(1)\n  expect(keypresses[0].name).toBe(\"up\")\n})\n\ntest(\"incomplete mouse input resets the timeout when more bytes arrive\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[<35;20;\"))\n  advanceCurrentClock(9)\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"5\"))\n  advanceCurrentClock(9)\n\n  expect(keypresses).toHaveLength(0)\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"m\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"chunked XTVersion response should not trigger keypresses\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Send XTVersion in chunks (chunks arrive quickly, within stdin parser timeout)\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1bP>|kit\"))\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"ty(0.40\"))\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\".1)\\x1b\\\\\"))\n\n  // Wait for stdin parser to process\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"chunked XTVersion followed by keypress\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Send XTVersion in chunks followed by 'x'\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1bP>|ghostty\"))\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\" 1.1.3\\x1b\\\\x\"))\n\n  // Wait for processing\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(1)\n  expect(keypresses[0].name).toBe(\"x\")\n})\n\ntest(\"chunked Kitty graphics response should not trigger keypresses\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Send Kitty graphics response in chunks (arriving quickly)\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b_Gi=1;\"))\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"EINVAL:\"))\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"Zero width/height not allowed\\x1b\\\\\"))\n\n  // Wait for processing\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"multiple DECRPM responses in sequence\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Simulate multiple DECRPM responses arriving together\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?1016;2$y\\x1b[?2027;0$y\\x1b[?2031;2$y\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"pixel resolution response should not trigger keypress\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Mark as waiting for resolution\n  // @ts-expect-error - accessing private property for testing\n  currentRenderer.waitingForPixelResolution = true\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[4;720;1280t\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n  expect(currentRenderer.resolution).toEqual({ width: 1280, height: 720 })\n})\n\ntest(\"chunked pixel resolution response\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // @ts-expect-error - accessing private property for testing\n  currentRenderer.waitingForPixelResolution = true\n\n  // Send pixel resolution in chunks (arriving quickly)\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[4;72\"))\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"0;1280t\"))\n\n  // Wait for processing\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n  expect(currentRenderer.resolution).toEqual({ width: 1280, height: 720 })\n})\n\ntest(\"kitty full capability response arriving in realistic chunks\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Simulate how kitty might send its full response in a few chunks\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?1016;2$y\\x1b[?2027;0$y\"))\n  advanceCurrentClock(1)\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?2031;2$y\\x1b[?1004;1$y\\x1b[1;2R\\x1b[1;3R\"))\n  advanceCurrentClock(1)\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1bP>|kitty(0.\"))\n  advanceCurrentClock(1)\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"40.1)\\x1b\\\\\\x1b_Gi=1;OK\\x1b\\\\\"))\n  advanceCurrentClock(1)\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?62;c\"))\n\n  // Wait for processing\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"capability response interleaved with user input\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // User types 'h'\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"h\"))\n\n  // Capability response arrives\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?1016;2$y\"))\n\n  // User types 'e'\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"e\"))\n\n  // More capability responses\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1bP>|kitty(0.40.1)\\x1b\\\\\"))\n\n  // User types 'llo'\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"llo\"))\n\n  // Wait for processing\n  advanceCurrentClock()\n\n  // Should only have user keypresses\n  expect(keypresses).toHaveLength(5)\n  expect(keypresses.map((k) => k.name)).toEqual([\"h\", \"e\", \"l\", \"l\", \"o\"])\n})\n\ntest(\"delayed capability responses should be processed\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // User input first\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"abc\"))\n\n  // Late capability response (e.g., terminal was slow to respond)\n  advanceCurrentClock(50)\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?2027;2$y\"))\n\n  // Wait for processing\n  advanceCurrentClock()\n\n  // Should have user input but not capability\n  expect(keypresses).toHaveLength(3)\n  expect(keypresses.map((k) => k.name)).toEqual([\"a\", \"b\", \"c\"])\n})\n\ntest(\"delayed explicit-width CPR stays in response path while setup probe is active\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // @ts-expect-error - accessing private helper for test coverage\n  currentRenderer.updateStdinParserProtocolContext({ explicitWidthCprActive: true })\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[1;2\"))\n  advanceCurrentClock()\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"R\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"delayed DECRPM stays in response path while capability probing is active\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // @ts-expect-error - accessing private helper for test coverage\n  currentRenderer.updateStdinParserProtocolContext({ privateCapabilityRepliesActive: true })\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?1016;2$\"))\n  advanceCurrentClock()\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"y\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"delayed pixel resolution response stays in response path while query is active\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // @ts-expect-error - accessing private property for testing\n  currentRenderer.waitingForPixelResolution = true\n  // @ts-expect-error - accessing private helper for test coverage\n  currentRenderer.updateStdinParserProtocolContext({ pixelResolutionQueryActive: true })\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[4;1080;192\"))\n  advanceCurrentClock()\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"0t\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n  expect(currentRenderer.resolution).toEqual({ width: 1920, height: 1080 })\n})\n\ntest(\"vscode minimal capability response\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // VSCode often sends just one DECRPM\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[?1016;2$y\"))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"alacritty capability response sequence\", async () => {\n  const keypresses: KeyEvent[] = []\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  // Simulate alacritty's response pattern\n  const alacrittyResponse =\n    \"\\x1b[?1016;0$y\\x1b[?2027;0$y\\x1b[?2031;0$y\\x1b[?1004;2$y\\x1b[?2004;2$y\\x1b[?2026;2$y\\x1b[1;1R\\x1b[1;1R\\x1b[?0u\\x1b[?6c\"\n  currentRenderer.stdin.emit(\"data\", Buffer.from(alacrittyResponse))\n  advanceCurrentClock()\n\n  expect(keypresses).toHaveLength(0)\n})\n\ntest(\"focus and blur events\", async () => {\n  const events: string[] = []\n\n  currentRenderer.on(\"focus\", () => {\n    events.push(\"focus\")\n  })\n\n  currentRenderer.on(\"blur\", () => {\n    events.push(\"blur\")\n  })\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n  advanceCurrentClock()\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n  advanceCurrentClock()\n\n  expect(events).toEqual([\"focus\", \"blur\"])\n})\n\ntest(\"focus events should not trigger keypress\", async () => {\n  const keypresses: KeyEvent[] = []\n  const focusEvents: string[] = []\n\n  currentRenderer.keyInput.on(\"keypress\", (event) => {\n    keypresses.push(event)\n  })\n\n  currentRenderer.on(\"focus\", () => {\n    focusEvents.push(\"focus\")\n  })\n\n  currentRenderer.on(\"blur\", () => {\n    focusEvents.push(\"blur\")\n  })\n\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n  advanceCurrentClock()\n  currentRenderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n  advanceCurrentClock()\n\n  expect(focusEvents).toEqual([\"focus\", \"blur\"])\n  expect(keypresses).toHaveLength(0)\n})\n\ndescribe(\"stdin routing\", () => {\n  test(\"mouse then key in one chunk\", async () => {\n    const { renderer, renderOnce, clock } = await createRoutingRenderer()\n    try {\n      const target = new MouseTarget(renderer, {\n        id: \"target-mouse-then-key\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: renderer.width,\n        height: renderer.height,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      const keys: string[] = []\n      let scrollCount = 0\n\n      renderer.keyInput.on(\"keypress\", (event) => {\n        keys.push(event.name)\n      })\n\n      target.onMouseScroll = () => {\n        scrollCount++\n      }\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[<64;10;5Mx\"))\n      advanceClock(clock)\n\n      expect(scrollCount).toBe(1)\n      expect(keys).toEqual([\"x\"])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"key then mouse in one chunk\", async () => {\n    const { renderer, renderOnce, clock } = await createRoutingRenderer()\n    try {\n      const target = new MouseTarget(renderer, {\n        id: \"target-key-then-mouse\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: renderer.width,\n        height: renderer.height,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      const keys: string[] = []\n      let scrollCount = 0\n\n      renderer.keyInput.on(\"keypress\", (event) => {\n        keys.push(event.name)\n      })\n\n      target.onMouseScroll = () => {\n        scrollCount++\n      }\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"x\\x1b[<64;10;5M\"))\n      advanceClock(clock)\n\n      expect(keys).toEqual([\"x\"])\n      expect(scrollCount).toBe(1)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"focus and key mixed in one chunk\", async () => {\n    const { renderer, clock } = await createRoutingRenderer()\n    try {\n      const events: string[] = []\n      const keys: string[] = []\n\n      renderer.on(\"focus\", () => {\n        events.push(\"focus\")\n      })\n\n      renderer.keyInput.on(\"keypress\", (event) => {\n        keys.push(event.name)\n      })\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[Ix\"))\n      advanceClock(clock)\n\n      expect(events).toEqual([\"focus\"])\n      expect(keys).toEqual([\"x\"])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"focus and mouse mixed in one chunk\", async () => {\n    const { renderer, renderOnce, clock } = await createRoutingRenderer()\n    try {\n      const events: string[] = []\n      let scrollCount = 0\n\n      const target = new MouseTarget(renderer, {\n        id: \"target-focus-then-mouse\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: renderer.width,\n        height: renderer.height,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      renderer.on(\"focus\", () => {\n        events.push(\"focus\")\n      })\n\n      target.onMouseScroll = () => {\n        scrollCount++\n      }\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\\x1b[<64;10;5M\"))\n      advanceClock(clock)\n\n      expect(events).toEqual([\"focus\"])\n      expect(scrollCount).toBe(1)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"mouse state resets when mouse mode toggles\", async () => {\n    const { renderer, renderOnce, clock } = await createRoutingRenderer()\n    try {\n      const target = new MouseTarget(renderer, {\n        id: \"target-mouse-toggle-reset\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: renderer.width,\n        height: renderer.height,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let moveCount = 0\n      let dragCount = 0\n      target.onMouseMove = () => {\n        moveCount++\n      }\n      target.onMouseDrag = () => {\n        dragCount++\n      }\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[<0;1;1M\"))\n      advanceClock(clock)\n\n      renderer.useMouse = false\n      renderer.useMouse = true\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[<32;2;2M\"))\n      advanceClock(clock)\n\n      expect(moveCount).toBe(1)\n      expect(dragCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"mouse state resets on resize\", async () => {\n    const { renderer, renderOnce, resize, clock } = await createRoutingRenderer()\n    try {\n      const target = new MouseTarget(renderer, {\n        id: \"target-resize-reset\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: renderer.width,\n        height: renderer.height,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let moveCount = 0\n      let dragCount = 0\n      target.onMouseMove = () => {\n        moveCount++\n      }\n      target.onMouseDrag = () => {\n        dragCount++\n      }\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[<0;1;1M\"))\n      advanceClock(clock)\n\n      resize(41, 20)\n      await renderOnce()\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[<32;2;2M\"))\n      advanceClock(clock)\n\n      expect(moveCount).toBe(1)\n      expect(dragCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"suspend resets parser state before resume\", async () => {\n    const { renderer, clock } = await createRoutingRenderer()\n\n    try {\n      const events: Array<{ name: string; meta: boolean }> = []\n      renderer.keyInput.on(\"keypress\", (event) => {\n        events.push({ name: event.name, meta: event.meta })\n      })\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[\"))\n      advanceClock(clock, 5)\n\n      renderer.suspend()\n      renderer.resume()\n      await new Promise((resolve) => setImmediate(resolve))\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"x\"))\n      advanceClock(clock)\n\n      expect(events).toEqual([{ name: \"x\", meta: false }])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"streams large paste bodies without dropping them and resumes afterward\", async () => {\n    const { renderer, clock } = await createRoutingRenderer({\n      stdinParserMaxBufferBytes: 64 * 1024,\n    })\n\n    try {\n      const keys: string[] = []\n      const pastes: string[] = []\n      renderer.keyInput.on(\"keypress\", (event) => {\n        keys.push(event.name)\n      })\n      renderer.keyInput.on(\"paste\", (event) => {\n        pastes.push(decodePasteBytes(event.bytes))\n      })\n\n      const largeChunk = Buffer.alloc(16 * 1024, \"x\")\n      const expectedPaste = largeChunk.toString().repeat(5) + \"z\"\n\n      expect(() => {\n        renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[200~\"))\n        for (let i = 0; i < 5; i++) {\n          renderer.stdin.emit(\"data\", largeChunk)\n        }\n        renderer.stdin.emit(\"data\", Buffer.from(\"z\"))\n        renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[20\"))\n        renderer.stdin.emit(\"data\", Buffer.from(\"1~\"))\n        renderer.stdin.emit(\"data\", Buffer.from(\"q\"))\n      }).not.toThrow()\n\n      advanceClock(clock)\n\n      expect(keys).toEqual([\"q\"])\n      expect(pastes).toEqual([expectedPaste])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"emits paste event for large bracketed paste under configured limit\", async () => {\n    const { renderer, clock } = await createRoutingRenderer({\n      stdinParserMaxBufferBytes: 512 * 1024,\n    })\n\n    try {\n      const payloadSize = 256 * 1024\n      let pasteCount = 0\n      let pastedBytes = 0\n\n      renderer.keyInput.on(\"paste\", (event) => {\n        pasteCount += 1\n        pastedBytes += event.bytes.length\n      })\n\n      const chunk = Buffer.alloc(payloadSize, \"x\")\n      const stream = Buffer.concat([Buffer.from(\"\\x1b[200~\"), chunk, Buffer.from(\"\\x1b[201~\")])\n      renderer.stdin.emit(\"data\", stream)\n      advanceClock(clock)\n\n      expect(pasteCount).toBe(1)\n      expect(pastedBytes).toBe(payloadSize)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"emits one paste event for one bracketed paste\", async () => {\n    const { renderer, clock } = await createRoutingRenderer()\n\n    try {\n      const payload = \"x\".repeat(70_000)\n      const pastes: string[] = []\n      renderer.keyInput.on(\"paste\", (event) => {\n        pastes.push(decodePasteBytes(event.bytes))\n      })\n\n      renderer.stdin.emit(\"data\", Buffer.from(`\\x1b[200~${payload}\\x1b[201~`))\n      advanceClock(clock)\n\n      expect(pastes).toEqual([payload])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"emits empty paste for empty bracketed paste\", async () => {\n    const { renderer, clock } = await createRoutingRenderer()\n\n    try {\n      const pastes: string[] = []\n      renderer.keyInput.on(\"paste\", (event) => {\n        pastes.push(decodePasteBytes(event.bytes))\n      })\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[200~\\x1b[201~\"))\n      advanceClock(clock)\n\n      expect(pastes).toEqual([\"\"])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"preserves UTF-8 across bracketed paste chunk boundaries\", async () => {\n    const { renderer, clock } = await createRoutingRenderer()\n\n    try {\n      const payload = \"a\".repeat(4095) + \"é\"\n      const pastes: string[] = []\n      renderer.keyInput.on(\"paste\", (event) => {\n        pastes.push(decodePasteBytes(event.bytes))\n      })\n\n      renderer.stdin.emit(\"data\", Buffer.from(`\\x1b[200~${payload}\\x1b[201~`))\n      advanceClock(clock)\n\n      expect(pastes.join(\"\")).toBe(payload)\n    } finally {\n      renderer.destroy()\n    }\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.kitty-flags.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { buildKittyKeyboardFlags } from \"../renderer.js\"\n\n// Kitty Keyboard Protocol progressive enhancement flags\n// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement\nconst KITTY_FLAG_DISAMBIGUATE = 0b1 // Report disambiguated escape codes\nconst KITTY_FLAG_EVENT_TYPES = 0b10 // Report event types (press/repeat/release)\nconst KITTY_FLAG_ALTERNATE_KEYS = 0b100 // Report alternate keys (e.g., numpad vs regular)\nconst KITTY_FLAG_ALL_KEYS_AS_ESCAPES = 0b1000 // Report all keys as escape codes\nconst KITTY_FLAG_REPORT_TEXT = 0b10000 // Report text associated with key events\n\ntest(\"buildKittyKeyboardFlags - null/undefined returns 0\", () => {\n  expect(buildKittyKeyboardFlags(null)).toBe(0)\n  expect(buildKittyKeyboardFlags(undefined)).toBe(0)\n})\n\ntest(\"buildKittyKeyboardFlags - empty object returns DISAMBIGUATE | ALTERNATE_KEYS (0b101)\", () => {\n  // Default behavior: disambiguate + alternate keys\n  // - Disambiguate fixes ESC timing issues, alt+key ambiguity, makes ctrl+c a key event\n  // - Alternate keys enables shifted/base-layout keys for robust shortcut matching\n  const expected = KITTY_FLAG_DISAMBIGUATE | KITTY_FLAG_ALTERNATE_KEYS\n  expect(buildKittyKeyboardFlags({})).toBe(expected)\n  expect(buildKittyKeyboardFlags({})).toBe(0b101)\n  expect(buildKittyKeyboardFlags({})).toBe(5)\n})\n\ntest(\"buildKittyKeyboardFlags - events: false returns DISAMBIGUATE | ALTERNATE_KEYS (0b101)\", () => {\n  // Explicit no events: disambiguate + alternate keys\n  const expected = KITTY_FLAG_DISAMBIGUATE | KITTY_FLAG_ALTERNATE_KEYS\n  expect(buildKittyKeyboardFlags({ events: false })).toBe(expected)\n  expect(buildKittyKeyboardFlags({ events: false })).toBe(0b101)\n  expect(buildKittyKeyboardFlags({ events: false })).toBe(5)\n})\n\ntest(\"buildKittyKeyboardFlags - events: true returns DISAMBIGUATE | ALTERNATE_KEYS | EVENT_TYPES (0b111)\", () => {\n  // With event types: disambiguate + alternate keys + event types (press/repeat/release)\n  const expected = KITTY_FLAG_DISAMBIGUATE | KITTY_FLAG_ALTERNATE_KEYS | KITTY_FLAG_EVENT_TYPES\n  expect(buildKittyKeyboardFlags({ events: true })).toBe(expected)\n  expect(buildKittyKeyboardFlags({ events: true })).toBe(0b111)\n  expect(buildKittyKeyboardFlags({ events: true })).toBe(7)\n})\n\ntest(\"buildKittyKeyboardFlags - flag values match kitty spec constants\", () => {\n  // Default: disambiguate + alternate keys\n  expect(buildKittyKeyboardFlags({})).toBe(KITTY_FLAG_DISAMBIGUATE | KITTY_FLAG_ALTERNATE_KEYS)\n\n  // With events: disambiguate + alternate keys + event types\n  expect(buildKittyKeyboardFlags({ events: true })).toBe(\n    KITTY_FLAG_DISAMBIGUATE | KITTY_FLAG_ALTERNATE_KEYS | KITTY_FLAG_EVENT_TYPES,\n  )\n})\n\ntest(\"kitty flag constants match spec bit positions\", () => {\n  // Verify our constants match the kitty keyboard protocol spec\n  // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement\n  expect(KITTY_FLAG_DISAMBIGUATE).toBe(1)\n  expect(KITTY_FLAG_EVENT_TYPES).toBe(2)\n  expect(KITTY_FLAG_ALTERNATE_KEYS).toBe(4)\n  expect(KITTY_FLAG_ALL_KEYS_AS_ESCAPES).toBe(8)\n  expect(KITTY_FLAG_REPORT_TEXT).toBe(16)\n})\n\ntest(\"flag bit positions are correct powers of 2\", () => {\n  // Each flag should be a distinct bit\n  expect(KITTY_FLAG_DISAMBIGUATE).toBe(1 << 0)\n  expect(KITTY_FLAG_EVENT_TYPES).toBe(1 << 1)\n  expect(KITTY_FLAG_ALTERNATE_KEYS).toBe(1 << 2)\n  expect(KITTY_FLAG_ALL_KEYS_AS_ESCAPES).toBe(1 << 3)\n  expect(KITTY_FLAG_REPORT_TEXT).toBe(1 << 4)\n})\n\ntest(\"flags can be combined with bitwise OR\", () => {\n  // Verify flags can be combined properly\n  const combined = KITTY_FLAG_ALTERNATE_KEYS | KITTY_FLAG_EVENT_TYPES\n  expect(combined).toBe(0b110)\n  expect(combined).toBe(6)\n\n  // Check individual bits are set\n  expect(combined & KITTY_FLAG_ALTERNATE_KEYS).toBeTruthy()\n  expect(combined & KITTY_FLAG_EVENT_TYPES).toBeTruthy()\n  expect(combined & KITTY_FLAG_DISAMBIGUATE).toBeFalsy()\n})\n\ntest(\"escape sequences match kitty spec format\", () => {\n  // According to the spec, the push escape code is: CSI > flags u\n  // Where CSI = 0x1b 0x5b = \\x1b[\n  // So the format should be: \\x1b[>5u for DISAMBIGUATE | ALTERNATE_KEYS\n  // and \\x1b[>7u for DISAMBIGUATE | ALTERNATE_KEYS | EVENT_TYPES\n\n  const defaultFlags = buildKittyKeyboardFlags({})\n  expect(defaultFlags).toBe(5)\n  // The escape sequence would be: \\x1b[>5u\n\n  const withEventsFlags = buildKittyKeyboardFlags({ events: true })\n  expect(withEventsFlags).toBe(7)\n  // The escape sequence would be: \\x1b[>7u\n})\n\ntest(\"default config enables disambiguate and alternate keys\", () => {\n  // Default enables two key enhancements:\n  // 1. Disambiguate (0b1): Fixes ESC timing, alt+key ambiguity, ctrl+c becomes key event\n  // 2. Alternate keys (0b100): Reports shifted/base-layout keys for cross-keyboard shortcuts\n  const flags = buildKittyKeyboardFlags({})\n\n  // Should have disambiguate and alternate keys bits set\n  expect(flags & KITTY_FLAG_DISAMBIGUATE).toBeTruthy()\n  expect(flags & KITTY_FLAG_ALTERNATE_KEYS).toBeTruthy()\n\n  // Should NOT have other enhancements by default\n  expect(flags & KITTY_FLAG_EVENT_TYPES).toBeFalsy()\n  expect(flags & KITTY_FLAG_ALL_KEYS_AS_ESCAPES).toBeFalsy()\n  expect(flags & KITTY_FLAG_REPORT_TEXT).toBeFalsy()\n})\n\ntest(\"events config adds event type reporting\", () => {\n  // With events enabled, we should be able to detect press/repeat/release\n  const flags = buildKittyKeyboardFlags({ events: true })\n\n  // Should have disambiguate, alternate keys, and event types\n  expect(flags & KITTY_FLAG_DISAMBIGUATE).toBeTruthy()\n  expect(flags & KITTY_FLAG_ALTERNATE_KEYS).toBeTruthy()\n  expect(flags & KITTY_FLAG_EVENT_TYPES).toBeTruthy()\n\n  // Should NOT have other enhancements\n  expect(flags & KITTY_FLAG_ALL_KEYS_AS_ESCAPES).toBeFalsy()\n  expect(flags & KITTY_FLAG_REPORT_TEXT).toBeFalsy()\n})\n\ntest(\"disambiguate flag solves key ambiguity issues\", () => {\n  // The disambiguate flag (0b1) fixes several critical problems:\n  // 1. ESC key: Without it, sends raw 0x1b (ambiguous with escape sequence start)\n  //    With it: sends CSI 27;1u (unambiguous)\n  // 2. Alt+[: Without it, sends 0x1b 0x5b (same as CSI!)\n  //    With it: sends CSI 91;3u (unambiguous)\n  // 3. Ctrl+C: Without it, sends 0x03 (generates SIGINT, kills process)\n  //    With it: sends CSI 99;5u (delivered as key event to app)\n\n  const flags = buildKittyKeyboardFlags({})\n  expect(flags & KITTY_FLAG_DISAMBIGUATE).toBeTruthy()\n\n  // Per the spec: \"This has the nice side effect of making it much easier\n  // to integrate into the application event loop.\"\n  // No more timing-based hacks to distinguish ESC from escape sequences!\n})\n\ntest(\"can explicitly disable disambiguate\", () => {\n  const flags = buildKittyKeyboardFlags({ disambiguate: false })\n  expect(flags & KITTY_FLAG_DISAMBIGUATE).toBeFalsy()\n  expect(flags & KITTY_FLAG_ALTERNATE_KEYS).toBeTruthy() // still enabled by default\n})\n\ntest(\"can explicitly disable alternateKeys\", () => {\n  const flags = buildKittyKeyboardFlags({ alternateKeys: false })\n  expect(flags & KITTY_FLAG_ALTERNATE_KEYS).toBeFalsy()\n  expect(flags & KITTY_FLAG_DISAMBIGUATE).toBeTruthy() // still enabled by default\n})\n\ntest(\"can disable both disambiguate and alternateKeys\", () => {\n  const flags = buildKittyKeyboardFlags({ disambiguate: false, alternateKeys: false })\n  expect(flags).toBe(0)\n})\n\ntest(\"can enable all flags\", () => {\n  const flags = buildKittyKeyboardFlags({\n    disambiguate: true,\n    alternateKeys: true,\n    events: true,\n    allKeysAsEscapes: true,\n    reportText: true,\n  })\n\n  const expected =\n    KITTY_FLAG_DISAMBIGUATE |\n    KITTY_FLAG_ALTERNATE_KEYS |\n    KITTY_FLAG_EVENT_TYPES |\n    KITTY_FLAG_ALL_KEYS_AS_ESCAPES |\n    KITTY_FLAG_REPORT_TEXT\n\n  expect(flags).toBe(expected)\n  expect(flags).toBe(0b11111)\n  expect(flags).toBe(31)\n})\n\ntest(\"optional flags default to false\", () => {\n  const flags = buildKittyKeyboardFlags({})\n\n  // These default to true\n  expect(flags & KITTY_FLAG_DISAMBIGUATE).toBeTruthy()\n  expect(flags & KITTY_FLAG_ALTERNATE_KEYS).toBeTruthy()\n\n  // These default to false\n  expect(flags & KITTY_FLAG_EVENT_TYPES).toBeFalsy()\n  expect(flags & KITTY_FLAG_ALL_KEYS_AS_ESCAPES).toBeFalsy()\n  expect(flags & KITTY_FLAG_REPORT_TEXT).toBeFalsy()\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.mouse.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"bun:test\"\nimport { createTestRenderer, MouseButtons, type MockMouse, type TestRenderer } from \"../testing.js\"\nimport { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport type { RenderContext } from \"../types.js\"\nimport type { Selection } from \"../lib/selection.js\"\nimport type { MouseEvent } from \"../renderer.js\"\n\nclass TestRenderable extends Renderable {\n  public selectionActive = false\n\n  constructor(ctx: RenderContext, options: RenderableOptions) {\n    super(ctx, options)\n  }\n\n  public shouldStartSelection(_x: number, _y: number): boolean {\n    return this.selectable\n  }\n\n  public onSelectionChanged(selection: Selection | null): boolean {\n    this.selectionActive = !!selection?.isActive\n    return this.selectionActive\n  }\n}\n\ndescribe(\"renderer handleMouseData\", () => {\n  let renderer: TestRenderer\n  let mockMouse: MockMouse\n  let renderOnce: () => Promise<void>\n\n  beforeEach(async () => {\n    ;({ renderer, mockMouse, renderOnce } = await createTestRenderer({ width: 40, height: 20 }))\n  })\n  test(\"non-mouse input falls through to input handlers\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"input-target\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      const sequences: string[] = []\n      renderer.prependInputHandler((sequence) => {\n        sequences.push(sequence)\n        return true\n      })\n\n      let mouseDown = false\n      target.onMouseDown = () => {\n        mouseDown = true\n      }\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"x\"))\n      await Bun.sleep(10)\n\n      expect(sequences).toContain(\"x\")\n      expect(mouseDown).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"non-mouse buffers are routed to input handlers\", async () => {\n    try {\n      const sequences: string[] = []\n      renderer.prependInputHandler((sequence) => {\n        sequences.push(sequence)\n        return true\n      })\n\n      renderer.stdin.emit(\"data\", Buffer.from(\"x\"))\n      await Bun.sleep(10)\n\n      expect(sequences).toContain(\"x\")\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"dispatches mouse down/up to hit-tested renderable\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"target\",\n        position: \"absolute\",\n        left: 2,\n        top: 3,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      const events: Array<{ type: string; x: number; y: number; button: number }> = []\n      target.onMouseDown = (event) => {\n        events.push({ type: event.type, x: event.x, y: event.y, button: event.button })\n      }\n      target.onMouseUp = (event) => {\n        events.push({ type: event.type, x: event.x, y: event.y, button: event.button })\n      }\n\n      const clickX = target.x + 1\n      const clickY = target.y + 1\n      await mockMouse.click(clickX, clickY)\n\n      expect(events).toHaveLength(2)\n      expect(events[0]).toMatchObject({ type: \"down\", x: clickX, y: clickY, button: 0 })\n      expect(events[1]).toMatchObject({ type: \"up\", x: clickX, y: clickY, button: 0 })\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"emits over/out only when hover target changes\", async () => {\n    try {\n      const left = new TestRenderable(renderer, {\n        id: \"left\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      const right = new TestRenderable(renderer, {\n        id: \"right\",\n        position: \"absolute\",\n        left: 10,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(left)\n      renderer.root.add(right)\n      await renderOnce()\n\n      const hoverEvents: string[] = []\n      left.onMouseOver = () => hoverEvents.push(\"over:left\")\n      left.onMouseOut = () => hoverEvents.push(\"out:left\")\n      right.onMouseOver = () => hoverEvents.push(\"over:right\")\n      right.onMouseOut = () => hoverEvents.push(\"out:right\")\n\n      await mockMouse.moveTo(left.x + 1, left.y + 1)\n      await mockMouse.moveTo(right.x + 1, right.y + 1)\n      await mockMouse.moveTo(right.x + 2, right.y + 1)\n\n      expect(hoverEvents).toEqual([\"over:left\", \"out:left\", \"over:right\"])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"moving off a renderable emits out without a new target\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"hover-target\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      const hoverEvents: string[] = []\n      target.onMouseOver = () => hoverEvents.push(\"over\")\n      target.onMouseOut = () => hoverEvents.push(\"out\")\n\n      await mockMouse.moveTo(target.x + 1, target.y + 1)\n      await mockMouse.moveTo(renderer.width - 1, renderer.height - 1)\n\n      expect(hoverEvents).toEqual([\"over\", \"out\"])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"scroll events are delivered to the hit-tested renderable\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"scroll-target\",\n        position: \"absolute\",\n        left: 4,\n        top: 2,\n        width: 8,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let scrollEvent: MouseEvent | null = null\n      target.onMouseScroll = (event) => {\n        scrollEvent = event\n      }\n\n      await mockMouse.scroll(target.x + 1, target.y + 1, \"down\")\n\n      expect(scrollEvent?.type).toBe(\"scroll\")\n      expect(scrollEvent?.scroll?.direction).toBe(\"down\")\n      expect(scrollEvent?.scroll?.delta).toBe(1)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"scroll outside renderables does not dispatch events when nothing is focused\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"scroll-target\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 5,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let scrollCount = 0\n      target.onMouseScroll = () => {\n        scrollCount++\n      }\n\n      await mockMouse.scroll(renderer.width - 1, renderer.height - 1, \"down\")\n      expect(scrollCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"scroll outside hit target falls back to focused renderable\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"focused-scroll-target\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 5,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let scrollCount = 0\n      let lastDirection: string | undefined\n      target.onMouseScroll = (event) => {\n        scrollCount++\n        lastDirection = event.scroll?.direction\n      }\n\n      target.focusable = true\n      target.focus()\n      await mockMouse.scroll(renderer.width - 1, renderer.height - 1, \"down\")\n\n      expect(scrollCount).toBe(1)\n      expect(lastDirection).toBe(\"down\")\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"console mouse handling consumes events inside console bounds\", async () => {\n    try {\n      renderer.useConsole = true\n      renderer.console.show()\n\n      const target = new TestRenderable(renderer, {\n        id: \"background\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: renderer.width,\n        height: renderer.height,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let clicks = 0\n      target.onMouseDown = () => {\n        clicks++\n      }\n\n      const bounds = renderer.console.bounds\n      const insideX = Math.min(bounds.x + 1, renderer.width - 1)\n      const insideY = Math.min(bounds.y + 1, renderer.height - 1)\n      await mockMouse.click(insideX, insideY)\n      expect(clicks).toBe(0)\n\n      const outsideY = bounds.y > 0 ? bounds.y - 1 : Math.min(bounds.y + bounds.height, renderer.height - 1)\n      await mockMouse.click(insideX, outsideY)\n      expect(clicks).toBe(1)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"console mouse handling falls through when not handled\", async () => {\n    try {\n      renderer.useConsole = true\n      renderer.console.show()\n\n      const target = new TestRenderable(renderer, {\n        id: \"background\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: renderer.width,\n        height: renderer.height,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      const originalHandle = renderer.console.handleMouse.bind(renderer.console)\n      let consoleCalls = 0\n      renderer.console.handleMouse = () => {\n        consoleCalls++\n        return false\n      }\n\n      let clicks = 0\n      target.onMouseDown = () => {\n        clicks++\n      }\n\n      const bounds = renderer.console.bounds\n      const insideX = Math.min(bounds.x + 1, renderer.width - 1)\n      const insideY = Math.min(bounds.y + 1, renderer.height - 1)\n      await mockMouse.pressDown(insideX, insideY)\n\n      const outsideY = bounds.y > 0 ? bounds.y - 1 : Math.min(bounds.y + bounds.height, renderer.height - 1)\n      await mockMouse.release(insideX, outsideY)\n\n      expect(consoleCalls).toBe(1)\n      expect(clicks).toBe(1)\n\n      renderer.console.handleMouse = originalHandle\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"selection drag marks events as dragging and ends on mouse up\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"selectable\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 12,\n        height: 6,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      let dragEvent: MouseEvent | null = null\n      let upEvent: MouseEvent | null = null\n      target.onMouseDrag = (event) => {\n        dragEvent = event\n      }\n      target.onMouseUp = (event) => {\n        upEvent = event\n      }\n\n      const startX = target.x + 1\n      const startY = target.y + 1\n      const endX = target.x + 6\n      const endY = target.y + 3\n\n      await mockMouse.pressDown(startX, startY)\n      await mockMouse.moveTo(endX, endY)\n      await mockMouse.release(endX, endY)\n\n      expect(renderer.hasSelection).toBe(true)\n      expect(dragEvent?.isDragging).toBe(true)\n      expect(upEvent?.isDragging).toBe(true)\n      expect(renderer.getSelection()?.isDragging).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"selection drag updates focus even when pointer leaves renderables\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"selectable\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      let dragCount = 0\n      let upCount = 0\n      target.onMouseDrag = () => {\n        dragCount++\n      }\n      target.onMouseUp = () => {\n        upCount++\n      }\n\n      const startX = target.x + 1\n      const startY = target.y + 1\n      const endX = renderer.width - 1\n      const endY = renderer.height - 1\n\n      await mockMouse.pressDown(startX, startY)\n      await mockMouse.moveTo(endX, endY)\n      await mockMouse.release(endX, endY)\n\n      const selection = renderer.getSelection()\n      expect(selection).not.toBeNull()\n      expect(selection?.focus).toEqual({ x: endX, y: endY })\n      expect(dragCount).toBe(0)\n      expect(upCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"ctrl+click extends selection instead of clearing\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"selectable-ctrl\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 12,\n        height: 6,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      await mockMouse.drag(target.x + 1, target.y + 1, target.x + 4, target.y + 1)\n      const selectionBefore = renderer.getSelection()\n      expect(selectionBefore).not.toBeNull()\n\n      const nextX = target.x + 2\n      const nextY = target.y + 4\n      await mockMouse.pressDown(nextX, nextY, MouseButtons.LEFT, { modifiers: { ctrl: true } })\n      await mockMouse.release(nextX, nextY, MouseButtons.LEFT, { modifiers: { ctrl: true } })\n\n      const selectionAfter = renderer.getSelection()\n      expect(selectionAfter).not.toBeNull()\n      expect(selectionAfter?.focus).toEqual({ x: nextX, y: nextY })\n      expect(renderer.hasSelection).toBe(true)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"ctrl+click with selection updates focus without mouse down\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"selectable-ctrl-branch\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 12,\n        height: 6,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      await mockMouse.drag(target.x + 1, target.y + 1, target.x + 4, target.y + 1)\n      expect(renderer.getSelection()).not.toBeNull()\n\n      let downCount = 0\n      target.onMouseDown = () => {\n        downCount++\n      }\n\n      const nextX = target.x + 2\n      const nextY = target.y + 4\n      await mockMouse.pressDown(nextX, nextY, MouseButtons.LEFT, { modifiers: { ctrl: true } })\n\n      expect(renderer.getSelection()?.isDragging).toBe(true)\n      expect(downCount).toBe(0)\n\n      await mockMouse.release(nextX, nextY, MouseButtons.LEFT, { modifiers: { ctrl: true } })\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"ctrl+click with selection does not auto-focus\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"selectable-ctrl-focus\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 12,\n        height: 6,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      await mockMouse.drag(target.x + 1, target.y + 1, target.x + 4, target.y + 1)\n      expect(renderer.getSelection()).not.toBeNull()\n\n      target.focusable = true\n      expect(target.focused).toBe(false)\n\n      const nextX = target.x + 2\n      const nextY = target.y + 4\n      await mockMouse.pressDown(nextX, nextY, MouseButtons.LEFT, { modifiers: { ctrl: true } })\n      await mockMouse.release(nextX, nextY, MouseButtons.LEFT, { modifiers: { ctrl: true } })\n\n      expect(target.focused).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"right click does not start selection\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"right-click\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 8,\n        height: 4,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      await mockMouse.click(target.x + 1, target.y + 1, MouseButtons.RIGHT)\n      expect(renderer.hasSelection).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"preventDefault keeps selection while empty click clears it\", async () => {\n    try {\n      const selectable = new TestRenderable(renderer, {\n        id: \"selectable-main\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 12,\n        height: 6,\n      })\n      selectable.selectable = true\n      renderer.root.add(selectable)\n\n      const blocker = new TestRenderable(renderer, {\n        id: \"blocker\",\n        position: \"absolute\",\n        left: 20,\n        top: 2,\n        width: 8,\n        height: 4,\n      })\n      renderer.root.add(blocker)\n      await renderOnce()\n\n      await mockMouse.drag(selectable.x + 1, selectable.y + 1, selectable.x + 4, selectable.y + 1)\n      expect(renderer.hasSelection).toBe(true)\n\n      blocker.onMouseDown = (event) => {\n        event.preventDefault()\n      }\n      await mockMouse.click(blocker.x + 1, blocker.y + 1)\n      expect(renderer.hasSelection).toBe(true)\n\n      await mockMouse.click(renderer.width - 1, renderer.height - 1)\n      expect(renderer.hasSelection).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"clicking another renderable clears selection when not prevented\", async () => {\n    try {\n      const selectable = new TestRenderable(renderer, {\n        id: \"selectable-clear\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 10,\n        height: 5,\n      })\n      selectable.selectable = true\n      renderer.root.add(selectable)\n\n      const other = new TestRenderable(renderer, {\n        id: \"other\",\n        position: \"absolute\",\n        left: 20,\n        top: 2,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(other)\n      await renderOnce()\n\n      await mockMouse.drag(selectable.x + 1, selectable.y + 1, selectable.x + 4, selectable.y + 1)\n      expect(renderer.hasSelection).toBe(true)\n\n      await mockMouse.click(other.x + 1, other.y + 1)\n      expect(renderer.hasSelection).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"drag capture delivers drag-end and drop with source\", async () => {\n    try {\n      const source = new TestRenderable(renderer, {\n        id: \"source\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      const target = new TestRenderable(renderer, {\n        id: \"target\",\n        position: \"absolute\",\n        left: 12,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(source)\n      renderer.root.add(target)\n      await renderOnce()\n\n      const events: string[] = []\n      let dropSource: Renderable | undefined\n      let overSource: Renderable | undefined\n      let targetDragged = false\n\n      source.onMouseDrag = () => {\n        events.push(\"drag:source\")\n      }\n      source.onMouseDragEnd = () => {\n        events.push(\"drag-end:source\")\n      }\n      source.onMouseUp = () => {\n        events.push(\"up:source\")\n      }\n      target.onMouseDrop = (event) => {\n        events.push(\"drop:target\")\n        dropSource = event.source\n      }\n      target.onMouseOver = (event) => {\n        overSource = event.source\n      }\n      target.onMouseDrag = () => {\n        targetDragged = true\n      }\n\n      await mockMouse.drag(source.x + 1, source.y + 1, target.x + 1, target.y + 1)\n\n      expect(events).toContain(\"drag-end:source\")\n      expect(events).toContain(\"up:source\")\n      expect(events).toContain(\"drop:target\")\n      expect(dropSource).toBe(source)\n      expect(overSource).toBe(source)\n      expect(targetDragged).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"captured drag release fires drop then mouse up on target\", async () => {\n    try {\n      const source = new TestRenderable(renderer, {\n        id: \"source-drop-order\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      const target = new TestRenderable(renderer, {\n        id: \"target-drop-order\",\n        position: \"absolute\",\n        left: 12,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(source)\n      renderer.root.add(target)\n      await renderOnce()\n\n      const events: string[] = []\n      target.onMouseDrop = () => {\n        events.push(\"drop\")\n      }\n      target.onMouseUp = () => {\n        events.push(\"up\")\n      }\n\n      await mockMouse.drag(source.x + 1, source.y + 1, target.x + 1, target.y + 1)\n\n      expect(events).toEqual([\"drop\", \"up\"])\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"captured drag keeps routing drag events to source\", async () => {\n    try {\n      const source = new TestRenderable(renderer, {\n        id: \"source-capture\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      const target = new TestRenderable(renderer, {\n        id: \"target-capture\",\n        position: \"absolute\",\n        left: 12,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(source)\n      renderer.root.add(target)\n      await renderOnce()\n\n      let sourceDragCount = 0\n      let targetDragCount = 0\n      source.onMouseDrag = () => {\n        sourceDragCount++\n      }\n      target.onMouseDrag = () => {\n        targetDragCount++\n      }\n\n      await mockMouse.pressDown(source.x + 1, source.y + 1)\n      await mockMouse.moveTo(source.x + 2, source.y + 1)\n      await mockMouse.moveTo(target.x + 1, target.y + 1)\n      await mockMouse.moveTo(target.x + 2, target.y + 1)\n      await mockMouse.release(target.x + 2, target.y + 1)\n\n      expect(sourceDragCount).toBeGreaterThan(1)\n      expect(targetDragCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"captured drag does not emit out on the captured renderable\", async () => {\n    try {\n      const source = new TestRenderable(renderer, {\n        id: \"source\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      const target = new TestRenderable(renderer, {\n        id: \"target\",\n        position: \"absolute\",\n        left: 12,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(source)\n      renderer.root.add(target)\n      await renderOnce()\n\n      let outCount = 0\n      source.onMouseOut = () => {\n        outCount++\n      }\n\n      await mockMouse.drag(source.x + 1, source.y + 1, target.x + 1, target.y + 1)\n\n      expect(outCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"non-left drag does not capture and routes by hit test\", async () => {\n    try {\n      const source = new TestRenderable(renderer, {\n        id: \"source-right-drag\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      const target = new TestRenderable(renderer, {\n        id: \"target-right-drag\",\n        position: \"absolute\",\n        left: 12,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(source)\n      renderer.root.add(target)\n      await renderOnce()\n\n      let sourceDragCount = 0\n      let targetDragCount = 0\n      source.onMouseDrag = () => {\n        sourceDragCount++\n      }\n      target.onMouseDrag = () => {\n        targetDragCount++\n      }\n\n      await mockMouse.drag(source.x + 1, source.y + 1, target.x + 1, target.y + 1, MouseButtons.RIGHT)\n\n      expect(sourceDragCount).toBeGreaterThan(0)\n      expect(targetDragCount).toBeGreaterThan(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"non-captured drag emits over/out transitions\", async () => {\n    try {\n      const source = new TestRenderable(renderer, {\n        id: \"source-drag-hover\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      const target = new TestRenderable(renderer, {\n        id: \"target-drag-hover\",\n        position: \"absolute\",\n        left: 12,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(source)\n      renderer.root.add(target)\n      await renderOnce()\n\n      const events: string[] = []\n      source.onMouseOver = () => events.push(\"over:source\")\n      source.onMouseOut = () => events.push(\"out:source\")\n      target.onMouseOver = () => events.push(\"over:target\")\n\n      await mockMouse.moveTo(source.x + 1, source.y + 1)\n      await mockMouse.drag(source.x + 1, source.y + 1, target.x + 1, target.y + 1, MouseButtons.RIGHT)\n\n      expect(events).toContain(\"over:source\")\n      expect(events).toContain(\"out:source\")\n      expect(events).toContain(\"over:target\")\n      expect(events.indexOf(\"out:source\")).toBeGreaterThan(events.indexOf(\"over:source\"))\n      expect(events.indexOf(\"over:target\")).toBeGreaterThan(events.indexOf(\"out:source\"))\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"move events include modifier flags\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"modifiers\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let modifiers: MouseEvent[\"modifiers\"] | null = null\n      target.onMouseMove = (event) => {\n        modifiers = event.modifiers\n      }\n\n      await mockMouse.moveTo(target.x + 1, target.y + 1, {\n        modifiers: { shift: true, alt: true },\n      })\n\n      expect(modifiers).toEqual({ shift: true, alt: true, ctrl: false })\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"basic mouse mode sequences are parsed and dispatched\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"basic-mode\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let downCount = 0\n      let upCount = 0\n      target.onMouseDown = () => {\n        downCount++\n      }\n      target.onMouseUp = () => {\n        upCount++\n      }\n\n      const clickX = target.x + 1\n      const clickY = target.y + 1\n      const encodeBasic = (buttonByte: number, x: number, y: number) => {\n        return (\n          \"\\x1b[M\" + String.fromCharCode(buttonByte + 32) + String.fromCharCode(x + 33) + String.fromCharCode(y + 33)\n        )\n      }\n\n      renderer.stdin.emit(\"data\", Buffer.from(encodeBasic(0, clickX, clickY)))\n      renderer.stdin.emit(\"data\", Buffer.from(encodeBasic(3, clickX, clickY)))\n\n      expect(downCount).toBe(1)\n      expect(upCount).toBe(1)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"overflow hidden clips hit grid for mouse events\", async () => {\n    try {\n      const container = new TestRenderable(renderer, {\n        id: \"container\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 6,\n        height: 4,\n        overflow: \"hidden\",\n      })\n      const child = new TestRenderable(renderer, {\n        id: \"child\",\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: 10,\n        height: 4,\n      })\n      container.add(child)\n      renderer.root.add(container)\n      await renderOnce()\n\n      let clicks = 0\n      child.onMouseDown = () => {\n        clicks++\n      }\n\n      await mockMouse.click(container.x + 1, container.y + 1)\n      expect(clicks).toBe(1)\n\n      const outsideX = container.x + container.width + 1\n      await mockMouse.click(outsideX, container.y + 1)\n      expect(clicks).toBe(1)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"shouldStartSelection false does not start selection\", async () => {\n    try {\n      class NoSelectionStartRenderable extends TestRenderable {\n        public shouldStartSelection(): boolean {\n          return false\n        }\n      }\n\n      const target = new NoSelectionStartRenderable(renderer, {\n        id: \"no-selection-start\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 6,\n        height: 4,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      let downCount = 0\n      target.onMouseDown = () => {\n        downCount++\n      }\n\n      await mockMouse.click(target.x + 1, target.y + 1)\n\n      expect(downCount).toBe(1)\n      expect(renderer.hasSelection).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"destroyed renderable does not start selection\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"destroyed-selectable\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 6,\n        height: 4,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      let downCount = 0\n      target.onMouseDown = () => {\n        downCount++\n      }\n\n      target.destroy()\n      await renderOnce()\n\n      await mockMouse.click(target.x + 1, target.y + 1)\n\n      expect(downCount).toBe(0)\n      expect(renderer.hasSelection).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"ctrl+click without selection does not start selection\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"ctrl-no-selection\",\n        position: \"absolute\",\n        left: 2,\n        top: 2,\n        width: 6,\n        height: 4,\n      })\n      target.selectable = true\n      renderer.root.add(target)\n      await renderOnce()\n\n      let downCount = 0\n      target.onMouseDown = () => {\n        downCount++\n      }\n\n      await mockMouse.click(target.x + 1, target.y + 1, MouseButtons.LEFT, { modifiers: { ctrl: true } })\n\n      expect(downCount).toBe(1)\n      expect(renderer.hasSelection).toBe(false)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"captured drag release on empty space skips drop\", async () => {\n    try {\n      const source = new TestRenderable(renderer, {\n        id: \"source-empty-drop\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      const target = new TestRenderable(renderer, {\n        id: \"target-empty-drop\",\n        position: \"absolute\",\n        left: 15,\n        top: 1,\n        width: 6,\n        height: 4,\n      })\n      renderer.root.add(source)\n      renderer.root.add(target)\n      await renderOnce()\n\n      let dragEndCount = 0\n      let upCount = 0\n      let dropCount = 0\n      source.onMouseDragEnd = () => {\n        dragEndCount++\n      }\n      source.onMouseUp = () => {\n        upCount++\n      }\n      target.onMouseDrop = () => {\n        dropCount++\n      }\n\n      const startX = source.x + 1\n      const startY = source.y + 1\n      const endX = renderer.width - 1\n      const endY = renderer.height - 1\n\n      await mockMouse.pressDown(startX, startY)\n      await mockMouse.moveTo(source.x + 2, startY)\n      await mockMouse.moveTo(endX, endY)\n      await mockMouse.release(endX, endY)\n\n      expect(dragEndCount).toBe(1)\n      expect(upCount).toBe(1)\n      expect(dropCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"mouse out is not fired on a destroyed renderable\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"destroyed-hover\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 4,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let overCount = 0\n      let outCount = 0\n      target.onMouseOver = () => {\n        overCount++\n      }\n      target.onMouseOut = () => {\n        outCount++\n      }\n\n      await mockMouse.moveTo(target.x + 1, target.y + 1)\n      expect(overCount).toBe(1)\n\n      target.destroy()\n      await renderOnce()\n\n      await mockMouse.moveTo(renderer.width - 1, renderer.height - 1)\n      expect(outCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"mouse out is not fired on a destroyed renderable before render\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"destroyed-hover-before-render\",\n        position: \"absolute\",\n        left: 1,\n        top: 1,\n        width: 4,\n        height: 4,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let overCount = 0\n      let outCount = 0\n      target.onMouseOver = () => {\n        overCount++\n      }\n      target.onMouseOut = () => {\n        outCount++\n      }\n\n      await mockMouse.moveTo(target.x + 1, target.y + 1)\n      expect(overCount).toBe(1)\n\n      // Destroy without rendering — the hit grid still has the old state,\n      // so the next mouse move hits handleMouseData's \"out\" path directly\n      target.destroy()\n\n      await mockMouse.moveTo(renderer.width - 1, renderer.height - 1)\n      expect(outCount).toBe(0)\n    } finally {\n      renderer.destroy()\n    }\n  })\n})\n\ndescribe(\"renderer handleMouseData split height\", () => {\n  const baseHeight = 20\n  const splitHeight = 6\n\n  let renderer: TestRenderer\n  let mockMouse: MockMouse\n  let renderOnce: () => Promise<void>\n\n  beforeEach(async () => {\n    ;({ renderer, mockMouse, renderOnce } = await createTestRenderer({\n      width: 40,\n      height: baseHeight,\n      experimental_splitHeight: splitHeight,\n    }))\n  })\n\n  test(\"split height offsets mouse coordinates and ignores events above render area\", async () => {\n    try {\n      const target = new TestRenderable(renderer, {\n        id: \"split-target\",\n        position: \"absolute\",\n        left: 2,\n        top: 1,\n        width: 6,\n        height: 3,\n      })\n      renderer.root.add(target)\n      await renderOnce()\n\n      let downEvent: MouseEvent | null = null\n      target.onMouseDown = (event) => {\n        downEvent = event\n      }\n\n      const renderOffset = baseHeight - splitHeight\n      await mockMouse.click(target.x + 1, Math.max(0, renderOffset - 1))\n      expect(downEvent).toBeNull()\n\n      const screenY = renderOffset + target.y + 1\n      await mockMouse.click(target.x + 1, screenY)\n      expect(downEvent?.y).toBe(target.y + 1)\n    } finally {\n      renderer.destroy()\n    }\n  })\n\n  test(\"split height returns false for input above render area\", async () => {\n    try {\n      const sequences: string[] = []\n      renderer.addInputHandler((sequence) => {\n        sequences.push(sequence)\n        return true\n      })\n\n      await renderOnce()\n\n      const renderOffset = baseHeight - splitHeight\n      const beforeSequences = sequences.length\n      await mockMouse.click(1, Math.max(0, renderOffset - 1))\n      await Bun.sleep(10)\n\n      expect(sequences.length).toBeGreaterThan(beforeSequences)\n    } finally {\n      renderer.destroy()\n    }\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.palette.test.ts",
    "content": "import { test, expect, describe } from \"bun:test\"\nimport { createTestRenderer, type TestRendererOptions } from \"../testing/test-renderer.js\"\nimport { EventEmitter } from \"events\"\nimport { Buffer } from \"node:buffer\"\nimport { Readable } from \"node:stream\"\nimport tty from \"tty\"\nimport { ManualClock } from \"../testing/manual-clock\"\nimport type { GetPaletteOptions, TerminalColors } from \"../lib/terminal-palette\"\n\nconst OSC_SUPPORT_TIMEOUT_MS = 300\n\nfunction flushAsync(): Promise<void> {\n  return Promise.resolve().then(() => Promise.resolve())\n}\n\nfunction schedule(clock: ManualClock | undefined, fn: () => void): void {\n  if (clock) {\n    clock.setTimeout(fn, 0)\n    return\n  }\n\n  process.nextTick(fn)\n}\n\nfunction createMockStreams(clock?: ManualClock) {\n  const mockStdin = new Readable({ read() {} }) as tty.ReadStream\n  mockStdin.isTTY = true\n  mockStdin.setRawMode = () => mockStdin\n  mockStdin.resume = () => mockStdin\n  mockStdin.pause = () => mockStdin\n  mockStdin.setEncoding = () => mockStdin\n\n  const writes: string[] = []\n  const mockStdout = {\n    isTTY: true,\n    columns: 80,\n    rows: 24,\n    write: (data: string | Buffer) => {\n      writes.push(data.toString())\n      const dataStr = data.toString()\n      if (dataStr === \"\\x1b]4;0;?\\x07\") {\n        schedule(clock, () => {\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;rgb:0000/0000/0000\\x07\"))\n        })\n      } else if (dataStr.includes(\"\\x1b]4;\")) {\n        schedule(clock, () => {\n          for (let i = 0; i < 16; i++) {\n            mockStdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};rgb:1000/2000/3000\\x07`))\n          }\n        })\n      } else if (dataStr.includes(\"\\x1b]10;?\")) {\n        schedule(clock, () => {\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]10;#ffffff\\x07\"))\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]11;#000000\\x07\"))\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]12;#00ff00\\x07\"))\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]13;#ffffff\\x07\"))\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]14;#000000\\x07\"))\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]15;#ffffff\\x07\"))\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]16;#000000\\x07\"))\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]17;#333333\\x07\"))\n          mockStdin.emit(\"data\", Buffer.from(\"\\x1b]19;#cccccc\\x07\"))\n        })\n      }\n      return true\n    },\n  } as any\n\n  return { mockStdin, mockStdout, writes }\n}\n\nasync function advancePaletteClock(clock: ManualClock, ms: number): Promise<void> {\n  await flushAsync()\n  // Flush queued 0ms mock terminal responses before advancing the real timeout window.\n  clock.advance(0)\n  await flushAsync()\n  clock.advance(ms)\n  await flushAsync()\n}\n\nasync function detectPaletteAndAdvanceClock(\n  renderer: {\n    getPalette(options?: GetPaletteOptions): Promise<TerminalColors>\n    paletteDetectionStatus: \"idle\" | \"detecting\" | \"cached\"\n  },\n  clock: ManualClock,\n  options?: GetPaletteOptions,\n): Promise<TerminalColors> {\n  const palettePromise = renderer.getPalette(options)\n\n  if (renderer.paletteDetectionStatus === \"detecting\") {\n    const detectionTimeoutMs = Math.max(options?.timeout ?? 5000, OSC_SUPPORT_TIMEOUT_MS)\n    await advancePaletteClock(clock, detectionTimeoutMs)\n  } else {\n    await flushAsync()\n  }\n\n  return palettePromise\n}\n\nasync function createPaletteRenderer(options: Partial<TestRendererOptions> = {}) {\n  const clock = options.clock instanceof ManualClock ? options.clock : new ManualClock()\n  const { mockStdin, mockStdout, writes } = createMockStreams(clock)\n  const { renderer } = await createTestRenderer({\n    stdin: mockStdin,\n    stdout: mockStdout,\n    ...options,\n    clock,\n  })\n\n  return { renderer, mockStdin, mockStdout, writes, clock }\n}\n\ndescribe(\"Palette caching behavior\", () => {\n  test(\"getPalette returns cached palette on subsequent calls\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    const palette1 = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n    const palette2 = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    expect(palette1).toBe(palette2)\n    expect(palette1).toEqual(palette2)\n\n    renderer.destroy()\n  })\n\n  test(\"getPalette caches correctly with non-256 size parameter\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    const palette1 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n    const palette2 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n\n    expect(palette1).toBe(palette2)\n    expect(renderer.paletteDetectionStatus).toBe(\"cached\")\n\n    renderer.destroy()\n  })\n\n  test(\"cached palette is returned instantly\", async () => {\n    const { renderer, clock, mockStdin, mockStdout, writes } = await createPaletteRenderer()\n\n    await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n    const writeCountAfterFirst = writes.length\n\n    const timeAfterFirstDetection = clock.now()\n    await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    expect(clock.now()).toBe(timeAfterFirstDetection)\n    expect(writes.length).toBe(writeCountAfterFirst)\n\n    renderer.destroy()\n  })\n\n  test(\"multiple concurrent calls share same detection\", async () => {\n    const { renderer, clock, mockStdin, mockStdout, writes } = await createPaletteRenderer()\n\n    const palettePromises = [\n      renderer.getPalette({ timeout: 300 }),\n      renderer.getPalette({ timeout: 300 }),\n      renderer.getPalette({ timeout: 300 }),\n    ]\n\n    await advancePaletteClock(clock, 300)\n\n    const [palette1, palette2, palette3] = await Promise.all(palettePromises)\n\n    expect(palette1).toBe(palette2)\n    expect(palette2).toBe(palette3)\n\n    const oscSupportChecks = writes.filter((w) => w.includes(\"\\x1b]4;0;?\"))\n    expect(oscSupportChecks.length).toBeLessThanOrEqual(2)\n\n    renderer.destroy()\n  })\n\n  test(\"palette detector created only once\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    // @ts-expect-error - accessing private property for testing\n    expect(renderer._paletteDetector).toBeNull()\n\n    await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    // @ts-expect-error - accessing private property for testing\n    const detector1 = renderer._paletteDetector\n    expect(detector1).not.toBeNull()\n\n    await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    // @ts-expect-error - accessing private property for testing\n    const detector2 = renderer._paletteDetector\n    expect(detector1).toBe(detector2)\n\n    renderer.destroy()\n  })\n\n  test(\"cache persists with different timeout values\", async () => {\n    const { renderer, clock, mockStdin, mockStdout, writes } = await createPaletteRenderer()\n\n    const palette1 = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 100 })\n    const writeCountAfterFirst = writes.length\n\n    const palette2 = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 5000 })\n\n    expect(writes.length).toBe(writeCountAfterFirst)\n    expect(palette1).toBe(palette2)\n\n    renderer.destroy()\n  })\n\n  test(\"cache persists across renderer lifecycle\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    const palette1 = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    renderer.start()\n    await flushAsync()\n    renderer.pause()\n    renderer.suspend()\n    renderer.resume()\n    renderer.stop()\n\n    const palette2 = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 100 })\n    expect(palette1).toBe(palette2)\n\n    renderer.destroy()\n  })\n})\n\ndescribe(\"Palette detection with non-TTY\", () => {\n  test(\"handles non-TTY streams gracefully\", async () => {\n    const clock = new ManualClock()\n    const mockStdin = new EventEmitter() as any\n    mockStdin.isTTY = false\n    mockStdin.setRawMode = () => {}\n    mockStdin.resume = () => {}\n    mockStdin.pause = () => {}\n    mockStdin.setEncoding = () => {}\n\n    const mockStdout = {\n      isTTY: false,\n      columns: 80,\n      rows: 24,\n      write: () => true,\n    } as any\n\n    const { renderer } = await createTestRenderer({\n      stdin: mockStdin,\n      stdout: mockStdout,\n      clock,\n    })\n\n    const palette = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 100 })\n\n    expect(typeof palette === \"object\" && palette !== null && Array.isArray(palette.palette)).toBe(true)\n\n    const cached = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 100 })\n    expect(palette).toBe(cached)\n\n    renderer.destroy()\n  })\n})\n\ndescribe(\"Palette detection with OSC responses\", () => {\n  test(\"detects colors from OSC responses\", async () => {\n    const clock = new ManualClock()\n    const mockStdin = new EventEmitter() as any\n    mockStdin.isTTY = true\n    mockStdin.setRawMode = () => {}\n    mockStdin.resume = () => {}\n    mockStdin.pause = () => {}\n    mockStdin.setEncoding = () => {}\n\n    const mockStdout = {\n      isTTY: true,\n      columns: 80,\n      rows: 24,\n      write: (data: string | Buffer) => {\n        const dataStr = data.toString()\n        clock.setTimeout(() => {\n          if (dataStr.includes(\"\\x1b]4;0;?\")) {\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#000000\\x07\"))\n          }\n          if (dataStr.match(/\\x1b\\]4;\\d+;/g)) {\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;#000000\\x07\"))\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;#ff0000\\x07\"))\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;2;#00ff00\\x07\"))\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;3;#0000ff\\x07\"))\n            for (let i = 4; i < 256; i++) {\n              mockStdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};#808080\\x07`))\n            }\n          }\n        }, 0)\n        return true\n      },\n    } as any\n\n    const { renderer } = await createTestRenderer({\n      stdin: mockStdin,\n      stdout: mockStdout,\n      useThread: false,\n      clock,\n    })\n\n    const palette = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    expect(typeof palette === \"object\" && palette !== null && Array.isArray(palette.palette)).toBe(true)\n    expect(palette.palette.length).toBeGreaterThanOrEqual(16)\n    expect(palette.palette[0]).toBe(\"#000000\")\n    expect(palette.palette[1]).toBe(\"#ff0000\")\n    expect(palette.palette[2]).toBe(\"#00ff00\")\n    expect(palette.palette[3]).toBe(\"#0000ff\")\n\n    const cached = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 100 })\n    expect(palette).toBe(cached)\n\n    renderer.destroy()\n  })\n\n  test(\"handles RGB format responses\", async () => {\n    const clock = new ManualClock()\n    const mockStdin = new EventEmitter() as any\n    mockStdin.isTTY = true\n    mockStdin.setRawMode = () => {}\n    mockStdin.resume = () => {}\n    mockStdin.pause = () => {}\n    mockStdin.setEncoding = () => {}\n\n    const mockStdout = {\n      isTTY: true,\n      columns: 80,\n      rows: 24,\n      write: (data: string | Buffer) => {\n        const dataStr = data.toString()\n        clock.setTimeout(() => {\n          if (dataStr.includes(\"\\x1b]4;0;?\")) {\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;rgb:0000/0000/0000\\x07\"))\n          }\n          if (dataStr.match(/\\x1b\\]4;\\d+;/g)) {\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;0;rgb:0000/0000/0000\\x07\"))\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;1;rgb:ffff/0000/0000\\x07\"))\n            mockStdin.emit(\"data\", Buffer.from(\"\\x1b]4;2;rgb:8000/8000/8000\\x07\"))\n            for (let i = 3; i < 256; i++) {\n              mockStdin.emit(\"data\", Buffer.from(`\\x1b]4;${i};rgb:1111/1111/1111\\x07`))\n            }\n          }\n        }, 0)\n        return true\n      },\n    } as any\n\n    const { renderer } = await createTestRenderer({\n      stdin: mockStdin,\n      stdout: mockStdout,\n      useThread: false,\n      clock,\n    })\n\n    const palette = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    expect(palette.palette[0]).toBe(\"#000000\")\n    expect(palette.palette[1]).toBe(\"#ff0000\")\n    expect(palette.palette[2]).toBe(\"#808080\")\n\n    renderer.destroy()\n  })\n})\n\ndescribe(\"Palette integration tests\", () => {\n  test(\"palette detection does not interfere with input handling\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    const keysReceived: string[] = []\n    renderer.keyInput.on(\"keypress\", (event) => {\n      keysReceived.push(event.name || \"unknown\")\n    })\n\n    const palettePromise = renderer.getPalette({ timeout: 300 })\n\n    mockStdin.emit(\"data\", Buffer.from(\"a\"))\n    mockStdin.emit(\"data\", Buffer.from(\"b\"))\n    mockStdin.emit(\"data\", Buffer.from(\"c\"))\n\n    await flushAsync()\n\n    expect(keysReceived.length).toBeGreaterThanOrEqual(3)\n\n    await advancePaletteClock(clock, 300)\n    await palettePromise\n\n    renderer.destroy()\n  })\n\n  test(\"getPalette works with different renderer configurations\", async () => {\n    const configs = [{ width: 40, height: 10 }, { width: 120, height: 40 }, { useMouse: false }]\n\n    for (const config of configs) {\n      const { renderer: testRenderer, clock, mockStdin, mockStdout } = await createPaletteRenderer(config)\n\n      const palette = await detectPaletteAndAdvanceClock(testRenderer, clock, { timeout: 300 })\n      expect(typeof palette === \"object\" && palette !== null && Array.isArray(palette.palette)).toBe(true)\n\n      const cached = await detectPaletteAndAdvanceClock(testRenderer, clock, { timeout: 100 })\n      expect(palette).toBe(cached)\n\n      testRenderer.destroy()\n    }\n  })\n})\n\ndescribe(\"Palette cache invalidation\", () => {\n  test(\"clearPaletteCache invalidates cache\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    const palette1 = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n    expect(renderer.paletteDetectionStatus).toBe(\"cached\")\n\n    renderer.clearPaletteCache()\n    expect(renderer.paletteDetectionStatus).toBe(\"idle\")\n\n    const palette2 = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    expect(palette1).not.toBe(palette2)\n    expect(renderer.paletteDetectionStatus).toBe(\"cached\")\n\n    renderer.destroy()\n  })\n\n  test(\"paletteDetectionStatus tracks detection lifecycle\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    expect(renderer.paletteDetectionStatus).toBe(\"idle\")\n\n    const palettePromise = renderer.getPalette({ timeout: 300 })\n    expect(renderer.paletteDetectionStatus).toBe(\"detecting\")\n\n    await advancePaletteClock(clock, 300)\n    await palettePromise\n    expect(renderer.paletteDetectionStatus).toBe(\"cached\")\n\n    renderer.destroy()\n  })\n})\n\ndescribe(\"Palette detection with suspended renderer\", () => {\n  test(\"getPalette throws error when renderer is suspended\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    renderer.suspend()\n\n    await expect(renderer.getPalette({ timeout: 300 })).rejects.toThrow(\n      \"Cannot detect palette while renderer is suspended\",\n    )\n\n    renderer.destroy()\n  })\n\n  test(\"getPalette works after resume\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    renderer.suspend()\n    renderer.resume()\n\n    const palette = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n    expect(typeof palette === \"object\" && palette !== null && Array.isArray(palette.palette)).toBe(true)\n\n    renderer.destroy()\n  })\n})\n\ndescribe(\"Palette detector cleanup\", () => {\n  test(\"destroy cleans up palette detector\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    renderer.destroy()\n\n    // @ts-expect-error - accessing private property for testing\n    expect(renderer._paletteDetector).toBeNull()\n    // @ts-expect-error - accessing private property for testing\n    expect(renderer._paletteDetectionPromise).toBeNull()\n    // @ts-expect-error - accessing private property for testing\n    expect(renderer._cachedPalette).toBeNull()\n  })\n\n  test(\"multiple destroy calls don't cause errors\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 300 })\n\n    expect(() => {\n      renderer.destroy()\n      renderer.destroy()\n      renderer.destroy()\n    }).not.toThrow()\n  })\n\n  test(\"palette detection uses router OSC source without extra stdin listeners\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    const baselineListenerCount = mockStdin.listenerCount(\"data\")\n    const palettePromise = renderer.getPalette({ timeout: 300 })\n\n    const duringDetectionCount = mockStdin.listenerCount(\"data\")\n    expect(duringDetectionCount).toBe(baselineListenerCount)\n\n    await advancePaletteClock(clock, 300)\n    await palettePromise\n\n    const afterDetectionCount = mockStdin.listenerCount(\"data\")\n    expect(afterDetectionCount).toBe(baselineListenerCount)\n\n    renderer.destroy()\n  })\n})\n\ndescribe(\"Palette detection error handling\", () => {\n  test(\"handles timeout gracefully\", async () => {\n    const clock = new ManualClock()\n    const mockStdin = new EventEmitter() as any\n    mockStdin.isTTY = true\n    mockStdin.setRawMode = () => {}\n    mockStdin.resume = () => {}\n    mockStdin.pause = () => {}\n    mockStdin.setEncoding = () => {}\n\n    const mockStdout = {\n      isTTY: true,\n      columns: 80,\n      rows: 24,\n      write: () => true,\n    } as any\n\n    const { renderer } = await createTestRenderer({\n      stdin: mockStdin,\n      stdout: mockStdout,\n      clock,\n    })\n\n    const palette = await detectPaletteAndAdvanceClock(renderer, clock, { timeout: 100 })\n    expect(typeof palette === \"object\" && palette !== null && Array.isArray(palette.palette)).toBe(true)\n    expect(palette.palette.every((c) => c === null)).toBe(true)\n\n    renderer.destroy()\n  })\n\n  test(\"handles stdin listener restoration on error\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    try {\n      const palettePromise = renderer.getPalette({ timeout: 300 })\n      await advancePaletteClock(clock, 300)\n      await palettePromise\n    } catch (error) {}\n\n    const listenerCount = mockStdin.listenerCount(\"data\")\n    expect(listenerCount).toBeGreaterThan(0)\n\n    renderer.destroy()\n  })\n})\n\ndescribe(\"Palette cache with different sizes\", () => {\n  test(\"cache works correctly when requesting size=16 twice\", async () => {\n    const { renderer, clock, mockStdin, mockStdout, writes } = await createPaletteRenderer()\n\n    const palette1 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n    const writeCountAfterFirst = writes.length\n\n    expect(renderer.paletteDetectionStatus).toBe(\"cached\")\n    expect(palette1.palette.length).toBe(16)\n\n    const timeAfterFirstDetection = clock.now()\n    const palette2 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n\n    expect(clock.now()).toBe(timeAfterFirstDetection)\n    expect(writes.length).toBe(writeCountAfterFirst)\n    expect(palette1).toBe(palette2)\n    expect(renderer.paletteDetectionStatus).toBe(\"cached\")\n\n    renderer.destroy()\n  })\n\n  test(\"cache is invalidated when requesting different size\", async () => {\n    const { renderer, clock, mockStdin, mockStdout, writes } = await createPaletteRenderer({ useThread: false })\n\n    const palette1 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n    const writeCountAfter16 = writes.length\n\n    const palette2 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 256, timeout: 300 })\n    const writeCountAfter256 = writes.length\n\n    expect(writeCountAfter256).toBeGreaterThan(writeCountAfter16)\n    expect(palette1).not.toBe(palette2)\n\n    renderer.destroy()\n  })\n\n  test(\"cache persists across multiple identical size requests\", async () => {\n    const { renderer, clock, mockStdin, mockStdout, writes } = await createPaletteRenderer()\n\n    const palette1 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n    const writeCountAfterFirst = writes.length\n\n    const palette2 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n    const palette3 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n    const palette4 = await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n\n    expect(writes.length).toBe(writeCountAfterFirst)\n    expect(palette1).toBe(palette2)\n    expect(palette2).toBe(palette3)\n    expect(palette3).toBe(palette4)\n\n    renderer.destroy()\n  })\n\n  test(\"cached call is significantly faster than initial detection\", async () => {\n    const { renderer, clock, mockStdin, mockStdout } = await createPaletteRenderer()\n\n    await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n    const timeAfterFirstDetection = clock.now()\n\n    await detectPaletteAndAdvanceClock(renderer, clock, { size: 16, timeout: 300 })\n\n    expect(clock.now()).toBe(timeAfterFirstDetection)\n\n    renderer.destroy()\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.selection.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\n\nlet renderer: TestRenderer\nlet renderOnce: () => void\n\nbeforeEach(async () => {\n  ;({ renderer, renderOnce } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  renderer.destroy()\n})\n\ntest(\"selection on destroyed renderable should not throw\", () => {\n  const text = new TextRenderable(renderer, {\n    content: \"Hello World\",\n    width: 20,\n    height: 1,\n  })\n\n  renderer.root.add(text)\n  renderOnce()\n\n  // Start selection\n  renderer.startSelection(text, 0, 0)\n\n  // Update selection - this should not throw\n  renderer.updateSelection(text, 5, 1)\n\n  expect(renderer.getSelection()).not.toBeNull()\n\n  // Destroy the text renderable\n  text.destroy()\n\n  expect(text.isDestroyed).toBe(true)\n\n  // Get selection - this should not throw\n  expect(renderer.getSelection()!.getSelectedText()).toBe(\"\")\n\n  // Update selection - this should not throw\n  renderer.updateSelection(text, 8, 1)\n\n  // Clear selection - this should not throw\n  renderer.clearSelection()\n\n  expect(renderer.getSelection()).toBeNull()\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.slot-registry.test.ts",
    "content": "import { describe, expect, test } from \"bun:test\"\nimport { EventEmitter } from \"events\"\nimport { createSlotRegistry, SlotRegistry } from \"../plugins/registry\"\nimport type { Plugin } from \"../plugins/types\"\nimport type { CliRenderer } from \"../renderer\"\n\ninterface AppSlots {\n  statusbar: { user: string }\n  sidebar: { items: string[] }\n}\n\ntype TestNode = string\ntype AppContext = {\n  appName: string\n  version: string\n}\n\ntype TestPlugin = Plugin<TestNode, AppSlots, AppContext>\n\nconst hostContext: AppContext = {\n  appName: \"slot-test-app\",\n  version: \"1.0.0\",\n}\n\nfunction createMockRenderer(): CliRenderer {\n  return new EventEmitter() as CliRenderer\n}\n\ndescribe(\"SlotRegistry\", () => {\n  test(\"resolves no renderers for missing slot contributions\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n    expect(registry.resolve(\"statusbar\")).toEqual([])\n  })\n\n  test(\"supports plugin setup and dispose lifecycles\", () => {\n    const calls: string[] = []\n    const renderer = createMockRenderer()\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(renderer, hostContext)\n\n    registry.register({\n      id: \"lifecycle-plugin\",\n      setup(ctx, setupRenderer) {\n        calls.push(`setup:${ctx.appName}:${ctx.version}:${setupRenderer === renderer ? \"same\" : \"different\"}`)\n      },\n      dispose() {\n        calls.push(\"dispose\")\n      },\n      slots: {\n        statusbar(_ctx, props) {\n          return `status:${props.user}`\n        },\n      },\n    })\n\n    registry.unregister(\"lifecycle-plugin\")\n\n    expect(calls).toEqual([\"setup:slot-test-app:1.0.0:same\", \"dispose\"])\n  })\n\n  test(\"register accepts class-based plugin instances\", () => {\n    const renderer = createMockRenderer()\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(renderer, hostContext)\n\n    class ClassPlugin implements Plugin<TestNode, AppSlots, AppContext> {\n      id = \"class-plugin\"\n      order = 0\n      setupCalls = 0\n      disposeCalls = 0\n      rendererSeen: CliRenderer | null = null\n      prefix = \"class\"\n\n      setup(ctx: Readonly<AppContext>, setupRenderer: CliRenderer): void {\n        this.setupCalls++\n        this.rendererSeen = setupRenderer\n        this.prefix = ctx.appName\n      }\n\n      dispose(): void {\n        this.disposeCalls++\n      }\n\n      slots = {\n        statusbar: (_ctx: Readonly<AppContext>, props: AppSlots[\"statusbar\"]) => `${this.prefix}:${props.user}`,\n      }\n    }\n\n    const plugin = new ClassPlugin()\n    registry.register(plugin)\n\n    const output = registry.resolve(\"statusbar\")[0](hostContext, { user: \"sam\" })\n    registry.unregister(plugin.id)\n\n    expect(output).toBe(\"slot-test-app:sam\")\n    expect(plugin.setupCalls).toBe(1)\n    expect(plugin.disposeCalls).toBe(1)\n    expect(plugin.rendererSeen).toBe(renderer)\n  })\n\n  test(\"rejects duplicate plugin ids\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n\n    const plugin: TestPlugin = {\n      id: \"duplicate\",\n      slots: {\n        statusbar(_ctx, props) {\n          return props.user\n        },\n      },\n    }\n\n    registry.register(plugin)\n\n    expect(() => {\n      registry.register(plugin)\n    }).toThrow('Plugin with id \"duplicate\" is already registered')\n  })\n\n  test(\"sorts renderers deterministically by order then registration order\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n\n    registry.register({\n      id: \"z-registered-first\",\n      order: 0,\n      slots: {\n        statusbar() {\n          return \"z-first\"\n        },\n      },\n    })\n\n    registry.register({\n      id: \"a-registered-second\",\n      order: 0,\n      slots: {\n        statusbar() {\n          return \"a-second\"\n        },\n      },\n    })\n\n    registry.register({\n      id: \"high-order\",\n      order: 10,\n      slots: {\n        statusbar() {\n          return \"high\"\n        },\n      },\n    })\n\n    registry.register({\n      id: \"low-order\",\n      order: -10,\n      slots: {\n        statusbar() {\n          return \"low\"\n        },\n      },\n    })\n\n    const output = registry.resolve(\"statusbar\").map((renderer) => renderer(hostContext, { user: \"sam\" }))\n    expect(output).toEqual([\"low\", \"z-first\", \"a-second\", \"high\"])\n  })\n\n  test(\"supports order updates and emits subscription notifications\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n    let notifyCount = 0\n\n    const unsubscribe = registry.subscribe(() => {\n      notifyCount++\n    })\n\n    registry.register({\n      id: \"first\",\n      order: 5,\n      slots: {\n        statusbar() {\n          return \"first\"\n        },\n      },\n    })\n\n    registry.register({\n      id: \"second\",\n      order: 10,\n      slots: {\n        statusbar() {\n          return \"second\"\n        },\n      },\n    })\n\n    const changed = registry.updateOrder(\"second\", 0)\n    expect(changed).toBe(true)\n\n    const output = registry.resolve(\"statusbar\").map((renderer) => renderer(hostContext, { user: \"sam\" }))\n    expect(output).toEqual([\"second\", \"first\"])\n\n    unsubscribe()\n    registry.unregister(\"first\")\n\n    expect(notifyCount).toBe(3)\n  })\n\n  test(\"returns false and does not notify when updating order for unknown plugin\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n    let notifyCount = 0\n\n    registry.subscribe(() => {\n      notifyCount++\n    })\n\n    const changed = registry.updateOrder(\"missing-plugin\", -1)\n\n    expect(changed).toBe(false)\n    expect(notifyCount).toBe(0)\n  })\n\n  test(\"supports multiple slot contributions per plugin\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n\n    registry.register({\n      id: \"multi-slot\",\n      slots: {\n        statusbar(_ctx, props) {\n          return `status:${props.user}`\n        },\n        sidebar(_ctx, props) {\n          return `sidebar:${props.items.join(\",\")}`\n        },\n      },\n    })\n\n    const statusbarRenderer = registry.resolve(\"statusbar\")[0]\n    const sidebarRenderer = registry.resolve(\"sidebar\")[0]\n\n    expect(statusbarRenderer(hostContext, { user: \"ava\" })).toBe(\"status:ava\")\n    expect(sidebarRenderer(hostContext, { items: [\"a\", \"b\"] })).toBe(\"sidebar:a,b\")\n  })\n\n  test(\"resolveEntries returns sorted plugin ids with renderers\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n\n    registry.register({\n      id: \"plugin-b\",\n      order: 2,\n      slots: {\n        statusbar() {\n          return \"b\"\n        },\n      },\n    })\n\n    registry.register({\n      id: \"plugin-a\",\n      order: 1,\n      slots: {\n        statusbar() {\n          return \"a\"\n        },\n      },\n    })\n\n    const entries = registry.resolveEntries(\"statusbar\")\n    expect(entries.map((entry) => entry.id)).toEqual([\"plugin-a\", \"plugin-b\"])\n    expect(entries.map((entry) => entry.renderer(hostContext, { user: \"sam\" }))).toEqual([\"a\", \"b\"])\n  })\n\n  test(\"invalidates cached sorting when registering after a resolve\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n\n    registry.register({\n      id: \"plugin-b\",\n      order: 2,\n      slots: {\n        statusbar() {\n          return \"b\"\n        },\n      },\n    })\n\n    expect(registry.resolveEntries(\"statusbar\").map((entry) => entry.id)).toEqual([\"plugin-b\"])\n\n    registry.register({\n      id: \"plugin-a\",\n      order: 1,\n      slots: {\n        statusbar() {\n          return \"a\"\n        },\n      },\n    })\n\n    expect(registry.resolveEntries(\"statusbar\").map((entry) => entry.id)).toEqual([\"plugin-a\", \"plugin-b\"])\n  })\n\n  test(\"invalidates cached sorting on unregister and clear\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n\n    registry.register({\n      id: \"plugin-a\",\n      order: 1,\n      slots: {\n        statusbar() {\n          return \"a\"\n        },\n      },\n    })\n\n    registry.register({\n      id: \"plugin-b\",\n      order: 2,\n      slots: {\n        statusbar() {\n          return \"b\"\n        },\n      },\n    })\n\n    expect(registry.resolveEntries(\"statusbar\").map((entry) => entry.id)).toEqual([\"plugin-a\", \"plugin-b\"])\n\n    registry.unregister(\"plugin-a\")\n    expect(registry.resolveEntries(\"statusbar\").map((entry) => entry.id)).toEqual([\"plugin-b\"])\n\n    registry.clear()\n    expect(registry.resolveEntries(\"statusbar\")).toEqual([])\n  })\n\n  test(\"re-sorts when plugin order changes outside updateOrder\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n\n    const latePlugin: TestPlugin = {\n      id: \"late-plugin\",\n      order: 10,\n      slots: {\n        statusbar() {\n          return \"late\"\n        },\n      },\n    }\n\n    const earlyPlugin: TestPlugin = {\n      id: \"early-plugin\",\n      order: 0,\n      slots: {\n        statusbar() {\n          return \"early\"\n        },\n      },\n    }\n\n    registry.register(latePlugin)\n    registry.register(earlyPlugin)\n\n    expect(registry.resolveEntries(\"statusbar\").map((entry) => entry.id)).toEqual([\"early-plugin\", \"late-plugin\"])\n\n    latePlugin.order = -5\n\n    expect(registry.resolveEntries(\"statusbar\").map((entry) => entry.id)).toEqual([\"late-plugin\", \"early-plugin\"])\n  })\n\n  test(\"slot registries are isolated per renderer and key\", () => {\n    const rendererA = createMockRenderer()\n    const rendererB = createMockRenderer()\n\n    const aFirst = createSlotRegistry<string, AppSlots, AppContext>(rendererA, \"demo-key\", hostContext)\n    const aSecond = createSlotRegistry<string, AppSlots, AppContext>(rendererA, \"demo-key\", hostContext)\n    const aOtherKey = createSlotRegistry<string, AppSlots, AppContext>(rendererA, \"other-key\", hostContext)\n    const bFirst = createSlotRegistry<string, AppSlots, AppContext>(rendererB, \"demo-key\", hostContext)\n\n    expect(aFirst).toBe(aSecond)\n    expect(aFirst).not.toBe(aOtherKey)\n    expect(aFirst).not.toBe(bFirst)\n\n    expect(() => {\n      createSlotRegistry<string, AppSlots, AppContext>(rendererA, \"demo-key\", {\n        appName: \"other-app\",\n        version: \"2.0.0\",\n      })\n    }).toThrow(\"different context\")\n  })\n\n  test(\"slot registry clears plugins on renderer destroy\", () => {\n    const renderer = createMockRenderer()\n    const disposeCalls: string[] = []\n\n    const registry = createSlotRegistry<string, AppSlots, AppContext>(renderer, \"cleanup-key\", hostContext)\n    registry.register({\n      id: \"cleanup-plugin\",\n      dispose() {\n        disposeCalls.push(\"disposed\")\n      },\n      slots: {\n        statusbar() {\n          return \"cleanup\"\n        },\n      },\n    })\n\n    renderer.emit(\"destroy\")\n\n    expect(disposeCalls).toEqual([\"disposed\"])\n\n    const recreated = createSlotRegistry<string, AppSlots, AppContext>(renderer, \"cleanup-key\", hostContext)\n    expect(recreated).not.toBe(registry)\n  })\n\n  test(\"does not register plugin when setup throws\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n    let notifyCount = 0\n    const errors: string[] = []\n    registry.subscribe(() => {\n      notifyCount++\n    })\n    registry.onPluginError((event) => {\n      errors.push(`${event.pluginId}:${event.phase}:${event.error.message}`)\n    })\n\n    registry.register({\n      id: \"setup-failure\",\n      setup() {\n        throw new Error(\"setup failed\")\n      },\n      slots: {\n        statusbar() {\n          return \"should-not-register\"\n        },\n      },\n    })\n\n    expect(registry.resolve(\"statusbar\")).toEqual([])\n    expect(notifyCount).toBe(0)\n    expect(errors).toEqual([\"setup-failure:setup:setup failed\"])\n\n    registry.register({\n      id: \"setup-failure\",\n      slots: {\n        statusbar() {\n          return \"registered-after-failure\"\n        },\n      },\n    })\n\n    expect(registry.resolve(\"statusbar\").map((renderer) => renderer(hostContext, { user: \"sam\" }))).toEqual([\n      \"registered-after-failure\",\n    ])\n  })\n\n  test(\"unregister removes plugin and notifies even if dispose throws\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n    let notifyCount = 0\n    const errors: string[] = []\n    registry.subscribe(() => {\n      notifyCount++\n    })\n    registry.onPluginError((event) => {\n      errors.push(`${event.pluginId}:${event.phase}:${event.error.message}`)\n    })\n\n    registry.register({\n      id: \"dispose-failure\",\n      dispose() {\n        throw new Error(\"dispose failed\")\n      },\n      slots: {\n        statusbar() {\n          return \"present-before-unregister\"\n        },\n      },\n    })\n\n    registry.unregister(\"dispose-failure\")\n\n    expect(registry.resolve(\"statusbar\")).toEqual([])\n    expect(notifyCount).toBe(2)\n    expect(errors).toEqual([\"dispose-failure:dispose:dispose failed\"])\n  })\n\n  test(\"supports unregister then re-register for the same plugin id\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n    const lifecycle: string[] = []\n\n    const plugin: TestPlugin = {\n      id: \"re-register-plugin\",\n      setup() {\n        lifecycle.push(\"setup\")\n      },\n      dispose() {\n        lifecycle.push(\"dispose\")\n      },\n      slots: {\n        statusbar() {\n          return \"first-registration\"\n        },\n      },\n    }\n\n    const unregisterFirst = registry.register(plugin)\n    expect(registry.resolve(\"statusbar\").map((renderer) => renderer(hostContext, { user: \"sam\" }))).toEqual([\n      \"first-registration\",\n    ])\n\n    unregisterFirst()\n    expect(registry.resolve(\"statusbar\")).toEqual([])\n\n    registry.register({\n      ...plugin,\n      slots: {\n        statusbar() {\n          return \"second-registration\"\n        },\n      },\n    })\n\n    expect(registry.resolve(\"statusbar\").map((renderer) => renderer(hostContext, { user: \"sam\" }))).toEqual([\n      \"second-registration\",\n    ])\n    expect(lifecycle).toEqual([\"setup\", \"dispose\", \"setup\"])\n  })\n\n  test(\"clear disposes every plugin and reports dispose errors\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext)\n    const disposeCalls: string[] = []\n    const errors: string[] = []\n    registry.onPluginError((event) => {\n      errors.push(`${event.pluginId}:${event.phase}:${event.error.message}`)\n    })\n\n    registry.register({\n      id: \"first-error\",\n      dispose() {\n        disposeCalls.push(\"first-error\")\n        throw new Error(\"first dispose error\")\n      },\n      slots: {\n        statusbar() {\n          return \"first\"\n        },\n      },\n    })\n\n    registry.register({\n      id: \"second-error\",\n      dispose() {\n        disposeCalls.push(\"second-error\")\n        throw new Error(\"second dispose error\")\n      },\n      slots: {\n        statusbar() {\n          return \"second\"\n        },\n      },\n    })\n\n    registry.register({\n      id: \"clean-dispose\",\n      dispose() {\n        disposeCalls.push(\"clean-dispose\")\n      },\n      slots: {\n        statusbar() {\n          return \"third\"\n        },\n      },\n    })\n\n    registry.clear()\n\n    expect(disposeCalls).toEqual([\"first-error\", \"second-error\", \"clean-dispose\"])\n    expect(registry.resolve(\"statusbar\")).toEqual([])\n    expect(errors).toEqual([\"first-error:dispose:first dispose error\", \"second-error:dispose:second dispose error\"])\n  })\n\n  test(\"stores plugin error history with source and slot metadata\", () => {\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext, {\n      maxPluginErrors: 2,\n    })\n\n    registry.reportPluginError({\n      pluginId: \"plugin-a\",\n      slot: \"statusbar\",\n      phase: \"render\",\n      source: \"core\",\n      error: new Error(\"first\"),\n    })\n\n    registry.reportPluginError({\n      pluginId: \"plugin-b\",\n      phase: \"setup\",\n      source: \"registry\",\n      error: \"second\",\n    })\n\n    registry.reportPluginError({\n      pluginId: \"plugin-c\",\n      phase: \"dispose\",\n      source: \"registry\",\n      error: new Error(\"third\"),\n    })\n\n    expect(\n      registry\n        .getPluginErrors()\n        .map((event) => `${event.pluginId}:${event.phase}:${event.source}:${event.error.message}`),\n    ).toEqual([\"plugin-b:setup:registry:second\", \"plugin-c:dispose:registry:third\"])\n\n    registry.clearPluginErrors()\n    expect(registry.getPluginErrors()).toEqual([])\n  })\n\n  test(\"configure can clear onPluginError and reset maxPluginErrors\", () => {\n    const callbackEvents: string[] = []\n    const registry = new SlotRegistry<TestNode, AppSlots, AppContext>(createMockRenderer(), hostContext, {\n      onPluginError(event) {\n        callbackEvents.push(event.pluginId)\n      },\n      maxPluginErrors: 1,\n    })\n\n    registry.reportPluginError({\n      pluginId: \"plugin-a\",\n      phase: \"render\",\n      source: \"core\",\n      error: new Error(\"first\"),\n    })\n\n    registry.configure({\n      onPluginError: undefined,\n      maxPluginErrors: undefined,\n    })\n\n    registry.reportPluginError({\n      pluginId: \"plugin-b\",\n      phase: \"render\",\n      source: \"core\",\n      error: new Error(\"second\"),\n    })\n\n    registry.reportPluginError({\n      pluginId: \"plugin-c\",\n      phase: \"render\",\n      source: \"core\",\n      error: new Error(\"third\"),\n    })\n\n    expect(callbackEvents).toEqual([\"plugin-a\"])\n    expect(registry.getPluginErrors().map((event) => event.pluginId)).toEqual([\"plugin-a\", \"plugin-b\", \"plugin-c\"])\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/renderer.useMouse.test.ts",
    "content": "import { test, expect, describe } from \"bun:test\"\nimport { createTestRenderer } from \"../testing/test-renderer.js\"\n\n// NOTE: These tests are not running the mouse activation sequences,\n// only verifying that the configuration is applied correctly.\n// Tests avoid actually outputting to the terminal during test runs,\n// to not mess up the terminal state.\n// What actually gets written can be tested properly when\n// https://github.com/anomalyco/opentui/pull/238 is merged.\ndescribe(\"useMouse configuration\", () => {\n  test(\"useMouse: true sets renderer.useMouse to true\", async () => {\n    const { renderer } = await createTestRenderer({\n      useMouse: true,\n      exitOnCtrlC: false,\n      useAlternateScreen: false,\n    })\n\n    expect(renderer.useMouse).toBe(true)\n    renderer.destroy()\n  })\n\n  test(\"useMouse: false disables mouse tracking\", async () => {\n    const { renderer } = await createTestRenderer({\n      useMouse: false,\n      exitOnCtrlC: false,\n      useAlternateScreen: false,\n    })\n\n    expect(renderer.useMouse).toBe(false)\n    renderer.destroy()\n  })\n\n  test(\"toggling useMouse property updates renderer state\", async () => {\n    const { renderer } = await createTestRenderer({\n      useMouse: false,\n      exitOnCtrlC: false,\n      useAlternateScreen: false,\n    })\n\n    expect(renderer.useMouse).toBe(false)\n\n    renderer.useMouse = true\n    expect(renderer.useMouse).toBe(true)\n\n    renderer.useMouse = false\n    expect(renderer.useMouse).toBe(false)\n\n    renderer.destroy()\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/runtime-plugin-resolve-roots.fixture.ts",
    "content": "import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { plugin as registerPlugin } from \"bun\"\nimport { createRuntimePlugin } from \"../runtime-plugin\"\n\nconst tempRoot = mkdtempSync(join(tmpdir(), \"core-runtime-plugin-resolve-roots-fixture-\"))\nconst hostModuleDir = join(tempRoot, \"host\")\nconst externalPluginDir = join(tempRoot, \"external-plugin\")\nconst externalNodeModules = join(externalPluginDir, \"node_modules\")\nconst externalDependencyDir = join(externalNodeModules, \"runtime-root-dependency\")\nconst hostModulePath = join(hostModuleDir, \"host-runtime.ts\")\nconst externalPluginEntryPath = join(externalPluginDir, \"index.ts\")\n\nmkdirSync(hostModuleDir, { recursive: true })\nmkdirSync(externalDependencyDir, { recursive: true })\n\nwriteFileSync(\n  join(externalPluginDir, \"package.json\"),\n  JSON.stringify({\n    name: \"runtime-plugin-external-fixture\",\n    private: true,\n    type: \"module\",\n  }),\n)\n\nwriteFileSync(\n  join(externalDependencyDir, \"package.json\"),\n  JSON.stringify({\n    name: \"runtime-root-dependency\",\n    version: \"1.0.0\",\n    type: \"module\",\n    exports: \"./index.js\",\n  }),\n)\n\nwriteFileSync(join(externalDependencyDir, \"index.js\"), 'export const marker = \"resolved-from-external-root\"\\n')\n\nwriteFileSync(\n  hostModulePath,\n  ['import { marker } from \"runtime-root-dependency\"', \"export const hostRuntimeMarker = marker\"].join(\"\\n\"),\n)\n\nwriteFileSync(\n  externalPluginEntryPath,\n  ['import { hostRuntimeMarker } from \"fixture-host-runtime\"', \"export const marker = hostRuntimeMarker\"].join(\"\\n\"),\n)\n\nregisterPlugin.clearAll()\n\nregisterPlugin(\n  createRuntimePlugin({\n    additional: {\n      \"fixture-host-runtime\": async () => (await import(hostModulePath)) as Record<string, unknown>,\n    },\n  }),\n)\n\ntry {\n  const externalPlugin = (await import(externalPluginEntryPath)) as { marker: string }\n  console.log(`marker=${externalPlugin.marker}`)\n} finally {\n  registerPlugin.clearAll()\n  rmSync(tempRoot, { recursive: true, force: true })\n}\n"
  },
  {
    "path": "packages/core/src/tests/runtime-plugin-support.fixture.ts",
    "content": "import { plugin as registerPlugin } from \"bun\"\n\nregisterPlugin.clearAll()\n\ntry {\n  const runtimePluginSupport = await import(\"../runtime-plugin-support\")\n  const alreadyInstalled = runtimePluginSupport.ensureRuntimePluginSupport() === false\n  console.log(`idempotent=${alreadyInstalled}`)\n} finally {\n  registerPlugin.clearAll()\n}\n"
  },
  {
    "path": "packages/core/src/tests/runtime-plugin-support.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { join } from \"node:path\"\n\ndescribe(\"runtime plugin support\", () => {\n  it(\"installs exactly once via drop-in module\", () => {\n    const fixturePath = join(import.meta.dir, \"runtime-plugin-support.fixture.ts\")\n    const result = Bun.spawnSync([process.execPath, fixturePath], {\n      cwd: join(import.meta.dir, \"..\", \"..\"),\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: process.env,\n    })\n\n    const stdout = result.stdout.toString().trim()\n    const stderr = result.stderr.toString().trim()\n\n    if (stdout) {\n      console.debug(`[runtime-plugin-support.fixture] stdout:\\n${stdout}`)\n    }\n\n    if (stderr) {\n      console.debug(`[runtime-plugin-support.fixture] stderr:\\n${stderr}`)\n    }\n\n    expect(result.exitCode).toBe(0)\n    expect(stdout).toContain(\"idempotent=true\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/runtime-plugin.fixture.ts",
    "content": "import { mkdtempSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { plugin as registerPlugin } from \"bun\"\nimport { createRuntimePlugin } from \"../runtime-plugin\"\n\nconst tempRoot = mkdtempSync(join(tmpdir(), \"core-runtime-plugin-fixture-\"))\nconst entryPath = join(tempRoot, \"entry.ts\")\n\nconst source = [\n  'import { marker as coreMarker } from \"@opentui/core\"',\n  'import { createTestRenderer } from \"@opentui/core/testing\"',\n  'import { value as syncValue } from \"fixture-sync\"',\n  'import { value as asyncValue } from \"@fixture/async-module\"',\n  \"console.log(`core=${coreMarker};coreTesting=${typeof createTestRenderer === 'function'};sync=${syncValue};async=${asyncValue}`)\",\n  \"export const noop = 1\",\n].join(\"\\n\")\n\nwriteFileSync(entryPath, source)\n\nregisterPlugin.clearAll()\n\nregisterPlugin(\n  createRuntimePlugin({\n    core: {\n      marker: \"core-value\",\n    },\n    additional: {\n      \"fixture-sync\": { value: \"sync-value\" },\n      \"@fixture/async-module\": async () => ({ value: \"async-value\" }),\n    },\n  }),\n)\n\ntry {\n  await import(`${entryPath}?reload=1`)\n} finally {\n  registerPlugin.clearAll()\n  rmSync(tempRoot, { recursive: true, force: true })\n}\n"
  },
  {
    "path": "packages/core/src/tests/runtime-plugin.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { join } from \"node:path\"\nimport * as coreRuntime from \"../index\"\nimport { createRuntimePlugin, runtimeModuleIdForSpecifier } from \"../runtime-plugin\"\n\ntype ResolveResult = { path: string; namespace?: string } | void\ntype ResolveCallback = (args: { path: string; importer: string }) => ResolveResult | Promise<ResolveResult>\ntype LoadCallback = (args: { path: string }) => unknown | Promise<unknown>\ntype ModuleCallback = () => unknown | Promise<unknown>\n\ntype ResolveHandler = {\n  filter: RegExp\n  callback: ResolveCallback\n}\n\ntype MockBuild = {\n  onResolve: (args: { filter: RegExp }, callback: ResolveCallback) => void\n  onLoad: (args: { filter: RegExp }, callback: LoadCallback) => void\n  module: (path: string, callback: ModuleCallback) => void\n}\n\nconst createMockBuild = (): {\n  build: MockBuild\n  resolveHandlers: ResolveHandler[]\n  modules: Map<string, ModuleCallback>\n} => {\n  const resolveHandlers: ResolveHandler[] = []\n  const modules = new Map<string, ModuleCallback>()\n\n  const build: MockBuild = {\n    onResolve(args, callback) {\n      resolveHandlers.push({ filter: args.filter, callback })\n    },\n    onLoad() {\n      return\n    },\n    module(path, callback) {\n      modules.set(path, callback)\n    },\n  }\n\n  return { build, resolveHandlers, modules }\n}\n\nconst resolveSpecifier = async (handlers: ResolveHandler[], specifier: string): Promise<ResolveResult> => {\n  for (const handler of handlers) {\n    if (!handler.filter.test(specifier)) continue\n\n    const result = await handler.callback({\n      path: specifier,\n      importer: import.meta.path,\n    })\n\n    if (result) {\n      return result\n    }\n  }\n\n  return undefined\n}\n\ndescribe(\"runtime plugin\", () => {\n  it(\"registers core runtime modules by default\", async () => {\n    const { build, resolveHandlers, modules } = createMockBuild()\n    createRuntimePlugin().setup(build as any)\n\n    const coreResolution = await resolveSpecifier(resolveHandlers, \"@opentui/core\")\n    const core3dResolution = await resolveSpecifier(resolveHandlers, \"@opentui/core/3d\")\n    const coreTestingResolution = await resolveSpecifier(resolveHandlers, \"@opentui/core/testing\")\n\n    expect(coreResolution).toEqual({ path: runtimeModuleIdForSpecifier(\"@opentui/core\") })\n    expect(core3dResolution).toBeUndefined()\n    expect(coreTestingResolution).toEqual({ path: runtimeModuleIdForSpecifier(\"@opentui/core/testing\") })\n\n    if (!coreResolution || !coreTestingResolution) {\n      throw new Error(\"Expected core runtime module resolutions\")\n    }\n\n    const coreModuleFactory = modules.get(coreResolution.path)\n    const coreTestingModuleFactory = modules.get(coreTestingResolution.path)\n\n    expect(coreModuleFactory).toBeDefined()\n    expect(coreTestingModuleFactory).toBeDefined()\n\n    if (!coreModuleFactory || !coreTestingModuleFactory) {\n      throw new Error(\"Expected core runtime module factories\")\n    }\n\n    expect(await coreModuleFactory()).toEqual({\n      exports: coreRuntime as Record<string, unknown>,\n      loader: \"object\",\n    })\n\n    const coreTestingModule = (await coreTestingModuleFactory()) as {\n      exports: Record<string, unknown>\n      loader: string\n    }\n\n    expect(coreTestingModule.loader).toBe(\"object\")\n    expect(typeof coreTestingModule.exports.createTestRenderer).toBe(\"function\")\n  })\n\n  it(\"registers @opentui/core/3d only when added explicitly\", async () => {\n    const { build, resolveHandlers, modules } = createMockBuild()\n\n    createRuntimePlugin({\n      additional: {\n        \"@opentui/core/3d\": { ThreeRenderable: \"three-value\" },\n      },\n    }).setup(build as any)\n\n    const core3dResolution = await resolveSpecifier(resolveHandlers, \"@opentui/core/3d\")\n\n    expect(core3dResolution).toEqual({ path: runtimeModuleIdForSpecifier(\"@opentui/core/3d\") })\n\n    if (!core3dResolution) {\n      throw new Error(\"Expected @opentui/core/3d runtime module resolution\")\n    }\n\n    const core3dModuleFactory = modules.get(core3dResolution.path)\n\n    expect(core3dModuleFactory).toBeDefined()\n\n    if (!core3dModuleFactory) {\n      throw new Error(\"Expected @opentui/core/3d runtime module factory\")\n    }\n\n    expect(await core3dModuleFactory()).toEqual({\n      exports: { ThreeRenderable: \"three-value\" },\n      loader: \"object\",\n    })\n  })\n\n  it(\"registers additional runtime modules with sync and async loaders\", async () => {\n    const { build, resolveHandlers, modules } = createMockBuild()\n\n    createRuntimePlugin({\n      core: { marker: \"core\" },\n      additional: {\n        \"fixture-sync\": { value: \"sync-value\" },\n        \"@fixture/async-module\": async () => ({ value: \"async-value\" }),\n      },\n    }).setup(build as any)\n\n    const coreResolution = await resolveSpecifier(resolveHandlers, \"@opentui/core\")\n    const syncResolution = await resolveSpecifier(resolveHandlers, \"fixture-sync\")\n    const asyncResolution = await resolveSpecifier(resolveHandlers, \"@fixture/async-module\")\n\n    expect(coreResolution).toEqual({ path: runtimeModuleIdForSpecifier(\"@opentui/core\") })\n    expect(syncResolution).toEqual({ path: runtimeModuleIdForSpecifier(\"fixture-sync\") })\n    expect(asyncResolution).toEqual({ path: runtimeModuleIdForSpecifier(\"@fixture/async-module\") })\n\n    if (!coreResolution || !syncResolution || !asyncResolution) {\n      throw new Error(\"Expected runtime module resolutions\")\n    }\n\n    const coreModuleFactory = modules.get(coreResolution.path)\n    const syncModuleFactory = modules.get(syncResolution.path)\n    const asyncModuleFactory = modules.get(asyncResolution.path)\n\n    expect(coreModuleFactory).toBeDefined()\n    expect(syncModuleFactory).toBeDefined()\n    expect(asyncModuleFactory).toBeDefined()\n\n    if (!coreModuleFactory || !syncModuleFactory || !asyncModuleFactory) {\n      throw new Error(\"Expected runtime module factories\")\n    }\n\n    expect(await coreModuleFactory()).toEqual({ exports: { marker: \"core\" }, loader: \"object\" })\n    expect(await syncModuleFactory()).toEqual({ exports: { value: \"sync-value\" }, loader: \"object\" })\n    expect(await asyncModuleFactory()).toEqual({ exports: { value: \"async-value\" }, loader: \"object\" })\n  })\n\n  it(\"escapes exact-match resolver filters for special characters\", async () => {\n    const { build, resolveHandlers } = createMockBuild()\n\n    createRuntimePlugin({\n      additional: {\n        \"fixture.with.dot\": { value: \"dot-value\" },\n      },\n    }).setup(build as any)\n\n    const exactMatch = await resolveSpecifier(resolveHandlers, \"fixture.with.dot\")\n    const nonMatch = await resolveSpecifier(resolveHandlers, \"fixtureXwithXdot\")\n\n    expect(exactMatch).toEqual({ path: runtimeModuleIdForSpecifier(\"fixture.with.dot\") })\n    expect(nonMatch).toBeUndefined()\n  })\n\n  it(\"encodes runtime module IDs deterministically\", () => {\n    expect(runtimeModuleIdForSpecifier(\"@opentui/core/testing\")).toBe(\n      \"opentui:runtime-module:%40opentui%2Fcore%2Ftesting\",\n    )\n  })\n\n  it(\"resolves runtime modules end-to-end in a subprocess\", () => {\n    const fixturePath = join(import.meta.dir, \"runtime-plugin.fixture.ts\")\n    const result = Bun.spawnSync([process.execPath, fixturePath], {\n      cwd: join(import.meta.dir, \"..\", \"..\"),\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: process.env,\n    })\n\n    const stdout = result.stdout.toString().trim()\n    const stderr = result.stderr.toString().trim()\n\n    if (stdout) {\n      console.debug(`[runtime-plugin.fixture] stdout:\\n${stdout}`)\n    }\n\n    if (stderr) {\n      console.debug(`[runtime-plugin.fixture] stderr:\\n${stderr}`)\n    }\n\n    expect(result.exitCode).toBe(0)\n    expect(stdout).toContain(\"core=core-value;coreTesting=true;sync=sync-value;async=async-value\")\n  })\n\n  it(\"resolves bare imports from external runtime roots\", () => {\n    const fixturePath = join(import.meta.dir, \"runtime-plugin-resolve-roots.fixture.ts\")\n    const result = Bun.spawnSync([process.execPath, fixturePath], {\n      cwd: join(import.meta.dir, \"..\", \"..\"),\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: process.env,\n    })\n\n    const stdout = result.stdout.toString().trim()\n    const stderr = result.stderr.toString().trim()\n\n    if (stdout) {\n      console.debug(`[runtime-plugin-resolve-roots.fixture] stdout:\\n${stdout}`)\n    }\n\n    if (stderr) {\n      console.debug(`[runtime-plugin-resolve-roots.fixture] stderr:\\n${stderr}`)\n    }\n\n    expect(result.exitCode).toBe(0)\n    expect(stdout).toContain(\"marker=resolved-from-external-root\")\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/scrollbox-culling-bug.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer } from \"../testing.js\"\nimport { ManualClock } from \"../testing/manual-clock.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { BoxRenderable } from \"../renderables/Box.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\nimport { TestRecorder } from \"../testing/test-recorder.js\"\n\nlet testRenderer: TestRenderer\nlet renderOnce: () => Promise<void>\nlet clock: ManualClock\n\nbeforeEach(async () => {\n  clock = new ManualClock()\n  ;({ renderer: testRenderer, renderOnce } = await createTestRenderer({ width: 50, height: 12, clock }))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ntest(\"scrollbox culling issue: last item not visible in frame after content grows with stickyScroll\", async () => {\n  // ISSUE: During updateLayout, when content.onSizeChange triggers recalculateBarProps,\n  // it changes translateY via the scrollbar onChange callback. Then _getVisibleChildren()\n  // is called for culling, but it uses the NEW translateY value with OLD child layout\n  // positions (since children haven't had updateFromLayout called yet). This causes\n  // incorrect culling where the last item is not rendered even though it should be visible.\n\n  // Container box with border to see constraints clearly\n  const container = new BoxRenderable(testRenderer, {\n    width: 48,\n    height: 10,\n    border: true,\n  })\n  testRenderer.root.add(container)\n\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: \"100%\",\n    height: \"100%\",\n    stickyScroll: true,\n    stickyStart: \"bottom\",\n  })\n  container.add(scrollBox)\n\n  const recorder = new TestRecorder(testRenderer)\n  recorder.rec()\n\n  for (let i = 0; i < 50; i++) {\n    const item = new BoxRenderable(testRenderer, {\n      id: `item-${i}`,\n      height: 3,\n      border: true,\n    })\n\n    const text = new TextRenderable(testRenderer, {\n      content: `Item ${i}`,\n    })\n    item.add(text)\n\n    scrollBox.add(item)\n    await renderOnce()\n  }\n\n  // Advance clock to trigger any pending re-render scheduled by stickyScroll's requestRender()\n  clock.advance(100)\n  await renderOnce()\n\n  recorder.stop()\n\n  const frames = recorder.recordedFrames\n\n  // With stickyScroll to bottom, there should NEVER be empty space at the bottom\n  // when there are items available to render\n\n  for (let frameIdx = 0; frameIdx < frames.length; frameIdx++) {\n    const frame = frames[frameIdx].frame\n    const lines = frame.split(\"\\n\")\n\n    const containerStart = lines.findIndex((line) => line.startsWith(\"┌\"))\n    const containerEnd = containerStart + 10 - 1\n\n    if (containerStart >= 0 && containerEnd > containerStart && containerEnd < lines.length) {\n      const contentLines = lines.slice(containerStart + 1, containerEnd)\n\n      let emptyLinesAtBottom = 0\n\n      for (let i = contentLines.length - 1; i >= 0; i--) {\n        const line = contentLines[i]\n        const content = line.replace(/^[│\\s]*/, \"\").replace(/[│█▄\\s]*$/, \"\")\n\n        if (content.length === 0) {\n          emptyLinesAtBottom++\n        } else {\n          break\n        }\n      }\n\n      const expectedItems = frameIdx + 1\n\n      // With stickyScroll to bottom, once we have enough items to fill the viewport,\n      // there should be NO empty space at the bottom\n      // Viewport is 8 lines (10 - 2 for borders), items are 3 lines each\n      // So with 3+ items (9 lines of content), we should always fill the viewport\n      if (expectedItems >= 3) {\n        expect(emptyLinesAtBottom).toBe(0)\n      }\n    }\n  }\n\n  // With stickyScroll to bottom, the last item should be visible after all items are added\n  const finalFrame = frames[frames.length - 1].frame\n  const hasItem49 = finalFrame.includes(\"Item 49\")\n  expect(hasItem49).toBe(true)\n})\n"
  },
  {
    "path": "packages/core/src/tests/scrollbox-hitgrid-resize.test.ts",
    "content": "import { test, expect } from \"bun:test\"\nimport { createTestRenderer, type MockMouse, type TestRenderer } from \"../testing.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { BoxRenderable } from \"../renderables/Box.js\"\nimport { Renderable } from \"../Renderable.js\"\n\ntest(\"hit grid works at all Y coordinates after terminal shrink\", async () => {\n  // Start wide: 160x50 = 8000 cells\n  const { renderer, mockMouse, resize } = await createTestRenderer({\n    width: 160,\n    height: 50,\n  })\n\n  const scrollBox = new ScrollBoxRenderable(renderer, {\n    width: \"100%\",\n    height: \"100%\",\n    scrollY: true,\n  })\n  renderer.root.add(scrollBox)\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 200; i++) {\n    const item = new BoxRenderable(renderer, {\n      id: `item-${i}`,\n      height: 2,\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await renderer.idle()\n\n  // Verify hit grid works at the original size\n  const hitBefore = renderer.hitTest(5, 10)\n  expect(hitBefore).not.toBe(0)\n\n  // Shrink to narrow+tall: 60x100 = 6000 cells (smaller total area)\n  resize(60, 100)\n  renderer.root.resize(60, 100)\n  await renderer.idle()\n\n  // Row 70 is beyond the old height (50). Before the fix, checkHit\n  // returned 0 here because hitGridHeight was still 50.\n  const hitAtRow70 = renderer.hitTest(5, 70)\n  expect(hitAtRow70).not.toBe(0)\n\n  // Row 95 -- near the bottom of the new terminal\n  const hitAtRow95 = renderer.hitTest(5, 95)\n  expect(hitAtRow95).not.toBe(0)\n\n  renderer.destroy()\n})\n\ntest(\"mouse scroll reaches scrollbox after terminal shrink\", async () => {\n  const { renderer, mockMouse, resize } = await createTestRenderer({\n    width: 160,\n    height: 50,\n  })\n\n  const scrollBox = new ScrollBoxRenderable(renderer, {\n    width: \"100%\",\n    height: \"100%\",\n    scrollY: true,\n  })\n  renderer.root.add(scrollBox)\n\n  for (let i = 0; i < 200; i++) {\n    scrollBox.add(\n      new BoxRenderable(renderer, {\n        id: `item-${i}`,\n        height: 2,\n      }),\n    )\n  }\n\n  await renderer.idle()\n\n  // Shrink terminal\n  resize(60, 100)\n  renderer.root.resize(60, 100)\n  await renderer.idle()\n\n  // Scroll to bottom first so we have room to scroll up\n  scrollBox.scrollTop = scrollBox.scrollHeight - 100\n  await renderer.idle()\n  const positionBefore = scrollBox.scrollTop\n\n  // Mouse wheel at row 70 (beyond the old height of 50)\n  await mockMouse.scroll(30, 70, \"up\")\n  await renderer.idle()\n\n  // scrollTop should have decreased -- the scroll event reached the scrollbox\n  expect(scrollBox.scrollTop).toBeLessThan(positionBefore)\n\n  renderer.destroy()\n})\n\ntest(\"hit grid works after multiple resize cycles\", async () => {\n  const { renderer, resize } = await createTestRenderer({\n    width: 80,\n    height: 40,\n  })\n\n  const box = new BoxRenderable(renderer, {\n    id: \"target\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n  renderer.root.add(box)\n  await renderer.idle()\n\n  // Grow: 80x40=3200 -> 120x60=7200\n  resize(120, 60)\n  renderer.root.resize(120, 60)\n  await renderer.idle()\n  expect(renderer.hitTest(5, 55)).not.toBe(0)\n\n  // Shrink back: 120x60=7200 -> 80x40=3200\n  resize(80, 40)\n  renderer.root.resize(80, 40)\n  await renderer.idle()\n  expect(renderer.hitTest(5, 35)).not.toBe(0)\n  // x=100 is outside the new width, should return 0\n  expect(renderer.hitTest(100, 10)).toBe(0)\n\n  // Shrink further: 80x40=3200 -> 40x30=1200\n  resize(40, 30)\n  renderer.root.resize(40, 30)\n  await renderer.idle()\n  expect(renderer.hitTest(5, 25)).not.toBe(0)\n  // Old coordinates should be out of bounds\n  expect(renderer.hitTest(5, 35)).toBe(0)\n  expect(renderer.hitTest(50, 10)).toBe(0)\n\n  renderer.destroy()\n})\n"
  },
  {
    "path": "packages/core/src/tests/scrollbox-hitgrid.test.ts",
    "content": "import { test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { createTestRenderer, MouseButtons, type MockMouse, type TestRenderer } from \"../testing.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { BoxRenderable } from \"../renderables/Box.js\"\nimport { Renderable } from \"../Renderable.js\"\n\nlet testRenderer: TestRenderer\nlet mockMouse: MockMouse\n\nclass MovingBoxRenderable extends BoxRenderable {\n  public shouldMove = false\n\n  protected onUpdate(_deltaTime: number): void {\n    if (this.shouldMove) {\n      this.shouldMove = false\n      this.translateY = 3\n    }\n  }\n}\n\nbeforeEach(async () => {\n  ;({ renderer: testRenderer, mockMouse } = await createTestRenderer({\n    width: 50,\n    height: 30,\n  }))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ntest(\"hit grid updates after render when scrollbox scrolls\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 40,\n    height: 20,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 30; i++) {\n    const item = new BoxRenderable(testRenderer, {\n      id: `item-${i}`,\n      height: 2,\n      backgroundColor: i % 2 === 0 ? \"red\" : \"blue\",\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const item0 = items[0]\n  const item4 = items[4]\n\n  expect(item0.y).toBe(0)\n  expect(item4.y).toBe(8)\n\n  const checkHitAt = (x: number, y: number): Renderable | undefined => {\n    const renderableId = testRenderer.hitTest(x, y)\n    return Renderable.renderablesByNumber.get(renderableId)\n  }\n\n  let hitAtItem0 = checkHitAt(5, item0.y)\n  expect(hitAtItem0?.id).toBe(\"item-0\")\n\n  let hitAtItem4 = checkHitAt(5, item4.y)\n  expect(hitAtItem4?.id).toBe(\"item-4\")\n\n  scrollBox.scrollTop = 10\n\n  expect(item0.y).toBe(-10)\n  expect(item4.y).toBe(-2)\n\n  const item5 = items[5]\n  const item9 = items[9]\n\n  expect(item5.y).toBe(0)\n  expect(item9.y).toBe(8)\n\n  // Hit grid updates after render\n  await testRenderer.idle()\n\n  const hitAtItem5 = checkHitAt(5, item5.y)\n  expect(hitAtItem5?.id).toBe(\"item-5\")\n\n  const hitAtItem9 = checkHitAt(5, item9.y)\n  expect(hitAtItem9?.id).toBe(\"item-9\")\n})\n\ntest(\"hover updates after scroll when pointer moves\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 20,\n    height: 6,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const hoverEvents: string[] = []\n  let hoveredId: string | null = null\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 5; i++) {\n    const itemId = `item-${i}`\n    const item = new BoxRenderable(testRenderer, {\n      id: itemId,\n      width: \"100%\",\n      height: 2,\n      onMouseOver: () => {\n        hoveredId = itemId\n        hoverEvents.push(`over:${itemId}`)\n      },\n      onMouseOut: () => {\n        if (hoveredId === itemId) {\n          hoveredId = null\n        }\n        hoverEvents.push(`out:${itemId}`)\n      },\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = items[0].x + 1\n  const pointerY = items[0].y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"item-0\")\n  expect(hoverEvents).toEqual([\"over:item-0\"])\n\n  scrollBox.scrollTop = 2\n  await testRenderer.idle()\n\n  // Hover updates when pointer moves after scroll and render\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"item-1\")\n  expect(hoverEvents).toEqual([\"over:item-0\", \"out:item-0\", \"over:item-1\"])\n})\n\ntest(\"hover updates after scroll without pointer movement\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 20,\n    height: 6,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const hoverEvents: string[] = []\n  let hoveredId: string | null = null\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 5; i++) {\n    const itemId = `item-${i}`\n    const item = new BoxRenderable(testRenderer, {\n      id: itemId,\n      width: \"100%\",\n      height: 2,\n      onMouseOver: () => {\n        hoveredId = itemId\n        hoverEvents.push(`over:${itemId}`)\n      },\n      onMouseOut: () => {\n        if (hoveredId === itemId) {\n          hoveredId = null\n        }\n        hoverEvents.push(`out:${itemId}`)\n      },\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = items[0].x + 1\n  const pointerY = items[0].y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"item-0\")\n  expect(hoverEvents).toEqual([\"over:item-0\"])\n\n  scrollBox.scrollTop = 2\n  await testRenderer.idle()\n\n  expect(hoveredId).toBe(\"item-1\")\n  expect(hoverEvents).toEqual([\"over:item-0\", \"out:item-0\", \"over:item-1\"])\n})\n\ntest(\"hover recheck uses neutral button and modifiers\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 20,\n    height: 6,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const hoverEvents: Array<{\n    type: \"over\" | \"out\"\n    button: number\n    modifiers: { shift: boolean; alt: boolean; ctrl: boolean }\n  }> = []\n  let hoveredId: string | null = null\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 5; i++) {\n    const itemId = `item-${i}`\n    const item = new BoxRenderable(testRenderer, {\n      id: itemId,\n      width: \"100%\",\n      height: 2,\n      onMouseOver: (event) => {\n        hoveredId = itemId\n        hoverEvents.push({\n          type: \"over\",\n          button: event.button,\n          modifiers: { ...event.modifiers },\n        })\n      },\n      onMouseOut: (event) => {\n        if (hoveredId === itemId) {\n          hoveredId = null\n        }\n        hoverEvents.push({\n          type: \"out\",\n          button: event.button,\n          modifiers: { ...event.modifiers },\n        })\n      },\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = items[0].x + 1\n  const pointerY = items[0].y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY, { modifiers: { shift: true } })\n  expect(hoveredId).toBe(\"item-0\")\n\n  await mockMouse.pressDown(pointerX, pointerY, MouseButtons.RIGHT, { modifiers: { shift: true } })\n\n  scrollBox.scrollTop = 2\n  await testRenderer.idle()\n\n  expect(hoveredId).toBe(\"item-1\")\n  expect(hoverEvents).toHaveLength(3)\n  const outEvent = hoverEvents[1]\n  const overEvent = hoverEvents[2]\n  // Synthetic hover recheck uses neutral button but preserves last known modifiers\n  expect(outEvent.button).toBe(0)\n  expect(outEvent.modifiers).toEqual({ shift: true, alt: false, ctrl: false })\n  expect(overEvent.button).toBe(0)\n  expect(overEvent.modifiers).toEqual({ shift: true, alt: false, ctrl: false })\n})\n\ntest(\"hover recheck over event has no source when not dragging\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 20,\n    height: 6,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const hoverEvents: Array<{\n    type: \"over\" | \"out\"\n    source: Renderable | undefined\n  }> = []\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 5; i++) {\n    const itemId = `item-${i}`\n    const item = new BoxRenderable(testRenderer, {\n      id: itemId,\n      width: \"100%\",\n      height: 2,\n      onMouseOver: (event) => {\n        hoverEvents.push({\n          type: \"over\",\n          source: event.source,\n        })\n      },\n      onMouseOut: (event) => {\n        hoverEvents.push({\n          type: \"out\",\n          source: event.source,\n        })\n      },\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = items[0].x + 1\n  const pointerY = items[0].y + 1\n\n  // Move to item-0 (not dragging)\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoverEvents).toHaveLength(1)\n  expect(hoverEvents[0].type).toBe(\"over\")\n  expect(hoverEvents[0].source).toBeUndefined()\n\n  // Scroll to trigger hover recheck - should have no source since we're not dragging\n  scrollBox.scrollTop = 2\n  await testRenderer.idle()\n\n  expect(hoverEvents).toHaveLength(3)\n  // out event from item-0\n  expect(hoverEvents[1].type).toBe(\"out\")\n  expect(hoverEvents[1].source).toBeUndefined()\n  // over event to item-1 - source should be undefined (not dragging)\n  expect(hoverEvents[2].type).toBe(\"over\")\n  expect(hoverEvents[2].source).toBeUndefined()\n})\n\ntest(\"hover updates on multiple scroll changes\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 20,\n    height: 6,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const hoverEvents: string[] = []\n  let hoveredId: string | null = null\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 5; i++) {\n    const itemId = `item-${i}`\n    const item = new BoxRenderable(testRenderer, {\n      id: itemId,\n      width: \"100%\",\n      height: 2,\n      onMouseOver: () => {\n        hoveredId = itemId\n        hoverEvents.push(`over:${itemId}`)\n      },\n      onMouseOut: () => {\n        if (hoveredId === itemId) {\n          hoveredId = null\n        }\n        hoverEvents.push(`out:${itemId}`)\n      },\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = items[0].x + 1\n  const pointerY = items[0].y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"item-0\")\n  expect(hoverEvents).toEqual([\"over:item-0\"])\n\n  // First scroll - hover recheck happens immediately after render\n  scrollBox.scrollTop = 2\n  await testRenderer.idle()\n  expect(hoveredId).toBe(\"item-1\")\n\n  // Second scroll - another immediate hover recheck\n  scrollBox.scrollTop = 4\n  await testRenderer.idle()\n\n  expect(hoveredId).toBe(\"item-2\")\n  // Each render triggers immediate hover recheck, so we see all transitions\n  expect(hoverEvents).toEqual([\"over:item-0\", \"out:item-0\", \"over:item-1\", \"out:item-1\", \"over:item-2\"])\n})\n\ntest(\"mouse move during scroll triggers normal hover\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 20,\n    height: 6,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const hoverEvents: string[] = []\n  let hoveredId: string | null = null\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 5; i++) {\n    const itemId = `item-${i}`\n    const item = new BoxRenderable(testRenderer, {\n      id: itemId,\n      width: \"100%\",\n      height: 2,\n      onMouseOver: () => {\n        hoveredId = itemId\n        hoverEvents.push(`over:${itemId}`)\n      },\n      onMouseOut: () => {\n        if (hoveredId === itemId) {\n          hoveredId = null\n        }\n        hoverEvents.push(`out:${itemId}`)\n      },\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = items[0].x + 1\n  const pointerY = items[0].y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"item-0\")\n  expect(hoverEvents).toEqual([\"over:item-0\"])\n\n  // Scroll triggers render which triggers immediate hover recheck\n  scrollBox.scrollTop = 2\n  await testRenderer.idle()\n  expect(hoveredId).toBe(\"item-1\")\n  expect(hoverEvents).toEqual([\"over:item-0\", \"out:item-0\", \"over:item-1\"])\n\n  // Mouse move also works and doesn't duplicate events since we're already on item-1\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"item-1\")\n  expect(hoverEvents).toEqual([\"over:item-0\", \"out:item-0\", \"over:item-1\"])\n})\n\ntest(\"hover updates immediately after render\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 20,\n    height: 6,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  let hoveredId: string | null = null\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 5; i++) {\n    const itemId = `item-${i}`\n    const item = new BoxRenderable(testRenderer, {\n      id: itemId,\n      width: \"100%\",\n      height: 2,\n      onMouseOver: () => {\n        hoveredId = itemId\n      },\n      onMouseOut: () => {\n        if (hoveredId === itemId) {\n          hoveredId = null\n        }\n      },\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = items[0].x + 1\n  const pointerY = items[0].y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"item-0\")\n\n  // Hover updates immediately after render - no delay needed\n  scrollBox.scrollTop = 2\n  await testRenderer.idle()\n  expect(hoveredId).toBe(\"item-1\")\n})\n\ntest(\"hit grid handles multiple scroll operations correctly\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 40,\n    height: 20,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 40; i++) {\n    const item = new BoxRenderable(testRenderer, {\n      id: `item-${i}`,\n      height: 2,\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const checkHitAt = (x: number, y: number): Renderable | undefined => {\n    const renderableId = testRenderer.hitTest(x, y)\n    return Renderable.renderablesByNumber.get(renderableId)\n  }\n\n  scrollBox.scrollTop = 20\n  expect(items[10].y).toBe(0)\n  await testRenderer.idle()\n  let hit = checkHitAt(5, items[10].y)\n  expect(hit?.id).toBe(\"item-10\")\n\n  scrollBox.scrollTop = 40\n  expect(items[20].y).toBe(0)\n  await testRenderer.idle()\n  hit = checkHitAt(5, items[20].y)\n  expect(hit?.id).toBe(\"item-20\")\n\n  scrollBox.scrollTop = 0\n  expect(items[0].y).toBe(0)\n  await testRenderer.idle()\n  hit = checkHitAt(5, items[0].y)\n  expect(hit?.id).toBe(\"item-0\")\n})\n\ntest(\"hit grid respects scrollbox viewport clipping when offset\", async () => {\n  const container = new BoxRenderable(testRenderer, {\n    flexDirection: \"column\",\n    width: \"100%\",\n    height: \"100%\",\n  })\n  testRenderer.root.add(container)\n\n  const header = new BoxRenderable(testRenderer, {\n    id: \"header\",\n    height: 5,\n    width: \"100%\",\n  })\n  container.add(header)\n\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 40,\n    height: 10,\n    scrollY: true,\n  })\n  container.add(scrollBox)\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 10; i++) {\n    const item = new BoxRenderable(testRenderer, {\n      id: `item-${i}`,\n      height: 2,\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const checkHitAt = (x: number, y: number): Renderable | undefined => {\n    const renderableId = testRenderer.hitTest(x, y)\n    return Renderable.renderablesByNumber.get(renderableId)\n  }\n\n  const headerHit = checkHitAt(2, header.y + 1)\n  expect(headerHit?.id).toBe(\"header\")\n\n  scrollBox.scrollTop = 4\n  await testRenderer.idle()\n\n  const headerHitAfterScroll = checkHitAt(2, header.y + 1)\n  expect(headerHitAfterScroll?.id).toBe(\"header\")\n\n  const viewportHit = checkHitAt(2, scrollBox.viewport.y + 1)\n  expect(viewportHit?.id).toBe(\"item-2\")\n})\n\ntest(\"hover recheck skips while dragging captured renderable\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 20,\n    height: 6,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const hoverEvents: string[] = []\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 5; i++) {\n    const itemId = `item-${i}`\n    const item = new BoxRenderable(testRenderer, {\n      id: itemId,\n      width: \"100%\",\n      height: 2,\n      onMouseOver: () => {\n        hoverEvents.push(`over:${itemId}`)\n      },\n      onMouseOut: () => {\n        hoverEvents.push(`out:${itemId}`)\n      },\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = items[0].x + 1\n  const pointerY = items[0].y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY)\n  await mockMouse.pressDown(pointerX, pointerY)\n  await mockMouse.moveTo(pointerX, pointerY)\n\n  scrollBox.scrollTop = 2\n  await testRenderer.idle()\n\n  // Hover recheck is skipped when there's a captured renderable (during drag)\n  expect(hoverEvents).toEqual([\"over:item-0\"])\n})\n\ntest(\"captured renderable is not in hit grid during scroll\", async () => {\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    width: 40,\n    height: 10,\n    scrollY: true,\n  })\n  testRenderer.root.add(scrollBox)\n\n  const items: BoxRenderable[] = []\n  for (let i = 0; i < 20; i++) {\n    const item = new BoxRenderable(testRenderer, {\n      id: `item-${i}`,\n      height: 2,\n    })\n    items.push(item)\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  const pointerX = 2\n  const pointerY = scrollBox.viewport.y + 1\n\n  await mockMouse.pressDown(pointerX, pointerY)\n  await mockMouse.moveTo(pointerX, pointerY + 1)\n\n  scrollBox.scrollTop = 4\n  await testRenderer.idle()\n\n  const renderableId = testRenderer.hitTest(pointerX, pointerY)\n  const hit = Renderable.renderablesByNumber.get(renderableId)\n  expect(hit?.id).toBe(\"item-2\")\n})\n\ntest(\"hit grid stays clipped after render\", async () => {\n  const container = new BoxRenderable(testRenderer, {\n    id: \"container\",\n    width: 10,\n    height: 4,\n    overflow: \"hidden\",\n  })\n  testRenderer.root.add(container)\n\n  const child = new BoxRenderable(testRenderer, {\n    id: \"child\",\n    width: 20,\n    height: 4,\n  })\n  container.add(child)\n\n  await testRenderer.idle()\n\n  const insideHitId = testRenderer.hitTest(container.x + 1, container.y + 1)\n  const insideHit = Renderable.renderablesByNumber.get(insideHitId)\n  expect(insideHit?.id).toBe(\"child\")\n\n  const outsideHitId = testRenderer.hitTest(container.x + container.width + 1, container.y + 1)\n  expect(outsideHitId).toBe(0)\n})\n\ntest(\"buffered overflow scissor uses screen coordinates for hit grid\", async () => {\n  const container = new BoxRenderable(testRenderer, {\n    id: \"buffered-container\",\n    width: 10,\n    height: 4,\n    overflow: \"hidden\",\n    buffered: true,\n    position: \"absolute\",\n    left: 10,\n    top: 5,\n  })\n  testRenderer.root.add(container)\n\n  const child = new BoxRenderable(testRenderer, {\n    id: \"buffered-child\",\n    width: 10,\n    height: 4,\n  })\n  container.add(child)\n\n  await testRenderer.idle()\n\n  const hitId = testRenderer.hitTest(container.x + 1, container.y + 1)\n  const hit = Renderable.renderablesByNumber.get(hitId)\n  expect(hit?.id).toBe(\"buffered-child\")\n})\n\ntest(\"hover updates after translate animation\", async () => {\n  const hoverEvents: string[] = []\n  let hoveredId: string | null = null\n\n  const under = new BoxRenderable(testRenderer, {\n    id: \"under\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    width: 6,\n    height: 2,\n    zIndex: 0,\n    onMouseOver: () => {\n      hoveredId = \"under\"\n      hoverEvents.push(\"over:under\")\n    },\n    onMouseOut: () => {\n      if (hoveredId === \"under\") {\n        hoveredId = null\n      }\n      hoverEvents.push(\"out:under\")\n    },\n  })\n  testRenderer.root.add(under)\n\n  const moving = new MovingBoxRenderable(testRenderer, {\n    id: \"moving\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    width: 6,\n    height: 2,\n    zIndex: 1,\n    onMouseOver: () => {\n      hoveredId = \"moving\"\n      hoverEvents.push(\"over:moving\")\n    },\n    onMouseOut: () => {\n      if (hoveredId === \"moving\") {\n        hoveredId = null\n      }\n      hoverEvents.push(\"out:moving\")\n    },\n  })\n  testRenderer.root.add(moving)\n\n  await testRenderer.idle()\n\n  const pointerX = moving.x + 1\n  const pointerY = moving.y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"moving\")\n  expect(hoverEvents).toEqual([\"over:moving\"])\n\n  moving.shouldMove = true\n  moving.requestRender()\n  await testRenderer.idle()\n\n  expect(hoveredId).toBe(\"under\")\n  expect(hoverEvents).toEqual([\"over:moving\", \"out:moving\", \"over:under\"])\n})\n\ntest(\"hover updates after z-index change\", async () => {\n  const hoverEvents: string[] = []\n  let hoveredId: string | null = null\n\n  const back = new BoxRenderable(testRenderer, {\n    id: \"back\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    width: 6,\n    height: 2,\n    zIndex: 0,\n    onMouseOver: () => {\n      hoveredId = \"back\"\n      hoverEvents.push(\"over:back\")\n    },\n    onMouseOut: () => {\n      if (hoveredId === \"back\") {\n        hoveredId = null\n      }\n      hoverEvents.push(\"out:back\")\n    },\n  })\n  testRenderer.root.add(back)\n\n  const front = new BoxRenderable(testRenderer, {\n    id: \"front\",\n    position: \"absolute\",\n    left: 2,\n    top: 2,\n    width: 6,\n    height: 2,\n    zIndex: 1,\n    onMouseOver: () => {\n      hoveredId = \"front\"\n      hoverEvents.push(\"over:front\")\n    },\n    onMouseOut: () => {\n      if (hoveredId === \"front\") {\n        hoveredId = null\n      }\n      hoverEvents.push(\"out:front\")\n    },\n  })\n  testRenderer.root.add(front)\n\n  await testRenderer.idle()\n\n  const pointerX = front.x + 1\n  const pointerY = front.y + 1\n\n  await mockMouse.moveTo(pointerX, pointerY)\n  expect(hoveredId).toBe(\"front\")\n  expect(hoverEvents).toEqual([\"over:front\"])\n\n  back.zIndex = 2\n  await testRenderer.idle()\n\n  expect(hoveredId).toBe(\"back\")\n  expect(hoverEvents).toEqual([\"over:front\", \"out:front\", \"over:back\"])\n})\n\ntest(\"scrolling does not steal clicks outside the list\", async () => {\n  let lastClick = \"none\"\n\n  const overlay = new BoxRenderable(testRenderer, {\n    id: \"overlay\",\n    position: \"absolute\",\n    left: 0,\n    top: 0,\n    width: \"100%\",\n    height: \"100%\",\n    zIndex: 100,\n    onMouseDown: () => {\n      lastClick = \"overlay\"\n    },\n  })\n  testRenderer.root.add(overlay)\n\n  const dialog = new BoxRenderable(testRenderer, {\n    id: \"dialog\",\n    position: \"absolute\",\n    left: 5,\n    top: 4,\n    width: 30,\n    height: 14,\n    flexDirection: \"column\",\n    padding: 1,\n    gap: 1,\n    onMouseDown: (event) => {\n      lastClick = \"dialog\"\n      event.stopPropagation()\n    },\n  })\n  overlay.add(dialog)\n\n  const header = new BoxRenderable(testRenderer, {\n    id: \"dialog-header\",\n    width: \"100%\",\n    height: 2,\n    onMouseDown: (event) => {\n      lastClick = \"header\"\n      event.stopPropagation()\n    },\n  })\n  dialog.add(header)\n\n  const scrollBox = new ScrollBoxRenderable(testRenderer, {\n    id: \"dialog-scrollbox\",\n    width: \"100%\",\n    height: 7,\n    scrollY: true,\n    onMouseDown: (event) => {\n      lastClick = \"scrollbox\"\n      event.stopPropagation()\n    },\n  })\n  dialog.add(scrollBox)\n\n  for (let i = 0; i < 20; i++) {\n    const item = new BoxRenderable(testRenderer, {\n      id: `line-${i}`,\n      width: \"100%\",\n      height: 1,\n    })\n    scrollBox.add(item)\n  }\n\n  await testRenderer.idle()\n\n  await mockMouse.click(scrollBox.viewport.x + 1, scrollBox.viewport.y + 1)\n  expect(lastClick).toBe(\"scrollbox\")\n\n  const headerClickY = header.y + 1\n  const targetScrollTop = Math.max(1, scrollBox.viewport.y - headerClickY)\n  scrollBox.scrollTop = targetScrollTop\n\n  await mockMouse.click(header.x + 1, headerClickY)\n  expect(lastClick).toBe(\"header\")\n\n  await mockMouse.click(dialog.x + 1, dialog.y - 1)\n  expect(lastClick).toBe(\"overlay\")\n\n  await testRenderer.idle()\n})\n"
  },
  {
    "path": "packages/core/src/tests/scrollbox.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe } from \"bun:test\"\nimport { createTestRenderer, type TestRenderer, type MockMouse, MockTreeSitterClient } from \"../testing.js\"\nimport { ScrollBoxRenderable } from \"../renderables/ScrollBox.js\"\nimport { BoxRenderable } from \"../renderables/Box.js\"\nimport { TextRenderable } from \"../renderables/Text.js\"\nimport { CodeRenderable } from \"../renderables/Code.js\"\nimport { LinearScrollAccel, MacOSScrollAccel, type ScrollAcceleration } from \"../lib/scroll-acceleration.js\"\nimport { SyntaxStyle } from \"../syntax-style.js\"\n\n// Test accelerator that returns a constant multiplier\nclass ConstantScrollAccel implements ScrollAcceleration {\n  constructor(private multiplier: number) {}\n  tick(_now?: number): number {\n    return this.multiplier\n  }\n  reset(): void {}\n}\n\nlet testRenderer: TestRenderer\nlet mockMouse: MockMouse\nlet renderOnce: () => Promise<void>\nlet captureCharFrame: () => string\nlet mockTreeSitterClient: MockTreeSitterClient\n\nbeforeEach(async () => {\n  ;({\n    renderer: testRenderer,\n    mockMouse,\n    renderOnce,\n    captureCharFrame,\n  } = await createTestRenderer({ width: 80, height: 24 }))\n  mockTreeSitterClient = new MockTreeSitterClient()\n  mockTreeSitterClient.setMockResult({ highlights: [] })\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ndescribe(\"ScrollBoxRenderable - child delegation\", () => {\n  test(\"delegates add to content wrapper\", () => {\n    const scrollbox = new ScrollBoxRenderable(testRenderer, { id: \"scrollbox\" })\n    const child = new BoxRenderable(testRenderer, { id: \"child\" })\n\n    scrollbox.add(child)\n\n    const children = scrollbox.getChildren()\n    expect(children.length).toBe(1)\n    expect(children[0].id).toBe(\"child\")\n    expect(child.parent).toBe(scrollbox.content)\n  })\n\n  test(\"delegates remove to content wrapper\", () => {\n    const scrollbox = new ScrollBoxRenderable(testRenderer, { id: \"scrollbox\" })\n    const child = new BoxRenderable(testRenderer, { id: \"child\" })\n\n    scrollbox.add(child)\n    expect(scrollbox.getChildren().length).toBe(1)\n\n    scrollbox.remove(child.id)\n    expect(scrollbox.getChildren().length).toBe(0)\n  })\n\n  test(\"delegates insertBefore to content wrapper\", () => {\n    const scrollbox = new ScrollBoxRenderable(testRenderer, { id: \"scrollbox\" })\n    const child1 = new BoxRenderable(testRenderer, { id: \"child1\" })\n    const child2 = new BoxRenderable(testRenderer, { id: \"child2\" })\n    const child3 = new BoxRenderable(testRenderer, { id: \"child3\" })\n\n    scrollbox.add(child1)\n    scrollbox.add(child2)\n    scrollbox.insertBefore(child3, child2)\n\n    const children = scrollbox.getChildren()\n    expect(children.length).toBe(3)\n    expect(children[0].id).toBe(\"child1\")\n    expect(children[1].id).toBe(\"child3\")\n    expect(children[2].id).toBe(\"child2\")\n  })\n})\n\ndescribe(\"ScrollBoxRenderable - clipping\", () => {\n  test(\"clips nested scrollbox content to inner viewport (see issue #388)\", async () => {\n    const root = new BoxRenderable(testRenderer, {\n      flexDirection: \"column\",\n      gap: 0,\n      width: 32,\n      height: 16,\n    })\n\n    const outer = new ScrollBoxRenderable(testRenderer, {\n      width: 30,\n      height: 10,\n      border: true,\n      overflow: \"hidden\",\n      scrollY: true,\n    })\n\n    const inner = new ScrollBoxRenderable(testRenderer, {\n      width: 26,\n      height: 6,\n      border: true,\n      overflow: \"hidden\",\n      scrollY: true,\n    })\n\n    for (let index = 0; index < 6; index += 1) {\n      inner.add(new TextRenderable(testRenderer, { content: `LEAK-${index}` }))\n    }\n\n    outer.add(inner)\n    root.add(outer)\n    testRenderer.root.add(root)\n\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    const innerViewportHeight = 4 // height 6 minus top/bottom border\n    const visibleLines = frame.split(\"\\n\").filter((line) => line.includes(\"LEAK-\"))\n\n    expect(visibleLines.length).toBeLessThanOrEqual(innerViewportHeight)\n  })\n})\n\ndescribe(\"ScrollBoxRenderable - padding behavior\", () => {\n  test(\"applies scrollbox padding to content while keeping scrollbar docked\", async () => {\n    const noPadding = new ScrollBoxRenderable(testRenderer, {\n      left: 0,\n      top: 0,\n      width: 26,\n      height: 9,\n      border: true,\n      scrollY: true,\n    })\n\n    const padded = new ScrollBoxRenderable(testRenderer, {\n      left: 0,\n      top: 11,\n      width: 26,\n      height: 9,\n      border: true,\n      scrollY: true,\n      padding: 2,\n    })\n\n    for (let index = 0; index < 24; index += 1) {\n      noPadding.add(new TextRenderable(testRenderer, { content: `NP-${index}` }))\n      padded.add(new TextRenderable(testRenderer, { content: `PD-${index}` }))\n    }\n\n    testRenderer.root.add(noPadding)\n    testRenderer.root.add(padded)\n\n    await renderOnce()\n\n    expect(noPadding.verticalScrollBar.visible).toBe(true)\n    expect(padded.verticalScrollBar.visible).toBe(true)\n    expect(noPadding.verticalScrollBar.x).toBe(noPadding.x + noPadding.width - 2)\n    expect(padded.verticalScrollBar.x).toBe(padded.x + padded.width - 2)\n\n    const frameLines = captureCharFrame().split(\"\\n\")\n    const noPaddingRow = frameLines.find((line) => line.includes(\"NP-0\"))\n    const paddedRow = frameLines.find((line) => line.includes(\"PD-0\"))\n\n    expect(noPaddingRow).toBeDefined()\n    expect(paddedRow).toBeDefined()\n\n    const noPaddingTextX = noPaddingRow?.indexOf(\"NP-0\") ?? -1\n    const paddedTextX = paddedRow?.indexOf(\"PD-0\") ?? -1\n\n    expect(paddedTextX).toBeGreaterThan(noPaddingTextX)\n  })\n\n  test(\"padding setter updates content inset without moving scrollbar\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 26,\n      height: 9,\n      border: true,\n      scrollY: true,\n    })\n\n    for (let index = 0; index < 24; index += 1) {\n      scrollBox.add(new TextRenderable(testRenderer, { content: `PX-${index}` }))\n    }\n\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    const beforeScrollbarX = scrollBox.verticalScrollBar.x\n    const beforeFrameLines = captureCharFrame().split(\"\\n\")\n    const beforeRow = beforeFrameLines.find((line) => line.includes(\"PX-0\"))\n    const beforeTextX = beforeRow?.indexOf(\"PX-0\") ?? -1\n\n    scrollBox.padding = 2\n    await renderOnce()\n\n    const afterFrameLines = captureCharFrame().split(\"\\n\")\n    const afterRow = afterFrameLines.find((line) => line.includes(\"PX-0\"))\n    const afterTextX = afterRow?.indexOf(\"PX-0\") ?? -1\n\n    expect(scrollBox.verticalScrollBar.x).toBe(beforeScrollbarX)\n    expect(afterTextX).toBeGreaterThan(beforeTextX)\n  })\n})\n\ndescribe(\"ScrollBoxRenderable - destroyRecursively\", () => {\n  test(\"destroys internal ScrollBox components\", () => {\n    const parent = new ScrollBoxRenderable(testRenderer, { id: \"scroll-parent\" })\n    const child = new BoxRenderable(testRenderer, { id: \"child\" })\n\n    parent.add(child)\n\n    const wrapper = parent.wrapper\n    const viewport = parent.viewport\n    const content = parent.content\n    const horizontalScrollBar = parent.horizontalScrollBar\n    const verticalScrollBar = parent.verticalScrollBar\n\n    expect(parent.isDestroyed).toBe(false)\n    expect(child.isDestroyed).toBe(false)\n    expect(wrapper.isDestroyed).toBe(false)\n    expect(viewport.isDestroyed).toBe(false)\n    expect(content.isDestroyed).toBe(false)\n    expect(horizontalScrollBar.isDestroyed).toBe(false)\n    expect(verticalScrollBar.isDestroyed).toBe(false)\n\n    parent.destroyRecursively()\n\n    expect(parent.isDestroyed).toBe(true)\n    expect(child.isDestroyed).toBe(true)\n    expect(wrapper.isDestroyed).toBe(true)\n    expect(viewport.isDestroyed).toBe(true)\n    expect(content.isDestroyed).toBe(true)\n    expect(horizontalScrollBar.isDestroyed).toBe(true)\n    expect(verticalScrollBar.isDestroyed).toBe(true)\n  })\n})\n\ndescribe(\"ScrollBoxRenderable - Mouse interaction\", () => {\n  test(\"scrolls with mouse wheel\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollAcceleration: new MacOSScrollAccel({ A: 0 }),\n    })\n    for (let i = 0; i < 50; i++) scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    await mockMouse.scroll(25, 10, \"down\")\n    await renderOnce()\n    expect(scrollBox.scrollTop).toBeGreaterThan(0)\n  })\n\n  test(\"single isolated scroll has same distance as linear\", async () => {\n    const linearBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollAcceleration: new LinearScrollAccel(),\n    })\n\n    for (let i = 0; i < 100; i++) linearBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    testRenderer.root.add(linearBox)\n    await renderOnce()\n\n    await mockMouse.scroll(25, 10, \"down\")\n    await renderOnce()\n    const linearDistance = linearBox.scrollTop\n\n    testRenderer.destroy()\n    ;({\n      renderer: testRenderer,\n      mockMouse,\n      renderOnce,\n      captureCharFrame,\n    } = await createTestRenderer({ width: 80, height: 24 }))\n\n    const accelBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollAcceleration: new MacOSScrollAccel(),\n    })\n\n    for (let i = 0; i < 100; i++) accelBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    testRenderer.root.add(accelBox)\n    await renderOnce()\n\n    await mockMouse.scroll(25, 10, \"down\")\n    await renderOnce()\n\n    expect(accelBox.scrollTop).toBe(linearDistance)\n  })\n\n  test(\"acceleration makes rapid scrolls cover more distance\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollAcceleration: new MacOSScrollAccel({ A: 0.8, tau: 3, maxMultiplier: 6 }),\n    })\n    for (let i = 0; i < 200; i++) scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    await mockMouse.scroll(25, 10, \"down\")\n    await renderOnce()\n    const slowScrollDistance = scrollBox.scrollTop\n\n    scrollBox.scrollTop = 0\n\n    for (let i = 0; i < 5; i++) {\n      await mockMouse.scroll(25, 10, \"down\")\n      await renderOnce()\n    }\n    const rapidScrollDistance = scrollBox.scrollTop\n\n    expect(rapidScrollDistance).toBeGreaterThan(slowScrollDistance * 3)\n  })\n\n  test(\"multiplier < 1 slows down scroll distance\", async () => {\n    // Test with slowdown using a constant multiplier < 1\n    const slowdownBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollAcceleration: new ConstantScrollAccel(0.5),\n    })\n    for (let i = 0; i < 200; i++) slowdownBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    testRenderer.root.add(slowdownBox)\n    await renderOnce()\n\n    // ConstantScrollAccel ignores timing, so no delay needed\n    for (let i = 0; i < 5; i++) {\n      await mockMouse.scroll(25, 10, \"down\")\n      await renderOnce()\n    }\n    const slowdownDistance = slowdownBox.scrollTop\n\n    testRenderer.destroy()\n    ;({\n      renderer: testRenderer,\n      mockMouse,\n      renderOnce,\n      captureCharFrame,\n    } = await createTestRenderer({\n      width: 80,\n      height: 24,\n    }))\n\n    // Compare with linear (no slowdown)\n    const linearBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollAcceleration: new LinearScrollAccel(),\n    })\n    for (let i = 0; i < 200; i++) linearBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    testRenderer.root.add(linearBox)\n    await renderOnce()\n\n    // LinearScrollAccel ignores timing, so no delay needed\n    for (let i = 0; i < 5; i++) {\n      await mockMouse.scroll(25, 10, \"down\")\n      await renderOnce()\n    }\n    const linearDistance = linearBox.scrollTop\n\n    expect(slowdownDistance).toBeLessThan(linearDistance)\n    expect(slowdownDistance).toBeGreaterThan(0)\n  })\n\n  test(\"multiplier < 1 accumulates fractional scroll amounts\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollAcceleration: new ConstantScrollAccel(0.3),\n    })\n    for (let i = 0; i < 200; i++) scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    // With multiplier < 1, fractional amounts accumulate\n    // It should take multiple scroll events to accumulate enough to scroll 1 full unit\n    let scrolled = false\n    for (let i = 0; i < 5; i++) {\n      await mockMouse.scroll(25, 10, \"down\")\n      await renderOnce()\n      if (scrollBox.scrollTop > 0) {\n        scrolled = true\n        break\n      }\n    }\n\n    expect(scrolled).toBe(true)\n    expect(scrollBox.scrollTop).toBeGreaterThan(0)\n  })\n\n  test(\"horizontal scroll with multiplier < 1 works correctly\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollX: true,\n      scrollAcceleration: new ConstantScrollAccel(0.4),\n    })\n\n    const wideBox = new BoxRenderable(testRenderer, { width: 300, height: 10 })\n    scrollBox.add(wideBox)\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    await mockMouse.scroll(25, 10, \"right\")\n    await renderOnce()\n\n    // Should eventually scroll after multiple events due to accumulation\n    let scrolled = false\n    for (let i = 0; i < 5; i++) {\n      await mockMouse.scroll(25, 10, \"right\")\n      await renderOnce()\n      if (scrollBox.scrollLeft > 0) {\n        scrolled = true\n        break\n      }\n    }\n\n    expect(scrolled).toBe(true)\n  })\n\n  test(\"multiplier < 1 with acceleration work together\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 50,\n      height: 20,\n      scrollAcceleration: new ConstantScrollAccel(0.3),\n    })\n    for (let i = 0; i < 200; i++) scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    // Multiple scrolls should accumulate fractional amounts\n    for (let i = 0; i < 10; i++) {\n      await mockMouse.scroll(25, 10, \"down\")\n      await renderOnce()\n    }\n    const scrollDistance = scrollBox.scrollTop\n\n    // With 0.3 multiplier and 10 scrolls: 10 * 1 * 0.3 = 3 pixels total\n    // Math.trunc applied each time, so we get 2 pixels actually scrolled\n    expect(scrollDistance).toBeGreaterThan(0)\n    expect(scrollDistance).toBeLessThan(5)\n  })\n})\n\ndescribe(\"ScrollBoxRenderable - Content Visibility\", () => {\n  test(\"maintains visibility when scrolling with many Code elements\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    const parent = new BoxRenderable(testRenderer, {\n      flexDirection: \"column\",\n      gap: 1,\n    })\n\n    const header = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    header.add(new TextRenderable(testRenderer, { content: \"Header Content\" }))\n\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      flexGrow: 1,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    footer.add(new TextRenderable(testRenderer, { content: \"Footer Content\" }))\n\n    parent.add(header)\n    parent.add(scrollBox)\n    parent.add(footer)\n    testRenderer.root.add(parent)\n\n    await renderOnce()\n    const initialFrame = captureCharFrame()\n    expect(initialFrame).toContain(\"Header Content\")\n    expect(initialFrame).toContain(\"Footer Content\")\n\n    const codeContent = `\n# HELLO\n\nworld\n\n## HELLO World\n\n\\`\\`\\`html\n<div class=\"example\">\n  <p>Content</p>\n</div>\n\\`\\`\\`\n`\n\n    const codes: CodeRenderable[] = []\n    for (let i = 0; i < 100; i++) {\n      const wrapper = new BoxRenderable(testRenderer, {\n        marginTop: 2,\n        marginBottom: 2,\n      })\n      const code = new CodeRenderable(testRenderer, {\n        content: codeContent,\n        filetype: \"markdown\",\n        syntaxStyle,\n        drawUnstyledText: false,\n        treeSitterClient: mockTreeSitterClient,\n      })\n      codes.push(code)\n      wrapper.add(code)\n      scrollBox.add(wrapper)\n    }\n\n    await renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await Promise.all(codes.map((c) => c.highlightingDone))\n    await renderOnce()\n\n    scrollBox.scrollTo(scrollBox.scrollHeight)\n    await renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await Promise.all(codes.map((c) => c.highlightingDone))\n    await renderOnce()\n\n    const frameAfterScroll = captureCharFrame()\n\n    expect(frameAfterScroll).toContain(\"Header Content\")\n    expect(frameAfterScroll).toContain(\"Footer Content\")\n\n    const hasCodeContent =\n      frameAfterScroll.includes(\"HELLO\") ||\n      frameAfterScroll.includes(\"world\") ||\n      frameAfterScroll.includes(\"<div\") ||\n      frameAfterScroll.includes(\"```\")\n\n    expect(hasCodeContent).toBe(true)\n\n    const nonWhitespaceChars = frameAfterScroll.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(50)\n  })\n\n  test(\"maintains visibility when scrolling with many Code elements (setter-based, like SolidJS)\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    const parent = new BoxRenderable(testRenderer, {\n      flexDirection: \"column\",\n      gap: 1,\n    })\n\n    const header = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    header.add(new TextRenderable(testRenderer, { content: \"Header Content\" }))\n\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      flexGrow: 1,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    footer.add(new TextRenderable(testRenderer, { content: \"Footer Content\" }))\n\n    parent.add(header)\n    parent.add(scrollBox)\n    parent.add(footer)\n    testRenderer.root.add(parent)\n\n    await renderOnce()\n    const initialFrame = captureCharFrame()\n    expect(initialFrame).toContain(\"Header Content\")\n    expect(initialFrame).toContain(\"Footer Content\")\n\n    const codeContent = `\n# HELLO\n\nworld\n\n## HELLO World\n\n\\`\\`\\`html\n<div class=\"example\">\n  <p>Content</p>\n</div>\n\\`\\`\\`\n`\n\n    for (let i = 0; i < 100; i++) {\n      const wrapper = new BoxRenderable(testRenderer, { id: `wrapper-${i}` })\n      wrapper.marginTop = 2\n      wrapper.marginBottom = 2\n\n      const code = new CodeRenderable(testRenderer, {\n        id: `code-${i}`,\n        syntaxStyle,\n        drawUnstyledText: false,\n        treeSitterClient: mockTreeSitterClient,\n      })\n\n      wrapper.add(code)\n      code.content = codeContent\n      code.filetype = \"markdown\"\n\n      scrollBox.add(wrapper)\n    }\n\n    await testRenderer.idle()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await Promise.resolve()\n\n    await testRenderer.idle()\n\n    scrollBox.scrollTo(scrollBox.scrollHeight)\n    await testRenderer.idle()\n\n    const frameAfterScroll = captureCharFrame()\n\n    expect(frameAfterScroll).toContain(\"Header Content\")\n    expect(frameAfterScroll).toContain(\"Footer Content\")\n\n    const hasCodeContent =\n      frameAfterScroll.includes(\"HELLO\") ||\n      frameAfterScroll.includes(\"world\") ||\n      frameAfterScroll.includes(\"<div\") ||\n      frameAfterScroll.includes(\"```\")\n\n    expect(hasCodeContent).toBe(true)\n\n    const nonWhitespaceChars = frameAfterScroll.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(50)\n  })\n\n  test(\"maintains visibility with simple Code elements (constructor)\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    const parent = new BoxRenderable(testRenderer, {\n      flexDirection: \"column\",\n      gap: 1,\n    })\n\n    const header = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    header.add(new TextRenderable(testRenderer, { content: \"Header\" }))\n\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      flexGrow: 1,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    footer.add(new TextRenderable(testRenderer, { content: \"Footer\" }))\n\n    parent.add(header)\n    parent.add(scrollBox)\n    parent.add(footer)\n    testRenderer.root.add(parent)\n\n    await renderOnce()\n\n    const codes: CodeRenderable[] = []\n    for (let i = 0; i < 50; i++) {\n      const wrapper = new BoxRenderable(testRenderer, {\n        marginTop: 1,\n        marginBottom: 1,\n      })\n      const code = new CodeRenderable(testRenderer, {\n        content: `Item ${i}`,\n        filetype: \"markdown\",\n        syntaxStyle,\n        drawUnstyledText: false,\n        treeSitterClient: mockTreeSitterClient,\n      })\n      codes.push(code)\n      wrapper.add(code)\n      scrollBox.add(wrapper)\n    }\n\n    await renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await Promise.all(codes.map((c) => c.highlightingDone))\n    await renderOnce()\n\n    scrollBox.scrollTo(scrollBox.scrollHeight)\n    await renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await Promise.all(codes.map((c) => c.highlightingDone))\n    await renderOnce()\n\n    const frame = captureCharFrame()\n\n    expect(frame).toContain(\"Header\")\n    expect(frame).toContain(\"Footer\")\n\n    const hasItems = /Item \\d+/.test(frame)\n    expect(hasItems).toBe(true)\n\n    const nonWhitespaceChars = frame.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(18)\n  })\n\n  test(\"maintains visibility with simple Code elements (setter-based, like SolidJS)\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    const parent = new BoxRenderable(testRenderer, {\n      flexDirection: \"column\",\n      gap: 1,\n    })\n\n    const header = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    header.add(new TextRenderable(testRenderer, { content: \"Header\" }))\n\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      flexGrow: 1,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    footer.add(new TextRenderable(testRenderer, { content: \"Footer\" }))\n\n    parent.add(header)\n    parent.add(scrollBox)\n    parent.add(footer)\n    testRenderer.root.add(parent)\n\n    await renderOnce()\n\n    const codes: CodeRenderable[] = []\n    for (let i = 0; i < 50; i++) {\n      const wrapper = new BoxRenderable(testRenderer, { id: `wrapper-${i}` })\n      wrapper.marginTop = 1\n      wrapper.marginBottom = 1\n\n      const code = new CodeRenderable(testRenderer, {\n        id: `code-${i}`,\n        syntaxStyle,\n        drawUnstyledText: false,\n        treeSitterClient: mockTreeSitterClient,\n      })\n\n      wrapper.add(code)\n      code.content = `Item ${i}`\n      code.filetype = \"markdown\"\n      codes.push(code)\n\n      scrollBox.add(wrapper)\n    }\n\n    await renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await Promise.all(codes.map((c) => c.highlightingDone))\n    await renderOnce()\n\n    scrollBox.scrollTo(scrollBox.scrollHeight)\n    await renderOnce()\n\n    const frame = captureCharFrame()\n\n    expect(frame).toContain(\"Header\")\n    expect(frame).toContain(\"Footer\")\n\n    const hasItems = /Item \\d+/.test(frame)\n    expect(hasItems).toBe(true)\n\n    const nonWhitespaceChars = frame.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(18)\n  })\n\n  test(\"maintains visibility with TextRenderable elements\", async () => {\n    const parent = new BoxRenderable(testRenderer, {\n      flexDirection: \"column\",\n      gap: 1,\n    })\n\n    const header = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    header.add(new TextRenderable(testRenderer, { content: \"Header\" }))\n\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      flexGrow: 1,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    footer.add(new TextRenderable(testRenderer, { content: \"Footer\" }))\n\n    parent.add(header)\n    parent.add(scrollBox)\n    parent.add(footer)\n    testRenderer.root.add(parent)\n\n    await renderOnce()\n\n    for (let i = 0; i < 50; i++) {\n      const wrapper = new BoxRenderable(testRenderer, {\n        marginTop: 1,\n        marginBottom: 1,\n      })\n      wrapper.add(new TextRenderable(testRenderer, { content: `Item ${i}` }))\n      scrollBox.add(wrapper)\n    }\n\n    await renderOnce()\n\n    scrollBox.scrollTo(scrollBox.scrollHeight)\n    await renderOnce()\n\n    const frame = captureCharFrame()\n\n    expect(frame).toContain(\"Header\")\n    expect(frame).toContain(\"Footer\")\n\n    const hasItems = /Item \\d+/.test(frame)\n    expect(hasItems).toBe(true)\n\n    const nonWhitespaceChars = frame.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(20)\n  })\n\n  test(\"stays scrolled to bottom with growing code renderables in sticky scroll mode\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    // Use manual-resolving mock client for deterministic behavior\n    const autoResolvingClient = new MockTreeSitterClient()\n    autoResolvingClient.setMockResult({ highlights: [] })\n\n    const parent = new BoxRenderable(testRenderer, {\n      flexDirection: \"column\",\n      gap: 1,\n    })\n\n    const header = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    header.add(new TextRenderable(testRenderer, { content: \"Header\" }))\n\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      flexGrow: 1,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    const footer = new BoxRenderable(testRenderer, { flexShrink: 0 })\n    footer.add(new TextRenderable(testRenderer, { content: \"Footer\" }))\n\n    parent.add(header)\n    parent.add(scrollBox)\n    parent.add(footer)\n    testRenderer.root.add(parent)\n\n    const scrollPositions: number[] = []\n    const maxScrollPositions: number[] = []\n    const wrapper1 = new BoxRenderable(testRenderer, {\n      marginTop: 1,\n      marginBottom: 1,\n    })\n    const code1 = new CodeRenderable(testRenderer, {\n      content: \"console.log('hello')\",\n      filetype: \"javascript\",\n      syntaxStyle,\n      drawUnstyledText: false,\n      treeSitterClient: autoResolvingClient,\n    })\n    wrapper1.add(code1)\n    scrollBox.add(wrapper1)\n\n    await renderOnce()\n    autoResolvingClient.resolveAllHighlightOnce()\n    await code1.highlightingDone\n    await renderOnce()\n\n    scrollPositions.push(scrollBox.scrollTop)\n    maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))\n    expect(scrollBox.scrollTop).toBe(maxScrollPositions[0])\n\n    code1.content = `console.log('hello')\nconst foo = 'bar'\nconst baz = 'qux'\nfunction test() {\n  return 42\n}\nconsole.log(test())`\n\n    await renderOnce()\n    autoResolvingClient.resolveAllHighlightOnce()\n    await code1.highlightingDone\n    await renderOnce()\n\n    scrollPositions.push(scrollBox.scrollTop)\n    maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))\n    expect(scrollBox.scrollTop).toBe(maxScrollPositions[1])\n\n    const wrapper2 = new BoxRenderable(testRenderer, {\n      marginTop: 1,\n      marginBottom: 1,\n    })\n    const code2 = new CodeRenderable(testRenderer, {\n      content: \"const x = 10\\nconst y = 20\",\n      filetype: \"javascript\",\n      syntaxStyle,\n      drawUnstyledText: false,\n      treeSitterClient: autoResolvingClient,\n    })\n    wrapper2.add(code2)\n    scrollBox.add(wrapper2)\n\n    await renderOnce()\n    autoResolvingClient.resolveAllHighlightOnce()\n    await code2.highlightingDone\n    await renderOnce()\n\n    scrollPositions.push(scrollBox.scrollTop)\n    maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))\n    expect(scrollBox.scrollTop).toBe(maxScrollPositions[2])\n\n    code2.content = `const x = 10\nconst y = 20\nconst z = x + y\nconsole.log(z)\nfunction multiply(a, b) {\n  return a * b\n}\nconst result = multiply(x, y)\nconsole.log('Result:', result)`\n\n    await renderOnce()\n    autoResolvingClient.resolveAllHighlightOnce()\n    await code2.highlightingDone\n    await renderOnce()\n\n    scrollPositions.push(scrollBox.scrollTop)\n    maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))\n    expect(scrollBox.scrollTop).toBe(maxScrollPositions[3])\n\n    const wrapper3 = new BoxRenderable(testRenderer, {\n      marginTop: 1,\n      marginBottom: 1,\n    })\n    const code3 = new CodeRenderable(testRenderer, {\n      content: \"// Final code block\\nconst final = 'done'\",\n      filetype: \"javascript\",\n      syntaxStyle,\n      drawUnstyledText: false,\n      treeSitterClient: autoResolvingClient,\n    })\n    wrapper3.add(code3)\n    scrollBox.add(wrapper3)\n\n    await renderOnce()\n    autoResolvingClient.resolveAllHighlightOnce()\n    await code3.highlightingDone\n    await renderOnce()\n\n    scrollPositions.push(scrollBox.scrollTop)\n    maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))\n    expect(scrollBox.scrollTop).toBe(maxScrollPositions[4])\n\n    code3.content = `// Final code block\nconst final = 'done'\n\nclass DataProcessor {\n  constructor(data) {\n    this.data = data\n  }\n  \n  process() {\n    return this.data.map(item => item * 2)\n  }\n  \n  filter(predicate) {\n    return this.data.filter(predicate)\n  }\n  \n  reduce(fn, initial) {\n    return this.data.reduce(fn, initial)\n  }\n}\n\nconst processor = new DataProcessor([1, 2, 3, 4, 5])\nconsole.log(processor.process())\nconsole.log(processor.filter(x => x > 2))\nconsole.log(processor.reduce((acc, val) => acc + val, 0))`\n\n    await renderOnce()\n    autoResolvingClient.resolveAllHighlightOnce()\n    await code3.highlightingDone\n    await renderOnce()\n\n    scrollPositions.push(scrollBox.scrollTop)\n    maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))\n    expect(scrollBox.scrollTop).toBe(maxScrollPositions[5])\n\n    const frame = captureCharFrame()\n    expect(frame).toContain(\"Header\")\n    expect(frame).toContain(\"Footer\")\n\n    const hasCodeContent =\n      frame.includes(\"console\") ||\n      frame.includes(\"function\") ||\n      frame.includes(\"const\") ||\n      frame.includes(\"DataProcessor\") ||\n      frame.includes(\"processor\")\n\n    expect(hasCodeContent).toBe(true)\n\n    const nonWhitespaceChars = frame.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(50)\n\n    for (let i = 0; i < scrollPositions.length; i++) {\n      expect(scrollPositions[i]).toBe(maxScrollPositions[i])\n    }\n  })\n\n  test(\"sticky scroll bottom stays at bottom after scrollBy/scrollTo is called\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    scrollBox.add(new TextRenderable(testRenderer, { content: `Line 0` }))\n    await renderOnce()\n\n    scrollBox.scrollBy(100000)\n    await renderOnce()\n\n    scrollBox.scrollTo(scrollBox.scrollHeight)\n    await renderOnce()\n\n    for (let i = 1; i < 30; i++) {\n      scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n      await renderOnce()\n\n      const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n\n      if (i === 16) {\n        expect(scrollBox.scrollTop).toBe(maxScroll)\n      }\n    }\n  })\n\n  test(\"scrolls CodeRenderable with LineNumberRenderable using mouse wheel\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n      scrollY: true,\n      scrollX: false,\n    })\n\n    // Create long code content that needs scrolling\n    let code = \"Line 1\\n\"\n    for (let i = 2; i <= 30; i++) {\n      code += `Line ${i}\\n`\n    }\n\n    const { LineNumberRenderable } = await import(\"../renderables/LineNumberRenderable\")\n    const codeRenderable = new CodeRenderable(testRenderer, {\n      content: code,\n      filetype: \"javascript\",\n      syntaxStyle,\n      drawUnstyledText: true,\n      treeSitterClient: mockTreeSitterClient,\n      width: \"100%\",\n    })\n\n    const codeWithLines = new LineNumberRenderable(testRenderer, {\n      target: codeRenderable,\n      width: \"100%\",\n    })\n\n    scrollBox.add(codeWithLines)\n    testRenderer.root.add(scrollBox)\n\n    await renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await codeRenderable.highlightingDone\n    await renderOnce()\n\n    // Capture initial frame (should show top lines)\n    const frameTop = captureCharFrame()\n    expect(frameTop).toContain(\"Line 1\")\n    expect(frameTop).not.toContain(\"Line 30\")\n\n    // Scroll down to bottom\n    for (let i = 0; i < 25; i++) {\n      await mockMouse.scroll(20, 5, \"down\")\n      await renderOnce()\n    }\n\n    // Capture after scroll (should show bottom lines)\n    const frameBottom = captureCharFrame()\n    expect(frameBottom).toMatchSnapshot()\n    expect(frameBottom).toContain(\"Line 30\")\n    expect(frameBottom).not.toContain(\"Line 1\")\n  })\n\n  test(\"sticky scroll bottom stays at bottom when gradually filled with code renderables\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    const scrollPositions: number[] = []\n    const maxScrollPositions: number[] = []\n\n    scrollPositions.push(scrollBox.scrollTop)\n    maxScrollPositions.push(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height))\n\n    for (let i = 0; i < 10; i++) {\n      const code = new CodeRenderable(testRenderer, {\n        syntaxStyle,\n        drawUnstyledText: false,\n        treeSitterClient: mockTreeSitterClient,\n      })\n\n      let content = `// Block ${i}\\n`\n      for (let j = 0; j <= i; j++) {\n        content += `const var${j} = ${j}\\n`\n      }\n      code.content = content\n      code.filetype = \"javascript\"\n\n      scrollBox.add(code)\n\n      await renderOnce()\n      mockTreeSitterClient.resolveAllHighlightOnce()\n      await code.highlightingDone\n      await renderOnce()\n\n      const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n      scrollPositions.push(scrollBox.scrollTop)\n      maxScrollPositions.push(maxScroll)\n    }\n\n    for (let i = 0; i < scrollPositions.length; i++) {\n      expect(scrollPositions[i]).toBe(maxScrollPositions[i])\n    }\n  })\n\n  test(\"clips nested scrollboxes when multiple stacked children overflow (app-style tool blocks)\", async () => {\n    const custom = await createTestRenderer({ width: 120, height: 40 })\n    const { renderer, renderOnce, captureCharFrame } = custom\n\n    const root = new BoxRenderable(renderer, { flexDirection: \"column\", width: 118, height: 38, gap: 0 })\n    const header = new BoxRenderable(renderer, { height: 3, border: true })\n    header.add(new TextRenderable(testRenderer, { content: \"HEADER\" }))\n    root.add(header)\n\n    const outer = new ScrollBoxRenderable(renderer, { height: 25, border: true, overflow: \"hidden\", scrollY: true })\n    expect((outer as any)._overflow).toBe(\"hidden\")\n\n    const addToolBlock = (id: number) => {\n      const wrapper = new BoxRenderable(renderer, { border: true, padding: 0, marginTop: 0, marginBottom: 0 })\n      const inner = new ScrollBoxRenderable(renderer, {\n        height: 10,\n        border: true,\n        overflow: \"hidden\",\n        scrollY: true,\n        contentOptions: { paddingTop: 0, paddingBottom: 0, paddingLeft: 0, paddingRight: 0 },\n      })\n      expect((inner as any)._overflow).toBe(\"hidden\")\n      for (let i = 0; i < 15; i += 1) {\n        inner.add(new TextRenderable(renderer, { content: `[tool ${id}] line ${i}` }))\n      }\n      wrapper.add(inner)\n      outer.add(wrapper)\n    }\n\n    addToolBlock(1)\n    addToolBlock(2)\n    addToolBlock(3)\n\n    root.add(outer)\n\n    const footer = new BoxRenderable(renderer, { height: 3, border: true })\n    footer.add(new TextRenderable(renderer, { content: \"FOOTER\" }))\n    root.add(footer)\n\n    renderer.root.add(root)\n    await renderer.idle()\n    expect(outer.width).toBeGreaterThan(0)\n    expect(outer.height).toBeGreaterThan(0)\n\n    const frame = captureCharFrame()\n\n    // The third tool block should be clipped entirely (outer height fits ~two blocks).\n    expect(frame).not.toMatch(/\\[tool 3\\] line 1/)\n\n    renderer.destroy()\n  })\n\n  test(\"does not overdraw above header when scrolling nested tool blocks upward\", async () => {\n    const custom = await createTestRenderer({ width: 120, height: 24 })\n    const { renderer, renderOnce, captureCharFrame } = custom\n\n    const root = new BoxRenderable(renderer, { flexDirection: \"column\", width: 118, height: 22, gap: 0 })\n    const header = new BoxRenderable(renderer, { height: 3, border: true })\n    header.add(new TextRenderable(renderer, { content: \"HEADER\" }))\n    root.add(header)\n\n    const outer = new ScrollBoxRenderable(renderer, { height: 14, border: true, overflow: \"hidden\", scrollY: true })\n    const inner = new ScrollBoxRenderable(renderer, { height: 10, border: true, overflow: \"hidden\", scrollY: true })\n    for (let i = 0; i < 12; i += 1) {\n      inner.add(new TextRenderable(renderer, { content: `[tool] line ${i}` }))\n    }\n    outer.add(inner)\n    root.add(outer)\n\n    const footer = new BoxRenderable(renderer, { height: 3, border: true })\n    footer.add(new TextRenderable(renderer, { content: \"FOOTER\" }))\n    root.add(footer)\n\n    renderer.root.add(root)\n    await renderOnce()\n\n    // Scroll up to try to draw above header\n    inner.scrollTo({ x: 0, y: -100 })\n    outer.scrollTo({ x: 0, y: -100 })\n    await renderOnce()\n\n    const frame = captureCharFrame()\n    const headerIndex = frame.indexOf(\"HEADER\")\n    const firstToolIndex = frame.indexOf(\"[tool] line 0\")\n\n    expect(headerIndex).toBeGreaterThan(-1)\n    expect(firstToolIndex).toBeGreaterThan(headerIndex)\n\n    renderer.destroy()\n  })\n\n  // Regression test for issue #530: sticky scroll jumps to top after manual scroll\n  test(\"resets _hasManualScroll when user scrolls back to sticky position (issue #530)\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    testRenderer.root.add(scrollBox)\n\n    // Add enough content to overflow the viewport\n    for (let i = 0; i < 20; i++) {\n      scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n    }\n    await renderOnce()\n\n    const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n    expect(scrollBox.scrollTop).toBe(maxScroll)\n    expect((scrollBox as any)._hasManualScroll).toBe(false)\n\n    // User scrolls up manually - this sets _hasManualScroll = true\n    scrollBox.scrollTo(5)\n    await renderOnce()\n\n    expect(scrollBox.scrollTop).toBe(5)\n    expect((scrollBox as any)._hasManualScroll).toBe(true)\n\n    // User scrolls back to bottom - this should reset _hasManualScroll = false\n    const newMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n    scrollBox.scrollTo(newMaxScroll)\n    await renderOnce()\n\n    expect(scrollBox.scrollTop).toBe(newMaxScroll)\n    // This is the fix: _hasManualScroll should be reset when back at sticky position\n    expect((scrollBox as any)._hasManualScroll).toBe(false)\n\n    // Add more content - should stay at bottom because sticky scroll is re-enabled\n    for (let i = 20; i < 30; i++) {\n      scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n      await renderOnce()\n\n      const expectedMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n      // Without the fix, this would fail: scroll would jump to top\n      expect(scrollBox.scrollTop).toBe(expectedMaxScroll)\n    }\n  })\n\n  // Regression test for issue #709: content size recalculation should not clear manual scroll state\n  test(\"does not reset _hasManualScroll during content size recalculation (issue #709)\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    testRenderer.root.add(scrollBox)\n\n    for (let i = 0; i < 30; i++) {\n      scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))\n    }\n    await renderOnce()\n\n    const initialMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n    expect(scrollBox.scrollTop).toBe(initialMaxScroll)\n    expect((scrollBox as any)._hasManualScroll).toBe(false)\n\n    scrollBox.scrollTo(5)\n    await renderOnce()\n\n    expect(scrollBox.scrollTop).toBe(5)\n    expect((scrollBox as any)._hasManualScroll).toBe(true)\n\n    // Force a size recalculation that programmatically clamps scrollTop to 0.\n    // This must not be treated as a user returning to sticky position.\n    for (let i = 0; i < 28; i++) {\n      scrollBox.remove(`line-${i}`)\n    }\n    await renderOnce()\n\n    expect(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)).toBe(0)\n    expect(scrollBox.scrollTop).toBe(0)\n    expect((scrollBox as any)._hasManualScroll).toBe(true)\n\n    // When content grows again, we should keep manual-scroll mode and stay away from sticky bottom.\n    for (let i = 30; i < 50; i++) {\n      scrollBox.add(new TextRenderable(testRenderer, { id: `line-${i}`, content: `Line ${i}` }))\n    }\n    await renderOnce()\n\n    const newMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n    expect(newMaxScroll).toBeGreaterThan(0)\n    expect((scrollBox as any)._hasManualScroll).toBe(true)\n    expect(scrollBox.scrollTop).toBe(0)\n  })\n\n  test(\"scrollChildIntoView does nothing when child is already visible\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n    })\n\n    for (let i = 0; i < 10; i++) {\n      scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, height: 1 }))\n    }\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    expect(scrollBox.scrollTop).toBe(0)\n\n    scrollBox.scrollChildIntoView(\"child-2\")\n    expect(scrollBox.scrollTop).toBe(0)\n  })\n\n  test(\"scrollChildIntoView scrolls down to reveal child below viewport\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n    })\n\n    for (let i = 0; i < 30; i++) {\n      scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, height: 1 }))\n    }\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    scrollBox.scrollChildIntoView(\"child-25\")\n    await renderOnce()\n\n    const child = scrollBox.content.findDescendantById(\"child-25\")!\n    expect(child.y).toBeGreaterThanOrEqual(scrollBox.viewport.y)\n    expect(child.y + child.height).toBe(scrollBox.viewport.y + scrollBox.viewport.height)\n  })\n\n  test(\"scrollChildIntoView scrolls up to reveal child above viewport\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n    })\n\n    for (let i = 0; i < 30; i++) {\n      scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, height: 1 }))\n    }\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    scrollBox.scrollTo(20)\n    await renderOnce()\n\n    scrollBox.scrollChildIntoView(\"child-15\")\n    await renderOnce()\n\n    const child = scrollBox.content.findDescendantById(\"child-15\")!\n    expect(child.y).toBe(scrollBox.viewport.y)\n    expect(child.y + child.height).toBeLessThanOrEqual(scrollBox.viewport.y + scrollBox.viewport.height)\n  })\n\n  test(\"scrollChildIntoView finds nested descendants by id\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n    })\n\n    for (let i = 0; i < 30; i++) {\n      const wrapper = new BoxRenderable(testRenderer, { id: `wrapper-${i}`, height: 1 })\n      wrapper.add(new BoxRenderable(testRenderer, { id: `nested-${i}`, height: 1 }))\n      scrollBox.add(wrapper)\n    }\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    scrollBox.scrollChildIntoView(\"nested-25\")\n    await renderOnce()\n\n    const child = scrollBox.content.findDescendantById(\"nested-25\")!\n    expect(child.y).toBeGreaterThanOrEqual(scrollBox.viewport.y)\n    expect(child.y + child.height).toBeLessThanOrEqual(scrollBox.viewport.y + scrollBox.viewport.height)\n  })\n\n  test(\"scrollChildIntoView does nothing for nonexistent child\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n    })\n\n    for (let i = 0; i < 20; i++) {\n      scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, height: 1 }))\n    }\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    scrollBox.scrollTo(5)\n    await renderOnce()\n\n    const scrollTopBefore = scrollBox.scrollTop\n    scrollBox.scrollChildIntoView(\"does-not-exist\")\n    expect(scrollBox.scrollTop).toBe(scrollTopBefore)\n  })\n\n  test(\"scrollChildIntoView handles horizontal scrolling\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 20,\n      height: 10,\n      scrollX: true,\n      contentOptions: {\n        flexDirection: \"row\",\n      },\n    })\n\n    for (let i = 0; i < 6; i++) {\n      scrollBox.add(new BoxRenderable(testRenderer, { id: `child-${i}`, width: 10, height: 2 }))\n    }\n\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    scrollBox.scrollLeft = 30\n    await renderOnce()\n\n    scrollBox.scrollChildIntoView(\"child-0\")\n    await renderOnce()\n\n    const child = scrollBox.content.findDescendantById(\"child-0\")!\n    expect(child.x).toBe(scrollBox.viewport.x)\n    expect(child.x + child.width).toBeLessThanOrEqual(scrollBox.viewport.x + scrollBox.viewport.width)\n  })\n\n  test(\"scrollChildIntoView follows nearest behavior for oversized children\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n    })\n\n    scrollBox.add(new BoxRenderable(testRenderer, { id: \"oversized\", height: 30 }))\n    testRenderer.root.add(scrollBox)\n    await renderOnce()\n\n    scrollBox.scrollChildIntoView(\"oversized\")\n    expect(scrollBox.scrollTop).toBe(0)\n\n    scrollBox.scrollTo(10)\n    await renderOnce()\n\n    scrollBox.scrollChildIntoView(\"oversized\")\n    expect(scrollBox.scrollTop).toBe(10)\n  })\n\n  // Regression test for issue #530: edge case when content fits in viewport\n  test(\"resets _hasManualScroll for stickyStart=bottom when content fits in viewport (issue #530)\", async () => {\n    const scrollBox = new ScrollBoxRenderable(testRenderer, {\n      width: 40,\n      height: 10,\n      stickyScroll: true,\n      stickyStart: \"bottom\",\n    })\n\n    testRenderer.root.add(scrollBox)\n\n    // Add content that fits in viewport (no actual scrolling needed)\n    scrollBox.add(new TextRenderable(testRenderer, { content: \"Line 0\" }))\n    scrollBox.add(new TextRenderable(testRenderer, { content: \"Line 1\" }))\n    await renderOnce()\n\n    // maxScrollTop should be 0 since content fits\n    const maxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n    expect(maxScroll).toBe(0)\n\n    // Simulate accidental scroll attempts (common with trackpads)\n    scrollBox.scrollTo(0)\n    await renderOnce()\n\n    // Even though we're at scrollTop=0, for stickyStart=\"bottom\" with maxScrollTop=0,\n    // we're effectively at both top AND bottom, so _hasManualScroll should be false\n    expect((scrollBox as any)._hasManualScroll).toBe(false)\n\n    // Add more content that causes overflow - should stay at bottom\n    for (let i = 2; i < 20; i++) {\n      scrollBox.add(new TextRenderable(testRenderer, { content: `Line ${i}` }))\n      await renderOnce()\n\n      const expectedMaxScroll = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height)\n      if (expectedMaxScroll > 0) {\n        expect(scrollBox.scrollTop).toBe(expectedMaxScroll)\n      }\n    }\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/wrap-resize-perf.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { TextBuffer } from \"../text-buffer.js\"\nimport { TextBufferView } from \"../text-buffer-view.js\"\nimport { stringToStyledText } from \"../lib/styled-text.js\"\n\n/**\n * These tests verify algorithmic complexity rather than absolute performance.\n * By comparing ratios of execution times for different input sizes, we can\n * detect O(n²) regressions regardless of the machine's speed.\n *\n * For O(n) algorithms: doubling input size should roughly double the time (ratio ~2)\n * For O(n²) algorithms: doubling input size should quadruple the time (ratio ~4)\n *\n * We use a threshold that allows for CI variance while still catching O(n²) behavior.\n * The threshold is set to catch quadratic complexity (ratio ~4) while allowing\n * linear complexity with noise (ratio ~2-3.5).\n */\ndescribe(\"Word wrap algorithmic complexity\", () => {\n  function measureBatch(fn: (width: number) => void, widths: number[], roundsPerSample: number): number {\n    const start = performance.now()\n\n    for (let round = 0; round < roundsPerSample; round++) {\n      for (const width of widths) {\n        fn(width)\n      }\n    }\n\n    return performance.now() - start\n  }\n\n  function calibrateRoundsPerSample(\n    fn: (width: number) => void,\n    widths: number[],\n    minBatchMs = 5,\n    initialRounds = 4,\n    maxRounds = 512,\n  ): number {\n    let roundsPerSample = initialRounds\n\n    while (roundsPerSample < maxRounds) {\n      const elapsed = measureBatch(fn, widths, roundsPerSample)\n      if (elapsed >= minBatchMs) {\n        return roundsPerSample\n      }\n      roundsPerSample *= 2\n    }\n\n    return roundsPerSample\n  }\n\n  function measureMedianRatio(\n    smallFn: (width: number) => void,\n    largeFn: (width: number) => void,\n    widths: number[],\n    roundsPerSample: number,\n    iterations = 9,\n  ): number {\n    const ratios: number[] = []\n\n    for (let i = 0; i < iterations; i++) {\n      let smallTime: number\n      let largeTime: number\n\n      if (i % 2 === 0) {\n        smallTime = measureBatch(smallFn, widths, roundsPerSample)\n        largeTime = measureBatch(largeFn, widths, roundsPerSample)\n      } else {\n        largeTime = measureBatch(largeFn, widths, roundsPerSample)\n        smallTime = measureBatch(smallFn, widths, roundsPerSample)\n      }\n\n      ratios.push(largeTime / smallTime)\n    }\n\n    ratios.sort((a, b) => a - b)\n    return ratios[Math.floor(ratios.length / 2)]\n  }\n\n  const COMPLEXITY_THRESHOLD = 1.75\n  const MEASURE_WIDTHS = [76, 77, 78, 79, 80, 81, 82, 83]\n\n  it(\"should have O(n) complexity for word wrap without word breaks\", () => {\n    const smallSize = 20000\n    const largeSize = 40000\n\n    const smallText = \"x\".repeat(smallSize)\n    const largeText = \"x\".repeat(largeSize)\n\n    const smallBuffer = TextBuffer.create(\"wcwidth\")\n    const largeBuffer = TextBuffer.create(\"wcwidth\")\n\n    smallBuffer.setStyledText(stringToStyledText(smallText))\n    largeBuffer.setStyledText(stringToStyledText(largeText))\n\n    const smallView = TextBufferView.create(smallBuffer)\n    const largeView = TextBufferView.create(largeBuffer)\n\n    smallView.setWrapMode(\"word\")\n    largeView.setWrapMode(\"word\")\n    smallView.setWrapWidth(80)\n    largeView.setWrapWidth(80)\n\n    for (const width of MEASURE_WIDTHS) {\n      smallView.measureForDimensions(width, 100)\n      largeView.measureForDimensions(width, 100)\n    }\n\n    const roundsPerSample = calibrateRoundsPerSample((width) => {\n      smallView.measureForDimensions(width, 100)\n    }, MEASURE_WIDTHS)\n\n    const ratio = measureMedianRatio(\n      (width) => {\n        smallView.measureForDimensions(width, 100)\n      },\n      (width) => {\n        largeView.measureForDimensions(width, 100)\n      },\n      MEASURE_WIDTHS,\n      roundsPerSample,\n    )\n\n    smallView.destroy()\n    largeView.destroy()\n    smallBuffer.destroy()\n    largeBuffer.destroy()\n\n    const inputRatio = largeSize / smallSize\n\n    expect(ratio).toBeLessThan(inputRatio * COMPLEXITY_THRESHOLD)\n  })\n\n  it(\"should have O(n) complexity for word wrap with word breaks\", () => {\n    const smallSize = 20000\n    const largeSize = 40000\n\n    const makeText = (size: number) => {\n      const words = Math.ceil(size / 11)\n      return Array(words).fill(\"xxxxxxxxxx\").join(\" \").slice(0, size)\n    }\n\n    const smallText = makeText(smallSize)\n    const largeText = makeText(largeSize)\n\n    const smallBuffer = TextBuffer.create(\"wcwidth\")\n    const largeBuffer = TextBuffer.create(\"wcwidth\")\n\n    smallBuffer.setStyledText(stringToStyledText(smallText))\n    largeBuffer.setStyledText(stringToStyledText(largeText))\n\n    const smallView = TextBufferView.create(smallBuffer)\n    const largeView = TextBufferView.create(largeBuffer)\n\n    smallView.setWrapMode(\"word\")\n    largeView.setWrapMode(\"word\")\n    smallView.setWrapWidth(80)\n    largeView.setWrapWidth(80)\n\n    // Warm up with changing widths so we measure wrap work, not cache hits.\n    for (const width of MEASURE_WIDTHS) {\n      smallView.measureForDimensions(width, 100)\n      largeView.measureForDimensions(width, 100)\n    }\n\n    const roundsPerSample = calibrateRoundsPerSample((width) => {\n      smallView.measureForDimensions(width, 100)\n    }, MEASURE_WIDTHS)\n\n    const ratio = measureMedianRatio(\n      (width) => {\n        smallView.measureForDimensions(width, 100)\n      },\n      (width) => {\n        largeView.measureForDimensions(width, 100)\n      },\n      MEASURE_WIDTHS,\n      roundsPerSample,\n    )\n\n    smallView.destroy()\n    largeView.destroy()\n    smallBuffer.destroy()\n    largeBuffer.destroy()\n\n    const inputRatio = largeSize / smallSize\n\n    expect(ratio).toBeLessThan(inputRatio * COMPLEXITY_THRESHOLD)\n  })\n\n  it(\"should have O(n) complexity for char wrap mode\", () => {\n    const smallSize = 20000\n    const largeSize = 40000\n\n    const smallText = \"x\".repeat(smallSize)\n    const largeText = \"x\".repeat(largeSize)\n\n    const smallBuffer = TextBuffer.create(\"wcwidth\")\n    const largeBuffer = TextBuffer.create(\"wcwidth\")\n\n    smallBuffer.setStyledText(stringToStyledText(smallText))\n    largeBuffer.setStyledText(stringToStyledText(largeText))\n\n    const smallView = TextBufferView.create(smallBuffer)\n    const largeView = TextBufferView.create(largeBuffer)\n\n    smallView.setWrapMode(\"char\")\n    largeView.setWrapMode(\"char\")\n    smallView.setWrapWidth(80)\n    largeView.setWrapWidth(80)\n\n    for (const width of MEASURE_WIDTHS) {\n      smallView.measureForDimensions(width, 100)\n      largeView.measureForDimensions(width, 100)\n    }\n\n    const roundsPerSample = calibrateRoundsPerSample((width) => {\n      smallView.measureForDimensions(width, 100)\n    }, MEASURE_WIDTHS)\n\n    const ratio = measureMedianRatio(\n      (width) => {\n        smallView.measureForDimensions(width, 100)\n      },\n      (width) => {\n        largeView.measureForDimensions(width, 100)\n      },\n      MEASURE_WIDTHS,\n      roundsPerSample,\n    )\n\n    smallView.destroy()\n    largeView.destroy()\n    smallBuffer.destroy()\n    largeBuffer.destroy()\n\n    const inputRatio = largeSize / smallSize\n\n    expect(ratio).toBeLessThan(inputRatio * COMPLEXITY_THRESHOLD)\n  })\n\n  // NOTE: Is flaky\n  it.skip(\"should scale linearly when wrap width changes\", () => {\n    const text = \"x\".repeat(50000)\n\n    const buffer = TextBuffer.create(\"wcwidth\")\n    buffer.setStyledText(stringToStyledText(text))\n\n    const view = TextBufferView.create(buffer)\n    view.setWrapMode(\"word\")\n\n    const widths = [60, 70, 80, 90, 100]\n    const times: number[] = []\n\n    // Warmup\n    view.setWrapWidth(50)\n    view.measureForDimensions(50, 100)\n\n    // Measure first (uncached) call for each width\n    for (const width of widths) {\n      view.setWrapWidth(width)\n      const start = performance.now()\n      view.measureForDimensions(width, 100)\n      times.push(performance.now() - start)\n    }\n\n    view.destroy()\n    buffer.destroy()\n\n    // All times should be roughly similar (within 3x of each other)\n    // since the text size is the same\n    const maxTime = Math.max(...times)\n    const minTime = Math.min(...times)\n\n    expect(maxTime / minTime).toBeLessThan(3)\n  })\n})\n"
  },
  {
    "path": "packages/core/src/tests/yoga-setters.test.ts",
    "content": "import { test, expect, beforeEach, afterEach, describe } from \"bun:test\"\nimport { Renderable, type RenderableOptions } from \"../Renderable.js\"\nimport { createTestRenderer, type TestRenderer } from \"../testing/test-renderer.js\"\nimport type { RenderContext } from \"../types.js\"\n\nclass TestRenderable extends Renderable {\n  constructor(ctx: RenderContext, options: RenderableOptions) {\n    super(ctx, options)\n  }\n}\n\nlet testRenderer: TestRenderer\n\nbeforeEach(async () => {\n  ;({ renderer: testRenderer } = await createTestRenderer({}))\n})\n\nafterEach(() => {\n  testRenderer.destroy()\n})\n\ndescribe(\"Yoga Prop Setters - flexGrow\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-grow\" })\n    expect(() => {\n      renderable.flexGrow = 1\n    }).not.toThrow()\n  })\n\n  test(\"accepts 0\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-grow-zero\" })\n    expect(() => {\n      renderable.flexGrow = 0\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-grow-null\" })\n    expect(() => {\n      renderable.flexGrow = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-grow-undefined\" })\n    expect(() => {\n      renderable.flexGrow = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - flexShrink\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-shrink\" })\n    expect(() => {\n      renderable.flexShrink = 1\n    }).not.toThrow()\n  })\n\n  test(\"accepts 0\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-shrink-zero\" })\n    expect(() => {\n      renderable.flexShrink = 0\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-shrink-null\" })\n    expect(() => {\n      renderable.flexShrink = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-shrink-undefined\" })\n    expect(() => {\n      renderable.flexShrink = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - flexDirection\", () => {\n  test(\"accepts valid string\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-direction\" })\n    expect(() => {\n      renderable.flexDirection = \"row\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts all valid values\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-direction-all\" })\n    expect(() => {\n      renderable.flexDirection = \"column\"\n      renderable.flexDirection = \"column-reverse\"\n      renderable.flexDirection = \"row\"\n      renderable.flexDirection = \"row-reverse\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-direction-null\" })\n    expect(() => {\n      renderable.flexDirection = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-direction-undefined\" })\n    expect(() => {\n      renderable.flexDirection = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - flexWrap\", () => {\n  test(\"accepts valid string\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-wrap\" })\n    expect(() => {\n      renderable.flexWrap = \"wrap\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts all valid values\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-wrap-all\" })\n    expect(() => {\n      renderable.flexWrap = \"no-wrap\"\n      renderable.flexWrap = \"wrap\"\n      renderable.flexWrap = \"wrap-reverse\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-wrap-null\" })\n    expect(() => {\n      renderable.flexWrap = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-wrap-undefined\" })\n    expect(() => {\n      renderable.flexWrap = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - alignItems\", () => {\n  test(\"accepts valid string\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-align-items\" })\n    expect(() => {\n      renderable.alignItems = \"center\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts all valid values\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-align-items-all\" })\n    expect(() => {\n      renderable.alignItems = \"auto\"\n      renderable.alignItems = \"flex-start\"\n      renderable.alignItems = \"center\"\n      renderable.alignItems = \"flex-end\"\n      renderable.alignItems = \"stretch\"\n      renderable.alignItems = \"baseline\"\n      renderable.alignItems = \"space-between\"\n      renderable.alignItems = \"space-around\"\n      renderable.alignItems = \"space-evenly\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-align-items-null\" })\n    expect(() => {\n      renderable.alignItems = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-align-items-undefined\" })\n    expect(() => {\n      renderable.alignItems = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - justifyContent\", () => {\n  test(\"accepts valid string\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-justify-content\" })\n    expect(() => {\n      renderable.justifyContent = \"center\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts all valid values\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-justify-content-all\" })\n    expect(() => {\n      renderable.justifyContent = \"flex-start\"\n      renderable.justifyContent = \"center\"\n      renderable.justifyContent = \"flex-end\"\n      renderable.justifyContent = \"space-between\"\n      renderable.justifyContent = \"space-around\"\n      renderable.justifyContent = \"space-evenly\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-justify-content-null\" })\n    expect(() => {\n      renderable.justifyContent = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-justify-content-undefined\" })\n    expect(() => {\n      renderable.justifyContent = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - alignSelf\", () => {\n  test(\"accepts valid string\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-align-self\" })\n    expect(() => {\n      renderable.alignSelf = \"center\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts all valid values\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-align-self-all\" })\n    expect(() => {\n      renderable.alignSelf = \"auto\"\n      renderable.alignSelf = \"flex-start\"\n      renderable.alignSelf = \"center\"\n      renderable.alignSelf = \"flex-end\"\n      renderable.alignSelf = \"stretch\"\n      renderable.alignSelf = \"baseline\"\n      renderable.alignSelf = \"space-between\"\n      renderable.alignSelf = \"space-around\"\n      renderable.alignSelf = \"space-evenly\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-align-self-null\" })\n    expect(() => {\n      renderable.alignSelf = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-align-self-undefined\" })\n    expect(() => {\n      renderable.alignSelf = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - overflow\", () => {\n  test(\"accepts valid string\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-overflow\" })\n    expect(() => {\n      renderable.overflow = \"hidden\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts all valid values\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-overflow-all\" })\n    expect(() => {\n      renderable.overflow = \"visible\"\n      renderable.overflow = \"hidden\"\n      renderable.overflow = \"scroll\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-overflow-null\" })\n    expect(() => {\n      renderable.overflow = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-overflow-undefined\" })\n    expect(() => {\n      renderable.overflow = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - position\", () => {\n  test(\"accepts valid string\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-position\" })\n    expect(() => {\n      renderable.position = \"absolute\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts all valid values\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-position-all\" })\n    expect(() => {\n      renderable.position = \"static\"\n      renderable.position = \"relative\"\n      renderable.position = \"absolute\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-position-null\" })\n    expect(() => {\n      renderable.position = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-position-undefined\" })\n    expect(() => {\n      renderable.position = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - flexBasis\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-basis\" })\n    expect(() => {\n      renderable.flexBasis = 100\n    }).not.toThrow()\n  })\n\n  test(\"accepts auto\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-basis-auto\" })\n    expect(() => {\n      renderable.flexBasis = \"auto\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-basis-null\" })\n    expect(() => {\n      renderable.flexBasis = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-flex-basis-undefined\" })\n    expect(() => {\n      renderable.flexBasis = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - minWidth\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-min-width\" })\n    expect(() => {\n      renderable.minWidth = 100\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-min-width-percent\" })\n    expect(() => {\n      renderable.minWidth = \"50%\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-min-width-null\" })\n    expect(() => {\n      renderable.minWidth = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-min-width-undefined\" })\n    expect(() => {\n      renderable.minWidth = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - maxWidth\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-max-width\" })\n    expect(() => {\n      renderable.maxWidth = 100\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-max-width-percent\" })\n    expect(() => {\n      renderable.maxWidth = \"50%\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-max-width-null\" })\n    expect(() => {\n      renderable.maxWidth = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-max-width-undefined\" })\n    expect(() => {\n      renderable.maxWidth = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - minHeight\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-min-height\" })\n    expect(() => {\n      renderable.minHeight = 100\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-min-height-percent\" })\n    expect(() => {\n      renderable.minHeight = \"50%\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-min-height-null\" })\n    expect(() => {\n      renderable.minHeight = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-min-height-undefined\" })\n    expect(() => {\n      renderable.minHeight = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - maxHeight\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-max-height\" })\n    expect(() => {\n      renderable.maxHeight = 100\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-max-height-percent\" })\n    expect(() => {\n      renderable.maxHeight = \"50%\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-max-height-null\" })\n    expect(() => {\n      renderable.maxHeight = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-max-height-undefined\" })\n    expect(() => {\n      renderable.maxHeight = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - margin\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin\" })\n    expect(() => {\n      renderable.margin = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts auto\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-auto\" })\n    expect(() => {\n      renderable.margin = \"auto\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-percent\" })\n    expect(() => {\n      renderable.margin = \"10%\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-null\" })\n    expect(() => {\n      renderable.margin = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-undefined\" })\n    expect(() => {\n      renderable.margin = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - marginX\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-x\" })\n    expect(() => {\n      renderable.marginX = 10\n    }).not.toThrow()\n  })\n  test(\"accepts auto\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-x-auto\" })\n    expect(() => {\n      renderable.marginX = \"auto\"\n    }).not.toThrow()\n  })\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-x-percent\" })\n    expect(() => {\n      renderable.marginX = \"10%\"\n    }).not.toThrow()\n  })\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-x-null\" })\n    expect(() => {\n      renderable.marginX = null\n    }).not.toThrow()\n  })\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-x-undefined\" })\n    expect(() => {\n      renderable.marginX = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - marginY\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-y\" })\n    expect(() => {\n      renderable.marginY = 10\n    }).not.toThrow()\n  })\n  test(\"accepts auto\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-y-auto\" })\n    expect(() => {\n      renderable.marginY = \"auto\"\n    }).not.toThrow()\n  })\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-y-percent\" })\n    expect(() => {\n      renderable.marginY = \"10%\"\n    }).not.toThrow()\n  })\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-y-null\" })\n    expect(() => {\n      renderable.marginY = null\n    }).not.toThrow()\n  })\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-y-undefined\" })\n    expect(() => {\n      renderable.marginY = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - marginTop\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-top\" })\n    expect(() => {\n      renderable.marginTop = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-top-null\" })\n    expect(() => {\n      renderable.marginTop = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-top-undefined\" })\n    expect(() => {\n      renderable.marginTop = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - marginRight\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-right\" })\n    expect(() => {\n      renderable.marginRight = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-right-null\" })\n    expect(() => {\n      renderable.marginRight = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-right-undefined\" })\n    expect(() => {\n      renderable.marginRight = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - marginBottom\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-bottom\" })\n    expect(() => {\n      renderable.marginBottom = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-bottom-null\" })\n    expect(() => {\n      renderable.marginBottom = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-bottom-undefined\" })\n    expect(() => {\n      renderable.marginBottom = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - marginLeft\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-left\" })\n    expect(() => {\n      renderable.marginLeft = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-left-null\" })\n    expect(() => {\n      renderable.marginLeft = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-margin-left-undefined\" })\n    expect(() => {\n      renderable.marginLeft = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - padding\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding\" })\n    expect(() => {\n      renderable.padding = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-percent\" })\n    expect(() => {\n      renderable.padding = \"10%\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-null\" })\n    expect(() => {\n      renderable.padding = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-undefined\" })\n    expect(() => {\n      renderable.padding = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - paddingX\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-x\" })\n    expect(() => {\n      renderable.paddingX = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-x-percent\" })\n    expect(() => {\n      renderable.paddingX = \"10%\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-x-null\" })\n    expect(() => {\n      renderable.paddingX = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-x-undefined\" })\n    expect(() => {\n      renderable.paddingX = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - paddingY\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-y\" })\n    expect(() => {\n      renderable.paddingY = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-y-percent\" })\n    expect(() => {\n      renderable.paddingY = \"10%\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-y-null\" })\n    expect(() => {\n      renderable.paddingY = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-y-undefined\" })\n    expect(() => {\n      renderable.paddingY = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - paddingTop\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-top\" })\n    expect(() => {\n      renderable.paddingTop = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-top-null\" })\n    expect(() => {\n      renderable.paddingTop = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-top-undefined\" })\n    expect(() => {\n      renderable.paddingTop = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - paddingRight\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-right\" })\n    expect(() => {\n      renderable.paddingRight = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-right-null\" })\n    expect(() => {\n      renderable.paddingRight = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-right-undefined\" })\n    expect(() => {\n      renderable.paddingRight = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - paddingBottom\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-bottom\" })\n    expect(() => {\n      renderable.paddingBottom = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-bottom-null\" })\n    expect(() => {\n      renderable.paddingBottom = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-bottom-undefined\" })\n    expect(() => {\n      renderable.paddingBottom = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - paddingLeft\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-left\" })\n    expect(() => {\n      renderable.paddingLeft = 10\n    }).not.toThrow()\n  })\n\n  test(\"accepts null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-left-null\" })\n    expect(() => {\n      renderable.paddingLeft = null\n    }).not.toThrow()\n  })\n\n  test(\"accepts undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-padding-left-undefined\" })\n    expect(() => {\n      renderable.paddingLeft = undefined\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - width\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-width\" })\n    expect(() => {\n      renderable.width = 100\n    }).not.toThrow()\n  })\n\n  test(\"accepts auto\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-width-auto\" })\n    expect(() => {\n      renderable.width = \"auto\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-width-percent\" })\n    expect(() => {\n      renderable.width = \"50%\"\n    }).not.toThrow()\n  })\n\n  test(\"handles null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-width-null\" })\n    expect(() => {\n      renderable.width = null as any\n    }).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-width-undefined\" })\n    expect(() => {\n      renderable.width = undefined as any\n    }).not.toThrow()\n  })\n})\n\ndescribe(\"Yoga Prop Setters - height\", () => {\n  test(\"accepts valid number\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-height\" })\n    expect(() => {\n      renderable.height = 100\n    }).not.toThrow()\n  })\n\n  test(\"accepts auto\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-height-auto\" })\n    expect(() => {\n      renderable.height = \"auto\"\n    }).not.toThrow()\n  })\n\n  test(\"accepts percentage\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-height-percent\" })\n    expect(() => {\n      renderable.height = \"50%\"\n    }).not.toThrow()\n  })\n\n  test(\"handles null\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-height-null\" })\n    expect(() => {\n      renderable.height = null as any\n    }).not.toThrow()\n  })\n\n  test(\"handles undefined\", () => {\n    const renderable = new TestRenderable(testRenderer, { id: \"test-height-undefined\" })\n    expect(() => {\n      renderable.height = undefined as any\n    }).not.toThrow()\n  })\n})\n"
  },
  {
    "path": "packages/core/src/text-buffer-view.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { TextBuffer } from \"./text-buffer.js\"\nimport { TextBufferView } from \"./text-buffer-view.js\"\nimport { StyledText, stringToStyledText } from \"./lib/styled-text.js\"\nimport { RGBA } from \"./lib/RGBA.js\"\n\ndescribe(\"TextBufferView\", () => {\n  let buffer: TextBuffer\n  let view: TextBufferView\n\n  beforeEach(() => {\n    buffer = TextBuffer.create(\"wcwidth\")\n    view = TextBufferView.create(buffer)\n  })\n\n  afterEach(() => {\n    view.destroy()\n    buffer.destroy()\n  })\n\n  describe(\"lineInfo getter with wrapping\", () => {\n    it(\"should return line info for empty buffer\", () => {\n      const emptyText = stringToStyledText(\"\")\n      buffer.setStyledText(emptyText)\n\n      const lineInfo = view.lineInfo\n      expect(lineInfo.lineStartCols).toEqual([0])\n      expect(lineInfo.lineWidthCols).toEqual([0])\n    })\n\n    it(\"should return single line info for simple text without newlines\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      const lineInfo = view.lineInfo\n      expect(lineInfo.lineStartCols).toEqual([0])\n      expect(lineInfo.lineWidthCols.length).toBe(1)\n      expect(lineInfo.lineWidthCols[0]).toBeGreaterThan(0)\n    })\n\n    it(\"should handle single newline correctly\", () => {\n      const styledText = stringToStyledText(\"Hello\\nWorld\")\n      buffer.setStyledText(styledText)\n\n      const lineInfo = view.lineInfo\n      // With newline-aware offsets: \"Hello\" (0-4) + newline (5) + \"World\" starts at 6\n      expect(lineInfo.lineStartCols).toEqual([0, 6])\n      expect(lineInfo.lineWidthCols.length).toBe(2)\n      expect(lineInfo.lineWidthCols[0]).toBeGreaterThan(0)\n      expect(lineInfo.lineWidthCols[1]).toBeGreaterThan(0)\n    })\n\n    it(\"should return virtual line info when text wrapping is enabled\", () => {\n      const longText = \"This is a very long text that should wrap when the text wrapping is enabled.\"\n      const styledText = stringToStyledText(longText)\n      buffer.setStyledText(styledText)\n\n      const unwrappedInfo = view.lineInfo\n      expect(unwrappedInfo.lineStartCols).toEqual([0])\n      expect(unwrappedInfo.lineWidthCols.length).toBe(1)\n      expect(unwrappedInfo.lineWidthCols[0]).toBe(76)\n\n      view.setWrapMode(\"char\") // Enable wrapping\n      view.setWrapWidth(20)\n\n      const wrappedInfo = view.lineInfo\n\n      expect(wrappedInfo.lineStartCols.length).toBeGreaterThan(1)\n      expect(wrappedInfo.lineWidthCols.length).toBeGreaterThan(1)\n\n      for (const width of wrappedInfo.lineWidthCols) {\n        expect(width).toBeLessThanOrEqual(20)\n      }\n\n      for (let i = 1; i < wrappedInfo.lineStartCols.length; i++) {\n        expect(wrappedInfo.lineStartCols[i]).toBeGreaterThan(wrappedInfo.lineStartCols[i - 1])\n      }\n    })\n\n    it(\"should return correct lineInfo for word wrapping\", () => {\n      const text = \"Hello world this is a test\"\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"word\")\n      view.setWrapWidth(12)\n\n      const lineInfo = view.lineInfo\n\n      expect(lineInfo.lineStartCols.length).toBeGreaterThan(1)\n\n      for (const width of lineInfo.lineWidthCols) {\n        expect(width).toBeLessThanOrEqual(12)\n      }\n    })\n\n    it(\"should return correct lineInfo for char wrapping\", () => {\n      const text = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n      view.setWrapWidth(10)\n\n      const lineInfo = view.lineInfo\n\n      expect(lineInfo.lineStartCols).toEqual([0, 10, 20])\n      expect(lineInfo.lineWidthCols).toEqual([10, 10, 6])\n    })\n\n    it(\"should update lineInfo when wrap width changes\", () => {\n      const text = \"The quick brown fox jumps over the lazy dog\"\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\") // Enable wrapping\n      view.setWrapWidth(15)\n\n      const lineInfo1 = view.lineInfo\n      const lineCount1 = lineInfo1.lineStartCols.length\n\n      view.setWrapWidth(30)\n\n      const lineInfo2 = view.lineInfo\n      const lineCount2 = lineInfo2.lineStartCols.length\n\n      expect(lineCount2).toBeLessThan(lineCount1)\n    })\n\n    it(\"should return original lineInfo when wrap is disabled\", () => {\n      const text = \"Line 1\\nLine 2\\nLine 3\"\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      const originalInfo = view.lineInfo\n      // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 (7-12) + newline (13) + Line 2 (14-19)\n      expect(originalInfo.lineStartCols).toEqual([0, 7, 14])\n\n      view.setWrapMode(\"char\") // Enable wrapping\n      view.setWrapWidth(5)\n\n      const wrappedInfo = view.lineInfo\n      expect(wrappedInfo.lineStartCols.length).toBeGreaterThan(3)\n\n      view.setWrapMode(\"none\") // Disable wrapping\n      view.setWrapWidth(null)\n\n      const unwrappedInfo = view.lineInfo\n      expect(unwrappedInfo.lineStartCols).toEqual([0, 7, 14])\n    })\n\n    it(\"should return extended wrap info\", () => {\n      const text = \"Line 1 content\\nLine 2\"\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n      view.setWrapWidth(10)\n\n      // Line 1 content (14 chars) wraps into two lines:\n      // \"Line 1 con\" (10)\n      // \"tent\" (4)\n      // Line 2 (6 chars) fits on one line\n\n      const info = view.lineInfo\n\n      expect(info.lineSources.length).toBe(3)\n      expect(info.lineWraps.length).toBe(3)\n\n      // First visual line: source line 0, wrap 0\n      expect(info.lineSources[0]).toBe(0)\n      expect(info.lineWraps[0]).toBe(0)\n\n      // Second visual line: source line 0, wrap 1 (continuation)\n      expect(info.lineSources[1]).toBe(0)\n      expect(info.lineWraps[1]).toBe(1)\n\n      // Third visual line: source line 1, wrap 0\n      expect(info.lineSources[2]).toBe(1)\n      expect(info.lineWraps[2]).toBe(0)\n    })\n  })\n\n  describe(\"getSelectedText\", () => {\n    it(\"should return empty string when no selection\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      const selectedText = view.getSelectedText()\n      expect(selectedText).toBe(\"\")\n    })\n\n    it(\"should return selected text for simple selection\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      view.setSelection(6, 11)\n      const selectedText = view.getSelectedText()\n      expect(selectedText).toBe(\"World\")\n    })\n\n    it(\"should return selected text with newlines\", () => {\n      const styledText = stringToStyledText(\"Line 1\\nLine 2\\nLine 3\")\n      buffer.setStyledText(styledText)\n\n      // Rope offsets: \"Line 1\" (0-5) + newline (6) + \"Line 2\" (7-12) + newline (13) + \"Line 3\" (14-19)\n      // Selection [0, 9) = \"Line 1\" (0-5) + newline (6) + \"Li\" (7-8) = 9 chars\n      view.setSelection(0, 9)\n      const selectedText = view.getSelectedText()\n      expect(selectedText).toBe(\"Line 1\\nLi\")\n    })\n\n    it(\"should handle Unicode characters in selection\", () => {\n      const styledText = stringToStyledText(\"Hello 世界 🌟\")\n      buffer.setStyledText(styledText)\n\n      view.setSelection(6, 12)\n      const selectedText = view.getSelectedText()\n      expect(selectedText).toBe(\"世界 🌟\")\n    })\n\n    it(\"should handle selection reset\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      view.setSelection(6, 11)\n      expect(view.getSelectedText()).toBe(\"World\")\n\n      view.resetSelection()\n      expect(view.getSelectedText()).toBe(\"\")\n    })\n  })\n\n  describe(\"selection state\", () => {\n    it(\"should track selection state\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      expect(view.hasSelection()).toBe(false)\n\n      view.setSelection(0, 5)\n      expect(view.hasSelection()).toBe(true)\n\n      const selection = view.getSelection()\n      expect(selection).toEqual({ start: 0, end: 5 })\n\n      view.resetSelection()\n      expect(view.hasSelection()).toBe(false)\n    })\n\n    it(\"should update selection end position\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      view.setSelection(0, 5)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n\n      view.updateSelection(11)\n      expect(view.getSelectedText()).toBe(\"Hello World\")\n\n      const selection = view.getSelection()\n      expect(selection).toEqual({ start: 0, end: 11 })\n    })\n\n    it(\"should shrink selection with updateSelection\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      view.setSelection(0, 11)\n      expect(view.getSelectedText()).toBe(\"Hello World\")\n\n      view.updateSelection(5)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n\n      const selection = view.getSelection()\n      expect(selection).toEqual({ start: 0, end: 5 })\n    })\n\n    it(\"should do nothing when updateSelection called with no selection\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      expect(view.hasSelection()).toBe(false)\n\n      view.updateSelection(5)\n      expect(view.hasSelection()).toBe(false)\n      expect(view.getSelectedText()).toBe(\"\")\n    })\n\n    it(\"should update local selection focus position\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      const changed1 = view.setLocalSelection(0, 0, 5, 0)\n      expect(changed1).toBe(true)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n\n      const changed2 = view.updateLocalSelection(0, 0, 11, 0)\n      expect(changed2).toBe(true)\n      expect(view.getSelectedText()).toBe(\"Hello World\")\n    })\n\n    it(\"should update local selection across lines\", () => {\n      const styledText = stringToStyledText(\"Line 1\\nLine 2\\nLine 3\")\n      buffer.setStyledText(styledText)\n\n      view.setLocalSelection(2, 0, 2, 0)\n\n      const changed = view.updateLocalSelection(2, 0, 4, 1)\n      expect(changed).toBe(true)\n\n      const selectedText = view.getSelectedText()\n      expect(selectedText).toContain(\"ne 1\")\n      expect(selectedText).toContain(\"Line\")\n    })\n\n    it(\"should fallback to setLocalSelection when updateLocalSelection called with no existing anchor\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      const changed = view.updateLocalSelection(0, 0, 5, 0)\n      expect(changed).toBe(true)\n      expect(view.hasSelection()).toBe(true)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n    })\n\n    it(\"should preserve anchor when updating local selection\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      view.setLocalSelection(0, 0, 5, 0)\n      expect(view.getSelectedText()).toBe(\"Hello\")\n\n      view.updateLocalSelection(0, 0, 6, 0)\n      expect(view.getSelectedText()).toBe(\"Hello \")\n\n      view.updateLocalSelection(0, 0, 11, 0)\n      expect(view.getSelectedText()).toBe(\"Hello World\")\n\n      view.updateLocalSelection(0, 0, 3, 0)\n      expect(view.getSelectedText()).toBe(\"Hel\")\n    })\n\n    it(\"should handle backward selection with updateLocalSelection\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      view.setLocalSelection(11, 0, 11, 0)\n\n      const changed = view.updateLocalSelection(11, 0, 6, 0)\n      expect(changed).toBe(true)\n      expect(view.getSelectedText()).toBe(\"World\")\n    })\n  })\n\n  describe(\"getPlainText\", () => {\n    it(\"should return empty string for empty buffer\", () => {\n      const emptyText = stringToStyledText(\"\")\n      buffer.setStyledText(emptyText)\n\n      const plainText = view.getPlainText()\n      expect(plainText).toBe(\"\")\n    })\n\n    it(\"should return plain text without styling\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      const plainText = view.getPlainText()\n      expect(plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should handle text with newlines\", () => {\n      const styledText = stringToStyledText(\"Line 1\\nLine 2\\nLine 3\")\n      buffer.setStyledText(styledText)\n\n      const plainText = view.getPlainText()\n      expect(plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n    })\n  })\n\n  describe(\"undo/redo with line info\", () => {\n    it(\"should update lineInfo correctly after undo\", () => {\n      // This test verifies that marker cache is invalidated after undo\n      const styledText = stringToStyledText(\"Line 1 content\\nLine 2\")\n      buffer.setStyledText(styledText)\n\n      const lineInfoBefore = view.lineInfo\n      expect(lineInfoBefore.lineStartCols).toEqual([0, 15])\n      expect(lineInfoBefore.lineWidthCols[0]).toBe(14)\n      expect(lineInfoBefore.lineWidthCols[1]).toBe(6)\n\n      // Modify the buffer (this would normally go through EditBuffer with undo tracking)\n      // For this test, we'll just verify the view updates correctly\n      const modifiedText = stringToStyledText(\"Line 1 \\nLine 2\")\n      buffer.setStyledText(modifiedText)\n\n      const lineInfoAfterModify = view.lineInfo\n      expect(lineInfoAfterModify.lineStartCols).toEqual([0, 8])\n      expect(lineInfoAfterModify.lineWidthCols[0]).toBe(7)\n\n      // Restore original (simulating undo)\n      buffer.setStyledText(styledText)\n\n      const lineInfoAfterRestore = view.lineInfo\n      expect(lineInfoAfterRestore.lineStartCols).toEqual([0, 15])\n      expect(lineInfoAfterRestore.lineWidthCols[0]).toBe(14)\n    })\n\n    it(\"should handle line info correctly through multiple undo/redo cycles\", () => {\n      const text1 = stringToStyledText(\"Short\\nLine 2\")\n      const text2 = stringToStyledText(\"This is a longer line\\nLine 2\")\n      const text3 = stringToStyledText(\"X\\nLine 2\")\n\n      buffer.setStyledText(text1)\n      const info1 = view.lineInfo\n      expect(info1.lineWidthCols[0]).toBe(5)\n\n      buffer.setStyledText(text2)\n      const info2 = view.lineInfo\n      expect(info2.lineWidthCols[0]).toBe(21)\n\n      buffer.setStyledText(text3)\n      const info3 = view.lineInfo\n      expect(info3.lineWidthCols[0]).toBe(1)\n\n      // Go back to text2 (simulating undo)\n      buffer.setStyledText(text2)\n      const info2Again = view.lineInfo\n      expect(info2Again.lineWidthCols[0]).toBe(21)\n\n      // Go back to text1 (simulating another undo)\n      buffer.setStyledText(text1)\n      const info1Again = view.lineInfo\n      expect(info1Again.lineWidthCols[0]).toBe(5)\n\n      // Forward to text2 (simulating redo)\n      buffer.setStyledText(text2)\n      const info2Redo = view.lineInfo\n      expect(info2Redo.lineWidthCols[0]).toBe(21)\n    })\n\n    it(\"should correctly track line starts after undo with multiline text\", () => {\n      const original = stringToStyledText(\"Line 1 content\\nLine 2 content\\nLine 3\")\n      const modified = stringToStyledText(\"Line 1 \\nLine 2 content\\nLine 3\")\n\n      buffer.setStyledText(original)\n      const originalInfo = view.lineInfo\n      expect(originalInfo.lineStartCols).toEqual([0, 15, 30])\n\n      buffer.setStyledText(modified)\n      const modifiedInfo = view.lineInfo\n      expect(modifiedInfo.lineStartCols).toEqual([0, 8, 23])\n\n      // Restore (undo)\n      buffer.setStyledText(original)\n      const restoredInfo = view.lineInfo\n      expect(restoredInfo.lineStartCols).toEqual([0, 15, 30])\n    })\n  })\n\n  describe(\"wrapped view offset stability\", () => {\n    it(\"should return line info for empty buffer\", () => {\n      const emptyText = stringToStyledText(\"\")\n      buffer.setStyledText(emptyText)\n\n      const lineInfo = view.lineInfo\n      expect(lineInfo.lineStartCols).toEqual([0])\n      expect(lineInfo.lineWidthCols).toEqual([0])\n    })\n\n    it(\"should maintain stable char offsets with wide characters\", () => {\n      const text = \"A世B界C\" // A(1) 世(2) B(1) 界(2) C(1) = 7 total width\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n      view.setWrapWidth(4)\n\n      const lineInfo = view.lineInfo\n      // Should wrap at display width boundaries\n      expect(lineInfo.lineStartCols[0]).toBe(0)\n      expect(lineInfo.lineStartCols.length).toBeGreaterThan(1)\n\n      // Each line should respect wrap width in display columns\n      for (const width of lineInfo.lineWidthCols) {\n        expect(width).toBeLessThanOrEqual(4)\n      }\n    })\n\n    it(\"should maintain stable selection with wrapped wide characters\", () => {\n      const text = \"世界世界世界\" // 6 CJK characters = 12 display width\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n      view.setWrapWidth(6)\n\n      // Select first 3 CJK characters (6 display width)\n      view.setSelection(0, 6)\n      const selected = view.getSelectedText()\n      expect(selected).toBe(\"世界世\")\n    })\n\n    it(\"should handle tabs correctly in wrapped view\", () => {\n      const text = \"A\\tB\\tC\"\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n      view.setWrapWidth(10)\n\n      const lineInfo = view.lineInfo\n      // Tabs expand to display width, offsets should account for this\n      expect(lineInfo.lineStartCols.length).toBeGreaterThanOrEqual(1)\n    })\n\n    it(\"should handle emoji in wrapped view\", () => {\n      const text = \"🌟🌟🌟🌟🌟\" // 5 emoji = 10 display width (assuming 2 each)\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n      view.setWrapWidth(6)\n\n      const lineInfo = view.lineInfo\n      expect(lineInfo.lineStartCols.length).toBeGreaterThan(1)\n\n      // Each wrapped line should respect display width limits\n      for (const width of lineInfo.lineWidthCols) {\n        expect(width).toBeLessThanOrEqual(6)\n      }\n    })\n\n    it(\"should maintain selection across wrapped lines\", () => {\n      const text = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n      const styledText = stringToStyledText(text)\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n      view.setWrapWidth(10)\n\n      // Select across wrap boundary: chars 8-12 (IJK)\n      view.setSelection(8, 13)\n      const selected = view.getSelectedText()\n      expect(selected).toBe(\"IJKLM\")\n    })\n  })\n\n  describe(\"measureForDimensions\", () => {\n    it(\"should measure without modifying cache\", () => {\n      const styledText = stringToStyledText(\"ABCDEFGHIJKLMNOPQRST\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n      view.setWrapWidth(100) // Large width\n\n      // Measure with different width\n      const measureResult = view.measureForDimensions(10, 10)\n      expect(measureResult).not.toBeNull()\n      expect(measureResult!.lineCount).toBe(2)\n      expect(measureResult!.widthColsMax).toBe(10)\n\n      // Verify cache wasn't modified (should be 1 line with wrap width 100)\n      const lineInfo = view.lineInfo\n      expect(lineInfo.lineStartCols.length).toBe(1)\n    })\n\n    it(\"should measure char wrap correctly\", () => {\n      const styledText = stringToStyledText(\"ABCDEFGHIJKLMNOPQRST\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n\n      // Test different widths\n      const result1 = view.measureForDimensions(10, 10)\n      expect(result1).not.toBeNull()\n      expect(result1!.lineCount).toBe(2)\n      expect(result1!.widthColsMax).toBe(10)\n\n      const result2 = view.measureForDimensions(5, 10)\n      expect(result2).not.toBeNull()\n      expect(result2!.lineCount).toBe(4)\n      expect(result2!.widthColsMax).toBe(5)\n\n      const result3 = view.measureForDimensions(20, 10)\n      expect(result3).not.toBeNull()\n      expect(result3!.lineCount).toBe(1)\n      expect(result3!.widthColsMax).toBe(20)\n    })\n\n    it(\"should handle no wrap mode\", () => {\n      const styledText = stringToStyledText(\"Hello\\nWorld\\nTest\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"none\")\n\n      const result = view.measureForDimensions(3, 10)\n      expect(result).not.toBeNull()\n      expect(result!.lineCount).toBe(3)\n      expect(result!.widthColsMax).toBeGreaterThanOrEqual(4)\n    })\n\n    it(\"should handle word wrap\", () => {\n      const styledText = stringToStyledText(\"Hello wonderful world\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"word\")\n\n      const result = view.measureForDimensions(10, 10)\n      expect(result).not.toBeNull()\n      expect(result!.lineCount).toBeGreaterThanOrEqual(2)\n      expect(result!.widthColsMax).toBeLessThanOrEqual(10)\n    })\n\n    it(\"should handle empty buffer\", () => {\n      const styledText = stringToStyledText(\"\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n\n      const result = view.measureForDimensions(10, 10)\n      expect(result).not.toBeNull()\n      expect(result!.lineCount).toBe(1)\n      expect(result!.widthColsMax).toBe(0)\n    })\n\n    it(\"should handle multiple lines with wrapping\", () => {\n      const styledText = stringToStyledText(\"Short\\nAVeryLongLineHere\\nMedium\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n\n      const result = view.measureForDimensions(10, 10)\n      expect(result).not.toBeNull()\n      // \"Short\" (1), \"AVeryLongLineHere\" (2), \"Medium\" (1) = 4 lines\n      expect(result!.lineCount).toBe(4)\n      expect(result!.widthColsMax).toBe(10)\n    })\n\n    it(\"should cache measure results for same width\", () => {\n      const styledText = stringToStyledText(\"ABCDEFGHIJKLMNOPQRST\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"char\")\n\n      // First call - cache miss\n      const result1 = view.measureForDimensions(10, 10)\n      expect(result1).not.toBeNull()\n      expect(result1!.lineCount).toBe(2)\n\n      // Second call with same width - should return cached result\n      const result2 = view.measureForDimensions(10, 10)\n      expect(result2).not.toBeNull()\n      expect(result2!.lineCount).toBe(2)\n      expect(result2!.widthColsMax).toBe(result1!.widthColsMax)\n    })\n\n    it(\"should invalidate cache when content changes\", () => {\n      const styledText1 = stringToStyledText(\"ABCDEFGHIJ\")\n      buffer.setStyledText(styledText1)\n\n      view.setWrapMode(\"char\")\n\n      // Measure with width 5 - should be 2 lines\n      const result1 = view.measureForDimensions(5, 10)\n      expect(result1!.lineCount).toBe(2)\n\n      // Change content to be longer\n      const styledText2 = stringToStyledText(\"ABCDEFGHIJKLMNOPQRST\")\n      buffer.setStyledText(styledText2)\n\n      // Same width should now return different result\n      const result2 = view.measureForDimensions(5, 10)\n      expect(result2!.lineCount).toBe(4)\n    })\n\n    it(\"should invalidate cache when wrap mode changes\", () => {\n      const styledText = stringToStyledText(\"Hello world test string here\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"word\")\n      const resultWord = view.measureForDimensions(10, 10)\n\n      view.setWrapMode(\"char\")\n      const resultChar = view.measureForDimensions(10, 10)\n\n      // Word and char wrap should produce different results\n      expect(resultWord!.lineCount).not.toBe(resultChar!.lineCount)\n    })\n\n    it(\"should handle width 0 for intrinsic measurement\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      view.setWrapMode(\"word\")\n\n      // Width 0 means get intrinsic width (no wrapping)\n      const result = view.measureForDimensions(0, 10)\n      expect(result).not.toBeNull()\n      expect(result!.lineCount).toBe(1)\n      expect(result!.widthColsMax).toBe(11) // \"Hello World\" = 11 chars\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/text-buffer-view.ts",
    "content": "import { RGBA } from \"./lib/RGBA.js\"\nimport { resolveRenderLib, type LineInfo, type RenderLib } from \"./zig.js\"\nimport { type Pointer } from \"bun:ffi\"\nimport type { TextBuffer } from \"./text-buffer.js\"\n\nexport class TextBufferView {\n  private lib: RenderLib\n  private viewPtr: Pointer\n  private textBuffer: TextBuffer\n  private _destroyed: boolean = false\n\n  constructor(lib: RenderLib, ptr: Pointer, textBuffer: TextBuffer) {\n    this.lib = lib\n    this.viewPtr = ptr\n    this.textBuffer = textBuffer\n  }\n\n  static create(textBuffer: TextBuffer): TextBufferView {\n    const lib = resolveRenderLib()\n    const viewPtr = lib.createTextBufferView(textBuffer.ptr)\n    return new TextBufferView(lib, viewPtr, textBuffer)\n  }\n\n  // Fail loud and clear\n  private guard(): void {\n    if (this._destroyed) throw new Error(\"TextBufferView is destroyed\")\n  }\n\n  public get ptr(): Pointer {\n    this.guard()\n    return this.viewPtr\n  }\n\n  public setSelection(start: number, end: number, bgColor?: RGBA, fgColor?: RGBA): void {\n    this.guard()\n    this.lib.textBufferViewSetSelection(this.viewPtr, start, end, bgColor || null, fgColor || null)\n  }\n\n  public updateSelection(end: number, bgColor?: RGBA, fgColor?: RGBA): void {\n    this.guard()\n    this.lib.textBufferViewUpdateSelection(this.viewPtr, end, bgColor || null, fgColor || null)\n  }\n\n  public resetSelection(): void {\n    this.guard()\n    this.lib.textBufferViewResetSelection(this.viewPtr)\n  }\n\n  public getSelection(): { start: number; end: number } | null {\n    this.guard()\n    return this.lib.textBufferViewGetSelection(this.viewPtr)\n  }\n\n  public hasSelection(): boolean {\n    this.guard()\n    return this.getSelection() !== null\n  }\n\n  public setLocalSelection(\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor?: RGBA,\n    fgColor?: RGBA,\n  ): boolean {\n    this.guard()\n    return this.lib.textBufferViewSetLocalSelection(\n      this.viewPtr,\n      anchorX,\n      anchorY,\n      focusX,\n      focusY,\n      bgColor || null,\n      fgColor || null,\n    )\n  }\n\n  public updateLocalSelection(\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor?: RGBA,\n    fgColor?: RGBA,\n  ): boolean {\n    this.guard()\n    return this.lib.textBufferViewUpdateLocalSelection(\n      this.viewPtr,\n      anchorX,\n      anchorY,\n      focusX,\n      focusY,\n      bgColor || null,\n      fgColor || null,\n    )\n  }\n\n  public resetLocalSelection(): void {\n    this.guard()\n    this.lib.textBufferViewResetLocalSelection(this.viewPtr)\n  }\n\n  public setWrapWidth(width: number | null): void {\n    this.guard()\n    this.lib.textBufferViewSetWrapWidth(this.viewPtr, width ?? 0)\n  }\n\n  public setWrapMode(mode: \"none\" | \"char\" | \"word\"): void {\n    this.guard()\n    this.lib.textBufferViewSetWrapMode(this.viewPtr, mode)\n  }\n\n  public setViewportSize(width: number, height: number): void {\n    this.guard()\n    this.lib.textBufferViewSetViewportSize(this.viewPtr, width, height)\n  }\n\n  public setViewport(x: number, y: number, width: number, height: number): void {\n    this.guard()\n    this.lib.textBufferViewSetViewport(this.viewPtr, x, y, width, height)\n  }\n\n  public get lineInfo(): LineInfo {\n    this.guard()\n    return this.lib.textBufferViewGetLineInfo(this.viewPtr)\n  }\n\n  public get logicalLineInfo(): LineInfo {\n    this.guard()\n    return this.lib.textBufferViewGetLogicalLineInfo(this.viewPtr)\n  }\n\n  public getSelectedText(): string {\n    this.guard()\n    const byteSize = this.textBuffer.byteSize\n    if (byteSize === 0) return \"\"\n\n    const selectedBytes = this.lib.textBufferViewGetSelectedTextBytes(this.viewPtr, byteSize)\n\n    if (!selectedBytes) return \"\"\n\n    return this.lib.decoder.decode(selectedBytes)\n  }\n\n  public getPlainText(): string {\n    this.guard()\n    const byteSize = this.textBuffer.byteSize\n    if (byteSize === 0) return \"\"\n\n    const plainBytes = this.lib.textBufferViewGetPlainTextBytes(this.viewPtr, byteSize)\n\n    if (!plainBytes) return \"\"\n\n    return this.lib.decoder.decode(plainBytes)\n  }\n\n  public setTabIndicator(indicator: string | number): void {\n    this.guard()\n    const codePoint = typeof indicator === \"string\" ? (indicator.codePointAt(0) ?? 0) : indicator\n    this.lib.textBufferViewSetTabIndicator(this.viewPtr, codePoint)\n  }\n\n  public setTabIndicatorColor(color: RGBA): void {\n    this.guard()\n    this.lib.textBufferViewSetTabIndicatorColor(this.viewPtr, color)\n  }\n\n  public setTruncate(truncate: boolean): void {\n    this.guard()\n    this.lib.textBufferViewSetTruncate(this.viewPtr, truncate)\n  }\n\n  public measureForDimensions(width: number, height: number): { lineCount: number; widthColsMax: number } | null {\n    this.guard()\n    return this.lib.textBufferViewMeasureForDimensions(this.viewPtr, width, height)\n  }\n\n  public getVirtualLineCount(): number {\n    this.guard()\n    return this.lib.textBufferViewGetVirtualLineCount(this.viewPtr)\n  }\n\n  public destroy(): void {\n    if (this._destroyed) return\n    this._destroyed = true\n    this.lib.destroyTextBufferView(this.viewPtr)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/text-buffer.test.ts",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { TextBuffer } from \"./text-buffer.js\"\nimport { StyledText, stringToStyledText } from \"./lib/styled-text.js\"\nimport { RGBA } from \"./lib/RGBA.js\"\n\ndescribe(\"TextBuffer\", () => {\n  let buffer: TextBuffer\n\n  beforeEach(() => {\n    buffer = TextBuffer.create(\"wcwidth\")\n  })\n\n  afterEach(() => {\n    buffer.destroy()\n  })\n\n  describe(\"setText and setStyledText\", () => {\n    it(\"should set text content\", () => {\n      const text = \"Hello World\"\n      buffer.setText(text)\n\n      expect(buffer.length).toBe(11)\n      expect(buffer.byteSize).toBeGreaterThan(0)\n    })\n\n    it(\"should set styled text\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      expect(buffer.length).toBe(11)\n    })\n\n    it(\"should handle empty text\", () => {\n      const emptyText = stringToStyledText(\"\")\n      buffer.setStyledText(emptyText)\n\n      expect(buffer.length).toBe(0)\n    })\n\n    it(\"should handle text with newlines\", () => {\n      const text = \"Line 1\\nLine 2\\nLine 3\"\n      buffer.setText(text)\n\n      expect(buffer.length).toBe(18) // 6 + 6 + 6 chars (newlines not counted)\n    })\n  })\n\n  describe(\"getPlainText\", () => {\n    it(\"should return empty string for empty buffer\", () => {\n      const emptyText = stringToStyledText(\"\")\n      buffer.setStyledText(emptyText)\n\n      const plainText = buffer.getPlainText()\n      expect(plainText).toBe(\"\")\n    })\n\n    it(\"should return plain text without styling\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      const plainText = buffer.getPlainText()\n      expect(plainText).toBe(\"Hello World\")\n    })\n\n    it(\"should handle text with newlines\", () => {\n      const styledText = stringToStyledText(\"Line 1\\nLine 2\\nLine 3\")\n      buffer.setStyledText(styledText)\n\n      const plainText = buffer.getPlainText()\n      expect(plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n    })\n\n    it(\"should handle Unicode characters correctly\", () => {\n      const styledText = stringToStyledText(\"Hello 世界 🌟\")\n      buffer.setStyledText(styledText)\n\n      const plainText = buffer.getPlainText()\n      expect(plainText).toBe(\"Hello 世界 🌟\")\n    })\n\n    it(\"should handle styled text with colors and attributes\", () => {\n      const redChunk = {\n        __isChunk: true as const,\n        text: \"Red\",\n        fg: RGBA.fromValues(1, 0, 0, 1),\n      }\n      const newlineChunk = {\n        __isChunk: true as const,\n        text: \"\\n\",\n      }\n      const blueChunk = {\n        __isChunk: true as const,\n        text: \"Blue\",\n        fg: RGBA.fromValues(0, 0, 1, 1),\n      }\n\n      const styledText = new StyledText([redChunk, newlineChunk, blueChunk])\n      buffer.setStyledText(styledText)\n\n      const plainText = buffer.getPlainText()\n      expect(plainText).toBe(\"Red\\nBlue\")\n    })\n  })\n\n  describe(\"length property\", () => {\n    it(\"should return correct length for simple text\", () => {\n      const styledText = stringToStyledText(\"Hello World\")\n      buffer.setStyledText(styledText)\n\n      expect(buffer.length).toBe(11)\n    })\n\n    it(\"should return 0 for empty buffer\", () => {\n      const emptyText = stringToStyledText(\"\")\n      buffer.setStyledText(emptyText)\n\n      expect(buffer.length).toBe(0)\n    })\n\n    it(\"should handle text with newlines correctly\", () => {\n      const styledText = stringToStyledText(\"Line 1\\nLine 2\\nLine 3\")\n      buffer.setStyledText(styledText)\n\n      expect(buffer.length).toBe(18) // 6 + 6 + 6 chars (newlines not counted)\n    })\n\n    it(\"should handle Unicode characters correctly\", () => {\n      const styledText = stringToStyledText(\"Hello 世界 🌟\")\n      buffer.setStyledText(styledText)\n\n      expect(buffer.length).toBe(13)\n    })\n  })\n\n  describe(\"default styles\", () => {\n    it(\"should set and reset default foreground color\", () => {\n      const fg = RGBA.fromValues(1, 0, 0, 1)\n      buffer.setDefaultFg(fg)\n      buffer.resetDefaults()\n\n      // No error should be thrown\n      expect(true).toBe(true)\n    })\n\n    it(\"should set and reset default background color\", () => {\n      const bg = RGBA.fromValues(0, 0, 1, 1)\n      buffer.setDefaultBg(bg)\n      buffer.resetDefaults()\n\n      // No error should be thrown\n      expect(true).toBe(true)\n    })\n\n    it(\"should set and reset default attributes\", () => {\n      buffer.setDefaultAttributes(1)\n      buffer.resetDefaults()\n\n      // No error should be thrown\n      expect(true).toBe(true)\n    })\n  })\n\n  describe(\"clear() vs reset()\", () => {\n    it(\"clear() should empty buffer but preserve text across setText calls\", () => {\n      // Set initial text\n      buffer.setText(\"First text\")\n      expect(buffer.length).toBe(10)\n\n      // Set new text (which calls clear() internally)\n      buffer.setText(\"Second text\")\n      expect(buffer.length).toBe(11)\n      expect(buffer.getPlainText()).toBe(\"Second text\")\n\n      // Explicit clear\n      buffer.clear()\n      expect(buffer.length).toBe(0)\n      expect(buffer.getPlainText()).toBe(\"\")\n    })\n\n    it(\"reset() should fully reset the buffer\", () => {\n      buffer.setText(\"Some text\")\n      expect(buffer.length).toBe(9)\n\n      buffer.reset()\n      expect(buffer.length).toBe(0)\n      expect(buffer.getPlainText()).toBe(\"\")\n\n      // Should be able to use buffer after reset\n      buffer.setText(\"New text\")\n      expect(buffer.length).toBe(8)\n    })\n\n    it(\"setText should preserve highlights (use clear() not reset())\", () => {\n      // This test verifies that setText now uses clear() internally\n      // and doesn't clear highlights\n      buffer.setText(\"Hello World\")\n\n      // Note: We can't easily test highlight preservation from TypeScript\n      // without a SyntaxStyle, but we verify the buffer still works\n      expect(buffer.length).toBe(11)\n\n      buffer.setText(\"New Text\")\n      expect(buffer.length).toBe(8)\n      expect(buffer.getPlainText()).toBe(\"New Text\")\n    })\n\n    it(\"setStyledText should preserve content across calls\", () => {\n      const firstText = stringToStyledText(\"First\")\n      buffer.setStyledText(firstText)\n      expect(buffer.length).toBe(5)\n\n      const secondText = stringToStyledText(\"Second\")\n      buffer.setStyledText(secondText)\n      expect(buffer.length).toBe(6)\n      expect(buffer.getPlainText()).toBe(\"Second\")\n    })\n\n    it(\"multiple setText calls should work correctly with clear()\", () => {\n      buffer.setText(\"Text 1\")\n      expect(buffer.length).toBe(6)\n\n      buffer.setText(\"Text 2\")\n      expect(buffer.length).toBe(6)\n\n      buffer.setText(\"Text 3\")\n      expect(buffer.length).toBe(6)\n\n      expect(buffer.getPlainText()).toBe(\"Text 3\")\n    })\n\n    it(\"clear() followed by setText should work\", () => {\n      buffer.setText(\"Initial\")\n      expect(buffer.length).toBe(7)\n\n      buffer.clear()\n      expect(buffer.length).toBe(0)\n\n      buffer.setText(\"After clear\")\n      expect(buffer.length).toBe(11)\n      expect(buffer.getPlainText()).toBe(\"After clear\")\n    })\n\n    it(\"reset() followed by setText should work\", () => {\n      buffer.setText(\"Initial\")\n      expect(buffer.length).toBe(7)\n\n      buffer.reset()\n      expect(buffer.length).toBe(0)\n\n      buffer.setText(\"After reset\")\n      expect(buffer.length).toBe(11)\n      expect(buffer.getPlainText()).toBe(\"After reset\")\n    })\n  })\n\n  describe(\"append()\", () => {\n    it(\"should append text to empty buffer\", () => {\n      buffer.append(\"Hello\")\n      expect(buffer.length).toBe(5)\n      expect(buffer.getPlainText()).toBe(\"Hello\")\n    })\n\n    it(\"should append text to existing content\", () => {\n      buffer.setText(\"Hello\")\n      buffer.append(\" World\")\n      expect(buffer.length).toBe(11)\n      expect(buffer.getPlainText()).toBe(\"Hello World\")\n    })\n\n    it(\"should append text with newlines\", () => {\n      buffer.setText(\"Line 1\")\n      buffer.append(\"\\nLine 2\")\n      expect(buffer.getPlainText()).toBe(\"Line 1\\nLine 2\")\n    })\n\n    it(\"should append multiple times\", () => {\n      buffer.setText(\"Start\")\n      buffer.append(\" middle\")\n      buffer.append(\" end\")\n      expect(buffer.getPlainText()).toBe(\"Start middle end\")\n    })\n\n    it(\"should handle appending empty string\", () => {\n      buffer.setText(\"Hello\")\n      const lengthBefore = buffer.length\n      buffer.append(\"\")\n      expect(buffer.length).toBe(lengthBefore)\n      expect(buffer.getPlainText()).toBe(\"Hello\")\n    })\n\n    it(\"should append unicode content\", () => {\n      buffer.setText(\"Hello \")\n      buffer.append(\"世界 🌟\")\n      expect(buffer.getPlainText()).toBe(\"Hello 世界 🌟\")\n    })\n\n    it(\"should handle streaming chunks\", () => {\n      buffer.append(\"First\")\n      buffer.append(\"\\nLine2\")\n      buffer.append(\"\\n\")\n      buffer.append(\"Line3\")\n      buffer.append(\" end\")\n      expect(buffer.getPlainText()).toBe(\"First\\nLine2\\nLine3 end\")\n    })\n\n    it(\"should handle CRLF line endings in append\", () => {\n      buffer.append(\"Line1\\r\\n\")\n      buffer.append(\"Line2\\r\\n\")\n      buffer.append(\"Line3\")\n      // CRLF should be normalized to LF\n      expect(buffer.getPlainText()).toBe(\"Line1\\nLine2\\nLine3\")\n    })\n\n    it(\"should work with clear and append\", () => {\n      buffer.setText(\"Initial\")\n      buffer.clear()\n      buffer.append(\"After clear\")\n      expect(buffer.getPlainText()).toBe(\"After clear\")\n    })\n\n    it(\"should work with reset and append\", () => {\n      buffer.setText(\"Initial\")\n      buffer.reset()\n      buffer.append(\"After reset\")\n      expect(buffer.getPlainText()).toBe(\"After reset\")\n    })\n\n    it(\"should handle large streaming append\", () => {\n      for (let i = 0; i < 100; i++) {\n        buffer.append(`Line ${i}\\n`)\n      }\n      const result = buffer.getPlainText()\n      expect(result).toContain(\"Line 0\")\n      expect(result).toContain(\"Line 99\")\n    })\n\n    it(\"should mix setText and append\", () => {\n      buffer.setText(\"First\")\n      buffer.append(\" appended\")\n      expect(buffer.getPlainText()).toBe(\"First appended\")\n\n      buffer.setText(\"Reset\")\n      buffer.append(\" again\")\n      expect(buffer.getPlainText()).toBe(\"Reset again\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/core/src/text-buffer.ts",
    "content": "import type { StyledText } from \"./lib/styled-text.js\"\nimport { RGBA } from \"./lib/RGBA.js\"\nimport { resolveRenderLib, type LineInfo, type RenderLib } from \"./zig.js\"\nimport { type Pointer } from \"bun:ffi\"\nimport { type WidthMethod, type Highlight } from \"./types.js\"\nimport type { SyntaxStyle } from \"./syntax-style.js\"\n\nexport interface TextChunk {\n  __isChunk: true\n  text: string\n  fg?: RGBA\n  bg?: RGBA\n  attributes?: number\n  link?: { url: string }\n}\n\nexport class TextBuffer {\n  private lib: RenderLib\n  private bufferPtr: Pointer\n  private _length: number = 0\n  private _byteSize: number = 0\n  private _lineInfo?: LineInfo\n  private _destroyed: boolean = false\n  private _syntaxStyle?: SyntaxStyle\n  private _textBytes?: Uint8Array\n  private _memId?: number\n  private _appendedChunks: Uint8Array[] = []\n\n  constructor(lib: RenderLib, ptr: Pointer) {\n    this.lib = lib\n    this.bufferPtr = ptr\n  }\n\n  static create(widthMethod: WidthMethod): TextBuffer {\n    const lib = resolveRenderLib()\n    return lib.createTextBuffer(widthMethod)\n  }\n\n  // Fail loud and clear\n  // Instead of trying to return values that could work or not,\n  // this at least will show a stack trace to know where the call to a destroyed TextBuffer was made\n  private guard(): void {\n    if (this._destroyed) throw new Error(\"TextBuffer is destroyed\")\n  }\n\n  public setText(text: string): void {\n    this.guard()\n    this._textBytes = this.lib.encoder.encode(text)\n\n    if (this._memId === undefined) {\n      this._memId = this.lib.textBufferRegisterMemBuffer(this.bufferPtr, this._textBytes, false)\n    } else {\n      this.lib.textBufferReplaceMemBuffer(this.bufferPtr, this._memId, this._textBytes, false)\n    }\n\n    this.lib.textBufferSetTextFromMem(this.bufferPtr, this._memId)\n    this._length = this.lib.textBufferGetLength(this.bufferPtr)\n    this._byteSize = this.lib.textBufferGetByteSize(this.bufferPtr)\n    this._lineInfo = undefined\n    this._appendedChunks = [] // Clear any previously appended chunks\n  }\n\n  public append(text: string): void {\n    this.guard()\n    const textBytes = this.lib.encoder.encode(text)\n    // Keep the bytes alive to prevent garbage collection\n    this._appendedChunks.push(textBytes)\n    this.lib.textBufferAppend(this.bufferPtr, textBytes)\n    this._length = this.lib.textBufferGetLength(this.bufferPtr)\n    this._byteSize = this.lib.textBufferGetByteSize(this.bufferPtr)\n    this._lineInfo = undefined\n  }\n\n  public loadFile(path: string): void {\n    this.guard()\n    const success = this.lib.textBufferLoadFile(this.bufferPtr, path)\n    if (!success) {\n      throw new Error(`Failed to load file: ${path}`)\n    }\n    this._length = this.lib.textBufferGetLength(this.bufferPtr)\n    this._byteSize = this.lib.textBufferGetByteSize(this.bufferPtr)\n    this._lineInfo = undefined\n    this._textBytes = undefined\n  }\n\n  public setStyledText(text: StyledText): void {\n    this.guard()\n\n    this.lib.textBufferSetStyledText(this.bufferPtr, text.chunks)\n\n    this._length = this.lib.textBufferGetLength(this.bufferPtr)\n    this._byteSize = this.lib.textBufferGetByteSize(this.bufferPtr)\n    this._lineInfo = undefined\n  }\n\n  public setDefaultFg(fg: RGBA | null): void {\n    this.guard()\n    this.lib.textBufferSetDefaultFg(this.bufferPtr, fg)\n  }\n\n  public setDefaultBg(bg: RGBA | null): void {\n    this.guard()\n    this.lib.textBufferSetDefaultBg(this.bufferPtr, bg)\n  }\n\n  public setDefaultAttributes(attributes: number | null): void {\n    this.guard()\n    this.lib.textBufferSetDefaultAttributes(this.bufferPtr, attributes)\n  }\n\n  public resetDefaults(): void {\n    this.guard()\n    this.lib.textBufferResetDefaults(this.bufferPtr)\n  }\n\n  public getLineCount(): number {\n    this.guard()\n    return this.lib.textBufferGetLineCount(this.bufferPtr)\n  }\n\n  public get length(): number {\n    this.guard()\n    return this._length\n  }\n\n  public get byteSize(): number {\n    this.guard()\n    return this._byteSize\n  }\n\n  public get ptr(): Pointer {\n    this.guard()\n    return this.bufferPtr\n  }\n\n  public getPlainText(): string {\n    this.guard()\n    if (this._byteSize === 0) return \"\"\n    // Use byteSize for accurate buffer allocation (includes newlines in byte count)\n    const plainBytes = this.lib.getPlainTextBytes(this.bufferPtr, this._byteSize)\n\n    if (!plainBytes) return \"\"\n\n    return this.lib.decoder.decode(plainBytes)\n  }\n\n  public getTextRange(startOffset: number, endOffset: number): string {\n    this.guard()\n    if (startOffset >= endOffset) return \"\"\n    if (this._byteSize === 0) return \"\"\n\n    const rangeBytes = this.lib.textBufferGetTextRange(this.bufferPtr, startOffset, endOffset, this._byteSize)\n\n    if (!rangeBytes) return \"\"\n\n    return this.lib.decoder.decode(rangeBytes)\n  }\n\n  /**\n   * Add a highlight using character offsets into the full text.\n   * start/end in highlight represent absolute character positions.\n   */\n  public addHighlightByCharRange(highlight: Highlight): void {\n    this.guard()\n    this.lib.textBufferAddHighlightByCharRange(this.bufferPtr, highlight)\n  }\n\n  /**\n   * Add a highlight to a specific line by column positions.\n   * start/end in highlight represent column offsets.\n   */\n  public addHighlight(lineIdx: number, highlight: Highlight): void {\n    this.guard()\n    this.lib.textBufferAddHighlight(this.bufferPtr, lineIdx, highlight)\n  }\n\n  public removeHighlightsByRef(hlRef: number): void {\n    this.guard()\n    this.lib.textBufferRemoveHighlightsByRef(this.bufferPtr, hlRef)\n  }\n\n  public clearLineHighlights(lineIdx: number): void {\n    this.guard()\n    this.lib.textBufferClearLineHighlights(this.bufferPtr, lineIdx)\n  }\n\n  public clearAllHighlights(): void {\n    this.guard()\n    this.lib.textBufferClearAllHighlights(this.bufferPtr)\n  }\n\n  public getLineHighlights(lineIdx: number): Array<Highlight> {\n    this.guard()\n    return this.lib.textBufferGetLineHighlights(this.bufferPtr, lineIdx)\n  }\n\n  public getHighlightCount(): number {\n    this.guard()\n    return this.lib.textBufferGetHighlightCount(this.bufferPtr)\n  }\n\n  public setSyntaxStyle(style: SyntaxStyle | null): void {\n    this.guard()\n    this._syntaxStyle = style ?? undefined\n    this.lib.textBufferSetSyntaxStyle(this.bufferPtr, style?.ptr ?? null)\n  }\n\n  public getSyntaxStyle(): SyntaxStyle | null {\n    this.guard()\n    return this._syntaxStyle ?? null\n  }\n\n  public setTabWidth(width: number): void {\n    this.guard()\n    this.lib.textBufferSetTabWidth(this.bufferPtr, width)\n  }\n\n  public getTabWidth(): number {\n    this.guard()\n    return this.lib.textBufferGetTabWidth(this.bufferPtr)\n  }\n\n  public clear(): void {\n    this.guard()\n    this.lib.textBufferClear(this.bufferPtr)\n    this._length = 0\n    this._byteSize = 0\n    this._lineInfo = undefined\n    this._textBytes = undefined\n    this._appendedChunks = []\n    // Note: _memId is NOT cleared - it can be reused for next setText\n  }\n\n  public reset(): void {\n    this.guard()\n    this.lib.textBufferReset(this.bufferPtr)\n    this._length = 0\n    this._byteSize = 0\n    this._lineInfo = undefined\n    this._textBytes = undefined\n    this._memId = undefined // Reset clears the registry, so clear our ID\n    this._appendedChunks = []\n  }\n\n  public destroy(): void {\n    if (this._destroyed) return\n    this._destroyed = true\n    this.lib.destroyTextBuffer(this.bufferPtr)\n  }\n}\n"
  },
  {
    "path": "packages/core/src/types.ts",
    "content": "import type { RGBA } from \"./lib/RGBA.js\"\nimport type { EventEmitter } from \"events\"\nimport type { Selection } from \"./lib/selection.js\"\nimport type { Renderable } from \"./Renderable.js\"\nimport type { InternalKeyHandler, KeyHandler } from \"./lib/KeyHandler.js\"\n\nexport const TextAttributes = {\n  NONE: 0,\n  BOLD: 1 << 0, // 1\n  DIM: 1 << 1, // 2\n  ITALIC: 1 << 2, // 4\n  UNDERLINE: 1 << 3, // 8\n  BLINK: 1 << 4, // 16\n  INVERSE: 1 << 5, // 32\n  HIDDEN: 1 << 6, // 64\n  STRIKETHROUGH: 1 << 7, // 128\n}\n\n// Constants for attribute bit packing\nexport const ATTRIBUTE_BASE_BITS = 8\nexport const ATTRIBUTE_BASE_MASK = 0xff\n\n/**\n * Extract the base 8 bits of attributes from a u32 attribute value.\n * Currently we only use the first 8 bits for standard text attributes.\n */\nexport function getBaseAttributes(attr: number): number {\n  return attr & ATTRIBUTE_BASE_MASK\n}\n\nexport type ThemeMode = \"dark\" | \"light\"\n\nexport type CursorStyle = \"block\" | \"line\" | \"underline\" | \"default\"\n\nexport type MousePointerStyle = \"default\" | \"pointer\" | \"text\" | \"crosshair\" | \"move\" | \"not-allowed\"\n\nexport interface CursorStyleOptions {\n  style?: CursorStyle\n  blinking?: boolean\n  color?: RGBA\n  cursor?: MousePointerStyle\n}\n\nexport enum DebugOverlayCorner {\n  topLeft = 0,\n  topRight = 1,\n  bottomLeft = 2,\n  bottomRight = 3,\n}\n\nexport enum TargetChannel {\n  FG = 1,\n  BG = 2,\n  Both = 3,\n}\n\nexport type WidthMethod = \"wcwidth\" | \"unicode\"\n\nexport interface RendererEvents {\n  resize: (width: number, height: number) => void\n  key: (data: Buffer) => void\n  \"memory:snapshot\": (snapshot: { heapUsed: number; heapTotal: number; arrayBuffers: number }) => void\n  selection: (selection: Selection) => void\n  \"debugOverlay:toggle\": (enabled: boolean) => void\n  theme_mode: (mode: ThemeMode) => void\n}\n\nexport interface RenderContext extends EventEmitter {\n  addToHitGrid: (x: number, y: number, width: number, height: number, id: number) => void\n  pushHitGridScissorRect: (x: number, y: number, width: number, height: number) => void\n  popHitGridScissorRect: () => void\n  clearHitGridScissorRects: () => void\n  width: number\n  height: number\n  requestRender: () => void\n  setCursorPosition: (x: number, y: number, visible: boolean) => void\n  setCursorStyle: (options: CursorStyleOptions) => void\n  setCursorColor: (color: RGBA) => void\n  setMousePointer: (shape: MousePointerStyle) => void\n  widthMethod: WidthMethod\n  capabilities: any | null\n  requestLive: () => void\n  dropLive: () => void\n  hasSelection: boolean\n  getSelection: () => Selection | null\n  requestSelectionUpdate: () => void\n  currentFocusedRenderable: Renderable | null\n  focusRenderable: (renderable: Renderable) => void\n  registerLifecyclePass: (renderable: Renderable) => void\n  unregisterLifecyclePass: (renderable: Renderable) => void\n  getLifecyclePasses: () => Set<Renderable>\n  keyInput: KeyHandler\n  _internalKeyInput: InternalKeyHandler\n  clearSelection: () => void\n  startSelection: (renderable: Renderable, x: number, y: number) => void\n  updateSelection: (\n    currentRenderable: Renderable | undefined,\n    x: number,\n    y: number,\n    options?: { finishDragging?: boolean },\n  ) => void\n}\n\nexport type Timeout = ReturnType<typeof setTimeout> | undefined\n\nexport interface ViewportBounds {\n  x: number\n  y: number\n  width: number\n  height: number\n}\n\nexport interface Highlight {\n  start: number\n  end: number\n  styleId: number\n  priority?: number | null\n  hlRef?: number | null\n}\n\nexport interface LineInfo {\n  /** Display-column offset for each visual line start. */\n  lineStartCols: number[]\n  /** Display-column width for each visual line. */\n  lineWidthCols: number[]\n  /** Maximum display-column width across the reported lines. */\n  lineWidthColsMax: number\n  /** Source logical line index for each visual line. */\n  lineSources: number[]\n  /** Wrap index within each source logical line. */\n  lineWraps: number[]\n}\n\nexport interface LineInfoProvider {\n  get lineInfo(): LineInfo\n  get lineCount(): number\n  get virtualLineCount(): number\n  get scrollY(): number\n}\n\nexport interface CapturedSpan {\n  text: string\n  fg: RGBA\n  bg: RGBA\n  attributes: number\n  width: number\n}\n\nexport interface CapturedLine {\n  spans: CapturedSpan[]\n}\n\nexport interface CapturedFrame {\n  cols: number\n  rows: number\n  cursor: [number, number]\n  lines: CapturedLine[]\n}\n"
  },
  {
    "path": "packages/core/src/utils.ts",
    "content": "import { TextAttributes } from \"./types.js\"\nimport { Renderable } from \"./Renderable.js\"\n\nexport function createTextAttributes({\n  bold = false,\n  italic = false,\n  underline = false,\n  dim = false,\n  blink = false,\n  inverse = false,\n  hidden = false,\n  strikethrough = false,\n}: {\n  bold?: boolean\n  italic?: boolean\n  underline?: boolean\n  dim?: boolean\n  blink?: boolean\n  inverse?: boolean\n  hidden?: boolean\n  strikethrough?: boolean\n} = {}): number {\n  let attributes = TextAttributes.NONE\n\n  if (bold) attributes |= TextAttributes.BOLD\n  if (italic) attributes |= TextAttributes.ITALIC\n  if (underline) attributes |= TextAttributes.UNDERLINE\n  if (dim) attributes |= TextAttributes.DIM\n  if (blink) attributes |= TextAttributes.BLINK\n  if (inverse) attributes |= TextAttributes.INVERSE\n  if (hidden) attributes |= TextAttributes.HIDDEN\n  if (strikethrough) attributes |= TextAttributes.STRIKETHROUGH\n\n  return attributes\n}\n\n// Link attribute helpers (bits 8-31 encode link_id)\nconst ATTRIBUTE_BASE_MASK = 0xff\nconst LINK_ID_SHIFT = 8\nconst LINK_ID_PAYLOAD_MASK = 0xffffff\n\nexport function attributesWithLink(baseAttributes: number, linkId: number): number {\n  const base = baseAttributes & ATTRIBUTE_BASE_MASK\n  const linkBits = (linkId & LINK_ID_PAYLOAD_MASK) << LINK_ID_SHIFT\n  return base | linkBits\n}\n\nexport function getLinkId(attributes: number): number {\n  return (attributes >>> LINK_ID_SHIFT) & LINK_ID_PAYLOAD_MASK\n}\n\n// For debugging purposes\nexport function visualizeRenderableTree(renderable: Renderable, maxDepth: number = 10): void {\n  function buildTreeLines(\n    node: Renderable,\n    prefix: string = \"\",\n    parentPrefix: string = \"\",\n    isLastChild: boolean = true,\n    depth: number = 0,\n  ): string[] {\n    if (depth >= maxDepth) {\n      return [`${prefix}${node.id} ... (max depth reached)`]\n    }\n\n    const lines: string[] = []\n    const children = node.getChildren()\n\n    // Add current node\n    lines.push(`${prefix}${node.id}`)\n\n    if (children.length > 0) {\n      const lastChildIndex = children.length - 1\n\n      children.forEach((child, index) => {\n        const childIsLast = index === lastChildIndex\n        const connector = childIsLast ? \"└── \" : \"├── \"\n        const childPrefix = parentPrefix + (isLastChild ? \"    \" : \"│   \")\n        const childLines = buildTreeLines(child, childPrefix + connector, childPrefix, childIsLast, depth + 1)\n        lines.push(...childLines)\n      })\n    }\n\n    return lines\n  }\n\n  const treeLines = buildTreeLines(renderable)\n  console.log(\"Renderable Tree:\\n\" + treeLines.join(\"\\n\"))\n}\n"
  },
  {
    "path": "packages/core/src/zig/ansi.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\npub const RGBA = [4]f32;\n\npub const AnsiError = error{\n    InvalidFormat,\n    WriteFailed,\n};\n\npub const ANSI = struct {\n    pub const reset = \"\\x1b[0m\";\n    pub const clear = \"\\x1b[2J\";\n    pub const home = \"\\x1b[H\";\n    pub const clearAndHome = \"\\x1b[H\\x1b[2J\";\n    pub const hideCursor = \"\\x1b[?25l\";\n    pub const showCursor = \"\\x1b[?25h\";\n    pub const defaultCursorStyle = \"\\x1b[0 q\";\n    pub const queryPixelSize = \"\\x1b[14t\";\n    pub const nextLine = \"\\x1b[E\";\n\n    // Direct writing to any writer - the most efficient option\n    pub fn moveToOutput(writer: anytype, x: u32, y: u32) AnsiError!void {\n        writer.print(\"\\x1b[{d};{d}H\", .{ y, x }) catch return AnsiError.WriteFailed;\n    }\n\n    pub fn fgColorOutput(writer: anytype, r: u8, g: u8, b: u8) AnsiError!void {\n        writer.print(\"\\x1b[38;2;{d};{d};{d}m\", .{ r, g, b }) catch return AnsiError.WriteFailed;\n    }\n\n    pub fn bgColorOutput(writer: anytype, r: u8, g: u8, b: u8) AnsiError!void {\n        writer.print(\"\\x1b[48;2;{d};{d};{d}m\", .{ r, g, b }) catch return AnsiError.WriteFailed;\n    }\n\n    // Text attribute constants\n    pub const bold = \"\\x1b[1m\";\n    pub const dim = \"\\x1b[2m\";\n    pub const italic = \"\\x1b[3m\";\n    pub const underline = \"\\x1b[4m\";\n    pub const blink = \"\\x1b[5m\";\n    pub const inverse = \"\\x1b[7m\";\n    pub const hidden = \"\\x1b[8m\";\n    pub const strikethrough = \"\\x1b[9m\";\n\n    // Cursor styles\n    pub const cursorBlock = \"\\x1b[2 q\";\n    pub const cursorBlockBlink = \"\\x1b[1 q\";\n    pub const cursorLine = \"\\x1b[6 q\";\n    pub const cursorLineBlink = \"\\x1b[5 q\";\n    pub const cursorUnderline = \"\\x1b[4 q\";\n    pub const cursorUnderlineBlink = \"\\x1b[3 q\";\n\n    pub fn cursorColorOutputWriter(writer: anytype, r: u8, g: u8, b: u8) AnsiError!void {\n        writer.print(\"\\x1b]12;#{x:0>2}{x:0>2}{x:0>2}\\x07\", .{ r, g, b }) catch return AnsiError.WriteFailed;\n    }\n\n    pub fn explicitWidthOutput(writer: anytype, width: u32, text: []const u8) AnsiError!void {\n        writer.print(\"\\x1b]66;w={d};{s}\\x1b\\\\\", .{ width, text }) catch return AnsiError.WriteFailed;\n    }\n\n    pub const resetCursorColor = \"\\x1b]112\\x07\";\n    pub const resetCursorColorFallback = \"\\x1b]12;default\\x07\";\n    pub const resetMousePointer = \"\\x1b]22;\\x07\";\n    pub const saveCursorState = \"\\x1b[s\";\n    pub const restoreCursorState = \"\\x1b[u\";\n\n    pub fn setMousePointerOutput(writer: anytype, shape: []const u8) AnsiError!void {\n        writer.print(\"\\x1b]22;{s}\\x07\", .{ shape }) catch return AnsiError.WriteFailed;\n    }\n\n    pub const switchToAlternateScreen = \"\\x1b[?1049h\";\n    pub const switchToMainScreen = \"\\x1b[?1049l\";\n\n    pub const enableMouseTracking = \"\\x1b[?1000h\";\n    pub const disableMouseTracking = \"\\x1b[?1000l\";\n    pub const enableButtonEventTracking = \"\\x1b[?1002h\";\n    pub const disableButtonEventTracking = \"\\x1b[?1002l\";\n    pub const enableAnyEventTracking = \"\\x1b[?1003h\";\n    pub const disableAnyEventTracking = \"\\x1b[?1003l\";\n    pub const enableSGRMouseMode = \"\\x1b[?1006h\";\n    pub const disableSGRMouseMode = \"\\x1b[?1006l\";\n    pub const mouseSetPixels = \"\\x1b[?1002;1003;1004;1016h\";\n\n    // Terminal capability queries\n    pub const primaryDeviceAttrs = \"\\x1b[c\";\n    pub const tertiaryDeviceAttrs = \"\\x1b[=c\";\n    pub const deviceStatusReport = \"\\x1b[5n\";\n    pub const xtversion = \"\\x1b[>0q\";\n    pub const decrqmFocus = \"\\x1b[?1004$p\";\n    pub const decrqmSgrPixels = \"\\x1b[?1016$p\";\n    pub const decrqmBracketedPaste = \"\\x1b[?2004$p\";\n    pub const decrqmSync = \"\\x1b[?2026$p\";\n    pub const decrqmUnicode = \"\\x1b[?2027$p\";\n    pub const decrqmColorScheme = \"\\x1b[?2031$p\";\n    pub const csiUQuery = \"\\x1b[?u\";\n    pub const kittyGraphicsQuery = \"\\x1b_Gi=31337,s=1,v=1,a=q,t=d,f=24;AAAA\\x1b\\\\\\x1b[c\";\n\n    pub const capabilityQueriesBase = decrqmSgrPixels ++\n        decrqmUnicode ++\n        decrqmColorScheme ++\n        decrqmFocus ++\n        decrqmBracketedPaste ++\n        decrqmSync;\n\n    pub const capabilityQueries = capabilityQueriesBase ++ csiUQuery;\n\n    // tmux DCS passthrough wrapper (ESC chars doubled)\n    pub const tmuxDcsStart = \"\\x1bPtmux;\";\n    pub const tmuxDcsEnd = \"\\x1b\\\\\";\n\n    // GNU Screen DCS passthrough wrapper (no tmux prefix)\n    pub const screenDcsStart = \"\\x1bP\";\n    pub const screenDcsEnd = \"\\x1b\\\\\";\n\n    pub fn wrapForTmux(comptime seq: []const u8) []const u8 {\n        comptime {\n            var result: []const u8 = tmuxDcsStart;\n            for (seq) |c| {\n                if (c == '\\x1b') {\n                    result = result ++ \"\\x1b\\x1b\";\n                } else {\n                    result = result ++ &[_]u8{c};\n                }\n            }\n            return result ++ tmuxDcsEnd;\n        }\n    }\n\n    pub const kittyGraphicsQueryTmux = wrapForTmux(kittyGraphicsQuery);\n    pub const capabilityQueriesTmux = wrapForTmux(capabilityQueriesBase) ++ csiUQuery;\n    pub const sixelGeometryQuery = \"\\x1b[?2;1;0S\";\n    pub const cursorPositionRequest = \"\\x1b[6n\";\n    pub const explicitWidthQuery = \"\\x1b]66;w=1; \\x1b\\\\\";\n    pub const scaledTextQuery = \"\\x1b]66;s=2; \\x1b\\\\\";\n\n    // Focus tracking\n    pub const focusSet = \"\\x1b[?1004h\";\n    pub const focusReset = \"\\x1b[?1004l\";\n\n    // Sync\n    pub const syncSet = \"\\x1b[?2026h\";\n    pub const syncReset = \"\\x1b[?2026l\";\n\n    // Unicode\n    pub const unicodeSet = \"\\x1b[?2027h\";\n    pub const unicodeReset = \"\\x1b[?2027l\";\n\n    // Bracketed paste\n    pub const bracketedPasteSet = \"\\x1b[?2004h\";\n    pub const bracketedPasteReset = \"\\x1b[?2004l\";\n\n    // Color scheme\n    pub const colorSchemeRequest = \"\\x1b[?996n\";\n    pub const colorSchemeSet = \"\\x1b[?2031h\";\n    pub const colorSchemeReset = \"\\x1b[?2031l\";\n\n    // Key encoding\n    pub const csiUPush = \"\\x1b[>{d}u\";\n    pub const csiUPop = \"\\x1b[<u\";\n\n    // modifyOtherKeys mode\n    pub const modifyOtherKeysSet = \"\\x1b[>4;1m\";\n    pub const modifyOtherKeysReset = \"\\x1b[>4;0m\";\n\n    // Movement and erase\n    pub const reverseIndex = \"\\x1bM\";\n    pub const eraseBelowCursor = \"\\x1b[J\";\n\n    // OSC 0 - Set window title\n    pub const setTerminalTitle = \"\\x1b]0;{s}\\x07\";\n\n    pub fn setTerminalTitleOutput(writer: anytype, title: []const u8) AnsiError!void {\n        writer.print(setTerminalTitle, .{title}) catch return AnsiError.WriteFailed;\n    }\n\n    pub fn makeRoomForRendererOutput(writer: anytype, height: u32) AnsiError!void {\n        if (height > 1) {\n            var i: u32 = 0;\n            while (i < height - 1) : (i += 1) {\n                writer.writeByte('\\n') catch return AnsiError.WriteFailed;\n            }\n        }\n    }\n};\n\npub const TextAttributes = struct {\n    pub const NONE: u8 = 0;\n    pub const BOLD: u8 = 1 << 0;\n    pub const DIM: u8 = 1 << 1;\n    pub const ITALIC: u8 = 1 << 2;\n    pub const UNDERLINE: u8 = 1 << 3;\n    pub const BLINK: u8 = 1 << 4;\n    pub const INVERSE: u8 = 1 << 5;\n    pub const HIDDEN: u8 = 1 << 6;\n    pub const STRIKETHROUGH: u8 = 1 << 7;\n\n    // Constants for attribute bit packing\n    pub const ATTRIBUTE_BASE_BITS: u5 = 8;\n    pub const ATTRIBUTE_BASE_MASK: u32 = 0xFF;\n\n    // Constants for link_id packing (bits 8-31)\n    pub const LINK_ID_BITS: u8 = 24;\n    pub const LINK_ID_SHIFT: u5 = ATTRIBUTE_BASE_BITS;\n    pub const LINK_ID_PAYLOAD_MASK: u32 = ((@as(u32, 1) << LINK_ID_BITS) - 1);\n    pub const LINK_ID_MASK: u32 = LINK_ID_PAYLOAD_MASK << LINK_ID_SHIFT;\n\n    /// Extract the base 8 bits of attributes from a u32 attribute value\n    pub fn getBaseAttributes(attr: u32) u8 {\n        return @intCast(attr & ATTRIBUTE_BASE_MASK);\n    }\n\n    /// Extract the link_id from bits 8-31 of attributes\n    pub fn getLinkId(attr: u32) u32 {\n        return (attr & LINK_ID_MASK) >> LINK_ID_SHIFT;\n    }\n\n    /// Set the link_id in an attribute value, preserving base attributes\n    pub fn setLinkId(attr: u32, link_id: u32) u32 {\n        const base = attr & ATTRIBUTE_BASE_MASK;\n        const link_bits = (link_id & LINK_ID_PAYLOAD_MASK) << LINK_ID_SHIFT;\n        return base | link_bits;\n    }\n\n    /// Check if an attribute value has a link\n    pub fn hasLink(attr: u32) bool {\n        return getLinkId(attr) != 0;\n    }\n\n    pub fn applyAttributesOutputWriter(writer: anytype, attributes: u32) AnsiError!void {\n        const base_attr = getBaseAttributes(attributes);\n        if (base_attr & BOLD != 0) writer.writeAll(ANSI.bold) catch return AnsiError.WriteFailed;\n        if (base_attr & DIM != 0) writer.writeAll(ANSI.dim) catch return AnsiError.WriteFailed;\n        if (base_attr & ITALIC != 0) writer.writeAll(ANSI.italic) catch return AnsiError.WriteFailed;\n        if (base_attr & UNDERLINE != 0) writer.writeAll(ANSI.underline) catch return AnsiError.WriteFailed;\n        if (base_attr & BLINK != 0) writer.writeAll(ANSI.blink) catch return AnsiError.WriteFailed;\n        if (base_attr & INVERSE != 0) writer.writeAll(ANSI.inverse) catch return AnsiError.WriteFailed;\n        if (base_attr & HIDDEN != 0) writer.writeAll(ANSI.hidden) catch return AnsiError.WriteFailed;\n        if (base_attr & STRIKETHROUGH != 0) writer.writeAll(ANSI.strikethrough) catch return AnsiError.WriteFailed;\n    }\n};\n\nconst HSV_SECTOR_COUNT = 6;\nconst HUE_SECTOR_DEGREES = 60.0;\n\npub fn hsvToRgb(h: f32, s: f32, v: f32) RGBA {\n    const clamped_h = @mod(h, 360.0);\n    const clamped_s = std.math.clamp(s, 0.0, 1.0);\n    const clamped_v = std.math.clamp(v, 0.0, 1.0);\n\n    const sector = @as(u8, @intFromFloat(@floor(clamped_h / HUE_SECTOR_DEGREES))) % HSV_SECTOR_COUNT;\n    const fractional = clamped_h / HUE_SECTOR_DEGREES - @floor(clamped_h / HUE_SECTOR_DEGREES);\n\n    const p = clamped_v * (1.0 - clamped_s);\n    const q = clamped_v * (1.0 - fractional * clamped_s);\n    const t = clamped_v * (1.0 - (1.0 - fractional) * clamped_s);\n\n    const rgb = switch (sector) {\n        0 => .{ clamped_v, t, p },\n        1 => .{ q, clamped_v, p },\n        2 => .{ p, clamped_v, t },\n        3 => .{ p, q, clamped_v },\n        4 => .{ t, p, clamped_v },\n        5 => .{ clamped_v, p, q },\n        else => unreachable,\n    };\n\n    return .{ rgb[0], rgb[1], rgb[2], 1.0 };\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/README.md",
    "content": "# OpenTUI Benchmarks\n\nThis directory contains benchmarks for the OpenTUI core library.\n\n## Running Benchmarks\n\nFrom the `packages/core` directory:\n\n```bash\n# Using the npm script (recommended)\nbun bench:native\n\n# Include memory statistics\nbun bench:native --mem\n\n# Or from packages/core/src/zig directory:\nzig build bench -Doptimize=ReleaseFast\nzig build bench -Doptimize=ReleaseFast -- --mem\n```\n\n## Adding New Benchmarks\n\nTo add a new benchmark:\n\n1. Create a new `*_bench.zig` file in the `bench/` directory\n2. Import shared types from `bench-utils.zig`:\n   ```zig\n   const bench_utils = @import(\"../bench-utils.zig\");\n   const BenchResult = bench_utils.BenchResult;\n   const MemStats = bench_utils.MemStats;\n   ```\n3. Implement a `pub fn run(allocator: std.mem.Allocator, show_mem: bool) ![]BenchResult` function:\n   - Set up any benchmark-specific dependencies (grapheme pool, Unicode data, etc.)\n   - Run your benchmarks and collect results\n   - Return a slice of `BenchResult` (caller will free it)\n   - The `show_mem` flag indicates whether to include memory statistics\n4. Import it in `bench.zig`:\n   ```zig\n   const my_new_bench = @import(\"bench/my_new_bench.zig\");\n   ```\n5. Call it and print results in `main()`:\n   ```zig\n   const my_results = try my_new_bench.run(allocator, show_mem);\n   defer allocator.free(my_results);\n   try bench_utils.printResults(stdout, my_results);\n   ```\n\nEach benchmark manages its own dependencies, so you only set up what you need.\n\nSee `bench/text-buffer-view_bench.zig` for a complete example.\n"
  },
  {
    "path": "packages/core/src/zig/bench/buffer-draw-text-buffer_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst buffer = @import(\"../buffer.zig\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst OptimizedBuffer = buffer.OptimizedBuffer;\nconst UnifiedTextBuffer = text_buffer.UnifiedTextBuffer;\nconst UnifiedTextBufferView = text_buffer_view.UnifiedTextBufferView;\nconst WrapMode = text_buffer.WrapMode;\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\nconst MemStat = bench_utils.MemStat;\n\npub const benchName = \"Buffer drawTextBuffer\";\n\nfn generateText(allocator: std.mem.Allocator, lines: u32, avg_line_len: u32) ![]u8 {\n    var buf: std.ArrayListUnmanaged(u8) = .{};\n    errdefer buf.deinit(allocator);\n\n    const patterns = [_][]const u8{\n        \"The quick brown fox jumps over the lazy dog. \",\n        \"Lorem ipsum dolor sit amet consectetur. \",\n        \"function test() { return 42; } \",\n        \"Hello 世界 Unicode テスト 🌍 \",\n        \"Mixed: ASCII 中文 emoji 🚀💻 text. \",\n    };\n\n    for (0..lines) |i| {\n        var line_len: u32 = 0;\n        while (line_len < avg_line_len) {\n            const pattern = patterns[i % patterns.len];\n            try buf.appendSlice(allocator, pattern);\n            line_len += @intCast(pattern.len);\n        }\n        try buf.append(allocator, '\\n');\n    }\n\n    return try buf.toOwnedSlice(allocator);\n}\n\nfn generateManySmallChunks(allocator: std.mem.Allocator, chunks: u32) ![]u8 {\n    var buf: std.ArrayListUnmanaged(u8) = .{};\n    errdefer buf.deinit(allocator);\n\n    for (0..chunks) |i| {\n        try buf.appendSlice(allocator, \"ab \");\n        if (i % 20 == 19) try buf.append(allocator, '\\n');\n    }\n\n    return try buf.toOwnedSlice(allocator);\n}\n\nfn setupTextBuffer(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    text: []const u8,\n    wrap_width: ?u32,\n) !struct { *UnifiedTextBuffer, *UnifiedTextBufferView } {\n    const link_pool = link.initGlobalLinkPool(allocator);\n    const tb = try UnifiedTextBuffer.init(allocator, pool, link_pool, .unicode);\n    errdefer tb.deinit();\n\n    try tb.setText(text);\n\n    const view = try UnifiedTextBufferView.init(allocator, tb);\n    errdefer view.deinit();\n\n    if (wrap_width) |w| {\n        view.setWrapMode(.char);\n        view.setWrapWidth(w);\n    } else {\n        view.setWrapMode(.none);\n    }\n\n    return .{ tb, view };\n}\n\nfn benchRenderColdCache(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const name = \"COLD: 120x40 render (500 lines, wrap=120, includes setup)\";\n    if (!bench_utils.matchesBenchFilter(name, bench_filter)) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 500, 100);\n    defer allocator.free(text);\n\n    var stats = BenchStats{};\n    var final_buf_mem: usize = 0;\n\n    for (0..iterations) |i| {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, 120);\n        defer tb.deinit();\n        defer view.deinit();\n\n        const buf = try OptimizedBuffer.init(allocator, 120, 40, .{ .pool = pool });\n        defer buf.deinit();\n\n        try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n        var timer = try std.time.Timer.start();\n        try buf.drawTextBuffer(view, 0, 0);\n        stats.record(timer.read());\n\n        if (i == iterations - 1 and show_mem) {\n            final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem_stat_slice = try allocator.alloc(MemStat, 1);\n        mem_stat_slice[0] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n        break :blk mem_stat_slice;\n    } else null;\n\n    try results.append(allocator, BenchResult{\n        .name = name,\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    });\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchWrapAndRender(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const name = \"WRAP+RENDER: 120x40 render (500 lines, wrap=120)\";\n    if (!bench_utils.matchesBenchFilter(name, bench_filter)) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 500, 100);\n    defer allocator.free(text);\n\n    var stats = BenchStats{};\n    var final_tb_mem: usize = 0;\n    var final_view_mem: usize = 0;\n    var final_buf_mem: usize = 0;\n\n    for (0..iterations) |i| {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, 120);\n        defer tb.deinit();\n        defer view.deinit();\n\n        const buf = try OptimizedBuffer.init(allocator, 120, 40, .{ .pool = pool });\n        defer buf.deinit();\n\n        try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n        var timer = try std.time.Timer.start();\n        try buf.drawTextBuffer(view, 0, 0);\n        stats.record(timer.read());\n\n        if (i == iterations - 1 and show_mem) {\n            final_tb_mem = tb.getArenaAllocatedBytes();\n            final_view_mem = view.getArenaAllocatedBytes();\n            final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem_stat_slice = try allocator.alloc(MemStat, 3);\n        mem_stat_slice[0] = .{ .name = \"TB\", .bytes = final_tb_mem };\n        mem_stat_slice[1] = .{ .name = \"View\", .bytes = final_view_mem };\n        mem_stat_slice[2] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n        break :blk mem_stat_slice;\n    } else null;\n\n    try results.append(allocator, BenchResult{\n        .name = name,\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    });\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderWarmCache(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const warm_name = \"WARM: 120x40 render (500 lines, pre-wrapped, pure render)\";\n    const hot_name = \"HOT:  120x40 render (500 lines, reused buffer, pure render)\";\n    const run_warm = bench_utils.matchesBenchFilter(warm_name, bench_filter);\n    const run_hot = bench_utils.matchesBenchFilter(hot_name, bench_filter);\n    if (!run_warm and !run_hot) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 500, 100);\n    defer allocator.free(text);\n\n    if (run_warm) {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, 120);\n        defer tb.deinit();\n        defer view.deinit();\n\n        var stats = BenchStats{};\n        var final_buf_mem: usize = 0;\n\n        for (0..iterations) |i| {\n            const buf = try OptimizedBuffer.init(allocator, 120, 40, .{ .pool = pool });\n            defer buf.deinit();\n\n            try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n            var timer = try std.time.Timer.start();\n            try buf.drawTextBuffer(view, 0, 0);\n            stats.record(timer.read());\n\n            if (i == iterations - 1 and show_mem) {\n                final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n            }\n        }\n\n        const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n            const mem_stat_slice = try allocator.alloc(MemStat, 1);\n            mem_stat_slice[0] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n            break :blk mem_stat_slice;\n        } else null;\n\n        try results.append(allocator, BenchResult{\n            .name = warm_name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = iterations,\n            .mem_stats = mem_stats,\n        });\n    }\n\n    if (run_hot) {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, 120);\n        defer tb.deinit();\n        defer view.deinit();\n\n        const buf = try OptimizedBuffer.init(allocator, 120, 40, .{ .pool = pool });\n        defer buf.deinit();\n\n        var stats = BenchStats{};\n\n        for (0..iterations) |_| {\n            try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n            var timer = try std.time.Timer.start();\n            try buf.drawTextBuffer(view, 0, 0);\n            stats.record(timer.read());\n        }\n\n        try results.append(allocator, BenchResult{\n            .name = hot_name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = iterations,\n            .mem_stats = null,\n        });\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderSmallResolution(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const no_wrap_name = \"80x24 render (100 lines, no wrap)\";\n    const wrap_name = \"80x24 render (100 lines, wrap=40)\";\n    const run_no_wrap = bench_utils.matchesBenchFilter(no_wrap_name, bench_filter);\n    const run_wrap = bench_utils.matchesBenchFilter(wrap_name, bench_filter);\n    if (!run_no_wrap and !run_wrap) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 100, 80);\n    defer allocator.free(text);\n\n    if (run_no_wrap) {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, 80);\n        defer tb.deinit();\n        defer view.deinit();\n\n        const buf = try OptimizedBuffer.init(allocator, 80, 24, .{ .pool = pool });\n        defer buf.deinit();\n\n        var stats = BenchStats{};\n        var final_buf_mem: usize = 0;\n\n        for (0..iterations) |i| {\n            try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n            var timer = try std.time.Timer.start();\n            try buf.drawTextBuffer(view, 0, 0);\n            stats.record(timer.read());\n\n            if (i == iterations - 1 and show_mem) {\n                final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n            }\n        }\n\n        const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n            const mem_stat_slice = try allocator.alloc(MemStat, 1);\n            mem_stat_slice[0] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n            break :blk mem_stat_slice;\n        } else null;\n\n        try results.append(allocator, BenchResult{\n            .name = no_wrap_name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = iterations,\n            .mem_stats = mem_stats,\n        });\n    }\n\n    if (run_wrap) {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, 40);\n        defer tb.deinit();\n        defer view.deinit();\n\n        const buf = try OptimizedBuffer.init(allocator, 80, 24, .{ .pool = pool });\n        defer buf.deinit();\n\n        var stats = BenchStats{};\n\n        for (0..iterations) |_| {\n            try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n            var timer = try std.time.Timer.start();\n            try buf.drawTextBuffer(view, 0, 0);\n            stats.record(timer.read());\n        }\n\n        try results.append(allocator, BenchResult{\n            .name = wrap_name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = iterations,\n            .mem_stats = null,\n        });\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderMediumResolution(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const name = \"200x60 render (1000 lines, wrap=200)\";\n    if (!bench_utils.matchesBenchFilter(name, bench_filter)) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 1000, 120);\n    defer allocator.free(text);\n\n    const tb, const view = try setupTextBuffer(allocator, pool, text, 200);\n    defer tb.deinit();\n    defer view.deinit();\n\n    const buf = try OptimizedBuffer.init(allocator, 200, 60, .{ .pool = pool });\n    defer buf.deinit();\n\n    var stats = BenchStats{};\n    var final_buf_mem: usize = 0;\n\n    for (0..iterations) |i| {\n        try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n        var timer = try std.time.Timer.start();\n        try buf.drawTextBuffer(view, 0, 0);\n        stats.record(timer.read());\n\n        if (i == iterations - 1 and show_mem) {\n            final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem_stat_slice = try allocator.alloc(MemStat, 1);\n        mem_stat_slice[0] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n        break :blk mem_stat_slice;\n    } else null;\n\n    try results.append(allocator, BenchResult{\n        .name = name,\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    });\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderMassiveResolution(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const name = \"400x200 render (10k lines, wrap=400)\";\n    if (!bench_utils.matchesBenchFilter(name, bench_filter)) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 10000, 200);\n    defer allocator.free(text);\n\n    const tb, const view = try setupTextBuffer(allocator, pool, text, 400);\n    defer tb.deinit();\n    defer view.deinit();\n\n    const buf = try OptimizedBuffer.init(allocator, 400, 200, .{ .pool = pool });\n    defer buf.deinit();\n\n    var stats = BenchStats{};\n    var final_buf_mem: usize = 0;\n\n    for (0..iterations) |i| {\n        try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n        var timer = try std.time.Timer.start();\n        try buf.drawTextBuffer(view, 0, 0);\n        stats.record(timer.read());\n\n        if (i == iterations - 1 and show_mem) {\n            final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem_stat_slice = try allocator.alloc(MemStat, 1);\n        mem_stat_slice[0] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n        break :blk mem_stat_slice;\n    } else null;\n\n    try results.append(allocator, BenchResult{\n        .name = name,\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    });\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderMassiveLines(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const name = \"120x40 render (50k lines, viewport first 40)\";\n    if (!bench_utils.matchesBenchFilter(name, bench_filter)) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 50000, 60);\n    defer allocator.free(text);\n\n    const tb, const view = try setupTextBuffer(allocator, pool, text, null);\n    defer tb.deinit();\n    defer view.deinit();\n\n    const buf = try OptimizedBuffer.init(allocator, 120, 40, .{ .pool = pool });\n    defer buf.deinit();\n\n    var stats = BenchStats{};\n    var final_buf_mem: usize = 0;\n\n    for (0..iterations) |i| {\n        try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n        var timer = try std.time.Timer.start();\n        try buf.drawTextBuffer(view, 0, 0);\n        stats.record(timer.read());\n\n        if (i == iterations - 1 and show_mem) {\n            final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem_stat_slice = try allocator.alloc(MemStat, 1);\n        mem_stat_slice[0] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n        break :blk mem_stat_slice;\n    } else null;\n\n    try results.append(allocator, BenchResult{\n        .name = name,\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    });\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderOneMassiveLine(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const name = \"80x30 render (1 massive line 500KB, wrap=80)\";\n    if (!bench_utils.matchesBenchFilter(name, bench_filter)) return try results.toOwnedSlice(allocator);\n\n    var buf_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer buf_builder.deinit(allocator);\n\n    for (0..100000) |_| {\n        try buf_builder.appendSlice(allocator, \"word \");\n    }\n    const text = try buf_builder.toOwnedSlice(allocator);\n    defer allocator.free(text);\n\n    const tb, const view = try setupTextBuffer(allocator, pool, text, 80);\n    defer tb.deinit();\n    defer view.deinit();\n\n    const buf = try OptimizedBuffer.init(allocator, 80, 30, .{ .pool = pool });\n    defer buf.deinit();\n\n    var stats = BenchStats{};\n    var final_buf_mem: usize = 0;\n\n    for (0..iterations) |i| {\n        try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n        var timer = try std.time.Timer.start();\n        try buf.drawTextBuffer(view, 0, 0);\n        stats.record(timer.read());\n\n        if (i == iterations - 1 and show_mem) {\n            final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem_stat_slice = try allocator.alloc(MemStat, 1);\n        mem_stat_slice[0] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n        break :blk mem_stat_slice;\n    } else null;\n\n    try results.append(allocator, BenchResult{\n        .name = name,\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    });\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderManySmallChunks(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const name = \"80x30 render (10k tiny chunks)\";\n    if (!bench_utils.matchesBenchFilter(name, bench_filter)) return try results.toOwnedSlice(allocator);\n\n    const text = try generateManySmallChunks(allocator, 10000);\n    defer allocator.free(text);\n\n    const tb, const view = try setupTextBuffer(allocator, pool, text, 80);\n    defer tb.deinit();\n    defer view.deinit();\n\n    const buf = try OptimizedBuffer.init(allocator, 80, 30, .{ .pool = pool });\n    defer buf.deinit();\n\n    var stats = BenchStats{};\n    var final_buf_mem: usize = 0;\n\n    for (0..iterations) |i| {\n        try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n        var timer = try std.time.Timer.start();\n        try buf.drawTextBuffer(view, 0, 0);\n        stats.record(timer.read());\n\n        if (i == iterations - 1 and show_mem) {\n            final_buf_mem = @sizeOf(OptimizedBuffer) + (buf.width * buf.height * (@sizeOf(u32) + @sizeOf(@TypeOf(buf.buffer.fg[0])) * 2 + @sizeOf(u8)));\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem_stat_slice = try allocator.alloc(MemStat, 1);\n        mem_stat_slice[0] = .{ .name = \"Buf\", .bytes = final_buf_mem };\n        break :blk mem_stat_slice;\n    } else null;\n\n    try results.append(allocator, BenchResult{\n        .name = name,\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    });\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderWithViewport(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n    _ = show_mem;\n\n    const viewport_name = \"100x30 render (10k lines, viewport at line 5000)\";\n    const no_viewport_name = \"100x30 render (10k lines, no viewport)\";\n    const run_viewport = bench_utils.matchesBenchFilter(viewport_name, bench_filter);\n    const run_no_viewport = bench_utils.matchesBenchFilter(no_viewport_name, bench_filter);\n    if (!run_viewport and !run_no_viewport) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 10000, 100);\n    defer allocator.free(text);\n\n    if (run_viewport) {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, null);\n        defer tb.deinit();\n        defer view.deinit();\n\n        view.setViewport(.{ .x = 0, .y = 5000, .width = 100, .height = 30 });\n\n        const buf = try OptimizedBuffer.init(allocator, 100, 30, .{ .pool = pool });\n        defer buf.deinit();\n\n        var stats = BenchStats{};\n\n        for (0..iterations) |_| {\n            try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n            var timer = try std.time.Timer.start();\n            try buf.drawTextBuffer(view, 0, 0);\n            stats.record(timer.read());\n        }\n\n        try results.append(allocator, BenchResult{\n            .name = viewport_name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = iterations,\n            .mem_stats = null,\n        });\n    }\n\n    if (run_no_viewport) {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, null);\n        defer tb.deinit();\n        defer view.deinit();\n\n        const buf = try OptimizedBuffer.init(allocator, 100, 30, .{ .pool = pool });\n        defer buf.deinit();\n\n        var stats = BenchStats{};\n\n        for (0..iterations) |_| {\n            try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n            var timer = try std.time.Timer.start();\n            try buf.drawTextBuffer(view, 0, 0);\n            stats.record(timer.read());\n        }\n\n        try results.append(allocator, BenchResult{\n            .name = no_viewport_name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = iterations,\n            .mem_stats = null,\n        });\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchRenderWithSelection(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n    _ = show_mem;\n\n    const selection_name = \"120x40 render (500 lines, with selection)\";\n    const no_selection_name = \"120x40 render (500 lines, no selection)\";\n    const run_selection = bench_utils.matchesBenchFilter(selection_name, bench_filter);\n    const run_no_selection = bench_utils.matchesBenchFilter(no_selection_name, bench_filter);\n    if (!run_selection and !run_no_selection) return try results.toOwnedSlice(allocator);\n\n    const text = try generateText(allocator, 500, 100);\n    defer allocator.free(text);\n\n    if (run_selection) {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, 120);\n        defer tb.deinit();\n        defer view.deinit();\n\n        view.setSelection(500, 1500, .{ 0.2, 0.4, 0.8, 1.0 }, .{ 1.0, 1.0, 1.0, 1.0 });\n\n        const buf = try OptimizedBuffer.init(allocator, 120, 40, .{ .pool = pool });\n        defer buf.deinit();\n\n        var stats = BenchStats{};\n\n        for (0..iterations) |_| {\n            try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n            var timer = try std.time.Timer.start();\n            try buf.drawTextBuffer(view, 0, 0);\n            stats.record(timer.read());\n        }\n\n        try results.append(allocator, BenchResult{\n            .name = selection_name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = iterations,\n            .mem_stats = null,\n        });\n    }\n\n    if (run_no_selection) {\n        const tb, const view = try setupTextBuffer(allocator, pool, text, 120);\n        defer tb.deinit();\n        defer view.deinit();\n\n        const buf = try OptimizedBuffer.init(allocator, 120, 40, .{ .pool = pool });\n        defer buf.deinit();\n\n        var stats = BenchStats{};\n\n        for (0..iterations) |_| {\n            try buf.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n\n            var timer = try std.time.Timer.start();\n            try buf.drawTextBuffer(view, 0, 0);\n            stats.record(timer.read());\n        }\n\n        try results.append(allocator, BenchResult{\n            .name = no_selection_name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = iterations,\n            .mem_stats = null,\n        });\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    // Global pool and unicode data are initialized once in bench.zig\n    const pool = gp.initGlobalPool(allocator);\n\n    var all_results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer all_results.deinit(allocator);\n\n    const iterations: usize = 10;\n\n    const cold_cache_results = try benchRenderColdCache(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, cold_cache_results);\n\n    const warm_cache_results = try benchRenderWarmCache(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, warm_cache_results);\n\n    const wrap_render_results = try benchWrapAndRender(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, wrap_render_results);\n\n    const small_res_results = try benchRenderSmallResolution(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, small_res_results);\n\n    const medium_res_results = try benchRenderMediumResolution(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, medium_res_results);\n\n    const massive_res_results = try benchRenderMassiveResolution(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, massive_res_results);\n\n    const massive_lines_results = try benchRenderMassiveLines(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, massive_lines_results);\n\n    const one_massive_line_results = try benchRenderOneMassiveLine(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, one_massive_line_results);\n\n    const many_chunks_results = try benchRenderManySmallChunks(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, many_chunks_results);\n\n    const viewport_results = try benchRenderWithViewport(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, viewport_results);\n\n    const selection_results = try benchRenderWithSelection(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, selection_results);\n\n    return try all_results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/edit-buffer_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst edit_buffer = @import(\"../edit-buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst EditBuffer = edit_buffer.EditBuffer;\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\nconst MemStat = bench_utils.MemStat;\n\npub const benchName = \"EditBuffer Operations\";\n\nfn benchInsertOperations(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n    const link_pool = link.initGlobalLinkPool(allocator);\n\n    // Single-line insert at start\n    {\n        const name = \"EditBuffer insert 1k times at start\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |iter| {\n                var eb = try EditBuffer.init(allocator, pool, link_pool, .unicode);\n                defer eb.deinit();\n\n                const text = \"Hello, world! \";\n                var timer = try std.time.Timer.start();\n                for (0..1000) |_| {\n                    try eb.insertText(text);\n                    try eb.setCursor(0, 0);\n                }\n                stats.record(timer.read());\n\n                if (iter == iterations - 1 and show_mem) {\n                    final_mem = eb.getTextBuffer().getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const s = try allocator.alloc(MemStat, 1);\n                s[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk s;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    // Multi-line insert\n    {\n        const name = \"EditBuffer insert 500 multi-line blocks\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |iter| {\n                var eb = try EditBuffer.init(allocator, pool, link_pool, .unicode);\n                defer eb.deinit();\n\n                const text = \"Line 1\\nLine 2\\nLine 3\\n\";\n                var timer = try std.time.Timer.start();\n                for (0..500) |_| {\n                    try eb.insertText(text);\n                }\n                stats.record(timer.read());\n\n                if (iter == iterations - 1 and show_mem) {\n                    final_mem = eb.getTextBuffer().getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const s = try allocator.alloc(MemStat, 1);\n                s[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk s;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchDeleteOperations(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n    const link_pool = link.initGlobalLinkPool(allocator);\n\n    // Single-line delete with backspace\n    {\n        const name = \"EditBuffer backspace 500 chars\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |iter| {\n                var eb = try EditBuffer.init(allocator, pool, link_pool, .unicode);\n                defer eb.deinit();\n\n                // Build up text\n                const text = \"Hello, world! \";\n                for (0..1000) |_| {\n                    try eb.insertText(text);\n                }\n\n                var timer = try std.time.Timer.start();\n                for (0..500) |_| {\n                    try eb.backspace();\n                }\n                stats.record(timer.read());\n\n                if (iter == iterations - 1 and show_mem) {\n                    final_mem = eb.getTextBuffer().getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const s = try allocator.alloc(MemStat, 1);\n                s[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk s;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    // Multi-line delete range\n    {\n        const name = \"EditBuffer delete 50-line range\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |iter| {\n                var eb = try EditBuffer.init(allocator, pool, link_pool, .unicode);\n                defer eb.deinit();\n\n                // Build up text with many lines\n                const text = \"Line 1\\nLine 2\\nLine 3\\n\";\n                for (0..100) |_| {\n                    try eb.insertText(text);\n                }\n\n                var timer = try std.time.Timer.start();\n                // Delete across 50 lines\n                try eb.deleteRange(.{ .row = 10, .col = 0 }, .{ .row = 60, .col = 0 });\n                stats.record(timer.read());\n\n                if (iter == iterations - 1 and show_mem) {\n                    final_mem = eb.getTextBuffer().getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const s = try allocator.alloc(MemStat, 1);\n                s[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk s;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchMixedOperations(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n    const link_pool = link.initGlobalLinkPool(allocator);\n\n    // Simulated typing session\n    {\n        const name = \"EditBuffer mixed operations (300 lines)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |iter| {\n                var eb = try EditBuffer.init(allocator, pool, link_pool, .unicode);\n                defer eb.deinit();\n\n                var timer = try std.time.Timer.start();\n\n                // Type some text\n                for (0..100) |_| {\n                    try eb.insertText(\"function test() {\\n\");\n                    try eb.insertText(\"    return 42;\\n\");\n                    try eb.insertText(\"}\\n\");\n                }\n\n                // Navigate and edit\n                try eb.setCursor(50, 0);\n                try eb.insertText(\"// Comment\\n\");\n\n                // Delete a range\n                try eb.deleteRange(.{ .row = 100, .col = 0 }, .{ .row = 120, .col = 0 });\n\n                stats.record(timer.read());\n\n                if (iter == iterations - 1 and show_mem) {\n                    final_mem = eb.getTextBuffer().getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const s = try allocator.alloc(MemStat, 1);\n                s[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk s;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchWordBoundaryOperations(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n    const link_pool = link.initGlobalLinkPool(allocator);\n\n    // Next word boundary navigation\n    {\n        const name = \"EditBuffer getNextWordBoundary 1k times\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |iter| {\n                var eb = try EditBuffer.init(allocator, pool, link_pool, .unicode);\n                defer eb.deinit();\n\n                // Build text with many words\n                const text = \"The quick brown fox jumps over the lazy dog. \";\n                for (0..100) |_| {\n                    try eb.insertText(text);\n                }\n\n                try eb.setCursor(0, 0);\n\n                var timer = try std.time.Timer.start();\n                // Navigate through 1000 word boundaries\n                for (0..1000) |_| {\n                    const cursor = eb.getNextWordBoundary();\n                    try eb.setCursor(cursor.row, cursor.col);\n                }\n                stats.record(timer.read());\n\n                if (iter == iterations - 1 and show_mem) {\n                    final_mem = eb.getTextBuffer().getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const s = try allocator.alloc(MemStat, 1);\n                s[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk s;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    // Previous word boundary navigation\n    {\n        const name = \"EditBuffer getPrevWordBoundary 1k times\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |iter| {\n                var eb = try EditBuffer.init(allocator, pool, link_pool, .unicode);\n                defer eb.deinit();\n\n                // Build text with many words\n                const text = \"The quick brown fox jumps over the lazy dog. \";\n                for (0..100) |_| {\n                    try eb.insertText(text);\n                }\n\n                // Start at end\n                const line_count = eb.getTextBuffer().lineCount();\n                const last_line = if (line_count > 0) line_count - 1 else 0;\n                try eb.setCursor(last_line, 4500);\n\n                var timer = try std.time.Timer.start();\n                // Navigate backward through 1000 word boundaries\n                for (0..1000) |_| {\n                    const cursor = eb.getPrevWordBoundary();\n                    try eb.setCursor(cursor.row, cursor.col);\n                }\n                stats.record(timer.read());\n\n                if (iter == iterations - 1 and show_mem) {\n                    final_mem = eb.getTextBuffer().getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const s = try allocator.alloc(MemStat, 1);\n                s[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk s;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    // Word boundary with multi-line text\n    {\n        const name = \"EditBuffer word boundary multi-line 500 times\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |iter| {\n                var eb = try EditBuffer.init(allocator, pool, link_pool, .unicode);\n                defer eb.deinit();\n\n                // Build multi-line text with words\n                const text = \"Hello world test\\nAnother line here\\nThird line content\\n\";\n                for (0..100) |_| {\n                    try eb.insertText(text);\n                }\n\n                try eb.setCursor(0, 0);\n\n                var timer = try std.time.Timer.start();\n                // Navigate through 500 word boundaries across lines\n                for (0..500) |_| {\n                    const cursor = eb.getNextWordBoundary();\n                    try eb.setCursor(cursor.row, cursor.col);\n                }\n                stats.record(timer.read());\n\n                if (iter == iterations - 1 and show_mem) {\n                    final_mem = eb.getTextBuffer().getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const s = try allocator.alloc(MemStat, 1);\n                s[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk s;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    // Global pool and unicode data are initialized once in bench.zig\n    const pool = gp.initGlobalPool(allocator);\n\n    var all_results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer all_results.deinit(allocator);\n\n    const iterations: usize = 5;\n\n    // Run all benchmark categories and filter results\n    const insert_results = try benchInsertOperations(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, insert_results);\n\n    const delete_results = try benchDeleteOperations(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, delete_results);\n\n    const mixed_results = try benchMixedOperations(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, mixed_results);\n\n    const word_boundary_results = try benchWordBoundaryOperations(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, word_boundary_results);\n\n    return try all_results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/native-span-feed_bench.zig",
    "content": "const std = @import(\"std\");\nconst raw = @import(\"../native-span-feed.zig\");\n\n/// Zero-copy benchmark producer (reserve/commit).\npub export fn benchProduce(\n    stream: ?*raw.Stream,\n    total_bytes: u64,\n    pattern_ptr: ?*const u8,\n    pattern_len: usize,\n    commit_every: u32,\n) callconv(.c) i32 {\n    if (stream == null) return raw.Status.err_invalid;\n    if (total_bytes == 0) return raw.Status.ok;\n\n    var pattern_slice: []const u8 = raw.default_pattern;\n    if (pattern_ptr != null and pattern_len > 0) {\n        pattern_slice = @as([*]const u8, @ptrCast(pattern_ptr.?))[0..pattern_len];\n    }\n    if (pattern_slice.len == 0) return raw.Status.err_invalid;\n\n    var remaining: u64 = total_bytes;\n    var reserve_info: raw.ReserveInfo = undefined;\n\n    while (remaining > 0) {\n        const remaining_usize = if (remaining > std.math.maxInt(usize))\n            std.math.maxInt(usize)\n        else\n            @as(usize, @intCast(remaining));\n\n        const reserve_status = raw.streamReserve(stream, 1, &reserve_info);\n        if (reserve_status != raw.Status.ok) return reserve_status;\n\n        const available: usize = @intCast(reserve_info.len);\n        const to_write = @min(available, remaining_usize);\n\n        const dest = @as([*]u8, @ptrFromInt(reserve_info.ptr))[0..to_write];\n        var dest_index: usize = 0;\n        while (dest_index < to_write) {\n            const copy_len = @min(pattern_slice.len, to_write - dest_index);\n            @memcpy(dest[dest_index .. dest_index + copy_len], pattern_slice[0..copy_len]);\n            dest_index += copy_len;\n        }\n\n        const commit_status = raw.streamCommitReserved(stream, @intCast(to_write));\n        if (commit_status != raw.Status.ok) return commit_status;\n\n        remaining -= @as(u64, to_write);\n    }\n\n    _ = commit_every;\n    return raw.Status.ok;\n}\n\n/// Copy benchmark producer (streamWrite).\npub export fn benchProduceWrite(\n    stream: ?*raw.Stream,\n    total_bytes: u64,\n    pattern_ptr: ?*const u8,\n    pattern_len: usize,\n    commit_every: u32,\n) callconv(.c) i32 {\n    if (stream == null) return raw.Status.err_invalid;\n    if (total_bytes == 0) return raw.Status.ok;\n\n    var pattern_slice: []const u8 = raw.default_pattern;\n    if (pattern_ptr != null and pattern_len > 0) {\n        pattern_slice = @as([*]const u8, @ptrCast(pattern_ptr.?))[0..pattern_len];\n    }\n    if (pattern_slice.len == 0) return raw.Status.err_invalid;\n\n    var remaining: u64 = total_bytes;\n    var bytes_since_commit: u64 = 0;\n\n    while (remaining > 0) {\n        const remaining_usize = if (remaining > std.math.maxInt(usize))\n            std.math.maxInt(usize)\n        else\n            @as(usize, @intCast(remaining));\n\n        const to_write = @min(pattern_slice.len, remaining_usize);\n        const status = raw.streamWrite(stream, @ptrCast(pattern_slice.ptr), to_write);\n        if (status != raw.Status.ok) return status;\n\n        bytes_since_commit += @as(u64, to_write);\n        remaining -= @as(u64, to_write);\n\n        if (commit_every != 0 and bytes_since_commit >= commit_every) {\n            const commit_status = raw.streamCommit(stream);\n            if (commit_status != raw.Status.ok) return commit_status;\n            bytes_since_commit = 0;\n        }\n    }\n\n    if (commit_every != 0 and bytes_since_commit > 0) {\n        const commit_status = raw.streamCommit(stream);\n        if (commit_status != raw.Status.ok) return commit_status;\n    }\n\n    return raw.Status.ok;\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/rope-markers_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst rope_mod = @import(\"../rope.zig\");\n\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\nconst MemStats = bench_utils.MemStats;\n\npub const benchName = \"Rope Marker Tracking\";\n\n// Test union type with markers (like Segment with .brk)\nconst Token = union(enum) {\n    text: u32, // Text segments (width)\n    marker: void, // Line markers\n\n    pub const MarkerTypes = &[_]std.meta.Tag(Token){.marker};\n\n    pub const Metrics = struct {\n        width: u32 = 0,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.width += other.width;\n        }\n\n        pub fn weight(self: *const Metrics) u32 {\n            return self.width;\n        }\n    };\n\n    pub fn measure(self: *const Token) Metrics {\n        return switch (self.*) {\n            .text => |w| .{ .width = w },\n            .marker => .{ .width = 0 },\n        };\n    }\n\n    pub fn empty() Token {\n        return .{ .text = 0 };\n    }\n\n    pub fn is_empty(self: *const Token) bool {\n        return switch (self.*) {\n            .text => |w| w == 0,\n            else => false,\n        };\n    }\n};\n\nconst RopeType = rope_mod.Rope(Token);\n\n/// Create a rope with specific marker density\n/// marker_every: insert a marker every N text tokens\nfn createRope(allocator: std.mem.Allocator, text_count: u32, marker_every: u32) !RopeType {\n    var tokens: std.ArrayListUnmanaged(Token) = .{};\n    defer tokens.deinit(allocator);\n\n    for (0..text_count) |i| {\n        try tokens.append(allocator, .{ .text = 10 }); // Each text segment has width 10\n        if ((i + 1) % marker_every == 0) {\n            try tokens.append(allocator, .{ .marker = {} });\n        }\n    }\n\n    return try RopeType.from_slice(allocator, tokens.items);\n}\n\nfn benchRebuildMarkerIndex(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Small rope, high marker density (every 10 tokens)\n    {\n        const name = \"Create rope with markers: 1k tokens, marker every 10 (~100 markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var timer = try std.time.Timer.start();\n                const rope = try createRope(arena.allocator(), 1000, 10);\n                _ = rope; // Markers are automatically indexed during rope creation\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Small rope, low marker density (every 100 tokens)\n    {\n        const name = \"Rebuild index: 1k tokens, marker every 100 (~10 markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var timer = try std.time.Timer.start();\n                const rope = try createRope(arena.allocator(), 1000, 100);\n                _ = rope; // Markers are automatically indexed during rope creation\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Medium rope, high marker density\n    {\n        const name = \"Rebuild index: 10k tokens, marker every 10 (~1k markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var timer = try std.time.Timer.start();\n                const rope = try createRope(arena.allocator(), 10000, 10);\n                _ = rope; // Markers are automatically indexed during rope creation\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Medium rope, low marker density\n    {\n        const name = \"Rebuild index: 10k tokens, marker every 100 (~100 markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var timer = try std.time.Timer.start();\n                const rope = try createRope(arena.allocator(), 10000, 100);\n                _ = rope; // Markers are automatically indexed during rope creation\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Large rope, text-editor-like density (marker every 50 = ~50 chars/line)\n    {\n        const name = \"Rebuild index: 50k tokens, marker every 50 (~1k markers, text-editor-like)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var timer = try std.time.Timer.start();\n                const rope = try createRope(arena.allocator(), 50000, 50);\n                _ = rope; // Markers are automatically indexed during rope creation\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Very large rope, sparse markers\n    {\n        const name = \"Rebuild index: 100k tokens, marker every 200 (~500 markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var timer = try std.time.Timer.start();\n                const rope = try createRope(arena.allocator(), 100000, 200);\n                _ = rope; // Markers are automatically indexed during rope creation\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchMarkerLookup(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // O(1) lookup in small rope\n    {\n        const name = \"O(1) lookup: 100 random marker accesses, ~100 markers\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createRope(arena.allocator(), 1000, 10);\n                // Markers are automatically indexed in the tree structure\n\n                var timer = try std.time.Timer.start();\n                for (0..100) |i| {\n                    _ = rope.getMarker(.marker, @intCast(i % rope.markerCount(.marker)));\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // O(1) lookup in medium rope\n    {\n        const name = \"O(1) lookup: 1k random marker accesses, ~200 markers\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createRope(arena.allocator(), 10000, 50);\n                // Markers are automatically indexed in the tree structure\n\n                var timer = try std.time.Timer.start();\n                for (0..1000) |i| {\n                    _ = rope.getMarker(.marker, @intCast(i % rope.markerCount(.marker)));\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // O(1) lookup in large rope (text-editor scenario)\n    {\n        const name = \"O(1) lookup: 10k random line jumps, ~1k lines (text-editor)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createRope(arena.allocator(), 50000, 50);\n                // Markers are automatically indexed in the tree structure\n                const marker_count = rope.markerCount(.marker);\n\n                var prng = std.Random.DefaultPrng.init(42);\n                const random = prng.random();\n\n                var timer = try std.time.Timer.start();\n                for (0..10000) |_| {\n                    const line = random.intRangeAtMost(u32, 0, marker_count - 1);\n                    _ = rope.getMarker(.marker, line);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Sequential marker access (best case)\n    {\n        const name = \"O(1) lookup: Sequential access to all ~200 markers\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createRope(arena.allocator(), 10000, 50);\n                // Markers are automatically indexed in the tree structure\n                const marker_count = rope.markerCount(.marker);\n\n                var timer = try std.time.Timer.start();\n                for (0..marker_count) |i| {\n                    _ = rope.getMarker(.marker, @intCast(i));\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchMarkerCount(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Count markers - should be O(1) hash lookup\n    {\n        const name = \"markerCount: 100k calls (should be ~O(1))\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createRope(arena.allocator(), 10000, 50);\n                // Markers are automatically indexed in the tree structure\n\n                var timer = try std.time.Timer.start();\n                for (0..100000) |_| {\n                    _ = rope.markerCount(.marker);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchDepthVsPerformance(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Shallow tree (from_slice creates balanced tree)\n    {\n        const name = \"Create BALANCED tree with markers: 10k tokens, ~200 markers\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var timer = try std.time.Timer.start();\n                const rope = try createRope(arena.allocator(), 10000, 50);\n                _ = rope; // Markers are automatically indexed during rope creation\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Deep tree (built by sequential appends)\n    {\n        const name = \"Rebuild on UNBALANCED tree: 10k tokens, ~200 markers\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                // Build unbalanced tree through sequential operations\n                var rope = try RopeType.init(arena.allocator());\n                for (0..10000) |i| {\n                    try rope.append(.{ .text = 10 });\n                    if ((i + 1) % 50 == 0) {\n                        try rope.append(.{ .marker = {} });\n                    }\n                }\n\n                var timer = try std.time.Timer.start();\n                // Markers are automatically indexed in the tree structure\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchEditThenRebuild(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Typical edit workflow: build, edit, rebuild\n    {\n        const name = \"Edit workflow: 3 inserts + rebuild (~200 markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createRope(arena.allocator(), 10000, 50);\n                // Markers are automatically indexed in the tree structure\n\n                var timer = try std.time.Timer.start();\n                // Simulate typing at line 50\n                const line50_marker = rope.getMarker(.marker, 50).?;\n                const insert_pos = line50_marker.leaf_index + 1;\n\n                // Insert some text\n                try rope.insert(insert_pos, .{ .text = 10 });\n                try rope.insert(insert_pos + 1, .{ .text = 10 });\n                try rope.insert(insert_pos + 2, .{ .text = 10 });\n\n                // Rebuild index after edit\n                // Markers are automatically indexed in the tree structure\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Insert new line (adds marker)\n    {\n        const name = \"Insert newline: insert marker + rebuild (~200 markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createRope(arena.allocator(), 10000, 50);\n                // Markers are automatically indexed in the tree structure\n\n                var timer = try std.time.Timer.start();\n                // Insert new line (marker) at position 100\n                try rope.insert(100, .{ .marker = {} });\n                // Markers are automatically indexed in the tree structure\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Delete line (removes marker)\n    {\n        const name = \"Delete line: remove marker + rebuild (~200 markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createRope(arena.allocator(), 10000, 50);\n                // Markers are automatically indexed in the tree structure\n\n                var timer = try std.time.Timer.start();\n                // Delete marker at position\n                const marker_pos = rope.getMarker(.marker, 50).?.leaf_index;\n                try rope.delete(marker_pos);\n                // Markers are automatically indexed in the tree structure\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchMemoryUsage(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Memory comparison: with vs without marker index\n    {\n        const name = \"Memory: 50k tokens WITHOUT marker index\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                const rope = try createRope(arena.allocator(), 50000, 50);\n                // Don't rebuild index - just measure rope creation\n                _ = rope;\n\n                const elapsed: u64 = 0; // Placeholder for memory measurement\n                stats.record(elapsed);\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    {\n        const name = \"Memory: 50k tokens WITH marker index (~1k markers)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                const rope = try createRope(arena.allocator(), 50000, 50);\n                _ = rope; // Markers are automatically indexed in the tree structure\n\n                const elapsed: u64 = 0; // Placeholder for memory measurement\n                stats.record(elapsed);\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    _ = show_mem;\n\n    var all_results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer all_results.deinit(allocator);\n\n    const iterations: usize = 10;\n\n    // Rebuild index benchmarks\n    const rebuild_results = try benchRebuildMarkerIndex(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, rebuild_results);\n\n    // Marker lookup benchmarks\n    const lookup_results = try benchMarkerLookup(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, lookup_results);\n\n    // Marker count benchmarks\n    const count_results = try benchMarkerCount(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, count_results);\n\n    // Tree depth impact\n    const depth_results = try benchDepthVsPerformance(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, depth_results);\n\n    // Edit workflows\n    const edit_results = try benchEditThenRebuild(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, edit_results);\n\n    // Memory usage comparison\n    const memory_results = try benchMemoryUsage(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, memory_results);\n\n    return try all_results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/rope_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst rope_mod = @import(\"../rope.zig\");\n\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\n\npub const benchName = \"Rope Data Structure\";\n\n// Simple test item type\nconst TestItem = struct {\n    value: u32,\n\n    pub fn empty() TestItem {\n        return .{ .value = 0 };\n    }\n\n    pub fn is_empty(self: *const TestItem) bool {\n        return self.value == 0;\n    }\n};\n\nconst RopeType = rope_mod.Rope(TestItem);\n\nfn benchInsertOperations(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Sequential appends\n    {\n        const name = \"Rope sequential append 10k items\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.init(arena.allocator());\n                var timer = try std.time.Timer.start();\n                for (0..10000) |i| {\n                    try rope.append(.{ .value = @intCast(i) });\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Sequential prepends\n    {\n        const name = \"Rope sequential prepend 10k items\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.init(arena.allocator());\n                var timer = try std.time.Timer.start();\n                for (0..10000) |i| {\n                    try rope.prepend(.{ .value = @intCast(i) });\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Random inserts\n    {\n        const name = \"Rope random insert 5k items\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.init(arena.allocator());\n                var prng = std.Random.DefaultPrng.init(42);\n                const random = prng.random();\n                var timer = try std.time.Timer.start();\n                for (0..5000) |i| {\n                    const pos = if (rope.count() > 0)\n                        random.intRangeAtMost(u32, 0, rope.count())\n                    else\n                        0;\n                    try rope.insert(pos, .{ .value = @intCast(i) });\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(allocator);\n}\n\nfn benchDeleteOperations(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    var items: [10000]TestItem = undefined;\n    for (&items, 0..) |*item, i| {\n        item.* = .{ .value = @intCast(i) };\n    }\n\n    // Sequential deletes from end\n    {\n        const name = \"Rope sequential delete 5k from end\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.from_slice(arena.allocator(), &items);\n                var timer = try std.time.Timer.start();\n                for (0..5000) |_| {\n                    try rope.delete(rope.count() - 1);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Sequential deletes from beginning\n    {\n        const name = \"Rope sequential delete 5k from beginning\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.from_slice(arena.allocator(), &items);\n                var timer = try std.time.Timer.start();\n                for (0..5000) |_| {\n                    try rope.delete(0);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Random deletes\n    {\n        const name = \"Rope random delete 5k items\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.from_slice(arena.allocator(), &items);\n                var prng = std.Random.DefaultPrng.init(42);\n                const random = prng.random();\n                var timer = try std.time.Timer.start();\n                for (0..5000) |_| {\n                    const pos = random.intRangeAtMost(u32, 0, rope.count() - 1);\n                    try rope.delete(pos);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(allocator);\n}\n\nfn benchBulkOperations(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    var items: [10000]TestItem = undefined;\n    for (&items, 0..) |*item, i| {\n        item.* = .{ .value = @intCast(i) };\n    }\n\n    // insert_slice\n    {\n        const name = \"Rope insert_slice 10x1k items\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.init(arena.allocator());\n                var chunk: [1000]TestItem = undefined;\n                for (&chunk, 0..) |*item, i| {\n                    item.* = .{ .value = @intCast(i) };\n                }\n                var timer = try std.time.Timer.start();\n                for (0..10) |_| {\n                    try rope.insert_slice(rope.count(), &chunk);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // delete_range\n    {\n        const name = \"Rope delete_range 10x500 items\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.from_slice(arena.allocator(), &items);\n                var timer = try std.time.Timer.start();\n                for (0..10) |_| {\n                    const start = if (rope.count() > 500) rope.count() - 500 else 0;\n                    const end = rope.count();\n                    try rope.delete_range(start, end);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // split/concat\n    {\n        const name = \"Rope split/concat 100 cycles at midpoint\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try RopeType.from_slice(arena.allocator(), &items);\n                var timer = try std.time.Timer.start();\n                for (0..100) |_| {\n                    const mid = rope.count() / 2;\n                    var right = try rope.split(mid);\n                    try rope.concat(&right);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // concat two ropes\n    {\n        const name = \"Rope concat two 5k-item ropes\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope1 = try RopeType.from_slice(arena.allocator(), items[0..5000]);\n                const rope2 = try RopeType.from_slice(arena.allocator(), items[5000..]);\n                var timer = try std.time.Timer.start();\n                try rope1.concat(&rope2);\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(allocator);\n}\n\nfn benchAccessPatterns(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    var items: [10000]TestItem = undefined;\n    for (&items, 0..) |*item, i| {\n        item.* = .{ .value = @intCast(i) };\n    }\n\n    // Sequential get\n    {\n        const name = \"Rope sequential get all 10k items\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                const rope = try RopeType.from_slice(arena.allocator(), &items);\n                var timer = try std.time.Timer.start();\n                for (0..10000) |i| {\n                    _ = rope.get(@intCast(i));\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Random get\n    {\n        const name = \"Rope random get 10k accesses\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                const rope = try RopeType.from_slice(arena.allocator(), &items);\n                var prng = std.Random.DefaultPrng.init(42);\n                const random = prng.random();\n                var timer = try std.time.Timer.start();\n                for (0..10000) |_| {\n                    const pos = random.intRangeAtMost(u32, 0, 9999);\n                    _ = rope.get(pos);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Walk\n    {\n        const name = \"Rope walk all 10k items\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                const rope = try RopeType.from_slice(arena.allocator(), &items);\n                const Ctx = struct {\n                    sum: u64 = 0,\n                    fn walker(ctx: *anyopaque, data: *const TestItem, index: u32) RopeType.Node.WalkerResult {\n                        _ = index;\n                        const self = @as(*@This(), @ptrCast(@alignCast(ctx)));\n                        self.sum += data.value;\n                        return .{};\n                    }\n                };\n                var ctx = Ctx{};\n                var timer = try std.time.Timer.start();\n                try rope.walk(&ctx, Ctx.walker);\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(allocator);\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    _ = show_mem; // Rope benchmarks don't currently track memory\n\n    var all_results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer all_results.deinit(allocator);\n\n    const iterations: usize = 10;\n\n    // Run all benchmark categories and filter results\n    const insert_results = try benchInsertOperations(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, insert_results);\n\n    const delete_results = try benchDeleteOperations(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, delete_results);\n\n    const bulk_results = try benchBulkOperations(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, bulk_results);\n\n    const access_results = try benchAccessPatterns(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, access_results);\n\n    return all_results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/styled-text_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst text_buffer_mod = @import(\"../text-buffer.zig\");\nconst syntax_style_mod = @import(\"../syntax-style.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\nconst MemStats = bench_utils.MemStats;\nconst TextBuffer = text_buffer_mod.UnifiedTextBuffer;\nconst StyledChunk = text_buffer_mod.StyledChunk; // Use the unified type from text-buffer\nconst SyntaxStyle = syntax_style_mod.SyntaxStyle;\n\npub const benchName = \"Styled Text Operations\";\n\n// Helper to convert RGBA to pointer for benchmark\nfn rgbaToPtr(rgba: *const [4]f32) [*]const f32 {\n    return @ptrCast(rgba);\n}\n\nfn benchSetStyledTextOperations(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Setup global resources\n    var arena = std.heap.ArenaAllocator.init(allocator);\n    defer arena.deinit();\n    const global_alloc = arena.allocator();\n\n    const pool = gp.initGlobalPool(global_alloc);\n    const link_pool = link.initGlobalLinkPool(global_alloc);\n\n    // Single chunk - baseline\n    {\n        const name = \"setStyledText - single chunk (55 chars)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            const text = \"Hello, World! This is a test of styled text rendering.\";\n            const fg_color = [4]f32{ 1.0, 1.0, 1.0, 1.0 };\n\n            for (0..iterations) |_| {\n                const tb = try TextBuffer.init(allocator, pool, link_pool, .wcwidth);\n                defer tb.deinit();\n\n                const style = try SyntaxStyle.init(allocator);\n                defer style.deinit();\n                tb.setSyntaxStyle(style);\n\n                const chunks = [_]StyledChunk{.{\n                    .text_ptr = text.ptr,\n                    .text_len = text.len,\n                    .fg_ptr = rgbaToPtr(&fg_color),\n                    .bg_ptr = null,\n                    .attributes = 0,\n                }};\n\n                var timer = try std.time.Timer.start();\n                try tb.setStyledText(&chunks);\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Multiple small chunks\n    {\n        const name = \"setStyledText - 6 small chunks (~6 chars each)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            const red = [4]f32{ 1.0, 0.0, 0.0, 1.0 };\n            const green = [4]f32{ 0.0, 1.0, 0.0, 1.0 };\n            const blue = [4]f32{ 0.0, 0.0, 1.0, 1.0 };\n            const yellow = [4]f32{ 1.0, 1.0, 0.0, 1.0 };\n            const cyan = [4]f32{ 0.0, 1.0, 1.0, 1.0 };\n            const magenta = [4]f32{ 1.0, 0.0, 1.0, 1.0 };\n\n            for (0..iterations) |_| {\n                const tb = try TextBuffer.init(allocator, pool, link_pool, .wcwidth);\n                defer tb.deinit();\n\n                const style = try SyntaxStyle.init(allocator);\n                defer style.deinit();\n                tb.setSyntaxStyle(style);\n\n                const text0 = \"Red \";\n                const text1 = \"Green \";\n                const text2 = \"Blue \";\n                const text3 = \"Yellow \";\n                const text4 = \"Cyan \";\n                const text5 = \"Magenta \";\n\n                const chunks = [_]StyledChunk{\n                    .{ .text_ptr = text0.ptr, .text_len = text0.len, .fg_ptr = rgbaToPtr(&red), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text1.ptr, .text_len = text1.len, .fg_ptr = rgbaToPtr(&green), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text2.ptr, .text_len = text2.len, .fg_ptr = rgbaToPtr(&blue), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text3.ptr, .text_len = text3.len, .fg_ptr = rgbaToPtr(&yellow), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text4.ptr, .text_len = text4.len, .fg_ptr = rgbaToPtr(&cyan), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text5.ptr, .text_len = text5.len, .fg_ptr = rgbaToPtr(&magenta), .bg_ptr = null, .attributes = 0 },\n                };\n\n                var timer = try std.time.Timer.start();\n                try tb.setStyledText(&chunks);\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Many chunks (simulating syntax highlighted code)\n    {\n        const name = \"setStyledText - 8 chunks (syntax highlighting)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            const keyword_color = [4]f32{ 0.8, 0.4, 1.0, 1.0 };\n            const identifier_color = [4]f32{ 0.7, 0.9, 1.0, 1.0 };\n            const operator_color = [4]f32{ 1.0, 1.0, 1.0, 1.0 };\n            const number_color = [4]f32{ 0.7, 1.0, 0.7, 1.0 };\n\n            for (0..iterations) |_| {\n                const tb = try TextBuffer.init(allocator, pool, link_pool, .wcwidth);\n                defer tb.deinit();\n\n                const style = try SyntaxStyle.init(allocator);\n                defer style.deinit();\n                tb.setSyntaxStyle(style);\n\n                // Simulate a line of syntax highlighted code: \"const x = 42;\"\n                const t0 = \"const\";\n                const t1 = \" \";\n                const t2 = \"x\";\n                const t3 = \" \";\n                const t4 = \"=\";\n                const t5 = \" \";\n                const t6 = \"42\";\n                const t7 = \";\";\n\n                const chunks = [_]StyledChunk{\n                    .{ .text_ptr = t0.ptr, .text_len = t0.len, .fg_ptr = rgbaToPtr(&keyword_color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = t1.ptr, .text_len = t1.len, .fg_ptr = null, .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = t2.ptr, .text_len = t2.len, .fg_ptr = rgbaToPtr(&identifier_color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = t3.ptr, .text_len = t3.len, .fg_ptr = null, .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = t4.ptr, .text_len = t4.len, .fg_ptr = rgbaToPtr(&operator_color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = t5.ptr, .text_len = t5.len, .fg_ptr = null, .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = t6.ptr, .text_len = t6.len, .fg_ptr = rgbaToPtr(&number_color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = t7.ptr, .text_len = t7.len, .fg_ptr = rgbaToPtr(&operator_color), .bg_ptr = null, .attributes = 0 },\n                };\n\n                var timer = try std.time.Timer.start();\n                try tb.setStyledText(&chunks);\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Large text with many chunks (simplified)\n    {\n        const name = \"setStyledText - 10 chunks (~120 chars total)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            const text = \"Lorem ipsum \";\n\n            for (0..iterations) |_| {\n                const tb = try TextBuffer.init(allocator, pool, link_pool, .wcwidth);\n                defer tb.deinit();\n\n                const style = try SyntaxStyle.init(allocator);\n                defer style.deinit();\n                tb.setSyntaxStyle(style);\n\n                // Just repeat the same chunk 10 times\n                const color = [4]f32{ 1.0, 0.5, 0.5, 1.0 };\n                const chunks = [_]StyledChunk{\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = text.ptr, .text_len = text.len, .fg_ptr = rgbaToPtr(&color), .bg_ptr = null, .attributes = 0 },\n                };\n\n                var timer = try std.time.Timer.start();\n                try tb.setStyledText(&chunks);\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Chunks with attributes (bold, italic, etc.)\n    {\n        const name = \"setStyledText - 5 chunks with attributes\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                const tb = try TextBuffer.init(allocator, pool, link_pool, .wcwidth);\n                defer tb.deinit();\n\n                const style = try SyntaxStyle.init(allocator);\n                defer style.deinit();\n                tb.setSyntaxStyle(style);\n\n                const t0 = \"Normal \";\n                const t1 = \"Bold \";\n                const t2 = \"Italic \";\n                const t3 = \"Underline \";\n                const t4 = \"Bold+Italic \";\n\n                const chunks = [_]StyledChunk{\n                    .{ .text_ptr = t0.ptr, .text_len = t0.len, .fg_ptr = null, .bg_ptr = null, .attributes = 0 },\n                    .{ .text_ptr = t1.ptr, .text_len = t1.len, .fg_ptr = null, .bg_ptr = null, .attributes = 1 },\n                    .{ .text_ptr = t2.ptr, .text_len = t2.len, .fg_ptr = null, .bg_ptr = null, .attributes = 2 },\n                    .{ .text_ptr = t3.ptr, .text_len = t3.len, .fg_ptr = null, .bg_ptr = null, .attributes = 4 },\n                    .{ .text_ptr = t4.ptr, .text_len = t4.len, .fg_ptr = null, .bg_ptr = null, .attributes = 3 },\n                };\n\n                var timer = try std.time.Timer.start();\n                try tb.setStyledText(&chunks);\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchHighlightOperations(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Setup global resources\n    var arena = std.heap.ArenaAllocator.init(allocator);\n    defer arena.deinit();\n    const global_alloc = arena.allocator();\n\n    const pool = gp.initGlobalPool(global_alloc);\n    const link_pool = link.initGlobalLinkPool(global_alloc);\n\n    // Baseline: 1000 sequential addHighlightByCharRange calls (unbatched)\n    {\n        const name = \"addHighlightByCharRange - 1000 calls (unbatched)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                const tb = try TextBuffer.init(allocator, pool, link_pool, .wcwidth);\n                defer tb.deinit();\n\n                const style = try SyntaxStyle.init(allocator);\n                defer style.deinit();\n                tb.setSyntaxStyle(style);\n\n                // Create a multi-line buffer\n                const text = \"Line 1 with some text\\nLine 2 with more text\\nLine 3 here\\nLine 4 content\\nLine 5 final\";\n                try tb.setText(text);\n\n                var timer = try std.time.Timer.start();\n\n                // Add 1000 highlights sequentially\n                for (0..1000) |i| {\n                    const start_char: u32 = @intCast((i * 2) % 50);\n                    const end_char = start_char + 3;\n                    const style_id: u32 = @intCast((i % 5) + 1);\n                    tb.addHighlightByCharRange(start_char, end_char, style_id, 1, 0) catch {};\n                }\n\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Batched: 1000 sequential addHighlightByCharRange calls in a transaction\n    {\n        const name = \"addHighlightByCharRange - 1000 calls (batched)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                const tb = try TextBuffer.init(allocator, pool, link_pool, .wcwidth);\n                defer tb.deinit();\n\n                const style = try SyntaxStyle.init(allocator);\n                defer style.deinit();\n                tb.setSyntaxStyle(style);\n\n                // Create a multi-line buffer\n                const text = \"Line 1 with some text\\nLine 2 with more text\\nLine 3 here\\nLine 4 content\\nLine 5 final\";\n                try tb.setText(text);\n\n                var timer = try std.time.Timer.start();\n\n                // Batch all highlights in a transaction\n                tb.startHighlightsTransaction();\n                defer tb.endHighlightsTransaction();\n\n                // Add 1000 highlights sequentially\n                for (0..1000) |i| {\n                    const start_char: u32 = @intCast((i * 2) % 50);\n                    const end_char = start_char + 3;\n                    const style_id: u32 = @intCast((i % 5) + 1);\n                    tb.addHighlightByCharRange(start_char, end_char, style_id, 1, 0) catch {};\n                }\n\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // setStyledText with 100 chunks (realistic syntax highlighting scenario)\n    {\n        const name = \"setStyledText - 100 chunks (realistic code)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            // Build a realistic multi-line code snippet with 100 chunks\n            var chunk_list: std.ArrayListUnmanaged(StyledChunk) = .{};\n            defer chunk_list.deinit(allocator);\n\n            const keyword_color = [4]f32{ 0.8, 0.4, 1.0, 1.0 };\n            const identifier_color = [4]f32{ 0.7, 0.9, 1.0, 1.0 };\n            const operator_color = [4]f32{ 1.0, 1.0, 1.0, 1.0 };\n            const number_color = [4]f32{ 0.7, 1.0, 0.7, 1.0 };\n            const string_color = [4]f32{ 0.9, 0.8, 0.5, 1.0 };\n\n            // Repeat a pattern to create 100 chunks\n            for (0..10) |_| {\n                try chunk_list.append(allocator, .{ .text_ptr = \"const\".ptr, .text_len = 5, .fg_ptr = rgbaToPtr(&keyword_color), .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \" \".ptr, .text_len = 1, .fg_ptr = null, .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \"myVar\".ptr, .text_len = 5, .fg_ptr = rgbaToPtr(&identifier_color), .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \" \".ptr, .text_len = 1, .fg_ptr = null, .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \"=\".ptr, .text_len = 1, .fg_ptr = rgbaToPtr(&operator_color), .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \" \".ptr, .text_len = 1, .fg_ptr = null, .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \"42\".ptr, .text_len = 2, .fg_ptr = rgbaToPtr(&number_color), .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \";\".ptr, .text_len = 1, .fg_ptr = rgbaToPtr(&operator_color), .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \"\\n\".ptr, .text_len = 1, .fg_ptr = null, .bg_ptr = null, .attributes = 0 });\n                try chunk_list.append(allocator, .{ .text_ptr = \"\\\"str\\\"\".ptr, .text_len = 5, .fg_ptr = rgbaToPtr(&string_color), .bg_ptr = null, .attributes = 0 });\n            }\n\n            for (0..iterations) |_| {\n                const tb = try TextBuffer.init(allocator, pool, link_pool, .wcwidth);\n                defer tb.deinit();\n\n                const style = try SyntaxStyle.init(allocator);\n                defer style.deinit();\n                tb.setSyntaxStyle(style);\n\n                var timer = try std.time.Timer.start();\n                try tb.setStyledText(chunk_list.items);\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    _ = show_mem;\n\n    var all_results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer all_results.deinit(allocator);\n\n    const iterations: usize = 100;\n\n    const styled_text_results = try benchSetStyledTextOperations(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, styled_text_results);\n\n    const highlight_results = try benchHighlightOperations(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, highlight_results);\n\n    return try all_results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/text-buffer-coords_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst seg_mod = @import(\"../text-buffer-segment.zig\");\nconst iter_mod = @import(\"../text-buffer-iterators.zig\");\n\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\nconst Segment = seg_mod.Segment;\nconst TextChunk = seg_mod.TextChunk;\nconst UnifiedRope = seg_mod.UnifiedRope;\n\npub const benchName = \"TextBuffer Coordinate Conversion\";\n\n/// Create a text buffer with N lines for testing\nfn createTestBuffer(allocator: std.mem.Allocator, line_count: u32, chars_per_line: u32) !UnifiedRope {\n    var segments: std.ArrayListUnmanaged(Segment) = .{};\n    defer segments.deinit(allocator);\n\n    for (0..line_count) |i| {\n        // Add text segment\n        try segments.append(allocator, Segment{\n            .text = TextChunk{\n                .mem_id = 0,\n                .byte_start = 0,\n                .byte_end = chars_per_line,\n                .width = @intCast(chars_per_line),\n                .flags = TextChunk.Flags.ASCII_ONLY,\n            },\n        });\n        // Add line break (except for last line)\n        if (i < line_count - 1) {\n            try segments.append(allocator, Segment{ .brk = {} });\n        }\n    }\n\n    return try UnifiedRope.from_slice(allocator, segments.items);\n}\n\nfn benchCoordsToOffsetCurrent(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Small buffer - 100 lines\n    {\n        const name = \"[CURRENT] coordsToOffset: 100 calls, 100 lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createTestBuffer(arena.allocator(), 100, 50);\n\n                var timer = try std.time.Timer.start();\n                // Access lines throughout the buffer\n                for (0..100) |i| {\n                    const line: u32 = @intCast(i % 100);\n                    _ = iter_mod.coordsToOffset(&rope, line, 25);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Medium buffer - 1k lines\n    {\n        const name = \"[CURRENT] coordsToOffset: 100 calls, 1k lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createTestBuffer(arena.allocator(), 1000, 50);\n\n                var timer = try std.time.Timer.start();\n                for (0..100) |i| {\n                    const line: u32 = @intCast((i * 10) % 1000);\n                    _ = iter_mod.coordsToOffset(&rope, line, 25);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Large buffer - 10k lines\n    {\n        const name = \"[CURRENT] coordsToOffset: 100 calls, 10k lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createTestBuffer(arena.allocator(), 10000, 50);\n\n                var timer = try std.time.Timer.start();\n                for (0..100) |i| {\n                    const line: u32 = @intCast((i * 100) % 10000);\n                    _ = iter_mod.coordsToOffset(&rope, line, 25);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Worst case: access last line repeatedly\n    {\n        const name = \"[CURRENT] coordsToOffset: 100 calls to LAST line, 1k lines (worst case)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createTestBuffer(arena.allocator(), 1000, 50);\n\n                var timer = try std.time.Timer.start();\n                for (0..100) |_| {\n                    _ = iter_mod.coordsToOffset(&rope, 999, 25); // Last line\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchOffsetToCoordsCurrent(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // Small buffer\n    {\n        const name = \"[CURRENT] offsetToCoords: 100 calls, 100 lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createTestBuffer(arena.allocator(), 100, 50);\n                const total_width = iter_mod.getTotalWidth(&rope);\n\n                var prng = std.Random.DefaultPrng.init(42);\n                const random = prng.random();\n\n                var timer = try std.time.Timer.start();\n                for (0..100) |_| {\n                    const offset = random.intRangeAtMost(u32, 0, total_width);\n                    _ = iter_mod.offsetToCoords(&rope, offset);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Medium buffer\n    {\n        const name = \"[CURRENT] offsetToCoords: 100 calls, 1k lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createTestBuffer(arena.allocator(), 1000, 50);\n                const total_width = iter_mod.getTotalWidth(&rope);\n\n                var prng = std.Random.DefaultPrng.init(42);\n                const random = prng.random();\n\n                var timer = try std.time.Timer.start();\n                for (0..100) |_| {\n                    const offset = random.intRangeAtMost(u32, 0, total_width);\n                    _ = iter_mod.offsetToCoords(&rope, offset);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Large buffer\n    {\n        const name = \"[CURRENT] offsetToCoords: 100 calls, 10k lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createTestBuffer(arena.allocator(), 10000, 50);\n                const total_width = iter_mod.getTotalWidth(&rope);\n\n                var prng = std.Random.DefaultPrng.init(42);\n                const random = prng.random();\n\n                var timer = try std.time.Timer.start();\n                for (0..100) |_| {\n                    const offset = random.intRangeAtMost(u32, 0, total_width);\n                    _ = iter_mod.offsetToCoords(&rope, offset);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchGetLineCount(\n    allocator: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    // getLineCount is already optimized with metrics\n    {\n        const name = \"getLineCount: 100k calls (already O(1) via metrics)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var stats = BenchStats{};\n\n            for (0..iterations) |_| {\n                var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n                defer arena.deinit();\n\n                var rope = try createTestBuffer(arena.allocator(), 10000, 50);\n\n                var timer = try std.time.Timer.start();\n                for (0..100000) |_| {\n                    _ = iter_mod.getLineCount(&rope);\n                }\n                stats.record(timer.read());\n            }\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    _ = show_mem;\n\n    var all_results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer all_results.deinit(allocator);\n\n    const iterations: usize = 10;\n\n    // Current implementation benchmarks\n    const coords_results = try benchCoordsToOffsetCurrent(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, coords_results);\n\n    const offset_results = try benchOffsetToCoordsCurrent(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, offset_results);\n\n    const count_results = try benchGetLineCount(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, count_results);\n\n    return try all_results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/text-buffer-view_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst UnifiedTextBuffer = text_buffer.UnifiedTextBuffer;\nconst UnifiedTextBufferView = text_buffer_view.UnifiedTextBufferView;\nconst WrapMode = text_buffer.WrapMode;\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\nconst MemStat = bench_utils.MemStat;\n\npub const benchName = \"TextBuffer Wrapping\";\n\nconst large_text_patterns = [_][]const u8{\n    \"The quick brown fox jumps over the lazy dog. \",\n    \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. \",\n    \"Hello, 世界! Unicode テスト 🌍🎉 \",\n    \"Mixed width: ASCII 中文字符 emoji 🚀🔥💻 and more text. \",\n    \"Programming languages: Rust, Zig, Go, Python, JavaScript. \",\n    \"Αυτό είναι ελληνικό κείμενο. Это русский текст. \",\n    \"Numbers and symbols: 12345 !@#$%^&*() []{}|;:',.<>? \",\n    \"Tab\\tseparated\\tvalues\\there\\tfor\\ttesting\\twrapping. \",\n};\n\npub fn generateLargeText(allocator: std.mem.Allocator, lines: u32, target_bytes: usize) ![]u8 {\n    var buffer: std.ArrayListUnmanaged(u8) = .{};\n    errdefer buffer.deinit(allocator);\n\n    var current_bytes: usize = 0;\n    var line_idx: u32 = 0;\n\n    while (current_bytes < target_bytes and line_idx < lines) : (line_idx += 1) {\n        const pattern = large_text_patterns[line_idx % large_text_patterns.len];\n        const repeat_count = 2 + (line_idx % 5);\n\n        for (0..repeat_count) |_| {\n            try buffer.appendSlice(allocator, pattern);\n            current_bytes += pattern.len;\n        }\n\n        try buffer.append(allocator, '\\n');\n        current_bytes += 1;\n    }\n\n    return try buffer.toOwnedSlice(allocator);\n}\n\npub fn generateLargeTextSingleLine(allocator: std.mem.Allocator, target_bytes: usize) ![]u8 {\n    var buffer: std.ArrayListUnmanaged(u8) = .{};\n    errdefer buffer.deinit(allocator);\n\n    var current_bytes: usize = 0;\n    var pattern_idx: usize = 0;\n\n    while (current_bytes < target_bytes) {\n        const pattern = large_text_patterns[pattern_idx % large_text_patterns.len];\n        try buffer.appendSlice(allocator, pattern);\n        current_bytes += pattern.len;\n        pattern_idx += 1;\n    }\n\n    return try buffer.toOwnedSlice(allocator);\n}\n\nfn computeLargeTextStats(lines: u32, target_bytes: usize) struct { bytes: usize, line_count: usize } {\n    var current_bytes: usize = 0;\n    var line_idx: u32 = 0;\n\n    while (current_bytes < target_bytes and line_idx < lines) : (line_idx += 1) {\n        const pattern = large_text_patterns[line_idx % large_text_patterns.len];\n        const repeat_count = 2 + (line_idx % 5);\n        current_bytes += pattern.len * repeat_count + 1;\n    }\n\n    return .{ .bytes = current_bytes, .line_count = line_idx };\n}\n\nfn computeSingleLineTextSize(target_bytes: usize) usize {\n    var current_bytes: usize = 0;\n    var pattern_idx: usize = 0;\n\n    while (current_bytes < target_bytes) {\n        const pattern = large_text_patterns[pattern_idx % large_text_patterns.len];\n        current_bytes += pattern.len;\n        pattern_idx += 1;\n    }\n\n    return current_bytes;\n}\n\nfn benchSetText(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    iterations: usize,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n    const link_pool = link.initGlobalLinkPool(allocator);\n\n    // Small text\n    {\n        const name = \"TextBuffer setText small (3 lines, 40 bytes)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            const text = \"Hello, world!\\nSecond line\\nThird line\";\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |i| {\n                var tb = try UnifiedTextBuffer.init(allocator, pool, link_pool, .unicode);\n                defer tb.deinit();\n\n                var timer = try std.time.Timer.start();\n                try tb.setText(text);\n                stats.record(timer.read());\n\n                if (i == iterations - 1 and show_mem) {\n                    final_mem = tb.getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const mem = try allocator.alloc(MemStat, 1);\n                mem[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk mem;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    // Large multi-line text\n    {\n        const text_stats = computeLargeTextStats(5000, 1 * 1024 * 1024);\n        const text_mb = @as(f64, @floatFromInt(text_stats.bytes)) / (1024.0 * 1024.0);\n        const name = try std.fmt.allocPrint(\n            allocator,\n            \"TextBuffer setText large ({d} lines, {d:.2} MiB)\",\n            .{ text_stats.line_count, text_mb },\n        );\n\n        if (!bench_utils.matchesBenchFilter(name, bench_filter)) {\n            allocator.free(name);\n        } else {\n            const text = try generateLargeText(allocator, 5000, 1 * 1024 * 1024);\n            defer allocator.free(text);\n\n            var stats = BenchStats{};\n            var final_mem: usize = 0;\n\n            for (0..iterations) |i| {\n                var tb = try UnifiedTextBuffer.init(allocator, pool, link_pool, .unicode);\n                defer tb.deinit();\n\n                var timer = try std.time.Timer.start();\n                try tb.setText(text);\n                stats.record(timer.read());\n\n                if (i == iterations - 1 and show_mem) {\n                    final_mem = tb.getArenaAllocatedBytes();\n                }\n            }\n\n            const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n                const mem = try allocator.alloc(MemStat, 1);\n                mem[0] = .{ .name = \"TB\", .bytes = final_mem };\n                break :blk mem;\n            } else null;\n\n            try results.append(allocator, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = mem_stats,\n            });\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n\nfn benchWrap(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    text: []const u8,\n    wrap_width: u32,\n    wrap_mode: WrapMode,\n    iterations: usize,\n    show_mem: bool,\n) !BenchResult {\n    var stats = BenchStats{};\n    var final_tb_mem: usize = 0;\n    var final_view_mem: usize = 0;\n    const link_pool = link.initGlobalLinkPool(allocator);\n\n    for (0..iterations) |i| {\n        var tb = try UnifiedTextBuffer.init(allocator, pool, link_pool, .unicode);\n        defer tb.deinit();\n\n        try tb.setText(text);\n\n        var view = try UnifiedTextBufferView.init(allocator, tb);\n        defer view.deinit();\n\n        view.setWrapMode(wrap_mode);\n\n        var timer = try std.time.Timer.start();\n        view.setWrapWidth(wrap_width);\n        const count = view.getVirtualLineCount();\n        stats.record(timer.read());\n        _ = count;\n\n        if (i == iterations - 1 and show_mem) {\n            final_tb_mem = tb.getArenaAllocatedBytes();\n            final_view_mem = view.getArenaAllocatedBytes();\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem = try allocator.alloc(MemStat, 2);\n        mem[0] = .{ .name = \"TB\", .bytes = final_tb_mem };\n        mem[1] = .{ .name = \"View\", .bytes = final_view_mem };\n        break :blk mem;\n    } else null;\n\n    return .{\n        .name = \"\",\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    };\n}\n\nfn benchMeasureForDimensionsLayout(\n    allocator: std.mem.Allocator,\n    pool: *gp.GraphemePool,\n    text: []const u8,\n    streaming: bool,\n    measure_width: u32,\n    layout_passes: usize,\n    iterations: usize,\n    show_mem: bool,\n) !BenchResult {\n    const steps: usize = 200;\n\n    var stats = BenchStats{};\n    var final_tb_mem: usize = 0;\n    var final_view_mem: usize = 0;\n    const link_pool = link.initGlobalLinkPool(allocator);\n\n    const token = \"token \";\n    const newline = \"\\n\";\n    const newline_stride: usize = 20;\n\n    for (0..iterations) |i| {\n        var tb = try UnifiedTextBuffer.init(allocator, pool, link_pool, .unicode);\n        defer tb.deinit();\n\n        try tb.setText(text);\n\n        var view = try UnifiedTextBufferView.init(allocator, tb);\n        defer view.deinit();\n\n        view.setWrapMode(.word);\n\n        var token_mem_id: u8 = 0;\n        var newline_mem_id: u8 = 0;\n        if (streaming) {\n            token_mem_id = try tb.registerMemBuffer(token, false);\n            newline_mem_id = try tb.registerMemBuffer(newline, false);\n        }\n\n        var timer = try std.time.Timer.start();\n        for (0..steps) |step| {\n            if (streaming) {\n                try tb.appendFromMemId(token_mem_id);\n                if ((step + 1) % newline_stride == 0) {\n                    try tb.appendFromMemId(newline_mem_id);\n                }\n            }\n\n            // Simulate Yoga's repeated measure calls within a single layout pass.\n            for (0..layout_passes) |_| {\n                _ = try view.measureForDimensions(measure_width, 24);\n            }\n        }\n        stats.record(timer.read());\n\n        if (i == iterations - 1 and show_mem) {\n            final_tb_mem = tb.getArenaAllocatedBytes();\n            final_view_mem = view.getArenaAllocatedBytes();\n        }\n    }\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem = try allocator.alloc(MemStat, 2);\n        mem[0] = .{ .name = \"TB\", .bytes = final_tb_mem };\n        mem[1] = .{ .name = \"View\", .bytes = final_view_mem };\n        break :blk mem;\n    } else null;\n\n    return .{\n        .name = \"\",\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    };\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    // Global pool and unicode data are initialized once in bench.zig\n    const pool = gp.initGlobalPool(allocator);\n\n    var all_results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer all_results.deinit(allocator);\n\n    const iterations: usize = 10;\n\n    // Run setText benchmarks\n    const setText_results = try benchSetText(allocator, pool, iterations, show_mem, bench_filter);\n    try all_results.appendSlice(allocator, setText_results);\n\n    var text_multiline: ?[]u8 = null;\n    defer if (text_multiline) |text| allocator.free(text);\n    var text_singleline: ?[]u8 = null;\n    defer if (text_singleline) |text| allocator.free(text);\n    const multiline_stats = computeLargeTextStats(5000, 1 * 1024 * 1024);\n    const multiline_mb = @as(f64, @floatFromInt(multiline_stats.bytes)) / (1024.0 * 1024.0);\n\n    // Run measureForDimensions benchmarks\n    const layout_passes: usize = 3;\n    const wrap_width: u32 = 80;\n    const measure_scenarios = [_]struct {\n        label: []const u8,\n        streaming: bool,\n        width: u32,\n    }{\n        .{ .label = \"layout streaming wrap\", .streaming = true, .width = wrap_width },\n        .{ .label = \"layout streaming intrinsic\", .streaming = true, .width = 0 },\n        .{ .label = \"layout static wrap\", .streaming = false, .width = wrap_width },\n    };\n\n    for (measure_scenarios) |scenario| {\n        const bench_name = try std.fmt.allocPrint(\n            allocator,\n            \"TextBufferView measureForDimensions ({s}, {d:.2} MiB)\",\n            .{ scenario.label, multiline_mb },\n        );\n\n        if (!bench_utils.matchesBenchFilter(bench_name, bench_filter)) {\n            allocator.free(bench_name);\n            continue;\n        }\n\n        if (text_multiline == null) {\n            text_multiline = try generateLargeText(allocator, 5000, 1 * 1024 * 1024);\n        }\n\n        var bench_result = try benchMeasureForDimensionsLayout(\n            allocator,\n            pool,\n            text_multiline.?,\n            scenario.streaming,\n            scenario.width,\n            layout_passes,\n            iterations,\n            show_mem,\n        );\n        bench_result.name = bench_name;\n\n        try all_results.append(allocator, bench_result);\n    }\n\n    // Test wrapping scenarios\n    const scenarios = [_]struct {\n        width: u32,\n        mode: WrapMode,\n        mode_str: []const u8,\n        single_line: bool,\n    }{\n        .{ .width = 40, .mode = .char, .mode_str = \"char\", .single_line = false },\n        .{ .width = 80, .mode = .char, .mode_str = \"char\", .single_line = false },\n        .{ .width = 120, .mode = .char, .mode_str = \"char\", .single_line = false },\n        .{ .width = 40, .mode = .word, .mode_str = \"word\", .single_line = false },\n        .{ .width = 80, .mode = .word, .mode_str = \"word\", .single_line = false },\n        .{ .width = 120, .mode = .word, .mode_str = \"word\", .single_line = false },\n        .{ .width = 40, .mode = .char, .mode_str = \"char\", .single_line = true },\n        .{ .width = 80, .mode = .char, .mode_str = \"char\", .single_line = true },\n        .{ .width = 120, .mode = .char, .mode_str = \"char\", .single_line = true },\n        .{ .width = 40, .mode = .word, .mode_str = \"word\", .single_line = true },\n        .{ .width = 80, .mode = .word, .mode_str = \"word\", .single_line = true },\n        .{ .width = 120, .mode = .word, .mode_str = \"word\", .single_line = true },\n    };\n\n    for (scenarios) |scenario| {\n        if (scenario.single_line) {\n            if (text_singleline == null) {\n                text_singleline = try generateLargeTextSingleLine(allocator, 2 * 1024 * 1024);\n            }\n        } else {\n            if (text_multiline == null) {\n                text_multiline = try generateLargeText(allocator, 5000, 1 * 1024 * 1024);\n            }\n        }\n        const text = if (scenario.single_line) text_singleline.? else text_multiline.?;\n        const line_type = if (scenario.single_line) \"single\" else \"multi\";\n\n        const bench_name = try std.fmt.allocPrint(allocator, \"TextBufferView wrap ({s}, width={d}, {s}-line)\", .{\n            scenario.mode_str,\n            scenario.width,\n            line_type,\n        });\n\n        if (!bench_utils.matchesBenchFilter(bench_name, bench_filter)) {\n            allocator.free(bench_name);\n            continue;\n        }\n\n        var bench_result = try benchWrap(\n            allocator,\n            pool,\n            text,\n            scenario.width,\n            scenario.mode,\n            iterations,\n            show_mem,\n        );\n        bench_result.name = bench_name;\n\n        try all_results.append(allocator, bench_result);\n    }\n\n    return try all_results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/text-chunk-graphemes_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst seg_mod = @import(\"../text-buffer-segment.zig\");\nconst mem_registry_mod = @import(\"../mem-registry.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst utf8 = @import(\"../utf8.zig\");\n\nconst TextChunk = seg_mod.TextChunk;\nconst MemRegistry = mem_registry_mod.MemRegistry;\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\nconst MemStat = bench_utils.MemStat;\n\npub const benchName = \"TextChunk getGraphemes\";\n\nconst TextType = enum { ascii, mixed, heavy_unicode };\n\nfn generateTestText(allocator: std.mem.Allocator, size: usize, text_type: TextType) ![]u8 {\n    var buffer: std.ArrayListUnmanaged(u8) = .{};\n    errdefer buffer.deinit(allocator);\n\n    switch (text_type) {\n        .ascii => {\n            // Pure ASCII text with tabs\n            const patterns = [_][]const u8{\n                \"The quick brown fox jumps over the lazy dog. \",\n                \"Lorem ipsum dolor sit amet, consectetur elit. \",\n                \"function test() {\\n\\tconst x = 10;\\n\\treturn x;\\n}\\n\",\n                \"Programming: Rust, Zig, Go, Python, JavaScript. \",\n            };\n            var pos: usize = 0;\n            while (pos < size) {\n                const pattern = patterns[pos % patterns.len];\n                const to_add = @min(pattern.len, size - pos);\n                try buffer.appendSlice(allocator, pattern[0..to_add]);\n                pos += to_add;\n            }\n        },\n        .mixed => {\n            // Mix of ASCII and Unicode (realistic code/text)\n            const patterns = [_][]const u8{\n                \"Hello, 世界! Unicode test. \",\n                \"Mixed: ASCII 中文 emoji 🌍 text. \",\n                \"Code: const x = 10; // comment\\n\",\n                \"Αυτό είναι ελληνικό. Это русский. \",\n                \"Numbers: 12345 symbols: !@#$% \",\n                \"\\tTab\\tseparated\\tvalues\\there. \",\n            };\n            var pos: usize = 0;\n            while (pos < size) {\n                const pattern = patterns[pos % patterns.len];\n                const to_add = @min(pattern.len, size - pos);\n                try buffer.appendSlice(allocator, pattern[0..to_add]);\n                pos += to_add;\n            }\n        },\n        .heavy_unicode => {\n            // Heavy Unicode with emojis, combining marks, and wide chars\n            const patterns = [_][]const u8{\n                \"世界中文字符測試文本。\",\n                \"こんにちは、日本語テキスト。\",\n                \"🌍🎉🚀🔥💻✨🌟⭐\",\n                \"👋🏿👩‍🚀🇺🇸❤️\",\n                \"café\\u{0301} naïve résumé\",\n                \"Ελληνικά Русский العربية\",\n            };\n            var pos: usize = 0;\n            while (pos < size) {\n                const pattern = patterns[pos % patterns.len];\n                const to_add = @min(pattern.len, size - pos);\n                try buffer.appendSlice(allocator, pattern[0..to_add]);\n                pos += to_add;\n            }\n        },\n    }\n\n    return try buffer.toOwnedSlice(allocator);\n}\n\nfn benchGetGraphemes(\n    allocator: std.mem.Allocator,\n    size: usize,\n    text_type: TextType,\n    iterations: usize,\n    show_mem: bool,\n) !BenchResult {\n    // Generate test text\n    const text = try generateTestText(allocator, size, text_type);\n    defer allocator.free(text);\n\n    // Create memory registry\n    var registry = MemRegistry.init(allocator);\n    defer registry.deinit();\n\n    const mem_id = try registry.register(text, false);\n\n    // Determine if ASCII-only\n    const is_ascii = switch (text_type) {\n        .ascii => true,\n        else => false,\n    };\n\n    // Create TextChunk\n    // Width is approximate - clamped to u16 max\n    const approx_width: u16 = @intCast(@min(text.len, std.math.maxInt(u16)));\n    var chunk = TextChunk{\n        .mem_id = mem_id,\n        .byte_start = 0,\n        .byte_end = @intCast(text.len),\n        .width = approx_width,\n        .flags = if (is_ascii) TextChunk.Flags.ASCII_ONLY else 0,\n    };\n\n    var stats = BenchStats{};\n    var grapheme_count: usize = 0;\n    var final_mem: usize = 0;\n\n    for (0..iterations) |i| {\n        // Create a fresh arena for each iteration\n        var arena = std.heap.ArenaAllocator.init(allocator);\n        defer arena.deinit();\n        const arena_alloc = arena.allocator();\n\n        // Clear cached graphemes\n        chunk.graphemes = null;\n\n        var timer = try std.time.Timer.start();\n        const graphemes = try chunk.getGraphemes(\n            &registry,\n            arena_alloc,\n            4, // tab width\n            .unicode,\n        );\n        stats.record(timer.read());\n\n        if (i == 0) {\n            grapheme_count = graphemes.len;\n        }\n\n        if (i == iterations - 1 and show_mem) {\n            // Estimate memory used for grapheme storage\n            final_mem = graphemes.len * @sizeOf(seg_mod.GraphemeInfo);\n        }\n    }\n\n    const type_str = switch (text_type) {\n        .ascii => \"ASCII\",\n        .mixed => \"Mixed\",\n        .heavy_unicode => \"Heavy Unicode\",\n    };\n\n    const name = try std.fmt.allocPrint(\n        allocator,\n        \"getGraphemes {s} ({d} bytes, {d} graphemes)\",\n        .{ type_str, size, grapheme_count },\n    );\n\n    const mem_stats: ?[]const MemStat = if (show_mem) blk: {\n        const mem_stat_slice = try allocator.alloc(MemStat, 1);\n        mem_stat_slice[0] = .{ .name = \"Graphemes\", .bytes = final_mem };\n        break :blk mem_stat_slice;\n    } else null;\n\n    return BenchResult{\n        .name = name,\n        .min_ns = stats.min_ns,\n        .avg_ns = stats.avg(),\n        .max_ns = stats.max_ns,\n        .total_ns = stats.total_ns,\n        .iterations = iterations,\n        .mem_stats = mem_stats,\n    };\n}\n\nfn computeBenchName(allocator: std.mem.Allocator, size: usize, text_type: TextType) ![]const u8 {\n    var temp_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n    defer temp_arena.deinit();\n    const temp_alloc = temp_arena.allocator();\n\n    const text = try generateTestText(temp_alloc, size, text_type);\n\n    var registry = MemRegistry.init(temp_alloc);\n    defer registry.deinit();\n\n    const mem_id = try registry.register(text, false);\n    const is_ascii = switch (text_type) {\n        .ascii => true,\n        else => false,\n    };\n    const approx_width: u16 = @intCast(@min(text.len, std.math.maxInt(u16)));\n    var chunk = TextChunk{\n        .mem_id = mem_id,\n        .byte_start = 0,\n        .byte_end = @intCast(text.len),\n        .width = approx_width,\n        .flags = if (is_ascii) TextChunk.Flags.ASCII_ONLY else 0,\n    };\n\n    const graphemes = try chunk.getGraphemes(\n        &registry,\n        temp_alloc,\n        4, // tab width\n        .unicode,\n    );\n\n    const type_str = switch (text_type) {\n        .ascii => \"ASCII\",\n        .mixed => \"Mixed\",\n        .heavy_unicode => \"Heavy Unicode\",\n    };\n\n    return try std.fmt.allocPrint(\n        allocator,\n        \"getGraphemes {s} ({d} bytes, {d} graphemes)\",\n        .{ type_str, size, graphemes.len },\n    );\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    // Global pool and unicode data are initialized once in bench.zig\n    _ = gp.initGlobalPool(allocator);\n\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(allocator);\n\n    const iterations: usize = 100;\n\n    // Test different chunk sizes: 100B, 1KB, 4KB, 16KB, 64KB\n    const sizes = [_]usize{ 100, 1024, 4 * 1024, 16 * 1024, 64 * 1024 };\n    const text_types = [_]TextType{ .ascii, .mixed, .heavy_unicode };\n\n    if (bench_filter == null) {\n        for (text_types) |text_type| {\n            for (sizes) |size| {\n                const result = try benchGetGraphemes(\n                    allocator,\n                    size,\n                    text_type,\n                    iterations,\n                    show_mem,\n                );\n                try results.append(allocator, result);\n            }\n        }\n    } else {\n        for (text_types) |text_type| {\n            for (sizes) |size| {\n                const name = try computeBenchName(allocator, size, text_type);\n                if (!bench_utils.matchesBenchFilter(name, bench_filter)) {\n                    allocator.free(name);\n                    continue;\n                }\n\n                var result = try benchGetGraphemes(\n                    allocator,\n                    size,\n                    text_type,\n                    iterations,\n                    show_mem,\n                );\n                allocator.free(result.name);\n                result.name = name;\n                try results.append(allocator, result);\n            }\n        }\n    }\n\n    return try results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench/utf8_bench.zig",
    "content": "const std = @import(\"std\");\nconst bench_utils = @import(\"../bench-utils.zig\");\nconst utf8 = @import(\"../utf8.zig\");\n\nconst BenchResult = bench_utils.BenchResult;\nconst BenchStats = bench_utils.BenchStats;\n\npub const benchName = \"UTF-8 Operations\";\n\n// Test data generators\nfn generateAsciiText(allocator: std.mem.Allocator, length: usize) ![]const u8 {\n    const text = try allocator.alloc(u8, length);\n    for (text, 0..) |*c, i| {\n        // Generate printable ASCII (32-126)\n        c.* = @as(u8, @intCast(32 + (i % 95)));\n    }\n    return text;\n}\n\nfn generateMixedText(allocator: std.mem.Allocator, length: usize) ![]const u8 {\n    var text: std.ArrayListUnmanaged(u8) = .{};\n    errdefer text.deinit(allocator);\n    var i: usize = 0;\n    while (text.items.len < length) : (i += 1) {\n        if (i % 4 == 0) {\n            try text.appendSlice(allocator, \"世\");\n        } else if (i % 4 == 1) {\n            try text.appendSlice(allocator, \"😀\");\n        } else {\n            try text.append(allocator, @as(u8, @intCast(32 + (i % 95))));\n        }\n    }\n    return text.toOwnedSlice(allocator);\n}\n\nfn generateUnicodeHeavyText(allocator: std.mem.Allocator, length: usize) ![]const u8 {\n    var text: std.ArrayListUnmanaged(u8) = .{};\n    errdefer text.deinit(allocator);\n    var i: usize = 0;\n    while (text.items.len < length) : (i += 1) {\n        if (i % 3 == 0) {\n            try text.appendSlice(allocator, \"世界\");\n        } else if (i % 3 == 1) {\n            try text.appendSlice(allocator, \"😀🎉\");\n        } else {\n            try text.appendSlice(allocator, \"Ñoño\");\n        }\n    }\n    return text.toOwnedSlice(allocator);\n}\n\n// Benchmark isAsciiOnly\nfn benchIsAsciiOnly(\n    results_alloc: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(results_alloc);\n\n    // Small ASCII text (1KB)\n    {\n        const name = \"isAsciiOnly: ASCII text (1KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.isAsciiOnly(text);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Large ASCII text (100KB)\n    {\n        const name = \"isAsciiOnly: ASCII text (100KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 100 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.isAsciiOnly(text);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Very large ASCII text (1MB)\n    {\n        const name = \"isAsciiOnly: ASCII text (1MB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 1024 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.isAsciiOnly(text);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Mixed text (10KB)\n    {\n        const name = \"isAsciiOnly: Mixed text (10KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateMixedText(temp.allocator(), 10 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.isAsciiOnly(text);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(results_alloc);\n}\n\n// Benchmark findLineBreaks\nfn benchFindLineBreaks(\n    results_alloc: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(results_alloc);\n\n    // Text with LF breaks\n    {\n        const name = \"findLineBreaks: 100 LF lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const alloc = temp.allocator();\n\n            var text: std.ArrayListUnmanaged(u8) = .{};\n            for (0..100) |_| {\n                try text.appendSlice(alloc, \"This is a line of text that ends with a newline character.\\n\");\n            }\n            const test_text = text.items;\n\n            var line_result = utf8.LineBreakResult.init(alloc);\n            defer line_result.deinit();\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                try utf8.findLineBreaks(test_text, &line_result);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Text with CRLF breaks\n    {\n        const name = \"findLineBreaks: 100 CRLF lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const alloc = temp.allocator();\n\n            var text: std.ArrayListUnmanaged(u8) = .{};\n            for (0..100) |_| {\n                try text.appendSlice(alloc, \"This is a line of text that ends with CRLF.\\r\\n\");\n            }\n            const test_text = text.items;\n\n            var line_result = utf8.LineBreakResult.init(alloc);\n            defer line_result.deinit();\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                try utf8.findLineBreaks(test_text, &line_result);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Large text with many lines\n    {\n        const name = \"findLineBreaks: 1000 short lines\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const alloc = temp.allocator();\n\n            var text: std.ArrayListUnmanaged(u8) = .{};\n            for (0..1000) |_| {\n                try text.appendSlice(alloc, \"Short line\\n\");\n            }\n            const test_text = text.items;\n\n            var line_result = utf8.LineBreakResult.init(alloc);\n            defer line_result.deinit();\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                try utf8.findLineBreaks(test_text, &line_result);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(results_alloc);\n}\n\n// Benchmark findWrapBreaks\nfn benchFindWrapBreaks(\n    results_alloc: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(results_alloc);\n\n    // ASCII text\n    {\n        const name = \"findWrapBreaks: ASCII (10KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const alloc = temp.allocator();\n            const text = try generateAsciiText(alloc, 10 * 1024);\n\n            var wrap_result = utf8.WrapBreakResult.init(alloc);\n            defer wrap_result.deinit();\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                try utf8.findWrapBreaks(text, &wrap_result, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Mixed text\n    {\n        const name = \"findWrapBreaks: Mixed (10KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const alloc = temp.allocator();\n            const text = try generateMixedText(alloc, 10 * 1024);\n\n            var wrap_result = utf8.WrapBreakResult.init(alloc);\n            defer wrap_result.deinit();\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                try utf8.findWrapBreaks(text, &wrap_result, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(results_alloc);\n}\n\n// Benchmark findWrapPosByWidth\nfn benchFindWrapPosByWidth(\n    results_alloc: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(results_alloc);\n\n    // ASCII text, narrow width\n    {\n        const name = \"findWrapPosByWidth: ASCII 1KB, width=40\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.findWrapPosByWidth(text, 40, 4, true, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // ASCII text, wide width\n    {\n        const name = \"findWrapPosByWidth: ASCII 1KB, width=120\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.findWrapPosByWidth(text, 120, 4, true, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Mixed text\n    {\n        const name = \"findWrapPosByWidth: Mixed 1KB, width=80\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateMixedText(temp.allocator(), 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.findWrapPosByWidth(text, 80, 4, false, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Unicode heavy text\n    {\n        const name = \"findWrapPosByWidth: Unicode 1KB, width=80\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateUnicodeHeavyText(temp.allocator(), 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.findWrapPosByWidth(text, 80, 4, false, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(results_alloc);\n}\n\n// Benchmark findPosByWidth\nfn benchFindPosByWidth(\n    results_alloc: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(results_alloc);\n\n    // ASCII text, find middle\n    {\n        const name = \"findPosByWidth: ASCII 1KB, target=500\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.findPosByWidth(text, 500, 4, true, true, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Large ASCII text, find near end\n    {\n        const name = \"findPosByWidth: ASCII 100KB, target=90000\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 100 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.findPosByWidth(text, 90000, 4, true, true, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Mixed text\n    {\n        const name = \"findPosByWidth: Mixed 10KB, target=5000\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateMixedText(temp.allocator(), 10 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.findPosByWidth(text, 5000, 4, false, true, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(results_alloc);\n}\n\n// Benchmark calculateTextWidth\nfn benchCalculateTextWidth(\n    results_alloc: std.mem.Allocator,\n    iterations: usize,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    var results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer results.deinit(results_alloc);\n\n    // Small ASCII text (1KB)\n    {\n        const name = \"calculateTextWidth: ASCII (1KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.calculateTextWidth(text, 4, true, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Large ASCII text (100KB)\n    {\n        const name = \"calculateTextWidth: ASCII (100KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 100 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.calculateTextWidth(text, 4, true, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Very large ASCII text (1MB)\n    {\n        const name = \"calculateTextWidth: ASCII (1MB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateAsciiText(temp.allocator(), 1024 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.calculateTextWidth(text, 4, true, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // ASCII with tabs\n    {\n        const name = \"calculateTextWidth: ASCII with tabs (10KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const alloc = temp.allocator();\n\n            var text: std.ArrayListUnmanaged(u8) = .{};\n            for (0..10 * 1024) |i| {\n                if (i % 20 == 0) {\n                    try text.append(alloc, '\\t');\n                } else {\n                    try text.append(alloc, @as(u8, @intCast(32 + (i % 95))));\n                }\n            }\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.calculateTextWidth(text.items, 4, false, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Mixed text\n    {\n        const name = \"calculateTextWidth: Mixed (10KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateMixedText(temp.allocator(), 10 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.calculateTextWidth(text, 4, false, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    // Unicode heavy text\n    {\n        const name = \"calculateTextWidth: Unicode heavy (10KB)\";\n        if (bench_utils.matchesBenchFilter(name, bench_filter)) {\n            var temp = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n            defer temp.deinit();\n            const text = try generateUnicodeHeavyText(temp.allocator(), 10 * 1024);\n\n            var stats = BenchStats{};\n            for (0..iterations) |_| {\n                var timer = try std.time.Timer.start();\n                _ = utf8.calculateTextWidth(text, 4, false, .unicode);\n                stats.record(timer.read());\n            }\n\n            try results.append(results_alloc, BenchResult{\n                .name = name,\n                .min_ns = stats.min_ns,\n                .avg_ns = stats.avg(),\n                .max_ns = stats.max_ns,\n                .total_ns = stats.total_ns,\n                .iterations = iterations,\n                .mem_stats = null,\n            });\n        }\n    }\n\n    return results.toOwnedSlice(results_alloc);\n}\n\npub fn run(\n    allocator: std.mem.Allocator,\n    show_mem: bool,\n    bench_filter: ?[]const u8,\n) ![]BenchResult {\n    _ = show_mem;\n\n    var all_results: std.ArrayListUnmanaged(BenchResult) = .{};\n    errdefer all_results.deinit(allocator);\n\n    const iterations: usize = 1000;\n\n    // isAsciiOnly benchmarks\n    const ascii_only_results = try benchIsAsciiOnly(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, ascii_only_results);\n\n    // findLineBreaks benchmarks\n    const line_breaks_results = try benchFindLineBreaks(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, line_breaks_results);\n\n    // findWrapBreaks benchmarks\n    const wrap_breaks_results = try benchFindWrapBreaks(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, wrap_breaks_results);\n\n    // findWrapPosByWidth benchmarks\n    const wrap_pos_results = try benchFindWrapPosByWidth(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, wrap_pos_results);\n\n    // findPosByWidth benchmarks\n    const pos_width_results = try benchFindPosByWidth(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, pos_width_results);\n\n    // calculateTextWidth benchmarks\n    const text_width_results = try benchCalculateTextWidth(allocator, iterations, bench_filter);\n    try all_results.appendSlice(allocator, text_width_results);\n\n    return all_results.toOwnedSlice(allocator);\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench-utils.zig",
    "content": "const std = @import(\"std\");\n\npub fn matchesBenchFilter(bench_name: []const u8, filter: ?[]const u8) bool {\n    if (filter == null) return true;\n    const filter_str = filter.?;\n    if (filter_str.len == 0) return true;\n\n    var i: usize = 0;\n    while (i + filter_str.len <= bench_name.len) : (i += 1) {\n        var matches = true;\n        for (filter_str, 0..) |filter_char, j| {\n            const bench_char = bench_name[i + j];\n            const filter_lower = if (filter_char >= 'A' and filter_char <= 'Z') filter_char + 32 else filter_char;\n            const bench_lower = if (bench_char >= 'A' and bench_char <= 'Z') bench_char + 32 else bench_char;\n            if (filter_lower != bench_lower) {\n                matches = false;\n                break;\n            }\n        }\n        if (matches) return true;\n    }\n    return false;\n}\n\npub const MemStat = struct {\n    name: []const u8,\n    bytes: usize,\n};\n\npub const BenchResult = struct {\n    name: []const u8,\n    min_ns: u64,\n    avg_ns: u64,\n    max_ns: u64,\n    total_ns: u64,\n    iterations: usize,\n    mem_stats: ?[]const MemStat,\n};\n\n/// Timing statistics collected during benchmark iterations\npub const BenchStats = struct {\n    min_ns: u64 = std.math.maxInt(u64),\n    max_ns: u64 = 0,\n    total_ns: u64 = 0,\n    count: usize = 0,\n\n    pub fn record(self: *BenchStats, elapsed_ns: u64) void {\n        self.min_ns = @min(self.min_ns, elapsed_ns);\n        self.max_ns = @max(self.max_ns, elapsed_ns);\n        self.total_ns += elapsed_ns;\n        self.count += 1;\n    }\n\n    pub fn avg(self: *const BenchStats) u64 {\n        if (self.count == 0) return 0;\n        return self.total_ns / self.count;\n    }\n};\n\n/// Helper for running benchmark iterations with timing\npub const BenchRunner = struct {\n    allocator: std.mem.Allocator,\n    results: std.ArrayListUnmanaged(BenchResult),\n\n    pub fn init(allocator: std.mem.Allocator) BenchRunner {\n        return .{\n            .allocator = allocator,\n            .results = .{},\n        };\n    }\n\n    /// Add a benchmark result from collected stats\n    pub fn addResult(\n        self: *BenchRunner,\n        name: []const u8,\n        stats: BenchStats,\n        mem_stats: ?[]const MemStat,\n    ) !void {\n        try self.results.append(self.allocator, BenchResult{\n            .name = name,\n            .min_ns = stats.min_ns,\n            .avg_ns = stats.avg(),\n            .max_ns = stats.max_ns,\n            .total_ns = stats.total_ns,\n            .iterations = stats.count,\n            .mem_stats = mem_stats,\n        });\n    }\n\n    /// Convenience: run a simple benchmark with the given function\n    pub fn bench(\n        self: *BenchRunner,\n        name: []const u8,\n        iterations: usize,\n        comptime benchFn: anytype,\n        args: anytype,\n    ) !void {\n        var stats = BenchStats{};\n        var iter: usize = 0;\n        while (iter < iterations) : (iter += 1) {\n            var timer = try std.time.Timer.start();\n            @call(.auto, benchFn, args);\n            stats.record(timer.read());\n        }\n        try self.addResult(name, stats, null);\n    }\n\n    /// Get the results slice (caller owns memory via arena)\n    pub fn finish(self: *BenchRunner) ![]BenchResult {\n        return try self.results.toOwnedSlice(self.allocator);\n    }\n\n    /// Append results from another runner or slice\n    pub fn appendSlice(self: *BenchRunner, other_results: []const BenchResult) !void {\n        try self.results.appendSlice(self.allocator, other_results);\n    }\n};\n\n/// Create a stdout writer with buffer for benchmark output\npub const StdoutWriter = struct {\n    buffer: [4096]u8 = undefined,\n    writer: std.fs.File.Writer = undefined,\n\n    pub fn init() StdoutWriter {\n        var self = StdoutWriter{};\n        self.writer = std.fs.File.stdout().writer(&self.buffer);\n        return self;\n    }\n\n    pub fn interface(self: *StdoutWriter) *std.Io.Writer {\n        return &self.writer.interface;\n    }\n\n    pub fn print(self: *StdoutWriter, comptime fmt: []const u8, args: anytype) !void {\n        try self.writer.interface.print(fmt, args);\n    }\n\n    pub fn flush(self: *StdoutWriter) !void {\n        try self.writer.interface.flush();\n    }\n};\n\npub fn formatDuration(ns: u64) struct { value: f64, unit: []const u8, color: []const u8 } {\n    if (ns < 1_000) {\n        // Bright green for nanoseconds\n        return .{ .value = @as(f64, @floatFromInt(ns)), .unit = \"ns\", .color = \"\\x1b[92m\" };\n    } else if (ns < 1_000_000) {\n        // Normal green for microseconds\n        return .{ .value = @as(f64, @floatFromInt(ns)) / 1_000.0, .unit = \"us\", .color = \"\\x1b[32m\" };\n    } else if (ns < 1_000_000_000) {\n        const ms = @as(f64, @floatFromInt(ns)) / 1_000_000.0;\n        if (ms < 1.0) {\n            // Normal green for < 1ms\n            return .{ .value = ms, .unit = \"ms\", .color = \"\\x1b[32m\" };\n        } else if (ms < 3.0) {\n            // Yellow to red gradient from 1ms to 3ms\n            if (ms < 1.5) {\n                return .{ .value = ms, .unit = \"ms\", .color = \"\\x1b[33m\" }; // Yellow\n            } else if (ms < 2.0) {\n                return .{ .value = ms, .unit = \"ms\", .color = \"\\x1b[38;5;208m\" }; // Orange\n            } else if (ms < 2.5) {\n                return .{ .value = ms, .unit = \"ms\", .color = \"\\x1b[38;5;202m\" }; // Dark orange\n            } else {\n                return .{ .value = ms, .unit = \"ms\", .color = \"\\x1b[31m\" }; // Red\n            }\n        } else {\n            // Full red for >= 3ms\n            return .{ .value = ms, .unit = \"ms\", .color = \"\\x1b[31m\" };\n        }\n    } else {\n        // Red for seconds\n        return .{ .value = @as(f64, @floatFromInt(ns)) / 1_000_000_000.0, .unit = \"s\", .color = \"\\x1b[31m\" };\n    }\n}\n\npub fn formatBytes(bytes: usize) struct { value: f64, unit: []const u8 } {\n    if (bytes < 1024) {\n        return .{ .value = @as(f64, @floatFromInt(bytes)), .unit = \"B\" };\n    } else if (bytes < 1024 * 1024) {\n        return .{ .value = @as(f64, @floatFromInt(bytes)) / 1024.0, .unit = \"KiB\" };\n    } else {\n        return .{ .value = @as(f64, @floatFromInt(bytes)) / (1024.0 * 1024.0), .unit = \"MiB\" };\n    }\n}\n\npub fn printResults(writer: anytype, results: []const BenchResult) !void {\n    if (results.len == 0) return;\n\n    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    // Collect all unique memory stat names\n    var mem_stat_names: std.ArrayListUnmanaged([]const u8) = .{};\n    for (results) |result| {\n        if (result.mem_stats) |stats| {\n            for (stats) |stat| {\n                // Check if we already have this name\n                var found = false;\n                for (mem_stat_names.items) |existing_name| {\n                    if (std.mem.eql(u8, existing_name, stat.name)) {\n                        found = true;\n                        break;\n                    }\n                }\n                if (!found) {\n                    try mem_stat_names.append(allocator, stat.name);\n                }\n            }\n        }\n    }\n\n    // Calculate column widths\n    var max_name_len: usize = 20; // minimum\n    var min_col_width: usize = 3; // minimum for \"Min\"\n    var avg_col_width: usize = 3; // minimum for \"Avg\"\n    var max_col_width: usize = 3; // minimum for \"Max\"\n\n    // Create a map to store column widths for each memory stat\n    var mem_col_widths: std.ArrayListUnmanaged(usize) = .{};\n    for (mem_stat_names.items) |name| {\n        try mem_col_widths.append(allocator, name.len); // minimum is the name length\n    }\n\n    // First pass: calculate maximum widths\n    for (results) |result| {\n        if (result.name.len > max_name_len) {\n            max_name_len = result.name.len;\n        }\n\n        const min = formatDuration(result.min_ns);\n        const avg = formatDuration(result.avg_ns);\n        const max = formatDuration(result.max_ns);\n\n        var min_buf: [32]u8 = undefined;\n        const min_str = std.fmt.bufPrint(&min_buf, \"{d:.2}{s}\", .{ min.value, min.unit }) catch unreachable;\n        if (min_str.len > min_col_width) min_col_width = min_str.len;\n\n        var avg_buf: [32]u8 = undefined;\n        const avg_str = std.fmt.bufPrint(&avg_buf, \"{d:.2}{s}\", .{ avg.value, avg.unit }) catch unreachable;\n        if (avg_str.len > avg_col_width) avg_col_width = avg_str.len;\n\n        var max_buf: [32]u8 = undefined;\n        const max_str = std.fmt.bufPrint(&max_buf, \"{d:.2}{s}\", .{ max.value, max.unit }) catch unreachable;\n        if (max_str.len > max_col_width) max_col_width = max_str.len;\n\n        if (result.mem_stats) |stats| {\n            for (stats) |stat| {\n                const mem = formatBytes(stat.bytes);\n                var mem_buf: [32]u8 = undefined;\n                const mem_str = std.fmt.bufPrint(&mem_buf, \"{d:.2} {s}\", .{ mem.value, mem.unit }) catch unreachable;\n\n                // Find the index of this stat name\n                for (mem_stat_names.items, 0..) |name, i| {\n                    if (std.mem.eql(u8, name, stat.name)) {\n                        if (mem_str.len > mem_col_widths.items[i]) {\n                            mem_col_widths.items[i] = mem_str.len;\n                        }\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    // Print header\n    var total_width = max_name_len + 3 + min_col_width + 3 + avg_col_width + 3 + max_col_width;\n    for (mem_col_widths.items) |width| {\n        total_width += 3 + width;\n    }\n    try writer.writeAll(\"\\x1b[2m\");\n    try writer.splatByteAll('-', total_width);\n    try writer.writeAll(\"\\x1b[0m\\n\");\n\n    // Column headers\n    try writer.writeAll(\"\\x1b[36m\");\n    try writer.writeAll(\"Benchmark\");\n    try writer.splatByteAll(' ', max_name_len - 9);\n    try writer.writeAll(\"\\x1b[0m\\x1b[2m | \\x1b[0m\");\n\n    try writer.writeAll(\"\\x1b[36m\");\n    try writer.writeAll(\"Min\");\n    try writer.splatByteAll(' ', min_col_width - 3);\n    try writer.writeAll(\"\\x1b[0m\\x1b[2m | \\x1b[0m\");\n\n    try writer.writeAll(\"\\x1b[36m\");\n    try writer.writeAll(\"Avg\");\n    try writer.splatByteAll(' ', avg_col_width - 3);\n    try writer.writeAll(\"\\x1b[0m\\x1b[2m | \\x1b[0m\");\n\n    try writer.writeAll(\"\\x1b[36m\");\n    try writer.writeAll(\"Max\");\n    try writer.splatByteAll(' ', max_col_width - 3);\n    try writer.writeAll(\"\\x1b[0m\");\n\n    // Dynamic memory stat headers\n    for (mem_stat_names.items, 0..) |name, i| {\n        try writer.writeAll(\"\\x1b[2m | \\x1b[0m\");\n        try writer.writeAll(\"\\x1b[36m\");\n        try writer.writeAll(name);\n        if (name.len < mem_col_widths.items[i]) {\n            try writer.splatByteAll(' ', mem_col_widths.items[i] - name.len);\n        }\n        try writer.writeAll(\"\\x1b[0m\");\n    }\n\n    try writer.writeByte('\\n');\n\n    try writer.writeAll(\"\\x1b[2m\");\n    try writer.splatByteAll('-', total_width);\n    try writer.writeAll(\"\\x1b[0m\\n\");\n\n    // Print each result\n    for (results, 0..) |result, row_idx| {\n        const min = formatDuration(result.min_ns);\n        const avg = formatDuration(result.avg_ns);\n        const max = formatDuration(result.max_ns);\n\n        // Format duration strings\n        var min_buf: [32]u8 = undefined;\n        const min_str = try std.fmt.bufPrint(&min_buf, \"{d:.2}{s}\", .{ min.value, min.unit });\n\n        var avg_buf: [32]u8 = undefined;\n        const avg_str = try std.fmt.bufPrint(&avg_buf, \"{d:.2}{s}\", .{ avg.value, avg.unit });\n\n        var max_buf: [32]u8 = undefined;\n        const max_str = try std.fmt.bufPrint(&max_buf, \"{d:.2}{s}\", .{ max.value, max.unit });\n\n        if (row_idx % 2 == 1) {\n            try writer.writeAll(\"\\x1b[48;5;234m\");\n        }\n\n        // Benchmark name\n        try writer.writeAll(result.name);\n        try writer.splatByteAll(' ', max_name_len - result.name.len);\n        try writer.writeAll(\"\\x1b[2m | \\x1b[0m\");\n        if (row_idx % 2 == 1) {\n            try writer.writeAll(\"\\x1b[48;5;234m\");\n        }\n\n        // Min (right-aligned with color)\n        if (min_str.len < min_col_width) {\n            try writer.splatByteAll(' ', min_col_width - min_str.len);\n        }\n        try writer.writeAll(min.color);\n        try writer.writeAll(min_str);\n        try writer.writeAll(\"\\x1b[0m\");\n        try writer.writeAll(\"\\x1b[2m | \\x1b[0m\");\n        if (row_idx % 2 == 1) {\n            try writer.writeAll(\"\\x1b[48;5;234m\");\n        }\n\n        // Avg (right-aligned with color)\n        if (avg_str.len < avg_col_width) {\n            try writer.splatByteAll(' ', avg_col_width - avg_str.len);\n        }\n        try writer.writeAll(avg.color);\n        try writer.writeAll(avg_str);\n        try writer.writeAll(\"\\x1b[0m\");\n        try writer.writeAll(\"\\x1b[2m | \\x1b[0m\");\n        if (row_idx % 2 == 1) {\n            try writer.writeAll(\"\\x1b[48;5;234m\");\n        }\n\n        // Max (right-aligned with color)\n        if (max_str.len < max_col_width) {\n            try writer.splatByteAll(' ', max_col_width - max_str.len);\n        }\n        try writer.writeAll(max.color);\n        try writer.writeAll(max_str);\n        try writer.writeAll(\"\\x1b[0m\");\n\n        // Dynamic memory stats columns\n        for (mem_stat_names.items, 0..) |stat_name, i| {\n            try writer.writeAll(\"\\x1b[2m | \\x1b[0m\");\n            if (row_idx % 2 == 1) {\n                try writer.writeAll(\"\\x1b[48;5;234m\");\n            }\n\n            // Look for this stat in the result's memory stats\n            var found_stat: ?usize = null;\n            if (result.mem_stats) |stats| {\n                for (stats) |stat| {\n                    if (std.mem.eql(u8, stat.name, stat_name)) {\n                        found_stat = stat.bytes;\n                        break;\n                    }\n                }\n            }\n\n            if (found_stat) |bytes| {\n                const mem = formatBytes(bytes);\n                var mem_buf: [32]u8 = undefined;\n                const mem_str = std.fmt.bufPrint(&mem_buf, \"{d:.2} {s}\", .{ mem.value, mem.unit }) catch unreachable;\n\n                // Right-aligned\n                if (mem_str.len < mem_col_widths.items[i]) {\n                    try writer.splatByteAll(' ', mem_col_widths.items[i] - mem_str.len);\n                }\n                try writer.writeAll(mem_str);\n            } else {\n                // Empty column\n                try writer.splatByteAll(' ', mem_col_widths.items[i]);\n            }\n        }\n\n        if (row_idx % 2 == 1) {\n            try writer.writeAll(\"\\x1b[0m\");\n        }\n        try writer.writeByte('\\n');\n    }\n\n    try writer.writeAll(\"\\x1b[2m\");\n    try writer.splatByteAll('-', total_width);\n    try writer.writeAll(\"\\x1b[0m\\n\");\n    try writer.flush();\n}\n\nconst BenchResultsJson = struct {\n    benchmark: []const u8,\n    results: []const BenchResult,\n};\n\npub fn printResultsJson(writer: anytype, results: []const BenchResult, bench_name: []const u8) !void {\n    const output = BenchResultsJson{\n        .benchmark = bench_name,\n        .results = results,\n    };\n    try std.json.Stringify.value(output, .{ .emit_null_optional_fields = false }, writer);\n    try writer.writeByte('\\n');\n}\n"
  },
  {
    "path": "packages/core/src/zig/bench.zig",
    "content": "// Benchmark Runner CLI\n//\n// This is the main entry point for running performance benchmarks for opentui core components.\n//\n// Usage:\n//   zig build bench              - Run all benchmarks\n//   zig build bench -- --help    - Show help message with available options\n//\n// Options:\n//   --mem                   Show memory statistics after each benchmark\n//   --filter, -f NAME       Run only benchmark categories matching NAME (case-insensitive substring)\n//   --bench, -b NAME        Run only specific benchmarks matching NAME\n//   --json                  Output results in JSON format (machine-readable)\n//   --help, -h              Display help message and list available benchmarks\n//\n// Examples:\n//   zig build bench -- --mem\n//     Run all benchmarks with memory statistics\n//\n//   zig build bench -- --filter rope\n//     Run only benchmarks with \"rope\" in their name (Rope Data Structure, Rope Marker Tracking)\n//\n//   zig build bench -- -f textbuffer --mem\n//     Run TextBuffer benchmarks with memory statistics\n//\n//   zig build bench -- --filter \"edit\"\n//     Run EditBuffer Operations benchmarks\n//\n//   zig build bench -- --bench \"ASCII\"\n//     Run only benchmarks with \"ASCII\" in their name\n//\n//   zig build bench -- --json\n//     Output results in JSON format for CI integration\n//\n// Adding New Benchmarks:\n//   1. Create a new file in bench/ directory (e.g., bench/my_bench.zig)\n//   2. Export `pub const benchName = \"My Benchmark\";`\n//   3. Export `pub fn run(allocator: std.mem.Allocator, show_mem: bool, bench_filter: ?[]const u8) ![]BenchResult`\n//   4. Import the module at the top of this file\n//   5. Add an entry to the `benchmarks` array in main() with your module\n\nconst std = @import(\"std\");\nconst bench_utils = @import(\"bench-utils.zig\");\nconst gp = @import(\"grapheme.zig\");\n\n// Import all benchmark modules\nconst text_buffer_view_bench = @import(\"bench/text-buffer-view_bench.zig\");\nconst edit_buffer_bench = @import(\"bench/edit-buffer_bench.zig\");\nconst rope_bench = @import(\"bench/rope_bench.zig\");\nconst rope_markers_bench = @import(\"bench/rope-markers_bench.zig\");\nconst text_buffer_coords_bench = @import(\"bench/text-buffer-coords_bench.zig\");\nconst styled_text_bench = @import(\"bench/styled-text_bench.zig\");\nconst buffer_draw_text_buffer_bench = @import(\"bench/buffer-draw-text-buffer_bench.zig\");\nconst utf8_bench = @import(\"bench/utf8_bench.zig\");\nconst text_chunk_graphemes_bench = @import(\"bench/text-chunk-graphemes_bench.zig\");\n\nconst BenchModule = struct {\n    name: []const u8,\n    run: *const fn (std.mem.Allocator, bool, ?[]const u8) anyerror![]bench_utils.BenchResult,\n};\n\nfn matchesFilter(bench_name: []const u8, filter: ?[]const u8) bool {\n    if (filter == null) return true;\n    const filter_str = filter.?;\n    if (filter_str.len == 0) return true;\n\n    var i: usize = 0;\n    while (i + filter_str.len <= bench_name.len) : (i += 1) {\n        var matches = true;\n        for (filter_str, 0..) |filter_char, j| {\n            const bench_char = bench_name[i + j];\n            const filter_lower = if (filter_char >= 'A' and filter_char <= 'Z') filter_char + 32 else filter_char;\n            const bench_lower = if (bench_char >= 'A' and bench_char <= 'Z') bench_char + 32 else bench_char;\n            if (filter_lower != bench_lower) {\n                matches = false;\n                break;\n            }\n        }\n        if (matches) return true;\n    }\n    return false;\n}\n\npub fn main() !void {\n    var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n    defer _ = gpa.deinit();\n    const allocator = gpa.allocator();\n\n    // Initialize global pool and unicode data ONCE with base GPA allocator\n    // This ensures they persist across all benchmarks (even with arena allocators)\n    _ = gp.initGlobalPool(allocator);\n    defer gp.deinitGlobalPool();\n\n    const benchmarks = [_]BenchModule{\n        .{ .name = text_buffer_view_bench.benchName, .run = text_buffer_view_bench.run },\n        .{ .name = edit_buffer_bench.benchName, .run = edit_buffer_bench.run },\n        .{ .name = rope_bench.benchName, .run = rope_bench.run },\n        .{ .name = rope_markers_bench.benchName, .run = rope_markers_bench.run },\n        .{ .name = text_buffer_coords_bench.benchName, .run = text_buffer_coords_bench.run },\n        .{ .name = styled_text_bench.benchName, .run = styled_text_bench.run },\n        .{ .name = buffer_draw_text_buffer_bench.benchName, .run = buffer_draw_text_buffer_bench.run },\n        .{ .name = utf8_bench.benchName, .run = utf8_bench.run },\n        .{ .name = text_chunk_graphemes_bench.benchName, .run = text_chunk_graphemes_bench.run },\n    };\n\n    const args = try std.process.argsAlloc(allocator);\n    defer std.process.argsFree(allocator, args);\n\n    var show_mem = false;\n    var json_output = false;\n    var filter: ?[]const u8 = null;\n    var bench_filter: ?[]const u8 = null;\n    var i: usize = 1;\n    while (i < args.len) : (i += 1) {\n        const arg = args[i];\n        if (std.mem.eql(u8, arg, \"--mem\")) {\n            show_mem = true;\n        } else if (std.mem.eql(u8, arg, \"--json\")) {\n            json_output = true;\n        } else if (std.mem.eql(u8, arg, \"--filter\") or std.mem.eql(u8, arg, \"-f\")) {\n            if (i + 1 < args.len) {\n                i += 1;\n                filter = args[i];\n            }\n        } else if (std.mem.eql(u8, arg, \"--bench\") or std.mem.eql(u8, arg, \"-b\")) {\n            if (i + 1 < args.len) {\n                i += 1;\n                bench_filter = args[i];\n            }\n        } else if (std.mem.eql(u8, arg, \"--help\") or std.mem.eql(u8, arg, \"-h\")) {\n            var stdout_buffer: [4096]u8 = undefined;\n            var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);\n            const stdout = &stdout_writer.interface;\n            try stdout.print(\"Usage: bench [options]\\n\\n\", .{});\n            try stdout.print(\"Options:\\n\", .{});\n            try stdout.print(\"  --mem                   Show memory statistics\\n\", .{});\n            try stdout.print(\"  --json                  Output in JSON format (machine-readable)\\n\", .{});\n            try stdout.print(\"  --filter, -f NAME       Run only benchmark categories matching NAME\\n\", .{});\n            try stdout.print(\"  --bench, -b NAME        Run only specific benchmarks matching NAME\\n\", .{});\n            try stdout.print(\"  --help, -h              Show this help message\\n\\n\", .{});\n            try stdout.print(\"Available benchmarks:\\n\", .{});\n            for (benchmarks) |bench| {\n                try stdout.print(\"  - {s}\\n\", .{bench.name});\n            }\n            try stdout.flush();\n            return;\n        }\n    }\n\n    var stdout_buffer: [4096]u8 = undefined;\n    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);\n    const stdout = &stdout_writer.interface;\n\n    if (!json_output and filter != null) {\n        try stdout.print(\"Filtering benchmarks by: \\\"{s}\\\"\\n\", .{filter.?});\n    }\n    if (!json_output and bench_filter != null) {\n        try stdout.print(\"Filtering individual benchmarks by: \\\"{s}\\\"\\n\", .{bench_filter.?});\n    }\n\n    var ran_any = false;\n\n    for (benchmarks) |bench| {\n        if (!matchesFilter(bench.name, filter)) continue;\n\n        // Use arena for results only - benchmark modules manage their own temp memory\n        var results_arena = std.heap.ArenaAllocator.init(allocator);\n        defer results_arena.deinit();\n\n        const start_time = std.time.nanoTimestamp();\n        const results = try bench.run(results_arena.allocator(), show_mem, bench_filter);\n        const end_time = std.time.nanoTimestamp();\n        const elapsed_ns = end_time - start_time;\n\n        if (results.len == 0) continue;\n\n        if (!json_output) {\n            try stdout.print(\"\\n=== {s} Benchmarks ===\\n\\n\", .{bench.name});\n            try stdout.flush();\n        }\n\n        if (json_output) {\n            try bench_utils.printResultsJson(stdout, results, bench.name);\n        } else {\n            try bench_utils.printResults(stdout, results);\n            const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0;\n            try stdout.print(\"\\n  Overall time: {d:.2}ms\\n\", .{elapsed_ms});\n        }\n\n        ran_any = true;\n    }\n\n    if (!ran_any) {\n        if (!json_output) {\n            if (filter != null and bench_filter != null) {\n                try stdout.print(\n                    \"\\nNo benchmarks matched filters: category=\\\"{s}\\\", bench=\\\"{s}\\\"\\n\",\n                    .{ filter.?, bench_filter.? },\n                );\n            } else if (bench_filter != null) {\n                try stdout.print(\"\\nNo benchmarks matched bench filter: \\\"{s}\\\"\\n\", .{bench_filter.?});\n            } else if (filter != null) {\n                try stdout.print(\"\\nNo benchmarks matched filter: \\\"{s}\\\"\\n\", .{filter.?});\n            } else {\n                try stdout.print(\"\\nNo benchmarks ran.\\n\", .{});\n            }\n            try stdout.print(\"Use --help to see available benchmarks.\\n\", .{});\n        }\n        try stdout.flush();\n        return;\n    }\n\n    if (!json_output) {\n        try stdout.print(\"\\n✓ Benchmarks complete\\n\", .{});\n    }\n    try stdout.flush();\n}\n"
  },
  {
    "path": "packages/core/src/zig/buffer-methods.zig",
    "content": "const std = @import(\"std\");\nconst math = std.math;\nconst ansi = @import(\"ansi.zig\");\n\nconst RGBA = ansi.RGBA;\nconst Vec4 = @Vector(4, f32);\n\n/// Target buffer(s) for color matrix operations\n/// Uses bitwise flags: FG=1, BG=2, Both=1|2=3\npub const ColorTarget = enum(u8) {\n    FG = 1,\n    BG = 2,\n    Both = 3,\n};\n\n/// Apply 4x4 RGBA matrix to 4 pixels using SIMD\n/// matrix: 16 floats in row-major order (4x4 matrix)\n/// pixels: array of 4 RGBA values (each is [4]f32)\n/// strength: blend factor\n/// result: output array of 4 RGBA values\nfn applyMatrix4x4SIMD(matrix: *const [16]f32, r_vec: Vec4, g_vec: Vec4, b_vec: Vec4, a_vec: Vec4, strength_vec: Vec4) struct { r: Vec4, g: Vec4, b: Vec4, a: Vec4 } {\n    // Matrix multiply: new_color = M * color\n    // Each row of matrix defines the coefficients for one output channel\n    // Row 0 -> Red output, Row 1 -> Green output, Row 2 -> Blue output, Row 3 -> Alpha output\n    const new_r = r_vec * @as(Vec4, @splat(matrix[0])) + g_vec * @as(Vec4, @splat(matrix[1])) + b_vec * @as(Vec4, @splat(matrix[2])) + a_vec * @as(Vec4, @splat(matrix[3]));\n    const new_g = r_vec * @as(Vec4, @splat(matrix[4])) + g_vec * @as(Vec4, @splat(matrix[5])) + b_vec * @as(Vec4, @splat(matrix[6])) + a_vec * @as(Vec4, @splat(matrix[7]));\n    const new_b = r_vec * @as(Vec4, @splat(matrix[8])) + g_vec * @as(Vec4, @splat(matrix[9])) + b_vec * @as(Vec4, @splat(matrix[10])) + a_vec * @as(Vec4, @splat(matrix[11]));\n    const new_a = r_vec * @as(Vec4, @splat(matrix[12])) + g_vec * @as(Vec4, @splat(matrix[13])) + b_vec * @as(Vec4, @splat(matrix[14])) + a_vec * @as(Vec4, @splat(matrix[15]));\n\n    // Blend: original + (new - original) * strength\n    const out_r = r_vec + (new_r - r_vec) * strength_vec;\n    const out_g = g_vec + (new_g - g_vec) * strength_vec;\n    const out_b = b_vec + (new_b - b_vec) * strength_vec;\n    const out_a = a_vec + (new_a - a_vec) * strength_vec;\n\n    return .{ .r = out_r, .g = out_g, .b = out_b, .a = out_a };\n}\n\n/// Apply 4x4 RGBA matrix to single pixel (scalar fallback)\n/// matrix is in row-major order (4x4 matrix), where each row defines coefficients for one output channel\nfn applyMatrix4x4Scalar(matrix: *const [16]f32, r: f32, g: f32, b: f32, a: f32, strength: f32) struct { r: f32, g: f32, b: f32, a: f32 } {\n    // Row 0 -> Red output, Row 1 -> Green output, Row 2 -> Blue output, Row 3 -> Alpha output\n    const new_r = matrix[0] * r + matrix[1] * g + matrix[2] * b + matrix[3] * a;\n    const new_g = matrix[4] * r + matrix[5] * g + matrix[6] * b + matrix[7] * a;\n    const new_b = matrix[8] * r + matrix[9] * g + matrix[10] * b + matrix[11] * a;\n    const new_a = matrix[12] * r + matrix[13] * g + matrix[14] * b + matrix[15] * a;\n\n    return .{\n        .r = r + (new_r - r) * strength,\n        .g = g + (new_g - g) * strength,\n        .b = b + (new_b - b) * strength,\n        .a = a + (new_a - a) * strength,\n    };\n}\n\n/// Apply 4x4 RGBA color matrix transformation to RGBA values at specified cell coordinates.\n/// matrix: 4x4 row-major matrix (16 values) where each row corresponds to output channel:\n///   Row 0: [r->r, g->r, b->r, a->r] - coefficients for Red output\n///   Row 1: [r->g, g->g, b->g, a->g] - coefficients for Green output\n///   Row 2: [r->b, g->b, b->b, a->b] - coefficients for Blue output\n///   Row 3: [r->a, g->a, b->a, a->a] - coefficients for Alpha output (usually identity)\n/// cellMask format: [x, y, strength, x, y, strength, ...]\n/// strength: global multiplier applied to each cell's strength value (1.0 = no change)\n/// target: which buffer(s) to apply the matrix to (FG=1, BG=2, Both=3)\n/// No clamping is performed - output values may exceed [0, 1] range\npub fn colorMatrix(self: anytype, matrix: []const f32, cellMask: []const f32, strength: f32, target: ColorTarget) void {\n    if (matrix.len < 16 or cellMask.len < 3) return;\n    if (@intFromEnum(target) == 0) return;\n    if (!math.isFinite(strength)) return;\n\n    const width = self.width;\n    const height = self.height;\n    const fg = self.buffer.fg;\n    const bg = self.buffer.bg;\n\n    // Use matrix directly as 4x4\n    const mat4 = matrix[0..16].*;\n    const max_u32_f = @as(f32, @floatFromInt(std.math.maxInt(u32)));\n\n    const len = cellMask.len - (cellMask.len % 3);\n    var i: usize = 0;\n    while (i < len) : (i += 3) {\n        const x_f = cellMask[i];\n        const y_f = cellMask[i + 1];\n\n        // Skip if coordinates are negative or non-finite before conversion\n        if (x_f < 0.0 or y_f < 0.0) continue;\n        if (!math.isFinite(x_f) or !math.isFinite(y_f)) continue;\n        if (x_f > max_u32_f or y_f > max_u32_f) continue;\n\n        const x: u32 = @intFromFloat(x_f);\n        const y: u32 = @intFromFloat(y_f);\n        const cellStrength = cellMask[i + 2] * strength;\n\n        if (x >= width or y >= height) continue;\n\n        if (!math.isFinite(cellStrength)) continue;\n        if (cellStrength == 0.0) continue;\n\n        const index = y * width + x;\n\n        // Apply color matrix to foreground if target includes FG\n        if (@intFromEnum(target) & 1 != 0) {\n            const fg_result = applyMatrix4x4Scalar(&mat4, fg[index][0], fg[index][1], fg[index][2], fg[index][3], cellStrength);\n            fg[index][0] = fg_result.r;\n            fg[index][1] = fg_result.g;\n            fg[index][2] = fg_result.b;\n            fg[index][3] = fg_result.a;\n        }\n\n        // Apply color matrix to background if target includes BG\n        if (@intFromEnum(target) & 2 != 0) {\n            const bg_result = applyMatrix4x4Scalar(&mat4, bg[index][0], bg[index][1], bg[index][2], bg[index][3], cellStrength);\n            bg[index][0] = bg_result.r;\n            bg[index][1] = bg_result.g;\n            bg[index][2] = bg_result.b;\n            bg[index][3] = bg_result.a;\n        }\n    }\n}\n\n/// Apply 4x4 RGBA color matrix transformation uniformly to all pixels using SIMD.\n/// matrix: 4x4 row-major matrix (16 values) where each row corresponds to output channel:\n///   Row 0: [r->r, g->r, b->r, a->r] - coefficients for Red output\n///   Row 1: [r->g, g->g, b->g, a->g] - coefficients for Green output\n///   Row 2: [r->b, g->b, b->b, a->b] - coefficients for Blue output\n///   Row 3: [r->a, g->a, b->a, a->a] - coefficients for Alpha output (usually identity)\n/// strength: multiplier applied to matrix effect (0.0 = no effect, 1.0 = full matrix)\n/// target: which buffer(s) to apply the matrix to (FG=1, BG=2, Both=3)\n/// This uses 4-wide SIMD to process pixels in batches for maximum throughput.\n/// No clamping is performed - output values may exceed [0, 1] range\npub fn colorMatrixUniform(self: anytype, matrix: []const f32, strength: f32, target: ColorTarget) void {\n    if (matrix.len < 16 or strength == 0.0) return;\n    if (@intFromEnum(target) == 0) return;\n    if (!math.isFinite(strength)) return;\n\n    const width = self.width;\n    const height = self.height;\n    const size = width * height;\n    const fg = self.buffer.fg;\n    const bg = self.buffer.bg;\n\n    // Use matrix directly as 4x4\n    const mat4 = matrix[0..16].*;\n\n    const processFG = @intFromEnum(target) & 1 != 0;\n    const processBG = @intFromEnum(target) & 2 != 0;\n\n    // Process 4 pixels at a time using SIMD\n    const strength_vec: Vec4 = @splat(strength);\n    var i: usize = 0;\n    const simd_end = size - (size % 4);\n\n    while (i < simd_end) : (i += 4) {\n        // Process foreground if target includes FG\n        if (processFG) {\n            // Load 4 pixels' RGBA values into separate channel vectors\n            const fg_r = Vec4{ fg[i][0], fg[i + 1][0], fg[i + 2][0], fg[i + 3][0] };\n            const fg_g = Vec4{ fg[i][1], fg[i + 1][1], fg[i + 2][1], fg[i + 3][1] };\n            const fg_b = Vec4{ fg[i][2], fg[i + 1][2], fg[i + 2][2], fg[i + 3][2] };\n            const fg_a = Vec4{ fg[i][3], fg[i + 1][3], fg[i + 2][3], fg[i + 3][3] };\n\n            // Apply matrix transformation\n            const fg_result = applyMatrix4x4SIMD(&mat4, fg_r, fg_g, fg_b, fg_a, strength_vec);\n\n            // Store results back\n            inline for (0..4) |j| {\n                fg[i + j][0] = fg_result.r[j];\n                fg[i + j][1] = fg_result.g[j];\n                fg[i + j][2] = fg_result.b[j];\n                fg[i + j][3] = fg_result.a[j];\n            }\n        }\n\n        // Process background if target includes BG\n        if (processBG) {\n            const bg_r = Vec4{ bg[i][0], bg[i + 1][0], bg[i + 2][0], bg[i + 3][0] };\n            const bg_g = Vec4{ bg[i][1], bg[i + 1][1], bg[i + 2][1], bg[i + 3][1] };\n            const bg_b = Vec4{ bg[i][2], bg[i + 1][2], bg[i + 2][2], bg[i + 3][2] };\n            const bg_a = Vec4{ bg[i][3], bg[i + 1][3], bg[i + 2][3], bg[i + 3][3] };\n\n            const bg_result = applyMatrix4x4SIMD(&mat4, bg_r, bg_g, bg_b, bg_a, strength_vec);\n\n            inline for (0..4) |j| {\n                bg[i + j][0] = bg_result.r[j];\n                bg[i + j][1] = bg_result.g[j];\n                bg[i + j][2] = bg_result.b[j];\n                bg[i + j][3] = bg_result.a[j];\n            }\n        }\n    }\n\n    // Handle remaining pixels (0-3) with scalar fallback\n    while (i < size) : (i += 1) {\n        if (processFG) {\n            const fg_result = applyMatrix4x4Scalar(&mat4, fg[i][0], fg[i][1], fg[i][2], fg[i][3], strength);\n            fg[i][0] = fg_result.r;\n            fg[i][1] = fg_result.g;\n            fg[i][2] = fg_result.b;\n            fg[i][3] = fg_result.a;\n        }\n\n        if (processBG) {\n            const bg_result = applyMatrix4x4Scalar(&mat4, bg[i][0], bg[i][1], bg[i][2], bg[i][3], strength);\n            bg[i][0] = bg_result.r;\n            bg[i][1] = bg_result.g;\n            bg[i][2] = bg_result.b;\n            bg[i][3] = bg_result.a;\n        }\n    }\n}\n"
  },
  {
    "path": "packages/core/src/zig/buffer.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst ansi = @import(\"ansi.zig\");\nconst tb = @import(\"text-buffer.zig\");\nconst tbv = @import(\"text-buffer-view.zig\");\nconst edv = @import(\"editor-view.zig\");\nconst ss = @import(\"syntax-style.zig\");\nconst math = std.math;\nconst assert = std.debug.assert;\n\nconst gp = @import(\"grapheme.zig\");\nconst link = @import(\"link.zig\");\n\nconst logger = @import(\"logger.zig\");\nconst utf8 = @import(\"utf8.zig\");\nconst uucode = @import(\"uucode\");\n\npub const RGBA = ansi.RGBA;\npub const Vec3f = @Vector(3, f32);\npub const Vec4f = @Vector(4, f32);\n\nconst TextBuffer = tb.TextBuffer;\nconst TextBufferView = tbv.TextBufferView;\nconst EditorView = edv.EditorView;\n\nconst INV_255: f32 = 1.0 / 255.0;\npub const DEFAULT_SPACE_CHAR: u32 = 32;\nconst MAX_UNICODE_CODEPOINT: u32 = 0x10FFFF;\nconst BLOCK_CHAR: u32 = 0x2588; // Full block █\nconst QUADRANT_CHARS_COUNT = 16;\n\nconst GRAYSCALE_CHARS = \" .'^\\\",:;Il!i><~+_-?][}{1)(|\\\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$\";\n\npub const BorderSides = packed struct {\n    top: bool = false,\n    right: bool = false,\n    bottom: bool = false,\n    left: bool = false,\n};\n\npub const BorderCharIndex = enum(u8) {\n    topLeft = 0,\n    topRight = 1,\n    bottomLeft = 2,\n    bottomRight = 3,\n    horizontal = 4,\n    vertical = 5,\n    topT = 6,\n    bottomT = 7,\n    leftT = 8,\n    rightT = 9,\n    cross = 10,\n};\n\npub const TextSelection = struct {\n    start: u32,\n    end: u32,\n    bgColor: ?RGBA,\n    fgColor: ?RGBA,\n};\n\npub const ClipRect = struct {\n    x: i32,\n    y: i32,\n    width: u32,\n    height: u32,\n};\n\npub const BufferError = error{\n    OutOfMemory,\n    InvalidDimensions,\n    InvalidUnicode,\n    BufferTooSmall,\n};\n\npub fn rgbaToVec4f(color: RGBA) Vec4f {\n    return Vec4f{ color[0], color[1], color[2], color[3] };\n}\n\npub fn rgbaEqual(a: RGBA, b: RGBA, epsilon: f32) bool {\n    const va = rgbaToVec4f(a);\n    const vb = rgbaToVec4f(b);\n    const diff = @abs(va - vb);\n    const eps = @as(Vec4f, @splat(epsilon));\n    return @reduce(.And, diff < eps);\n}\n\npub const Cell = struct {\n    char: u32,\n    fg: RGBA,\n    bg: RGBA,\n    attributes: u32,\n};\n\nfn isRGBAWithAlpha(color: RGBA) bool {\n    return color[3] < 1.0;\n}\n\ninline fn isFullyOpaque(opacity: f32, fg: RGBA, bg: RGBA) bool {\n    return opacity == 1.0 and !isRGBAWithAlpha(fg) and !isRGBAWithAlpha(bg);\n}\n\nfn blendColors(overlay: RGBA, text: RGBA, blendBackdropColor: ?RGBA) RGBA {\n    var dest = text;\n    if (dest[3] == 0.0) {\n        if (blendBackdropColor) |backdrop| {\n            dest = backdrop;\n        }\n    }\n\n    if (overlay[3] == 1.0) {\n        return overlay;\n    }\n\n    if (dest[3] == 0.0) {\n        const alpha = overlay[3];\n        const r = overlay[0] * alpha;\n        const g = overlay[1] * alpha;\n        const b = overlay[2] * alpha;\n        if (r < 0.01 and g < 0.01 and b < 0.01) {\n            return .{ 0.0, 0.0, 0.0, 0.0 };\n        }\n        return .{ r, g, b, alpha };\n    }\n\n    const alpha = overlay[3];\n    var perceptualAlpha: f32 = undefined;\n\n    // For high alpha values (>0.8), use a more aggressive curve\n    if (alpha > 0.8) {\n        const normalizedHighAlpha = (alpha - 0.8) * 5.0;\n        const curvedHighAlpha = std.math.pow(f32, normalizedHighAlpha, 0.2);\n        perceptualAlpha = 0.8 + (curvedHighAlpha * 0.2);\n    } else {\n        perceptualAlpha = std.math.pow(f32, alpha, 0.9);\n    }\n\n    const overlayVec = Vec3f{ overlay[0], overlay[1], overlay[2] };\n    const textVec = Vec3f{ dest[0], dest[1], dest[2] };\n    const alphaSplat = @as(Vec3f, @splat(perceptualAlpha));\n    const oneMinusAlpha = @as(Vec3f, @splat(1.0 - perceptualAlpha));\n    const blended = overlayVec * alphaSplat + textVec * oneMinusAlpha;\n\n    const resultAlpha = alpha + dest[3] * (1.0 - alpha);\n\n    return .{ blended[0], blended[1], blended[2], resultAlpha };\n}\n\n/// Optimized buffer for terminal rendering\npub const OptimizedBuffer = struct {\n    buffer: struct {\n        char: []u32,\n        fg: []RGBA,\n        bg: []RGBA,\n        attributes: []u32,\n    },\n    width: u32,\n    height: u32,\n    respectAlpha: bool,\n    blendBackdropColor: ?RGBA,\n    allocator: Allocator,\n    pool: *gp.GraphemePool,\n    link_pool: *link.LinkPool,\n\n    grapheme_tracker: gp.GraphemeTracker,\n    link_tracker: link.LinkTracker,\n    width_method: utf8.WidthMethod,\n    id: []const u8,\n    scissor_stack: std.ArrayListUnmanaged(ClipRect),\n    opacity_stack: std.ArrayListUnmanaged(f32),\n\n    const InitOptions = struct {\n        respectAlpha: bool = false,\n        blendBackdropColor: ?RGBA = null,\n        pool: *gp.GraphemePool,\n        width_method: utf8.WidthMethod = .unicode,\n        id: []const u8 = \"unnamed buffer\",\n        link_pool: ?*link.LinkPool = null,\n    };\n\n    pub fn init(allocator: Allocator, width: u32, height: u32, options: InitOptions) BufferError!*OptimizedBuffer {\n        if (width == 0 or height == 0) {\n            logger.warn(\"OptimizedBuffer.init: Invalid dimensions {}x{}\", .{ width, height });\n            return BufferError.InvalidDimensions;\n        }\n\n        const self = allocator.create(OptimizedBuffer) catch return BufferError.OutOfMemory;\n        errdefer allocator.destroy(self);\n\n        const size = width * height;\n\n        const owned_id = allocator.dupe(u8, options.id) catch return BufferError.OutOfMemory;\n        errdefer allocator.free(owned_id);\n\n        var scissor_stack: std.ArrayListUnmanaged(ClipRect) = .{};\n        errdefer scissor_stack.deinit(allocator);\n\n        var opacity_stack: std.ArrayListUnmanaged(f32) = .{};\n        errdefer opacity_stack.deinit(allocator);\n\n        const lp = options.link_pool orelse link.initGlobalLinkPool(allocator);\n        const char_buffer = allocator.alloc(u32, size) catch return BufferError.OutOfMemory;\n        errdefer allocator.free(char_buffer);\n\n        const fg_buffer = allocator.alloc(RGBA, size) catch return BufferError.OutOfMemory;\n        errdefer allocator.free(fg_buffer);\n\n        const bg_buffer = allocator.alloc(RGBA, size) catch return BufferError.OutOfMemory;\n        errdefer allocator.free(bg_buffer);\n\n        const attributes_buffer = allocator.alloc(u32, size) catch return BufferError.OutOfMemory;\n        errdefer allocator.free(attributes_buffer);\n\n        self.* = .{\n            .buffer = .{\n                .char = char_buffer,\n                .fg = fg_buffer,\n                .bg = bg_buffer,\n                .attributes = attributes_buffer,\n            },\n            .width = width,\n            .height = height,\n            .respectAlpha = options.respectAlpha,\n            .blendBackdropColor = options.blendBackdropColor,\n            .allocator = allocator,\n            .pool = options.pool,\n            .link_pool = lp,\n            .grapheme_tracker = gp.GraphemeTracker.init(allocator, options.pool),\n            .link_tracker = link.LinkTracker.init(allocator, lp),\n            .width_method = options.width_method,\n            .id = owned_id,\n            .scissor_stack = scissor_stack,\n            .opacity_stack = opacity_stack,\n        };\n\n        @memset(self.buffer.char, 0);\n        @memset(self.buffer.fg, .{ 0.0, 0.0, 0.0, 0.0 });\n        @memset(self.buffer.bg, .{ 0.0, 0.0, 0.0, 0.0 });\n        @memset(self.buffer.attributes, 0);\n\n        return self;\n    }\n\n    pub fn getCharPtr(self: *OptimizedBuffer) [*]u32 {\n        return self.buffer.char.ptr;\n    }\n\n    pub fn getFgPtr(self: *OptimizedBuffer) [*]RGBA {\n        return self.buffer.fg.ptr;\n    }\n\n    pub fn getBgPtr(self: *OptimizedBuffer) [*]RGBA {\n        return self.buffer.bg.ptr;\n    }\n\n    pub fn getAttributesPtr(self: *OptimizedBuffer) [*]u32 {\n        return self.buffer.attributes.ptr;\n    }\n\n    pub fn deinit(self: *OptimizedBuffer) void {\n        self.opacity_stack.deinit(self.allocator);\n        self.scissor_stack.deinit(self.allocator);\n        self.link_tracker.deinit();\n        self.grapheme_tracker.deinit();\n        self.allocator.free(self.buffer.char);\n        self.allocator.free(self.buffer.fg);\n        self.allocator.free(self.buffer.bg);\n        self.allocator.free(self.buffer.attributes);\n        self.allocator.free(self.id);\n        self.allocator.destroy(self);\n    }\n\n    pub fn getCurrentScissorRect(self: *const OptimizedBuffer) ?ClipRect {\n        if (self.scissor_stack.items.len == 0) return null;\n        return self.scissor_stack.items[self.scissor_stack.items.len - 1];\n    }\n\n    pub fn isPointInScissor(self: *const OptimizedBuffer, x: i32, y: i32) bool {\n        const scissor = self.getCurrentScissorRect() orelse return true;\n        return x >= scissor.x and x < scissor.x + @as(i32, @intCast(scissor.width)) and\n            y >= scissor.y and y < scissor.y + @as(i32, @intCast(scissor.height));\n    }\n\n    pub fn isRectInScissor(self: *const OptimizedBuffer, x: i32, y: i32, width: u32, height: u32) bool {\n        const scissor = self.getCurrentScissorRect() orelse return true;\n\n        const rect_end_x = x + @as(i32, @intCast(width));\n        const rect_end_y = y + @as(i32, @intCast(height));\n        const scissor_end_x = scissor.x + @as(i32, @intCast(scissor.width));\n        const scissor_end_y = scissor.y + @as(i32, @intCast(scissor.height));\n\n        return !(x >= scissor_end_x or rect_end_x <= scissor.x or\n            y >= scissor_end_y or rect_end_y <= scissor.y);\n    }\n\n    pub fn clipRectToScissor(self: *const OptimizedBuffer, x: i32, y: i32, width: u32, height: u32) ?ClipRect {\n        const scissor = self.getCurrentScissorRect() orelse return ClipRect{\n            .x = x,\n            .y = y,\n            .width = width,\n            .height = height,\n        };\n\n        const rect_end_x = x + @as(i32, @intCast(width));\n        const rect_end_y = y + @as(i32, @intCast(height));\n        const scissor_end_x = scissor.x + @as(i32, @intCast(scissor.width));\n        const scissor_end_y = scissor.y + @as(i32, @intCast(scissor.height));\n\n        const intersect_x = @max(x, scissor.x);\n        const intersect_y = @max(y, scissor.y);\n        const intersect_end_x = @min(rect_end_x, scissor_end_x);\n        const intersect_end_y = @min(rect_end_y, scissor_end_y);\n\n        if (intersect_x >= intersect_end_x or intersect_y >= intersect_end_y) {\n            return null; // No intersection\n        }\n\n        return ClipRect{\n            .x = intersect_x,\n            .y = intersect_y,\n            .width = @intCast(intersect_end_x - intersect_x),\n            .height = @intCast(intersect_end_y - intersect_y),\n        };\n    }\n\n    pub fn pushScissorRect(self: *OptimizedBuffer, x: i32, y: i32, width: u32, height: u32) !void {\n        var rect = ClipRect{\n            .x = x,\n            .y = y,\n            .width = width,\n            .height = height,\n        };\n\n        // Intersect with current scissor (if any) so nested scissor rects always clip to parents.\n        if (self.getCurrentScissorRect() != null) {\n            const intersect = self.clipRectToScissor(rect.x, rect.y, rect.width, rect.height);\n            if (intersect) |clipped| {\n                rect = clipped;\n            } else {\n                // Completely outside current scissor; push a degenerate rect so nothing renders.\n                rect = ClipRect{ .x = 0, .y = 0, .width = 0, .height = 0 };\n            }\n        }\n\n        try self.scissor_stack.append(self.allocator, rect);\n    }\n\n    pub fn popScissorRect(self: *OptimizedBuffer) void {\n        if (self.scissor_stack.items.len > 0) {\n            _ = self.scissor_stack.pop();\n        }\n    }\n\n    pub fn clearScissorRects(self: *OptimizedBuffer) void {\n        self.scissor_stack.clearRetainingCapacity();\n    }\n\n    /// Get the current effective opacity (product of all stacked opacities)\n    pub fn getCurrentOpacity(self: *const OptimizedBuffer) f32 {\n        if (self.opacity_stack.items.len == 0) return 1.0;\n        return self.opacity_stack.items[self.opacity_stack.items.len - 1];\n    }\n\n    /// Push an opacity value onto the stack. The effective opacity is multiplied with the current.\n    pub fn pushOpacity(self: *OptimizedBuffer, opacity: f32) !void {\n        const current = self.getCurrentOpacity();\n        const effective = current * std.math.clamp(opacity, 0.0, 1.0);\n        try self.opacity_stack.append(self.allocator, effective);\n    }\n\n    /// Pop an opacity value from the stack\n    pub fn popOpacity(self: *OptimizedBuffer) void {\n        if (self.opacity_stack.items.len > 0) {\n            _ = self.opacity_stack.pop();\n        }\n    }\n\n    /// Clear all opacity values from the stack\n    pub fn clearOpacity(self: *OptimizedBuffer) void {\n        self.opacity_stack.clearRetainingCapacity();\n    }\n\n    pub fn resize(self: *OptimizedBuffer, width: u32, height: u32) BufferError!void {\n        if (self.width == width and self.height == height) return;\n        if (width == 0 or height == 0) return BufferError.InvalidDimensions;\n\n        const size = width * height;\n\n        self.buffer.char = self.allocator.realloc(self.buffer.char, size) catch return BufferError.OutOfMemory;\n        self.buffer.fg = self.allocator.realloc(self.buffer.fg, size) catch return BufferError.OutOfMemory;\n        self.buffer.bg = self.allocator.realloc(self.buffer.bg, size) catch return BufferError.OutOfMemory;\n        self.buffer.attributes = self.allocator.realloc(self.buffer.attributes, size) catch return BufferError.OutOfMemory;\n\n        self.width = width;\n        self.height = height;\n\n        // Always clear after resize to initialize cells (realloc doesn't zero memory)\n        // This handles both growing (new cells are garbage) and shrinking (grapheme cleanup)\n        try self.clear(.{ 0.0, 0.0, 0.0, 1.0 }, null);\n    }\n\n    fn coordsToIndex(self: *const OptimizedBuffer, x: u32, y: u32) u32 {\n        return y * self.width + x;\n    }\n\n    fn indexToCoords(self: *const OptimizedBuffer, index: u32) struct { x: u32, y: u32 } {\n        return .{\n            .x = index % self.width,\n            .y = index / self.width,\n        };\n    }\n\n    pub fn clear(self: *OptimizedBuffer, bg: RGBA, char: ?u32) !void {\n        const cellChar = char orelse DEFAULT_SPACE_CHAR;\n        self.link_tracker.clear();\n        self.grapheme_tracker.clear();\n        @memset(self.buffer.char, @intCast(cellChar));\n        @memset(self.buffer.attributes, 0);\n        @memset(self.buffer.fg, .{ 1.0, 1.0, 1.0, 1.0 });\n        @memset(self.buffer.bg, bg);\n    }\n\n    /// Write a single cell and update link tracker. No grapheme tracking,\n    /// span cleanup, or continuation propagation.\n    pub fn setRaw(self: *OptimizedBuffer, x: u32, y: u32, cell: Cell) void {\n        const index = self.validateAndIndex(x, y) orelse return;\n        self.writeCellAndLinks(index, cell);\n    }\n\n    /// Like set(), but without span cleanup. Writes the cell, its continuation\n    /// cells (for width-2+ graphemes), and updates grapheme/link trackers.\n    ///\n    /// Intended for the renderer's diff loop where cells are synced from an\n    /// authoritative source buffer. Span cleanup is skipped because it can\n    /// destroy continuation cells that were correctly written by an earlier\n    /// iteration of the same left-to-right pass (issue #723).\n    pub fn syncCell(self: *OptimizedBuffer, x: u32, y: u32, cell: Cell) void {\n        self.setInternal(x, y, cell, false);\n    }\n\n    pub fn set(self: *OptimizedBuffer, x: u32, y: u32, cell: Cell) void {\n        self.setInternal(x, y, cell, true);\n    }\n\n    fn setInternal(self: *OptimizedBuffer, x: u32, y: u32, cell: Cell, comptime span_cleanup: bool) void {\n        const index = self.validateAndIndex(x, y) orelse return;\n        const prev_char = self.buffer.char[index];\n        const prev_link_id = ansi.TextAttributes.getLinkId(self.buffer.attributes[index]);\n        var tracker_replaced = false;\n\n        if (!span_cleanup) {\n            const old_start_id: ?u32 = if (gp.isGraphemeChar(prev_char)) gp.graphemeIdFromChar(prev_char) else null;\n            const new_start_id: ?u32 = blk: {\n                if (!gp.isGraphemeChar(cell.char)) break :blk null;\n                const new_width = gp.charRightExtent(cell.char) + 1;\n                if (x + new_width > self.width) break :blk null;\n                break :blk gp.graphemeIdFromChar(cell.char);\n            };\n\n            if (old_start_id != null or new_start_id != null) {\n                self.grapheme_tracker.replace(old_start_id, new_start_id);\n                tracker_replaced = true;\n            }\n        }\n\n        // If overwriting a grapheme span (start or continuation) with a different char, clear that span first\n        if (span_cleanup) {\n            if ((gp.isGraphemeChar(prev_char) or gp.isContinuationChar(prev_char)) and prev_char != cell.char) {\n                const row_start: u32 = y * self.width;\n                const row_end: u32 = row_start + self.width - 1;\n                const left = gp.charLeftExtent(prev_char);\n                const right = gp.charRightExtent(prev_char);\n                const id = gp.graphemeIdFromChar(prev_char);\n\n                const new_grapheme_id: ?u32 = blk: {\n                    if (!gp.isGraphemeChar(cell.char)) break :blk null;\n                    const new_width = gp.charRightExtent(cell.char) + 1;\n                    if (x + new_width > self.width) break :blk null;\n                    break :blk gp.graphemeIdFromChar(cell.char);\n                };\n                self.grapheme_tracker.replace(id, new_grapheme_id);\n                tracker_replaced = true;\n\n                const span_start = index - @min(left, index - row_start);\n                const span_end = index + @min(right, row_end - index);\n\n                var span_i: u32 = span_start;\n                while (span_i <= span_end) : (span_i += 1) {\n                    const span_char = self.buffer.char[span_i];\n                    if (!(gp.isGraphemeChar(span_char) or gp.isContinuationChar(span_char))) continue;\n                    if (gp.graphemeIdFromChar(span_char) != id) continue;\n\n                    const span_link_id = ansi.TextAttributes.getLinkId(self.buffer.attributes[span_i]);\n                    if (span_link_id != 0) {\n                        self.link_tracker.removeCellRef(span_link_id);\n                    }\n\n                    self.buffer.char[span_i] = @intCast(DEFAULT_SPACE_CHAR);\n                    self.buffer.attributes[span_i] = 0;\n                }\n            }\n        }\n\n        if (gp.isGraphemeChar(cell.char)) {\n            const right = gp.charRightExtent(cell.char);\n            const width: u32 = 1 + right;\n\n            if (x + width > self.width) {\n                const end_of_line = (y + 1) * self.width;\n                var eol_i = index;\n                while (eol_i < end_of_line) : (eol_i += 1) {\n                    const eol_link_id = ansi.TextAttributes.getLinkId(self.buffer.attributes[eol_i]);\n                    if (eol_link_id != 0) {\n                        self.link_tracker.removeCellRef(eol_link_id);\n                    }\n                }\n                @memset(self.buffer.char[index..end_of_line], @intCast(DEFAULT_SPACE_CHAR));\n                @memset(self.buffer.attributes[index..end_of_line], cell.attributes);\n                @memset(self.buffer.fg[index..end_of_line], cell.fg);\n                @memset(self.buffer.bg[index..end_of_line], cell.bg);\n                const new_link_id = ansi.TextAttributes.getLinkId(cell.attributes);\n                if (new_link_id != 0) {\n                    const cells_written = end_of_line - index;\n                    var link_i: u32 = 0;\n                    while (link_i < cells_written) : (link_i += 1) {\n                        self.link_tracker.addCellRef(new_link_id);\n                    }\n                }\n                return;\n            }\n\n            self.buffer.char[index] = cell.char;\n            self.buffer.fg[index] = cell.fg;\n            self.buffer.bg[index] = cell.bg;\n            self.buffer.attributes[index] = cell.attributes;\n\n            const id: u32 = gp.graphemeIdFromChar(cell.char);\n            const is_same_grapheme_start = gp.isGraphemeChar(prev_char) and prev_char == cell.char;\n            if (!tracker_replaced and !is_same_grapheme_start) {\n                self.grapheme_tracker.add(id);\n            }\n\n            const new_link_id = ansi.TextAttributes.getLinkId(cell.attributes);\n            if (prev_link_id != 0 and prev_link_id != new_link_id) {\n                self.link_tracker.removeCellRef(prev_link_id);\n            }\n            if (new_link_id != 0 and new_link_id != prev_link_id) {\n                self.link_tracker.addCellRef(new_link_id);\n            }\n\n            if (width > 1) {\n                const row_end_index: u32 = (y * self.width) + self.width - 1;\n                const max_right = @min(right, row_end_index - index);\n                if (max_right > 0) {\n                    var cont_i: u32 = 1;\n                    while (cont_i <= max_right) : (cont_i += 1) {\n                        const cont_link_id = ansi.TextAttributes.getLinkId(self.buffer.attributes[index + cont_i]);\n                        if (cont_link_id != 0) {\n                            self.link_tracker.removeCellRef(cont_link_id);\n                        }\n                    }\n\n                    @memset(self.buffer.fg[index + 1 .. index + 1 + max_right], cell.fg);\n                    @memset(self.buffer.bg[index + 1 .. index + 1 + max_right], cell.bg);\n                    @memset(self.buffer.attributes[index + 1 .. index + 1 + max_right], cell.attributes);\n                    var k: u32 = 1;\n                    while (k <= max_right) : (k += 1) {\n                        const cont = gp.packContinuation(k, max_right - k, id);\n                        self.buffer.char[index + k] = cont;\n                        if (new_link_id != 0) {\n                            self.link_tracker.addCellRef(new_link_id);\n                        }\n                    }\n                }\n            }\n        } else {\n            self.writeCellAndLinks(index, cell);\n        }\n    }\n\n    /// Validate coordinates and return buffer index, or null if out of bounds / scissor.\n    fn validateAndIndex(self: *OptimizedBuffer, x: u32, y: u32) ?u32 {\n        if (x >= self.width or y >= self.height) return null;\n        if (!self.isPointInScissor(@intCast(x), @intCast(y))) return null;\n        return self.coordsToIndex(x, y);\n    }\n\n    /// Write cell data at index and update link tracker.\n    fn writeCellAndLinks(self: *OptimizedBuffer, index: u32, cell: Cell) void {\n        const prev_link_id = ansi.TextAttributes.getLinkId(self.buffer.attributes[index]);\n        const new_link_id = ansi.TextAttributes.getLinkId(cell.attributes);\n\n        self.buffer.char[index] = cell.char;\n        self.buffer.fg[index] = cell.fg;\n        self.buffer.bg[index] = cell.bg;\n        self.buffer.attributes[index] = cell.attributes;\n\n        if (prev_link_id != 0 and prev_link_id != new_link_id) {\n            self.link_tracker.removeCellRef(prev_link_id);\n        }\n        if (new_link_id != 0 and new_link_id != prev_link_id) {\n            self.link_tracker.addCellRef(new_link_id);\n        }\n    }\n\n    pub fn get(self: *const OptimizedBuffer, x: u32, y: u32) ?Cell {\n        if (x >= self.width or y >= self.height) return null;\n\n        const index = self.coordsToIndex(x, y);\n        return Cell{\n            .char = self.buffer.char[index],\n            .fg = self.buffer.fg[index],\n            .bg = self.buffer.bg[index],\n            .attributes = self.buffer.attributes[index],\n        };\n    }\n\n    pub fn getWidth(self: *const OptimizedBuffer) u32 {\n        return self.width;\n    }\n\n    pub fn getHeight(self: *const OptimizedBuffer) u32 {\n        return self.height;\n    }\n\n    pub fn setRespectAlpha(self: *OptimizedBuffer, respectAlpha: bool) void {\n        self.respectAlpha = respectAlpha;\n    }\n\n    pub fn getRespectAlpha(self: *const OptimizedBuffer) bool {\n        return self.respectAlpha;\n    }\n\n    pub fn setBlendBackdropColor(self: *OptimizedBuffer, color: ?RGBA) void {\n        self.blendBackdropColor = color;\n    }\n\n    pub fn getBlendBackdropColor(self: *const OptimizedBuffer) ?RGBA {\n        return self.blendBackdropColor;\n    }\n\n    pub fn getId(self: *const OptimizedBuffer) []const u8 {\n        return self.id;\n    }\n\n    /// Calculate the real byte size of the character buffer including grapheme pool data\n    pub fn getRealCharSize(self: *const OptimizedBuffer) u32 {\n        const total_chars = self.width * self.height;\n        const grapheme_count = self.grapheme_tracker.getGraphemeCellCount();\n        const total_grapheme_bytes = self.grapheme_tracker.getTotalGraphemeBytes();\n\n        const regular_char_bytes = (total_chars - grapheme_count) * @sizeOf(u32);\n        return regular_char_bytes + total_grapheme_bytes;\n    }\n\n    /// Write all resolved character bytes to the given output buffer\n    /// Returns the number of bytes written, or 0 if the output buffer is too small\n    pub fn writeResolvedChars(self: *const OptimizedBuffer, output_buffer: []u8, addLineBreaks: bool) BufferError!u32 {\n        var bytes_written: u32 = 0;\n        const total_cells = self.width * self.height;\n\n        var i: u32 = 0;\n        while (i < total_cells) : (i += 1) {\n            const char_code = self.buffer.char[i];\n\n            if (gp.isGraphemeChar(char_code)) {\n                const gid = gp.graphemeIdFromChar(char_code);\n                if (self.pool.get(gid)) |grapheme_bytes| {\n                    if (bytes_written + grapheme_bytes.len > output_buffer.len) {\n                        return BufferError.BufferTooSmall;\n                    }\n                    @memcpy(output_buffer[bytes_written .. bytes_written + grapheme_bytes.len], grapheme_bytes);\n                    bytes_written += @intCast(grapheme_bytes.len);\n                } else |_| {\n                    if (bytes_written + 1 > output_buffer.len) {\n                        return BufferError.BufferTooSmall;\n                    }\n                    output_buffer[bytes_written] = ' ';\n                    bytes_written += 1;\n                }\n            } else if (gp.isContinuationChar(char_code)) {\n                continue;\n            } else {\n                const codepoint = char_code;\n\n                if (codepoint > 0x10FFFF) {\n                    if (bytes_written + 1 > output_buffer.len) {\n                        return BufferError.BufferTooSmall;\n                    }\n                    output_buffer[bytes_written] = ' ';\n                    bytes_written += 1;\n                    continue;\n                }\n\n                var utf8_bytes: [4]u8 = undefined;\n                const utf8_len = std.unicode.utf8Encode(@intCast(codepoint), &utf8_bytes) catch {\n                    if (bytes_written + 1 > output_buffer.len) {\n                        return BufferError.BufferTooSmall;\n                    }\n                    output_buffer[bytes_written] = ' ';\n                    bytes_written += 1;\n                    continue;\n                };\n\n                if (bytes_written + utf8_len > output_buffer.len) {\n                    return BufferError.BufferTooSmall;\n                }\n                @memcpy(output_buffer[bytes_written .. bytes_written + utf8_len], utf8_bytes[0..utf8_len]);\n                bytes_written += @intCast(utf8_len);\n            }\n\n            if (addLineBreaks and (i + 1) % self.width == 0) {\n                if (bytes_written + 1 > output_buffer.len) {\n                    return BufferError.BufferTooSmall;\n                }\n                output_buffer[bytes_written] = '\\n';\n                bytes_written += 1;\n            }\n        }\n\n        return bytes_written;\n    }\n\n    pub fn blendCells(self: *const OptimizedBuffer, overlayCell: Cell, destCell: Cell) Cell {\n        const hasBgAlpha = isRGBAWithAlpha(overlayCell.bg);\n        const hasFgAlpha = isRGBAWithAlpha(overlayCell.fg);\n\n        if (hasBgAlpha or hasFgAlpha) {\n            const blendedBg = if (hasBgAlpha)\n                blendColors(overlayCell.bg, destCell.bg, self.blendBackdropColor)\n            else\n                overlayCell.bg;\n            const charIsDefaultSpace = overlayCell.char == DEFAULT_SPACE_CHAR;\n            const destNotZero = destCell.char != 0;\n            const destNotDefaultSpace = destCell.char != DEFAULT_SPACE_CHAR;\n            const destWidthIsOne = gp.encodedCharWidth(destCell.char) == 1;\n\n            const preserveChar = (charIsDefaultSpace and\n                destNotZero and\n                destNotDefaultSpace and\n                destWidthIsOne);\n            const finalChar = if (preserveChar) destCell.char else overlayCell.char;\n\n            var finalFg: RGBA = undefined;\n            if (preserveChar) {\n                finalFg = blendColors(overlayCell.bg, destCell.fg, self.blendBackdropColor);\n            } else {\n                finalFg = if (hasFgAlpha)\n                    blendColors(overlayCell.fg, destCell.bg, self.blendBackdropColor)\n                else\n                    overlayCell.fg;\n            }\n\n            // When preserving char, preserve its base attributes but NOT its link\n            // Links ALWAYS come from overlay, never from destination\n            // Even if overlay has no link (link_id=0), it clears the destination's link\n            const baseAttrs = if (preserveChar)\n                ansi.TextAttributes.getBaseAttributes(destCell.attributes)\n            else\n                ansi.TextAttributes.getBaseAttributes(overlayCell.attributes);\n            // Overlay link always wins - whether it's a real link or 0 (no link)\n            const overlayLinkId = ansi.TextAttributes.getLinkId(overlayCell.attributes);\n            const finalAttributes = ansi.TextAttributes.setLinkId(@as(u32, baseAttrs), overlayLinkId);\n\n            // When overlay background is fully transparent, preserve destination background alpha\n            const finalBgAlpha = if (overlayCell.bg[3] == 0.0) destCell.bg[3] else overlayCell.bg[3];\n\n            return Cell{\n                .char = finalChar,\n                .fg = finalFg,\n                .bg = .{ blendedBg[0], blendedBg[1], blendedBg[2], finalBgAlpha },\n                .attributes = finalAttributes,\n            };\n        }\n\n        return overlayCell;\n    }\n\n    pub fn setCellWithAlphaBlending(\n        self: *OptimizedBuffer,\n        x: u32,\n        y: u32,\n        char: u32,\n        fg: RGBA,\n        bg: RGBA,\n        attributes: u32,\n    ) !void {\n        if (!self.isPointInScissor(@intCast(x), @intCast(y))) return;\n\n        // Apply current opacity from the stack\n        const opacity = self.getCurrentOpacity();\n        if (isFullyOpaque(opacity, fg, bg)) {\n            self.set(x, y, Cell{ .char = char, .fg = fg, .bg = bg, .attributes = attributes });\n            return;\n        }\n\n        const effectiveFg = RGBA{ fg[0], fg[1], fg[2], fg[3] * opacity };\n        const effectiveBg = RGBA{ bg[0], bg[1], bg[2], bg[3] * opacity };\n\n        const overlayCell = Cell{ .char = char, .fg = effectiveFg, .bg = effectiveBg, .attributes = attributes };\n\n        if (self.get(x, y)) |destCell| {\n            const blendedCell = self.blendCells(overlayCell, destCell);\n            self.set(x, y, blendedCell);\n        } else {\n            self.set(x, y, overlayCell);\n        }\n    }\n\n    pub fn setCellWithAlphaBlendingRaw(\n        self: *OptimizedBuffer,\n        x: u32,\n        y: u32,\n        char: u32,\n        fg: RGBA,\n        bg: RGBA,\n        attributes: u32,\n    ) !void {\n        if (!self.isPointInScissor(@intCast(x), @intCast(y))) return;\n\n        // Apply current opacity from the stack\n        const opacity = self.getCurrentOpacity();\n        if (isFullyOpaque(opacity, fg, bg)) {\n            const overlayCell = Cell{ .char = char, .fg = fg, .bg = bg, .attributes = attributes };\n            assert(!gp.isGraphemeChar(char));\n            assert(!gp.isContinuationChar(char));\n            self.setRaw(x, y, overlayCell);\n            return;\n        }\n\n        const effectiveFg = RGBA{ fg[0], fg[1], fg[2], fg[3] * opacity };\n        const effectiveBg = RGBA{ bg[0], bg[1], bg[2], bg[3] * opacity };\n\n        const overlayCell = Cell{ .char = char, .fg = effectiveFg, .bg = effectiveBg, .attributes = attributes };\n\n        if (self.get(x, y)) |destCell| {\n            const blendedCell = self.blendCells(overlayCell, destCell);\n            assert(!gp.isGraphemeChar(blendedCell.char));\n            assert(!gp.isContinuationChar(blendedCell.char));\n            self.setRaw(x, y, blendedCell);\n        } else {\n            assert(!gp.isGraphemeChar(overlayCell.char));\n            assert(!gp.isContinuationChar(overlayCell.char));\n            self.setRaw(x, y, overlayCell);\n        }\n    }\n\n    pub fn drawChar(\n        self: *OptimizedBuffer,\n        char: u32,\n        x: u32,\n        y: u32,\n        fg: RGBA,\n        bg: RGBA,\n        attributes: u32,\n    ) !void {\n        if (!self.isPointInScissor(@intCast(x), @intCast(y))) return;\n\n        if (isRGBAWithAlpha(bg) or isRGBAWithAlpha(fg)) {\n            try self.setCellWithAlphaBlending(x, y, char, fg, bg, attributes);\n        } else {\n            self.set(x, y, Cell{\n                .char = char,\n                .fg = fg,\n                .bg = bg,\n                .attributes = attributes,\n            });\n        }\n    }\n\n    pub fn fillRect(\n        self: *OptimizedBuffer,\n        x: u32,\n        y: u32,\n        width: u32,\n        height: u32,\n        bg: RGBA,\n    ) !void {\n        if (self.width == 0 or self.height == 0 or width == 0 or height == 0) return;\n        if (x >= self.width or y >= self.height) return;\n\n        if (!self.isRectInScissor(@intCast(x), @intCast(y), width, height)) return;\n\n        const startX = x;\n        const startY = y;\n        const maxEndX = if (x < self.width) self.width - 1 else 0;\n        const maxEndY = if (y < self.height) self.height - 1 else 0;\n        const requestedEndX = x + width - 1;\n        const requestedEndY = y + height - 1;\n        const endX = @min(maxEndX, requestedEndX);\n        const endY = @min(maxEndY, requestedEndY);\n\n        if (startX > endX or startY > endY) return;\n\n        const clippedRect = self.clipRectToScissor(@intCast(startX), @intCast(startY), endX - startX + 1, endY - startY + 1) orelse return;\n        const clippedStartX = @max(startX, @as(u32, @intCast(clippedRect.x)));\n        const clippedStartY = @max(startY, @as(u32, @intCast(clippedRect.y)));\n        const clippedEndX = @min(endX, @as(u32, @intCast(clippedRect.x + @as(i32, @intCast(clippedRect.width)) - 1)));\n        const clippedEndY = @min(endY, @as(u32, @intCast(clippedRect.y + @as(i32, @intCast(clippedRect.height)) - 1)));\n\n        const opacity = self.getCurrentOpacity();\n        const hasAlpha = isRGBAWithAlpha(bg) or opacity < 1.0;\n        const linkAware = self.link_tracker.hasAny();\n\n        if (hasAlpha or self.grapheme_tracker.hasAny() or linkAware) {\n            var fillY = clippedStartY;\n            while (fillY <= clippedEndY) : (fillY += 1) {\n                var fillX = clippedStartX;\n                while (fillX <= clippedEndX) : (fillX += 1) {\n                    try self.setCellWithAlphaBlending(fillX, fillY, DEFAULT_SPACE_CHAR, .{ 1.0, 1.0, 1.0, 1.0 }, bg, 0);\n                }\n            }\n        } else {\n            // For non-alpha (fully opaque) backgrounds with no graphemes or links, we can do direct filling\n            var fillY = clippedStartY;\n            while (fillY <= clippedEndY) : (fillY += 1) {\n                const rowStartIndex = self.coordsToIndex(@intCast(clippedStartX), @intCast(fillY));\n                const rowWidth = clippedEndX - clippedStartX + 1;\n\n                const rowSliceChar = self.buffer.char[rowStartIndex .. rowStartIndex + rowWidth];\n                const rowSliceFg = self.buffer.fg[rowStartIndex .. rowStartIndex + rowWidth];\n                const rowSliceBg = self.buffer.bg[rowStartIndex .. rowStartIndex + rowWidth];\n                const rowSliceAttrs = self.buffer.attributes[rowStartIndex .. rowStartIndex + rowWidth];\n\n                @memset(rowSliceChar, @intCast(DEFAULT_SPACE_CHAR));\n                @memset(rowSliceFg, .{ 1.0, 1.0, 1.0, 1.0 });\n                @memset(rowSliceBg, bg);\n                @memset(rowSliceAttrs, 0);\n            }\n        }\n    }\n\n    pub fn drawText(\n        self: *OptimizedBuffer,\n        text: []const u8,\n        x: u32,\n        y: u32,\n        fg: RGBA,\n        bg: ?RGBA,\n        attributes: u32,\n    ) BufferError!void {\n        if (x >= self.width or y >= self.height) return;\n        if (text.len == 0) return;\n\n        const is_ascii_only = utf8.isAsciiOnly(text);\n\n        var grapheme_list: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n        defer grapheme_list.deinit(self.allocator);\n\n        const tab_width: u8 = 2;\n        try utf8.findGraphemeInfo(text, tab_width, is_ascii_only, self.width_method, self.allocator, &grapheme_list);\n        const specials = grapheme_list.items;\n\n        var advance_cells: u32 = 0;\n        var byte_offset: u32 = 0;\n        var col: u32 = 0;\n        var special_idx: usize = 0;\n\n        while (byte_offset < text.len) {\n            const charX = x + advance_cells;\n            if (charX >= self.width) break;\n\n            const at_special = special_idx < specials.len and specials[special_idx].col_offset == col;\n\n            var grapheme_bytes: []const u8 = undefined;\n            var g_width: u8 = undefined;\n\n            if (at_special) {\n                const g = specials[special_idx];\n                grapheme_bytes = text[g.byte_offset .. g.byte_offset + g.byte_len];\n                g_width = g.width;\n                byte_offset = g.byte_offset + g.byte_len;\n                special_idx += 1;\n            } else {\n                if (byte_offset >= text.len) break;\n                grapheme_bytes = text[byte_offset .. byte_offset + 1];\n                g_width = 1;\n                byte_offset += 1;\n            }\n\n            if (!self.isPointInScissor(@intCast(charX), @intCast(y))) {\n                advance_cells += g_width;\n                col += g_width;\n                continue;\n            }\n\n            var bgColor: RGBA = undefined;\n            if (bg) |b| {\n                bgColor = b;\n            } else if (self.get(charX, y)) |existingCell| {\n                bgColor = existingCell.bg;\n            } else {\n                bgColor = .{ 0.0, 0.0, 0.0, 1.0 };\n            }\n\n            const cell_width = utf8.getWidthAt(text, if (at_special) specials[special_idx - 1].byte_offset else byte_offset - 1, tab_width, self.width_method);\n            if (cell_width == 0) {\n                col += g_width;\n                continue;\n            }\n\n            if (grapheme_bytes.len == 1 and grapheme_bytes[0] == '\\t') {\n                var tab_col: u32 = 0;\n                while (tab_col < g_width) : (tab_col += 1) {\n                    const tab_x = charX + tab_col;\n                    if (tab_x >= self.width) break;\n\n                    if (isRGBAWithAlpha(bgColor)) {\n                        try self.setCellWithAlphaBlending(\n                            tab_x,\n                            y,\n                            DEFAULT_SPACE_CHAR,\n                            fg,\n                            bgColor,\n                            attributes,\n                        );\n                    } else {\n                        self.set(tab_x, y, Cell{\n                            .char = DEFAULT_SPACE_CHAR,\n                            .fg = fg,\n                            .bg = bgColor,\n                            .attributes = attributes,\n                        });\n                    }\n                }\n                advance_cells += g_width;\n                col += g_width;\n                continue;\n            }\n\n            var encoded_char: u32 = 0;\n            if (grapheme_bytes.len == 1 and cell_width == 1 and grapheme_bytes[0] >= 32) {\n                encoded_char = @as(u32, grapheme_bytes[0]);\n            } else {\n                const gid = self.pool.alloc(grapheme_bytes) catch return BufferError.OutOfMemory;\n                encoded_char = gp.packGraphemeStart(gid & gp.GRAPHEME_ID_MASK, cell_width);\n            }\n\n            if (isRGBAWithAlpha(bgColor)) {\n                try self.setCellWithAlphaBlending(charX, y, encoded_char, fg, bgColor, attributes);\n            } else {\n                self.set(charX, y, Cell{\n                    .char = encoded_char,\n                    .fg = fg,\n                    .bg = bgColor,\n                    .attributes = attributes,\n                });\n            }\n\n            advance_cells += cell_width;\n            col += g_width;\n        }\n    }\n\n    pub fn drawFrameBuffer(self: *OptimizedBuffer, destX: i32, destY: i32, frameBuffer: *OptimizedBuffer, sourceX: ?u32, sourceY: ?u32, sourceWidth: ?u32, sourceHeight: ?u32) void {\n        if (self.width == 0 or self.height == 0 or frameBuffer.width == 0 or frameBuffer.height == 0) return;\n\n        const srcX = sourceX orelse 0;\n        const srcY = sourceY orelse 0;\n        const srcWidth = sourceWidth orelse frameBuffer.width;\n        const srcHeight = sourceHeight orelse frameBuffer.height;\n\n        if (srcX >= frameBuffer.width or srcY >= frameBuffer.height) return;\n        if (srcWidth == 0 or srcHeight == 0) return;\n\n        const clampedSrcWidth = @min(srcWidth, frameBuffer.width - srcX);\n        const clampedSrcHeight = @min(srcHeight, frameBuffer.height - srcY);\n\n        const startDestX = @max(0, destX);\n        const startDestY = @max(0, destY);\n        const endDestX = @min(@as(i32, @intCast(self.width)) - 1, destX + @as(i32, @intCast(clampedSrcWidth)) - 1);\n        const endDestY = @min(@as(i32, @intCast(self.height)) - 1, destY + @as(i32, @intCast(clampedSrcHeight)) - 1);\n\n        if (startDestX > endDestX or startDestY > endDestY) return;\n\n        // Check if the destination rectangle intersects with the scissor rect\n        const destWidth = @as(u32, @intCast(endDestX - startDestX + 1));\n        const destHeight = @as(u32, @intCast(endDestY - startDestY + 1));\n        if (!self.isRectInScissor(startDestX, startDestY, destWidth, destHeight)) return;\n\n        const graphemeAware = self.grapheme_tracker.hasAny() or frameBuffer.grapheme_tracker.hasAny();\n        const linkAware = self.link_tracker.hasAny() or frameBuffer.link_tracker.hasAny();\n\n        // Calculate clipping once for both paths\n        const clippedRect = self.clipRectToScissor(startDestX, startDestY, destWidth, destHeight) orelse return;\n        const clippedStartX = @max(startDestX, clippedRect.x);\n        const clippedStartY = @max(startDestY, clippedRect.y);\n        const clippedEndX = @min(endDestX, @as(i32, @intCast(clippedRect.x + @as(i32, @intCast(clippedRect.width)) - 1)));\n        const clippedEndY = @min(endDestY, @as(i32, @intCast(clippedRect.y + @as(i32, @intCast(clippedRect.height)) - 1)));\n\n        if (!graphemeAware and !frameBuffer.respectAlpha and !linkAware) {\n            // Fast path: direct memory copy\n            var dY = clippedStartY;\n\n            while (dY <= clippedEndY) : (dY += 1) {\n                const relativeDestY = dY - destY;\n                const sY = srcY + @as(u32, @intCast(relativeDestY));\n\n                if (sY >= frameBuffer.height) continue;\n\n                const relativeDestX = clippedStartX - destX;\n                const sX = srcX + @as(u32, @intCast(relativeDestX));\n\n                if (sX >= frameBuffer.width) continue;\n\n                const destRowStart = self.coordsToIndex(@intCast(clippedStartX), @intCast(dY));\n                const srcRowStart = frameBuffer.coordsToIndex(sX, sY);\n                const actualCopyWidth = @min(@as(u32, @intCast(clippedEndX - clippedStartX + 1)), frameBuffer.width - sX);\n\n                @memcpy(self.buffer.char[destRowStart .. destRowStart + actualCopyWidth], frameBuffer.buffer.char[srcRowStart .. srcRowStart + actualCopyWidth]);\n                @memcpy(self.buffer.fg[destRowStart .. destRowStart + actualCopyWidth], frameBuffer.buffer.fg[srcRowStart .. srcRowStart + actualCopyWidth]);\n                @memcpy(self.buffer.bg[destRowStart .. destRowStart + actualCopyWidth], frameBuffer.buffer.bg[srcRowStart .. srcRowStart + actualCopyWidth]);\n                @memcpy(self.buffer.attributes[destRowStart .. destRowStart + actualCopyWidth], frameBuffer.buffer.attributes[srcRowStart .. srcRowStart + actualCopyWidth]);\n            }\n            return;\n        }\n\n        var dY = clippedStartY;\n        while (dY <= clippedEndY) : (dY += 1) {\n            var lastDrawnGraphemeId: u32 = 0;\n\n            var dX = clippedStartX;\n            while (dX <= clippedEndX) : (dX += 1) {\n                const relativeDestX = dX - destX;\n                const relativeDestY = dY - destY;\n                const sX = srcX + @as(u32, @intCast(relativeDestX));\n                const sY = srcY + @as(u32, @intCast(relativeDestY));\n\n                if (sX >= frameBuffer.width or sY >= frameBuffer.height) continue;\n\n                const srcIndex = frameBuffer.coordsToIndex(sX, sY);\n                if (srcIndex >= frameBuffer.buffer.char.len) continue;\n\n                const srcChar = frameBuffer.buffer.char[srcIndex];\n                const srcFg = frameBuffer.buffer.fg[srcIndex];\n                const srcBg = frameBuffer.buffer.bg[srcIndex];\n                const srcAttr = frameBuffer.buffer.attributes[srcIndex];\n\n                if (srcBg[3] == 0.0 and srcFg[3] == 0.0) continue;\n\n                if (graphemeAware) {\n                    if (gp.isContinuationChar(srcChar)) {\n                        const graphemeId = srcChar & gp.GRAPHEME_ID_MASK;\n                        if (graphemeId != lastDrawnGraphemeId) {\n                            // We haven't drawn the start character for this grapheme (likely out of bounds to the left)\n                            // Draw a space with the same attributes to fill the cell\n                            self.setCellWithAlphaBlending(@intCast(dX), @intCast(dY), DEFAULT_SPACE_CHAR, srcFg, srcBg, srcAttr) catch {};\n                        }\n                        continue;\n                    }\n\n                    if (gp.isGraphemeChar(srcChar)) {\n                        lastDrawnGraphemeId = srcChar & gp.GRAPHEME_ID_MASK;\n                    }\n\n                    self.setCellWithAlphaBlending(@intCast(dX), @intCast(dY), srcChar, srcFg, srcBg, srcAttr) catch {};\n                    continue;\n                }\n\n                self.setCellWithAlphaBlendingRaw(@intCast(dX), @intCast(dY), srcChar, srcFg, srcBg, srcAttr) catch {};\n            }\n        }\n    }\n\n    /// Draw a TextBufferView to this OptimizedBuffer with selection support and optional syntax highlighting\n    pub fn drawTextBuffer(\n        self: *OptimizedBuffer,\n        text_buffer_view: *TextBufferView,\n        x: i32,\n        y: i32,\n    ) !void {\n        try self.drawTextBufferInternal(TextBufferView, text_buffer_view, x, y);\n    }\n\n    /// Internal implementation that accepts either TextBufferView or EditorView\n    /// Both types must expose: getVirtualLines(), getViewport(), getCachedLineInfo(), getVirtualLineSpans(), getTextBuffer(), getSelection()\n    fn drawTextBufferInternal(\n        self: *OptimizedBuffer,\n        comptime ViewType: type,\n        view: *ViewType,\n        x: i32,\n        y: i32,\n    ) !void {\n        const virtual_lines = view.getVirtualLines();\n        if (virtual_lines.len == 0) return;\n\n        const firstVisibleLine: u32 = if (y < 0) @intCast(-y) else 0;\n        const bufferBottomY = self.height;\n        const lastPossibleLine = if (y >= @as(i32, @intCast(bufferBottomY)))\n            0\n        else if (y < 0)\n            @min(virtual_lines.len, firstVisibleLine + bufferBottomY)\n        else\n            @min(virtual_lines.len, bufferBottomY - @as(u32, @intCast(y)));\n\n        if (firstVisibleLine >= virtual_lines.len or lastPossibleLine == 0) return;\n        if (firstVisibleLine >= lastPossibleLine) return;\n\n        const viewport = view.getViewport();\n        const horizontal_offset: u32 = if (viewport) |vp| vp.x else 0;\n        const viewport_width: u32 = if (viewport) |vp| vp.width else std.math.maxInt(u32);\n\n        var currentX = x;\n        var currentY = y + @as(i32, @intCast(firstVisibleLine));\n        const text_buffer = view.getTextBuffer();\n        const total_line_count = text_buffer.lineCount();\n\n        const line_info = view.getCachedLineInfo();\n        var globalCharPos: u32 = if (firstVisibleLine < line_info.line_start_cols.len)\n            line_info.line_start_cols[firstVisibleLine]\n        else\n            0;\n\n        for (virtual_lines[firstVisibleLine..lastPossibleLine], 0..) |vline, slice_idx| {\n            if (currentY >= bufferBottomY) break;\n\n            currentX = x;\n            var column_in_line: u32 = 0;\n            globalCharPos = vline.col_offset;\n\n            // When viewport is set, virtual_lines is a slice starting from viewport.y\n            // But getVirtualLineSpans expects absolute indices, so we need to use the absolute index\n            // slice_idx is relative to the slice (0, 1, 2...), we need to add viewport offset + firstVisibleLine\n            const viewport_offset: u32 = if (viewport) |vp| vp.y else 0;\n            const vline_idx = viewport_offset + firstVisibleLine + slice_idx;\n            const vline_span_info = view.getVirtualLineSpans(vline_idx);\n            const spans = vline_span_info.spans;\n            const col_offset = vline_span_info.col_offset;\n            var span_idx: usize = 0;\n            const defaults = text_buffer.defaults();\n            var lineFg = defaults.fg orelse RGBA{ 1.0, 1.0, 1.0, 1.0 };\n            var lineBg = defaults.bg orelse RGBA{ 0.0, 0.0, 0.0, 0.0 };\n            var lineAttributes = defaults.attributes orelse 0;\n            const defaultFg = lineFg;\n            const defaultBg = lineBg;\n            const defaultAttributes = lineAttributes;\n\n            // Find the span that contains the starting render position (col_offset + horizontal_offset)\n            const start_col = col_offset + horizontal_offset;\n            while (span_idx < spans.len and spans[span_idx].next_col <= start_col) {\n                span_idx += 1;\n            }\n\n            var next_change_col: u32 = if (span_idx < spans.len)\n                spans[span_idx].next_col\n            else\n                std.math.maxInt(u32);\n\n            // Apply the style at the starting position\n            if (span_idx < spans.len and spans[span_idx].col <= start_col and spans[span_idx].style_id != 0) {\n                if (text_buffer.getSyntaxStyle()) |style| {\n                    if (style.resolveById(spans[span_idx].style_id)) |resolved_style| {\n                        if (resolved_style.fg) |fg| lineFg = fg;\n                        if (resolved_style.bg) |bg| lineBg = bg;\n                        lineAttributes |= resolved_style.attributes;\n                    }\n                }\n            }\n\n            for (vline.chunks.items) |vchunk| {\n                const chunk = vchunk.chunk;\n                const chunk_bytes = chunk.getBytes(text_buffer.memRegistry());\n                const specials = chunk.getGraphemes(text_buffer.memRegistry(), text_buffer.getAllocator(), text_buffer.tabWidth(), text_buffer.widthMethod()) catch continue;\n                const line_col_offset = vline.col_offset;\n\n                if (currentX >= @as(i32, @intCast(self.width))) {\n                    globalCharPos += vchunk.width;\n                    currentX += @intCast(vchunk.width);\n                    continue;\n                }\n                const col_end = vchunk.grapheme_start + vchunk.width;\n                var col = vchunk.grapheme_start;\n                var special_idx: usize = 0;\n                var byte_offset: u32 = 0;\n\n                if (vchunk.grapheme_start > 0) {\n                    // Use UTF-8 aware position finding to skip to the grapheme_start\n                    const is_ascii_only = (vchunk.chunk.flags & tb.TextChunk.Flags.ASCII_ONLY) != 0;\n                    const pos_result = utf8.findPosByWidth(chunk_bytes, vchunk.grapheme_start, text_buffer.tabWidth(), is_ascii_only, false, text_buffer.widthMethod());\n                    byte_offset = pos_result.byte_offset;\n\n                    // Advance special_idx to match the skipped columns\n                    var init_col: u32 = 0;\n                    while (init_col < vchunk.grapheme_start and special_idx < specials.len) {\n                        const g = specials[special_idx];\n                        if (g.col_offset < vchunk.grapheme_start) {\n                            special_idx += 1;\n                            init_col = g.col_offset + g.width;\n                        } else {\n                            break;\n                        }\n                    }\n                }\n\n                while (col < col_end) {\n                    const at_special = special_idx < specials.len and specials[special_idx].col_offset == col;\n\n                    var grapheme_bytes: []const u8 = undefined;\n                    var g_width: u8 = undefined;\n\n                    if (at_special) {\n                        const g = specials[special_idx];\n                        grapheme_bytes = chunk_bytes[g.byte_offset .. g.byte_offset + g.byte_len];\n                        g_width = g.width;\n                        byte_offset = g.byte_offset + g.byte_len;\n                        special_idx += 1;\n                    } else {\n                        if (byte_offset >= chunk_bytes.len) break;\n                        const cp_len = std.unicode.utf8ByteSequenceLength(chunk_bytes[byte_offset]) catch 1;\n                        const next_byte_offset = @min(byte_offset + cp_len, chunk_bytes.len);\n                        grapheme_bytes = chunk_bytes[byte_offset..next_byte_offset];\n                        g_width = 1;\n                        byte_offset = next_byte_offset;\n                    }\n\n                    if (column_in_line < horizontal_offset) {\n                        globalCharPos += g_width;\n                        column_in_line += g_width;\n                        col += g_width;\n                        continue;\n                    }\n\n                    if (column_in_line >= horizontal_offset + viewport_width) {\n                        globalCharPos += (col_end - col);\n                        break;\n                    }\n\n                    if (currentX < -@as(i32, @intCast(g_width))) {\n                        globalCharPos += g_width;\n                        currentX += @as(i32, @intCast(g_width));\n                        column_in_line += g_width;\n                        col += g_width;\n                        continue;\n                    }\n\n                    if (currentX >= @as(i32, @intCast(self.width))) {\n                        globalCharPos += (col_end - col);\n                        break;\n                    }\n\n                    if (!self.isPointInScissor(currentX, currentY)) {\n                        globalCharPos += g_width;\n                        currentX += @as(i32, @intCast(g_width));\n                        column_in_line += g_width;\n                        col += g_width;\n                        continue;\n                    }\n\n                    var selection_offset = globalCharPos;\n                    if (vline.is_truncated and globalCharPos >= line_col_offset) {\n                        const ellipsis_width: u32 = 3;\n                        const column_offset_in_line = globalCharPos - line_col_offset;\n                        if (column_offset_in_line >= vline.ellipsis_pos and column_offset_in_line < vline.ellipsis_pos + ellipsis_width) {\n                            selection_offset = line_col_offset + vline.ellipsis_pos;\n                        } else if (column_offset_in_line >= vline.ellipsis_pos + ellipsis_width) {\n                            selection_offset = line_col_offset + vline.truncation_suffix_start +\n                                (column_offset_in_line - vline.ellipsis_pos - ellipsis_width);\n                        } else {\n                            selection_offset = line_col_offset + column_offset_in_line;\n                        }\n                    }\n\n                    // Track the actual column position in the source line (including horizontal offset)\n                    var source_col_pos = col_offset + column_in_line;\n                    if (vline.is_truncated) {\n                        const ellipsis_width: u32 = 3;\n                        const column_offset_in_line = globalCharPos - line_col_offset;\n                        if (column_offset_in_line >= vline.ellipsis_pos and column_offset_in_line < vline.ellipsis_pos + ellipsis_width) {\n                            source_col_pos = std.math.maxInt(u32);\n                        } else if (column_offset_in_line >= vline.ellipsis_pos + ellipsis_width) {\n                            source_col_pos = vline.truncation_suffix_start + (column_offset_in_line - vline.ellipsis_pos - ellipsis_width);\n                        }\n                    }\n\n                    if (source_col_pos >= next_change_col and span_idx + 1 < spans.len) {\n                        span_idx += 1;\n                        const new_span = spans[span_idx];\n\n                        lineFg = defaultFg;\n                        lineBg = defaultBg;\n                        lineAttributes = defaultAttributes;\n\n                        if (text_buffer.getSyntaxStyle()) |style| {\n                            if (new_span.style_id != 0) {\n                                if (style.resolveById(new_span.style_id)) |resolved_style| {\n                                    if (resolved_style.fg) |fg| lineFg = fg;\n                                    if (resolved_style.bg) |bg| lineBg = bg;\n                                    lineAttributes |= resolved_style.attributes;\n                                }\n                            }\n                        }\n\n                        next_change_col = new_span.next_col;\n                    }\n\n                    if (vline.is_truncated) {\n                        const column_offset_in_line = globalCharPos - line_col_offset;\n                        const ellipsis_width: u32 = 3;\n                        if (column_offset_in_line >= vline.ellipsis_pos and column_offset_in_line < vline.ellipsis_pos + ellipsis_width) {\n                            lineFg = defaultFg;\n                            lineBg = defaultBg;\n                            lineAttributes = defaultAttributes;\n                        } else if (column_offset_in_line >= vline.ellipsis_pos + ellipsis_width) {\n                            const suffix_col_pos = vline.truncation_suffix_start + (column_offset_in_line - vline.ellipsis_pos - ellipsis_width);\n                            if (spans.len == 0) {\n                                lineFg = defaultFg;\n                                lineBg = defaultBg;\n                                lineAttributes = defaultAttributes;\n                                next_change_col = std.math.maxInt(u32);\n                            } else {\n                                var suffix_span_idx: usize = 0;\n                                while (suffix_span_idx < spans.len and spans[suffix_span_idx].next_col <= suffix_col_pos) {\n                                    suffix_span_idx += 1;\n                                }\n                                if (suffix_span_idx < spans.len) {\n                                    span_idx = suffix_span_idx;\n                                }\n                                const active_span = spans[span_idx];\n                                lineFg = defaultFg;\n                                lineBg = defaultBg;\n                                lineAttributes = defaultAttributes;\n                                if (text_buffer.getSyntaxStyle()) |style| {\n                                    if (active_span.style_id != 0) {\n                                        if (style.resolveById(active_span.style_id)) |resolved_style| {\n                                            if (resolved_style.fg) |fg| lineFg = fg;\n                                            if (resolved_style.bg) |bg| lineBg = bg;\n                                            lineAttributes |= resolved_style.attributes;\n                                        }\n                                    }\n                                }\n                                next_change_col = active_span.next_col;\n                            }\n                        }\n                    }\n\n                    var finalFg = lineFg;\n                    var finalBg = lineBg;\n                    const finalAttributes = lineAttributes;\n\n                    var cell_idx: u32 = 0;\n                    while (cell_idx < g_width) : (cell_idx += 1) {\n                        if (view.getSelection()) |sel| {\n                            const isSelected = selection_offset + cell_idx >= sel.start and selection_offset + cell_idx < sel.end;\n                            if (isSelected) {\n                                if (sel.bgColor) |selBg| {\n                                    finalBg = selBg;\n                                    if (sel.fgColor) |selFg| {\n                                        finalFg = selFg;\n                                    }\n                                } else {\n                                    const temp = lineFg;\n                                    finalFg = if (lineBg[3] > 0) lineBg else RGBA{ 0.0, 0.0, 0.0, 1.0 };\n                                    finalBg = temp;\n                                }\n                                break;\n                            }\n                        }\n                    }\n\n                    // Skip zero-width characters (ZWJ, VS16, etc.) - don't render them\n                    // Don't increment col since they take no space\n                    if (g_width == 0) {\n                        continue;\n                    }\n\n                    var drawFg = finalFg;\n                    var drawBg = finalBg;\n                    const drawAttributes = finalAttributes;\n\n                    if (drawAttributes & (1 << 5) != 0) {\n                        const temp = drawFg;\n                        drawFg = drawBg;\n                        drawBg = temp;\n                    }\n\n                    if (grapheme_bytes.len == 1 and grapheme_bytes[0] == '\\t') {\n                        const tab_indicator = view.getTabIndicator();\n                        const tab_indicator_color = view.getTabIndicatorColor();\n\n                        var tab_col: u32 = 0;\n                        while (tab_col < g_width) : (tab_col += 1) {\n                            if (currentX + @as(i32, @intCast(tab_col)) >= @as(i32, @intCast(self.width))) break;\n\n                            const char = if (tab_col == 0 and tab_indicator != null) tab_indicator.? else DEFAULT_SPACE_CHAR;\n                            const fg = if (tab_col == 0 and tab_indicator_color != null) tab_indicator_color.? else drawFg;\n\n                            try self.setCellWithAlphaBlending(\n                                @intCast(currentX + @as(i32, @intCast(tab_col))),\n                                @intCast(currentY),\n                                char,\n                                fg,\n                                drawBg,\n                                drawAttributes,\n                            );\n                        }\n                    } else {\n                        var encoded_char: u32 = 0;\n                        if (grapheme_bytes.len == 1 and g_width == 1 and grapheme_bytes[0] >= 32) {\n                            encoded_char = @as(u32, grapheme_bytes[0]);\n                        } else {\n                            const gid = self.pool.alloc(grapheme_bytes) catch |err| {\n                                logger.warn(\"GraphemePool.alloc FAILED for grapheme (len={d}, bytes={any}): {}\", .{ grapheme_bytes.len, grapheme_bytes, err });\n                                globalCharPos += g_width;\n                                currentX += @as(i32, @intCast(g_width));\n                                col += g_width;\n                                continue;\n                            };\n                            encoded_char = gp.packGraphemeStart(gid & gp.GRAPHEME_ID_MASK, g_width);\n                        }\n\n                        try self.setCellWithAlphaBlending(\n                            @intCast(currentX),\n                            @intCast(currentY),\n                            encoded_char,\n                            drawFg,\n                            drawBg,\n                            drawAttributes,\n                        );\n                    }\n\n                    globalCharPos += g_width;\n                    currentX += @as(i32, @intCast(g_width));\n                    column_in_line += g_width;\n                    col += g_width;\n                }\n            }\n\n            const is_last_vline_of_logical_line = (slice_idx + 1 >= virtual_lines[firstVisibleLine..lastPossibleLine].len) or\n                (virtual_lines[firstVisibleLine..lastPossibleLine][slice_idx + 1].source_line != vline.source_line);\n\n            if (is_last_vline_of_logical_line) {\n                const is_last_logical_line = vline.source_line + 1 >= total_line_count;\n                if (!is_last_logical_line) {\n                    globalCharPos += 1;\n                }\n            }\n\n            currentY += 1;\n        }\n    }\n\n    /// Draw an EditorView to this OptimizedBuffer\n    /// EditorView wraps TextBufferView, so we just delegate to drawTextBufferInternal\n    /// EditorView handles viewport management and returns only the visible lines\n    pub fn drawEditorView(\n        self: *OptimizedBuffer,\n        editor_view: *EditorView,\n        x: i32,\n        y: i32,\n    ) !void {\n        try self.drawTextBufferInternal(EditorView, editor_view, x, y);\n    }\n\n    /// Draw a complete border grid in a single call.\n    /// columnOffsets and rowOffsets include an extra trailing entry so that\n    /// the range for column `i` is `[columnOffsets[i]+1 .. columnOffsets[i+1]-1]`.\n    pub fn drawGrid(\n        self: *OptimizedBuffer,\n        borderChars: [*]const u32,\n        borderFg: RGBA,\n        borderBg: RGBA,\n        columnOffsets: [*]const i32,\n        columnCount: u32,\n        rowOffsets: [*]const i32,\n        rowCount: u32,\n        drawInner: bool,\n        drawOuter: bool,\n    ) void {\n        if (rowCount == 0 or columnCount == 0) return;\n        if (!drawInner and !drawOuter) return;\n\n        const hChar = borderChars[@intFromEnum(BorderCharIndex.horizontal)];\n        const vChar = borderChars[@intFromEnum(BorderCharIndex.vertical)];\n        const bufWidth = self.width;\n        const bufHeight = self.height;\n        const bufWidthI32 = @as(i32, @intCast(bufWidth));\n        const bufHeightI32 = @as(i32, @intCast(bufHeight));\n\n        // Draw row-by-row: horizontal border line, then vertical borders for the row's content area\n        var rowIdx: u32 = 0;\n        while (rowIdx <= rowCount) : (rowIdx += 1) {\n            const is_outer_row = rowIdx == 0 or rowIdx == rowCount;\n            const should_draw_horizontal = if (is_outer_row) drawOuter else drawInner;\n            const borderY = rowOffsets[rowIdx];\n            if (borderY >= bufHeightI32) break;\n\n            // --- horizontal border line: intersections + fills ---\n            if (should_draw_horizontal and borderY >= 0) {\n                var colBorderIdx: u32 = 0;\n                while (colBorderIdx <= columnCount) : (colBorderIdx += 1) {\n                    const is_outer_col = colBorderIdx == 0 or colBorderIdx == columnCount;\n                    const should_draw_vertical = if (is_outer_col) drawOuter else drawInner;\n                    if (!should_draw_vertical) continue;\n\n                    const bx = columnOffsets[colBorderIdx];\n                    if (bx >= bufWidthI32) break;\n                    if (bx < 0) continue;\n\n                    const has_up = rowIdx > 0 and should_draw_vertical;\n                    const has_down = rowIdx < rowCount and should_draw_vertical;\n                    const has_left = colBorderIdx > 0;\n                    const has_right = colBorderIdx < columnCount;\n                    const intersection = tableBorderIntersectionByConnections(borderChars, has_up, has_down, has_left, has_right);\n\n                    self.setRaw(@as(u32, @intCast(bx)), @as(u32, @intCast(borderY)), Cell{ .char = intersection, .fg = borderFg, .bg = borderBg, .attributes = 0 });\n                }\n\n                var colIdx: u32 = 0;\n                while (colIdx < columnCount) : (colIdx += 1) {\n                    const has_boundary_after = if (colIdx < columnCount - 1) drawInner else drawOuter;\n                    const boundary_padding: i32 = if (has_boundary_after) 0 else 1;\n                    const startX = columnOffsets[colIdx] + 1;\n                    const endX = columnOffsets[colIdx + 1] + boundary_padding;\n\n                    if (startX >= bufWidthI32) break;\n                    if (endX <= 0) continue;\n\n                    const clampedStart = @as(u32, @intCast(@max(@as(i32, 0), startX)));\n                    const clampedEnd = @as(u32, @intCast(@min(bufWidthI32, endX)));\n\n                    if (clampedStart < clampedEnd) {\n                        const borderYU32 = @as(u32, @intCast(borderY));\n                        @memset(self.buffer.char[borderYU32 * bufWidth + clampedStart .. borderYU32 * bufWidth + clampedEnd], hChar);\n                        @memset(self.buffer.fg[borderYU32 * bufWidth + clampedStart .. borderYU32 * bufWidth + clampedEnd], borderFg);\n                        @memset(self.buffer.bg[borderYU32 * bufWidth + clampedStart .. borderYU32 * bufWidth + clampedEnd], borderBg);\n                        @memset(self.buffer.attributes[borderYU32 * bufWidth + clampedStart .. borderYU32 * bufWidth + clampedEnd], 0);\n                    }\n                }\n            }\n\n            if (rowIdx >= rowCount) break;\n\n            // --- vertical borders for each content line in this row ---\n            const has_row_boundary_after = if (rowIdx < rowCount - 1) drawInner else drawOuter;\n            const row_boundary_padding: i32 = if (has_row_boundary_after) 0 else 1;\n            const contentStartY = borderY + 1;\n            const contentEndY = rowOffsets[rowIdx + 1] + row_boundary_padding;\n            var cy = contentStartY;\n            while (cy < contentEndY and cy < bufHeightI32) : (cy += 1) {\n                if (cy < 0) continue;\n\n                const rowBase = @as(u32, @intCast(cy)) * bufWidth;\n                var colBorderIdx: u32 = 0;\n                while (colBorderIdx <= columnCount) : (colBorderIdx += 1) {\n                    const is_outer_col = colBorderIdx == 0 or colBorderIdx == columnCount;\n                    const should_draw_vertical = if (is_outer_col) drawOuter else drawInner;\n                    if (!should_draw_vertical) continue;\n\n                    const bx = columnOffsets[colBorderIdx];\n                    if (bx >= bufWidthI32) break;\n                    if (bx < 0) continue;\n\n                    const idx = rowBase + @as(u32, @intCast(bx));\n                    self.buffer.char[idx] = vChar;\n                    self.buffer.fg[idx] = borderFg;\n                    self.buffer.bg[idx] = borderBg;\n                    self.buffer.attributes[idx] = 0;\n                }\n            }\n        }\n    }\n\n    fn tableBorderIntersectionByConnections(borderChars: [*]const u32, hasUp: bool, hasDown: bool, hasLeft: bool, hasRight: bool) u32 {\n        if (hasUp and hasDown and hasLeft and hasRight) return borderChars[@intFromEnum(BorderCharIndex.cross)];\n\n        if (!hasUp and hasDown and !hasLeft and hasRight) return borderChars[@intFromEnum(BorderCharIndex.topLeft)];\n        if (!hasUp and hasDown and hasLeft and !hasRight) return borderChars[@intFromEnum(BorderCharIndex.topRight)];\n        if (hasUp and !hasDown and !hasLeft and hasRight) return borderChars[@intFromEnum(BorderCharIndex.bottomLeft)];\n        if (hasUp and !hasDown and hasLeft and !hasRight) return borderChars[@intFromEnum(BorderCharIndex.bottomRight)];\n\n        if (hasUp and hasDown and !hasLeft and hasRight) return borderChars[@intFromEnum(BorderCharIndex.leftT)];\n        if (hasUp and hasDown and hasLeft and !hasRight) return borderChars[@intFromEnum(BorderCharIndex.rightT)];\n        if (!hasUp and hasDown and hasLeft and hasRight) return borderChars[@intFromEnum(BorderCharIndex.topT)];\n        if (hasUp and !hasDown and hasLeft and hasRight) return borderChars[@intFromEnum(BorderCharIndex.bottomT)];\n\n        if ((hasLeft or hasRight) and !hasUp and !hasDown) return borderChars[@intFromEnum(BorderCharIndex.horizontal)];\n        if ((hasUp or hasDown) and !hasLeft and !hasRight) return borderChars[@intFromEnum(BorderCharIndex.vertical)];\n\n        return borderChars[@intFromEnum(BorderCharIndex.cross)];\n    }\n\n    /// Draw a box with borders and optional fill\n    pub fn drawBox(\n        self: *OptimizedBuffer,\n        x: i32,\n        y: i32,\n        width: u32,\n        height: u32,\n        borderChars: [*]const u32, // Array of 11 border characters\n        borderSides: BorderSides,\n        borderColor: RGBA,\n        backgroundColor: RGBA,\n        shouldFill: bool,\n        title: ?[]const u8,\n        titleAlignment: u8, // 0=left, 1=center, 2=right\n    ) !void {\n        const startX = @max(0, x);\n        const startY = @max(0, y);\n        const endX = @min(@as(i32, @intCast(self.width)) - 1, x + @as(i32, @intCast(width)) - 1);\n        const endY = @min(@as(i32, @intCast(self.height)) - 1, y + @as(i32, @intCast(height)) - 1);\n\n        if (startX > endX or startY > endY) return;\n\n        const boxWidth = @as(u32, @intCast(endX - startX + 1));\n        const boxHeight = @as(u32, @intCast(endY - startY + 1));\n        if (!self.isRectInScissor(startX, startY, boxWidth, boxHeight)) return;\n\n        const isAtActualLeft = startX == x;\n        const isAtActualRight = endX == x + @as(i32, @intCast(width)) - 1;\n        const isAtActualTop = startY == y;\n        const isAtActualBottom = endY == y + @as(i32, @intCast(height)) - 1;\n\n        var shouldDrawTitle = false;\n        var titleX: i32 = startX;\n        var titleStartX: i32 = 0;\n        var titleEndX: i32 = 0;\n\n        if (title) |titleText| {\n            if (titleText.len > 0 and borderSides.top and isAtActualTop) {\n                const is_ascii = utf8.isAsciiOnly(titleText);\n                const titleLength = @as(i32, @intCast(utf8.calculateTextWidth(titleText, 2, is_ascii, self.width_method)));\n                const minTitleSpace = 4;\n\n                shouldDrawTitle = @as(i32, @intCast(width)) >= titleLength + minTitleSpace;\n\n                if (shouldDrawTitle) {\n                    const padding = 2;\n\n                    if (titleAlignment == 1) { // center\n                        titleX = startX + @max(padding, @divFloor(@as(i32, @intCast(width)) - titleLength, 2));\n                    } else if (titleAlignment == 2) { // right\n                        titleX = startX + @as(i32, @intCast(width)) - padding - titleLength;\n                    } else { // left\n                        titleX = startX + padding;\n                    }\n\n                    titleX = @max(startX + padding, @min(titleX, endX - titleLength));\n                    titleStartX = titleX;\n                    titleEndX = titleX + titleLength - 1;\n                }\n            }\n        }\n\n        if (shouldFill) {\n            if (!borderSides.top and !borderSides.right and !borderSides.bottom and !borderSides.left) {\n                const fillWidth = @as(u32, @intCast(endX - startX + 1));\n                const fillHeight = @as(u32, @intCast(endY - startY + 1));\n                try self.fillRect(@intCast(startX), @intCast(startY), fillWidth, fillHeight, backgroundColor);\n            } else {\n                const innerStartX = startX + if (borderSides.left and isAtActualLeft) @as(i32, 1) else @as(i32, 0);\n                const innerStartY = startY + if (borderSides.top and isAtActualTop) @as(i32, 1) else @as(i32, 0);\n                const innerEndX = endX - if (borderSides.right and isAtActualRight) @as(i32, 1) else @as(i32, 0);\n                const innerEndY = endY - if (borderSides.bottom and isAtActualBottom) @as(i32, 1) else @as(i32, 0);\n\n                if (innerEndX >= innerStartX and innerEndY >= innerStartY) {\n                    const fillWidth = @as(u32, @intCast(innerEndX - innerStartX + 1));\n                    const fillHeight = @as(u32, @intCast(innerEndY - innerStartY + 1));\n                    try self.fillRect(@intCast(innerStartX), @intCast(innerStartY), fillWidth, fillHeight, backgroundColor);\n                }\n            }\n        }\n\n        // Special cases for extending vertical borders\n        const leftBorderOnly = borderSides.left and isAtActualLeft and !borderSides.top and !borderSides.bottom;\n        const rightBorderOnly = borderSides.right and isAtActualRight and !borderSides.top and !borderSides.bottom;\n        const bottomOnlyWithVerticals = borderSides.bottom and isAtActualBottom and !borderSides.top and (borderSides.left or borderSides.right);\n        const topOnlyWithVerticals = borderSides.top and isAtActualTop and !borderSides.bottom and (borderSides.left or borderSides.right);\n\n        const extendVerticalsToTop = leftBorderOnly or rightBorderOnly or bottomOnlyWithVerticals;\n        const extendVerticalsToBottom = leftBorderOnly or rightBorderOnly or topOnlyWithVerticals;\n\n        // Draw horizontal borders\n        if (borderSides.top or borderSides.bottom) {\n            // Draw top border\n            if (borderSides.top and isAtActualTop) {\n                var drawX = startX;\n                while (drawX <= endX) : (drawX += 1) {\n                    if (startY >= 0 and startY < @as(i32, @intCast(self.height))) {\n                        if (shouldDrawTitle and drawX >= titleStartX and drawX <= titleEndX) {\n                            continue;\n                        }\n\n                        var char = borderChars[@intFromEnum(BorderCharIndex.horizontal)];\n\n                        // Handle corners\n                        if (drawX == startX and isAtActualLeft) {\n                            char = if (borderSides.left) borderChars[@intFromEnum(BorderCharIndex.topLeft)] else borderChars[@intFromEnum(BorderCharIndex.horizontal)];\n                        } else if (drawX == endX and isAtActualRight) {\n                            char = if (borderSides.right) borderChars[@intFromEnum(BorderCharIndex.topRight)] else borderChars[@intFromEnum(BorderCharIndex.horizontal)];\n                        }\n\n                        try self.setCellWithAlphaBlending(@intCast(drawX), @intCast(startY), char, borderColor, backgroundColor, 0);\n                    }\n                }\n            }\n\n            // Draw bottom border\n            if (borderSides.bottom and isAtActualBottom) {\n                var drawX = startX;\n                while (drawX <= endX) : (drawX += 1) {\n                    if (endY >= 0 and endY < @as(i32, @intCast(self.height))) {\n                        var char = borderChars[@intFromEnum(BorderCharIndex.horizontal)];\n\n                        // Handle corners\n                        if (drawX == startX and isAtActualLeft) {\n                            char = if (borderSides.left) borderChars[@intFromEnum(BorderCharIndex.bottomLeft)] else borderChars[@intFromEnum(BorderCharIndex.horizontal)];\n                        } else if (drawX == endX and isAtActualRight) {\n                            char = if (borderSides.right) borderChars[@intFromEnum(BorderCharIndex.bottomRight)] else borderChars[@intFromEnum(BorderCharIndex.horizontal)];\n                        }\n\n                        try self.setCellWithAlphaBlending(@intCast(drawX), @intCast(endY), char, borderColor, backgroundColor, 0);\n                    }\n                }\n            }\n        }\n\n        // Draw vertical borders\n        const verticalStartY = if (extendVerticalsToTop) startY else startY + if (borderSides.top and isAtActualTop) @as(i32, 1) else @as(i32, 0);\n        const verticalEndY = if (extendVerticalsToBottom) endY else endY - if (borderSides.bottom and isAtActualBottom) @as(i32, 1) else @as(i32, 0);\n\n        if (borderSides.left or borderSides.right) {\n            var drawY = verticalStartY;\n            while (drawY <= verticalEndY) : (drawY += 1) {\n                // Left border\n                if (borderSides.left and isAtActualLeft and startX >= 0 and startX < @as(i32, @intCast(self.width))) {\n                    try self.setCellWithAlphaBlending(@intCast(startX), @intCast(drawY), borderChars[@intFromEnum(BorderCharIndex.vertical)], borderColor, backgroundColor, 0);\n                }\n\n                // Right border\n                if (borderSides.right and isAtActualRight and endX >= 0 and endX < @as(i32, @intCast(self.width))) {\n                    try self.setCellWithAlphaBlending(@intCast(endX), @intCast(drawY), borderChars[@intFromEnum(BorderCharIndex.vertical)], borderColor, backgroundColor, 0);\n                }\n            }\n        }\n\n        if (shouldDrawTitle) {\n            if (title) |titleText| {\n                try self.drawText(titleText, @intCast(titleX), @intCast(startY), borderColor, backgroundColor, 0);\n            }\n        }\n    }\n\n    /// Draw a buffer of pixel data using super sampling (2x2 pixels per character cell)\n    /// alignedBytesPerRow: The number of bytes per row in the pixelData buffer, considering alignment/padding.\n    pub fn drawSuperSampleBuffer(\n        self: *OptimizedBuffer,\n        posX: u32,\n        posY: u32,\n        pixelData: [*]const u8,\n        len: usize,\n        format: u8, // 0: bgra8unorm, 1: rgba8unorm\n        alignedBytesPerRow: u32,\n    ) !void {\n        const bytesPerPixel = 4;\n        const isBGRA = (format == 0);\n\n        // TODO: A more robust implementation might take source width/height explicitly.\n\n        var y_cell = posY;\n        while (y_cell < self.height) : (y_cell += 1) {\n            var x_cell = posX;\n            while (x_cell < self.width) : (x_cell += 1) {\n                if (!self.isPointInScissor(@intCast(x_cell), @intCast(y_cell))) {\n                    continue;\n                }\n\n                const renderX: u32 = (x_cell - posX) * 2;\n                const renderY: u32 = (y_cell - posY) * 2;\n\n                const tlIndex: usize = @intCast(renderY * alignedBytesPerRow + renderX * bytesPerPixel);\n                const trIndex: usize = tlIndex + bytesPerPixel;\n                const blIndex: usize = @intCast((renderY + 1) * alignedBytesPerRow + renderX * bytesPerPixel);\n                const brIndex: usize = blIndex + bytesPerPixel;\n\n                const indices = [_]usize{ tlIndex, trIndex, blIndex, brIndex };\n\n                // Get RGBA colors for TL, TR, BL, BR\n                var pixelsRgba: [4]RGBA = undefined;\n                pixelsRgba[0] = getPixelColor(indices[0], pixelData, len, isBGRA); // TL\n                pixelsRgba[1] = getPixelColor(indices[1], pixelData, len, isBGRA); // TR\n                pixelsRgba[2] = getPixelColor(indices[2], pixelData, len, isBGRA); // BL\n                pixelsRgba[3] = getPixelColor(indices[3], pixelData, len, isBGRA); // BR\n\n                const cellResult = renderQuadrantBlock(pixelsRgba);\n\n                try self.setCellWithAlphaBlending(x_cell, y_cell, cellResult.char, cellResult.fg, cellResult.bg, 0);\n            }\n        }\n    }\n\n    /// Draw a buffer of pixel data using pre-computed super sample results from compute shader\n    /// data contains an array of CellResult structs (48 bytes each)\n    /// Each CellResult: bg(16) + fg(16) + char(4) + padding1(4) + padding2(4) + padding3(4) = 48 bytes\n    pub fn drawPackedBuffer(\n        self: *OptimizedBuffer,\n        data: [*]const u8,\n        dataLen: usize,\n        posX: u32,\n        posY: u32,\n        terminalWidthCells: u32,\n        terminalHeightCells: u32,\n    ) void {\n        const cellResultSize = 48;\n        const numCells = dataLen / cellResultSize;\n        const bufferWidthCells = terminalWidthCells;\n\n        var i: usize = 0;\n        while (i < numCells) : (i += 1) {\n            const cellDataOffset = i * cellResultSize;\n\n            const cellX = posX + @as(u32, @intCast(i % bufferWidthCells));\n            const cellY = posY + @as(u32, @intCast(i / bufferWidthCells));\n\n            if (cellX >= terminalWidthCells or cellY >= terminalHeightCells) continue;\n            if (cellX >= self.width or cellY >= self.height) continue;\n\n            if (!self.isPointInScissor(@intCast(cellX), @intCast(cellY))) continue;\n\n            const bgPtr = @as([*]const f32, @ptrCast(@alignCast(data + cellDataOffset)));\n            const bg: RGBA = .{ bgPtr[0], bgPtr[1], bgPtr[2], bgPtr[3] };\n\n            const fgPtr = @as([*]const f32, @ptrCast(@alignCast(data + cellDataOffset + 16)));\n            const fg: RGBA = .{ fgPtr[0], fgPtr[1], fgPtr[2], fgPtr[3] };\n\n            const charPtr = @as([*]const u32, @ptrCast(@alignCast(data + cellDataOffset + 32)));\n            var char = charPtr[0];\n\n            if (char == 0 or char > MAX_UNICODE_CODEPOINT) {\n                char = DEFAULT_SPACE_CHAR;\n            }\n\n            if (char < 32 or (char > 126 and char < 0x2580)) {\n                char = BLOCK_CHAR;\n            }\n\n            self.setCellWithAlphaBlending(cellX, cellY, char, fg, bg, 0) catch {};\n        }\n    }\n\n    fn getGrayscaleChar(intensity: f32) u32 {\n        if (intensity < 0.01) return ' ';\n        const clamped = @min(@max(intensity, 0.0), 1.0);\n        const index: usize = @intFromFloat(clamped * @as(f32, @floatFromInt(GRAYSCALE_CHARS.len - 1)));\n        return @as(u32, GRAYSCALE_CHARS[index]);\n    }\n\n    pub fn drawGrayscaleBuffer(\n        self: *OptimizedBuffer,\n        posX: i32,\n        posY: i32,\n        intensities: [*]const f32,\n        srcWidth: u32,\n        srcHeight: u32,\n        fgColor: ?RGBA,\n        bgColor: ?RGBA,\n    ) void {\n        const bg = bgColor orelse RGBA{ 0.0, 0.0, 0.0, 0.0 };\n        if (srcWidth == 0 or srcHeight == 0) return;\n        if (posX >= @as(i32, @intCast(self.width)) or posY >= @as(i32, @intCast(self.height))) return;\n\n        const startX: u32 = if (posX < 0) @intCast(-posX) else 0;\n        const startY: u32 = if (posY < 0) @intCast(-posY) else 0;\n\n        const destStartX: u32 = if (posX < 0) 0 else @intCast(posX);\n        const destStartY: u32 = if (posY < 0) 0 else @intCast(posY);\n\n        if (startX >= srcWidth or startY >= srcHeight) return;\n\n        const visibleWidth = @min(srcWidth - startX, self.width - destStartX);\n        const visibleHeight = @min(srcHeight - startY, self.height - destStartY);\n\n        if (visibleWidth == 0 or visibleHeight == 0) return;\n\n        const baseFg = fgColor orelse RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n        const opacity = self.getCurrentOpacity();\n        const graphemeAware = self.grapheme_tracker.hasAny();\n        const linkAware = self.link_tracker.hasAny();\n\n        var srcY: u32 = startY;\n        var destY: u32 = destStartY;\n        while (srcY < startY + visibleHeight) : ({\n            srcY += 1;\n            destY += 1;\n        }) {\n            var srcX: u32 = startX;\n            var destX: u32 = destStartX;\n            while (srcX < startX + visibleWidth) : ({\n                srcX += 1;\n                destX += 1;\n            }) {\n                if (!self.isPointInScissor(@intCast(destX), @intCast(destY))) continue;\n\n                const srcIndex = srcY * srcWidth + srcX;\n                const intensity = intensities[srcIndex];\n\n                if (intensity < 0.01) continue;\n\n                const char = getGrayscaleChar(intensity);\n\n                const gray = @min(@max(intensity, 0.0), 1.0);\n                const fg: RGBA = .{ baseFg[0], baseFg[1], baseFg[2], gray * baseFg[3] * opacity };\n\n                if (graphemeAware or linkAware) {\n                    self.setCellWithAlphaBlending(destX, destY, char, fg, bg, 0) catch {};\n                } else {\n                    self.setCellWithAlphaBlendingRaw(destX, destY, char, fg, bg, 0) catch {};\n                }\n            }\n        }\n    }\n\n    pub fn drawGrayscaleBufferSupersampled(\n        self: *OptimizedBuffer,\n        posX: i32,\n        posY: i32,\n        intensities: [*]const f32,\n        srcWidth: u32,\n        srcHeight: u32,\n        fgColor: ?RGBA,\n        bgColor: ?RGBA,\n    ) void {\n        const bg = bgColor orelse RGBA{ 0.0, 0.0, 0.0, 0.0 };\n        const termWidth = srcWidth / 2;\n        const termHeight = srcHeight / 2;\n\n        if (termWidth == 0 or termHeight == 0) return;\n        if (posX >= @as(i32, @intCast(self.width)) or posY >= @as(i32, @intCast(self.height))) return;\n\n        const startX: u32 = if (posX < 0) @intCast(-posX) else 0;\n        const startY: u32 = if (posY < 0) @intCast(-posY) else 0;\n\n        const destStartX: u32 = if (posX < 0) 0 else @intCast(posX);\n        const destStartY: u32 = if (posY < 0) 0 else @intCast(posY);\n\n        if (startX >= termWidth or startY >= termHeight) return;\n\n        const visibleWidth = @min(termWidth - startX, self.width - destStartX);\n        const visibleHeight = @min(termHeight - startY, self.height - destStartY);\n\n        if (visibleWidth == 0 or visibleHeight == 0) return;\n\n        const baseFg = fgColor orelse RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n        const opacity = self.getCurrentOpacity();\n        const graphemeAware = self.grapheme_tracker.hasAny();\n        const linkAware = self.link_tracker.hasAny();\n\n        const maxIdx = srcHeight * srcWidth;\n        var cellY: u32 = startY;\n        var destY: u32 = destStartY;\n        while (cellY < startY + visibleHeight) : ({\n            cellY += 1;\n            destY += 1;\n        }) {\n            var cellX: u32 = startX;\n            var destX: u32 = destStartX;\n            while (cellX < startX + visibleWidth) : ({\n                cellX += 1;\n                destX += 1;\n            }) {\n                if (!self.isPointInScissor(@intCast(destX), @intCast(destY))) continue;\n\n                const qx = cellX * 2;\n                const qy = cellY * 2;\n\n                const tlIdx = qy * srcWidth + qx;\n                const trIdx = qy * srcWidth + qx + 1;\n                const blIdx = (qy + 1) * srcWidth + qx;\n                const brIdx = (qy + 1) * srcWidth + qx + 1;\n\n                const tl: f32 = if (tlIdx < maxIdx) intensities[tlIdx] else 0.0;\n                const tr: f32 = if (trIdx < maxIdx and qx + 1 < srcWidth) intensities[trIdx] else 0.0;\n                const bl: f32 = if (blIdx < maxIdx and qy + 1 < srcHeight) intensities[blIdx] else 0.0;\n                const br: f32 = if (brIdx < maxIdx and qx + 1 < srcWidth and qy + 1 < srcHeight) intensities[brIdx] else 0.0;\n\n                const avgIntensity = (tl + tr + bl + br) / 4.0;\n\n                if (avgIntensity < 0.01) continue;\n\n                const char = getGrayscaleChar(avgIntensity);\n\n                const gray = @min(@max(avgIntensity, 0.0), 1.0);\n                const fg: RGBA = .{ baseFg[0], baseFg[1], baseFg[2], gray * baseFg[3] * opacity };\n\n                if (graphemeAware or linkAware) {\n                    self.setCellWithAlphaBlending(destX, destY, char, fg, bg, 0) catch {};\n                } else {\n                    self.setCellWithAlphaBlendingRaw(destX, destY, char, fg, bg, 0) catch {};\n                }\n            }\n        }\n    }\n};\n\nfn getPixelColor(idx: usize, data: [*]const u8, dataLen: usize, bgra: bool) RGBA {\n    if (idx + 3 >= dataLen) {\n        return .{ 1.0, 0.0, 1.0, 0.0 }; // Return Transparent Magenta for out-of-bounds\n    }\n    var rByte: u8 = undefined;\n    var gByte: u8 = undefined;\n    var bByte: u8 = undefined;\n    var aByte: u8 = undefined;\n\n    if (bgra) {\n        bByte = data[idx];\n        gByte = data[idx + 1];\n        rByte = data[idx + 2];\n        aByte = data[idx + 3];\n    } else { // Assume RGBA\n        rByte = data[idx];\n        gByte = data[idx + 1];\n        bByte = data[idx + 2];\n        aByte = data[idx + 3];\n    }\n\n    return .{\n        @as(f32, @floatFromInt(rByte)) * INV_255,\n        @as(f32, @floatFromInt(gByte)) * INV_255,\n        @as(f32, @floatFromInt(bByte)) * INV_255,\n        @as(f32, @floatFromInt(aByte)) * INV_255,\n    };\n}\n\nconst quadrantChars = [_]u32{\n    32, // 0000\n    0x2597, // 0001 BR ░\n    0x2596, // 0010 BL ░\n    0x2584, // 0011 Lower Half Block ▄\n    0x259D, // 0100 TR ░\n    0x2590, // 0101 Right Half Block ▐\n    0x259E, // 0110 TR+BL ░\n    0x259F, // 0111 TR+BL+BR ░\n    0x2598, // 1000 TL ░\n    0x259A, // 1001 TL+BR ░\n    0x258C, // 1010 Left Half Block ▌\n    0x2599, // 1011 TL+BL+BR ░\n    0x2580, // 1100 Upper Half Block ▀\n    0x259C, // 1101 TL+TR+BR ░\n    0x259B, // 1110 TL+TR+BL ░\n    0x2588, // 1111 Full Block █\n};\n\nfn colorDistance(a: RGBA, b: RGBA) f32 {\n    const dr = a[0] - b[0];\n    const dg = a[1] - b[1];\n    const db = a[2] - b[2];\n    return dr * dr + dg * dg + db * db;\n}\n\nfn closestColorIndex(pixel: RGBA, candidates: [2]RGBA) u1 {\n    return if (colorDistance(pixel, candidates[0]) <= colorDistance(pixel, candidates[1])) 0 else 1;\n}\n\nfn averageColorRgba(pixels: []const RGBA) RGBA {\n    if (pixels.len == 0) return .{ 0.0, 0.0, 0.0, 0.0 };\n\n    var sumR: f32 = 0.0;\n    var sumG: f32 = 0.0;\n    var sumB: f32 = 0.0;\n    var sumA: f32 = 0.0;\n\n    for (pixels) |p| {\n        sumR += p[0];\n        sumG += p[1];\n        sumB += p[2];\n        sumA += p[3];\n    }\n\n    const len = @as(f32, @floatFromInt(pixels.len));\n    return .{ sumR / len, sumG / len, sumB / len, sumA / len };\n}\n\nfn luminance(color: RGBA) f32 {\n    return 0.2126 * color[0] + 0.7152 * color[1] + 0.0722 * color[2];\n}\n\npub const QuadrantResult = struct {\n    char: u32,\n    fg: RGBA,\n    bg: RGBA,\n};\n\n// Calculate the quadrant block character and colors from RGBA pixels\nfn renderQuadrantBlock(pixels: [4]RGBA) QuadrantResult {\n    // 1. Find the most different pair of pixels\n    var p_idxA: u3 = 0;\n    var p_idxB: u3 = 1;\n    var maxDist = colorDistance(pixels[0], pixels[1]);\n\n    inline for (0..4) |i| {\n        inline for ((i + 1)..4) |j| {\n            const dist = colorDistance(pixels[i], pixels[j]);\n            if (dist > maxDist) {\n                p_idxA = @intCast(i);\n                p_idxB = @intCast(j);\n                maxDist = dist;\n            }\n        }\n    }\n    const p_candA = pixels[p_idxA];\n    const p_candB = pixels[p_idxB];\n\n    // 2. Determine chosen_dark_color and chosen_light_color based on luminance\n    var chosen_dark_color: RGBA = undefined;\n    var chosen_light_color: RGBA = undefined;\n\n    if (luminance(p_candA) <= luminance(p_candB)) {\n        chosen_dark_color = p_candA;\n        chosen_light_color = p_candB;\n    } else {\n        chosen_dark_color = p_candB;\n        chosen_light_color = p_candA;\n    }\n\n    // 3. Classify quadrants and build quadrantBits\n    var quadrantBits: u4 = 0;\n    const bitValues = [_]u4{ 8, 4, 2, 1 };\n\n    inline for (0..4) |i| {\n        const pixelRgba = pixels[i];\n        if (closestColorIndex(pixelRgba, .{ chosen_dark_color, chosen_light_color }) == 0) {\n            quadrantBits |= bitValues[i];\n        }\n    }\n\n    // 4. Construct Result\n    if (quadrantBits == 0) { // All light\n        return QuadrantResult{\n            .char = 32,\n            .fg = chosen_dark_color,\n            .bg = averageColorRgba(pixels[0..4]),\n        };\n    } else if (quadrantBits == 15) { // All dark\n        return QuadrantResult{\n            .char = quadrantChars[15],\n            .fg = averageColorRgba(pixels[0..4]),\n            .bg = chosen_light_color,\n        };\n    } else { // Mixed pattern\n        return QuadrantResult{\n            .char = quadrantChars[quadrantBits],\n            .fg = chosen_dark_color,\n            .bg = chosen_light_color,\n        };\n    }\n}\n"
  },
  {
    "path": "packages/core/src/zig/build.zig",
    "content": "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst SupportedZigVersion = struct {\n    major: u32,\n    minor: u32,\n    patch: u32,\n};\n\nconst SUPPORTED_ZIG_VERSIONS = [_]SupportedZigVersion{\n    .{ .major = 0, .minor = 15, .patch = 2 },\n};\n\nconst SupportedTarget = struct {\n    zig_target: []const u8,\n    output_name: []const u8,\n    description: []const u8,\n};\n\nconst SUPPORTED_TARGETS = [_]SupportedTarget{\n    .{ .zig_target = \"x86_64-linux\", .output_name = \"x86_64-linux\", .description = \"Linux x86_64\" },\n    .{ .zig_target = \"aarch64-linux\", .output_name = \"aarch64-linux\", .description = \"Linux aarch64\" },\n    .{ .zig_target = \"x86_64-macos\", .output_name = \"x86_64-macos\", .description = \"macOS x86_64 (Intel)\" },\n    .{ .zig_target = \"aarch64-macos\", .output_name = \"aarch64-macos\", .description = \"macOS aarch64 (Apple Silicon)\" },\n    .{ .zig_target = \"x86_64-windows-gnu\", .output_name = \"x86_64-windows\", .description = \"Windows x86_64\" },\n    .{ .zig_target = \"aarch64-windows-gnu\", .output_name = \"aarch64-windows\", .description = \"Windows aarch64\" },\n};\n\nconst LIB_NAME = \"opentui\";\nconst ROOT_SOURCE_FILE = \"lib.zig\";\n\n/// Apply dependencies to a module\nfn applyDependencies(\n    b: *std.Build,\n    module: *std.Build.Module,\n    optimize: std.builtin.OptimizeMode,\n    target: std.Build.ResolvedTarget,\n    build_options: *std.Build.Step.Options,\n) void {\n    module.addOptions(\"build_options\", build_options);\n\n    // Add uucode for grapheme break detection and width calculation\n    if (b.lazyDependency(\"uucode\", .{\n        .target = target,\n        .optimize = optimize,\n        .fields = @as([]const []const u8, &.{\n            \"grapheme_break\",\n            \"east_asian_width\",\n            \"general_category\",\n            \"is_emoji_presentation\",\n        }),\n    })) |uucode_dep| {\n        module.addImport(\"uucode\", uucode_dep.module(\"uucode\"));\n    }\n}\n\nfn checkZigVersion() void {\n    const current_version = builtin.zig_version;\n    var is_supported = false;\n\n    for (SUPPORTED_ZIG_VERSIONS) |supported| {\n        if (current_version.major == supported.major and\n            current_version.minor == supported.minor and\n            current_version.patch == supported.patch)\n        {\n            is_supported = true;\n            break;\n        }\n    }\n\n    if (!is_supported) {\n        std.debug.print(\"\\x1b[31mError: Unsupported Zig version {}.{}.{}\\x1b[0m\\n\", .{\n            current_version.major,\n            current_version.minor,\n            current_version.patch,\n        });\n        std.debug.print(\"Supported Zig versions:\\n\", .{});\n        for (SUPPORTED_ZIG_VERSIONS) |supported| {\n            std.debug.print(\"  - {}.{}.{}\\n\", .{\n                supported.major,\n                supported.minor,\n                supported.patch,\n            });\n        }\n        std.debug.print(\"\\nPlease install a supported Zig version to continue.\\n\", .{});\n        std.process.exit(1);\n    }\n}\n\npub fn build(b: *std.Build) void {\n    checkZigVersion();\n\n    const optimize = b.standardOptimizeOption(.{});\n    const bench_optimize = b.option(std.builtin.OptimizeMode, \"bench-optimize\", \"Optimize mode for benchmarks\") orelse .ReleaseFast;\n    const debug_use_llvm = b.option(bool, \"debug-llvm\", \"Use LLVM backend for debug/test artifacts\");\n    const target_option = b.option([]const u8, \"target\", \"Build for specific target (e.g., 'x86_64-linux-gnu').\");\n    const build_all = b.option(bool, \"all\", \"Build for all supported targets\") orelse false;\n    const gpa_safe_stats = b.option(bool, \"gpa-safe-stats\", \"Enable GPA safety checks for trustworthy allocator stats\") orelse false;\n    const build_options = b.addOptions();\n    build_options.addOption(bool, \"gpa_safe_stats\", gpa_safe_stats);\n\n    if (target_option) |target_str| {\n        // Build single target\n        buildSingleTarget(b, target_str, optimize, build_options) catch |err| {\n            std.debug.print(\"Error building target '{s}': {}\\n\", .{ target_str, err });\n            std.process.exit(1);\n        };\n    } else if (build_all) {\n        // Build all supported targets\n        buildAllTargets(b, optimize, build_options);\n    } else {\n        // Build for native target only (default)\n        buildNativeTarget(b, optimize, build_options);\n    }\n\n    // Test step (native only)\n    const test_step = b.step(\"test\", \"Run unit tests\");\n    const native_target = b.resolveTargetQuery(.{});\n    const test_mod = b.createModule(.{\n        .root_source_file = b.path(\"test.zig\"),\n        .target = native_target,\n        .optimize = .Debug,\n    });\n    applyDependencies(b, test_mod, .Debug, native_target, build_options);\n    const run_test = b.addRunArtifact(b.addTest(.{\n        .root_module = test_mod,\n        .filters = if (b.option([]const u8, \"test-filter\", \"Skip tests that do not match filter\")) |f| &.{f} else &.{},\n        .use_llvm = debug_use_llvm,\n    }));\n    test_step.dependOn(&run_test.step);\n\n    // Bench step (native only)\n    const bench_step = b.step(\"bench\", \"Run benchmarks\");\n    const bench_mod = b.createModule(.{\n        .root_source_file = b.path(\"bench.zig\"),\n        .target = native_target,\n        .optimize = bench_optimize,\n    });\n    applyDependencies(b, bench_mod, bench_optimize, native_target, build_options);\n    const bench_exe = b.addExecutable(.{\n        .name = \"opentui-bench\",\n        .root_module = bench_mod,\n    });\n    const run_bench = b.addRunArtifact(bench_exe);\n    if (b.args) |args| {\n        run_bench.addArgs(args);\n    }\n    bench_step.dependOn(&run_bench.step);\n\n    const bench_ffi_step = b.step(\"bench-ffi\", \"Build NativeSpanFeed benchmark library\");\n    const bench_ffi_mod = b.createModule(.{\n        .root_source_file = b.path(\"native-span-feed-bench-lib.zig\"),\n        .target = native_target,\n        .optimize = bench_optimize,\n    });\n    applyDependencies(b, bench_ffi_mod, bench_optimize, native_target, build_options);\n    const bench_ffi_lib = b.addLibrary(.{\n        .name = \"native_span_feed_bench\",\n        .root_module = bench_ffi_mod,\n        .linkage = .dynamic,\n    });\n    const install_bench_ffi = b.addInstallArtifact(bench_ffi_lib, .{});\n    bench_ffi_step.dependOn(&install_bench_ffi.step);\n    bench_step.dependOn(bench_ffi_step);\n\n    // Debug step (native only)\n    const debug_step = b.step(\"debug\", \"Run debug executable\");\n    const debug_mod = b.createModule(.{\n        .root_source_file = b.path(\"debug-view.zig\"),\n        .target = native_target,\n        .optimize = .Debug,\n    });\n    applyDependencies(b, debug_mod, .Debug, native_target, build_options);\n    const debug_exe = b.addExecutable(.{\n        .name = \"opentui-debug\",\n        .root_module = debug_mod,\n        .use_llvm = debug_use_llvm,\n    });\n    const run_debug = b.addRunArtifact(debug_exe);\n    debug_step.dependOn(&run_debug.step);\n}\n\nfn buildAllTargets(b: *std.Build, optimize: std.builtin.OptimizeMode, build_options: *std.Build.Step.Options) void {\n    for (SUPPORTED_TARGETS) |supported_target| {\n        buildTarget(\n            b,\n            supported_target.zig_target,\n            supported_target.output_name,\n            supported_target.description,\n            optimize,\n            build_options,\n        ) catch |err| {\n            std.debug.print(\"Failed to build target {s}: {}\\n\", .{ supported_target.description, err });\n            continue;\n        };\n    }\n}\n\nfn buildNativeTarget(b: *std.Build, optimize: std.builtin.OptimizeMode, build_options: *std.Build.Step.Options) void {\n    // Find the matching supported target for the native platform\n    const native_arch = @tagName(builtin.cpu.arch);\n    const native_os = @tagName(builtin.os.tag);\n\n    for (SUPPORTED_TARGETS) |supported_target| {\n        // Check if this target matches the native platform\n        if (std.mem.indexOf(u8, supported_target.zig_target, native_arch) != null and\n            std.mem.indexOf(u8, supported_target.zig_target, native_os) != null)\n        {\n            buildTarget(\n                b,\n                supported_target.zig_target,\n                supported_target.output_name,\n                supported_target.description,\n                optimize,\n                build_options,\n            ) catch |err| {\n                std.debug.print(\"Failed to build native target {s}: {}\\n\", .{ supported_target.description, err });\n            };\n            return;\n        }\n    }\n\n    std.debug.print(\"No matching supported target for native platform ({s}-{s})\\n\", .{ native_arch, native_os });\n}\n\nfn buildSingleTarget(\n    b: *std.Build,\n    target_str: []const u8,\n    optimize: std.builtin.OptimizeMode,\n    build_options: *std.Build.Step.Options,\n) !void {\n    // Check if it matches a known target, use its output_name\n    for (SUPPORTED_TARGETS) |supported_target| {\n        if (std.mem.eql(u8, target_str, supported_target.zig_target)) {\n            try buildTarget(\n                b,\n                supported_target.zig_target,\n                supported_target.output_name,\n                supported_target.description,\n                optimize,\n                build_options,\n            );\n            return;\n        }\n    }\n    // Custom target - use target string as output name\n    const description = try std.fmt.allocPrint(b.allocator, \"Custom target: {s}\", .{target_str});\n    try buildTarget(b, target_str, target_str, description, optimize, build_options);\n}\n\nfn buildTarget(\n    b: *std.Build,\n    zig_target: []const u8,\n    output_name: []const u8,\n    description: []const u8,\n    optimize: std.builtin.OptimizeMode,\n    build_options: *std.Build.Step.Options,\n) !void {\n    const target_query = try std.Target.Query.parse(.{ .arch_os_abi = zig_target });\n    const target = b.resolveTargetQuery(target_query);\n\n    const module = b.createModule(.{\n        .root_source_file = b.path(ROOT_SOURCE_FILE),\n        .target = target,\n        .optimize = optimize,\n    });\n\n    applyDependencies(b, module, optimize, target, build_options);\n\n    const lib = b.addLibrary(.{\n        .name = LIB_NAME,\n        .root_module = module,\n        .linkage = .dynamic,\n    });\n\n    const install_dir = b.addInstallArtifact(lib, .{\n        .dest_dir = .{\n            .override = .{\n                .custom = try std.fmt.allocPrint(b.allocator, \"../lib/{s}\", .{output_name}),\n            },\n        },\n    });\n\n    const build_step_name = try std.fmt.allocPrint(b.allocator, \"build-{s}\", .{output_name});\n    const build_step = b.step(build_step_name, try std.fmt.allocPrint(b.allocator, \"Build for {s}\", .{description}));\n    build_step.dependOn(&install_dir.step);\n\n    b.getInstallStep().dependOn(&install_dir.step);\n}\n"
  },
  {
    "path": "packages/core/src/zig/build.zig.zon",
    "content": ".{\n    .name = .opentui,\n    .version = \"0.1.11\",\n    .fingerprint = 0x5445027d063f5083,\n    .minimum_zig_version = \"0.15.2\",\n    .dependencies = .{\n        .uucode = .{\n            .url = \"https://github.com/jacobsandlund/uucode/archive/84ceda8561a17ba4a9b96ac5c583f779660bbd4e.tar.gz\",\n            .hash = \"uucode-0.1.0-ZZjBPtA_TQCWp5PIKmfm5tu1WOkKWFmBGFEMxircPfkA\",\n        },\n    },\n    .paths = .{\n        \"build.zig\",\n        \"build.zig.zon\",\n    },\n}\n"
  },
  {
    "path": "packages/core/src/zig/edit-buffer.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst tb = @import(\"text-buffer.zig\");\nconst iter_mod = @import(\"text-buffer-iterators.zig\");\nconst seg_mod = @import(\"text-buffer-segment.zig\");\nconst gp = @import(\"grapheme.zig\");\nconst link = @import(\"link.zig\");\n\nconst utf8 = @import(\"utf8.zig\");\nconst event_emitter = @import(\"event-emitter.zig\");\nconst event_bus = @import(\"event-bus.zig\");\n\nconst UnifiedTextBuffer = tb.UnifiedTextBuffer;\nconst TextChunk = seg_mod.TextChunk;\nconst Segment = seg_mod.Segment;\nconst UnifiedRope = seg_mod.UnifiedRope;\n\nvar global_edit_buffer_id: u16 = 0;\n\npub const EditBufferError = error{\n    OutOfMemory,\n    InvalidCursor,\n};\n\npub const EditBufferEvent = enum {\n    cursorChanged,\n};\n\n/// Cursor position (row, col in display-width coordinates)\npub const Cursor = struct {\n    row: u32,\n    col: u32,\n    desired_col: u32 = 0,\n    offset: u32 = 0, // Global display-width offset from buffer start\n};\n\nconst CursorCoords = struct { row: u32, col: u32 };\n\nconst AddBuffer = struct {\n    mem_id: u8,\n    ptr: [*]u8,\n    len: usize,\n    cap: usize,\n    allocator: Allocator,\n\n    fn init(allocator: Allocator, text_buffer: *UnifiedTextBuffer, initial_cap: usize) !AddBuffer {\n        const mem = try allocator.alloc(u8, initial_cap);\n        const mem_id = try text_buffer.registerMemBuffer(mem, true);\n\n        return .{\n            .mem_id = mem_id,\n            .ptr = mem.ptr,\n            .len = 0,\n            .cap = mem.len,\n            .allocator = allocator,\n        };\n    }\n\n    fn ensureCapacity(self: *AddBuffer, text_buffer: *UnifiedTextBuffer, need: usize) !void {\n        if (self.len + need <= self.cap) return;\n\n        // TODO: Create a new buffer, register the new buffer and use the new mem_id for subsequent inserts\n        const new_cap = @max(self.cap * 2, self.len + need);\n        const new_mem = try self.allocator.alloc(u8, new_cap);\n        const new_mem_id = try text_buffer.registerMemBuffer(new_mem, true);\n        self.mem_id = new_mem_id;\n        self.ptr = new_mem.ptr;\n        self.len = 0;\n        self.cap = new_mem.len;\n    }\n\n    fn append(self: *AddBuffer, bytes: []const u8) struct { mem_id: u8, start: u32, end: u32 } {\n        std.debug.assert(self.len + bytes.len <= self.cap);\n        const start: u32 = @intCast(self.len);\n\n        const dest_slice = self.ptr[0..self.cap];\n        @memcpy(dest_slice[self.len .. self.len + bytes.len], bytes);\n\n        self.len += bytes.len;\n        const end: u32 = @intCast(self.len);\n        return .{ .mem_id = self.mem_id, .start = start, .end = end };\n    }\n};\n\npub const EditBuffer = struct {\n    id: u16,\n    tb: *UnifiedTextBuffer,\n    add_buffer: AddBuffer,\n    cursors: std.ArrayListUnmanaged(Cursor),\n    allocator: Allocator,\n    events: event_emitter.EventEmitter(EditBufferEvent),\n    segment_splitter: UnifiedRope.Node.LeafSplitFn,\n\n    pub fn init(\n        allocator: Allocator,\n        pool: *gp.GraphemePool,\n        link_pool: *link.LinkPool,\n        width_method: utf8.WidthMethod,\n    ) !*EditBuffer {\n        const self = try allocator.create(EditBuffer);\n        errdefer allocator.destroy(self);\n\n        const text_buffer = try UnifiedTextBuffer.init(allocator, pool, link_pool, width_method);\n        errdefer text_buffer.deinit();\n\n        const add_buffer = try AddBuffer.init(allocator, text_buffer, 65536);\n        errdefer {}\n\n        var cursors: std.ArrayListUnmanaged(Cursor) = .{};\n        errdefer cursors.deinit(allocator);\n\n        try cursors.append(allocator, .{ .row = 0, .col = 0 });\n\n        const buffer_id = global_edit_buffer_id;\n        global_edit_buffer_id += 1;\n\n        self.* = .{\n            .id = buffer_id,\n            .tb = text_buffer,\n            .add_buffer = add_buffer,\n            .cursors = cursors,\n            .allocator = allocator,\n            .events = event_emitter.EventEmitter(EditBufferEvent).init(allocator),\n            .segment_splitter = .{ .ctx = self, .splitFn = splitSegmentCallback },\n        };\n\n        return self;\n    }\n\n    pub fn deinit(self: *EditBuffer) void {\n        // Registry owns all AddBuffer memory, don't free it manually\n        self.events.deinit();\n        self.tb.deinit();\n        self.cursors.deinit(self.allocator);\n        self.allocator.destroy(self);\n    }\n\n    pub fn getId(self: *const EditBuffer) u16 {\n        return self.id;\n    }\n\n    fn emitNativeEvent(self: *const EditBuffer, event_name: []const u8) void {\n        var id_bytes: [2]u8 = undefined;\n        std.mem.writeInt(u16, &id_bytes, self.id, .little);\n\n        const full_name = std.fmt.allocPrint(self.allocator, \"eb_{s}\", .{event_name}) catch return;\n        defer self.allocator.free(full_name);\n\n        event_bus.emit(full_name, &id_bytes);\n    }\n\n    pub fn getTextBuffer(self: *EditBuffer) *UnifiedTextBuffer {\n        return self.tb;\n    }\n\n    pub fn getCursor(self: *const EditBuffer, idx: usize) ?Cursor {\n        if (idx >= self.cursors.items.len) return null;\n        return self.cursors.items[idx];\n    }\n\n    pub fn getPrimaryCursor(self: *const EditBuffer) Cursor {\n        if (self.cursors.items.len == 0) return .{ .row = 0, .col = 0 };\n        return self.cursors.items[0];\n    }\n\n    pub fn setCursor(self: *EditBuffer, row: u32, col: u32) !void {\n        const line_count = self.tb.lineCount();\n        const clamped_row = @min(row, line_count -| 1);\n\n        const line_width = iter_mod.lineWidthAt(self.tb.rope(), clamped_row);\n        const clamped_col = @min(col, line_width);\n\n        const offset = iter_mod.coordsToOffset(self.tb.rope(), clamped_row, clamped_col) orelse 0;\n\n        if (self.cursors.items.len == 0) {\n            try self.cursors.append(self.allocator, .{ .row = clamped_row, .col = clamped_col, .desired_col = clamped_col, .offset = offset });\n        } else {\n            self.cursors.items[0] = .{ .row = clamped_row, .col = clamped_col, .desired_col = clamped_col, .offset = offset };\n        }\n\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursor-changed\");\n    }\n\n    pub fn setCursorByOffset(self: *EditBuffer, offset: u32) !void {\n        const coords = iter_mod.offsetToCoords(self.tb.rope(), offset) orelse iter_mod.Coords{ .row = 0, .col = 0 };\n        try self.setCursor(coords.row, coords.col);\n    }\n\n    fn ensureAddCapacity(self: *EditBuffer, need: usize) !void {\n        try self.add_buffer.ensureCapacity(self.tb, need);\n    }\n\n    /// TODO: This method should live in text-buffer-segment.zig and the Rope should take it as comptime param\n    fn splitChunkAtWeight(\n        self: *EditBuffer,\n        chunk: *const TextChunk,\n        weight: u32,\n    ) error{ OutOfBounds, OutOfMemory }!struct { left: TextChunk, right: TextChunk } {\n        const chunk_weight = chunk.width;\n\n        if (weight == 0) {\n            return .{\n                .left = TextChunk{ .mem_id = 0, .byte_start = 0, .byte_end = 0, .width = 0 },\n                .right = chunk.*,\n            };\n        } else if (weight >= chunk_weight) {\n            return .{\n                .left = chunk.*,\n                .right = TextChunk{ .mem_id = 0, .byte_start = 0, .byte_end = 0, .width = 0 },\n            };\n        }\n\n        const chunk_bytes = chunk.getBytes(self.tb.memRegistry());\n        const is_ascii_only = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n\n        const result = utf8.findPosByWidth(chunk_bytes, weight, self.tb.tabWidth(), is_ascii_only, false, self.tb.widthMethod());\n        const split_byte_offset = result.byte_offset;\n\n        const left_chunk = self.tb.createChunk(\n            chunk.mem_id,\n            chunk.byte_start,\n            chunk.byte_start + split_byte_offset,\n        );\n\n        const right_chunk = self.tb.createChunk(\n            chunk.mem_id,\n            chunk.byte_start + split_byte_offset,\n            chunk.byte_end,\n        );\n\n        return .{ .left = left_chunk, .right = right_chunk };\n    }\n\n    fn splitSegmentCallback(\n        ctx: ?*anyopaque,\n        allocator: Allocator,\n        leaf: *const Segment,\n        weight_in_leaf: u32,\n    ) error{ OutOfBounds, OutOfMemory }!UnifiedRope.Node.LeafSplitResult {\n        _ = allocator;\n        const edit_buf = @as(*EditBuffer, @ptrCast(@alignCast(ctx.?)));\n\n        if (leaf.asText()) |chunk| {\n            const result = try edit_buf.splitChunkAtWeight(chunk, weight_in_leaf);\n            return .{\n                .left = Segment{ .text = result.left },\n                .right = Segment{ .text = result.right },\n            };\n        } else {\n            return .{\n                .left = Segment{ .brk = {} },\n                .right = Segment{ .brk = {} },\n            };\n        }\n    }\n\n    pub fn insertText(self: *EditBuffer, bytes: []const u8) !void {\n        if (bytes.len == 0) return;\n        if (self.cursors.items.len == 0) return;\n\n        try self.autoStoreUndo();\n\n        const cursor = self.cursors.items[0];\n\n        try self.ensureAddCapacity(bytes.len);\n\n        const insert_offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, cursor.col) orelse return EditBufferError.InvalidCursor;\n\n        const chunk_ref = self.add_buffer.append(bytes);\n        const base_mem_id = chunk_ref.mem_id;\n        const base_start = chunk_ref.start;\n\n        var result = try self.tb.textToSegments(self.allocator, bytes, base_mem_id, base_start, false);\n        defer result.segments.deinit(result.allocator);\n\n        const inserted_width = result.total_width;\n\n        // Calculate width after last break\n        var width_after_last_break: u32 = 0;\n        var num_breaks: usize = 0;\n        for (result.segments.items) |seg| {\n            if (seg.isBreak()) {\n                num_breaks += 1;\n                width_after_last_break = 0;\n            } else if (seg.asText()) |chunk| {\n                width_after_last_break += chunk.width;\n            }\n        }\n\n        if (result.segments.items.len > 0) {\n            try self.tb.rope().insertSliceByWeight(insert_offset, result.segments.items, &self.segment_splitter);\n        }\n        if (num_breaks > 0) {\n            const new_row = cursor.row + @as(u32, @intCast(num_breaks));\n            const new_col = width_after_last_break;\n            const new_offset = iter_mod.coordsToOffset(self.tb.rope(), new_row, new_col) orelse 0;\n            self.cursors.items[0] = .{\n                .row = new_row,\n                .col = new_col,\n                .desired_col = new_col,\n                .offset = new_offset,\n            };\n        } else {\n            const new_col = cursor.col + inserted_width;\n            const new_offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, new_col) orelse 0;\n            self.cursors.items[0] = .{\n                .row = cursor.row,\n                .col = new_col,\n                .desired_col = new_col,\n                .offset = new_offset,\n            };\n        }\n\n        self.tb.markViewsDirty();\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursor-changed\");\n        self.emitNativeEvent(\"content-changed\");\n    }\n\n    pub fn deleteRange(self: *EditBuffer, start_cursor: Cursor, end_cursor: Cursor) !void {\n        var start = start_cursor;\n        var end = end_cursor;\n        if (start.row > end.row or (start.row == end.row and start.col > end.col)) {\n            const temp = start;\n            start = end;\n            end = temp;\n        }\n\n        if (start.row == end.row and start.col == end.col) return;\n\n        try self.autoStoreUndo();\n\n        const start_offset = iter_mod.coordsToOffset(self.tb.rope(), start.row, start.col) orelse return EditBufferError.InvalidCursor;\n        const end_offset = iter_mod.coordsToOffset(self.tb.rope(), end.row, end.col) orelse return EditBufferError.InvalidCursor;\n\n        if (start_offset >= end_offset) return;\n\n        try self.tb.rope().deleteRangeByWeight(start_offset, end_offset, &self.segment_splitter);\n\n        self.tb.markViewsDirty();\n\n        if (self.cursors.items.len > 0) {\n            const line_count = self.tb.lineCount();\n            const clamped_row = if (start.row >= line_count) line_count -| 1 else start.row;\n            const line_width = if (line_count > 0) iter_mod.lineWidthAt(self.tb.rope(), clamped_row) else 0;\n            const clamped_col = @min(start.col, line_width);\n            const offset = iter_mod.coordsToOffset(self.tb.rope(), clamped_row, clamped_col) orelse 0;\n\n            self.cursors.items[0] = .{ .row = clamped_row, .col = clamped_col, .desired_col = clamped_col, .offset = offset };\n        }\n\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursor-changed\");\n        self.emitNativeEvent(\"content-changed\");\n    }\n\n    pub fn backspace(self: *EditBuffer) !void {\n        if (self.cursors.items.len == 0) return;\n        const cursor = self.cursors.items[0];\n\n        if (cursor.row == 0 and cursor.col == 0) return;\n\n        if (cursor.col == 0) {\n            if (cursor.row > 0) {\n                const prev_line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row - 1);\n                try self.deleteRange(\n                    .{ .row = cursor.row - 1, .col = prev_line_width },\n                    .{ .row = cursor.row, .col = 0 },\n                );\n            }\n        } else {\n            const prev_grapheme_width = self.tb.getPrevGraphemeWidth(cursor.row, cursor.col);\n            if (prev_grapheme_width == 0) return; // Nothing to delete\n\n            const target_col = cursor.col - prev_grapheme_width;\n            try self.deleteRange(\n                .{ .row = cursor.row, .col = target_col },\n                .{ .row = cursor.row, .col = cursor.col },\n            );\n        }\n\n        // deleteRange already checks for placeholder insertion\n    }\n\n    pub fn deleteForward(self: *EditBuffer) !void {\n        if (self.cursors.items.len == 0) return;\n        const cursor = self.cursors.items[0];\n\n        try self.autoStoreUndo();\n\n        const line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n        const line_count = self.tb.lineCount();\n\n        if (cursor.col >= line_width) {\n            if (cursor.row + 1 < line_count) {\n                try self.deleteRange(\n                    .{ .row = cursor.row, .col = line_width },\n                    .{ .row = cursor.row + 1, .col = 0 },\n                );\n            }\n        } else {\n            const grapheme_width = self.tb.getGraphemeWidthAt(cursor.row, cursor.col);\n            if (grapheme_width > 0) {\n                try self.deleteRange(\n                    .{ .row = cursor.row, .col = cursor.col },\n                    .{ .row = cursor.row, .col = cursor.col + grapheme_width },\n                );\n            }\n        }\n    }\n\n    pub fn moveLeft(self: *EditBuffer) void {\n        if (self.cursors.items.len == 0) {\n            return;\n        }\n        const cursor = &self.cursors.items[0];\n\n        if (cursor.col > 0) {\n            const prev_width = self.tb.getPrevGraphemeWidth(cursor.row, cursor.col);\n            cursor.col -= prev_width;\n        } else if (cursor.row > 0) {\n            cursor.row -= 1;\n            const line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n            cursor.col = line_width;\n        }\n        cursor.desired_col = cursor.col;\n        cursor.offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, cursor.col) orelse 0;\n\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursor-changed\");\n    }\n\n    pub fn moveRight(self: *EditBuffer) void {\n        if (self.cursors.items.len == 0) return;\n        const cursor = &self.cursors.items[0];\n\n        const line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n        const line_count = self.tb.getLineCount();\n\n        if (cursor.col < line_width) {\n            const grapheme_width = self.tb.getGraphemeWidthAt(cursor.row, cursor.col);\n            cursor.col += grapheme_width;\n        } else if (cursor.row + 1 < line_count) {\n            cursor.row += 1;\n            cursor.col = 0;\n        }\n        cursor.desired_col = cursor.col;\n        cursor.offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, cursor.col) orelse 0;\n\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursor-changed\");\n    }\n\n    pub fn moveUp(self: *EditBuffer) void {\n        if (self.cursors.items.len == 0) return;\n        const cursor = &self.cursors.items[0];\n\n        if (cursor.row > 0) {\n            if (cursor.desired_col == 0) {\n                cursor.desired_col = cursor.col;\n            }\n\n            cursor.row -= 1;\n\n            const line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n\n            cursor.col = @min(cursor.desired_col, line_width);\n            cursor.offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, cursor.col) orelse 0;\n        }\n\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursor-changed\");\n    }\n\n    pub fn moveDown(self: *EditBuffer) void {\n        if (self.cursors.items.len == 0) return;\n        const cursor = &self.cursors.items[0];\n\n        const line_count = self.tb.getLineCount();\n        if (cursor.row + 1 < line_count) {\n            if (cursor.desired_col == 0) {\n                cursor.desired_col = cursor.col;\n            }\n\n            cursor.row += 1;\n\n            const line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n\n            cursor.col = @min(cursor.desired_col, line_width);\n            cursor.offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, cursor.col) orelse 0;\n        }\n\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursor-changed\");\n    }\n\n    /// Set text and completely reset the buffer state (clears history, resets add_buffer)\n    pub fn setText(self: *EditBuffer, text: []const u8) !void {\n        const owned_text = try self.allocator.dupe(u8, text);\n        const mem_id = try self.tb.registerMemBuffer(owned_text, true);\n        try self.setTextFromMemId(mem_id);\n    }\n\n    /// Set text from memory ID and completely reset the buffer state (clears history, resets add_buffer)\n    pub fn setTextFromMemId(self: *EditBuffer, mem_id: u8) !void {\n        self.tb.rope().clear_history();\n        self.add_buffer.len = 0;\n\n        try self.tb.setTextFromMemId(mem_id);\n        try self.setCursor(0, 0);\n\n        self.emitNativeEvent(\"content-changed\");\n    }\n\n    /// Replace text while preserving undo history (creates an undo point)\n    pub fn replaceText(self: *EditBuffer, text: []const u8) !void {\n        const owned_text = try self.allocator.dupe(u8, text);\n        const mem_id = try self.tb.registerMemBuffer(owned_text, true);\n        try self.replaceTextFromMemId(mem_id);\n    }\n\n    /// Replace text from memory ID while preserving undo history (creates an undo point)\n    pub fn replaceTextFromMemId(self: *EditBuffer, mem_id: u8) !void {\n        try self.autoStoreUndo();\n\n        try self.tb.setTextFromMemId(mem_id);\n        try self.setCursor(0, 0);\n\n        self.emitNativeEvent(\"content-changed\");\n    }\n\n    pub fn getText(self: *EditBuffer, out_buffer: []u8) usize {\n        return self.tb.getPlainTextIntoBuffer(out_buffer);\n    }\n\n    pub fn deleteLine(self: *EditBuffer) !void {\n        const cursor = self.getPrimaryCursor();\n        const line_count = self.tb.lineCount();\n\n        if (cursor.row >= line_count) return;\n\n        if (cursor.row + 1 < line_count) {\n            try self.deleteRange(\n                .{ .row = cursor.row, .col = 0 },\n                .{ .row = cursor.row + 1, .col = 0 },\n            );\n        } else if (cursor.row > 0) {\n            const prev_line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row - 1);\n            const curr_line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n\n            try self.deleteRange(\n                .{ .row = cursor.row - 1, .col = prev_line_width },\n                .{ .row = cursor.row, .col = curr_line_width },\n            );\n\n            self.tb.markViewsDirty();\n\n            const new_row = cursor.row - 1;\n            const new_col = prev_line_width;\n            const new_offset = iter_mod.coordsToOffset(self.tb.rope(), new_row, new_col) orelse 0;\n            self.cursors.items[0] = .{ .row = new_row, .col = new_col, .desired_col = new_col, .offset = new_offset };\n            self.events.emit(.cursorChanged);\n            self.emitNativeEvent(\"cursor-changed\");\n        } else {\n            const line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n            if (line_width > 0) {\n                try self.deleteRange(\n                    .{ .row = cursor.row, .col = 0 },\n                    .{ .row = cursor.row, .col = line_width },\n                );\n            }\n        }\n    }\n\n    pub fn gotoLine(self: *EditBuffer, line: u32) !void {\n        const line_count = self.tb.lineCount();\n        const target_line = @min(line, line_count -| 1);\n\n        if (line >= line_count) {\n            const last_line_width = iter_mod.lineWidthAt(self.tb.rope(), target_line);\n            try self.setCursor(target_line, last_line_width);\n        } else {\n            try self.setCursor(target_line, 0);\n        }\n    }\n\n    pub fn getCursorPosition(self: *const EditBuffer) struct { line: u32, visual_col: u32, offset: u32 } {\n        const cursor = self.getPrimaryCursor();\n\n        return .{\n            .line = cursor.row,\n            .visual_col = cursor.col,\n            .offset = cursor.offset,\n        };\n    }\n\n    pub fn debugLogRope(self: *const EditBuffer) void {\n        self.tb.debugLogRope();\n    }\n\n    fn autoStoreUndo(self: *EditBuffer) !void {\n        try self.tb.rope().store_undo(\"edit\");\n    }\n\n    pub fn undo(self: *EditBuffer) ![]const u8 {\n        const prev_meta = try self.tb.rope().undo(\"current\");\n\n        const cursor = self.getPrimaryCursor();\n        try self.setCursor(cursor.row, cursor.col);\n\n        self.tb.markViewsDirty();\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursorChanged\");\n\n        return prev_meta;\n    }\n\n    pub fn redo(self: *EditBuffer) ![]const u8 {\n        const next_meta = try self.tb.rope().redo();\n\n        const cursor = self.getPrimaryCursor();\n        try self.setCursor(cursor.row, cursor.col);\n\n        self.tb.markViewsDirty();\n        self.events.emit(.cursorChanged);\n        self.emitNativeEvent(\"cursorChanged\");\n\n        return next_meta;\n    }\n\n    pub fn canUndo(self: *const EditBuffer) bool {\n        return self.tb.rope().can_undo();\n    }\n\n    pub fn canRedo(self: *const EditBuffer) bool {\n        return self.tb.rope().can_redo();\n    }\n\n    pub fn clearHistory(self: *EditBuffer) void {\n        self.tb.rope().clear_history();\n    }\n\n    pub fn clear(self: *EditBuffer) !void {\n        self.tb.clear();\n        try self.setCursor(0, 0);\n        self.emitNativeEvent(\"content-changed\");\n    }\n\n    pub fn getNextWordBoundary(self: *EditBuffer) Cursor {\n        if (self.cursors.items.len == 0) return .{ .row = 0, .col = 0 };\n        const cursor = self.cursors.items[0];\n\n        const line_count = self.tb.lineCount();\n        if (cursor.row >= line_count) return cursor;\n\n        const line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n\n        const linestart = self.tb.rope().getMarker(.linestart, cursor.row) orelse return cursor;\n        var seg_idx = linestart.leaf_index + 1;\n        var cols_before: u32 = 0;\n        var passed_cursor = false;\n\n        while (seg_idx < self.tb.rope().count()) : (seg_idx += 1) {\n            const seg = self.tb.rope().get(seg_idx) orelse break;\n            if (seg.isBreak() or seg.isLineStart()) break;\n            if (seg.asText()) |chunk| {\n                const next_cols = cols_before + chunk.width;\n\n                // Check this chunk if cursor is within it OR if we've already passed the cursor\n                if (cursor.col < next_cols or passed_cursor) {\n                    const wrap_offsets = self.tb.getWrapOffsetsFor(chunk) catch {\n                        cols_before = next_cols;\n                        passed_cursor = true;\n                        continue;\n                    };\n                    const is_ascii_only = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n                    const graphemes: []const seg_mod.GraphemeInfo = if (is_ascii_only)\n                        &[_]seg_mod.GraphemeInfo{}\n                    else\n                        chunk.getGraphemes(self.tb.memRegistry(), self.tb.getAllocator(), self.tb.tabWidth(), self.tb.widthMethod()) catch &[_]seg_mod.GraphemeInfo{};\n                    var grapheme_idx: usize = 0;\n                    var col_delta: i64 = 0;\n\n                    // For chunks containing or after the cursor, find the first break after cursor position\n                    const local_cursor_col = if (cursor.col > cols_before) cursor.col - cols_before else 0;\n\n                    for (wrap_offsets) |wrap_break| {\n                        const break_info = iter_mod.charOffsetToColumn(wrap_break.char_offset, graphemes, &grapheme_idx, &col_delta);\n                        const break_col = break_info.col;\n\n                        // If we've passed the cursor chunk, any break is valid\n                        // If we're in the cursor chunk, break must be after cursor position\n                        if (passed_cursor or break_col > local_cursor_col) {\n                            // break_col points at the break grapheme start.\n                            // Adding width moves the cursor to the boundary after it.\n                            const target_col = cols_before + break_col + break_info.width;\n                            if (target_col <= line_width) {\n                                const offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, target_col) orelse cursor.offset;\n                                return .{ .row = cursor.row, .col = target_col, .desired_col = target_col, .offset = offset };\n                            }\n                        }\n\n                        // A boundary at the cursor can still be the next word step\n                        // for script-transition cases like \"a日\", \"日a\", or \"丽abc\".\n                        // Only accept it when the boundary starts on a word codepoint.\n                        if (!passed_cursor and break_col == local_cursor_col) {\n                            const break_byte_offset: usize = @intCast(wrap_break.byte_offset);\n                            const chunk_bytes = chunk.getBytes(self.tb.memRegistry());\n                            if (break_byte_offset < chunk_bytes.len) {\n                                const break_cp = utf8.decodeUtf8Unchecked(chunk_bytes, break_byte_offset).cp;\n                                if (utf8.isWordCodepoint(break_cp)) {\n                                    const target_col = cols_before + break_col + break_info.width;\n                                    if (target_col <= line_width) {\n                                        const offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, target_col) orelse cursor.offset;\n                                        return .{ .row = cursor.row, .col = target_col, .desired_col = target_col, .offset = offset };\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    // Mark that we've processed/passed the cursor position\n                    passed_cursor = true;\n                }\n                cols_before = next_cols;\n            }\n        }\n\n        if (cursor.row + 1 < line_count) {\n            const offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row + 1, 0) orelse cursor.offset;\n            return .{ .row = cursor.row + 1, .col = 0, .desired_col = 0, .offset = offset };\n        }\n\n        const offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, line_width) orelse cursor.offset;\n        return .{ .row = cursor.row, .col = line_width, .desired_col = line_width, .offset = offset };\n    }\n\n    pub fn getPrevWordBoundary(self: *EditBuffer) Cursor {\n        if (self.cursors.items.len == 0) return .{ .row = 0, .col = 0 };\n        const cursor = self.cursors.items[0];\n\n        if (cursor.row == 0 and cursor.col == 0) return cursor;\n\n        const linestart = self.tb.rope().getMarker(.linestart, cursor.row) orelse return cursor;\n        var seg_idx = linestart.leaf_index + 1;\n        var cols_before: u32 = 0;\n        var last_boundary: ?u32 = null;\n\n        while (seg_idx < self.tb.rope().count()) : (seg_idx += 1) {\n            const seg = self.tb.rope().get(seg_idx) orelse break;\n            if (seg.isBreak() or seg.isLineStart()) break;\n            if (seg.asText()) |chunk| {\n                const next_cols = cols_before + chunk.width;\n\n                const wrap_offsets = self.tb.getWrapOffsetsFor(chunk) catch {\n                    cols_before = next_cols;\n                    continue;\n                };\n                const is_ascii_only = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n                const graphemes: []const seg_mod.GraphemeInfo = if (is_ascii_only)\n                    &[_]seg_mod.GraphemeInfo{}\n                else\n                    chunk.getGraphemes(self.tb.memRegistry(), self.tb.getAllocator(), self.tb.tabWidth(), self.tb.widthMethod()) catch &[_]seg_mod.GraphemeInfo{};\n                var grapheme_idx: usize = 0;\n                var col_delta: i64 = 0;\n\n                for (wrap_offsets) |wrap_break| {\n                    const break_info = iter_mod.charOffsetToColumn(wrap_break.char_offset, graphemes, &grapheme_idx, &col_delta);\n                    // break_info follows the same convention as getNextWordBoundary:\n                    // use break start + grapheme width to land after the break grapheme.\n                    const boundary_col = cols_before + break_info.col + break_info.width;\n                    if (boundary_col < cursor.col) {\n                        last_boundary = boundary_col;\n                    }\n                }\n\n                cols_before = next_cols;\n                if (cursor.col <= cols_before) break;\n            }\n        }\n\n        if (last_boundary) |boundary_col| {\n            const offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, boundary_col) orelse cursor.offset;\n            return .{ .row = cursor.row, .col = boundary_col, .desired_col = boundary_col, .offset = offset };\n        }\n\n        if (cursor.row > 0) {\n            const prev_line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row - 1);\n            const offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row - 1, prev_line_width) orelse cursor.offset;\n            return .{ .row = cursor.row - 1, .col = prev_line_width, .desired_col = prev_line_width, .offset = offset };\n        }\n\n        return .{ .row = 0, .col = 0, .desired_col = 0, .offset = 0 };\n    }\n\n    pub fn getEOL(self: *EditBuffer) Cursor {\n        if (self.cursors.items.len == 0) return .{ .row = 0, .col = 0 };\n        const cursor = self.cursors.items[0];\n\n        const line_count = self.tb.lineCount();\n        if (cursor.row >= line_count) return cursor;\n\n        const line_width = iter_mod.lineWidthAt(self.tb.rope(), cursor.row);\n        const offset = iter_mod.coordsToOffset(self.tb.rope(), cursor.row, line_width) orelse cursor.offset;\n\n        return .{ .row = cursor.row, .col = line_width, .desired_col = line_width, .offset = offset };\n    }\n\n    /// Get text within a range of display-width offsets\n    /// Automatically snaps to grapheme boundaries:\n    /// - start_offset excludes graphemes that start before it\n    /// - end_offset includes graphemes that start before it\n    /// Returns number of bytes written to out_buffer\n    pub fn getTextRange(self: *EditBuffer, start_offset: u32, end_offset: u32, out_buffer: []u8) !usize {\n        return self.tb.getTextRange(start_offset, end_offset, out_buffer);\n    }\n\n    /// Get text within a range specified by row/col coordinates\n    /// Automatically snaps to grapheme boundaries:\n    /// Returns number of bytes written to out_buffer\n    pub fn getTextRangeByCoords(self: *EditBuffer, start_row: u32, start_col: u32, end_row: u32, end_col: u32, out_buffer: []u8) usize {\n        return self.tb.getTextRangeByCoords(start_row, start_col, end_row, end_col, out_buffer);\n    }\n};\n"
  },
  {
    "path": "packages/core/src/zig/editor-view.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst tb = @import(\"text-buffer.zig\");\nconst tbv = @import(\"text-buffer-view.zig\");\nconst eb = @import(\"edit-buffer.zig\");\nconst iter_mod = @import(\"text-buffer-iterators.zig\");\nconst gp = @import(\"grapheme.zig\");\nconst ss = @import(\"syntax-style.zig\");\nconst event_emitter = @import(\"event-emitter.zig\");\nconst logger = @import(\"logger.zig\");\n\nconst EditBuffer = eb.EditBuffer;\n\n// Use the unified types to match EditBuffer\nconst UnifiedTextBuffer = tb.UnifiedTextBuffer;\nconst UnifiedTextBufferView = tbv.UnifiedTextBufferView;\nconst VirtualLine = tbv.VirtualLine;\n\npub const EditorViewError = error{\n    OutOfMemory,\n};\n\n/// VisualCursor represents a cursor position with both visual and logical coordinates.\n/// Visual coordinates (visual_row, visual_col) are VIEWPORT-RELATIVE.\n/// This means visual_row=0 is the first visible line in the viewport, not the first line in the document.\n/// Logical coordinates (logical_row, logical_col) are document-absolute.\npub const VisualCursor = struct {\n    visual_row: u32, // Viewport-relative row (0 = top of viewport)\n    visual_col: u32, // Viewport-relative column (0 = left edge of viewport when not wrapping)\n    logical_row: u32, // Document-absolute row\n    logical_col: u32, // Document-absolute column\n    offset: u32, // Global display-width offset from buffer start\n};\n\n/// EditorView wraps a TextBufferView and manages viewport state for efficient rendering\n/// It also holds a reference to an EditBuffer for cursor/editing operations\npub const EditorView = struct {\n    text_buffer_view: *UnifiedTextBufferView,\n    edit_buffer: *EditBuffer, // Reference to the EditBuffer (not owned)\n    scroll_margin: f32, // Fraction of viewport height (0.0-0.5) to keep cursor away from edges\n    desired_visual_col: ?u32, // Preserved visual column for visual up/down navigation\n    selection_follow_cursor: bool, // Keep viewport synced during selection\n    cursor_changed_listener: event_emitter.EventEmitter(eb.EditBufferEvent).Listener,\n\n    placeholder_buffer: ?*UnifiedTextBuffer,\n    placeholder_syntax_style: ?*ss.SyntaxStyle,\n    placeholder_active: bool,\n\n    // Memory management\n    global_allocator: Allocator,\n\n    fn onCursorChanged(ctx: *anyopaque) void {\n        const self: *EditorView = @ptrCast(@alignCast(ctx));\n        self.desired_visual_col = null;\n        self.updatePlaceholderVisibility();\n\n        const has_selection = self.text_buffer_view.selection != null;\n        if (!has_selection or self.selection_follow_cursor) {\n            const cursor = self.edit_buffer.getPrimaryCursor();\n            const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n            self.ensureCursorVisible(vcursor.visual_row);\n        }\n    }\n\n    pub fn init(global_allocator: Allocator, edit_buffer: *EditBuffer, viewport_width: u32, viewport_height: u32) EditorViewError!*EditorView {\n        const self = global_allocator.create(EditorView) catch return EditorViewError.OutOfMemory;\n        errdefer global_allocator.destroy(self);\n\n        const text_buffer = edit_buffer.getTextBuffer();\n        const text_buffer_view = UnifiedTextBufferView.init(global_allocator, text_buffer) catch return EditorViewError.OutOfMemory;\n        errdefer text_buffer_view.deinit();\n\n        self.* = .{\n            .text_buffer_view = text_buffer_view,\n            .edit_buffer = edit_buffer,\n            .scroll_margin = 0.15, // Default 15% margin\n            .desired_visual_col = null,\n            .selection_follow_cursor = false,\n            .cursor_changed_listener = .{\n                .ctx = undefined, // Will be set below\n                .handle = onCursorChanged,\n            },\n            .placeholder_buffer = null,\n            .placeholder_syntax_style = null,\n            .placeholder_active = false,\n            .global_allocator = global_allocator,\n        };\n\n        self.cursor_changed_listener.ctx = self;\n\n        edit_buffer.events.on(.cursorChanged, self.cursor_changed_listener) catch return EditorViewError.OutOfMemory;\n\n        text_buffer_view.setViewport(tbv.Viewport{\n            .x = 0,\n            .y = 0,\n            .width = viewport_width,\n            .height = viewport_height,\n        });\n\n        return self;\n    }\n\n    pub fn deinit(self: *EditorView) void {\n        self.edit_buffer.events.off(.cursorChanged, self.cursor_changed_listener);\n\n        if (self.placeholder_syntax_style) |style| {\n            style.deinit();\n        }\n\n        if (self.placeholder_buffer) |placeholder| {\n            placeholder.deinit();\n        }\n\n        self.text_buffer_view.deinit();\n        self.global_allocator.destroy(self);\n    }\n\n    /// Set the viewport. If wrapping is enabled and viewport width differs from current wrap width,\n    /// this will trigger a reflow by updating the TextBufferView's wrap width.\n    /// moveCursor: if true, moves cursor to stay within viewport bounds (prevents viewport reset)\n    pub fn setViewport(self: *EditorView, vp: ?tbv.Viewport, moveCursor: bool) void {\n        self.text_buffer_view.setViewport(vp);\n\n        if (moveCursor) {\n            self.makeCursorVisible();\n        }\n    }\n\n    pub fn getViewport(self: *const EditorView) ?tbv.Viewport {\n        return self.text_buffer_view.getViewport();\n    }\n\n    /// Move the cursor to be within the current viewport if it's outside.\n    /// Unlike ensureCursorVisible, this moves the cursor, not the viewport.\n    /// Respects scroll margins to prevent immediate re-scrolling by ensureCursorVisible.\n    pub fn makeCursorVisible(self: *EditorView) void {\n        const vp = self.text_buffer_view.getViewport() orelse return;\n        const cursor = self.edit_buffer.getPrimaryCursor();\n        const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n\n        const viewport_height = vp.height;\n        const margin_lines = @max(1, @as(u32, @intFromFloat(@as(f32, @floatFromInt(viewport_height)) * self.scroll_margin)));\n\n        const cursor_above_viewport = vcursor.visual_row < vp.y;\n        const cursor_below_viewport = vcursor.visual_row >= vp.y + vp.height;\n        const cursor_too_close_to_top = vcursor.visual_row < vp.y + margin_lines;\n        const cursor_too_close_to_bottom = vcursor.visual_row >= vp.y + vp.height - margin_lines;\n\n        if (cursor_above_viewport or cursor_below_viewport or cursor_too_close_to_top or cursor_too_close_to_bottom) {\n            const target_visual_row = if (cursor_above_viewport or cursor_too_close_to_top)\n                vp.y + margin_lines\n            else\n                vp.y + vp.height - margin_lines - 1;\n\n            self.text_buffer_view.updateVirtualLines();\n            const vlines = self.text_buffer_view.virtual_lines.items;\n            if (target_visual_row < vlines.len) {\n                const target_vline = &vlines[target_visual_row];\n                const target_logical_row = @as(u32, @intCast(target_vline.source_line));\n\n                const line_width = iter_mod.lineWidthAt(self.edit_buffer.tb.rope(), target_logical_row);\n                const target_col = @min(cursor.col, line_width);\n\n                if (self.edit_buffer.cursors.items.len > 0) {\n                    const offset = iter_mod.coordsToOffset(self.edit_buffer.tb.rope(), target_logical_row, target_col) orelse return;\n                    self.edit_buffer.cursors.items[0] = .{\n                        .row = target_logical_row,\n                        .col = target_col,\n                        .desired_col = target_col,\n                        .offset = offset,\n                    };\n                }\n            }\n        }\n    }\n\n    /// Set the scroll margin as a fraction of viewport height (0.0 to 0.5)\n    /// The cursor will stay at least this many lines from the top/bottom edges when scrolling\n    pub fn setScrollMargin(self: *EditorView, margin: f32) void {\n        self.scroll_margin = @max(0.0, @min(0.5, margin));\n    }\n\n    pub fn setSelectionFollowCursor(self: *EditorView, enabled: bool) void {\n        self.selection_follow_cursor = enabled;\n    }\n\n    /// Ensure the cursor is visible within the viewport, adjusting viewport.y and viewport.x if needed\n    /// cursor_line: The virtual line index where the cursor is located\n    pub fn ensureCursorVisible(self: *EditorView, cursor_line: u32) void {\n        const vp = self.text_buffer_view.getViewport() orelse return;\n\n        const viewport_height = vp.height;\n        const viewport_width = vp.width;\n        if (viewport_height == 0 or viewport_width == 0) return;\n\n        const raw_margin_lines = @max(1, @as(u32, @intFromFloat(@as(f32, @floatFromInt(viewport_height)) * self.scroll_margin)));\n        const max_margin_lines = if (viewport_height > 1) (viewport_height - 1) / 2 else 0;\n        const margin_lines = @min(raw_margin_lines, max_margin_lines);\n\n        const raw_margin_cols = @max(1, @as(u32, @intFromFloat(@as(f32, @floatFromInt(viewport_width)) * self.scroll_margin)));\n        const max_margin_cols = if (viewport_width > 1) (viewport_width - 1) / 2 else 0;\n        const margin_cols = @min(raw_margin_cols, max_margin_cols);\n\n        const total_lines = self.text_buffer_view.getVirtualLineCount();\n        const max_offset_y = if (total_lines > viewport_height) total_lines - viewport_height else 0;\n\n        var new_offset_y = vp.y;\n        var new_offset_x = vp.x;\n\n        if (cursor_line < vp.y + margin_lines) {\n            if (cursor_line >= margin_lines) {\n                new_offset_y = cursor_line - margin_lines;\n            } else {\n                new_offset_y = 0;\n            }\n        } else if (cursor_line >= vp.y + viewport_height - margin_lines) {\n            const desired_offset = cursor_line + margin_lines - viewport_height + 1;\n            new_offset_y = @min(desired_offset, max_offset_y);\n        }\n\n        if (self.text_buffer_view.wrap_mode == .none) {\n            const cursor = self.edit_buffer.getPrimaryCursor();\n            const cursor_col = cursor.col;\n\n            if (cursor_col < vp.x + margin_cols) {\n                if (cursor_col >= margin_cols) {\n                    new_offset_x = cursor_col - margin_cols;\n                } else {\n                    new_offset_x = 0;\n                }\n            } else if (cursor_col >= vp.x + viewport_width - margin_cols) {\n                new_offset_x = cursor_col + margin_cols - viewport_width + 1;\n            }\n        }\n\n        if (new_offset_y != vp.y or new_offset_x != vp.x) {\n            self.text_buffer_view.setViewport(tbv.Viewport{\n                .x = new_offset_x,\n                .y = new_offset_y,\n                .width = vp.width,\n                .height = vp.height,\n            });\n        }\n    }\n\n    /// Always ensures cursor visibility since cursor movements don't mark buffer dirty\n    /// Note: With eager viewport updates in onCursorChanged, this is mainly for rendering methods\n    pub fn updateBeforeRender(self: *EditorView) void {\n        self.updatePlaceholderVisibility();\n\n        const has_selection = self.text_buffer_view.selection != null;\n\n        if (!has_selection or self.selection_follow_cursor) {\n            const cursor = self.edit_buffer.getPrimaryCursor();\n            const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n            self.ensureCursorVisible(vcursor.visual_row);\n        }\n    }\n\n    /// Automatically ensures cursor is visible before rendering\n    pub fn getVirtualLines(self: *EditorView) []const VirtualLine {\n        self.updateBeforeRender();\n        return self.text_buffer_view.getVirtualLines();\n    }\n\n    /// Automatically ensures cursor is visible before rendering\n    pub fn getCachedLineInfo(self: *EditorView) tbv.LineInfo {\n        self.updateBeforeRender();\n        return self.text_buffer_view.getCachedLineInfo();\n    }\n\n    pub fn getLogicalLineInfo(self: *EditorView) tbv.LineInfo {\n        self.updatePlaceholderVisibility();\n        self.text_buffer_view.virtual_lines_dirty = true;\n        const line_info = self.text_buffer_view.getLogicalLineInfo();\n        return line_info;\n    }\n\n    pub fn getTextBufferView(self: *EditorView) *UnifiedTextBufferView {\n        return self.text_buffer_view;\n    }\n\n    pub fn getTotalVirtualLineCount(self: *EditorView) u32 {\n        return self.text_buffer_view.getVirtualLineCount();\n    }\n\n    pub fn getVirtualLineSpans(self: *const EditorView, vline_idx: usize) tbv.VirtualLineSpanInfo {\n        return self.text_buffer_view.getVirtualLineSpans(vline_idx);\n    }\n\n    pub fn getTextBuffer(self: *const EditorView) *UnifiedTextBuffer {\n        return self.text_buffer_view.text_buffer;\n    }\n\n    pub fn getSelection(self: *const EditorView) ?tb.TextSelection {\n        return self.text_buffer_view.selection;\n    }\n\n    pub fn setSelection(self: *EditorView, start: u32, end: u32, bgColor: ?tb.RGBA, fgColor: ?tb.RGBA) void {\n        self.text_buffer_view.setSelection(start, end, bgColor, fgColor);\n    }\n\n    pub fn updateSelection(self: *EditorView, end: u32, bgColor: ?tb.RGBA, fgColor: ?tb.RGBA) void {\n        self.text_buffer_view.updateSelection(end, bgColor, fgColor);\n    }\n\n    pub fn resetSelection(self: *EditorView) void {\n        self.text_buffer_view.resetSelection();\n    }\n\n    pub fn setLocalSelection(self: *EditorView, anchorX: i32, anchorY: i32, focusX: i32, focusY: i32, bgColor: ?tb.RGBA, fgColor: ?tb.RGBA, updateCursor: bool) bool {\n        const changed = self.text_buffer_view.setLocalSelection(anchorX, anchorY, focusX, focusY, bgColor, fgColor);\n\n        if (changed and updateCursor) {\n            self.updateCursorToSelectionFocus(focusX, focusY);\n        }\n\n        return changed;\n    }\n\n    pub fn updateLocalSelection(self: *EditorView, anchorX: i32, anchorY: i32, focusX: i32, focusY: i32, bgColor: ?tb.RGBA, fgColor: ?tb.RGBA, updateCursor: bool) bool {\n        const changed = self.text_buffer_view.updateLocalSelection(anchorX, anchorY, focusX, focusY, bgColor, fgColor);\n\n        if (changed and updateCursor) {\n            self.updateCursorToSelectionFocus(focusX, focusY);\n        }\n\n        return changed;\n    }\n\n    pub fn resetLocalSelection(self: *EditorView) void {\n        self.text_buffer_view.resetLocalSelection();\n    }\n\n    /// Updates the cursor position to match the selection focus position.\n    /// Does NOT trigger viewport scrolling - TypeScript layer handles that.\n    fn updateCursorToSelectionFocus(self: *EditorView, _: i32, _: i32) void {\n        const selection = self.text_buffer_view.getSelection() orelse return;\n\n        const focus_offset = if (self.text_buffer_view.selection_anchor_offset) |anchor| blk: {\n            if (anchor == selection.start) {\n                break :blk selection.end;\n            } else {\n                break :blk selection.start;\n            }\n        } else blk: {\n            break :blk selection.end;\n        };\n\n        const focus_coords = iter_mod.offsetToCoords(self.edit_buffer.tb.rope(), focus_offset) orelse return;\n\n        const line_count = iter_mod.getLineCount(self.edit_buffer.tb.rope());\n        if (focus_coords.row >= line_count) return;\n\n        const line_width = iter_mod.lineWidthAt(self.edit_buffer.tb.rope(), focus_coords.row);\n        if (focus_coords.col > line_width) return;\n\n        // Update cursor to focus position\n        if (self.edit_buffer.cursors.items.len > 0) {\n            self.edit_buffer.cursors.items[0] = .{\n                .row = focus_coords.row,\n                .col = focus_coords.col,\n                .desired_col = focus_coords.col,\n                .offset = focus_offset,\n            };\n        }\n    }\n\n    pub fn getSelectedTextIntoBuffer(self: *EditorView, out_buffer: []u8) usize {\n        return self.text_buffer_view.getSelectedTextIntoBuffer(out_buffer);\n    }\n\n    pub fn packSelectionInfo(self: *const EditorView) u64 {\n        return self.text_buffer_view.packSelectionInfo();\n    }\n\n    /// This is a convenience method that preserves existing offset\n    /// After resize, ensures cursor is visible and clamps viewport offset to valid range\n    pub fn setViewportSize(self: *EditorView, width: u32, height: u32) void {\n        self.text_buffer_view.setViewportSize(width, height);\n\n        const vp = self.text_buffer_view.getViewport() orelse return;\n        const total_lines = self.text_buffer_view.getVirtualLineCount();\n        const max_offset_y = if (total_lines > vp.height) total_lines - vp.height else 0;\n\n        var new_offset_x = vp.x;\n        if (self.text_buffer_view.wrap_mode == .none) {\n            const max_line_width = iter_mod.getMaxLineWidth(self.edit_buffer.tb.rope());\n            const max_offset_x = if (max_line_width > vp.width) max_line_width - vp.width else 0;\n            if (vp.x > max_offset_x) {\n                new_offset_x = max_offset_x;\n            }\n        }\n\n        if (vp.y > max_offset_y or new_offset_x != vp.x) {\n            self.text_buffer_view.setViewport(tbv.Viewport{\n                .x = new_offset_x,\n                .y = @min(vp.y, max_offset_y),\n                .width = vp.width,\n                .height = vp.height,\n            });\n        }\n\n        const cursor = self.edit_buffer.getPrimaryCursor();\n        const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n        self.ensureCursorVisible(vcursor.visual_row);\n    }\n\n    pub fn setWrapMode(self: *EditorView, mode: tb.WrapMode) void {\n        self.text_buffer_view.setWrapMode(mode);\n    }\n\n    pub fn getPrimaryCursor(self: *const EditorView) eb.Cursor {\n        return self.edit_buffer.getPrimaryCursor();\n    }\n\n    pub fn getCursor(self: *const EditorView, idx: usize) ?eb.Cursor {\n        return self.edit_buffer.getCursor(idx);\n    }\n\n    pub fn getText(self: *EditorView, out_buffer: []u8) usize {\n        return self.edit_buffer.getText(out_buffer);\n    }\n\n    /// Get the EditBuffer for direct access when needed\n    pub fn getEditBuffer(self: *EditorView) *EditBuffer {\n        return self.edit_buffer;\n    }\n\n    // ============================================================================\n    // VisualCursor - Wrapping-aware cursor translation\n    // ============================================================================\n\n    /// Returns viewport-relative visual coordinates for external API consumers\n    pub fn getVisualCursor(self: *EditorView) VisualCursor {\n        self.updateBeforeRender();\n        const cursor = self.edit_buffer.getPrimaryCursor();\n        const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n\n        // Convert absolute visual coordinates to viewport-relative for the API\n        const vp = self.text_buffer_view.getViewport() orelse return vcursor;\n\n        const viewport_relative_row = if (vcursor.visual_row >= vp.y) vcursor.visual_row - vp.y else 0;\n        const viewport_relative_col = if (self.text_buffer_view.wrap_mode == .none)\n            (if (vcursor.visual_col >= vp.x) vcursor.visual_col - vp.x else 0)\n        else\n            vcursor.visual_col;\n\n        return VisualCursor{\n            .visual_row = viewport_relative_row,\n            .visual_col = viewport_relative_col,\n            .logical_row = vcursor.logical_row,\n            .logical_col = vcursor.logical_col,\n            .offset = vcursor.offset,\n        };\n    }\n\n    /// This accounts for line wrapping by finding which virtual line contains the logical position\n    /// Returns absolute visual coordinates (document-absolute, not viewport-relative)\n    pub fn logicalToVisualCursor(self: *EditorView, logical_row: u32, logical_col: u32) VisualCursor {\n        // Clamp logical coordinates to valid buffer ranges\n        const line_count = iter_mod.getLineCount(self.edit_buffer.tb.rope());\n        const clamped_row = if (line_count > 0) @min(logical_row, line_count - 1) else 0;\n\n        const line_width = iter_mod.lineWidthAt(self.edit_buffer.tb.rope(), clamped_row);\n        const clamped_col = @min(logical_col, line_width);\n\n        const visual_row_idx = self.text_buffer_view.findVisualLineIndex(clamped_row, clamped_col);\n\n        const vlines = self.text_buffer_view.virtual_lines.items;\n        if (vlines.len == 0 or visual_row_idx >= vlines.len) {\n            // Fallback for edge cases\n            const offset = iter_mod.coordsToOffset(self.edit_buffer.tb.rope(), clamped_row, clamped_col) orelse 0;\n            return VisualCursor{\n                .visual_row = 0,\n                .visual_col = 0,\n                .logical_row = clamped_row,\n                .logical_col = clamped_col,\n                .offset = offset,\n            };\n        }\n\n        const vline = &vlines[visual_row_idx];\n        const vline_start_col = vline.source_col_offset;\n\n        // Calculate visual column within this virtual line\n        const visual_col = if (clamped_col >= vline_start_col)\n            clamped_col - vline_start_col\n        else\n            0;\n\n        const offset = iter_mod.coordsToOffset(self.edit_buffer.tb.rope(), clamped_row, clamped_col) orelse 0;\n\n        return VisualCursor{\n            .visual_row = visual_row_idx,\n            .visual_col = visual_col,\n            .logical_row = clamped_row,\n            .logical_col = clamped_col,\n            .offset = offset,\n        };\n    }\n\n    /// Input visual coordinates are absolute (document-absolute)\n    /// Returns a VisualCursor with absolute visual coordinates\n    pub fn visualToLogicalCursor(self: *EditorView, visual_row: u32, visual_col: u32) ?VisualCursor {\n        self.text_buffer_view.updateVirtualLines();\n\n        const vlines = self.text_buffer_view.virtual_lines.items;\n        if (visual_row >= vlines.len) return null;\n\n        const vline = &vlines[visual_row];\n        const clamped_visual_col = @min(visual_col, vline.width_cols);\n        const logical_col = vline.source_col_offset + clamped_visual_col;\n        const logical_row = @as(u32, @intCast(vline.source_line));\n\n        const offset = iter_mod.coordsToOffset(self.edit_buffer.tb.rope(), logical_row, logical_col) orelse 0;\n\n        return VisualCursor{\n            .visual_row = visual_row,\n            .visual_col = clamped_visual_col,\n            .logical_row = logical_row,\n            .logical_col = logical_col,\n            .offset = offset,\n        };\n    }\n\n    pub fn moveUpVisual(self: *EditorView) void {\n        const cursor = self.edit_buffer.getPrimaryCursor();\n        const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n\n        if (vcursor.visual_row == 0) {\n            return;\n        }\n\n        const target_visual_row = vcursor.visual_row - 1;\n\n        // This persists across empty/narrow lines to restore column when possible\n        if (self.desired_visual_col == null) {\n            self.desired_visual_col = vcursor.visual_col;\n        }\n        const desired_visual_col = self.desired_visual_col.?;\n\n        if (self.visualToLogicalCursor(target_visual_row, desired_visual_col)) |new_vcursor| {\n            if (self.edit_buffer.cursors.items.len > 0) {\n                self.edit_buffer.cursors.items[0] = .{\n                    .row = new_vcursor.logical_row,\n                    .col = new_vcursor.logical_col,\n                    .desired_col = new_vcursor.logical_col,\n                    .offset = new_vcursor.offset,\n                };\n                self.ensureCursorVisible(new_vcursor.visual_row);\n\n                // Restore desired_visual_col after the cursor change event resets it\n                self.desired_visual_col = desired_visual_col;\n            }\n        }\n    }\n\n    pub fn moveDownVisual(self: *EditorView) void {\n        const cursor = self.edit_buffer.getPrimaryCursor();\n        const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n\n        self.text_buffer_view.updateVirtualLines();\n        const vlines = self.text_buffer_view.virtual_lines.items;\n\n        if (vcursor.visual_row + 1 >= vlines.len) {\n            return;\n        }\n\n        const target_visual_row = vcursor.visual_row + 1;\n\n        // This persists across empty/narrow lines to restore column when possible\n        if (self.desired_visual_col == null) {\n            self.desired_visual_col = vcursor.visual_col;\n        }\n        const desired_visual_col = self.desired_visual_col.?;\n\n        if (self.visualToLogicalCursor(target_visual_row, desired_visual_col)) |new_vcursor| {\n            if (self.edit_buffer.cursors.items.len > 0) {\n                self.edit_buffer.cursors.items[0] = .{\n                    .row = new_vcursor.logical_row,\n                    .col = new_vcursor.logical_col,\n                    .desired_col = new_vcursor.logical_col,\n                    .offset = new_vcursor.offset,\n                };\n                self.ensureCursorVisible(new_vcursor.visual_row);\n\n                // Restore desired_visual_col after the cursor change event resets it\n                self.desired_visual_col = desired_visual_col;\n            }\n        }\n    }\n\n    pub fn deleteSelectedText(self: *EditorView) !void {\n        const selection = self.text_buffer_view.getSelection() orelse {\n            return;\n        };\n\n        const start_coords = iter_mod.offsetToCoords(self.edit_buffer.tb.rope(), selection.start) orelse {\n            return;\n        };\n        const end_coords = iter_mod.offsetToCoords(self.edit_buffer.tb.rope(), selection.end) orelse {\n            return;\n        };\n\n        const start_cursor = eb.Cursor{\n            .row = start_coords.row,\n            .col = start_coords.col,\n            .desired_col = start_coords.col,\n        };\n        const end_cursor = eb.Cursor{\n            .row = end_coords.row,\n            .col = end_coords.col,\n            .desired_col = end_coords.col,\n        };\n\n        try self.edit_buffer.deleteRange(start_cursor, end_cursor);\n        self.text_buffer_view.resetLocalSelection();\n        self.updateBeforeRender();\n    }\n\n    pub fn setCursorByOffset(self: *EditorView, offset: u32) !void {\n        try self.edit_buffer.setCursorByOffset(offset);\n        self.updateBeforeRender();\n    }\n\n    pub fn getNextWordBoundary(self: *EditorView) VisualCursor {\n        const logical_cursor = self.edit_buffer.getNextWordBoundary();\n        return self.logicalToVisualCursor(logical_cursor.row, logical_cursor.col);\n    }\n\n    pub fn getPrevWordBoundary(self: *EditorView) VisualCursor {\n        const logical_cursor = self.edit_buffer.getPrevWordBoundary();\n        return self.logicalToVisualCursor(logical_cursor.row, logical_cursor.col);\n    }\n\n    pub fn getEOL(self: *EditorView) VisualCursor {\n        const logical_cursor = self.edit_buffer.getEOL();\n        return self.logicalToVisualCursor(logical_cursor.row, logical_cursor.col);\n    }\n\n    /// Get the start of the current visual line (SOL = Start Of Line)\n    /// Returns a cursor at column 0 of the current visual line\n    pub fn getVisualSOL(self: *EditorView) VisualCursor {\n        const cursor = self.edit_buffer.getPrimaryCursor();\n        const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n\n        self.text_buffer_view.updateVirtualLines();\n        const vlines = self.text_buffer_view.virtual_lines.items;\n\n        if (vcursor.visual_row >= vlines.len) {\n            // Fallback: return cursor at column 0 of current logical line\n            const offset = iter_mod.coordsToOffset(self.edit_buffer.tb.rope(), cursor.row, 0) orelse 0;\n            return VisualCursor{\n                .visual_row = vcursor.visual_row,\n                .visual_col = 0,\n                .logical_row = cursor.row,\n                .logical_col = 0,\n                .offset = offset,\n            };\n        }\n\n        const vline = &vlines[vcursor.visual_row];\n        const logical_col = vline.source_col_offset; // Start column of this visual line\n        const logical_row = @as(u32, @intCast(vline.source_line));\n        const offset = iter_mod.coordsToOffset(self.edit_buffer.tb.rope(), logical_row, logical_col) orelse 0;\n\n        return VisualCursor{\n            .visual_row = vcursor.visual_row,\n            .visual_col = 0,\n            .logical_row = logical_row,\n            .logical_col = logical_col,\n            .offset = offset,\n        };\n    }\n\n    /// Get the end of the current visual line (EOL = End Of Line)\n    /// Returns a cursor at the last position of the current visual line\n    /// For wrapped lines, this is the position just before the wrap boundary to ensure\n    /// the cursor stays on the current visual line when used with setCursor()\n    pub fn getVisualEOL(self: *EditorView) VisualCursor {\n        const cursor = self.edit_buffer.getPrimaryCursor();\n        const vcursor = self.logicalToVisualCursor(cursor.row, cursor.col);\n\n        self.text_buffer_view.updateVirtualLines();\n        const vlines = self.text_buffer_view.virtual_lines.items;\n\n        if (vcursor.visual_row >= vlines.len) {\n            // Fallback: return end of current logical line\n            const logical_cursor = self.edit_buffer.getEOL();\n            return self.logicalToVisualCursor(logical_cursor.row, logical_cursor.col);\n        }\n\n        const vline = &vlines[vcursor.visual_row];\n        const logical_row = @as(u32, @intCast(vline.source_line));\n\n        // Determine the logical column at the end of this visual line\n        var logical_col: u32 = undefined;\n        if (vcursor.visual_row + 1 < vlines.len) {\n            const next_vline = &vlines[vcursor.visual_row + 1];\n            if (next_vline.source_line == vline.source_line) {\n                // Next visual line is a continuation of the same logical line\n                // The wrap boundary is at next_vline.source_col_offset\n                // To stay on the current visual line, we need to be one position BEFORE the boundary\n                // However, if width is 0, just use the start position\n                if (vline.width_cols > 0) {\n                    logical_col = vline.source_col_offset + vline.width_cols - 1;\n                } else {\n                    logical_col = vline.source_col_offset;\n                }\n            } else {\n                // Next visual line is a different logical line, so we're at the end\n                logical_col = iter_mod.lineWidthAt(self.edit_buffer.tb.rope(), logical_row);\n            }\n        } else {\n            // This is the last visual line, use end of logical line\n            logical_col = iter_mod.lineWidthAt(self.edit_buffer.tb.rope(), logical_row);\n        }\n\n        return self.logicalToVisualCursor(logical_row, logical_col);\n    }\n\n    // ============================================================================\n    // Placeholder - Visual Only\n    // ============================================================================\n\n    pub fn setPlaceholderStyledText(self: *EditorView, chunks: []const tb.StyledChunk) !void {\n        if (chunks.len == 0) {\n            if (self.placeholder_syntax_style) |style| {\n                style.deinit();\n                self.placeholder_syntax_style = null;\n            }\n            if (self.placeholder_buffer) |placeholder| {\n                placeholder.deinit();\n                self.placeholder_buffer = null;\n            }\n            if (self.placeholder_active) {\n                self.text_buffer_view.switchToOriginalBuffer();\n                self.placeholder_active = false;\n            }\n            return;\n        }\n\n        if (self.placeholder_buffer == null) {\n            self.placeholder_buffer = try UnifiedTextBuffer.init(\n                self.global_allocator,\n                self.edit_buffer.tb.pool,\n                self.edit_buffer.tb.link_pool,\n                self.edit_buffer.tb.width_method,\n            );\n            const syntax_style = try ss.SyntaxStyle.init(self.global_allocator);\n            self.placeholder_syntax_style = syntax_style;\n            const placeholder = self.placeholder_buffer.?;\n            placeholder.setSyntaxStyle(syntax_style);\n        }\n\n        const placeholder = self.placeholder_buffer.?;\n\n        try placeholder.setStyledText(chunks);\n\n        if (self.placeholder_active) {\n            self.text_buffer_view.virtual_lines_dirty = true;\n        }\n\n        self.updatePlaceholderVisibility();\n    }\n\n    fn shouldShowPlaceholder(self: *const EditorView) bool {\n        const rope_len = self.edit_buffer.tb.rope().totalWeight();\n        return rope_len == 0 and self.placeholder_buffer != null;\n    }\n\n    fn updatePlaceholderVisibility(self: *EditorView) void {\n        const should_show = self.shouldShowPlaceholder();\n\n        if (should_show and !self.placeholder_active) {\n            if (self.placeholder_buffer) |placeholder| {\n                self.text_buffer_view.switchToBuffer(placeholder);\n                self.placeholder_active = true;\n            }\n        } else if (!should_show and self.placeholder_active) {\n            self.text_buffer_view.switchToOriginalBuffer();\n            self.placeholder_active = false;\n        }\n    }\n\n    pub fn setTabIndicator(self: *EditorView, indicator: ?u32) void {\n        self.text_buffer_view.setTabIndicator(indicator);\n    }\n\n    pub fn getTabIndicator(self: *const EditorView) ?u32 {\n        return self.text_buffer_view.getTabIndicator();\n    }\n\n    pub fn setTabIndicatorColor(self: *EditorView, color: ?tb.RGBA) void {\n        self.text_buffer_view.setTabIndicatorColor(color);\n    }\n\n    pub fn getTabIndicatorColor(self: *const EditorView) ?tb.RGBA {\n        return self.text_buffer_view.getTabIndicatorColor();\n    }\n};\n"
  },
  {
    "path": "packages/core/src/zig/event-bus.zig",
    "content": "const std = @import(\"std\");\n\nvar global_event_callback: ?*const fn (namePtr: [*]const u8, nameLen: usize, dataPtr: [*]const u8, dataLen: usize) callconv(.c) void = null;\n\npub fn setEventCallback(callback: ?*const fn (namePtr: [*]const u8, nameLen: usize, dataPtr: [*]const u8, dataLen: usize) callconv(.c) void) void {\n    global_event_callback = callback;\n}\n\npub fn emit(name: []const u8, data: []const u8) void {\n    if (global_event_callback) |callback| {\n        callback(name.ptr, name.len, data.ptr, data.len);\n    }\n}\n"
  },
  {
    "path": "packages/core/src/zig/event-emitter.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\npub fn EventEmitter(comptime EventType: type) type {\n    if (@typeInfo(EventType) != .@\"enum\") {\n        @compileError(\"EventType must be an enum\");\n    }\n\n    return struct {\n        const Self = @This();\n\n        pub const Listener = struct {\n            ctx: *anyopaque,\n            handle: *const fn (ctx: *anyopaque) void,\n        };\n\n        allocator: Allocator,\n        listeners: std.EnumMap(EventType, std.ArrayListUnmanaged(Listener)),\n\n        pub fn init(allocator: Allocator) Self {\n            return .{\n                .allocator = allocator,\n                .listeners = std.EnumMap(EventType, std.ArrayListUnmanaged(Listener)).init(.{}),\n            };\n        }\n\n        pub fn deinit(self: *Self) void {\n            var iter = self.listeners.iterator();\n            while (iter.next()) |entry| {\n                entry.value.deinit(self.allocator);\n            }\n        }\n\n        pub fn on(self: *Self, event: EventType, listener: Listener) !void {\n            const list_ptr = self.listeners.getPtr(event) orelse {\n                self.listeners.put(event, .{});\n                return self.on(event, listener);\n            };\n\n            try list_ptr.append(self.allocator, listener);\n        }\n\n        pub fn off(self: *Self, event: EventType, listener: Listener) void {\n            const list_ptr = self.listeners.getPtr(event) orelse return;\n\n            var i: usize = 0;\n            while (i < list_ptr.items.len) {\n                const item = list_ptr.items[i];\n                if (item.ctx == listener.ctx and item.handle == listener.handle) {\n                    _ = list_ptr.swapRemove(i);\n                } else {\n                    i += 1;\n                }\n            }\n        }\n\n        pub fn emit(self: *Self, event: EventType) void {\n            const list_ptr = self.listeners.getPtr(event) orelse return;\n\n            for (list_ptr.items) |listener| {\n                listener.handle(listener.ctx);\n            }\n        }\n    };\n}\n"
  },
  {
    "path": "packages/core/src/zig/file-logger.zig",
    "content": "const std = @import(\"std\");\n\npub const LogLevel = enum(u8) {\n    err = 0,\n    warn = 1,\n    info = 2,\n    debug = 3,\n};\n\nvar log_file: ?std.fs.File = null;\nvar log_mutex: std.Thread.Mutex = .{};\nvar initialized: bool = false;\n\n/// Initialize the file logger with a timestamped filename (called automatically on first use)\nfn ensureInit() void {\n    if (initialized) return;\n\n    const timestamp = std.time.timestamp();\n    var filename_buf: [128]u8 = undefined;\n    const filename = std.fmt.bufPrint(&filename_buf, \"opentui_debug_{d}.log\", .{timestamp}) catch return;\n\n    log_file = std.fs.cwd().createFile(filename, .{ .truncate = true }) catch return;\n\n    // Log initialization\n    const init_msg = std.fmt.bufPrint(&filename_buf, \"=== Log initialized at timestamp {d} ===\\n\", .{timestamp}) catch return;\n    _ = log_file.?.write(init_msg) catch return;\n    log_file.?.sync() catch return;\n\n    initialized = true;\n}\n\n/// Close the log file\npub fn deinit() void {\n    log_mutex.lock();\n    defer log_mutex.unlock();\n\n    if (log_file) |file| {\n        file.close();\n        log_file = null;\n        initialized = false;\n    }\n}\n\n/// Log a message with level, file, line info and immediate flush\npub fn logMessage(level: LogLevel, comptime format: []const u8, args: anytype) void {\n    log_mutex.lock();\n    defer log_mutex.unlock();\n\n    // Auto-initialize on first use\n    if (!initialized) {\n        ensureInit();\n    }\n\n    if (log_file == null) return;\n\n    var buf: [8192]u8 = undefined;\n\n    const level_str = switch (level) {\n        .err => \"ERROR\",\n        .warn => \"WARN \",\n        .info => \"INFO \",\n        .debug => \"DEBUG\",\n    };\n\n    const timestamp = std.time.microTimestamp();\n\n    const msg = std.fmt.bufPrint(&buf, \"[{d}] {s}: \", .{ timestamp, level_str }) catch return;\n    _ = log_file.?.write(msg) catch return;\n\n    const user_msg = std.fmt.bufPrint(&buf, format, args) catch return;\n    _ = log_file.?.write(user_msg) catch return;\n    _ = log_file.?.write(\"\\n\") catch return;\n\n    // CRITICAL: Flush immediately so logs are on disk even if we crash\n    log_file.?.sync() catch return;\n}\n\npub fn err(comptime format: []const u8, args: anytype) void {\n    logMessage(.err, format, args);\n}\n\npub fn warn(comptime format: []const u8, args: anytype) void {\n    logMessage(.warn, format, args);\n}\n\npub fn info(comptime format: []const u8, args: anytype) void {\n    logMessage(.info, format, args);\n}\n\npub fn debug(comptime format: []const u8, args: anytype) void {\n    logMessage(.debug, format, args);\n}\n"
  },
  {
    "path": "packages/core/src/zig/grapheme.zig",
    "content": "const std = @import(\"std\");\n\npub const GraphemePoolError = error{\n    OutOfMemory,\n    InvalidId,\n    WrongGeneration,\n};\n\n// Encoding flags for char buffer entries (u32)\n// Bits 31-30: encoding type\n//   00xxxxxxxx: direct unicode scalar value (30 bits, as-is)\n//   10xxxxxxxx: grapheme start cell with pool ID (26 bits total payload)\n//   11xxxxxxxx: continuation cell marker for wide/grapheme rendering\npub const CHAR_FLAG_GRAPHEME: u32 = 0x8000_0000;\npub const CHAR_FLAG_CONTINUATION: u32 = 0xC000_0000;\n\n// For grapheme start and continuation cells:\n// Bits 29..28: right extent (u2), Bits 27..26: left extent (u2)\npub const CHAR_EXT_RIGHT_SHIFT: u5 = 28;\npub const CHAR_EXT_LEFT_SHIFT: u5 = 26;\npub const CHAR_EXT_MASK: u32 = 0x3;\n\n// Grapheme ID payload layout (26 bits total):\n// [ class (3 bits) | generation (7 bits) | slot_index (16 bits) ]\npub const GRAPHEME_ID_MASK: u32 = 0x03FF_FFFF;\npub const CLASS_BITS: u5 = 3;\npub const GENERATION_BITS: u5 = 7;\npub const SLOT_BITS: u5 = 16;\npub const CLASS_MASK: u32 = (@as(u32, 1) << CLASS_BITS) - 1; // 0b111\npub const GENERATION_MASK: u32 = (@as(u32, 1) << GENERATION_BITS) - 1; // 0b1111111\npub const SLOT_MASK: u32 = (@as(u32, 1) << SLOT_BITS) - 1; // 0xFFFF\n\n/// Global slab-allocated pool for grapheme clusters (byte slices)\n/// This is total overkill probably, but fun\n/// ID layout (26-bit payload):\n/// [ class (3 bits) | generation (7 bits) | slot_index (16 bits) ]\npub const GraphemePool = struct {\n    const MAX_CLASSES: u5 = 5; // 0..4 => 8,16,32,64,128\n    const CLASS_SIZES = [_]u32{ 8, 16, 32, 64, 128 };\n    const DEFAULT_SLOTS_PER_PAGE = [_]u32{ 256, 128, 64, 16, 8 };\n\n    pub const IdPayload = u32;\n\n    pub const InitOptions = struct {\n        /// Slots per page for each size class. If null, uses DEFAULT_SLOTS_PER_PAGE.\n        /// Used to limit pool size for testing.\n        slots_per_page: ?[MAX_CLASSES]u32 = null,\n    };\n\n    allocator: std.mem.Allocator,\n    classes: [MAX_CLASSES]ClassPool,\n    interned_live_ids: std.StringHashMapUnmanaged(IdPayload),\n\n    const SlotHeader = extern struct {\n        len: u16,\n        refcount: u32,\n        generation: u32,\n        is_owned: u32, // 0 = unowned (external memory), 1 = owned (copied into pool)\n    };\n\n    pub fn init(allocator: std.mem.Allocator) GraphemePool {\n        return initWithOptions(allocator, .{});\n    }\n\n    pub fn initWithOptions(allocator: std.mem.Allocator, options: InitOptions) GraphemePool {\n        const slots_per_page = options.slots_per_page orelse DEFAULT_SLOTS_PER_PAGE;\n        var classes: [MAX_CLASSES]ClassPool = undefined;\n        var i: usize = 0;\n        while (i < MAX_CLASSES) : (i += 1) {\n            classes[i] = ClassPool.init(allocator, CLASS_SIZES[i], slots_per_page[i]);\n        }\n        return .{ .allocator = allocator, .classes = classes, .interned_live_ids = .{} };\n    }\n\n    pub fn deinit(self: *GraphemePool) void {\n        var key_it = self.interned_live_ids.keyIterator();\n        while (key_it.next()) |key_ptr| {\n            self.allocator.free(@constCast(key_ptr.*));\n        }\n        self.interned_live_ids.deinit(self.allocator);\n\n        var i: usize = 0;\n        while (i < MAX_CLASSES) : (i += 1) {\n            self.classes[i].deinit();\n        }\n    }\n\n    /// removeInternedLiveId removes an interned ID from the live set if it\n    /// matches the expected ID.\n    fn removeInternedLiveId(self: *GraphemePool, bytes: []const u8, expected_id: IdPayload) void {\n        const live_id = self.interned_live_ids.get(bytes) orelse return;\n        if (live_id != expected_id) return;\n        if (self.interned_live_ids.fetchRemove(bytes)) |removed| {\n            self.allocator.free(@constCast(removed.key));\n        }\n    }\n\n    /// lookupOrInvalidate checks if the given bytes are already interned and live, returning the existing ID if so.\n    fn lookupOrInvalidate(self: *GraphemePool, bytes: []const u8) ?IdPayload {\n        const live_id = self.interned_live_ids.get(bytes) orelse return null;\n\n        // Verify that the live ID is still valid and matches the bytes. If get\n        // fails, the ID is no longer valid, so remove it from the interned map.\n        const live_bytes = self.get(live_id) catch {\n            self.removeInternedLiveId(bytes, live_id);\n            return null;\n        };\n\n        // If the bytes don't match, this means the ID was recycled and now points\n        // to different data. Invalidate the interned ID.\n        if (!std.mem.eql(u8, live_bytes, bytes)) {\n            self.removeInternedLiveId(bytes, live_id);\n            return null;\n        }\n\n        // check refcount > 0 to ensure the ID is still live. If refcount is 0,\n        // the slot is free but hasn't been reused yet, so we can treat it as\n        // not found.\n        const live_refcount = self.getRefcount(live_id) catch {\n            self.removeInternedLiveId(bytes, live_id);\n            return null;\n        };\n        if (live_refcount == 0) {\n            self.removeInternedLiveId(bytes, live_id);\n            return null;\n        }\n\n        return live_id;\n    }\n\n    /// internLiveId interns the grapheme bytes.\n    fn internLiveId(self: *GraphemePool, id: IdPayload, bytes: []const u8) GraphemePoolError!void {\n        if (self.lookupOrInvalidate(bytes) != null) {\n            // Keep existing interned ID if it's still valid.\n            return;\n        }\n\n        const owned_key = self.allocator.dupe(u8, bytes) catch return GraphemePoolError.OutOfMemory;\n        errdefer self.allocator.free(owned_key);\n\n        if (self.interned_live_ids.fetchPut(self.allocator, owned_key, id) catch return GraphemePoolError.OutOfMemory) |replaced| {\n            // A previous key allocation was replaced.\n            self.allocator.free(@constCast(replaced.key));\n        }\n    }\n\n    fn classForSize(size: usize) u32 {\n        if (size <= 8) return 0;\n        if (size <= 16) return 1;\n        if (size <= 32) return 2;\n        if (size <= 64) return 3;\n        return 4; // up to 128\n    }\n\n    fn packId(class_id: u32, slot_index: u32, generation: u32) GraphemePoolError!IdPayload {\n        if (slot_index > SLOT_MASK) return GraphemePoolError.OutOfMemory;\n        return (class_id << (GENERATION_BITS + SLOT_BITS)) |\n            ((generation & GENERATION_MASK) << SLOT_BITS) |\n            (slot_index & SLOT_MASK);\n    }\n\n    pub fn alloc(self: *GraphemePool, bytes: []const u8) GraphemePoolError!IdPayload {\n        if (self.lookupOrInvalidate(bytes)) |live_id| {\n            return live_id;\n        }\n\n        const class_id: u32 = classForSize(bytes.len);\n        const slot_index = try self.classes[class_id].allocInternal(bytes, true);\n        const generation = self.classes[class_id].getGeneration(slot_index);\n        return try packId(class_id, slot_index, generation);\n    }\n\n    /// Allocate an ID for externally managed memory (no copy, just reference)\n    /// The caller is responsible for keeping the memory valid while the ID is in use\n    pub fn allocUnowned(self: *GraphemePool, bytes: []const u8) GraphemePoolError!IdPayload {\n        // For unowned allocations, we need space for a pointer\n        const ptr_size = @sizeOf(usize);\n        const class_id: u32 = classForSize(ptr_size);\n        const slot_index = try self.classes[class_id].allocInternal(bytes, false);\n        const generation = self.classes[class_id].getGeneration(slot_index);\n        return try packId(class_id, slot_index, generation);\n    }\n\n    pub fn incref(self: *GraphemePool, id: IdPayload) GraphemePoolError!void {\n        const class_id: u32 = (id >> (GENERATION_BITS + SLOT_BITS)) & CLASS_MASK;\n        if (class_id >= MAX_CLASSES) return GraphemePoolError.InvalidId;\n        const slot_index: u32 = id & SLOT_MASK;\n        const generation: u32 = (id >> SLOT_BITS) & GENERATION_MASK;\n        const old_refcount = try self.classes[class_id].getRefcount(slot_index, generation);\n        try self.classes[class_id].incref(slot_index, generation);\n\n        if (old_refcount == 0) {\n            const is_owned = try self.classes[class_id].isOwned(slot_index, generation);\n            if (is_owned) {\n                // This is a transition from 0 to 1 for owned bytes, so intern it.\n                const bytes = try self.classes[class_id].get(slot_index, generation);\n                try self.internLiveId(id, bytes);\n            }\n        }\n    }\n\n    pub fn decref(self: *GraphemePool, id: IdPayload) GraphemePoolError!void {\n        const class_id: u32 = (id >> (GENERATION_BITS + SLOT_BITS)) & CLASS_MASK;\n        if (class_id >= MAX_CLASSES) return GraphemePoolError.InvalidId;\n        const slot_index: u32 = id & SLOT_MASK;\n        const generation: u32 = (id >> SLOT_BITS) & GENERATION_MASK;\n\n        const old_refcount = try self.classes[class_id].getRefcount(slot_index, generation);\n        if (old_refcount == 1) {\n            const is_owned = try self.classes[class_id].isOwned(slot_index, generation);\n            if (is_owned) {\n                // This is a transition from 1 to 0 for owned bytes, remove map entry.\n                const bytes = try self.classes[class_id].get(slot_index, generation);\n                self.removeInternedLiveId(bytes, id);\n            }\n        }\n\n        try self.classes[class_id].decref(slot_index, generation);\n    }\n\n    /// Free a freshly allocated slot that was never incref'd (refcount=0).\n    /// Use this for cleanup when allocation succeeded but the slot was never used.\n    /// This prevents slot leaks when an error occurs between alloc and incref.\n    pub fn freeUnreferenced(self: *GraphemePool, id: IdPayload) GraphemePoolError!void {\n        const class_id: u32 = (id >> (GENERATION_BITS + SLOT_BITS)) & CLASS_MASK;\n        if (class_id >= MAX_CLASSES) return GraphemePoolError.InvalidId;\n        const slot_index: u32 = id & SLOT_MASK;\n        const generation: u32 = (id >> SLOT_BITS) & GENERATION_MASK;\n\n        const is_owned = try self.classes[class_id].isOwned(slot_index, generation);\n        if (is_owned) {\n            const bytes = try self.classes[class_id].get(slot_index, generation);\n            self.removeInternedLiveId(bytes, id);\n        }\n\n        try self.classes[class_id].freeUnreferenced(slot_index, generation);\n    }\n\n    pub fn get(self: *GraphemePool, id: IdPayload) GraphemePoolError![]const u8 {\n        const class_id: u32 = (id >> (GENERATION_BITS + SLOT_BITS)) & CLASS_MASK;\n        if (class_id >= MAX_CLASSES) return GraphemePoolError.InvalidId;\n        const slot_index: u32 = id & SLOT_MASK;\n        const generation: u32 = (id >> SLOT_BITS) & GENERATION_MASK;\n        return self.classes[class_id].get(slot_index, generation);\n    }\n\n    pub fn getRefcount(self: *GraphemePool, id: IdPayload) GraphemePoolError!u32 {\n        const class_id: u32 = (id >> (GENERATION_BITS + SLOT_BITS)) & CLASS_MASK;\n        if (class_id >= MAX_CLASSES) return GraphemePoolError.InvalidId;\n        const slot_index: u32 = id & SLOT_MASK;\n        const generation: u32 = (id >> SLOT_BITS) & GENERATION_MASK;\n        return self.classes[class_id].getRefcount(slot_index, generation);\n    }\n\n    const ClassPool = struct {\n        allocator: std.mem.Allocator,\n        slot_capacity: u32,\n        slots_per_page: u32,\n        slot_size_bytes: usize,\n        slots: std.ArrayListUnmanaged(u8),\n        free_list: std.ArrayListUnmanaged(u32),\n        num_slots: u32,\n\n        pub fn init(allocator: std.mem.Allocator, slot_capacity: u32, slots_per_page: u32) ClassPool {\n            // Align slot size to SlotHeader alignment to prevent UB from misaligned access\n            const raw_slot_size = @sizeOf(SlotHeader) + slot_capacity;\n            const slot_size_bytes = std.mem.alignForward(usize, raw_slot_size, @alignOf(SlotHeader));\n            return .{\n                .allocator = allocator,\n                .slot_capacity = slot_capacity,\n                .slots_per_page = slots_per_page,\n                .slot_size_bytes = slot_size_bytes,\n                .slots = .{},\n                .free_list = .{},\n                .num_slots = 0,\n            };\n        }\n\n        pub fn deinit(self: *ClassPool) void {\n            self.slots.deinit(self.allocator);\n            self.free_list.deinit(self.allocator);\n        }\n\n        fn grow(self: *ClassPool) GraphemePoolError!void {\n            const add_bytes = self.slot_size_bytes * self.slots_per_page;\n\n            try self.slots.ensureTotalCapacity(self.allocator, self.slots.items.len + add_bytes);\n            try self.slots.appendNTimes(self.allocator, 0, add_bytes);\n\n            var i: u32 = 0;\n            while (i < self.slots_per_page) : (i += 1) {\n                try self.free_list.append(self.allocator, self.num_slots + i);\n            }\n            self.num_slots += self.slots_per_page;\n        }\n\n        fn slotPtr(self: *ClassPool, slot_index: u32) *u8 {\n            const offset: usize = @as(usize, slot_index) * self.slot_size_bytes;\n            return &self.slots.items[offset];\n        }\n\n        pub fn allocInternal(self: *ClassPool, bytes: []const u8, is_owned: bool) GraphemePoolError!u32 {\n            // Validate size for owned allocations\n            if (is_owned and bytes.len > self.slot_capacity) {\n                @panic(\"ClassPool.allocInternal: bytes.len > slot_capacity\");\n            }\n\n            if (self.free_list.items.len == 0) try self.grow();\n\n            const slot_index = self.free_list.pop().?;\n            const p = self.slotPtr(slot_index);\n            const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n\n            // Increment generation when reusing a slot, wrapping at 7 bits (128 values)\n            const new_generation = (header_ptr.generation + 1) & GENERATION_MASK;\n\n            // Calculate length based on ownership\n            const len: u16 = if (is_owned) @intCast(@min(bytes.len, self.slot_capacity)) else @intCast(bytes.len);\n\n            header_ptr.* = .{\n                .len = len,\n                .refcount = 0,\n                .generation = new_generation,\n                .is_owned = if (is_owned) 1 else 0,\n            };\n\n            const data_ptr = @as([*]u8, @ptrCast(p)) + @sizeOf(SlotHeader);\n\n            if (is_owned) {\n                // Owned: copy bytes into our storage\n                @memcpy(data_ptr[0..header_ptr.len], bytes[0..header_ptr.len]);\n            } else {\n                // Unowned: store pointer to external memory\n                const ptr_storage = @as(*[*]const u8, @ptrCast(@alignCast(data_ptr)));\n                ptr_storage.* = bytes.ptr;\n            }\n\n            return slot_index;\n        }\n\n        pub fn getGeneration(self: *ClassPool, slot_index: u32) u32 {\n            if (slot_index >= self.num_slots) return 0;\n            const p = self.slotPtr(slot_index);\n            const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n            return header_ptr.generation;\n        }\n\n        pub fn incref(self: *ClassPool, slot_index: u32, expected_generation: u32) GraphemePoolError!void {\n            const p = self.slotPtr(slot_index);\n            const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n            if (header_ptr.generation != expected_generation) {\n                // Generation mismatch - this is a stale reference\n                return GraphemePoolError.WrongGeneration;\n            }\n            header_ptr.refcount +%= 1;\n        }\n\n        pub fn decref(self: *ClassPool, slot_index: u32, expected_generation: u32) GraphemePoolError!void {\n            const p = self.slotPtr(slot_index);\n            const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n\n            if (header_ptr.refcount == 0) return GraphemePoolError.InvalidId;\n            if (header_ptr.generation != expected_generation) return GraphemePoolError.WrongGeneration;\n\n            header_ptr.refcount -%= 1;\n\n            if (header_ptr.refcount == 0) {\n                try self.free_list.append(self.allocator, slot_index);\n            }\n        }\n\n        /// Free a slot that has refcount=0 (freshly allocated, never incref'd).\n        /// This is used for cleanup when allocation succeeded but the caller\n        /// needs to abort before taking ownership via incref.\n        pub fn freeUnreferenced(self: *ClassPool, slot_index: u32, expected_generation: u32) GraphemePoolError!void {\n            if (slot_index >= self.num_slots) return GraphemePoolError.InvalidId;\n            const p = self.slotPtr(slot_index);\n            const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n\n            if (header_ptr.generation != expected_generation) return GraphemePoolError.WrongGeneration;\n            if (header_ptr.refcount != 0) return GraphemePoolError.InvalidId; // Not unreferenced\n\n            try self.free_list.append(self.allocator, slot_index);\n        }\n\n        pub fn get(self: *ClassPool, slot_index: u32, expected_generation: u32) GraphemePoolError![]const u8 {\n            if (slot_index >= self.num_slots) return GraphemePoolError.InvalidId;\n\n            const p = self.slotPtr(slot_index);\n            const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n            // Validate generation to prevent accessing stale data\n            if (header_ptr.generation != expected_generation) return GraphemePoolError.WrongGeneration;\n\n            const data_ptr = @as([*]u8, @ptrCast(p)) + @sizeOf(SlotHeader);\n\n            if (header_ptr.is_owned == 1) {\n                // Owned memory: return slice from our storage\n                return data_ptr[0..header_ptr.len];\n            } else {\n                // Unowned memory: dereference stored pointer\n                const ptr_storage = @as(*[*]const u8, @ptrCast(@alignCast(data_ptr)));\n                const external_ptr = ptr_storage.*;\n                return external_ptr[0..header_ptr.len];\n            }\n        }\n\n        pub fn getRefcount(self: *ClassPool, slot_index: u32, expected_generation: u32) GraphemePoolError!u32 {\n            if (slot_index >= self.num_slots) return GraphemePoolError.InvalidId;\n            const p = self.slotPtr(slot_index);\n            const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n            if (header_ptr.generation != expected_generation) return GraphemePoolError.WrongGeneration;\n            return header_ptr.refcount;\n        }\n\n        pub fn isOwned(self: *ClassPool, slot_index: u32, expected_generation: u32) GraphemePoolError!bool {\n            if (slot_index >= self.num_slots) return GraphemePoolError.InvalidId;\n            const p = self.slotPtr(slot_index);\n            const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n            if (header_ptr.generation != expected_generation) return GraphemePoolError.WrongGeneration;\n            return header_ptr.is_owned == 1;\n        }\n    };\n};\n\n// Bit manipulation functions for encoded char values\n\npub fn isGraphemeChar(c: u32) bool {\n    return (c & 0xC000_0000) == CHAR_FLAG_GRAPHEME;\n}\n\npub fn isContinuationChar(c: u32) bool {\n    return (c & 0xC000_0000) == CHAR_FLAG_CONTINUATION;\n}\n\npub fn isClusterChar(c: u32) bool {\n    return (c & 0x8000_0000) == 0x8000_0000;\n}\n\npub fn graphemeIdFromChar(c: u32) u32 {\n    return c & GRAPHEME_ID_MASK;\n}\n\npub fn charRightExtent(c: u32) u32 {\n    return (c >> CHAR_EXT_RIGHT_SHIFT) & CHAR_EXT_MASK;\n}\n\npub fn charLeftExtent(c: u32) u32 {\n    return (c >> CHAR_EXT_LEFT_SHIFT) & CHAR_EXT_MASK;\n}\n\npub fn packGraphemeStart(gid: u32, total_width: u32) u32 {\n    const width_minus_one: u32 = if (total_width == 0) 0 else @intCast(@min(total_width - 1, 3));\n    const right: u32 = width_minus_one;\n    const left: u32 = 0;\n    return CHAR_FLAG_GRAPHEME |\n        ((right & CHAR_EXT_MASK) << CHAR_EXT_RIGHT_SHIFT) |\n        ((left & CHAR_EXT_MASK) << CHAR_EXT_LEFT_SHIFT) |\n        (gid & GRAPHEME_ID_MASK);\n}\n\npub fn packContinuation(left: u32, right: u32, gid: u32) u32 {\n    return CHAR_FLAG_CONTINUATION |\n        ((@min(left, 3) & CHAR_EXT_MASK) << CHAR_EXT_LEFT_SHIFT) |\n        ((@min(right, 3) & CHAR_EXT_MASK) << CHAR_EXT_RIGHT_SHIFT) |\n        (gid & GRAPHEME_ID_MASK);\n}\n\npub fn encodedCharWidth(c: u32) u32 {\n    if (isContinuationChar(c)) {\n        const left = charLeftExtent(c);\n        const right = charRightExtent(c);\n        return left + 1 + right;\n    } else if (isGraphemeChar(c)) {\n        return charRightExtent(c) + 1;\n    } else {\n        return 1;\n    }\n}\n\nvar GLOBAL_POOL_STORAGE: ?GraphemePool = null;\n\npub fn initGlobalPool(allocator: std.mem.Allocator) *GraphemePool {\n    return initGlobalPoolWithOptions(allocator, .{});\n}\n\npub fn initGlobalPoolWithOptions(allocator: std.mem.Allocator, options: GraphemePool.InitOptions) *GraphemePool {\n    if (GLOBAL_POOL_STORAGE == null) {\n        GLOBAL_POOL_STORAGE = GraphemePool.initWithOptions(allocator, options);\n    }\n    return &GLOBAL_POOL_STORAGE.?;\n}\n\npub fn deinitGlobalPool() void {\n    if (GLOBAL_POOL_STORAGE) |*p| {\n        p.deinit();\n        GLOBAL_POOL_STORAGE = null;\n    }\n}\n\npub const GraphemeTracker = struct {\n    pool: *GraphemePool,\n    used_ids: std.AutoHashMap(u32, u32), // id -> number of cells in this buffer\n\n    pub fn init(allocator: std.mem.Allocator, pool: *GraphemePool) GraphemeTracker {\n        return .{\n            .pool = pool,\n            .used_ids = std.AutoHashMap(u32, u32).init(allocator),\n        };\n    }\n\n    fn decRefAll(self: *GraphemeTracker) void {\n        var it = self.used_ids.keyIterator();\n        while (it.next()) |idp| {\n            // Pool refs are tracked per ID (first/last cell transition), so clear\n            // decrefs once per tracked ID, not once per per-buffer cell count.\n            self.pool.decref(idp.*) catch {};\n        }\n    }\n\n    pub fn deinit(self: *GraphemeTracker) void {\n        self.decRefAll();\n        self.used_ids.deinit();\n    }\n\n    pub fn clear(self: *GraphemeTracker) void {\n        self.decRefAll();\n        self.used_ids.clearRetainingCapacity();\n    }\n\n    pub fn add(self: *GraphemeTracker, id: u32) void {\n        const res = self.used_ids.getOrPut(id) catch |err| {\n            std.debug.panic(\"GraphemeTracker.add failed: {}\\n\", .{err});\n        };\n        if (!res.found_existing) {\n            res.value_ptr.* = 1;\n            self.pool.incref(id) catch |err| {\n                std.debug.panic(\"GraphemeTracker.add incref failed: {}\\n\", .{err});\n            };\n        } else {\n            res.value_ptr.* += 1;\n        }\n    }\n\n    pub fn remove(self: *GraphemeTracker, id: u32) void {\n        const count_ptr = self.used_ids.getPtr(id) orelse return;\n        if (count_ptr.* > 1) {\n            count_ptr.* -= 1;\n            return;\n        }\n\n        if (self.used_ids.remove(id)) {\n            self.pool.decref(id) catch {};\n        }\n    }\n\n    pub fn replace(self: *GraphemeTracker, old_id: ?u32, new_id: ?u32) void {\n        if (old_id != null and new_id != null and old_id.? == new_id.?) return;\n\n        if (new_id) |id| self.add(id);\n        if (old_id) |id| self.remove(id);\n    }\n\n    pub fn contains(self: *const GraphemeTracker, id: u32) bool {\n        return self.used_ids.contains(id);\n    }\n\n    pub fn hasAny(self: *const GraphemeTracker) bool {\n        return self.used_ids.count() > 0;\n    }\n\n    pub fn getGraphemeCount(self: *const GraphemeTracker) u32 {\n        return @intCast(self.used_ids.count());\n    }\n\n    pub fn getGraphemeCellCount(self: *const GraphemeTracker) u32 {\n        var total: u32 = 0;\n        var it = self.used_ids.valueIterator();\n        while (it.next()) |count_ptr| {\n            total += count_ptr.*;\n        }\n        return total;\n    }\n\n    pub fn getTotalGraphemeBytes(self: *const GraphemeTracker) u32 {\n        var total_bytes: u32 = 0;\n        var it = self.used_ids.iterator();\n        while (it.next()) |entry| {\n            const id = entry.key_ptr.*;\n            const count = entry.value_ptr.*;\n            if (self.pool.get(id)) |bytes| {\n                total_bytes += @as(u32, @intCast(bytes.len)) * count;\n            } else |_| {\n                // If we can't get the bytes, this shouldn't happen but handle gracefully\n                continue;\n            }\n        }\n        return total_bytes;\n    }\n};\n"
  },
  {
    "path": "packages/core/src/zig/lib.zig",
    "content": "const std = @import(\"std\");\nconst build_options = @import(\"build_options\");\nconst Allocator = std.mem.Allocator;\n\nconst ansi = @import(\"ansi.zig\");\nconst buffer = @import(\"buffer.zig\");\nconst renderer = @import(\"renderer.zig\");\nconst gp = @import(\"grapheme.zig\");\nconst link = @import(\"link.zig\");\nconst text_buffer = @import(\"text-buffer.zig\");\nconst text_buffer_view = @import(\"text-buffer-view.zig\");\nconst edit_buffer_mod = @import(\"edit-buffer.zig\");\nconst editor_view = @import(\"editor-view.zig\");\nconst syntax_style = @import(\"syntax-style.zig\");\nconst terminal = @import(\"terminal.zig\");\nconst utf8 = @import(\"utf8.zig\");\nconst logger = @import(\"logger.zig\");\nconst event_bus = @import(\"event-bus.zig\");\nconst utils = @import(\"utils.zig\");\nconst native_span_feed = @import(\"native-span-feed.zig\");\nconst buffer_effects = @import(\"buffer-methods.zig\");\n\npub const OptimizedBuffer = buffer.OptimizedBuffer;\npub const CliRenderer = renderer.CliRenderer;\npub const Terminal = terminal.Terminal;\npub const RGBA = buffer.RGBA;\n\ncomptime {\n    _ = native_span_feed;\n}\n\nexport fn setLogCallback(callback: ?*const fn (level: u8, msgPtr: [*]const u8, msgLen: usize) callconv(.c) void) void {\n    logger.setLogCallback(callback);\n}\n\nexport fn setEventCallback(callback: ?*const fn (namePtr: [*]const u8, nameLen: usize, dataPtr: [*]const u8, dataLen: usize) callconv(.c) void) void {\n    event_bus.setEventCallback(callback);\n}\n\nvar gpa = std.heap.GeneralPurposeAllocator(.{\n    .enable_memory_limit = build_options.gpa_safe_stats,\n    .safety = build_options.gpa_safe_stats,\n}){};\nconst globalAllocator = gpa.allocator();\nvar arena = std.heap.ArenaAllocator.init(globalAllocator);\nconst globalArena = arena.allocator();\n\npub const ExternalBuildOptions = extern struct {\n    gpa_safe_stats: bool,\n    gpa_memory_limit_tracking: bool,\n};\n\npub const ExternalAllocatorStats = extern struct {\n    total_requested_bytes: u64,\n    active_allocations: u64,\n    small_allocations: u64,\n    large_allocations: u64,\n    requested_bytes_valid: bool,\n};\n\nfn toNonNegativeU64(value: anytype) u64 {\n    const ValueType = @TypeOf(value);\n\n    return switch (@typeInfo(ValueType)) {\n        .int => |int_info| if (int_info.signedness == .signed) blk: {\n            const signed_value: i64 = @intCast(value);\n            if (signed_value <= 0) break :blk 0;\n            break :blk @intCast(signed_value);\n        } else @intCast(value),\n        .comptime_int => blk: {\n            if (value <= 0) break :blk 0;\n            break :blk @intCast(value);\n        },\n        else => 0,\n    };\n}\n\nconst RequestedBytesInfo = struct {\n    bytes: u64,\n    valid: bool,\n};\n\nfn sanitizeRequestedBytes(value: u64) RequestedBytesInfo {\n    const signed_value: i64 = @bitCast(value);\n    if (signed_value < 0) {\n        return .{ .bytes = 0, .valid = false };\n    }\n\n    return .{ .bytes = @intCast(signed_value), .valid = true };\n}\n\nfn queryStatsField(comptime field_names: []const []const u8) ?u64 {\n    if (!@hasDecl(@TypeOf(gpa), \"queryStats\")) {\n        return null;\n    }\n\n    const stats = gpa.queryStats();\n    const StatsType = @TypeOf(stats);\n\n    inline for (field_names) |field_name| {\n        if (@hasField(StatsType, field_name)) {\n            return toNonNegativeU64(@field(stats, field_name));\n        }\n    }\n\n    return null;\n}\n\nfn getTotalRequestedBytesInfo() RequestedBytesInfo {\n    if (!build_options.gpa_safe_stats) {\n        return .{ .bytes = 0, .valid = false };\n    }\n\n    if (queryStatsField(&.{\"total_requested_bytes\"})) |value| {\n        return sanitizeRequestedBytes(value);\n    }\n\n    if (@hasField(@TypeOf(gpa), \"total_requested_bytes\")) {\n        if (@TypeOf(gpa.total_requested_bytes) == void) {\n            return .{ .bytes = 0, .valid = false };\n        }\n\n        return sanitizeRequestedBytes(toNonNegativeU64(gpa.total_requested_bytes));\n    }\n\n    return .{ .bytes = 0, .valid = false };\n}\n\nfn getSmallAllocationCount() u64 {\n    if (queryStatsField(&.{ \"small_allocations\", \"small_allocation_count\" })) |value| {\n        return value;\n    }\n\n    var total: u64 = 0;\n    for (gpa.buckets) |bucket_head| {\n        var current = bucket_head;\n        while (current) |bucket| {\n            const allocated: u64 = @intCast(bucket.allocated_count);\n            const freed: u64 = @intCast(bucket.freed_count);\n            if (allocated >= freed) {\n                total += allocated - freed;\n            }\n            current = bucket.next;\n        }\n    }\n\n    return total;\n}\n\nfn getLargeAllocationCount() u64 {\n    if (queryStatsField(&.{ \"large_allocations\", \"large_allocation_count\" })) |value| {\n        return value;\n    }\n\n    return @intCast(gpa.large_allocations.count());\n}\n\nexport fn createNativeSpanFeed(options_ptr: ?*const native_span_feed.Options) ?*native_span_feed.Stream {\n    return native_span_feed.createNativeSpanFeedWithAllocator(globalAllocator, options_ptr);\n}\n\nexport fn getArenaAllocatedBytes() usize {\n    return arena.queryCapacity();\n}\n\nexport fn getBuildOptions(out_ptr: *ExternalBuildOptions) void {\n    out_ptr.* = .{\n        .gpa_safe_stats = build_options.gpa_safe_stats,\n        .gpa_memory_limit_tracking = build_options.gpa_safe_stats,\n    };\n}\n\nexport fn getAllocatorStats(out_ptr: *ExternalAllocatorStats) void {\n    const small_allocations = getSmallAllocationCount();\n    const large_allocations = getLargeAllocationCount();\n    const active_allocations = small_allocations + large_allocations;\n    const requested_bytes = getTotalRequestedBytesInfo();\n\n    out_ptr.* = .{\n        .total_requested_bytes = requested_bytes.bytes,\n        .active_allocations = active_allocations,\n        .small_allocations = small_allocations,\n        .large_allocations = large_allocations,\n        .requested_bytes_valid = requested_bytes.valid,\n    };\n}\n\nexport fn createRenderer(width: u32, height: u32, testing: bool, remote: bool) ?*renderer.CliRenderer {\n    if (width == 0 or height == 0) {\n        logger.warn(\"Invalid renderer dimensions: {}x{}\", .{ width, height });\n        return null;\n    }\n\n    const pool = gp.initGlobalPool(globalArena);\n    _ = link.initGlobalLinkPool(globalArena);\n    return renderer.CliRenderer.createWithOptions(globalAllocator, width, height, pool, testing, remote) catch |err| {\n        logger.err(\"Failed to create renderer: {}\", .{err});\n        return null;\n    };\n}\n\nexport fn setTerminalEnvVar(rendererPtr: *renderer.CliRenderer, keyPtr: [*]const u8, keyLen: usize, valuePtr: [*]const u8, valueLen: usize) bool {\n    const key = keyPtr[0..keyLen];\n    const value = valuePtr[0..valueLen];\n    return rendererPtr.setTerminalEnvVar(key, value);\n}\n\nexport fn setUseThread(rendererPtr: *renderer.CliRenderer, useThread: bool) void {\n    rendererPtr.setUseThread(useThread);\n}\n\nexport fn destroyRenderer(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.destroy();\n}\n\nexport fn setBackgroundColor(rendererPtr: *renderer.CliRenderer, color: [*]const f32) void {\n    rendererPtr.setBackgroundColor(utils.f32PtrToRGBA(color));\n}\n\nexport fn setRenderOffset(rendererPtr: *renderer.CliRenderer, offset: u32) void {\n    rendererPtr.setRenderOffset(offset);\n}\n\nexport fn updateStats(rendererPtr: *renderer.CliRenderer, time: f64, fps: u32, frameCallbackTime: f64) void {\n    rendererPtr.updateStats(time, fps, frameCallbackTime);\n}\n\nexport fn updateMemoryStats(rendererPtr: *renderer.CliRenderer, heapUsed: u32, heapTotal: u32, arrayBuffers: u32) void {\n    rendererPtr.updateMemoryStats(heapUsed, heapTotal, arrayBuffers);\n}\n\nexport fn getNextBuffer(rendererPtr: *renderer.CliRenderer) *buffer.OptimizedBuffer {\n    return rendererPtr.getNextBuffer();\n}\n\nexport fn getCurrentBuffer(rendererPtr: *renderer.CliRenderer) *buffer.OptimizedBuffer {\n    return rendererPtr.getCurrentBuffer();\n}\n\nconst OutputSlice = extern struct {\n    ptr: [*]const u8,\n    len: usize,\n};\n\nexport fn getLastOutputForTest(rendererPtr: *renderer.CliRenderer, outSlice: *OutputSlice) void {\n    const output = rendererPtr.getLastOutputForTest();\n    outSlice.ptr = output.ptr;\n    outSlice.len = output.len;\n}\n\nexport fn setHyperlinksCapability(rendererPtr: *renderer.CliRenderer, enabled: bool) void {\n    rendererPtr.terminal.caps.hyperlinks = enabled;\n}\n\nexport fn clearGlobalLinkPool() void {\n    link.deinitGlobalLinkPool();\n}\n\nexport fn getBufferWidth(bufferPtr: *buffer.OptimizedBuffer) u32 {\n    return bufferPtr.width;\n}\n\nexport fn getBufferHeight(bufferPtr: *buffer.OptimizedBuffer) u32 {\n    return bufferPtr.height;\n}\n\nexport fn render(rendererPtr: *renderer.CliRenderer, force: bool) void {\n    rendererPtr.render(force);\n}\n\nexport fn createOptimizedBuffer(width: u32, height: u32, respectAlpha: bool, widthMethod: u8, idPtr: [*]const u8, idLen: usize) ?*buffer.OptimizedBuffer {\n    if (width == 0 or height == 0) {\n        logger.warn(\"Invalid buffer dimensions: {}x{}\", .{ width, height });\n        return null;\n    }\n\n    const pool = gp.initGlobalPool(globalArena);\n    const link_pool = link.initGlobalLinkPool(globalArena);\n    const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else .unicode;\n    const id = idPtr[0..idLen];\n\n    return buffer.OptimizedBuffer.init(globalAllocator, width, height, .{\n        .respectAlpha = respectAlpha,\n        .pool = pool,\n        .width_method = wMethod,\n        .id = id,\n        .link_pool = link_pool,\n    }) catch |err| {\n        logger.err(\"Failed to create optimized buffer: {}\", .{err});\n        return null;\n    };\n}\n\nexport fn destroyOptimizedBuffer(bufferPtr: *buffer.OptimizedBuffer) void {\n    bufferPtr.deinit();\n}\n\nexport fn destroyFrameBuffer(frameBufferPtr: *buffer.OptimizedBuffer) void {\n    destroyOptimizedBuffer(frameBufferPtr);\n}\n\nexport fn drawFrameBuffer(targetPtr: *buffer.OptimizedBuffer, destX: i32, destY: i32, frameBuffer: *buffer.OptimizedBuffer, sourceX: u32, sourceY: u32, sourceWidth: u32, sourceHeight: u32) void {\n    const srcX = if (sourceX == 0) null else sourceX;\n    const srcY = if (sourceY == 0) null else sourceY;\n    const srcWidth = if (sourceWidth == 0) null else sourceWidth;\n    const srcHeight = if (sourceHeight == 0) null else sourceHeight;\n\n    targetPtr.drawFrameBuffer(destX, destY, frameBuffer, srcX, srcY, srcWidth, srcHeight);\n}\n\nexport fn setCursorPosition(rendererPtr: *renderer.CliRenderer, x: i32, y: i32, visible: bool) void {\n    rendererPtr.terminal.setCursorPosition(@intCast(@max(1, x)), @intCast(@max(1, y)), visible);\n}\n\npub const ExternalCapabilities = extern struct {\n    kitty_keyboard: bool,\n    kitty_graphics: bool,\n    rgb: bool,\n    unicode: u8, // 0 = wcwidth, 1 = unicode\n    sgr_pixels: bool,\n    color_scheme_updates: bool,\n    explicit_width: bool,\n    scaled_text: bool,\n    sixel: bool,\n    focus_tracking: bool,\n    sync: bool,\n    bracketed_paste: bool,\n    hyperlinks: bool,\n    osc52: bool,\n    explicit_cursor_positioning: bool,\n    term_name_ptr: [*]const u8,\n    term_name_len: usize,\n    term_version_ptr: [*]const u8,\n    term_version_len: usize,\n    term_from_xtversion: bool,\n};\n\nexport fn getTerminalCapabilities(rendererPtr: *renderer.CliRenderer, capsPtr: *ExternalCapabilities) void {\n    const caps = rendererPtr.getTerminalCapabilities();\n    const term = &rendererPtr.terminal;\n\n    capsPtr.* = .{\n        .kitty_keyboard = caps.kitty_keyboard,\n        .kitty_graphics = caps.kitty_graphics,\n        .rgb = caps.rgb,\n        .unicode = if (caps.unicode == .wcwidth) 0 else 1,\n        .sgr_pixels = caps.sgr_pixels,\n        .color_scheme_updates = caps.color_scheme_updates,\n        .explicit_width = caps.explicit_width,\n        .scaled_text = caps.scaled_text,\n        .sixel = caps.sixel,\n        .focus_tracking = caps.focus_tracking,\n        .sync = caps.sync,\n        .bracketed_paste = caps.bracketed_paste,\n        .hyperlinks = caps.hyperlinks,\n        .osc52 = caps.osc52,\n        .explicit_cursor_positioning = caps.explicit_cursor_positioning,\n        .term_name_ptr = &term.term_info.name,\n        .term_name_len = term.term_info.name_len,\n        .term_version_ptr = &term.term_info.version,\n        .term_version_len = term.term_info.version_len,\n        .term_from_xtversion = term.term_info.from_xtversion,\n    };\n}\n\nexport fn processCapabilityResponse(rendererPtr: *renderer.CliRenderer, responsePtr: [*]const u8, responseLen: usize) void {\n    const response = responsePtr[0..responseLen];\n    rendererPtr.processCapabilityResponse(response);\n}\n\nexport fn setCursorColor(rendererPtr: *renderer.CliRenderer, color: [*]const f32) void {\n    rendererPtr.terminal.setCursorColor(utils.f32PtrToRGBA(color));\n}\n\npub const CursorStyleOptions = extern struct {\n    style: u8,\n    blinking: u8,\n    color: ?[*]const f32,\n    cursor: u8,\n};\n\nexport fn setCursorStyleOptions(rendererPtr: *renderer.CliRenderer, options: *const CursorStyleOptions) void {\n    const current = rendererPtr.terminal.getCursorStyle();\n\n    const style = if (options.style <= 3) @as(terminal.CursorStyle, @enumFromInt(options.style)) else current.style;\n    const blinking = if (options.blinking <= 1) options.blinking == 1 else current.blinking;\n\n    if (options.style <= 3 or options.blinking <= 1) {\n        rendererPtr.terminal.setCursorStyle(style, blinking);\n    }\n    if (options.color) |rgba| {\n        rendererPtr.terminal.setCursorColor(utils.f32PtrToRGBA(rgba));\n    }\n    if (options.cursor <= 5) {\n        rendererPtr.terminal.setMousePointerStyle(@enumFromInt(options.cursor));\n    }\n}\n\npub const ExternalCursorState = extern struct {\n    x: u32,\n    y: u32,\n    visible: bool,\n    style: u8,\n    blinking: bool,\n    r: f32,\n    g: f32,\n    b: f32,\n    a: f32,\n};\n\nexport fn getCursorState(rendererPtr: *renderer.CliRenderer, outPtr: *ExternalCursorState) void {\n    const pos = rendererPtr.terminal.getCursorPosition();\n    const style = rendererPtr.terminal.getCursorStyle();\n    const color = rendererPtr.terminal.getCursorColor();\n\n    const styleTag: u8 = switch (style.style) {\n        .block => 0,\n        .line => 1,\n        .underline => 2,\n        .default => 3,\n    };\n\n    outPtr.* = .{\n        .x = pos.x,\n        .y = pos.y,\n        .visible = pos.visible,\n        .style = styleTag,\n        .blinking = style.blinking,\n        .r = color[0],\n        .g = color[1],\n        .b = color[2],\n        .a = color[3],\n    };\n}\n\nexport fn setDebugOverlay(rendererPtr: *renderer.CliRenderer, enabled: bool, corner: u8) void {\n    const cornerEnum: renderer.DebugOverlayCorner = switch (corner) {\n        0 => .topLeft,\n        1 => .topRight,\n        2 => .bottomLeft,\n        else => .bottomRight,\n    };\n\n    rendererPtr.setDebugOverlay(enabled, cornerEnum);\n}\n\nexport fn clearTerminal(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.clearTerminal();\n}\n\nexport fn setTerminalTitle(rendererPtr: *renderer.CliRenderer, titlePtr: [*]const u8, titleLen: usize) void {\n    const title = titlePtr[0..titleLen];\n    rendererPtr.setTerminalTitle(title);\n}\n\nexport fn copyToClipboardOSC52(rendererPtr: *renderer.CliRenderer, target: u8, payloadPtr: [*]const u8, payloadLen: usize) bool {\n    const targetEnum = std.meta.intToEnum(terminal.ClipboardTarget, target) catch .clipboard;\n    const payload = payloadPtr[0..payloadLen];\n    return rendererPtr.copyToClipboardOSC52(targetEnum, payload);\n}\n\nexport fn clearClipboardOSC52(rendererPtr: *renderer.CliRenderer, target: u8) bool {\n    const targetEnum = std.meta.intToEnum(terminal.ClipboardTarget, target) catch .clipboard;\n    return rendererPtr.clearClipboardOSC52(targetEnum);\n}\n\n// Buffer functions\nexport fn bufferClear(bufferPtr: *buffer.OptimizedBuffer, bg: [*]const f32) void {\n    bufferPtr.clear(utils.f32PtrToRGBA(bg), null) catch {};\n}\n\nexport fn bufferGetCharPtr(bufferPtr: *buffer.OptimizedBuffer) [*]u32 {\n    return bufferPtr.getCharPtr();\n}\n\nexport fn bufferGetFgPtr(bufferPtr: *buffer.OptimizedBuffer) [*]RGBA {\n    return bufferPtr.getFgPtr();\n}\n\nexport fn bufferGetBgPtr(bufferPtr: *buffer.OptimizedBuffer) [*]RGBA {\n    return bufferPtr.getBgPtr();\n}\n\nexport fn bufferGetAttributesPtr(bufferPtr: *buffer.OptimizedBuffer) [*]u32 {\n    return bufferPtr.getAttributesPtr();\n}\n\nexport fn bufferGetRespectAlpha(bufferPtr: *buffer.OptimizedBuffer) bool {\n    return bufferPtr.getRespectAlpha();\n}\n\nexport fn bufferSetRespectAlpha(bufferPtr: *buffer.OptimizedBuffer, respectAlpha: bool) void {\n    bufferPtr.setRespectAlpha(respectAlpha);\n}\n\nexport fn bufferGetId(bufferPtr: *buffer.OptimizedBuffer, outPtr: [*]u8, maxLen: usize) usize {\n    const id = bufferPtr.getId();\n    const copyLen = @min(id.len, maxLen);\n    @memcpy(outPtr[0..copyLen], id[0..copyLen]);\n    return copyLen;\n}\n\nexport fn bufferGetRealCharSize(bufferPtr: *buffer.OptimizedBuffer) u32 {\n    return bufferPtr.getRealCharSize();\n}\n\nexport fn bufferWriteResolvedChars(bufferPtr: *buffer.OptimizedBuffer, outputPtr: [*]u8, outputLen: usize, addLineBreaks: bool) u32 {\n    const output_slice = outputPtr[0..outputLen];\n    return bufferPtr.writeResolvedChars(output_slice, addLineBreaks) catch 0;\n}\n\nexport fn bufferDrawText(bufferPtr: *buffer.OptimizedBuffer, text: [*]const u8, textLen: usize, x: u32, y: u32, fg: [*]const f32, bg: ?[*]const f32, attributes: u32) void {\n    const rgbaFg = utils.f32PtrToRGBA(fg);\n    const rgbaBg = if (bg) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    bufferPtr.drawText(text[0..textLen], x, y, rgbaFg, rgbaBg, attributes) catch {};\n}\n\nexport fn bufferSetCellWithAlphaBlending(bufferPtr: *buffer.OptimizedBuffer, x: u32, y: u32, char: u32, fg: [*]const f32, bg: [*]const f32, attributes: u32) void {\n    const rgbaFg = utils.f32PtrToRGBA(fg);\n    const rgbaBg = utils.f32PtrToRGBA(bg);\n    bufferPtr.setCellWithAlphaBlending(x, y, char, rgbaFg, rgbaBg, attributes) catch {};\n}\n\nexport fn bufferSetCell(bufferPtr: *buffer.OptimizedBuffer, x: u32, y: u32, char: u32, fg: [*]const f32, bg: [*]const f32, attributes: u32) void {\n    const rgbaFg = utils.f32PtrToRGBA(fg);\n    const rgbaBg = utils.f32PtrToRGBA(bg);\n    const cell = buffer.Cell{\n        .char = char,\n        .fg = rgbaFg,\n        .bg = rgbaBg,\n        .attributes = attributes,\n    };\n    bufferPtr.set(x, y, cell);\n}\n\nexport fn bufferFillRect(bufferPtr: *buffer.OptimizedBuffer, x: u32, y: u32, width: u32, height: u32, bg: [*]const f32) void {\n    const rgbaBg = utils.f32PtrToRGBA(bg);\n    bufferPtr.fillRect(x, y, width, height, rgbaBg) catch {};\n}\n\nexport fn bufferColorMatrix(bufferPtr: *buffer.OptimizedBuffer, matrixPtr: [*]const f32, cellMaskPtr: [*]const f32, cellMaskCount: usize, strength: f32, target: u8) void {\n    if (cellMaskCount == 0) return;\n    const matrix = matrixPtr[0..16];\n    const len = cellMaskCount * 3;\n    const cellMask = cellMaskPtr[0..len];\n    const targetEnum: buffer_effects.ColorTarget = @enumFromInt(target);\n    buffer_effects.colorMatrix(bufferPtr, matrix, cellMask, strength, targetEnum);\n}\n\nexport fn bufferColorMatrixUniform(bufferPtr: *buffer.OptimizedBuffer, matrixPtr: [*]const f32, strength: f32, target: u8) void {\n    const matrix = matrixPtr[0..16];\n    const targetEnum: buffer_effects.ColorTarget = @enumFromInt(target);\n    buffer_effects.colorMatrixUniform(bufferPtr, matrix, strength, targetEnum);\n}\n\nexport fn bufferDrawPackedBuffer(bufferPtr: *buffer.OptimizedBuffer, data: [*]const u8, dataLen: usize, posX: u32, posY: u32, terminalWidthCells: u32, terminalHeightCells: u32) void {\n    bufferPtr.drawPackedBuffer(data, dataLen, posX, posY, terminalWidthCells, terminalHeightCells);\n}\n\nexport fn bufferDrawGrayscaleBuffer(bufferPtr: *buffer.OptimizedBuffer, posX: i32, posY: i32, intensities: [*]const f32, srcWidth: u32, srcHeight: u32, fg: ?[*]const f32, bg: ?[*]const f32) void {\n    const rgbaFg = if (fg) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    const rgbaBg = if (bg) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    bufferPtr.drawGrayscaleBuffer(posX, posY, intensities, srcWidth, srcHeight, rgbaFg, rgbaBg);\n}\n\nexport fn bufferDrawGrayscaleBufferSupersampled(bufferPtr: *buffer.OptimizedBuffer, posX: i32, posY: i32, intensities: [*]const f32, srcWidth: u32, srcHeight: u32, fg: ?[*]const f32, bg: ?[*]const f32) void {\n    const rgbaFg = if (fg) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    const rgbaBg = if (bg) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    bufferPtr.drawGrayscaleBufferSupersampled(posX, posY, intensities, srcWidth, srcHeight, rgbaFg, rgbaBg);\n}\n\nexport fn bufferPushScissorRect(bufferPtr: *buffer.OptimizedBuffer, x: i32, y: i32, width: u32, height: u32) void {\n    bufferPtr.pushScissorRect(x, y, width, height) catch {};\n}\n\nexport fn bufferPopScissorRect(bufferPtr: *buffer.OptimizedBuffer) void {\n    bufferPtr.popScissorRect();\n}\n\nexport fn bufferClearScissorRects(bufferPtr: *buffer.OptimizedBuffer) void {\n    bufferPtr.clearScissorRects();\n}\n\n// Opacity stack functions\nexport fn bufferPushOpacity(bufferPtr: *buffer.OptimizedBuffer, opacity: f32) void {\n    bufferPtr.pushOpacity(opacity) catch {};\n}\n\nexport fn bufferPopOpacity(bufferPtr: *buffer.OptimizedBuffer) void {\n    bufferPtr.popOpacity();\n}\n\nexport fn bufferGetCurrentOpacity(bufferPtr: *buffer.OptimizedBuffer) f32 {\n    return bufferPtr.getCurrentOpacity();\n}\n\nexport fn bufferClearOpacity(bufferPtr: *buffer.OptimizedBuffer) void {\n    bufferPtr.clearOpacity();\n}\n\nexport fn bufferDrawSuperSampleBuffer(bufferPtr: *buffer.OptimizedBuffer, x: u32, y: u32, pixelData: [*]const u8, len: usize, format: u8, alignedBytesPerRow: u32) void {\n    bufferPtr.drawSuperSampleBuffer(x, y, pixelData, len, format, alignedBytesPerRow) catch {};\n}\n\nexport fn linkAlloc(urlPtr: [*]const u8, urlLen: usize) u32 {\n    const url = urlPtr[0..urlLen];\n    const link_pool = link.initGlobalLinkPool(globalArena);\n    return link_pool.alloc(url) catch 0;\n}\n\nexport fn linkGetUrl(id: u32, outPtr: [*]u8, maxLen: usize) usize {\n    const link_pool = link.initGlobalLinkPool(globalArena);\n    const url_bytes = link_pool.get(id) catch return 0;\n    const copyLen = @min(url_bytes.len, maxLen);\n    @memcpy(outPtr[0..copyLen], url_bytes[0..copyLen]);\n    return copyLen;\n}\n\nexport fn attributesWithLink(baseAttributes: u32, linkId: u32) u32 {\n    return ansi.TextAttributes.setLinkId(baseAttributes, linkId);\n}\n\nexport fn attributesGetLinkId(attributes: u32) u32 {\n    return ansi.TextAttributes.getLinkId(attributes);\n}\n\npub const ExternalGridDrawOptions = extern struct {\n    draw_inner: bool,\n    draw_outer: bool,\n};\n\nexport fn bufferDrawGrid(\n    bufferPtr: *buffer.OptimizedBuffer,\n    borderChars: [*]const u32,\n    borderFg: [*]const f32,\n    borderBg: [*]const f32,\n    columnOffsets: [*]const i32,\n    columnCount: u32,\n    rowOffsets: [*]const i32,\n    rowCount: u32,\n    options: *const ExternalGridDrawOptions,\n) void {\n    bufferPtr.drawGrid(\n        borderChars,\n        utils.f32PtrToRGBA(borderFg),\n        utils.f32PtrToRGBA(borderBg),\n        columnOffsets,\n        columnCount,\n        rowOffsets,\n        rowCount,\n        options.draw_inner,\n        options.draw_outer,\n    );\n}\n\nexport fn bufferDrawBox(\n    bufferPtr: *buffer.OptimizedBuffer,\n    x: i32,\n    y: i32,\n    width: u32,\n    height: u32,\n    borderChars: [*]const u32,\n    packedOptions: u32,\n    borderColor: [*]const f32,\n    backgroundColor: [*]const f32,\n    title: ?[*]const u8,\n    titleLen: u32,\n) void {\n    const borderSides = buffer.BorderSides{\n        .top = (packedOptions & 0b1000) != 0,\n        .right = (packedOptions & 0b0100) != 0,\n        .bottom = (packedOptions & 0b0010) != 0,\n        .left = (packedOptions & 0b0001) != 0,\n    };\n\n    const shouldFill = ((packedOptions >> 4) & 1) != 0;\n    const titleAlignment = @as(u8, @intCast((packedOptions >> 5) & 0b11));\n\n    const titleSlice = if (title) |t| t[0..titleLen] else null;\n\n    bufferPtr.drawBox(\n        x,\n        y,\n        width,\n        height,\n        borderChars,\n        borderSides,\n        utils.f32PtrToRGBA(borderColor),\n        utils.f32PtrToRGBA(backgroundColor),\n        shouldFill,\n        titleSlice,\n        titleAlignment,\n    ) catch {};\n}\n\nexport fn bufferResize(bufferPtr: *buffer.OptimizedBuffer, width: u32, height: u32) void {\n    bufferPtr.resize(width, height) catch {};\n}\n\nexport fn resizeRenderer(rendererPtr: *renderer.CliRenderer, width: u32, height: u32) void {\n    rendererPtr.resize(width, height) catch {};\n}\n\nexport fn addToHitGrid(rendererPtr: *renderer.CliRenderer, x: i32, y: i32, width: u32, height: u32, id: u32) void {\n    rendererPtr.addToHitGrid(x, y, width, height, id);\n}\n\nexport fn clearCurrentHitGrid(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.clearCurrentHitGrid();\n}\n\nexport fn hitGridPushScissorRect(rendererPtr: *renderer.CliRenderer, x: i32, y: i32, width: u32, height: u32) void {\n    rendererPtr.hitGridPushScissorRect(x, y, width, height);\n}\n\nexport fn hitGridPopScissorRect(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.hitGridPopScissorRect();\n}\n\nexport fn hitGridClearScissorRects(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.hitGridClearScissorRects();\n}\n\nexport fn addToCurrentHitGridClipped(rendererPtr: *renderer.CliRenderer, x: i32, y: i32, width: u32, height: u32, id: u32) void {\n    rendererPtr.addToCurrentHitGridClipped(x, y, width, height, id);\n}\n\nexport fn checkHit(rendererPtr: *renderer.CliRenderer, x: u32, y: u32) u32 {\n    return rendererPtr.checkHit(x, y);\n}\n\nexport fn getHitGridDirty(rendererPtr: *renderer.CliRenderer) bool {\n    return rendererPtr.getHitGridDirty();\n}\n\nexport fn dumpHitGrid(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.dumpHitGrid();\n}\n\nexport fn dumpBuffers(rendererPtr: *renderer.CliRenderer, timestamp: i64) void {\n    rendererPtr.dumpBuffers(timestamp);\n}\n\nexport fn dumpStdoutBuffer(rendererPtr: *renderer.CliRenderer, timestamp: i64) void {\n    rendererPtr.dumpStdoutBuffer(timestamp);\n}\n\nexport fn restoreTerminalModes(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.restoreTerminalModes();\n}\n\nexport fn enableMouse(rendererPtr: *renderer.CliRenderer, enableMovement: bool) void {\n    rendererPtr.enableMouse(enableMovement);\n}\n\nexport fn disableMouse(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.disableMouse();\n}\n\nexport fn queryPixelResolution(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.queryPixelResolution();\n}\n\nexport fn enableKittyKeyboard(rendererPtr: *renderer.CliRenderer, flags: u8) void {\n    rendererPtr.enableKittyKeyboard(flags);\n}\n\nexport fn disableKittyKeyboard(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.disableKittyKeyboard();\n}\n\nexport fn setKittyKeyboardFlags(rendererPtr: *renderer.CliRenderer, flags: u8) void {\n    rendererPtr.setKittyKeyboardFlags(flags);\n}\n\nexport fn getKittyKeyboardFlags(rendererPtr: *renderer.CliRenderer) u8 {\n    return rendererPtr.getKittyKeyboardFlags();\n}\n\nexport fn setupTerminal(rendererPtr: *renderer.CliRenderer, useAlternateScreen: bool) void {\n    rendererPtr.setupTerminal(useAlternateScreen);\n}\n\nexport fn suspendRenderer(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.suspendRenderer();\n}\n\nexport fn resumeRenderer(rendererPtr: *renderer.CliRenderer) void {\n    rendererPtr.resumeRenderer();\n}\n\nexport fn writeOut(rendererPtr: *renderer.CliRenderer, dataPtr: [*]const u8, dataLen: usize) void {\n    if (dataLen == 0) return;\n    const data = dataPtr[0..dataLen];\n    rendererPtr.writeOut(data);\n}\n\nexport fn createTextBuffer(widthMethod: u8) ?*text_buffer.UnifiedTextBuffer {\n    const pool = gp.initGlobalPool(globalArena);\n    const link_pool = link.initGlobalLinkPool(globalArena);\n    const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else .unicode;\n\n    return text_buffer.UnifiedTextBuffer.init(globalAllocator, pool, link_pool, wMethod) catch {\n        return null;\n    };\n}\n\nexport fn destroyTextBuffer(tb: *text_buffer.UnifiedTextBuffer) void {\n    tb.deinit();\n}\n\nexport fn textBufferGetLength(tb: *text_buffer.UnifiedTextBuffer) u32 {\n    return tb.getLength();\n}\n\nexport fn textBufferGetByteSize(tb: *text_buffer.UnifiedTextBuffer) u32 {\n    return tb.getByteSize();\n}\n\nexport fn textBufferReset(tb: *text_buffer.UnifiedTextBuffer) void {\n    tb.reset();\n}\n\nexport fn textBufferClear(tb: *text_buffer.UnifiedTextBuffer) void {\n    tb.clear();\n}\n\nexport fn textBufferSetDefaultFg(tb: *text_buffer.UnifiedTextBuffer, fg: ?[*]const f32) void {\n    const fgColor = if (fg) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    tb.setDefaultFg(fgColor);\n}\n\nexport fn textBufferSetDefaultBg(tb: *text_buffer.UnifiedTextBuffer, bg: ?[*]const f32) void {\n    const bgColor = if (bg) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    tb.setDefaultBg(bgColor);\n}\n\nexport fn textBufferSetDefaultAttributes(tb: *text_buffer.UnifiedTextBuffer, attr: ?[*]const u32) void {\n    const attributes = if (attr) |a| a[0] else null;\n    tb.setDefaultAttributes(attributes);\n}\n\nexport fn textBufferResetDefaults(tb: *text_buffer.UnifiedTextBuffer) void {\n    tb.resetDefaults();\n}\n\nexport fn textBufferGetTabWidth(tb: *text_buffer.UnifiedTextBuffer) u8 {\n    return tb.tabWidth();\n}\n\nexport fn textBufferSetTabWidth(tb: *text_buffer.UnifiedTextBuffer, width: u8) void {\n    tb.setTabWidth(width);\n}\n\nexport fn textBufferRegisterMemBuffer(tb: *text_buffer.UnifiedTextBuffer, dataPtr: [*]const u8, dataLen: usize, owned: bool) u16 {\n    const data = dataPtr[0..dataLen];\n    const mem_id = tb.registerMemBuffer(data, owned) catch return 0xFFFF;\n    return @intCast(mem_id);\n}\n\nexport fn textBufferReplaceMemBuffer(tb: *text_buffer.UnifiedTextBuffer, id: u8, dataPtr: [*]const u8, dataLen: usize, owned: bool) bool {\n    const data = dataPtr[0..dataLen];\n    tb.replaceMemBuffer(id, data, owned) catch return false;\n    return true;\n}\n\nexport fn textBufferClearMemRegistry(tb: *text_buffer.UnifiedTextBuffer) void {\n    tb.clearMemRegistry();\n}\n\nexport fn textBufferSetTextFromMem(tb: *text_buffer.UnifiedTextBuffer, id: u8) void {\n    tb.setTextFromMemId(id) catch {};\n}\n\nexport fn textBufferAppend(tb: *text_buffer.UnifiedTextBuffer, dataPtr: [*]const u8, dataLen: usize) void {\n    const data = dataPtr[0..dataLen];\n    tb.append(data) catch {};\n}\n\nexport fn textBufferAppendFromMemId(tb: *text_buffer.UnifiedTextBuffer, id: u8) void {\n    tb.appendFromMemId(id) catch {};\n}\n\nexport fn textBufferLoadFile(tb: *text_buffer.UnifiedTextBuffer, pathPtr: [*]const u8, pathLen: usize) bool {\n    const path = pathPtr[0..pathLen];\n    tb.loadFile(path) catch return false;\n    return true;\n}\n\nexport fn textBufferSetStyledText(\n    tb: *text_buffer.UnifiedTextBuffer,\n    chunksPtr: [*]const text_buffer.StyledChunk,\n    chunkCount: usize,\n) void {\n    if (chunkCount == 0) return;\n    const chunks = chunksPtr[0..chunkCount];\n    tb.setStyledText(chunks) catch {};\n}\n\nexport fn textBufferGetLineCount(tb: *text_buffer.UnifiedTextBuffer) u32 {\n    return tb.getLineCount();\n}\n\nexport fn textBufferGetPlainText(tb: *text_buffer.UnifiedTextBuffer, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return tb.getPlainTextIntoBuffer(outBuffer);\n}\n\n// TextBufferView functions (Array-based for backward compatibility)\nexport fn createTextBufferView(tb: *text_buffer.UnifiedTextBuffer) ?*text_buffer_view.UnifiedTextBufferView {\n    return text_buffer_view.UnifiedTextBufferView.init(globalAllocator, tb) catch {\n        return null;\n    };\n}\n\nexport fn destroyTextBufferView(view: *text_buffer_view.UnifiedTextBufferView) void {\n    view.deinit();\n}\n\nexport fn textBufferViewSetSelection(view: *text_buffer_view.UnifiedTextBufferView, start: u32, end: u32, bgColor: ?[*]const f32, fgColor: ?[*]const f32) void {\n    const bg = if (bgColor) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    const fg = if (fgColor) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    view.setSelection(start, end, bg, fg);\n}\n\nexport fn textBufferViewResetSelection(view: *text_buffer_view.UnifiedTextBufferView) void {\n    view.resetSelection();\n}\n\nexport fn textBufferViewGetSelectionInfo(view: *text_buffer_view.UnifiedTextBufferView) u64 {\n    return view.packSelectionInfo();\n}\n\nexport fn textBufferViewSetLocalSelection(view: *text_buffer_view.UnifiedTextBufferView, anchorX: i32, anchorY: i32, focusX: i32, focusY: i32, bgColor: ?[*]const f32, fgColor: ?[*]const f32) bool {\n    const bg = if (bgColor) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    const fg = if (fgColor) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    return view.setLocalSelection(anchorX, anchorY, focusX, focusY, bg, fg);\n}\n\nexport fn textBufferViewUpdateSelection(view: *text_buffer_view.UnifiedTextBufferView, end: u32, bgColor: ?[*]const f32, fgColor: ?[*]const f32) void {\n    const bg = if (bgColor) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    const fg = if (fgColor) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    view.updateSelection(end, bg, fg);\n}\n\nexport fn textBufferViewUpdateLocalSelection(view: *text_buffer_view.UnifiedTextBufferView, anchorX: i32, anchorY: i32, focusX: i32, focusY: i32, bgColor: ?[*]const f32, fgColor: ?[*]const f32) bool {\n    const bg = if (bgColor) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    const fg = if (fgColor) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    return view.updateLocalSelection(anchorX, anchorY, focusX, focusY, bg, fg);\n}\n\nexport fn textBufferViewResetLocalSelection(view: *text_buffer_view.UnifiedTextBufferView) void {\n    view.resetLocalSelection();\n}\n\nexport fn textBufferViewSetWrapWidth(view: *text_buffer_view.UnifiedTextBufferView, width: u32) void {\n    view.setWrapWidth(if (width == 0) null else width);\n}\n\nexport fn textBufferViewSetWrapMode(view: *text_buffer_view.UnifiedTextBufferView, mode: u8) void {\n    const wrapMode: text_buffer.WrapMode = switch (mode) {\n        0 => .none,\n        1 => .char,\n        2 => .word,\n        else => .none,\n    };\n    view.setWrapMode(wrapMode);\n}\n\nexport fn textBufferViewSetViewportSize(view: *text_buffer_view.UnifiedTextBufferView, width: u32, height: u32) void {\n    view.setViewportSize(width, height);\n}\n\nexport fn textBufferViewSetViewport(view: *text_buffer_view.UnifiedTextBufferView, x: u32, y: u32, width: u32, height: u32) void {\n    view.setViewport(text_buffer_view.Viewport{\n        .x = x,\n        .y = y,\n        .width = width,\n        .height = height,\n    });\n}\n\nexport fn textBufferViewGetVirtualLineCount(view: *text_buffer_view.UnifiedTextBufferView) u32 {\n    return view.getVirtualLineCount();\n}\n\nexport fn textBufferViewGetLineInfoDirect(view: *text_buffer_view.UnifiedTextBufferView, outPtr: *ExternalLineInfo) void {\n    const line_info = view.getCachedLineInfo();\n\n    outPtr.* = .{\n        .start_cols_ptr = line_info.line_start_cols.ptr,\n        .start_cols_len = @intCast(line_info.line_start_cols.len),\n        .width_cols_ptr = line_info.line_width_cols.ptr,\n        .width_cols_len = @intCast(line_info.line_width_cols.len),\n        .sources_ptr = line_info.line_sources.ptr,\n        .sources_len = @intCast(line_info.line_sources.len),\n        .wraps_ptr = line_info.line_wraps.ptr,\n        .wraps_len = @intCast(line_info.line_wraps.len),\n        .width_cols_max = line_info.line_width_cols_max,\n    };\n}\n\nexport fn textBufferViewGetLogicalLineInfoDirect(view: *text_buffer_view.UnifiedTextBufferView, outPtr: *ExternalLineInfo) void {\n    const line_info = view.getLogicalLineInfo();\n\n    outPtr.* = .{\n        .start_cols_ptr = line_info.line_start_cols.ptr,\n        .start_cols_len = @intCast(line_info.line_start_cols.len),\n        .width_cols_ptr = line_info.line_width_cols.ptr,\n        .width_cols_len = @intCast(line_info.line_width_cols.len),\n        .sources_ptr = line_info.line_sources.ptr,\n        .sources_len = @intCast(line_info.line_sources.len),\n        .wraps_ptr = line_info.line_wraps.ptr,\n        .wraps_len = @intCast(line_info.line_wraps.len),\n        .width_cols_max = line_info.line_width_cols_max,\n    };\n}\n\nexport fn textBufferViewGetSelectedText(view: *text_buffer_view.UnifiedTextBufferView, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return view.getSelectedTextIntoBuffer(outBuffer);\n}\n\nexport fn textBufferViewGetPlainText(view: *text_buffer_view.UnifiedTextBufferView, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return view.getPlainTextIntoBuffer(outBuffer);\n}\n\nexport fn textBufferViewSetTabIndicator(view: *text_buffer_view.UnifiedTextBufferView, indicator: u32) void {\n    view.setTabIndicator(indicator);\n}\n\nexport fn textBufferViewSetTabIndicatorColor(view: *text_buffer_view.UnifiedTextBufferView, color: [*]const f32) void {\n    view.setTabIndicatorColor(utils.f32PtrToRGBA(color));\n}\n\nexport fn textBufferViewSetTruncate(view: *text_buffer_view.UnifiedTextBufferView, truncate: bool) void {\n    view.setTruncate(truncate);\n}\n\npub const ExternalMeasureResult = extern struct {\n    line_count: u32,\n    width_cols_max: u32,\n};\n\nexport fn textBufferViewMeasureForDimensions(view: *text_buffer_view.UnifiedTextBufferView, width: u32, height: u32, outPtr: *ExternalMeasureResult) bool {\n    const result = view.measureForDimensions(width, height) catch return false;\n    outPtr.* = .{\n        .line_count = result.line_count,\n        .width_cols_max = result.width_cols_max,\n    };\n    return true;\n}\n\n// ===== EditBuffer Exports =====\n\nexport fn createEditBuffer(widthMethod: u8) ?*edit_buffer_mod.EditBuffer {\n    const pool = gp.initGlobalPool(globalArena);\n    const link_pool = link.initGlobalLinkPool(globalArena);\n    const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else .unicode;\n\n    return edit_buffer_mod.EditBuffer.init(\n        globalAllocator,\n        pool,\n        link_pool,\n        wMethod,\n    ) catch null;\n}\n\nexport fn destroyEditBuffer(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.deinit();\n}\n\nexport fn editBufferGetTextBuffer(edit_buffer: *edit_buffer_mod.EditBuffer) *text_buffer.UnifiedTextBuffer {\n    return edit_buffer.getTextBuffer();\n}\n\nexport fn editBufferInsertText(edit_buffer: *edit_buffer_mod.EditBuffer, textPtr: [*]const u8, textLen: usize) void {\n    const text = textPtr[0..textLen];\n    edit_buffer.insertText(text) catch {};\n}\n\nexport fn editBufferDeleteRange(edit_buffer: *edit_buffer_mod.EditBuffer, start_row: u32, start_col: u32, end_row: u32, end_col: u32) void {\n    const start = edit_buffer_mod.Cursor{ .row = start_row, .col = start_col };\n    const end = edit_buffer_mod.Cursor{ .row = end_row, .col = end_col };\n    edit_buffer.deleteRange(start, end) catch {};\n}\n\nexport fn editBufferDeleteCharBackward(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.backspace() catch {};\n}\n\nexport fn editBufferDeleteChar(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.deleteForward() catch {};\n}\n\nexport fn editBufferMoveCursorLeft(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.moveLeft();\n}\n\nexport fn editBufferMoveCursorRight(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.moveRight();\n}\n\nexport fn editBufferMoveCursorUp(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.moveUp();\n}\n\nexport fn editBufferMoveCursorDown(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.moveDown();\n}\n\nexport fn editBufferGetCursor(edit_buffer: *edit_buffer_mod.EditBuffer, outRow: *u32, outCol: *u32) void {\n    const cursor = edit_buffer.getPrimaryCursor();\n    outRow.* = cursor.row;\n    outCol.* = cursor.col;\n}\n\nexport fn editBufferSetCursor(edit_buffer: *edit_buffer_mod.EditBuffer, row: u32, col: u32) void {\n    edit_buffer.setCursor(row, col) catch {};\n}\n\nexport fn editBufferSetCursorToLineCol(edit_buffer: *edit_buffer_mod.EditBuffer, row: u32, col: u32) void {\n    edit_buffer.setCursor(row, col) catch {};\n}\n\nexport fn editBufferSetCursorByOffset(edit_buffer: *edit_buffer_mod.EditBuffer, offset: u32) void {\n    edit_buffer.setCursorByOffset(offset) catch {};\n}\n\nexport fn editBufferGetNextWordBoundary(edit_buffer: *edit_buffer_mod.EditBuffer, outPtr: *ExternalLogicalCursor) void {\n    const cursor = edit_buffer.getNextWordBoundary();\n    outPtr.* = .{\n        .row = cursor.row,\n        .col = cursor.col,\n        .offset = cursor.offset,\n    };\n}\n\nexport fn editBufferGetPrevWordBoundary(edit_buffer: *edit_buffer_mod.EditBuffer, outPtr: *ExternalLogicalCursor) void {\n    const cursor = edit_buffer.getPrevWordBoundary();\n    outPtr.* = .{\n        .row = cursor.row,\n        .col = cursor.col,\n        .offset = cursor.offset,\n    };\n}\n\nexport fn editBufferGetEOL(edit_buffer: *edit_buffer_mod.EditBuffer, outPtr: *ExternalLogicalCursor) void {\n    const cursor = edit_buffer.getEOL();\n    outPtr.* = .{\n        .row = cursor.row,\n        .col = cursor.col,\n        .offset = cursor.offset,\n    };\n}\n\nexport fn editBufferOffsetToPosition(edit_buffer: *edit_buffer_mod.EditBuffer, offset: u32, outPtr: *ExternalLogicalCursor) bool {\n    const iter_mod = @import(\"text-buffer-iterators.zig\");\n    const coords = iter_mod.offsetToCoords(edit_buffer.tb.rope(), offset) orelse return false;\n    outPtr.* = .{\n        .row = coords.row,\n        .col = coords.col,\n        .offset = offset,\n    };\n    return true;\n}\n\nexport fn editBufferPositionToOffset(edit_buffer: *edit_buffer_mod.EditBuffer, row: u32, col: u32) u32 {\n    const iter_mod = @import(\"text-buffer-iterators.zig\");\n    return iter_mod.coordsToOffset(edit_buffer.tb.rope(), row, col) orelse 0;\n}\n\nexport fn editBufferGetLineStartOffset(edit_buffer: *edit_buffer_mod.EditBuffer, row: u32) u32 {\n    const iter_mod = @import(\"text-buffer-iterators.zig\");\n    return iter_mod.coordsToOffset(edit_buffer.tb.rope(), row, 0) orelse 0;\n}\n\nexport fn editBufferGetTextRange(edit_buffer: *edit_buffer_mod.EditBuffer, start_offset: u32, end_offset: u32, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return edit_buffer.getTextRange(start_offset, end_offset, outBuffer) catch 0;\n}\n\nexport fn editBufferGetTextRangeByCoords(edit_buffer: *edit_buffer_mod.EditBuffer, start_row: u32, start_col: u32, end_row: u32, end_col: u32, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return edit_buffer.getTextRangeByCoords(start_row, start_col, end_row, end_col, outBuffer);\n}\n\nexport fn editBufferSetText(edit_buffer: *edit_buffer_mod.EditBuffer, textPtr: [*]const u8, textLen: usize) void {\n    const text = textPtr[0..textLen];\n    edit_buffer.setText(text) catch {};\n}\n\nexport fn editBufferSetTextFromMem(edit_buffer: *edit_buffer_mod.EditBuffer, mem_id: u8) void {\n    edit_buffer.setTextFromMemId(mem_id) catch {};\n}\n\nexport fn editBufferReplaceText(edit_buffer: *edit_buffer_mod.EditBuffer, textPtr: [*]const u8, textLen: usize) void {\n    const text = textPtr[0..textLen];\n    edit_buffer.replaceText(text) catch {};\n}\n\nexport fn editBufferReplaceTextFromMem(edit_buffer: *edit_buffer_mod.EditBuffer, mem_id: u8) void {\n    edit_buffer.replaceTextFromMemId(mem_id) catch {};\n}\n\nexport fn editBufferGetText(edit_buffer: *edit_buffer_mod.EditBuffer, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return edit_buffer.getText(outBuffer);\n}\n\nexport fn editBufferInsertChar(edit_buffer: *edit_buffer_mod.EditBuffer, charPtr: [*]const u8, charLen: usize) void {\n    const text = charPtr[0..charLen];\n    edit_buffer.insertText(text) catch {};\n}\n\nexport fn editBufferNewLine(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.insertText(\"\\n\") catch {};\n}\n\nexport fn editBufferDeleteLine(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.deleteLine() catch {};\n}\n\nexport fn editBufferGotoLine(edit_buffer: *edit_buffer_mod.EditBuffer, line: u32) void {\n    edit_buffer.gotoLine(line) catch {};\n}\n\nexport fn editBufferGetCursorPosition(edit_buffer: *edit_buffer_mod.EditBuffer, outPtr: *ExternalLogicalCursor) void {\n    const pos = edit_buffer.getCursorPosition();\n    outPtr.* = .{\n        .row = pos.line,\n        .col = pos.visual_col,\n        .offset = pos.offset,\n    };\n}\n\nexport fn editBufferGetId(edit_buffer: *edit_buffer_mod.EditBuffer) u16 {\n    return edit_buffer.getId();\n}\n\nexport fn editBufferDebugLogRope(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.debugLogRope();\n}\n\nexport fn editBufferUndo(edit_buffer: *edit_buffer_mod.EditBuffer, outPtr: [*]u8, maxLen: usize) usize {\n    const prev_meta = edit_buffer.undo() catch return 0;\n    const copyLen = @min(prev_meta.len, maxLen);\n    @memcpy(outPtr[0..copyLen], prev_meta[0..copyLen]);\n    return copyLen;\n}\n\nexport fn editBufferRedo(edit_buffer: *edit_buffer_mod.EditBuffer, outPtr: [*]u8, maxLen: usize) usize {\n    const next_meta = edit_buffer.redo() catch return 0;\n    const copyLen = @min(next_meta.len, maxLen);\n    @memcpy(outPtr[0..copyLen], next_meta[0..copyLen]);\n    return copyLen;\n}\n\nexport fn editBufferCanUndo(edit_buffer: *edit_buffer_mod.EditBuffer) bool {\n    return edit_buffer.canUndo();\n}\n\nexport fn editBufferCanRedo(edit_buffer: *edit_buffer_mod.EditBuffer) bool {\n    return edit_buffer.canRedo();\n}\n\nexport fn editBufferClearHistory(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.clearHistory();\n}\n\nexport fn editBufferClear(edit_buffer: *edit_buffer_mod.EditBuffer) void {\n    edit_buffer.clear() catch {};\n}\n\n// ===== EditorView Exports =====\n\nexport fn createEditorView(edit_buffer: *edit_buffer_mod.EditBuffer, viewport_width: u32, viewport_height: u32) ?*editor_view.EditorView {\n    return editor_view.EditorView.init(globalArena, edit_buffer, viewport_width, viewport_height) catch null;\n}\n\nexport fn destroyEditorView(view: *editor_view.EditorView) void {\n    view.deinit();\n}\n\nexport fn editorViewSetViewport(view: *editor_view.EditorView, x: u32, y: u32, width: u32, height: u32, moveCursor: bool) void {\n    view.setViewport(text_buffer_view.Viewport{ .x = x, .y = y, .width = width, .height = height }, moveCursor);\n}\n\nexport fn editorViewClearViewport(view: *editor_view.EditorView) void {\n    view.setViewport(null, false);\n}\n\nexport fn editorViewGetViewport(view: *editor_view.EditorView, outX: *u32, outY: *u32, outWidth: *u32, outHeight: *u32) bool {\n    if (view.getViewport()) |vp| {\n        outX.* = vp.x;\n        outY.* = vp.y;\n        outWidth.* = vp.width;\n        outHeight.* = vp.height;\n        return true;\n    }\n    return false;\n}\n\nexport fn editorViewSetScrollMargin(view: *editor_view.EditorView, margin: f32) void {\n    view.setScrollMargin(margin);\n}\n\nexport fn editorViewGetVirtualLineCount(view: *editor_view.EditorView) u32 {\n    // TODO: There is a getter for that directly, no?\n    return @intCast(view.getVirtualLines().len);\n}\n\nexport fn editorViewGetTotalVirtualLineCount(view: *editor_view.EditorView) u32 {\n    return view.getTotalVirtualLineCount();\n}\n\nexport fn editorViewGetLineInfoDirect(view: *editor_view.EditorView, outPtr: *ExternalLineInfo) void {\n    const line_info = view.getCachedLineInfo();\n    outPtr.* = .{\n        .start_cols_ptr = line_info.line_start_cols.ptr,\n        .start_cols_len = @intCast(line_info.line_start_cols.len),\n        .width_cols_ptr = line_info.line_width_cols.ptr,\n        .width_cols_len = @intCast(line_info.line_width_cols.len),\n        .sources_ptr = line_info.line_sources.ptr,\n        .sources_len = @intCast(line_info.line_sources.len),\n        .wraps_ptr = line_info.line_wraps.ptr,\n        .wraps_len = @intCast(line_info.line_wraps.len),\n        .width_cols_max = line_info.line_width_cols_max,\n    };\n}\n\nexport fn editorViewGetTextBufferView(view: *editor_view.EditorView) *text_buffer_view.UnifiedTextBufferView {\n    return view.getTextBufferView();\n}\n\nexport fn editorViewGetLogicalLineInfoDirect(view: *editor_view.EditorView, outPtr: *ExternalLineInfo) void {\n    const line_info = view.getLogicalLineInfo();\n    outPtr.* = .{\n        .start_cols_ptr = line_info.line_start_cols.ptr,\n        .start_cols_len = @intCast(line_info.line_start_cols.len),\n        .width_cols_ptr = line_info.line_width_cols.ptr,\n        .width_cols_len = @intCast(line_info.line_width_cols.len),\n        .sources_ptr = line_info.line_sources.ptr,\n        .sources_len = @intCast(line_info.line_sources.len),\n        .wraps_ptr = line_info.line_wraps.ptr,\n        .wraps_len = @intCast(line_info.line_wraps.len),\n        .width_cols_max = line_info.line_width_cols_max,\n    };\n}\n\nexport fn editorViewSetViewportSize(view: *editor_view.EditorView, width: u32, height: u32) void {\n    view.setViewportSize(width, height);\n}\n\nexport fn editorViewSetWrapMode(view: *editor_view.EditorView, mode: u8) void {\n    const wrapMode: text_buffer.WrapMode = switch (mode) {\n        0 => .none,\n        1 => .char,\n        2 => .word,\n        else => .none,\n    };\n    view.setWrapMode(wrapMode);\n}\n\n// EditorView selection methods - delegate to TextBufferView\nexport fn editorViewSetSelection(view: *editor_view.EditorView, start: u32, end: u32, bgColor: ?[*]const f32, fgColor: ?[*]const f32) void {\n    const bg = if (bgColor) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    const fg = if (fgColor) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    view.text_buffer_view.setSelection(start, end, bg, fg);\n}\n\nexport fn editorViewResetSelection(view: *editor_view.EditorView) void {\n    view.text_buffer_view.resetSelection();\n}\n\nexport fn editorViewGetSelection(view: *editor_view.EditorView) u64 {\n    return view.text_buffer_view.packSelectionInfo();\n}\n\nexport fn editorViewSetLocalSelection(view: *editor_view.EditorView, anchorX: i32, anchorY: i32, focusX: i32, focusY: i32, bgColor: ?[*]const f32, fgColor: ?[*]const f32, updateCursor: bool, followCursor: bool) bool {\n    const bg = if (bgColor) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    const fg = if (fgColor) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    view.setSelectionFollowCursor(followCursor);\n    return view.setLocalSelection(anchorX, anchorY, focusX, focusY, bg, fg, updateCursor);\n}\n\nexport fn editorViewUpdateSelection(view: *editor_view.EditorView, end: u32, bgColor: ?[*]const f32, fgColor: ?[*]const f32) void {\n    const bg = if (bgColor) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    const fg = if (fgColor) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    view.updateSelection(end, bg, fg);\n}\n\nexport fn editorViewUpdateLocalSelection(view: *editor_view.EditorView, anchorX: i32, anchorY: i32, focusX: i32, focusY: i32, bgColor: ?[*]const f32, fgColor: ?[*]const f32, updateCursor: bool, followCursor: bool) bool {\n    const bg = if (bgColor) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    const fg = if (fgColor) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    view.setSelectionFollowCursor(followCursor);\n    return view.updateLocalSelection(anchorX, anchorY, focusX, focusY, bg, fg, updateCursor);\n}\n\nexport fn editorViewResetLocalSelection(view: *editor_view.EditorView) void {\n    view.setSelectionFollowCursor(false);\n    view.text_buffer_view.resetLocalSelection();\n}\n\nexport fn editorViewGetSelectedTextBytes(view: *editor_view.EditorView, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return view.text_buffer_view.getSelectedTextIntoBuffer(outBuffer);\n}\n\n// EditorView cursor and text methods\nexport fn editorViewGetCursor(view: *editor_view.EditorView, outRow: *u32, outCol: *u32) void {\n    const cursor = view.getPrimaryCursor();\n    outRow.* = cursor.row;\n    outCol.* = cursor.col;\n}\n\nexport fn editorViewGetText(view: *editor_view.EditorView, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return view.getText(outBuffer);\n}\n\n// ===== EditorView VisualCursor Exports =====\n\nexport fn editorViewGetVisualCursor(view: *editor_view.EditorView, outPtr: *ExternalVisualCursor) void {\n    const vcursor = view.getVisualCursor();\n    outPtr.* = .{\n        .visual_row = vcursor.visual_row,\n        .visual_col = vcursor.visual_col,\n        .logical_row = vcursor.logical_row,\n        .logical_col = vcursor.logical_col,\n        .offset = vcursor.offset,\n    };\n}\n\nexport fn editorViewMoveUpVisual(view: *editor_view.EditorView) void {\n    view.moveUpVisual();\n}\n\nexport fn editorViewMoveDownVisual(view: *editor_view.EditorView) void {\n    view.moveDownVisual();\n}\n\nexport fn editorViewDeleteSelectedText(view: *editor_view.EditorView) void {\n    view.deleteSelectedText() catch {};\n}\n\nexport fn editorViewSetCursorByOffset(view: *editor_view.EditorView, offset: u32) void {\n    view.setCursorByOffset(offset) catch {};\n}\n\nexport fn editorViewGetNextWordBoundary(view: *editor_view.EditorView, outPtr: *ExternalVisualCursor) void {\n    const vcursor = view.getNextWordBoundary();\n    outPtr.* = .{\n        .visual_row = vcursor.visual_row,\n        .visual_col = vcursor.visual_col,\n        .logical_row = vcursor.logical_row,\n        .logical_col = vcursor.logical_col,\n        .offset = vcursor.offset,\n    };\n}\n\nexport fn editorViewGetPrevWordBoundary(view: *editor_view.EditorView, outPtr: *ExternalVisualCursor) void {\n    const vcursor = view.getPrevWordBoundary();\n    outPtr.* = .{\n        .visual_row = vcursor.visual_row,\n        .visual_col = vcursor.visual_col,\n        .logical_row = vcursor.logical_row,\n        .logical_col = vcursor.logical_col,\n        .offset = vcursor.offset,\n    };\n}\n\nexport fn editorViewGetEOL(view: *editor_view.EditorView, outPtr: *ExternalVisualCursor) void {\n    const vcursor = view.getEOL();\n    outPtr.* = .{\n        .visual_row = vcursor.visual_row,\n        .visual_col = vcursor.visual_col,\n        .logical_row = vcursor.logical_row,\n        .logical_col = vcursor.logical_col,\n        .offset = vcursor.offset,\n    };\n}\n\nexport fn editorViewGetVisualSOL(view: *editor_view.EditorView, outPtr: *ExternalVisualCursor) void {\n    const vcursor = view.getVisualSOL();\n    outPtr.* = .{\n        .visual_row = vcursor.visual_row,\n        .visual_col = vcursor.visual_col,\n        .logical_row = vcursor.logical_row,\n        .logical_col = vcursor.logical_col,\n        .offset = vcursor.offset,\n    };\n}\n\nexport fn editorViewGetVisualEOL(view: *editor_view.EditorView, outPtr: *ExternalVisualCursor) void {\n    const vcursor = view.getVisualEOL();\n    outPtr.* = .{\n        .visual_row = vcursor.visual_row,\n        .visual_col = vcursor.visual_col,\n        .logical_row = vcursor.logical_row,\n        .logical_col = vcursor.logical_col,\n        .offset = vcursor.offset,\n    };\n}\n\nexport fn editorViewSetPlaceholderStyledText(\n    view: *editor_view.EditorView,\n    chunksPtr: [*]const text_buffer.StyledChunk,\n    chunkCount: usize,\n) void {\n    if (chunkCount == 0) {\n        view.setPlaceholderStyledText(&[_]text_buffer.StyledChunk{}) catch {};\n        return;\n    }\n    const chunks = chunksPtr[0..chunkCount];\n    view.setPlaceholderStyledText(chunks) catch {};\n}\n\nexport fn editorViewSetTabIndicator(view: *editor_view.EditorView, indicator: u32) void {\n    view.setTabIndicator(indicator);\n}\n\nexport fn editorViewSetTabIndicatorColor(view: *editor_view.EditorView, color: [*]const f32) void {\n    view.setTabIndicatorColor(utils.f32PtrToRGBA(color));\n}\n\nexport fn bufferDrawEditorView(\n    bufferPtr: *buffer.OptimizedBuffer,\n    viewPtr: *editor_view.EditorView,\n    x: i32,\n    y: i32,\n) void {\n    bufferPtr.drawEditorView(viewPtr, x, y) catch {};\n}\n\nexport fn bufferDrawTextBufferView(\n    bufferPtr: *buffer.OptimizedBuffer,\n    viewPtr: *text_buffer_view.UnifiedTextBufferView,\n    x: i32,\n    y: i32,\n) void {\n    bufferPtr.drawTextBuffer(viewPtr, x, y) catch {};\n}\n\npub const ExternalHighlight = extern struct {\n    start: u32,\n    end: u32,\n    style_id: u32,\n    priority: u8,\n    hl_ref: u16,\n};\n\npub const ExternalLogicalCursor = extern struct {\n    row: u32,\n    col: u32,\n    offset: u32,\n};\n\npub const ExternalVisualCursor = extern struct {\n    visual_row: u32,\n    visual_col: u32,\n    logical_row: u32,\n    logical_col: u32,\n    offset: u32,\n};\n\npub const ExternalLineInfo = extern struct {\n    start_cols_ptr: [*]const u32,\n    start_cols_len: u32,\n    width_cols_ptr: [*]const u32,\n    width_cols_len: u32,\n    sources_ptr: [*]const u32,\n    sources_len: u32,\n    wraps_ptr: [*]const u32,\n    wraps_len: u32,\n    width_cols_max: u32,\n};\n\nexport fn textBufferAddHighlightByCharRange(\n    tb: *text_buffer.UnifiedTextBuffer,\n    hl_ptr: [*]const ExternalHighlight,\n) void {\n    const hl = hl_ptr[0];\n    // For char-range highlights, start/end in the struct are unused (passed as char_start/char_end)\n    tb.addHighlightByCharRange(hl.start, hl.end, hl.style_id, hl.priority, hl.hl_ref) catch {};\n}\n\nexport fn textBufferAddHighlight(\n    tb: *text_buffer.UnifiedTextBuffer,\n    line_idx: u32,\n    hl_ptr: [*]const ExternalHighlight,\n) void {\n    const hl = hl_ptr[0];\n    // For line-based highlights, start/end are column offsets\n    tb.addHighlight(line_idx, hl.start, hl.end, hl.style_id, hl.priority, hl.hl_ref) catch {};\n}\n\nexport fn textBufferRemoveHighlightsByRef(tb: *text_buffer.UnifiedTextBuffer, hl_ref: u16) void {\n    tb.removeHighlightsByRef(hl_ref);\n}\n\nexport fn textBufferClearLineHighlights(tb: *text_buffer.UnifiedTextBuffer, line_idx: u32) void {\n    tb.clearLineHighlights(line_idx);\n}\n\nexport fn textBufferClearAllHighlights(tb: *text_buffer.UnifiedTextBuffer) void {\n    tb.clearAllHighlights();\n}\n\nexport fn textBufferSetSyntaxStyle(tb: *text_buffer.UnifiedTextBuffer, style: ?*syntax_style.SyntaxStyle) void {\n    tb.setSyntaxStyle(style);\n}\n\nexport fn textBufferGetLineHighlightsPtr(\n    tb: *text_buffer.UnifiedTextBuffer,\n    line_idx: u32,\n    out_count: *usize,\n) ?[*]const ExternalHighlight {\n    const highs = tb.getLineHighlightsSlice(@intCast(line_idx));\n\n    if (highs.len == 0) {\n        out_count.* = 0;\n        return null;\n    }\n\n    var slice = globalAllocator.alloc(ExternalHighlight, highs.len) catch return null;\n\n    for (highs, 0..) |hl, i| {\n        slice[i] = .{\n            .start = hl.col_start,\n            .end = hl.col_end,\n            .style_id = hl.style_id,\n            .priority = hl.priority,\n            .hl_ref = hl.hl_ref,\n        };\n    }\n\n    out_count.* = highs.len;\n    return slice.ptr;\n}\n\nexport fn textBufferFreeLineHighlights(ptr: [*]const ExternalHighlight, count: usize) void {\n    globalAllocator.free(@constCast(ptr)[0..count]);\n}\n\nexport fn textBufferGetHighlightCount(tb: *text_buffer.UnifiedTextBuffer) u32 {\n    return tb.getHighlightCount();\n}\n\nexport fn textBufferGetTextRange(tb: *text_buffer.UnifiedTextBuffer, start_offset: u32, end_offset: u32, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return tb.getTextRange(start_offset, end_offset, outBuffer);\n}\n\nexport fn textBufferGetTextRangeByCoords(tb: *text_buffer.UnifiedTextBuffer, start_row: u32, start_col: u32, end_row: u32, end_col: u32, outPtr: [*]u8, maxLen: usize) usize {\n    const outBuffer = outPtr[0..maxLen];\n    return tb.getTextRangeByCoords(start_row, start_col, end_row, end_col, outBuffer);\n}\n\n// SyntaxStyle functions\nexport fn createSyntaxStyle() ?*syntax_style.SyntaxStyle {\n    return syntax_style.SyntaxStyle.init(globalAllocator) catch |err| {\n        logger.err(\"Failed to create SyntaxStyle: {}\", .{err});\n        return null;\n    };\n}\n\nexport fn destroySyntaxStyle(style: *syntax_style.SyntaxStyle) void {\n    style.deinit();\n}\n\nexport fn syntaxStyleRegister(style: *syntax_style.SyntaxStyle, namePtr: [*]const u8, nameLen: usize, fg: ?[*]const f32, bg: ?[*]const f32, attributes: u32) u32 {\n    const name = namePtr[0..nameLen];\n    const fgColor = if (fg) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n    const bgColor = if (bg) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n    return style.registerStyle(name, fgColor, bgColor, attributes) catch 0;\n}\n\nexport fn syntaxStyleResolveByName(style: *syntax_style.SyntaxStyle, namePtr: [*]const u8, nameLen: usize) u32 {\n    const name = namePtr[0..nameLen];\n    return style.resolveByName(name) orelse 0;\n}\n\nexport fn syntaxStyleGetStyleCount(style: *syntax_style.SyntaxStyle) usize {\n    return style.getStyleCount();\n}\n\n// Unicode encoding API\n\npub const EncodedChar = extern struct {\n    width: u8,\n    char: u32,\n};\n\nexport fn encodeUnicode(\n    textPtr: [*]const u8,\n    textLen: usize,\n    outPtr: *[*]EncodedChar,\n    outLenPtr: *usize,\n    widthMethod: u8,\n) bool {\n    const text = textPtr[0..textLen];\n    const pool = gp.initGlobalPool(globalArena);\n    const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else .unicode;\n\n    // Check if ASCII only for optimization\n    const is_ascii_only = utf8.isAsciiOnly(text);\n\n    // Find grapheme info\n    var grapheme_list: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer grapheme_list.deinit(globalAllocator);\n\n    const tab_width: u8 = 2;\n    utf8.findGraphemeInfo(text, tab_width, is_ascii_only, wMethod, globalAllocator, &grapheme_list) catch return false;\n    const specials = grapheme_list.items;\n\n    // Allocate output array\n    const estimated_count = if (is_ascii_only) text.len else text.len * 2;\n    var result = globalAllocator.alloc(EncodedChar, estimated_count) catch return false;\n    var result_idx: usize = 0;\n    var success = false;\n    var pending_gid: ?u32 = null; // Track grapheme allocated but not yet stored in result\n\n    // Clean up result array and any allocated grapheme IDs on failure\n    defer {\n        if (!success) {\n            // Clean up pending grapheme that wasn't stored yet\n            if (pending_gid) |gid| {\n                // Try decref first (works if incref was called, refcount >= 1)\n                // If that fails (refcount was 0), use freeUnreferenced\n                pool.decref(gid) catch {\n                    pool.freeUnreferenced(gid) catch {};\n                };\n            }\n            // Decref any grapheme IDs we allocated before the failure\n            for (result[0..result_idx]) |encoded_char| {\n                if (gp.isGraphemeChar(encoded_char.char)) {\n                    const gid = gp.graphemeIdFromChar(encoded_char.char);\n                    pool.decref(gid) catch {};\n                }\n            }\n            globalAllocator.free(result);\n        }\n    }\n\n    var byte_offset: u32 = 0;\n    var col: u32 = 0;\n    var special_idx: usize = 0;\n\n    while (byte_offset < text.len) {\n        const at_special = special_idx < specials.len and specials[special_idx].col_offset == col;\n\n        var grapheme_bytes: []const u8 = undefined;\n        var g_width: u8 = undefined;\n\n        if (at_special) {\n            const g = specials[special_idx];\n            grapheme_bytes = text[g.byte_offset .. g.byte_offset + g.byte_len];\n            g_width = g.width;\n            byte_offset = g.byte_offset + g.byte_len;\n            special_idx += 1;\n        } else {\n            if (byte_offset >= text.len) break;\n            grapheme_bytes = text[byte_offset .. byte_offset + 1];\n            g_width = 1;\n            byte_offset += 1;\n        }\n\n        const cell_width = utf8.getWidthAt(text, if (at_special) specials[special_idx - 1].byte_offset else byte_offset - 1, tab_width, wMethod);\n        if (cell_width == 0) {\n            col += g_width;\n            continue;\n        }\n\n        // Encode the character\n        var encoded_char: u32 = 0;\n        if (grapheme_bytes.len == 1 and cell_width == 1 and grapheme_bytes[0] >= 32) {\n            // Simple ASCII character\n            encoded_char = @as(u32, grapheme_bytes[0]);\n        } else {\n            // Multi-byte or special character - allocate in pool\n            const gid = pool.alloc(grapheme_bytes) catch return false;\n            pending_gid = gid; // Track until stored in result\n            encoded_char = gp.packGraphemeStart(gid & gp.GRAPHEME_ID_MASK, cell_width);\n\n            // Incref since we're handing this off to the caller\n            // Note: incref can only fail if gid is invalid, which shouldn't happen\n            // for a freshly allocated gid. If it does fail, the slot leaks but\n            // this is an edge case that indicates a bug elsewhere.\n            pool.incref(gid) catch return false;\n        }\n\n        // Ensure we have space\n        if (result_idx >= result.len) {\n            const new_len = result.len * 2;\n            result = globalAllocator.realloc(result, new_len) catch return false;\n        }\n\n        result[result_idx] = EncodedChar{\n            .width = @intCast(cell_width),\n            .char = encoded_char,\n        };\n        pending_gid = null; // Successfully stored, no longer pending\n        result_idx += 1;\n        col += g_width;\n    }\n\n    // Trim to actual size\n    result = globalAllocator.realloc(result, result_idx) catch result;\n\n    outPtr.* = result.ptr;\n    outLenPtr.* = result_idx;\n    success = true;\n    return true;\n}\n\nexport fn freeUnicode(charsPtr: [*]const EncodedChar, charsLen: usize) void {\n    const chars = charsPtr[0..charsLen];\n    const pool = gp.initGlobalPool(globalArena);\n\n    for (chars) |encoded_char| {\n        const char = encoded_char.char;\n\n        // Check if this is a packed grapheme\n        if (gp.isGraphemeChar(char)) {\n            const gid = gp.graphemeIdFromChar(char);\n            pool.decref(gid) catch {};\n        }\n    }\n\n    // Free the array itself\n    globalAllocator.free(chars);\n}\n\nexport fn bufferDrawChar(\n    bufferPtr: *buffer.OptimizedBuffer,\n    char: u32,\n    x: u32,\n    y: u32,\n    fg: [*]const f32,\n    bg: [*]const f32,\n    attributes: u32,\n) void {\n    const rgbaFg = utils.f32PtrToRGBA(fg);\n    const rgbaBg = utils.f32PtrToRGBA(bg);\n    bufferPtr.drawChar(char, x, y, rgbaFg, rgbaBg, attributes) catch {};\n}\n"
  },
  {
    "path": "packages/core/src/zig/link.zig",
    "content": "const std = @import(\"std\");\n\npub const LinkPoolError = error{\n    OutOfMemory,\n    InvalidId,\n    WrongGeneration,\n    UrlTooLong,\n};\n\n// ID layout within 24 bits: [ generation (8 bits) | slot_index (16 bits) ]\npub const GEN_BITS: u5 = 8;\npub const SLOT_BITS: u5 = 16;\npub const GEN_MASK: u32 = (@as(u32, 1) << GEN_BITS) - 1;\npub const SLOT_MASK: u32 = (@as(u32, 1) << SLOT_BITS) - 1;\npub const MAX_URL_LENGTH: usize = 512;\n\npub const IdPayload = u32;\n\nconst SlotHeader = extern struct {\n    len: u32,\n    refcount: u32,\n    generation: u32,\n};\n\n/// Simple link pool for storing URL strings with reusable IDs\npub const LinkPool = struct {\n    allocator: std.mem.Allocator,\n    slot_capacity: u32,\n    slots_per_page: u32,\n    slot_size_bytes: usize,\n    slots: std.ArrayListUnmanaged(u8),\n    free_list: std.ArrayListUnmanaged(u32),\n    num_slots: u32,\n    interned_live_ids: std.StringHashMapUnmanaged(IdPayload),\n\n    pub fn init(allocator: std.mem.Allocator) LinkPool {\n        const slot_capacity = MAX_URL_LENGTH;\n        const slots_per_page = 64;\n        const slot_size_bytes = @sizeOf(SlotHeader) + slot_capacity;\n        return .{\n            .allocator = allocator,\n            .slot_capacity = slot_capacity,\n            .slots_per_page = slots_per_page,\n            .slot_size_bytes = slot_size_bytes,\n            .slots = .{},\n            .free_list = .{},\n            .num_slots = 0,\n            .interned_live_ids = .{},\n        };\n    }\n\n    pub fn deinit(self: *LinkPool) void {\n        var key_it = self.interned_live_ids.keyIterator();\n        while (key_it.next()) |key_ptr| {\n            self.allocator.free(@constCast(key_ptr.*));\n        }\n        self.interned_live_ids.deinit(self.allocator);\n\n        self.slots.deinit(self.allocator);\n        self.free_list.deinit(self.allocator);\n    }\n\n    fn grow(self: *LinkPool) LinkPoolError!void {\n        const add_bytes = self.slot_size_bytes * self.slots_per_page;\n\n        try self.slots.ensureTotalCapacity(self.allocator, self.slots.items.len + add_bytes);\n        try self.slots.appendNTimes(self.allocator, 0, add_bytes);\n\n        var i: u32 = 0;\n        while (i < self.slots_per_page) : (i += 1) {\n            try self.free_list.append(self.allocator, self.num_slots + i);\n        }\n        self.num_slots += self.slots_per_page;\n    }\n\n    fn slotPtr(self: *LinkPool, slot_index: u32) *u8 {\n        const offset: usize = @as(usize, slot_index) * self.slot_size_bytes;\n        return &self.slots.items[offset];\n    }\n\n    fn packId(slot_index: u32, generation: u32) LinkPoolError!IdPayload {\n        if (slot_index > SLOT_MASK) return LinkPoolError.OutOfMemory;\n        return ((generation & GEN_MASK) << SLOT_BITS) | (slot_index & SLOT_MASK);\n    }\n\n    fn unpackId(id: IdPayload) struct { slot_index: u32, generation: u32 } {\n        return .{\n            .slot_index = id & SLOT_MASK,\n            .generation = (id >> SLOT_BITS) & GEN_MASK,\n        };\n    }\n\n    fn removeInternedLiveId(self: *LinkPool, url: []const u8, expected_id: IdPayload) void {\n        const live_id = self.interned_live_ids.get(url) orelse return;\n        if (live_id != expected_id) return;\n        if (self.interned_live_ids.fetchRemove(url)) |removed| {\n            self.allocator.free(@constCast(removed.key));\n        }\n    }\n\n    fn lookupOrInvalidate(self: *LinkPool, url: []const u8) ?IdPayload {\n        const live_id = self.interned_live_ids.get(url) orelse return null;\n\n        const live_url = self.get(live_id) catch {\n            self.removeInternedLiveId(url, live_id);\n            return null;\n        };\n\n        if (!std.mem.eql(u8, live_url, url)) {\n            self.removeInternedLiveId(url, live_id);\n            return null;\n        }\n\n        const live_refcount = self.getRefcount(live_id) catch {\n            self.removeInternedLiveId(url, live_id);\n            return null;\n        };\n\n        if (live_refcount == 0) {\n            self.removeInternedLiveId(url, live_id);\n            return null;\n        }\n\n        return live_id;\n    }\n\n    fn internLiveId(self: *LinkPool, id: IdPayload, url: []const u8) LinkPoolError!void {\n        if (self.lookupOrInvalidate(url) != null) {\n            return;\n        }\n\n        const owned_key = self.allocator.dupe(u8, url) catch return LinkPoolError.OutOfMemory;\n        errdefer self.allocator.free(owned_key);\n\n        if (self.interned_live_ids.fetchPut(self.allocator, owned_key, id) catch return LinkPoolError.OutOfMemory) |replaced| {\n            self.allocator.free(@constCast(replaced.key));\n        }\n    }\n\n    pub fn alloc(self: *LinkPool, url: []const u8) LinkPoolError!IdPayload {\n        if (url.len > self.slot_capacity) {\n            return LinkPoolError.UrlTooLong;\n        }\n\n        if (self.lookupOrInvalidate(url)) |live_id| {\n            return live_id;\n        }\n\n        if (self.free_list.items.len == 0) try self.grow();\n\n        const slot_index = self.free_list.pop().?;\n        const p = self.slotPtr(slot_index);\n        const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n\n        // Increment generation when reusing a slot; reserve generation 0 so ID 0 remains an error sentinel in FFI.\n        var new_generation = (header_ptr.generation + 1) & GEN_MASK;\n        if (new_generation == 0) new_generation = 1;\n\n        header_ptr.* = .{\n            .len = @intCast(url.len),\n            .refcount = 0,\n            .generation = new_generation,\n        };\n\n        const data_ptr = @as([*]u8, @ptrCast(p)) + @sizeOf(SlotHeader);\n        @memcpy(data_ptr[0..url.len], url);\n\n        return try packId(slot_index, new_generation);\n    }\n\n    pub fn incref(self: *LinkPool, id: IdPayload) LinkPoolError!void {\n        const unpacked = unpackId(id);\n        if (unpacked.slot_index >= self.num_slots) return LinkPoolError.InvalidId;\n\n        const p = self.slotPtr(unpacked.slot_index);\n        const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n\n        if (header_ptr.generation != unpacked.generation) {\n            return LinkPoolError.WrongGeneration;\n        }\n\n        const old_refcount = header_ptr.refcount;\n        header_ptr.refcount +%= 1;\n\n        if (old_refcount == 0) {\n            const live_url = try self.get(id);\n            try self.internLiveId(id, live_url);\n        }\n    }\n\n    pub fn decref(self: *LinkPool, id: IdPayload) LinkPoolError!void {\n        const unpacked = unpackId(id);\n        if (unpacked.slot_index >= self.num_slots) return LinkPoolError.InvalidId;\n\n        const p = self.slotPtr(unpacked.slot_index);\n        const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n\n        if (header_ptr.refcount == 0) return LinkPoolError.InvalidId;\n        if (header_ptr.generation != unpacked.generation) return LinkPoolError.WrongGeneration;\n\n        if (header_ptr.refcount == 1) {\n            const live_url = try self.get(id);\n            self.removeInternedLiveId(live_url, id);\n        }\n\n        header_ptr.refcount -%= 1;\n\n        if (header_ptr.refcount == 0) {\n            try self.free_list.append(self.allocator, unpacked.slot_index);\n        }\n    }\n\n    pub fn get(self: *LinkPool, id: IdPayload) LinkPoolError![]const u8 {\n        const unpacked = unpackId(id);\n        if (unpacked.slot_index >= self.num_slots) return LinkPoolError.InvalidId;\n\n        const p = self.slotPtr(unpacked.slot_index);\n        const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n\n        if (header_ptr.generation != unpacked.generation) return LinkPoolError.WrongGeneration;\n\n        const data_ptr = @as([*]u8, @ptrCast(p)) + @sizeOf(SlotHeader);\n        return data_ptr[0..header_ptr.len];\n    }\n\n    pub fn getRefcount(self: *LinkPool, id: IdPayload) LinkPoolError!u32 {\n        const unpacked = unpackId(id);\n        if (unpacked.slot_index >= self.num_slots) return LinkPoolError.InvalidId;\n\n        const p = self.slotPtr(unpacked.slot_index);\n        const header_ptr = @as(*SlotHeader, @ptrCast(@alignCast(p)));\n\n        if (header_ptr.generation != unpacked.generation) return LinkPoolError.WrongGeneration;\n\n        return header_ptr.refcount;\n    }\n\n    pub fn getTotalSlots(self: *const LinkPool) u64 {\n        return self.num_slots;\n    }\n\n    pub fn getFreeSlotCount(self: *const LinkPool) u64 {\n        return self.free_list.items.len;\n    }\n\n    pub fn getLiveSlotCount(self: *const LinkPool) u64 {\n        return self.num_slots - @as(u32, @intCast(self.free_list.items.len));\n    }\n};\n\n/// Track link usage per buffer with per-cell refcounting\npub const LinkTracker = struct {\n    pool: *LinkPool,\n    used_ids: std.AutoHashMap(u32, u32), // id -> cell_count\n\n    pub fn init(allocator: std.mem.Allocator, pool: *LinkPool) LinkTracker {\n        return .{\n            .pool = pool,\n            .used_ids = std.AutoHashMap(u32, u32).init(allocator),\n        };\n    }\n\n    fn decRefAll(self: *LinkTracker) void {\n        var it = self.used_ids.iterator();\n        while (it.next()) |entry| {\n            const id = entry.key_ptr.*;\n            self.pool.decref(id) catch {};\n        }\n    }\n\n    pub fn deinit(self: *LinkTracker) void {\n        self.decRefAll();\n        self.used_ids.deinit();\n    }\n\n    pub fn clear(self: *LinkTracker) void {\n        self.decRefAll();\n        self.used_ids.clearRetainingCapacity();\n    }\n\n    pub fn addCellRef(self: *LinkTracker, id: u32) void {\n        const res = self.used_ids.getOrPut(id) catch |err| {\n            std.debug.panic(\"LinkTracker.addCellRef getOrPut failed: {}\\n\", .{err});\n        };\n        if (!res.found_existing) {\n            // First time seeing this ID - try to incref in pool\n            self.pool.incref(id) catch {\n                // Invalid ID (not allocated in pool) - silently ignore\n                // This can happen with garbage in attribute bits\n                return;\n            };\n            res.value_ptr.* = 1;\n        } else {\n            res.value_ptr.* += 1;\n        }\n    }\n\n    pub fn removeCellRef(self: *LinkTracker, id: u32) void {\n        if (self.used_ids.getPtr(id)) |count_ptr| {\n            if (count_ptr.* > 0) {\n                count_ptr.* -= 1;\n                if (count_ptr.* == 0) {\n                    _ = self.used_ids.remove(id);\n                    self.pool.decref(id) catch {};\n                }\n            }\n        }\n    }\n\n    pub fn hasAny(self: *const LinkTracker) bool {\n        return self.used_ids.count() > 0;\n    }\n\n    pub fn getLinkCount(self: *const LinkTracker) u32 {\n        return @intCast(self.used_ids.count());\n    }\n};\n\nvar GLOBAL_LINK_POOL: ?LinkPool = null;\n\npub fn initGlobalLinkPool(allocator: std.mem.Allocator) *LinkPool {\n    if (GLOBAL_LINK_POOL == null) {\n        GLOBAL_LINK_POOL = LinkPool.init(allocator);\n    }\n    return &GLOBAL_LINK_POOL.?;\n}\n\npub fn deinitGlobalLinkPool() void {\n    if (GLOBAL_LINK_POOL) |*p| {\n        p.deinit();\n        GLOBAL_LINK_POOL = null;\n    }\n}\n"
  },
  {
    "path": "packages/core/src/zig/logger.zig",
    "content": "const std = @import(\"std\");\n\npub const LogLevel = enum(u8) {\n    err = 0,\n    warn = 1,\n    info = 2,\n    debug = 3,\n};\n\nvar global_log_callback: ?*const fn (level: u8, msgPtr: [*]const u8, msgLen: usize) callconv(.c) void = null;\n\npub fn setLogCallback(callback: ?*const fn (level: u8, msgPtr: [*]const u8, msgLen: usize) callconv(.c) void) void {\n    global_log_callback = callback;\n}\n\n// Helper function to log messages - can be used directly throughout the codebase\npub fn logMessage(level: LogLevel, comptime format: []const u8, args: anytype) void {\n    if (global_log_callback) |callback| {\n        var buf: [4096]u8 = undefined;\n        const msg = std.fmt.bufPrint(&buf, format, args) catch {\n            const fallback = \"Log formatting failed\";\n            callback(@intFromEnum(LogLevel.err), fallback.ptr, fallback.len);\n            return;\n        };\n        callback(@intFromEnum(level), msg.ptr, msg.len);\n    }\n}\n\npub fn err(comptime format: []const u8, args: anytype) void {\n    logMessage(.err, format, args);\n}\n\npub fn warn(comptime format: []const u8, args: anytype) void {\n    logMessage(.warn, format, args);\n}\n\npub fn info(comptime format: []const u8, args: anytype) void {\n    logMessage(.info, format, args);\n}\n\npub fn debug(comptime format: []const u8, args: anytype) void {\n    logMessage(.debug, format, args);\n}\n"
  },
  {
    "path": "packages/core/src/zig/mem-registry.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\npub const MemRegistryError = error{\n    OutOfMemory,\n    InvalidMemId,\n};\n\n/// Memory buffer reference in the registry\npub const MemBuffer = struct {\n    data: []const u8,\n    owned: bool,\n    active: bool, // Track if slot is in use\n};\n\n/// Registry for multiple memory buffers\npub const MemRegistry = struct {\n    buffers: std.ArrayListUnmanaged(MemBuffer),\n    free_slots: std.ArrayListUnmanaged(u8), // Track free slot indices\n    allocator: Allocator,\n\n    pub fn init(allocator: Allocator) MemRegistry {\n        return .{\n            .buffers = .{},\n            .free_slots = .{},\n            .allocator = allocator,\n        };\n    }\n\n    pub fn deinit(self: *MemRegistry) void {\n        for (self.buffers.items) |mem_buf| {\n            if (mem_buf.active and mem_buf.owned) {\n                self.allocator.free(mem_buf.data);\n            }\n        }\n        self.buffers.deinit(self.allocator);\n        self.free_slots.deinit(self.allocator);\n    }\n\n    pub fn register(self: *MemRegistry, data: []const u8, owned: bool) MemRegistryError!u8 {\n        // Try to reuse a free slot first\n        if (self.free_slots.items.len > 0) {\n            const id = self.free_slots.items[self.free_slots.items.len - 1];\n            _ = self.free_slots.pop();\n            self.buffers.items[id] = MemBuffer{\n                .data = data,\n                .owned = owned,\n                .active = true,\n            };\n            return id;\n        }\n\n        // No free slots, allocate a new one\n        if (self.buffers.items.len >= 255) {\n            return MemRegistryError.OutOfMemory;\n        }\n        const id: u8 = @intCast(self.buffers.items.len);\n        try self.buffers.append(self.allocator, MemBuffer{\n            .data = data,\n            .owned = owned,\n            .active = true,\n        });\n        return id;\n    }\n\n    pub fn get(self: *const MemRegistry, id: u8) ?[]const u8 {\n        if (id >= self.buffers.items.len) return null;\n        const buf = self.buffers.items[id];\n        if (!buf.active) return null;\n        return buf.data;\n    }\n\n    pub fn replace(self: *MemRegistry, id: u8, data: []const u8, owned: bool) MemRegistryError!void {\n        if (id >= self.buffers.items.len) return MemRegistryError.InvalidMemId;\n        const prev = self.buffers.items[id];\n        if (!prev.active) return MemRegistryError.InvalidMemId;\n        if (prev.owned) {\n            self.allocator.free(prev.data);\n        }\n        self.buffers.items[id] = .{ .data = data, .owned = owned, .active = true };\n    }\n\n    pub fn unregister(self: *MemRegistry, id: u8) MemRegistryError!void {\n        if (id >= self.buffers.items.len) return MemRegistryError.InvalidMemId;\n        var buf = &self.buffers.items[id];\n        if (!buf.active) return MemRegistryError.InvalidMemId;\n\n        // Free owned memory\n        if (buf.owned) {\n            self.allocator.free(buf.data);\n        }\n\n        // Mark slot as inactive\n        buf.active = false;\n        buf.data = &[_]u8{};\n        buf.owned = false;\n\n        // Add to free slots list\n        try self.free_slots.append(self.allocator, id);\n    }\n\n    pub fn clear(self: *MemRegistry) void {\n        for (self.buffers.items) |mem_buf| {\n            if (mem_buf.active and mem_buf.owned) {\n                self.allocator.free(mem_buf.data);\n            }\n        }\n        self.buffers.clearRetainingCapacity();\n        self.free_slots.clearRetainingCapacity();\n    }\n\n    pub fn getUsedSlots(self: *const MemRegistry) usize {\n        // Count only active slots\n        var count: usize = 0;\n        for (self.buffers.items) |buf| {\n            if (buf.active) count += 1;\n        }\n        return count;\n    }\n\n    pub fn getFreeSlots(self: *const MemRegistry) usize {\n        // Total capacity (255) minus buffers allocated plus explicitly freed slots\n        return 255 - self.buffers.items.len + self.free_slots.items.len;\n    }\n};\n"
  },
  {
    "path": "packages/core/src/zig/native-span-feed-bench-lib.zig",
    "content": "const lib = @import(\"lib.zig\");\nconst bench = @import(\"bench/native-span-feed_bench.zig\");\n\ncomptime {\n    _ = lib;\n    _ = bench;\n}\n"
  },
  {
    "path": "packages/core/src/zig/native-span-feed.zig",
    "content": "const std = @import(\"std\");\n\npub const CallbackFn = fn (stream_ptr: usize, event_id: u32, arg0: usize, arg1: u64) callconv(.c) void;\n\npub const GrowthPolicy = enum(u8) {\n    grow = 0,\n    block = 1,\n};\n\npub const Options = extern struct {\n    chunk_size: u32,\n    initial_chunks: u32,\n    max_bytes: u64,\n    growth_policy: u8,\n    auto_commit_on_full: u8,\n    span_queue_capacity: u32,\n};\n\npub const Stats = extern struct {\n    bytes_written: u64,\n    spans_committed: u64,\n    chunks: u32,\n    pending_spans: u32,\n};\n\nconst Chunk = struct {\n    ptr: [*]u8,\n    len: u32,\n};\n\npub const SpanInfo = extern struct {\n    chunk_ptr: usize,\n    offset: u32,\n    len: u32,\n    chunk_index: u32,\n    reserved: u32,\n\n    pub fn slice(self: SpanInfo) []u8 {\n        const base: [*]u8 = @ptrFromInt(self.chunk_ptr);\n        const start: usize = @intCast(self.offset);\n        const length: usize = @intCast(self.len);\n        return base[start .. start + length];\n    }\n};\n\nconst SpanRing = struct {\n    buffer: []SpanInfo,\n    capacity: u32,\n    head: u32,\n    tail: u32,\n\n    pub fn count(self: *SpanRing) u32 {\n        return self.tail -% self.head;\n    }\n\n    pub fn push(self: *SpanRing, stream: *Stream, span: SpanInfo, notify: *bool) StreamError!void {\n        const capacity = self.capacity;\n        const head = self.head;\n        var tail = self.tail;\n        const queued = tail -% head;\n\n        if (queued >= capacity) {\n            return StreamError.NoSpace;\n        }\n\n        const index = tail % capacity;\n        self.buffer[index] = span;\n        tail +%= 1;\n        self.tail = tail;\n        const new_count = queued + 1;\n\n        stream.stats.pending_spans = new_count;\n        if (stream.attached and stream.callback != null) {\n            if (queued < notify_threshold_default and new_count >= notify_threshold_default) {\n                notify.* = true;\n            }\n        }\n    }\n\n    pub fn popMany(self: *SpanRing, out: []SpanInfo) u32 {\n        const available = self.tail -% self.head;\n        if (available == 0) return 0;\n        const to_read: u32 = if (available < out.len) @intCast(available) else @intCast(out.len);\n\n        var i: u32 = 0;\n        while (i < to_read) : (i += 1) {\n            const index = (self.head +% i) % self.capacity;\n            out[i] = self.buffer[index];\n        }\n        self.head +%= to_read;\n        return to_read;\n    }\n};\n\npub const ReserveInfo = extern struct {\n    ptr: usize,\n    len: u32,\n    reserved: u32,\n\n    pub fn slice(self: ReserveInfo) []u8 {\n        const base: [*]u8 = @ptrFromInt(self.ptr);\n        const length: usize = @intCast(self.len);\n        return base[0..length];\n    }\n};\n\npub const Stream = struct {\n    allocator: std.mem.Allocator,\n    options: Options,\n    chunks: std.ArrayList(Chunk),\n    current_chunk_index: usize,\n    write_offset: usize,\n    pending_chunk_index: usize,\n    pending_offset: usize,\n    pending_len: usize,\n    reserved_active: bool,\n    reserved_chunk_index: usize,\n    reserved_offset: usize,\n    reserved_len: usize,\n    attached: bool,\n    callback: ?*const CallbackFn,\n    closed: bool,\n    span_ring: SpanRing,\n    state_buffer: []u8,\n    state_capacity: u32,\n    stats: Stats,\n\n    pub fn create(allocator: std.mem.Allocator, options: ?Options) StreamError!*Stream {\n        const opts = normalizeOptions(options orelse defaultOptions());\n        const stream = allocator.create(Stream) catch return StreamError.OutOfMemory;\n        stream.* = .{\n            .allocator = allocator,\n            .options = opts,\n            .chunks = std.ArrayList(Chunk).empty,\n            .current_chunk_index = 0,\n            .write_offset = 0,\n            .pending_chunk_index = 0,\n            .pending_offset = 0,\n            .pending_len = 0,\n            .reserved_active = false,\n            .reserved_chunk_index = 0,\n            .reserved_offset = 0,\n            .reserved_len = 0,\n            .attached = false,\n            .callback = null,\n            .closed = false,\n            .span_ring = .{\n                .buffer = &[_]SpanInfo{},\n                .capacity = 0,\n                .head = 0,\n                .tail = 0,\n            },\n            .state_buffer = &[_]u8{},\n            .state_capacity = 0,\n            .stats = .{\n                .bytes_written = 0,\n                .spans_committed = 0,\n                .chunks = 0,\n                .pending_spans = 0,\n            },\n        };\n\n        errdefer stream.destroy();\n\n        const ring_capacity = opts.span_queue_capacity;\n        const ring_buffer = allocator.alloc(SpanInfo, ring_capacity) catch return StreamError.OutOfMemory;\n        stream.span_ring = .{\n            .buffer = ring_buffer,\n            .capacity = ring_capacity,\n            .head = 0,\n            .tail = 0,\n        };\n\n        try stream.ensureStateCapacity(@intCast(opts.initial_chunks));\n\n        const initial = @as(usize, opts.initial_chunks);\n        var i: usize = 0;\n        while (i < initial) : (i += 1) {\n            try stream.addChunkLocked();\n        }\n        stream.stats.chunks = @intCast(stream.chunks.items.len);\n        return stream;\n    }\n\n    pub fn attach(self: *Stream) StreamError!void {\n        if (self.closed) return StreamError.Invalid;\n\n        var notify = false;\n        var queued: u32 = 0;\n        defer self.finish(notify, queued);\n\n        self.attached = true;\n        if (self.callback == null) return;\n\n        self.emitStateBuffer();\n\n        for (self.chunks.items) |chunk| {\n            self.emitChunkAdded(chunk);\n        }\n\n        queued = self.span_ring.count();\n        if (queued > 0) {\n            notify = true;\n        }\n    }\n\n    pub fn setCallback(self: *Stream, cb: ?*const CallbackFn) void {\n        self.callback = cb;\n        if (cb == null or !self.attached) return;\n\n        self.emitStateBuffer();\n        for (self.chunks.items) |chunk| {\n            self.emitChunkAdded(chunk);\n        }\n        const queued = self.span_ring.count();\n        if (queued > 0) {\n            self.emitDataAvailable(queued);\n        }\n    }\n\n    pub fn write(self: *Stream, data: []const u8) StreamError!void {\n        if (self.closed) return StreamError.Invalid;\n        if (data.len == 0) return;\n        if (self.reserved_active) return StreamError.Busy;\n\n        var notify = false;\n        // finish() must run on success and error so committed spans notify.\n        defer self.finish(notify, 0);\n\n        var remaining = data.len;\n        var src_index: usize = 0;\n        const auto_commit = self.options.auto_commit_on_full != 0;\n        const chunk_len = self.options.chunk_size;\n\n        while (remaining > 0) {\n            var available = @as(usize, chunk_len) - self.write_offset;\n            if (available == 0) {\n                if (self.pending_len > 0) {\n                    try self.commitLocked(&notify);\n                }\n                try self.ensureWritableChunkLocked();\n                available = @as(usize, chunk_len);\n            }\n\n            if (remaining > available and !auto_commit) {\n                return StreamError.NoSpace;\n            }\n\n            const to_write = if (remaining < available) remaining else available;\n            if (self.pending_len == 0) {\n                self.pending_chunk_index = self.current_chunk_index;\n                self.pending_offset = self.write_offset;\n            }\n\n            const chunk = self.chunks.items[self.current_chunk_index];\n            @memcpy(chunk.ptr[self.write_offset .. self.write_offset + to_write], data[src_index .. src_index + to_write]);\n\n            self.write_offset += to_write;\n            self.pending_len += to_write;\n            self.stats.bytes_written += @as(u64, to_write);\n            src_index += to_write;\n            remaining -= to_write;\n\n            if (self.write_offset == @as(usize, chunk_len) and auto_commit) {\n                try self.commitLocked(&notify);\n                if (remaining > 0) {\n                    try self.ensureWritableChunkLocked();\n                }\n            }\n        }\n    }\n\n    pub fn reserve(self: *Stream, min_len: u32) StreamError!ReserveInfo {\n        if (self.closed) return StreamError.Invalid;\n        return self.reserveLocked(min_len);\n    }\n\n    pub fn commitReserved(self: *Stream, len: u32) StreamError!void {\n        if (self.closed) return StreamError.Invalid;\n\n        var notify = false;\n        defer self.finish(notify, 0);\n        try self.commitReservedLocked(len, &notify);\n    }\n\n    pub fn commit(self: *Stream) StreamError!void {\n        if (self.closed) return StreamError.Invalid;\n        var notify = false;\n        defer self.finish(notify, 0);\n        if (self.reserved_active) return StreamError.Busy;\n        try self.commitLocked(&notify);\n    }\n\n    pub fn getStats(self: *Stream) Stats {\n        var out: Stats = undefined;\n        out = self.stats;\n        return out;\n    }\n\n    /// Apply only runtime-safe options; creation-time fields are ignored.\n    pub fn setOptions(self: *Stream, options: Options) StreamError!void {\n        if (self.closed) return StreamError.Invalid;\n        self.options.max_bytes = options.max_bytes;\n        self.options.growth_policy = options.growth_policy;\n        self.options.auto_commit_on_full = options.auto_commit_on_full;\n    }\n\n    pub fn close(self: *Stream) StreamError!void {\n        var notify = false;\n        if (self.closed) {\n            return;\n        }\n        if (self.reserved_active) {\n            return StreamError.Busy;\n        }\n        if (self.pending_len > 0) {\n            try self.commitLocked(&notify);\n        }\n        self.closed = true;\n        self.attached = false;\n        self.finish(notify, 0);\n        self.emitClosed();\n    }\n\n    pub fn destroy(self: *Stream) void {\n        if (!self.closed) {\n            _ = self.close() catch {};\n        }\n        for (self.chunks.items) |chunk| {\n            self.allocator.free(chunk.ptr[0..@as(usize, chunk.len)]);\n        }\n        self.chunks.deinit(self.allocator);\n        if (self.span_ring.capacity > 0) {\n            self.allocator.free(self.span_ring.buffer);\n        }\n        if (self.state_capacity > 0) {\n            self.allocator.free(self.state_buffer);\n        }\n        self.allocator.destroy(self);\n    }\n\n    pub fn drainSpans(self: *Stream, out: []SpanInfo) u32 {\n        if (out.len == 0) return 0;\n        const count = self.span_ring.popMany(out);\n        self.stats.pending_spans = self.span_ring.count();\n        return count;\n    }\n\n    pub fn hasPendingSpans(self: *Stream) bool {\n        return self.span_ring.count() > 0;\n    }\n\n    pub fn stateBuffer(self: *Stream) []u8 {\n        return self.state_buffer;\n    }\n\n    pub fn markChunkFree(self: *Stream, chunk_index: u32) void {\n        if (chunk_index < self.state_capacity) {\n            self.state_buffer[chunk_index] -|= 1;\n        }\n    }\n\n    pub fn markSpanConsumed(self: *Stream, span: SpanInfo) void {\n        self.markChunkFree(span.chunk_index);\n    }\n\n    pub fn finish(self: *Stream, notify: bool, queued_override: u32) void {\n        if (notify and self.callback != null) {\n            const queued = if (queued_override != 0)\n                queued_override\n            else\n                self.span_ring.count();\n            if (queued > 0) self.emitDataAvailable(queued);\n        }\n    }\n\n    fn ensureStateCapacity(self: *Stream, required: u32) StreamError!void {\n        if (required <= self.state_capacity) return;\n        var new_capacity: u32 = if (self.state_capacity == 0) 1 else self.state_capacity;\n        while (new_capacity < required) : (new_capacity *= 2) {}\n        const new_buffer = self.allocator.alloc(u8, new_capacity) catch return StreamError.OutOfMemory;\n        @memset(new_buffer, 0);\n        if (self.state_capacity > 0) {\n            std.mem.copyForwards(u8, new_buffer[0..self.state_capacity], self.state_buffer);\n            self.allocator.free(self.state_buffer);\n        }\n        self.state_buffer = new_buffer;\n        self.state_capacity = new_capacity;\n        if (self.attached and self.callback != null) {\n            self.emitStateBuffer();\n        }\n    }\n\n    fn isChunkFree(self: *Stream, index: usize) bool {\n        if (index >= self.state_capacity) return true;\n        return self.state_buffer[index] == 0;\n    }\n\n    pub fn commitLocked(self: *Stream, notify: *bool) StreamError!void {\n        if (self.pending_len == 0) return;\n        const chunk = self.chunks.items[self.pending_chunk_index];\n        const info = SpanInfo{\n            .chunk_ptr = @intFromPtr(chunk.ptr),\n            .offset = @intCast(self.pending_offset),\n            .len = @intCast(self.pending_len),\n            .chunk_index = @intCast(self.pending_chunk_index),\n            .reserved = 0,\n        };\n        try self.span_ring.push(self, info, notify);\n        if (self.pending_chunk_index < self.state_capacity) {\n            self.state_buffer[self.pending_chunk_index] +|= 1;\n            // Avoid refcount saturation, which can corrupt data.\n            if (self.state_buffer[self.pending_chunk_index] == 255) {\n                self.write_offset = self.options.chunk_size;\n            }\n        }\n        self.stats.spans_committed += 1;\n        self.pending_len = 0;\n        self.pending_offset = self.write_offset;\n        self.pending_chunk_index = self.current_chunk_index;\n    }\n\n    pub fn reserveLocked(self: *Stream, min_len: u32) StreamError!ReserveInfo {\n        if (self.reserved_active) return StreamError.Busy;\n        if (self.pending_len != 0) return StreamError.Busy;\n\n        try self.ensureWritableChunkLocked();\n\n        const chunk = self.chunks.items[self.current_chunk_index];\n        const available = @as(usize, chunk.len) - self.write_offset;\n        if (available < min_len) return StreamError.NoSpace;\n\n        self.reserved_active = true;\n        self.reserved_chunk_index = self.current_chunk_index;\n        self.reserved_offset = self.write_offset;\n        self.reserved_len = available;\n\n        return .{\n            .ptr = @intFromPtr(chunk.ptr + self.write_offset),\n            .len = @intCast(available),\n            .reserved = 0,\n        };\n    }\n\n    pub fn commitReservedLocked(self: *Stream, len: u32, notify: *bool) StreamError!void {\n        if (!self.reserved_active) return StreamError.Invalid;\n        if (len > self.reserved_len) return StreamError.NoSpace;\n\n        self.pending_chunk_index = self.reserved_chunk_index;\n        self.pending_offset = self.reserved_offset;\n        self.pending_len = len;\n        self.write_offset = self.reserved_offset + len;\n        self.reserved_active = false;\n        self.reserved_len = 0;\n\n        self.stats.bytes_written += @as(u64, len);\n\n        try self.commitLocked(notify);\n    }\n\n    fn addChunkLocked(self: *Stream) StreamError!void {\n        const chunk_size: u32 = self.options.chunk_size;\n        const max_bytes = self.options.max_bytes;\n        const allocated = @as(u64, self.chunks.items.len) * @as(u64, chunk_size);\n        if (max_bytes != 0 and allocated + @as(u64, chunk_size) > max_bytes) {\n            return StreamError.MaxBytes;\n        }\n\n        // Grow state buffer first to keep chunk/refcount in sync on failure.\n        try self.ensureStateCapacity(@as(u32, @intCast(self.chunks.items.len)) + 1);\n\n        const mem = self.allocator.alloc(u8, chunk_size) catch return StreamError.OutOfMemory;\n        errdefer self.allocator.free(mem);\n        const chunk = Chunk{ .ptr = mem.ptr, .len = chunk_size };\n        self.chunks.append(self.allocator, chunk) catch return StreamError.OutOfMemory;\n        self.stats.chunks = @intCast(self.chunks.items.len);\n        if (self.attached and self.callback != null) {\n            self.emitChunkAdded(chunk);\n        }\n    }\n\n    fn ensureWritableChunkLocked(self: *Stream) StreamError!void {\n        const total = self.chunks.items.len;\n        if (total == 0) return StreamError.Invalid;\n\n        var attempts: usize = 0;\n        var index = self.current_chunk_index % total;\n        while (attempts < total) : (attempts += 1) {\n            if (self.isChunkFree(index)) {\n                self.current_chunk_index = index;\n                self.write_offset = 0;\n                self.pending_chunk_index = index;\n                self.pending_offset = 0;\n                self.pending_len = 0;\n                return;\n            }\n            index = (index + 1) % total;\n        }\n\n        if (self.options.growth_policy == @intFromEnum(GrowthPolicy.block)) {\n            return StreamError.NoSpace;\n        }\n\n        try self.addChunkLocked();\n        const new_total = self.chunks.items.len;\n        if (new_total == 0) return StreamError.Invalid;\n        self.current_chunk_index = new_total - 1;\n        self.write_offset = 0;\n        self.pending_chunk_index = self.current_chunk_index;\n        self.pending_offset = 0;\n        self.pending_len = 0;\n    }\n\n    fn emitChunkAdded(self: *Stream, chunk: Chunk) void {\n        if (self.callback) |cb| {\n            cb(@intFromPtr(self), Event.ChunkAdded, @intFromPtr(chunk.ptr), chunk.len);\n        }\n    }\n\n    fn emitDataAvailable(self: *Stream, count: u32) void {\n        if (self.callback) |cb| {\n            cb(@intFromPtr(self), Event.DataAvailable, count, 0);\n        }\n    }\n\n    fn emitStateBuffer(self: *Stream) void {\n        if (self.callback) |cb| {\n            cb(@intFromPtr(self), Event.StateBuffer, @intFromPtr(self.state_buffer.ptr), self.state_capacity);\n        }\n    }\n\n    fn emitClosed(self: *Stream) void {\n        if (self.callback) |cb| {\n            cb(@intFromPtr(self), Event.Closed, 0, 0);\n        }\n    }\n};\n\npub const default_pattern = \"\\x1b[32mnative-span-feed\\x1b[0m\\n\";\nconst span_queue_capacity_default: u32 = 4096;\nconst notify_threshold_default: u32 = 1;\n\npub const EventId = enum(u32) {\n    ChunkAdded = 2,\n    Closed = 5,\n    Error = 6,\n    DataAvailable = 7,\n    StateBuffer = 8,\n};\n\nconst Event = struct {\n    pub const ChunkAdded: u32 = @intFromEnum(EventId.ChunkAdded);\n    pub const Closed: u32 = @intFromEnum(EventId.Closed);\n    pub const Error: u32 = @intFromEnum(EventId.Error);\n    pub const DataAvailable: u32 = @intFromEnum(EventId.DataAvailable);\n    pub const StateBuffer: u32 = @intFromEnum(EventId.StateBuffer);\n};\n\npub const Status = struct {\n    pub const ok: i32 = 0;\n    pub const err_no_space: i32 = -1;\n    pub const err_max_bytes: i32 = -2;\n    pub const err_invalid: i32 = -3;\n    pub const err_alloc: i32 = -4;\n    pub const err_busy: i32 = -5;\n};\n\npub const StreamError = error{\n    NoSpace,\n    MaxBytes,\n    Invalid,\n    OutOfMemory,\n    Busy,\n};\n\npub fn defaultOptions() Options {\n    return .{\n        .chunk_size = 64 * 1024,\n        .initial_chunks = 2,\n        .max_bytes = 0,\n        .growth_policy = @intFromEnum(GrowthPolicy.grow),\n        .auto_commit_on_full = 1,\n        .span_queue_capacity = 0,\n    };\n}\n\npub fn normalizeOptions(opts: Options) Options {\n    var out = opts;\n    if (out.chunk_size == 0) out.chunk_size = 64 * 1024;\n    if (out.initial_chunks == 0) out.initial_chunks = 1;\n    if (out.span_queue_capacity == 0) out.span_queue_capacity = span_queue_capacity_default;\n    return out;\n}\n\nfn errorToStatus(err: StreamError) i32 {\n    return switch (err) {\n        StreamError.NoSpace => Status.err_no_space,\n        StreamError.MaxBytes => Status.err_max_bytes,\n        StreamError.Invalid => Status.err_invalid,\n        StreamError.OutOfMemory => Status.err_alloc,\n        StreamError.Busy => Status.err_busy,\n    };\n}\n\npub fn createNativeSpanFeedWithAllocator(allocator: std.mem.Allocator, options_ptr: ?*const Options) ?*Stream {\n    const opts = normalizeOptions(if (options_ptr) |p| p.* else defaultOptions());\n    return Stream.create(allocator, opts) catch null;\n}\n\npub export fn streamSetCallback(stream: ?*Stream, callback: ?*const CallbackFn) void {\n    if (stream == null) return;\n    stream.?.setCallback(callback);\n}\n\npub export fn attachNativeSpanFeed(stream: ?*Stream) i32 {\n    if (stream == null) return Status.err_invalid;\n    const s = stream.?;\n    s.attach() catch |err| return errorToStatus(err);\n    return Status.ok;\n}\n\npub export fn streamClose(stream: ?*Stream) i32 {\n    if (stream == null) return Status.err_invalid;\n    const s = stream.?;\n    s.close() catch |err| return errorToStatus(err);\n    return Status.ok;\n}\n\npub export fn destroyNativeSpanFeed(stream: ?*Stream) void {\n    if (stream == null) return;\n    const s = stream.?;\n    s.destroy();\n}\n\n/// Copy API: copies len bytes from src_ptr into the stream's chunk pool.\n/// Handles spanning across multiple chunks automatically. If auto_commit_on_full\n/// is enabled, commits and emits DataAvailable each time a chunk fills.\n/// Best for producers that already have data in a buffer (formatted output,\n/// serialized messages, file contents).\n/// When auto_commit_on_full is disabled, writes are all-or-nothing per\n/// chunk boundary: a write that fits in the remaining space succeeds,\n/// but a write that would exceed it returns err_no_space without writing\n/// any bytes. A write that exactly fills the chunk succeeds; the next\n/// write will move to a new chunk (committing the full one first).\npub export fn streamWrite(stream: ?*Stream, src_ptr: ?*const u8, len: usize) i32 {\n    if (stream == null or src_ptr == null) return Status.err_invalid;\n    const s = stream.?;\n    if (len == 0) return Status.ok;\n    const src = @as([*]const u8, @ptrCast(src_ptr.?))[0..len];\n    s.write(src) catch |err| return errorToStatus(err);\n    return Status.ok;\n}\n\n/// Commits the pending span accumulated by streamWrite and emits DataAvailable.\n/// Only needed when auto_commit_on_full is disabled or to flush a partially\n/// filled chunk.\npub export fn streamCommit(stream: ?*Stream) i32 {\n    if (stream == null) return Status.err_invalid;\n    const s = stream.?;\n    s.commit() catch |err| return errorToStatus(err);\n    return Status.ok;\n}\n\n/// Zero-copy API: returns a pointer and available length for direct writes\n/// into the current chunk's memory. The caller writes directly into this\n/// region (no memcpy) and then calls streamCommitReserved with the number\n/// of bytes actually written.\n/// Best for producers that can format output in place (e.g., serializing\n/// directly into the chunk buffer). Only one reservation can be active at\n/// a time; the stream is locked until streamCommitReserved is called.\n/// Returns at most one chunk's worth of available space.\npub export fn streamReserve(stream: ?*Stream, min_len: u32, out_ptr: ?*ReserveInfo) i32 {\n    if (stream == null or out_ptr == null) return Status.err_invalid;\n    const s = stream.?;\n    const info = s.reserve(min_len) catch |err| return errorToStatus(err);\n    out_ptr.?.* = info;\n    return Status.ok;\n}\n\n/// Commits len bytes of the previously reserved region and emits DataAvailable.\n/// Must be called after streamReserve. len must not exceed the reserved length.\npub export fn streamCommitReserved(stream: ?*Stream, len: u32) i32 {\n    if (stream == null) return Status.err_invalid;\n    const s = stream.?;\n    s.commitReserved(len) catch |err| return errorToStatus(err);\n    return Status.ok;\n}\n\npub export fn streamSetOptions(stream: ?*Stream, options_ptr: ?*const Options) i32 {\n    if (stream == null or options_ptr == null) return Status.err_invalid;\n    const s = stream.?;\n    s.setOptions(options_ptr.?.*) catch |err| return errorToStatus(err);\n    return Status.ok;\n}\n\npub export fn streamGetStats(stream: ?*Stream, stats_ptr: ?*Stats) i32 {\n    if (stream == null or stats_ptr == null) return Status.err_invalid;\n    const s = stream.?;\n    stats_ptr.?.* = s.getStats();\n    return Status.ok;\n}\n\npub export fn streamDrainSpans(stream: ?*Stream, out_ptr: ?*SpanInfo, max_spans: u32) u32 {\n    if (stream == null or out_ptr == null or max_spans == 0) return 0;\n    const s = stream.?;\n    const out = @as([*]SpanInfo, @ptrCast(out_ptr.?))[0..max_spans];\n    return s.drainSpans(out);\n}\n"
  },
  {
    "path": "packages/core/src/zig/renderer.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst ansi = @import(\"ansi.zig\");\nconst buf = @import(\"buffer.zig\");\nconst gp = @import(\"grapheme.zig\");\nconst link = @import(\"link.zig\");\nconst Terminal = @import(\"terminal.zig\");\nconst logger = @import(\"logger.zig\");\n\npub const RGBA = ansi.RGBA;\npub const OptimizedBuffer = buf.OptimizedBuffer;\npub const TextAttributes = ansi.TextAttributes;\npub const CursorStyle = Terminal.CursorStyle;\n\nconst CLEAR_CHAR = '\\u{0a00}';\nconst MAX_STAT_SAMPLES = 30;\nconst STAT_SAMPLE_CAPACITY = 30;\n\nconst COLOR_EPSILON_DEFAULT: f32 = 0.00001;\nconst OUTPUT_BUFFER_SIZE = 1024 * 1024 * 2; // 2MB\n\npub const RendererError = error{\n    OutOfMemory,\n    InvalidDimensions,\n    ThreadingFailed,\n    WriteFailed,\n};\n\nfn rgbaComponentToU8(component: f32) u8 {\n    if (!std.math.isFinite(component)) return 0;\n\n    const clamped = std.math.clamp(component, 0.0, 1.0);\n    return @intFromFloat(@round(clamped * 255.0));\n}\n\npub const DebugOverlayCorner = enum {\n    topLeft,\n    topRight,\n    bottomLeft,\n    bottomRight,\n};\n\npub const CliRenderer = struct {\n    width: u32,\n    height: u32,\n    currentRenderBuffer: *OptimizedBuffer,\n    nextRenderBuffer: *OptimizedBuffer,\n    pool: *gp.GraphemePool,\n    backgroundColor: RGBA,\n    renderOffset: u32,\n    terminal: Terminal,\n    testing: bool = false,\n    useAlternateScreen: bool = true,\n    terminalSetup: bool = false,\n\n    renderStats: struct {\n        lastFrameTime: f64,\n        averageFrameTime: f64,\n        frameCount: u64,\n        fps: u32,\n        cellsUpdated: u32,\n        renderTime: ?f64,\n        overallFrameTime: ?f64,\n        bufferResetTime: ?f64,\n        stdoutWriteTime: ?f64,\n        heapUsed: u32,\n        heapTotal: u32,\n        arrayBuffers: u32,\n        frameCallbackTime: ?f64,\n    },\n    statSamples: struct {\n        lastFrameTime: std.ArrayListUnmanaged(f64),\n        renderTime: std.ArrayListUnmanaged(f64),\n        overallFrameTime: std.ArrayListUnmanaged(f64),\n        bufferResetTime: std.ArrayListUnmanaged(f64),\n        stdoutWriteTime: std.ArrayListUnmanaged(f64),\n        cellsUpdated: std.ArrayListUnmanaged(u32),\n        frameCallbackTime: std.ArrayListUnmanaged(f64),\n    },\n    lastRenderTime: i64,\n    allocator: Allocator,\n    renderThread: ?std.Thread = null,\n    stdoutBuffer: [4096]u8,\n    writeOutBuf: [1024]u8 = undefined,\n    debugOverlay: struct {\n        enabled: bool,\n        corner: DebugOverlayCorner,\n    } = .{\n        .enabled = false,\n        .corner = .bottomRight,\n    },\n    // Threading\n    useThread: bool = false,\n    renderMutex: std.Thread.Mutex = .{},\n    renderCondition: std.Thread.Condition = .{},\n    renderRequested: bool = false,\n    shouldTerminate: bool = false,\n    renderInProgress: bool = false,\n    currentOutputBuffer: []u8 = &[_]u8{},\n    currentOutputLen: usize = 0,\n\n    // Hit grid for mouse event dispatch.\n    //\n    // The hit grid is a screen-sized array where each cell stores the renderable ID\n    // at that position. Mouse events query checkHit(x, y) to find which element to\n    // dispatch to.\n    //\n    // Double buffering: During render, addToHitGrid writes to nextHitGrid. After\n    // render completes, the buffers swap. This keeps hit testing consistent during\n    // a frame. Queries see the previous frame's state, not a half-built grid.\n    //\n    // On-demand sync: When scroll/translate changes between renders, the TypeScript\n    // layer can rebuild currentHitGrid directly via addToCurrentHitGridClipped. This\n    // updates hover states immediately rather than waiting for the next render.\n    //\n    // Scissor clipping: The hitScissorStack mirrors overflow:hidden regions. Elements\n    // outside their parent's visible area are excluded from hit testing. The stack\n    // uses screen coordinates. Buffered renderables need getHitGridScissorRect() to\n    // convert from buffer-local (0,0) to their actual screen position.\n    currentHitGrid: []u32,\n    nextHitGrid: []u32,\n    hitGridWidth: u32,\n    hitGridHeight: u32,\n    hitScissorStack: std.ArrayListUnmanaged(buf.ClipRect),\n    hitGridDirty: bool = false,\n\n    lastCursorStyleTag: ?u8 = null,\n    lastCursorBlinking: ?bool = null,\n    lastCursorColorRGB: ?[3]u8 = null,\n    lastMousePointerStyle: Terminal.MousePointerStyle = .default,\n\n    // Preallocated output buffer\n    var outputBuffer: [OUTPUT_BUFFER_SIZE]u8 = undefined;\n    var outputBufferLen: usize = 0;\n    var outputBufferB: [OUTPUT_BUFFER_SIZE]u8 = undefined;\n    var outputBufferBLen: usize = 0;\n    var activeBuffer: enum { A, B } = .A;\n\n    const OutputBufferWriter = struct {\n        pub fn write(_: void, data: []const u8) !usize {\n            const bufferLen = if (activeBuffer == .A) &outputBufferLen else &outputBufferBLen;\n            const buffer = if (activeBuffer == .A) &outputBuffer else &outputBufferB;\n\n            if (bufferLen.* + data.len > buffer.len) {\n                // TODO: Resize buffer when necessary\n                return error.BufferFull;\n            }\n\n            @memcpy(buffer.*[bufferLen.*..][0..data.len], data);\n            bufferLen.* += data.len;\n\n            return data.len;\n        }\n\n        // TODO: std.io.GenericWriter is deprecated, however the \"correct\" option seems to be much more involved\n        // So I have simply used GenericWriter here, and then the proper migration can be done later\n        pub fn writer() std.io.GenericWriter(void, error{BufferFull}, write) {\n            return .{ .context = {} };\n        }\n    };\n\n    pub fn create(allocator: Allocator, width: u32, height: u32, pool: *gp.GraphemePool, testing: bool) !*CliRenderer {\n        return createWithOptions(allocator, width, height, pool, testing, false);\n    }\n\n    pub fn createWithOptions(\n        allocator: Allocator,\n        width: u32,\n        height: u32,\n        pool: *gp.GraphemePool,\n        testing: bool,\n        remote: bool,\n    ) !*CliRenderer {\n        const self = try allocator.create(CliRenderer);\n        errdefer allocator.destroy(self);\n\n        const currentBuffer = try OptimizedBuffer.init(allocator, width, height, .{ .pool = pool, .width_method = .unicode, .id = \"current buffer\" });\n        errdefer currentBuffer.deinit();\n        const nextBuffer = try OptimizedBuffer.init(allocator, width, height, .{ .pool = pool, .width_method = .unicode, .id = \"next buffer\" });\n        errdefer nextBuffer.deinit();\n\n        // stat sample arrays\n        var lastFrameTime: std.ArrayListUnmanaged(f64) = .{};\n        errdefer lastFrameTime.deinit(allocator);\n        var renderTime: std.ArrayListUnmanaged(f64) = .{};\n        errdefer renderTime.deinit(allocator);\n        var overallFrameTime: std.ArrayListUnmanaged(f64) = .{};\n        errdefer overallFrameTime.deinit(allocator);\n        var bufferResetTime: std.ArrayListUnmanaged(f64) = .{};\n        errdefer bufferResetTime.deinit(allocator);\n        var stdoutWriteTime: std.ArrayListUnmanaged(f64) = .{};\n        errdefer stdoutWriteTime.deinit(allocator);\n        var cellsUpdated: std.ArrayListUnmanaged(u32) = .{};\n        errdefer cellsUpdated.deinit(allocator);\n        var frameCallbackTimes: std.ArrayListUnmanaged(f64) = .{};\n        errdefer frameCallbackTimes.deinit(allocator);\n\n        try lastFrameTime.ensureTotalCapacity(allocator, STAT_SAMPLE_CAPACITY);\n        try renderTime.ensureTotalCapacity(allocator, STAT_SAMPLE_CAPACITY);\n        try overallFrameTime.ensureTotalCapacity(allocator, STAT_SAMPLE_CAPACITY);\n        try bufferResetTime.ensureTotalCapacity(allocator, STAT_SAMPLE_CAPACITY);\n        try stdoutWriteTime.ensureTotalCapacity(allocator, STAT_SAMPLE_CAPACITY);\n        try cellsUpdated.ensureTotalCapacity(allocator, STAT_SAMPLE_CAPACITY);\n        try frameCallbackTimes.ensureTotalCapacity(allocator, STAT_SAMPLE_CAPACITY);\n\n        const hitGridSize = width * height;\n        const currentHitGrid = try allocator.alloc(u32, hitGridSize);\n        errdefer allocator.free(currentHitGrid);\n        const nextHitGrid = try allocator.alloc(u32, hitGridSize);\n        errdefer allocator.free(nextHitGrid);\n        @memset(currentHitGrid, 0); // Initialize with 0 (no renderable)\n        @memset(nextHitGrid, 0);\n        const hitScissorStack: std.ArrayListUnmanaged(buf.ClipRect) = .{};\n\n        self.* = .{\n            .width = width,\n            .height = height,\n            .currentRenderBuffer = currentBuffer,\n            .nextRenderBuffer = nextBuffer,\n            .pool = pool,\n            .backgroundColor = .{ 0.0, 0.0, 0.0, 0.0 },\n            .renderOffset = 0,\n            .terminal = Terminal.init(.{ .remote = remote }),\n            .testing = testing,\n            .lastCursorStyleTag = null,\n            .lastCursorBlinking = null,\n            .lastCursorColorRGB = null,\n\n            .renderStats = .{\n                .lastFrameTime = 0,\n                .averageFrameTime = 0,\n                .frameCount = 0,\n                .fps = 0,\n                .cellsUpdated = 0,\n                .renderTime = null,\n                .overallFrameTime = null,\n                .bufferResetTime = null,\n                .stdoutWriteTime = null,\n                .heapUsed = 0,\n                .heapTotal = 0,\n                .arrayBuffers = 0,\n                .frameCallbackTime = null,\n            },\n            .statSamples = .{\n                .lastFrameTime = lastFrameTime,\n                .renderTime = renderTime,\n                .overallFrameTime = overallFrameTime,\n                .bufferResetTime = bufferResetTime,\n                .stdoutWriteTime = stdoutWriteTime,\n                .cellsUpdated = cellsUpdated,\n                .frameCallbackTime = frameCallbackTimes,\n            },\n            .lastRenderTime = std.time.microTimestamp(),\n            .allocator = allocator,\n            .stdoutBuffer = undefined,\n            .currentHitGrid = currentHitGrid,\n            .nextHitGrid = nextHitGrid,\n            .hitGridWidth = width,\n            .hitGridHeight = height,\n            .hitScissorStack = hitScissorStack,\n        };\n\n        nextBuffer.setBlendBackdropColor(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], 1.0 });\n\n        try currentBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3] }, CLEAR_CHAR);\n        try nextBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3] }, null);\n\n        return self;\n    }\n\n    pub fn destroy(self: *CliRenderer) void {\n        self.renderMutex.lock();\n        while (self.renderInProgress) {\n            self.renderCondition.wait(&self.renderMutex);\n        }\n\n        self.shouldTerminate = true;\n        self.renderRequested = true;\n        self.renderCondition.signal();\n        self.renderMutex.unlock();\n\n        if (self.renderThread) |thread| {\n            thread.join();\n        }\n\n        self.performShutdownSequence();\n        self.terminal.deinit();\n\n        self.currentRenderBuffer.deinit();\n        self.nextRenderBuffer.deinit();\n\n        // Free stat sample arrays\n        self.statSamples.lastFrameTime.deinit(self.allocator);\n        self.statSamples.renderTime.deinit(self.allocator);\n        self.statSamples.overallFrameTime.deinit(self.allocator);\n        self.statSamples.bufferResetTime.deinit(self.allocator);\n        self.statSamples.stdoutWriteTime.deinit(self.allocator);\n        self.statSamples.cellsUpdated.deinit(self.allocator);\n        self.statSamples.frameCallbackTime.deinit(self.allocator);\n\n        self.allocator.free(self.currentHitGrid);\n        self.allocator.free(self.nextHitGrid);\n        self.hitScissorStack.deinit(self.allocator);\n\n        self.allocator.destroy(self);\n    }\n\n    pub fn setupTerminal(self: *CliRenderer, useAlternateScreen: bool) void {\n        self.useAlternateScreen = useAlternateScreen;\n        self.terminalSetup = true;\n\n        var stdoutWriter = std.fs.File.stdout().writer(&self.stdoutBuffer);\n        const writer = &stdoutWriter.interface;\n\n        self.terminal.queryTerminalSend(writer) catch {\n            logger.warn(\"Failed to query terminal capabilities\", .{});\n        };\n        writer.flush() catch {};\n\n        self.setupTerminalWithoutDetection(useAlternateScreen);\n    }\n\n    fn setupTerminalWithoutDetection(self: *CliRenderer, useAlternateScreen: bool) void {\n        var stdoutWriter = std.fs.File.stdout().writer(&self.stdoutBuffer);\n        const writer = &stdoutWriter.interface;\n\n        writer.writeAll(ansi.ANSI.saveCursorState) catch {};\n\n        if (useAlternateScreen) {\n            self.terminal.enterAltScreen(writer) catch {};\n        } else {\n            ansi.ANSI.makeRoomForRendererOutput(writer, @max(self.height, 1)) catch {};\n        }\n\n        self.terminal.setCursorPosition(1, 1, false);\n        const useKitty = self.terminal.opts.kitty_keyboard_flags > 0;\n        self.terminal.enableDetectedFeatures(writer, useKitty) catch {};\n\n        writer.flush() catch {};\n    }\n\n    pub fn suspendRenderer(self: *CliRenderer) void {\n        if (!self.terminalSetup) return;\n        self.performShutdownSequence();\n    }\n\n    pub fn resumeRenderer(self: *CliRenderer) void {\n        if (!self.terminalSetup) return;\n        self.setupTerminalWithoutDetection(self.useAlternateScreen);\n    }\n\n    pub fn performShutdownSequence(self: *CliRenderer) void {\n        if (!self.terminalSetup) return;\n\n        var stdoutWriter = std.fs.File.stdout().writer(&self.stdoutBuffer);\n        const direct = &stdoutWriter.interface;\n        self.terminal.resetState(direct) catch {\n            logger.warn(\"Failed to reset terminal state\", .{});\n        };\n\n        if (self.useAlternateScreen) {\n            direct.flush() catch {};\n        } else if (self.renderOffset == 0) {\n            direct.writeAll(\"\\x1b[H\\x1b[J\") catch {};\n            direct.flush() catch {};\n        } else if (self.renderOffset > 0) {\n            // Currently still handled in typescript\n            // const consoleEndLine = self.height - self.renderOffset;\n            // ansi.ANSI.moveToOutput(direct, 1, consoleEndLine) catch {};\n        }\n\n        // NOTE: This messes up state after shutdown, but might be necessary for windows?\n        // direct.writeAll(ansi.ANSI.restoreCursorState) catch {};\n\n        direct.writeAll(ansi.ANSI.resetCursorColorFallback) catch {};\n        direct.writeAll(ansi.ANSI.resetCursorColor) catch {};\n        direct.writeAll(ansi.ANSI.defaultCursorStyle) catch {};\n        // Workaround for Ghostty not showing the cursor after shutdown for some reason\n        direct.writeAll(ansi.ANSI.showCursor) catch {};\n        direct.flush() catch {};\n        std.Thread.sleep(10 * std.time.ns_per_ms);\n        direct.writeAll(ansi.ANSI.showCursor) catch {};\n        direct.flush() catch {};\n        std.Thread.sleep(10 * std.time.ns_per_ms);\n    }\n\n    fn addStatSample(self: *CliRenderer, comptime T: type, samples: *std.ArrayListUnmanaged(T), value: T) void {\n        samples.append(self.allocator, value) catch return;\n\n        if (samples.items.len > MAX_STAT_SAMPLES) {\n            _ = samples.orderedRemove(0);\n        }\n    }\n\n    fn getStatAverage(comptime T: type, samples: *const std.ArrayListUnmanaged(T)) T {\n        if (samples.items.len == 0) {\n            return 0;\n        }\n\n        var sum: T = 0;\n        for (samples.items) |value| {\n            sum += value;\n        }\n\n        if (@typeInfo(T) == .float) {\n            return sum / @as(T, @floatFromInt(samples.items.len));\n        } else {\n            return sum / @as(T, @intCast(samples.items.len));\n        }\n    }\n\n    pub fn setUseThread(self: *CliRenderer, useThread: bool) void {\n        if (self.useThread == useThread) return;\n\n        if (useThread) {\n            if (self.renderThread == null) {\n                self.renderThread = std.Thread.spawn(.{}, renderThreadFn, .{self}) catch |err| {\n                    std.log.warn(\"Failed to spawn render thread: {}, falling back to non-threaded mode\", .{err});\n                    self.useThread = false;\n                    return;\n                };\n            }\n        } else {\n            if (self.renderThread) |thread| {\n                // Signal the render thread to terminate (same pattern as destroy)\n                self.renderMutex.lock();\n                while (self.renderInProgress) {\n                    self.renderCondition.wait(&self.renderMutex);\n                }\n                self.shouldTerminate = true;\n                self.renderRequested = true;\n                self.renderCondition.signal();\n                self.renderMutex.unlock();\n\n                thread.join();\n                self.renderThread = null;\n\n                // Reset termination flag so thread can be re-enabled later\n                self.shouldTerminate = false;\n            }\n        }\n\n        self.useThread = useThread;\n    }\n\n    pub fn updateStats(self: *CliRenderer, time: f64, fps: u32, frameCallbackTime: f64) void {\n        self.renderStats.overallFrameTime = time;\n        self.renderStats.fps = fps;\n        self.renderStats.frameCallbackTime = frameCallbackTime;\n\n        self.addStatSample(f64, &self.statSamples.overallFrameTime, time);\n        self.addStatSample(f64, &self.statSamples.frameCallbackTime, frameCallbackTime);\n    }\n\n    pub fn updateMemoryStats(self: *CliRenderer, heapUsed: u32, heapTotal: u32, arrayBuffers: u32) void {\n        self.renderStats.heapUsed = heapUsed;\n        self.renderStats.heapTotal = heapTotal;\n        self.renderStats.arrayBuffers = arrayBuffers;\n    }\n\n    pub fn resize(self: *CliRenderer, width: u32, height: u32) !void {\n        if (self.width == width and self.height == height) return;\n\n        self.width = width;\n        self.height = height;\n\n        try self.currentRenderBuffer.resize(width, height);\n        try self.nextRenderBuffer.resize(width, height);\n        self.nextRenderBuffer.setBlendBackdropColor(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], 1.0 });\n\n        try self.currentRenderBuffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, CLEAR_CHAR);\n        try self.nextRenderBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3] }, null);\n\n        const newHitGridSize = width * height;\n        const currentHitGridSize = self.hitGridWidth * self.hitGridHeight;\n        if (newHitGridSize > currentHitGridSize) {\n            const newCurrentHitGrid = try self.allocator.alloc(u32, newHitGridSize);\n            errdefer self.allocator.free(newCurrentHitGrid);\n            const newNextHitGrid = try self.allocator.alloc(u32, newHitGridSize);\n            @memset(newCurrentHitGrid, 0);\n            @memset(newNextHitGrid, 0);\n\n            self.allocator.free(self.currentHitGrid);\n            self.allocator.free(self.nextHitGrid);\n            self.currentHitGrid = newCurrentHitGrid;\n            self.nextHitGrid = newNextHitGrid;\n        }\n\n        // Always update dimensions. The backing buffer is at least as large as\n        // width*height, so this is safe even when the terminal shrinks. Without\n        // this, checkHit keeps using stale dimensions after a shrink and returns\n        // 0 for any coordinate beyond the old bounds.\n        self.hitGridWidth = width;\n        self.hitGridHeight = height;\n\n        const cursor = self.terminal.getCursorPosition();\n        self.terminal.setCursorPosition(@min(cursor.x, width), @min(cursor.y, height), cursor.visible);\n    }\n\n    pub fn setBackgroundColor(self: *CliRenderer, rgba: RGBA) void {\n        self.backgroundColor = rgba;\n        self.nextRenderBuffer.setBlendBackdropColor(.{ rgba[0], rgba[1], rgba[2], 1.0 });\n    }\n\n    pub fn setRenderOffset(self: *CliRenderer, offset: u32) void {\n        self.renderOffset = offset;\n    }\n\n    fn renderThreadFn(self: *CliRenderer) void {\n        while (true) {\n            self.renderMutex.lock();\n            while (!self.renderRequested and !self.shouldTerminate) {\n                self.renderCondition.wait(&self.renderMutex);\n            }\n\n            if (self.shouldTerminate and !self.renderRequested) {\n                self.renderMutex.unlock();\n                break;\n            }\n\n            self.renderRequested = false;\n\n            const outputData = self.currentOutputBuffer;\n            const outputLen = self.currentOutputLen;\n\n            const writeStart = std.time.microTimestamp();\n\n            if (outputLen > 0 and !self.testing) {\n                var stdoutWriter = std.fs.File.stdout().writer(&self.stdoutBuffer);\n                const w = &stdoutWriter.interface;\n                w.writeAll(outputData[0..outputLen]) catch {};\n                w.flush() catch {};\n            }\n\n            // Signal that rendering is complete\n            self.renderStats.stdoutWriteTime = @as(f64, @floatFromInt(std.time.microTimestamp() - writeStart));\n            self.renderInProgress = false;\n            self.renderCondition.signal();\n            self.renderMutex.unlock();\n        }\n    }\n\n    // Render once with current state\n    pub fn render(self: *CliRenderer, force: bool) void {\n        const now = std.time.microTimestamp();\n        const deltaTimeMs = @as(f64, @floatFromInt(now - self.lastRenderTime));\n        const deltaTime = deltaTimeMs / 1000.0; // Convert to seconds\n\n        self.lastRenderTime = now;\n        self.renderDebugOverlay();\n\n        self.prepareRenderFrame(force);\n\n        if (self.useThread) {\n            self.renderMutex.lock();\n            while (self.renderInProgress) {\n                self.renderCondition.wait(&self.renderMutex);\n            }\n\n            if (activeBuffer == .A) {\n                activeBuffer = .B;\n                self.currentOutputBuffer = &outputBuffer;\n                self.currentOutputLen = outputBufferLen;\n            } else {\n                activeBuffer = .A;\n                self.currentOutputBuffer = &outputBufferB;\n                self.currentOutputLen = outputBufferBLen;\n            }\n\n            self.renderRequested = true;\n            self.renderInProgress = true;\n            self.renderCondition.signal();\n            self.renderMutex.unlock();\n        } else {\n            const writeStart = std.time.microTimestamp();\n            if (!self.testing) {\n                var stdoutWriter = std.fs.File.stdout().writer(&self.stdoutBuffer);\n                const w = &stdoutWriter.interface;\n                w.writeAll(outputBuffer[0..outputBufferLen]) catch {};\n                w.flush() catch {};\n            }\n            self.renderStats.stdoutWriteTime = @as(f64, @floatFromInt(std.time.microTimestamp() - writeStart));\n        }\n\n        self.renderStats.lastFrameTime = deltaTime * 1000.0;\n        self.renderStats.frameCount += 1;\n\n        self.addStatSample(f64, &self.statSamples.lastFrameTime, deltaTime * 1000.0);\n        if (self.renderStats.renderTime) |rt| {\n            self.addStatSample(f64, &self.statSamples.renderTime, rt);\n        }\n        if (self.renderStats.bufferResetTime) |brt| {\n            self.addStatSample(f64, &self.statSamples.bufferResetTime, brt);\n        }\n        if (self.renderStats.stdoutWriteTime) |swt| {\n            self.addStatSample(f64, &self.statSamples.stdoutWriteTime, swt);\n        }\n        self.addStatSample(u32, &self.statSamples.cellsUpdated, self.renderStats.cellsUpdated);\n    }\n\n    pub fn getNextBuffer(self: *CliRenderer) *OptimizedBuffer {\n        return self.nextRenderBuffer;\n    }\n\n    pub fn getCurrentBuffer(self: *CliRenderer) *OptimizedBuffer {\n        return self.currentRenderBuffer;\n    }\n\n    fn prepareRenderFrame(self: *CliRenderer, force: bool) void {\n        const renderStartTime = std.time.microTimestamp();\n        var cellsUpdated: u32 = 0;\n\n        if (activeBuffer == .A) {\n            outputBufferLen = 0;\n        } else {\n            outputBufferBLen = 0;\n        }\n\n        var writer = OutputBufferWriter.writer();\n\n        writer.writeAll(ansi.ANSI.syncSet) catch {};\n        writer.writeAll(ansi.ANSI.hideCursor) catch {};\n\n        var currentFg: ?RGBA = null;\n        var currentBg: ?RGBA = null;\n        var currentAttributes: i32 = -1;\n        var currentLinkId: u32 = 0;\n        var utf8Buf: [4]u8 = undefined;\n\n        const colorEpsilon: f32 = COLOR_EPSILON_DEFAULT;\n        const hyperlinksEnabled = self.terminal.getCapabilities().hyperlinks;\n\n        for (0..self.height) |uy| {\n            const y = @as(u32, @intCast(uy));\n\n            var runStart: i64 = -1;\n            var runLength: u32 = 0;\n\n            for (0..self.width) |ux| {\n                const x = @as(u32, @intCast(ux));\n                const currentCell = self.currentRenderBuffer.get(x, y);\n                const nextCell = self.nextRenderBuffer.get(x, y);\n\n                if (currentCell == null or nextCell == null) continue;\n\n                if (!force) {\n                    const charEqual = currentCell.?.char == nextCell.?.char;\n                    const attrEqual = currentCell.?.attributes == nextCell.?.attributes;\n\n                    if (charEqual and attrEqual and\n                        buf.rgbaEqual(currentCell.?.fg, nextCell.?.fg, colorEpsilon) and\n                        buf.rgbaEqual(currentCell.?.bg, nextCell.?.bg, colorEpsilon))\n                    {\n                        if (runLength > 0) {\n                            writer.writeAll(ansi.ANSI.reset) catch {};\n                            runStart = -1;\n                            runLength = 0;\n                        }\n                        continue;\n                    }\n                }\n\n                const cell = nextCell.?;\n\n                const fgMatch = currentFg != null and buf.rgbaEqual(currentFg.?, cell.fg, colorEpsilon);\n                const bgMatch = currentBg != null and buf.rgbaEqual(currentBg.?, cell.bg, colorEpsilon);\n                const sameAttributes = fgMatch and bgMatch and @as(i32, @intCast(cell.attributes)) == currentAttributes;\n\n                const linkId = if (hyperlinksEnabled) ansi.TextAttributes.getLinkId(cell.attributes) else 0;\n\n                if (hyperlinksEnabled and linkId != currentLinkId) {\n                    if (currentLinkId != 0) {\n                        writer.writeAll(\"\\x1b]8;;\\x1b\\\\\") catch {};\n                    }\n                    currentLinkId = linkId;\n                    if (currentLinkId != 0) {\n                        const lp = link.initGlobalLinkPool(self.allocator);\n                        if (lp.get(currentLinkId)) |url_bytes| {\n                            writer.print(\"\\x1b]8;id={d};{s}\\x1b\\\\\", .{ currentLinkId, url_bytes }) catch {};\n                        } else |_| {\n                            // Link not found, treat as no link\n                            currentLinkId = 0;\n                        }\n                    }\n                }\n\n                if (!sameAttributes or runStart == -1) {\n                    if (runLength > 0) {\n                        writer.writeAll(ansi.ANSI.reset) catch {};\n                    }\n\n                    runStart = @intCast(x);\n                    runLength = 0;\n\n                    currentFg = cell.fg;\n                    currentBg = cell.bg;\n                    currentAttributes = @as(i32, @intCast(cell.attributes));\n\n                    ansi.ANSI.moveToOutput(writer, x + 1, y + 1 + self.renderOffset) catch {};\n\n                    const fgR = rgbaComponentToU8(cell.fg[0]);\n                    const fgG = rgbaComponentToU8(cell.fg[1]);\n                    const fgB = rgbaComponentToU8(cell.fg[2]);\n\n                    const bgR = rgbaComponentToU8(cell.bg[0]);\n                    const bgG = rgbaComponentToU8(cell.bg[1]);\n                    const bgB = rgbaComponentToU8(cell.bg[2]);\n                    const bgA = cell.bg[3];\n\n                    ansi.ANSI.fgColorOutput(writer, fgR, fgG, fgB) catch {};\n\n                    // If alpha is 0 (transparent), use terminal default background instead of black\n                    if (bgA < 0.001) {\n                        writer.writeAll(\"\\x1b[49m\") catch {};\n                    } else {\n                        ansi.ANSI.bgColorOutput(writer, bgR, bgG, bgB) catch {};\n                    }\n\n                    ansi.TextAttributes.applyAttributesOutputWriter(writer, cell.attributes) catch {};\n                }\n\n                // Handle grapheme characters\n                if (gp.isGraphemeChar(cell.char)) {\n                    const gid: u32 = gp.graphemeIdFromChar(cell.char);\n                    const bytes = self.pool.get(gid) catch |err| {\n                        self.performShutdownSequence();\n                        std.debug.panic(\"Fatal: no grapheme bytes in pool for gid {d}: {}\", .{ gid, err });\n                    };\n                    if (bytes.len > 0) {\n                        const capabilities = self.terminal.getCapabilities();\n                        const graphemeWidth = gp.charRightExtent(cell.char) + 1;\n                        if (capabilities.explicit_width) {\n                            ansi.ANSI.explicitWidthOutput(writer, graphemeWidth, bytes) catch {};\n                        } else {\n                            writer.writeAll(bytes) catch {};\n                            if (capabilities.explicit_cursor_positioning) {\n                                const nextX = x + graphemeWidth;\n                                if (nextX < self.width) {\n                                    ansi.ANSI.moveToOutput(writer, nextX + 1, y + 1 + self.renderOffset) catch {};\n                                }\n                            }\n                        }\n                    }\n                } else if (gp.isContinuationChar(cell.char)) {\n                    // Write a space for continuation cells to clear any previous content\n                    writer.writeByte(' ') catch {};\n                } else {\n                    const len = std.unicode.utf8Encode(@intCast(cell.char), &utf8Buf) catch 1;\n                    writer.writeAll(utf8Buf[0..len]) catch {};\n                }\n                runLength += 1;\n\n                // Sync this cell to the current buffer so the next frame's diff\n                // is correct. Use syncCell (set without span cleanup) because\n                // span cleanup would destroy continuation cells written by an\n                // earlier iteration of this same left-to-right pass (#723).\n                self.currentRenderBuffer.syncCell(x, y, nextCell.?);\n\n                cellsUpdated += 1;\n            }\n        }\n\n        if (hyperlinksEnabled and currentLinkId != 0) {\n            writer.writeAll(\"\\x1b]8;;\\x1b\\\\\") catch {};\n        }\n\n        writer.writeAll(ansi.ANSI.reset) catch {};\n\n        const cursorPos = self.terminal.getCursorPosition();\n        const cursorStyle = self.terminal.getCursorStyle();\n        const cursorColor = self.terminal.getCursorColor();\n\n        if (cursorPos.visible) {\n            var cursorStyleCode: []const u8 = undefined;\n\n            switch (cursorStyle.style) {\n                .block => {\n                    cursorStyleCode = if (cursorStyle.blinking)\n                        ansi.ANSI.cursorBlockBlink\n                    else\n                        ansi.ANSI.cursorBlock;\n                },\n                .line => {\n                    cursorStyleCode = if (cursorStyle.blinking)\n                        ansi.ANSI.cursorLineBlink\n                    else\n                        ansi.ANSI.cursorLine;\n                },\n                .underline => {\n                    cursorStyleCode = if (cursorStyle.blinking)\n                        ansi.ANSI.cursorUnderlineBlink\n                    else\n                        ansi.ANSI.cursorUnderline;\n                },\n                .default => {\n                    cursorStyleCode = ansi.ANSI.defaultCursorStyle;\n                },\n            }\n\n            const cursorR = rgbaComponentToU8(cursorColor[0]);\n            const cursorG = rgbaComponentToU8(cursorColor[1]);\n            const cursorB = rgbaComponentToU8(cursorColor[2]);\n\n            const styleTag: u8 = @intFromEnum(cursorStyle.style);\n            const styleChanged = (self.lastCursorStyleTag == null or self.lastCursorStyleTag.? != styleTag) or\n                (self.lastCursorBlinking == null or self.lastCursorBlinking.? != cursorStyle.blinking);\n            const colorChanged = (self.lastCursorColorRGB == null or self.lastCursorColorRGB.?[0] != cursorR or self.lastCursorColorRGB.?[1] != cursorG or self.lastCursorColorRGB.?[2] != cursorB);\n\n            if (colorChanged) {\n                ansi.ANSI.cursorColorOutputWriter(writer, cursorR, cursorG, cursorB) catch {};\n                self.lastCursorColorRGB = .{ cursorR, cursorG, cursorB };\n            }\n            if (styleChanged) {\n                writer.writeAll(cursorStyleCode) catch {};\n                self.lastCursorStyleTag = styleTag;\n                self.lastCursorBlinking = cursorStyle.blinking;\n            }\n            ansi.ANSI.moveToOutput(writer, cursorPos.x, cursorPos.y + self.renderOffset) catch {};\n            writer.writeAll(ansi.ANSI.showCursor) catch {};\n        } else {\n            writer.writeAll(ansi.ANSI.hideCursor) catch {};\n            self.lastCursorStyleTag = null;\n            self.lastCursorBlinking = null;\n            self.lastCursorColorRGB = null;\n        }\n\n        const mousePointer = self.terminal.getMousePointer();\n        if (mousePointer != self.lastMousePointerStyle) {\n            ansi.ANSI.setMousePointerOutput(writer, mousePointer.toName()) catch {};\n            self.lastMousePointerStyle = mousePointer;\n        }\n\n        writer.writeAll(ansi.ANSI.syncReset) catch {};\n\n        const renderEndTime = std.time.microTimestamp();\n        const renderTime = @as(f64, @floatFromInt(renderEndTime - renderStartTime));\n\n        self.renderStats.cellsUpdated = cellsUpdated;\n        self.renderStats.renderTime = renderTime;\n\n        self.nextRenderBuffer.clear(.{ self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2], self.backgroundColor[3] }, null) catch {};\n\n        // Compare hit grids before swap to detect changes. This allows TypeScript to\n        // know if hover state needs rechecking without manually tracking dirty state.\n        self.hitGridDirty = !std.mem.eql(u32, self.currentHitGrid, self.nextHitGrid);\n\n        // Swap hit grids: nextHitGrid (built this frame) becomes the active grid for\n        // hit testing. The old currentHitGrid becomes nextHitGrid and is cleared for\n        // the next frame.\n        const temp = self.currentHitGrid;\n        self.currentHitGrid = self.nextHitGrid;\n        self.nextHitGrid = temp;\n        @memset(self.nextHitGrid, 0);\n    }\n\n    pub fn setDebugOverlay(self: *CliRenderer, enabled: bool, corner: DebugOverlayCorner) void {\n        self.debugOverlay.enabled = enabled;\n        self.debugOverlay.corner = corner;\n    }\n\n    pub fn clearTerminal(self: *CliRenderer) void {\n        self.writeOut(ansi.ANSI.clearAndHome);\n    }\n\n    pub fn writeOut(self: *CliRenderer, data: []const u8) void {\n        if (data.len == 0) return;\n        if (self.testing) return;\n\n        if (self.useThread) {\n            self.renderMutex.lock();\n            while (self.renderInProgress) {\n                self.renderCondition.wait(&self.renderMutex);\n            }\n            self.renderMutex.unlock();\n        }\n\n        var stdoutWriter = std.fs.File.stdout().writer(&self.stdoutBuffer);\n        const w = &stdoutWriter.interface;\n        w.writeAll(data) catch {};\n        w.flush() catch {};\n    }\n\n    pub fn writeOutMultiple(self: *CliRenderer, data_slices: []const []const u8) void {\n        if (self.testing) return;\n\n        if (self.useThread) {\n            self.renderMutex.lock();\n            while (self.renderInProgress) {\n                self.renderCondition.wait(&self.renderMutex);\n            }\n            self.renderMutex.unlock();\n        }\n\n        var totalLen: usize = 0;\n        for (data_slices) |slice| {\n            totalLen += slice.len;\n        }\n\n        if (totalLen == 0) return;\n\n        var stdoutWriter = std.fs.File.stdout().writer(&self.stdoutBuffer);\n        const w = &stdoutWriter.interface;\n        for (data_slices) |slice| {\n            w.writeAll(slice) catch {};\n        }\n        w.flush() catch {};\n    }\n\n    /// Write a renderable's bounds to nextHitGrid for the upcoming frame.\n    ///\n    /// Called during render for each visible renderable. The rect is clipped to\n    /// the current hit scissor stack, so elements inside overflow:hidden parents\n    /// only register hits within the visible region. Later renderables overwrite\n    /// earlier ones. Z-order is determined by render order.\n    pub fn addToHitGrid(self: *CliRenderer, x: i32, y: i32, width: u32, height: u32, id: u32) void {\n        const clipped = self.clipRectToHitScissor(x, y, width, height) orelse return;\n        const startX = @max(0, clipped.x);\n        const startY = @max(0, clipped.y);\n        const endX = @min(\n            @as(i32, @intCast(self.hitGridWidth)),\n            clipped.x + @as(i32, @intCast(clipped.width)),\n        );\n        const endY = @min(\n            @as(i32, @intCast(self.hitGridHeight)),\n            clipped.y + @as(i32, @intCast(clipped.height)),\n        );\n\n        if (startX >= endX or startY >= endY) return;\n\n        const uStartX: u32 = @intCast(startX);\n        const uStartY: u32 = @intCast(startY);\n        const uEndX: u32 = @intCast(endX);\n        const uEndY: u32 = @intCast(endY);\n\n        for (uStartY..uEndY) |row| {\n            const rowStart = row * self.hitGridWidth;\n            const startIdx = rowStart + uStartX;\n            const endIdx = rowStart + uEndX;\n\n            @memset(self.nextHitGrid[startIdx..endIdx], id);\n        }\n    }\n\n    /// Clear currentHitGrid before an immediate rebuild.\n    ///\n    /// Used by syncHitGridIfNeeded in TypeScript when scroll/translate changes\n    /// require updating hit targets without waiting for the next render.\n    pub fn clearCurrentHitGrid(self: *CliRenderer) void {\n        @memset(self.currentHitGrid, 0);\n    }\n\n    /// Return whether the hit grid changed during the last render.\n    /// This is set by comparing the previous and current hit grids after render.\n    /// TypeScript can use this to decide if hover state needs rechecking.\n    pub fn getHitGridDirty(self: *CliRenderer) bool {\n        return self.hitGridDirty;\n    }\n\n    /// Return the renderable ID at screen position (x, y), or 0 if none.\n    pub fn checkHit(self: *CliRenderer, x: u32, y: u32) u32 {\n        if (x >= self.hitGridWidth or y >= self.hitGridHeight) {\n            return 0;\n        }\n\n        const index = y * self.hitGridWidth + x;\n        return self.currentHitGrid[index];\n    }\n\n    /// Return the current (topmost) hit scissor rect, or null if the stack is empty.\n    fn getCurrentHitScissorRect(self: *const CliRenderer) ?buf.ClipRect {\n        if (self.hitScissorStack.items.len == 0) return null;\n        return self.hitScissorStack.items[self.hitScissorStack.items.len - 1];\n    }\n\n    /// Intersect a rect with the current hit scissor. Returns null if fully clipped.\n    fn clipRectToHitScissor(self: *const CliRenderer, x: i32, y: i32, width: u32, height: u32) ?buf.ClipRect {\n        const scissor = self.getCurrentHitScissorRect() orelse return buf.ClipRect{\n            .x = x,\n            .y = y,\n            .width = width,\n            .height = height,\n        };\n\n        const rect_end_x = x + @as(i32, @intCast(width));\n        const rect_end_y = y + @as(i32, @intCast(height));\n        const scissor_end_x = scissor.x + @as(i32, @intCast(scissor.width));\n        const scissor_end_y = scissor.y + @as(i32, @intCast(scissor.height));\n\n        const intersect_x = @max(x, scissor.x);\n        const intersect_y = @max(y, scissor.y);\n        const intersect_end_x = @min(rect_end_x, scissor_end_x);\n        const intersect_end_y = @min(rect_end_y, scissor_end_y);\n\n        if (intersect_x >= intersect_end_x or intersect_y >= intersect_end_y) {\n            return null;\n        }\n\n        return buf.ClipRect{\n            .x = intersect_x,\n            .y = intersect_y,\n            .width = @intCast(intersect_end_x - intersect_x),\n            .height = @intCast(intersect_end_y - intersect_y),\n        };\n    }\n\n    /// Push a scissor rect for hit grid clipping.\n    ///\n    /// The rect is intersected with any existing scissor, so nested overflow:hidden\n    /// containers compound correctly. All coordinates are in screen space.\n    pub fn hitGridPushScissorRect(self: *CliRenderer, x: i32, y: i32, width: u32, height: u32) void {\n        var rect = buf.ClipRect{\n            .x = x,\n            .y = y,\n            .width = width,\n            .height = height,\n        };\n\n        if (self.getCurrentHitScissorRect() != null) {\n            const intersect = self.clipRectToHitScissor(rect.x, rect.y, rect.width, rect.height);\n            if (intersect) |clipped| {\n                rect = clipped;\n            } else {\n                rect = buf.ClipRect{ .x = 0, .y = 0, .width = 0, .height = 0 };\n            }\n        }\n\n        self.hitScissorStack.append(self.allocator, rect) catch |err| {\n            logger.warn(\"Failed to push hit-grid scissor rect: {}\", .{err});\n        };\n    }\n\n    pub fn hitGridPopScissorRect(self: *CliRenderer) void {\n        if (self.hitScissorStack.items.len > 0) {\n            _ = self.hitScissorStack.pop();\n        }\n    }\n\n    /// Clear all hit grid scissors. Called at start of render to reset state.\n    pub fn hitGridClearScissorRects(self: *CliRenderer) void {\n        self.hitScissorStack.clearRetainingCapacity();\n    }\n\n    /// Write directly to currentHitGrid with scissor clipping.\n    ///\n    /// Used for immediate hit grid sync when scroll/translate changes. Unlike\n    /// addToHitGrid (which writes to nextHitGrid for the upcoming frame), this\n    /// updates the grid that checkHit reads right now. Lets hover states update\n    /// without waiting for the next render.\n    pub fn addToCurrentHitGridClipped(self: *CliRenderer, x: i32, y: i32, width: u32, height: u32, id: u32) void {\n        const clipped = self.clipRectToHitScissor(x, y, width, height) orelse return;\n\n        const startX = @max(0, clipped.x);\n        const startY = @max(0, clipped.y);\n        const endX = @min(@as(i32, @intCast(self.hitGridWidth)), clipped.x + @as(i32, @intCast(clipped.width)));\n        const endY = @min(@as(i32, @intCast(self.hitGridHeight)), clipped.y + @as(i32, @intCast(clipped.height)));\n\n        if (startX >= endX or startY >= endY) return;\n\n        const uStartX: u32 = @intCast(startX);\n        const uStartY: u32 = @intCast(startY);\n        const uEndX: u32 = @intCast(endX);\n        const uEndY: u32 = @intCast(endY);\n\n        for (uStartY..uEndY) |row| {\n            const rowStart = row * self.hitGridWidth;\n            const startIdx = rowStart + uStartX;\n            const endIdx = rowStart + uEndX;\n\n            @memset(self.currentHitGrid[startIdx..endIdx], id);\n        }\n    }\n\n    pub fn dumpHitGrid(self: *CliRenderer) void {\n        const timestamp = std.time.timestamp();\n        var filename_buf: [64]u8 = undefined;\n        const filename = std.fmt.bufPrint(&filename_buf, \"hitgrid_{d}.txt\", .{timestamp}) catch return;\n\n        const file = std.fs.cwd().createFile(filename, .{}) catch return;\n        defer file.close();\n\n        var fileBuffer: [4096]u8 = undefined;\n        var fileWriter = file.writer(&fileBuffer);\n        const writer = &fileWriter.interface;\n\n        for (0..self.hitGridHeight) |y| {\n            for (0..self.hitGridWidth) |x| {\n                const index = y * self.hitGridWidth + x;\n                const id = self.currentHitGrid[index];\n\n                const char = if (id == 0) '.' else ('0' + @as(u8, @intCast(id % 10)));\n                writer.writeByte(char) catch return;\n            }\n            writer.writeByte('\\n') catch return;\n        }\n        writer.flush() catch {};\n    }\n\n    fn dumpSingleBuffer(self: *CliRenderer, buffer: *OptimizedBuffer, buffer_name: []const u8, timestamp: i64) void {\n        std.fs.cwd().makeDir(\"buffer_dump\") catch |err| switch (err) {\n            error.PathAlreadyExists => {},\n            else => return,\n        };\n\n        var filename_buf: [128]u8 = undefined;\n        const filename = std.fmt.bufPrint(&filename_buf, \"buffer_dump/{s}_buffer_{d}.txt\", .{ buffer_name, timestamp }) catch return;\n\n        const file = std.fs.cwd().createFile(filename, .{}) catch return;\n        defer file.close();\n\n        var fileBuffer: [4096]u8 = undefined;\n        var fileWriter = file.writer(&fileBuffer);\n        const writer = &fileWriter.interface;\n\n        writer.print(\"{s} Buffer ({d}x{d}):\\n\", .{ buffer_name, self.width, self.height }) catch return;\n        writer.writeAll(\"Characters:\\n\") catch return;\n\n        for (0..self.height) |y| {\n            for (0..self.width) |x| {\n                const cell = buffer.get(@intCast(x), @intCast(y));\n                if (cell) |c| {\n                    if (gp.isContinuationChar(c.char)) {\n                        // skip\n                    } else if (gp.isGraphemeChar(c.char)) {\n                        const gid: u32 = gp.graphemeIdFromChar(c.char);\n                        const bytes = self.pool.get(gid) catch &[_]u8{};\n                        if (bytes.len > 0) writer.writeAll(bytes) catch return;\n                    } else {\n                        var utf8Buf: [4]u8 = undefined;\n                        const len = std.unicode.utf8Encode(@intCast(c.char), &utf8Buf) catch 1;\n                        writer.writeAll(utf8Buf[0..len]) catch return;\n                    }\n                } else {\n                    writer.writeByte(' ') catch return;\n                }\n            }\n            writer.writeByte('\\n') catch return;\n        }\n        writer.flush() catch {};\n    }\n\n    pub fn getLastOutputForTest(_: *CliRenderer) []const u8 {\n        // In non-threaded mode, we want the current active buffer\n        // In threaded mode, we want the previously rendered buffer\n        const currentBuffer = if (activeBuffer == .A) &outputBuffer else &outputBufferB;\n        const currentLen = if (activeBuffer == .A) outputBufferLen else outputBufferBLen;\n        return currentBuffer.*[0..currentLen];\n    }\n\n    pub fn dumpStdoutBuffer(self: *CliRenderer, timestamp: i64) void {\n        _ = self;\n        std.fs.cwd().makeDir(\"buffer_dump\") catch |err| switch (err) {\n            error.PathAlreadyExists => {},\n            else => return,\n        };\n\n        var filename_buf: [128]u8 = undefined;\n        const filename = std.fmt.bufPrint(&filename_buf, \"buffer_dump/stdout_buffer_{d}.txt\", .{timestamp}) catch return;\n\n        const file = std.fs.cwd().createFile(filename, .{}) catch return;\n        defer file.close();\n\n        var fileBuffer: [4096]u8 = undefined;\n        var fileWriter = file.writer(&fileBuffer);\n        const writer = &fileWriter.interface;\n\n        writer.print(\"Stdout Buffer Output (timestamp: {d}):\\n\", .{timestamp}) catch return;\n        writer.writeAll(\"Last Rendered ANSI Output:\\n\") catch return;\n        writer.writeAll(\"================\\n\") catch return;\n\n        const lastBuffer = if (activeBuffer == .A) &outputBufferB else &outputBuffer;\n        const lastLen = if (activeBuffer == .A) outputBufferBLen else outputBufferLen;\n\n        if (lastLen > 0) {\n            writer.writeAll(lastBuffer.*[0..lastLen]) catch return;\n        } else {\n            writer.writeAll(\"(no output rendered yet)\\n\") catch return;\n        }\n\n        writer.writeAll(\"\\n================\\n\") catch return;\n        writer.print(\"Buffer size: {d} bytes\\n\", .{lastLen}) catch return;\n        writer.print(\"Active buffer: {s}\\n\", .{if (activeBuffer == .A) \"A\" else \"B\"}) catch return;\n        writer.flush() catch {};\n    }\n\n    pub fn dumpBuffers(self: *CliRenderer, timestamp: i64) void {\n        self.dumpSingleBuffer(self.currentRenderBuffer, \"current\", timestamp);\n        self.dumpSingleBuffer(self.nextRenderBuffer, \"next\", timestamp);\n        self.dumpStdoutBuffer(timestamp);\n    }\n\n    pub fn restoreTerminalModes(self: *CliRenderer) void {\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        self.terminal.restoreTerminalModes(stream.writer()) catch {};\n        self.writeOut(stream.getWritten());\n    }\n\n    pub fn enableMouse(self: *CliRenderer, enableMovement: bool) void {\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        self.terminal.setMouseMode(stream.writer(), true, enableMovement) catch {};\n        self.writeOut(stream.getWritten());\n    }\n\n    pub fn queryPixelResolution(self: *CliRenderer) void {\n        self.writeOut(ansi.ANSI.queryPixelSize);\n    }\n\n    pub fn disableMouse(self: *CliRenderer) void {\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        self.terminal.setMouseMode(stream.writer(), false, self.terminal.state.mouse_movement) catch {};\n        self.writeOut(stream.getWritten());\n    }\n\n    pub fn enableKittyKeyboard(self: *CliRenderer, flags: u8) void {\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        self.terminal.setKittyKeyboard(stream.writer(), true, flags) catch {};\n        self.writeOut(stream.getWritten());\n    }\n\n    pub fn disableKittyKeyboard(self: *CliRenderer) void {\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        self.terminal.setKittyKeyboard(stream.writer(), false, 0) catch {};\n        self.writeOut(stream.getWritten());\n    }\n\n    pub fn getTerminalCapabilities(self: *CliRenderer) Terminal.Capabilities {\n        return self.terminal.getCapabilities();\n    }\n\n    pub fn setTerminalEnvVar(self: *CliRenderer, key: []const u8, value: []const u8) bool {\n        self.terminal.setHostEnvVar(self.allocator, key, value) catch return false;\n        return true;\n    }\n\n    pub fn processCapabilityResponse(self: *CliRenderer, response: []const u8) void {\n        self.terminal.processCapabilityResponse(response);\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        _ = self.terminal.sendPendingQueries(stream.writer()) catch |err| blk: {\n            logger.warn(\"Failed to send pending queries: {}\", .{err});\n            break :blk false;\n        };\n        const useKitty = self.terminal.opts.kitty_keyboard_flags > 0;\n        self.terminal.enableDetectedFeatures(stream.writer(), useKitty) catch {};\n        self.writeOut(stream.getWritten());\n    }\n\n    pub fn setKittyKeyboardFlags(self: *CliRenderer, flags: u8) void {\n        self.terminal.setKittyKeyboardFlags(flags);\n    }\n\n    pub fn getKittyKeyboardFlags(self: *CliRenderer) u8 {\n        return self.terminal.opts.kitty_keyboard_flags;\n    }\n\n    pub fn setTerminalTitle(self: *CliRenderer, title: []const u8) void {\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        self.terminal.setTerminalTitle(stream.writer(), title);\n        self.writeOut(stream.getWritten());\n    }\n\n    pub fn copyToClipboardOSC52(self: *CliRenderer, target: Terminal.ClipboardTarget, payload: []const u8) bool {\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        self.terminal.writeClipboard(stream.writer(), target, payload) catch return false;\n        self.writeOut(stream.getWritten());\n        return true;\n    }\n\n    pub fn clearClipboardOSC52(self: *CliRenderer, target: Terminal.ClipboardTarget) bool {\n        var stream = std.io.fixedBufferStream(&self.writeOutBuf);\n        self.terminal.writeClipboard(stream.writer(), target, \"\") catch return false;\n        self.writeOut(stream.getWritten());\n        return true;\n    }\n\n    fn renderDebugOverlay(self: *CliRenderer) void {\n        if (!self.debugOverlay.enabled) return;\n\n        const width: u32 = 40;\n        const height: u32 = 11;\n        var x: u32 = 0;\n        var y: u32 = 0;\n\n        if (self.width < width + 2 or self.height < height + 2) return;\n\n        switch (self.debugOverlay.corner) {\n            .topLeft => {\n                x = 1;\n                y = 1;\n            },\n            .topRight => {\n                x = self.width - width - 1;\n                y = 1;\n            },\n            .bottomLeft => {\n                x = 1;\n                y = self.height - height - 1;\n            },\n            .bottomRight => {\n                x = self.width - width - 1;\n                y = self.height - height - 1;\n            },\n        }\n\n        self.nextRenderBuffer.fillRect(x, y, width, height, .{ 20.0 / 255.0, 20.0 / 255.0, 40.0 / 255.0, 1.0 }) catch {};\n        self.nextRenderBuffer.drawText(\"Debug Information\", x + 1, y + 1, .{ 1.0, 1.0, 100.0 / 255.0, 1.0 }, .{ 0.0, 0.0, 0.0, 0.0 }, ansi.TextAttributes.BOLD) catch {};\n\n        var row: u32 = 2;\n        const bg: RGBA = .{ 0.0, 0.0, 0.0, 0.0 };\n        const fg: RGBA = .{ 200.0 / 255.0, 200.0 / 255.0, 200.0 / 255.0, 1.0 };\n\n        // Calculate averages\n        const lastFrameTimeAvg = getStatAverage(f64, &self.statSamples.lastFrameTime);\n        const renderTimeAvg = getStatAverage(f64, &self.statSamples.renderTime);\n        const overallFrameTimeAvg = getStatAverage(f64, &self.statSamples.overallFrameTime);\n        const bufferResetTimeAvg = getStatAverage(f64, &self.statSamples.bufferResetTime);\n        const stdoutWriteTimeAvg = getStatAverage(f64, &self.statSamples.stdoutWriteTime);\n        const cellsUpdatedAvg = getStatAverage(u32, &self.statSamples.cellsUpdated);\n        const frameCallbackTimeAvg = getStatAverage(f64, &self.statSamples.frameCallbackTime);\n\n        // FPS\n        var fpsText: [32]u8 = undefined;\n        const fpsLen = std.fmt.bufPrint(&fpsText, \"FPS: {d}\", .{self.renderStats.fps}) catch return;\n        self.nextRenderBuffer.drawText(fpsLen, x + 1, y + row, fg, bg, 0) catch {};\n        row += 1;\n\n        // Frame Time\n        var frameTimeText: [64]u8 = undefined;\n        const frameTimeLen = std.fmt.bufPrint(&frameTimeText, \"Frame: {d:.3}ms (avg: {d:.3}ms)\", .{ self.renderStats.lastFrameTime / 1000.0, lastFrameTimeAvg / 1000.0 }) catch return;\n        self.nextRenderBuffer.drawText(frameTimeLen, x + 1, y + row, fg, bg, 0) catch {};\n        row += 1;\n\n        // Frame Callback Time\n        if (self.renderStats.frameCallbackTime) |frameCallbackTime| {\n            var frameCallbackTimeText: [64]u8 = undefined;\n            const frameCallbackTimeLen = std.fmt.bufPrint(&frameCallbackTimeText, \"Frame Callback: {d:.3}ms (avg: {d:.3}ms)\", .{ frameCallbackTime, frameCallbackTimeAvg }) catch return;\n            self.nextRenderBuffer.drawText(frameCallbackTimeLen, x + 1, y + row, fg, bg, 0) catch {};\n            row += 1;\n        }\n\n        // Overall Time\n        if (self.renderStats.overallFrameTime) |overallTime| {\n            var overallTimeText: [64]u8 = undefined;\n            const overallTimeLen = std.fmt.bufPrint(&overallTimeText, \"Overall: {d:.3}ms (avg: {d:.3}ms)\", .{ overallTime, overallFrameTimeAvg }) catch return;\n            self.nextRenderBuffer.drawText(overallTimeLen, x + 1, y + row, fg, bg, 0) catch {};\n            row += 1;\n        }\n\n        // Render Time\n        if (self.renderStats.renderTime) |renderTime| {\n            var renderTimeText: [64]u8 = undefined;\n            const renderTimeLen = std.fmt.bufPrint(&renderTimeText, \"Render: {d:.3}ms (avg: {d:.3}ms)\", .{ renderTime / 1000.0, renderTimeAvg / 1000.0 }) catch return;\n            self.nextRenderBuffer.drawText(renderTimeLen, x + 1, y + row, fg, bg, 0) catch {};\n            row += 1;\n        }\n\n        // Buffer Reset Time\n        if (self.renderStats.bufferResetTime) |resetTime| {\n            var resetTimeText: [64]u8 = undefined;\n            const resetTimeLen = std.fmt.bufPrint(&resetTimeText, \"Reset: {d:.3}ms (avg: {d:.3}ms)\", .{ resetTime / 1000.0, bufferResetTimeAvg / 1000.0 }) catch return;\n            self.nextRenderBuffer.drawText(resetTimeLen, x + 1, y + row, fg, bg, 0) catch {};\n            row += 1;\n        }\n\n        // Stdout Write Time\n        if (self.renderStats.stdoutWriteTime) |writeTime| {\n            var writeTimeText: [64]u8 = undefined;\n            const writeTimeLen = std.fmt.bufPrint(&writeTimeText, \"Stdout: {d:.3}ms (avg: {d:.3}ms)\", .{ writeTime / 1000.0, stdoutWriteTimeAvg / 1000.0 }) catch return;\n            self.nextRenderBuffer.drawText(writeTimeLen, x + 1, y + row, fg, bg, 0) catch {};\n            row += 1;\n        }\n\n        // Cells Updated\n        var cellsText: [64]u8 = undefined;\n        const cellsLen = std.fmt.bufPrint(&cellsText, \"Cells: {d} (avg: {d})\", .{ self.renderStats.cellsUpdated, cellsUpdatedAvg }) catch return;\n        self.nextRenderBuffer.drawText(cellsLen, x + 1, y + row, fg, bg, 0) catch {};\n        row += 1;\n\n        if (self.renderStats.heapUsed > 0 or self.renderStats.heapTotal > 0) {\n            var memoryText: [64]u8 = undefined;\n            const memoryLen = std.fmt.bufPrint(&memoryText, \"Memory: {d:.2}MB / {d:.2}MB / {d:.2}MB\", .{ @as(f64, @floatFromInt(self.renderStats.heapUsed)) / 1024.0 / 1024.0, @as(f64, @floatFromInt(self.renderStats.heapTotal)) / 1024.0 / 1024.0, @as(f64, @floatFromInt(self.renderStats.arrayBuffers)) / 1024.0 / 1024.0 }) catch return;\n            self.nextRenderBuffer.drawText(memoryLen, x + 1, y + row, fg, bg, 0) catch {};\n            row += 1;\n        }\n\n        // Is threaded?\n        var isThreadedText: [64]u8 = undefined;\n        const isThreadedLen = std.fmt.bufPrint(&isThreadedText, \"Threaded: {s}\", .{if (self.useThread) \"Yes\" else \"No\"}) catch return;\n        self.nextRenderBuffer.drawText(isThreadedLen, x + 1, y + row, fg, bg, 0) catch {};\n        row += 1;\n    }\n};\n"
  },
  {
    "path": "packages/core/src/zig/rope.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\n/// This is a persistent/immutable rope - operations create new nodes without\n/// freeing old ones. Use an ArenaAllocator to avoid manual memory management:\n///\n///   var arena = std.heap.ArenaAllocator.init(allocator);\n///   defer arena.deinit();\n///   var rope = try Rope(T).init(arena.allocator());\n///\n/// TODO: Needs a startTransaction and endTransaction to track changes\n/// -> used to trigger a change event _after_ a batch of changes is complete (group as operation)\n/// -> used to group history operations (undo/redo), so not everything is a single history entry\n///\npub fn Rope(comptime T: type) type {\n    return struct {\n        const Self = @This();\n\n        pub const max_imbalance = 7;\n\n        pub const Config = struct {\n            max_undo_depth: ?usize = null, // null = unlimited\n        };\n\n        const marker_enabled = @typeInfo(T) == .@\"union\" and @hasDecl(T, \"MarkerTypes\");\n        const MarkerTagCount = if (marker_enabled) T.MarkerTypes.len else 0;\n\n        const boundary_enabled = @hasDecl(T, \"BoundaryAction\");\n        pub const BoundaryAction = if (boundary_enabled) T.BoundaryAction else struct {\n            delete_left: bool = false,\n            delete_right: bool = false,\n            insert_between: []const T = &[_]T{},\n        };\n\n        pub const MarkerPosition = struct {\n            leaf_index: u32,\n            global_weight: u32,\n        };\n        pub const MarkerCache = if (marker_enabled) struct {\n            // Flat arrays of positions for each marker type\n            positions: std.AutoHashMap(std.meta.Tag(T), std.ArrayListUnmanaged(MarkerPosition)),\n            version: u64, // Rope version when cache was built\n            allocator: Allocator,\n\n            pub fn init(allocator: Allocator) MarkerCache {\n                return .{\n                    .positions = std.AutoHashMap(std.meta.Tag(T), std.ArrayListUnmanaged(MarkerPosition)).init(allocator),\n                    .version = std.math.maxInt(u64), // Sentinel: cache is invalid until first rebuild\n                    .allocator = allocator,\n                };\n            }\n\n            pub fn deinit(self: *MarkerCache) void {\n                var iter = self.positions.valueIterator();\n                while (iter.next()) |list| {\n                    list.deinit(self.allocator);\n                }\n                self.positions.deinit();\n            }\n\n            fn clear(self: *MarkerCache) void {\n                var iter = self.positions.valueIterator();\n                while (iter.next()) |list| {\n                    list.clearRetainingCapacity();\n                }\n            }\n        } else struct {\n            pub fn init(_: Allocator) @This() {\n                return .{};\n            }\n            pub fn deinit(_: *@This()) void {}\n            fn clear(_: *@This()) void {}\n        };\n\n        pub const Metrics = struct {\n            count: u32 = 0,\n            depth: u32 = 1,\n            custom: if (@hasDecl(T, \"Metrics\")) T.Metrics else void = if (@hasDecl(T, \"Metrics\")) .{} else {},\n\n            marker_counts: if (marker_enabled) [MarkerTagCount]u32 else void = if (marker_enabled) [_]u32{0} ** MarkerTagCount else {},\n\n            pub fn add(self: *Metrics, other: Metrics) void {\n                self.count += other.count;\n                self.depth = @max(self.depth, other.depth);\n\n                if (@hasDecl(T, \"Metrics\")) {\n                    if (@hasDecl(T.Metrics, \"add\")) {\n                        self.custom.add(other.custom);\n                    }\n                }\n\n                if (marker_enabled) {\n                    inline for (&self.marker_counts, 0..) |*dst, i| {\n                        dst.* += other.marker_counts[i];\n                    }\n                }\n            }\n\n            pub fn weight(self: *const Metrics) u32 {\n                if (@hasDecl(T, \"Metrics\")) {\n                    if (@hasDecl(T.Metrics, \"weight\")) {\n                        return self.custom.weight();\n                    }\n                }\n                return self.count;\n            }\n        };\n\n        pub const Branch = struct {\n            left: *const Node,\n            right: *const Node,\n            left_metrics: Metrics,\n            total_metrics: Metrics,\n\n            fn is_balanced(self: *const Branch) bool {\n                const left_weight = self.left.metrics().weight();\n                const right_weight = self.right.metrics().weight();\n                const total_weight = left_weight + right_weight;\n\n                if (total_weight == 0) return true;\n\n                const max_side = (total_weight * 3) / 4;\n                return left_weight <= max_side and right_weight <= max_side;\n            }\n        };\n\n        pub const Leaf = struct {\n            data: T,\n            is_sentinel: bool = false,\n\n            fn metrics(self: *const Leaf) Metrics {\n                var m = Metrics{\n                    .count = if (self.is_sentinel) 0 else 1,\n                    .depth = 1,\n                };\n\n                if (@hasDecl(T, \"Metrics\")) {\n                    if (@hasDecl(T, \"measure\")) {\n                        m.custom = self.data.measure();\n                    }\n                }\n\n                if (!self.is_sentinel and marker_enabled) {\n                    const tag = std.meta.activeTag(self.data);\n                    inline for (T.MarkerTypes, 0..) |mt, i| {\n                        if (tag == mt) {\n                            m.marker_counts[i] = 1;\n                            break;\n                        }\n                    }\n                }\n\n                return m;\n            }\n        };\n\n        pub const Node = union(enum) {\n            branch: Branch,\n            leaf: Leaf,\n\n            pub fn metrics(self: *const Node) Metrics {\n                return switch (self.*) {\n                    .branch => |*b| b.total_metrics,\n                    .leaf => |*l| l.metrics(),\n                };\n            }\n\n            pub fn depth(self: *const Node) u32 {\n                return self.metrics().depth;\n            }\n\n            pub fn count(self: *const Node) u32 {\n                return self.metrics().count;\n            }\n\n            pub fn is_balanced(self: *const Node) bool {\n                return switch (self.*) {\n                    .branch => |*b| b.is_balanced(),\n                    .leaf => true,\n                };\n            }\n\n            pub fn is_empty(self: *const Node) bool {\n                return switch (self.*) {\n                    .branch => |*b| b.left.is_empty() and b.right.is_empty(),\n                    .leaf => |*l| {\n                        if (@hasDecl(T, \"is_empty\")) {\n                            return l.data.is_empty();\n                        }\n                        return false;\n                    },\n                };\n            }\n\n            pub fn is_sentinel(self: *const Node, empty_leaf: *const Node) bool {\n                return self == empty_leaf;\n            }\n\n            pub fn new_branch(allocator: Allocator, left: *const Node, right: *const Node) !*const Node {\n                const node = try allocator.create(Node);\n                errdefer allocator.destroy(node);\n\n                const left_metrics = left.metrics();\n                var total_metrics = Metrics{};\n                total_metrics.add(left_metrics);\n                total_metrics.add(right.metrics());\n                total_metrics.depth += 1;\n\n                node.* = .{ .branch = .{\n                    .left = left,\n                    .right = right,\n                    .left_metrics = left_metrics,\n                    .total_metrics = total_metrics,\n                } };\n\n                return node;\n            }\n\n            pub fn new_leaf(allocator: Allocator, data: T) !*const Node {\n                const node = try allocator.create(Node);\n                errdefer allocator.destroy(node);\n\n                node.* = .{ .leaf = .{ .data = data } };\n                return node;\n            }\n\n            pub fn get(self: *const Node, index: u32) ?*const T {\n                return switch (self.*) {\n                    .branch => |*b| {\n                        const left_count = b.left_metrics.count;\n                        if (index < left_count) {\n                            return b.left.get(index);\n                        }\n                        return b.right.get(index - left_count);\n                    },\n                    .leaf => |*l| if (index == 0) &l.data else null,\n                };\n            }\n\n            pub const WalkerFn = *const fn (ctx: *anyopaque, data: *const T, index: u32) WalkerResult;\n\n            pub const WalkerResult = struct {\n                keep_walking: bool = true,\n                err: ?anyerror = null,\n            };\n\n            pub fn walk(self: *const Node, ctx: *anyopaque, f: WalkerFn, current_index: *u32) WalkerResult {\n                return switch (self.*) {\n                    .branch => |*b| {\n                        const left_result = b.left.walk(ctx, f, current_index);\n                        if (!left_result.keep_walking or left_result.err != null) {\n                            return left_result;\n                        }\n                        return b.right.walk(ctx, f, current_index);\n                    },\n                    .leaf => |*l| {\n                        const result = f(ctx, &l.data, current_index.*);\n                        current_index.* += 1;\n                        return result;\n                    },\n                };\n            }\n\n            pub fn walk_from(self: *const Node, start_index: u32, ctx: *anyopaque, f: WalkerFn) WalkerResult {\n                var current_index: u32 = start_index;\n                return self.walk_from_internal(start_index, ctx, f, &current_index);\n            }\n\n            fn walk_from_internal(self: *const Node, start_index: u32, ctx: *anyopaque, f: WalkerFn, current_index: *u32) WalkerResult {\n                return switch (self.*) {\n                    .branch => |*b| {\n                        const left_count = b.left_metrics.count;\n                        if (start_index >= left_count) {\n                            return b.right.walk_from_internal(start_index - left_count, ctx, f, current_index);\n                        }\n\n                        const left_result = b.left.walk_from_internal(start_index, ctx, f, current_index);\n                        if (!left_result.keep_walking or left_result.err != null) {\n                            return left_result;\n                        }\n                        return b.right.walk(ctx, f, current_index);\n                    },\n                    .leaf => |*l| {\n                        if (start_index == 0) {\n                            const result = f(ctx, &l.data, current_index.*);\n                            current_index.* += 1;\n                            return result;\n                        }\n                        return .{};\n                    },\n                };\n            }\n\n            fn collect(self: *const Node, list: *std.ArrayListUnmanaged(*const Node), allocator: Allocator) !void {\n                switch (self.*) {\n                    .branch => |*b| {\n                        try b.left.collect(list, allocator);\n                        try b.right.collect(list, allocator);\n                    },\n                    .leaf => try list.append(allocator, self),\n                }\n            }\n\n            fn merge_leaves(leaves: []*const Node, allocator: Allocator) error{OutOfMemory}!*const Node {\n                const len = leaves.len;\n                if (len == 0) return error.OutOfMemory; // Should not happen\n                if (len == 1) return leaves[0];\n                if (len == 2) return try Node.new_branch(allocator, leaves[0], leaves[1]);\n\n                const mid = len / 2;\n                return try Node.new_branch(\n                    allocator,\n                    try merge_leaves(leaves[0..mid], allocator),\n                    try merge_leaves(leaves[mid..], allocator),\n                );\n            }\n\n            pub fn rebalance(self: *const Node, allocator: Allocator, tmp_allocator: Allocator) !*const Node {\n                if (self.is_balanced()) return self;\n\n                var leaves: std.ArrayListUnmanaged(*const Node) = .{};\n                defer leaves.deinit(tmp_allocator);\n\n                try leaves.ensureTotalCapacity(tmp_allocator, self.count());\n                try self.collect(&leaves, tmp_allocator);\n\n                return try merge_leaves(leaves.items, allocator);\n            }\n\n            /// Structural split at index - returns (left, right) without flattening\n            pub fn split_at(node: *const Node, index: u32, allocator: Allocator, empty_leaf: *const Node) error{OutOfMemory}!struct { left: *const Node, right: *const Node } {\n                return switch (node.*) {\n                    .leaf => {\n                        if (index == 0) {\n                            return .{ .left = empty_leaf, .right = node };\n                        } else {\n                            return .{ .left = node, .right = empty_leaf };\n                        }\n                    },\n                    .branch => |*b| {\n                        const left_count = b.left_metrics.count;\n                        if (index < left_count) {\n                            const result = try split_at(b.left, index, allocator, empty_leaf);\n                            const new_right = try join_balanced(result.right, b.right, allocator);\n                            return .{ .left = result.left, .right = new_right };\n                        } else if (index > left_count) {\n                            const result = try split_at(b.right, index - left_count, allocator, empty_leaf);\n                            const new_left = try join_balanced(b.left, result.left, allocator);\n                            return .{ .left = new_left, .right = result.right };\n                        } else {\n                            return .{ .left = b.left, .right = b.right };\n                        }\n                    },\n                };\n            }\n\n            pub fn join_balanced(left: *const Node, right: *const Node, allocator: Allocator) error{OutOfMemory}!*const Node {\n                const left_count = left.metrics().count;\n                const right_count = right.metrics().count;\n\n                if (left_count == 0) return right;\n                if (right_count == 0) return left;\n\n                const left_weight = left.metrics().weight();\n                const right_weight = right.metrics().weight();\n                const total_weight = left_weight + right_weight;\n\n                if (total_weight > 0) {\n                    const max_side = (total_weight * 3) / 4;\n                    if (left_weight <= max_side and right_weight <= max_side) {\n                        return try new_branch(allocator, left, right);\n                    }\n                }\n\n                if (left_weight > right_weight * 3) {\n                    return switch (left.*) {\n                        .leaf => try new_branch(allocator, left, right),\n                        .branch => |*b| {\n                            const new_right = try join_balanced(b.right, right, allocator);\n                            return try new_branch(allocator, b.left, new_right);\n                        },\n                    };\n                }\n\n                return switch (right.*) {\n                    .leaf => try new_branch(allocator, left, right),\n                    .branch => |*b| {\n                        const new_left = try join_balanced(left, b.left, allocator);\n                        return try new_branch(allocator, new_left, b.right);\n                    },\n                };\n            }\n\n            pub const LeafSplitResult = struct { left: T, right: T };\n\n            pub const LeafSplitFn = struct {\n                ctx: ?*anyopaque = null,\n                splitFn: *const fn (ctx: ?*anyopaque, allocator: Allocator, leaf: *const T, weight_in_leaf: u32) error{ OutOfBounds, OutOfMemory }!LeafSplitResult,\n\n                pub fn call(self: *const @This(), allocator: Allocator, leaf: *const T, weight: u32) error{ OutOfBounds, OutOfMemory }!LeafSplitResult {\n                    return self.splitFn(self.ctx, allocator, leaf, weight);\n                }\n            };\n\n            pub fn split_at_weight(\n                node: *const Node,\n                target_weight: u32,\n                allocator: Allocator,\n                empty_leaf: *const Node,\n                split_leaf_fn: *const LeafSplitFn,\n            ) error{ OutOfMemory, OutOfBounds }!struct { left: *const Node, right: *const Node } {\n                return switch (node.*) {\n                    .leaf => |*l| {\n                        const leaf_weight = node.metrics().weight();\n\n                        if (target_weight == 0) {\n                            return .{ .left = empty_leaf, .right = node };\n                        } else if (target_weight >= leaf_weight) {\n                            return .{ .left = node, .right = empty_leaf };\n                        }\n\n                        const split_result = try split_leaf_fn.call(allocator, &l.data, target_weight);\n                        const left_node = try new_leaf(allocator, split_result.left);\n                        const right_node = try new_leaf(allocator, split_result.right);\n                        return .{ .left = left_node, .right = right_node };\n                    },\n                    .branch => |*b| {\n                        const left_weight = b.left_metrics.weight();\n\n                        if (target_weight < left_weight) {\n                            const result = try split_at_weight(b.left, target_weight, allocator, empty_leaf, split_leaf_fn);\n                            const new_right = try join_balanced(result.right, b.right, allocator);\n                            return .{ .left = result.left, .right = new_right };\n                        } else if (target_weight > left_weight) {\n                            const result = try split_at_weight(b.right, target_weight - left_weight, allocator, empty_leaf, split_leaf_fn);\n                            const new_left = try join_balanced(b.left, result.left, allocator);\n                            return .{ .left = new_left, .right = result.right };\n                        } else {\n                            return .{ .left = b.left, .right = b.right };\n                        }\n                    },\n                };\n            }\n        };\n\n        pub const UndoNode = struct {\n            root: *const Node,\n            next: ?*UndoNode = null,\n            branches: ?*UndoBranch = null,\n            meta: []const u8,\n        };\n\n        pub const UndoBranch = struct {\n            redo: *UndoNode,\n            next: ?*UndoBranch,\n        };\n\n        root: *const Node,\n        allocator: Allocator,\n        empty_leaf: *const Node,\n        undo_history: ?*UndoNode = null,\n        redo_history: ?*UndoNode = null,\n        curr_history: ?*UndoNode = null,\n        config: Config = .{},\n        undo_depth: usize = 0,\n        version: u64 = 0,\n        marker_cache: MarkerCache,\n\n        pub fn init(allocator: Allocator) error{OutOfMemory}!Self {\n            return initWithConfig(allocator, .{});\n        }\n\n        pub fn initWithConfig(allocator: Allocator, config: Config) error{OutOfMemory}!Self {\n            const empty_data = if (@hasDecl(T, \"empty\"))\n                T.empty()\n            else\n                std.mem.zeroes(T);\n\n            const node = try allocator.create(Node);\n            node.* = .{ .leaf = .{ .data = empty_data, .is_sentinel = true } };\n\n            var self = Self{\n                .root = node,\n                .allocator = allocator,\n                .empty_leaf = node,\n                .config = config,\n                .marker_cache = MarkerCache.init(allocator),\n            };\n\n            try self.applyEndsInvariant();\n\n            return self;\n        }\n\n        pub fn from_item(allocator: Allocator, data: T) !Self {\n            return from_itemWithConfig(allocator, data, .{});\n        }\n\n        pub fn from_itemWithConfig(allocator: Allocator, data: T, config: Config) !Self {\n            const root = try Node.new_leaf(allocator, data);\n            const empty_data = if (@hasDecl(T, \"empty\"))\n                T.empty()\n            else\n                std.mem.zeroes(T);\n\n            const empty_node = try allocator.create(Node);\n            empty_node.* = .{ .leaf = .{ .data = empty_data, .is_sentinel = true } };\n\n            return .{\n                .root = root,\n                .allocator = allocator,\n                .empty_leaf = empty_node,\n                .config = config,\n                .marker_cache = MarkerCache.init(allocator),\n            };\n        }\n\n        pub fn from_slice(allocator: Allocator, items: []const T) !Self {\n            return from_sliceWithConfig(allocator, items, .{});\n        }\n\n        pub fn from_sliceWithConfig(allocator: Allocator, items: []const T, config: Config) !Self {\n            if (items.len == 0) {\n                return try initWithConfig(allocator, config);\n            }\n\n            var leaves: std.ArrayListUnmanaged(*const Node) = .{};\n            defer leaves.deinit(allocator);\n            try leaves.ensureTotalCapacity(allocator, items.len);\n\n            for (items) |item| {\n                const leaf = try Node.new_leaf(allocator, item);\n                try leaves.append(allocator, leaf);\n            }\n\n            const root = try Node.merge_leaves(leaves.items, allocator);\n            const empty_data = if (@hasDecl(T, \"empty\"))\n                T.empty()\n            else\n                std.mem.zeroes(T);\n\n            const empty_node = try allocator.create(Node);\n            empty_node.* = .{ .leaf = .{ .data = empty_data, .is_sentinel = true } };\n\n            return .{\n                .root = root,\n                .allocator = allocator,\n                .empty_leaf = empty_node,\n                .config = config,\n                .marker_cache = MarkerCache.init(allocator),\n            };\n        }\n\n        pub fn count(self: *const Self) u32 {\n            return self.root.count();\n        }\n\n        pub fn get(self: *const Self, index: u32) ?*const T {\n            return self.root.get(index);\n        }\n\n        pub fn walk(self: *const Self, ctx: *anyopaque, f: Node.WalkerFn) !void {\n            var index: u32 = 0;\n            const result = self.walkNode(self.root, ctx, f, &index);\n            if (result.err) |e| return e;\n        }\n\n        fn walkNode(self: *const Self, node: *const Node, ctx: *anyopaque, f: Node.WalkerFn, current_index: *u32) Node.WalkerResult {\n            return switch (node.*) {\n                .branch => |*b| {\n                    const left_result = self.walkNode(b.left, ctx, f, current_index);\n                    if (!left_result.keep_walking or left_result.err != null) {\n                        return left_result;\n                    }\n                    return self.walkNode(b.right, ctx, f, current_index);\n                },\n                .leaf => |*l| {\n                    if (node.count() == 0) {\n                        return .{};\n                    }\n                    const result = f(ctx, &l.data, current_index.*);\n                    current_index.* += 1;\n                    return result;\n                },\n            };\n        }\n\n        pub fn walk_from(self: *const Self, start_index: u32, ctx: *anyopaque, f: Node.WalkerFn) !void {\n            var current_index: u32 = 0;\n            const result = self.walkFromNode(self.root, start_index, ctx, f, &current_index);\n            if (result.err) |e| return e;\n        }\n\n        fn walkFromNode(self: *const Self, node: *const Node, start_index: u32, ctx: *anyopaque, f: Node.WalkerFn, current_index: *u32) Node.WalkerResult {\n            return switch (node.*) {\n                .branch => |*b| {\n                    const left_count = b.left_metrics.count;\n                    if (start_index >= left_count) {\n                        return self.walkFromNode(b.right, start_index - left_count, ctx, f, current_index);\n                    }\n\n                    const left_result = self.walkFromNode(b.left, start_index, ctx, f, current_index);\n                    if (!left_result.keep_walking or left_result.err != null) {\n                        return left_result;\n                    }\n                    return self.walkNode(b.right, ctx, f, current_index);\n                },\n                .leaf => |*l| {\n                    if (node.count() == 0) {\n                        return .{};\n                    }\n                    if (start_index == 0) {\n                        const result = f(ctx, &l.data, current_index.*);\n                        current_index.* += 1;\n                        return result;\n                    }\n                    return .{};\n                },\n            };\n        }\n\n        pub fn rebalance(self: *Self, tmp_allocator: Allocator) !void {\n            self.root = try self.root.rebalance(self.allocator, tmp_allocator);\n        }\n\n        pub fn insert(self: *Self, index: u32, data: T) !void {\n            try self.insert_slice(index, &[_]T{data});\n        }\n\n        pub fn delete(self: *Self, index: u32) !void {\n            try self.delete_range(index, index + 1);\n        }\n\n        pub fn replace(self: *Self, index: u32, data: T) !void {\n            if (index >= self.count()) return;\n\n            try self.delete_range(index, index + 1);\n            try self.insert_slice(index, &[_]T{data});\n        }\n\n        pub fn append(self: *Self, data: T) !void {\n            try self.insert(self.count(), data);\n        }\n\n        pub fn prepend(self: *Self, data: T) !void {\n            try self.insert(0, data);\n        }\n\n        pub fn concat(self: *Self, other: *const Self) !void {\n            self.root = try self.joinWithBoundary(self.root, other.root);\n            self.version += 1;\n        }\n\n        pub fn split(self: *Self, index: u32) !Self {\n            const result = try Node.split_at(self.root, index, self.allocator, self.empty_leaf);\n            self.root = result.left;\n            self.version += 1;\n            return Self{\n                .root = result.right,\n                .allocator = self.allocator,\n                .empty_leaf = self.empty_leaf,\n                .undo_history = null,\n                .redo_history = null,\n                .curr_history = null,\n                .marker_cache = MarkerCache.init(self.allocator),\n            };\n        }\n\n        pub fn slice(self: *const Self, start: u32, end: u32, allocator: Allocator) ![]T {\n            if (start >= end) return &[_]T{};\n\n            const SliceContext = struct {\n                items: std.ArrayListUnmanaged(T),\n                allocator: Allocator,\n                start: u32,\n                end: u32,\n                current_index: u32 = 0,\n\n                fn walker(ctx: *anyopaque, data: *const T, idx: u32) Node.WalkerResult {\n                    _ = idx;\n                    const context = @as(*@This(), @ptrCast(@alignCast(ctx)));\n                    if (context.current_index >= context.start and context.current_index < context.end) {\n                        context.items.append(context.allocator, data.*) catch |e| return .{ .err = e };\n                    }\n                    context.current_index += 1;\n                    if (context.current_index >= context.end) {\n                        return .{ .keep_walking = false };\n                    }\n                    return .{};\n                }\n            };\n\n            var context = SliceContext{\n                .items = .{},\n                .allocator = allocator,\n                .start = start,\n                .end = end,\n            };\n            errdefer context.items.deinit(allocator);\n\n            try self.walk(&context, SliceContext.walker);\n            return context.items.toOwnedSlice(allocator);\n        }\n\n        pub fn delete_range(self: *Self, start: u32, end: u32) !void {\n            if (start >= end) return;\n\n            const first_split = try Node.split_at(self.root, start, self.allocator, self.empty_leaf);\n            const second_split = try Node.split_at(first_split.right, end - start, self.allocator, self.empty_leaf);\n\n            self.root = try self.joinWithBoundary(first_split.left, second_split.right);\n\n            self.version += 1;\n            try self.applyEndsInvariant();\n        }\n\n        pub fn insert_slice(self: *Self, index: u32, items: []const T) !void {\n            if (items.len == 0) return;\n\n            const insert_rope = try Self.from_slice(self.allocator, items);\n\n            const split_result = try Node.split_at(self.root, index, self.allocator, self.empty_leaf);\n\n            const left_joined = try self.joinWithBoundary(split_result.left, insert_rope.root);\n            self.root = try self.joinWithBoundary(left_joined, split_result.right);\n\n            self.version += 1;\n            try self.applyEndsInvariant();\n        }\n\n        pub fn to_array(self: *const Self, allocator: Allocator) ![]T {\n            const ToArrayContext = struct {\n                items: std.ArrayListUnmanaged(T),\n                allocator: Allocator,\n\n                fn walker(ctx: *anyopaque, data: *const T, idx: u32) Node.WalkerResult {\n                    _ = idx;\n                    const context = @as(*@This(), @ptrCast(@alignCast(ctx)));\n                    context.items.append(context.allocator, data.*) catch |e| return .{ .err = e };\n                    return .{};\n                }\n            };\n\n            var context = ToArrayContext{\n                .items = .{},\n                .allocator = allocator,\n            };\n            errdefer context.items.deinit(allocator);\n\n            try self.walk(&context, ToArrayContext.walker);\n            return context.items.toOwnedSlice(allocator);\n        }\n\n        pub fn toText(self: *const Self, allocator: Allocator) ![]u8 {\n            var buffer: std.ArrayListUnmanaged(u8) = .{};\n            errdefer buffer.deinit(allocator);\n\n            try buffer.appendSlice(allocator, \"[root\");\n            try nodeToText(self.root, &buffer, allocator);\n            try buffer.append(allocator, ']');\n\n            return buffer.toOwnedSlice(allocator);\n        }\n\n        fn nodeToText(node: *const Node, buffer: *std.ArrayListUnmanaged(u8), allocator: Allocator) !void {\n            switch (node.*) {\n                .branch => |*b| {\n                    try buffer.appendSlice(allocator, \"[branch\");\n                    try nodeToText(b.left, buffer, allocator);\n                    try nodeToText(b.right, buffer, allocator);\n                    try buffer.append(allocator, ']');\n                },\n                .leaf => |*l| {\n                    if (l.is_sentinel) {\n                        try buffer.appendSlice(allocator, \"[empty]\");\n                        return;\n                    }\n\n                    if (@typeInfo(T) == .@\"union\") {\n                        const tag = std.meta.activeTag(l.data);\n                        const tag_name = @tagName(tag);\n\n                        try buffer.append(allocator, '[');\n                        try buffer.appendSlice(allocator, tag_name);\n\n                        if (@hasDecl(T, \"Metrics\")) {\n                            const metrics = l.metrics();\n                            try buffer.append(allocator, ':');\n                            try buffer.writer(allocator).print(\"w{d}\", .{metrics.weight()});\n\n                            if (@hasDecl(T.Metrics, \"total_width\")) {\n                                try buffer.writer(allocator).print(\",tw{d}\", .{metrics.custom.total_width});\n                            }\n                            if (@hasDecl(T.Metrics, \"total_bytes\")) {\n                                try buffer.writer(allocator).print(\",b{d}\", .{metrics.custom.total_bytes});\n                            }\n                        }\n\n                        try buffer.append(allocator, ']');\n                    } else {\n                        try buffer.appendSlice(allocator, \"[leaf\");\n                        if (@hasDecl(T, \"Metrics\")) {\n                            const metrics = l.metrics();\n                            try buffer.append(allocator, ':');\n                            try buffer.writer(allocator).print(\"w{d}\", .{metrics.weight()});\n                        }\n                        try buffer.append(allocator, ']');\n                    }\n                },\n            }\n        }\n\n        pub fn totalWeight(self: *const Self) u32 {\n            return self.root.metrics().weight();\n        }\n\n        pub fn splitByWeight(self: *Self, weight: u32, split_leaf_fn: *const Node.LeafSplitFn) !Self {\n            const result = try Node.split_at_weight(self.root, weight, self.allocator, self.empty_leaf, split_leaf_fn);\n            self.root = result.left;\n            self.version += 1;\n            return Self{\n                .root = result.right,\n                .allocator = self.allocator,\n                .empty_leaf = self.empty_leaf,\n                .undo_history = null,\n                .redo_history = null,\n                .curr_history = null,\n                .marker_cache = MarkerCache.init(self.allocator),\n            };\n        }\n\n        fn getLastLeaf(self: *const Self) ?*const T {\n            if (self.count() == 0) return null;\n            return self.get(self.count() - 1);\n        }\n\n        fn getFirstLeaf(self: *const Self) ?*const T {\n            if (self.count() == 0) return null;\n            return self.get(0);\n        }\n\n        fn getFirstLeafIn(node: *const Node) ?*const T {\n            return switch (node.*) {\n                .branch => |*b| getFirstLeafIn(b.left),\n                .leaf => |*l| if (node.count() == 0) null else &l.data,\n            };\n        }\n\n        fn getLastLeafIn(node: *const Node) ?*const T {\n            return switch (node.*) {\n                .branch => |*b| getLastLeafIn(b.right),\n                .leaf => |*l| if (node.count() == 0) null else &l.data,\n            };\n        }\n\n        fn dropFirst(node: *const Node, allocator: Allocator, empty_leaf: *const Node) error{OutOfMemory}!*const Node {\n            if (node.count() == 0) return node;\n            const split_result = try Node.split_at(node, 1, allocator, empty_leaf);\n            return split_result.right;\n        }\n\n        fn dropLast(node: *const Node, allocator: Allocator, empty_leaf: *const Node) error{OutOfMemory}!*const Node {\n            const cnt = node.count();\n            if (cnt == 0) return node;\n            const split_result = try Node.split_at(node, cnt - 1, allocator, empty_leaf);\n            return split_result.left;\n        }\n\n        fn joinWithBoundary(self: *Self, left: *const Node, right: *const Node) error{OutOfMemory}!*const Node {\n            if (!boundary_enabled or !@hasDecl(T, \"rewriteBoundary\")) {\n                return try Node.join_balanced(left, right, self.allocator);\n            }\n\n            const l_last = getLastLeafIn(left);\n            const r_first = getFirstLeafIn(right);\n\n            if (@hasDecl(T, \"canMerge\") and @hasDecl(T, \"merge\")) {\n                if (l_last != null and r_first != null) {\n                    if (T.canMerge(l_last.?, r_first.?)) {\n                        const merged = T.merge(self.allocator, l_last.?, r_first.?);\n                        const merged_leaf = try Node.new_leaf(self.allocator, merged);\n\n                        const L = try dropLast(left, self.allocator, self.empty_leaf);\n                        const R = try dropFirst(right, self.allocator, self.empty_leaf);\n\n                        const left_with_merged = try Node.join_balanced(L, merged_leaf, self.allocator);\n                        return try Node.join_balanced(left_with_merged, R, self.allocator);\n                    }\n                }\n            }\n\n            const action = try T.rewriteBoundary(self.allocator, l_last, r_first);\n\n            var L = left;\n            var R = right;\n\n            if (action.delete_left) {\n                L = try dropLast(L, self.allocator, self.empty_leaf);\n            }\n            if (action.delete_right) {\n                R = try dropFirst(R, self.allocator, self.empty_leaf);\n            }\n\n            if (action.insert_between.len > 0) {\n                const insert_rope = try Self.from_slice(self.allocator, action.insert_between);\n                const left_with_insert = try Node.join_balanced(L, insert_rope.root, self.allocator);\n                return try Node.join_balanced(left_with_insert, R, self.allocator);\n            }\n\n            return try Node.join_balanced(L, R, self.allocator);\n        }\n\n        fn applyEndsInvariant(self: *Self) !void {\n            if (!boundary_enabled or !@hasDecl(T, \"rewriteEnds\")) return;\n\n            const first = self.getFirstLeaf();\n            const last = self.getLastLeaf();\n            const action = try T.rewriteEnds(self.allocator, first, last);\n\n            // Handle deletion operations first\n            if (action.delete_left and self.count() > 0) {\n                const split_result = try Node.split_at(self.root, 1, self.allocator, self.empty_leaf);\n                self.root = split_result.right;\n            }\n            if (action.delete_right and self.count() > 0) {\n                const cnt = self.count();\n                const split_result = try Node.split_at(self.root, cnt - 1, self.allocator, self.empty_leaf);\n                self.root = split_result.left;\n            }\n\n            // Handle insertion\n            if (action.insert_between.len > 0) {\n                var leaves: std.ArrayListUnmanaged(*const Node) = .{};\n                defer leaves.deinit(self.allocator);\n                try leaves.ensureTotalCapacity(self.allocator, action.insert_between.len);\n\n                for (action.insert_between) |item| {\n                    const leaf = try Node.new_leaf(self.allocator, item);\n                    try leaves.append(self.allocator, leaf);\n                }\n\n                const insert_root = try Node.merge_leaves(leaves.items, self.allocator);\n                self.root = try Node.join_balanced(insert_root, self.root, self.allocator);\n            }\n        }\n\n        pub fn deleteRangeByWeight(self: *Self, start: u32, end: u32, split_leaf_fn: *const Node.LeafSplitFn) !void {\n            if (start >= end) return;\n\n            const first_split = try Node.split_at_weight(self.root, start, self.allocator, self.empty_leaf, split_leaf_fn);\n            const second_split = try Node.split_at_weight(first_split.right, end - start, self.allocator, self.empty_leaf, split_leaf_fn);\n\n            self.root = try self.joinWithBoundary(first_split.left, second_split.right);\n\n            self.version += 1;\n            try self.applyEndsInvariant();\n        }\n\n        pub fn insertSliceByWeight(self: *Self, weight: u32, items: []const T, split_leaf_fn: *const Node.LeafSplitFn) !void {\n            if (items.len == 0) return;\n\n            const insert_rope = try Self.from_slice(self.allocator, items);\n\n            const split_result = try Node.split_at_weight(self.root, weight, self.allocator, self.empty_leaf, split_leaf_fn);\n\n            const left_joined = try self.joinWithBoundary(split_result.left, insert_rope.root);\n            self.root = try self.joinWithBoundary(left_joined, split_result.right);\n\n            self.version += 1;\n            try self.applyEndsInvariant();\n        }\n\n        pub const WeightFindResult = struct { leaf: *const T, start_weight: u32 };\n\n        pub fn findByWeight(self: *const Self, weight: u32) ?WeightFindResult {\n            return self.findByWeightInNode(self.root, weight, 0);\n        }\n\n        fn findByWeightInNode(self: *const Self, node: *const Node, target_weight: u32, current_weight: u32) ?WeightFindResult {\n            return switch (node.*) {\n                .branch => |*b| {\n                    const left_weight = b.left_metrics.weight();\n                    if (target_weight < current_weight + left_weight) {\n                        return self.findByWeightInNode(b.left, target_weight, current_weight);\n                    }\n                    return self.findByWeightInNode(b.right, target_weight, current_weight + left_weight);\n                },\n                .leaf => |*l| {\n                    const leaf_weight = node.metrics().weight();\n                    if (target_weight < current_weight + leaf_weight) {\n                        return .{ .leaf = &l.data, .start_weight = current_weight };\n                    }\n                    return null;\n                },\n            };\n        }\n\n        /// Undo/Redo operations\n        pub fn store_undo(self: *Self, meta: []const u8) !void {\n            const undo_node = try self.create_undo_node(self.root, meta);\n            self.push_undo(undo_node);\n            self.curr_history = null;\n            try self.push_redo_branch();\n        }\n\n        fn create_undo_node(self: *const Self, root: *const Node, meta_: []const u8) !*UndoNode {\n            const undo_node = try self.allocator.create(UndoNode);\n            const meta = try self.allocator.dupe(u8, meta_);\n            undo_node.* = UndoNode{\n                .root = root,\n                .meta = meta,\n            };\n            return undo_node;\n        }\n\n        fn push_undo(self: *Self, undo_node: *UndoNode) void {\n            const next = self.undo_history;\n            self.undo_history = undo_node;\n            undo_node.next = next;\n            self.undo_depth += 1;\n\n            // Trim history if we exceed max_undo_depth\n            if (self.config.max_undo_depth) |max_depth| {\n                if (self.undo_depth > max_depth) {\n                    self.trimUndoHistory(max_depth);\n                }\n            }\n        }\n\n        fn trimUndoHistory(self: *Self, max_depth: usize) void {\n            var current = self.undo_history;\n            var depth_count: usize = 0;\n            var prev: ?*UndoNode = null;\n\n            while (current) |node| {\n                depth_count += 1;\n                if (depth_count >= max_depth) {\n                    // Cut off the rest of the history\n                    if (prev) |p| {\n                        p.next = null;\n                    }\n                    self.undo_depth = max_depth;\n                    return;\n                }\n                prev = node;\n                current = node.next;\n            }\n        }\n\n        fn push_redo(self: *Self, undo_node: *UndoNode) void {\n            const next = self.redo_history;\n            self.redo_history = undo_node;\n            undo_node.next = next;\n        }\n\n        fn push_redo_branch(self: *Self) !void {\n            const r = self.redo_history orelse return;\n            const u = self.undo_history orelse return;\n            const next = u.branches;\n            const b = try self.allocator.create(UndoBranch);\n            b.* = .{\n                .redo = r,\n                .next = next,\n            };\n            u.branches = b;\n            self.redo_history = null;\n        }\n\n        pub fn undo(self: *Self, meta: []const u8) ![]const u8 {\n            const r = self.curr_history orelse try self.create_undo_node(self.root, meta);\n            const h = self.undo_history orelse return error.Stop;\n            self.undo_history = h.next;\n            self.curr_history = h;\n            self.root = h.root;\n            self.version += 1;\n            self.push_redo(r);\n            if (self.undo_depth > 0) self.undo_depth -= 1;\n            return h.meta;\n        }\n\n        pub fn redo(self: *Self) ![]const u8 {\n            const u = self.curr_history orelse return error.Stop;\n            const h = self.redo_history orelse return error.Stop;\n            if (u.root != self.root) return error.Stop;\n            self.redo_history = h.next;\n            self.curr_history = h;\n            self.root = h.root;\n            self.version += 1;\n            self.push_undo(u);\n            return h.meta;\n        }\n\n        pub fn can_undo(self: *const Self) bool {\n            return self.undo_history != null;\n        }\n\n        pub fn can_redo(self: *const Self) bool {\n            return self.redo_history != null and self.curr_history != null;\n        }\n\n        pub fn clear_history(self: *Self) void {\n            self.undo_history = null;\n            self.redo_history = null;\n            self.curr_history = null;\n            self.undo_depth = 0;\n        }\n\n        pub fn clear(self: *Self) void {\n            self.root = self.empty_leaf;\n            self.version += 1;\n            self.applyEndsInvariant() catch {};\n        }\n\n        /// Replace the rope content with new items, using same structure as from_slice\n        /// This is useful for repeated setText operations without creating a new rope instance\n        pub fn setSegments(self: *Self, items: []const T) !void {\n            if (items.len == 0) {\n                self.root = self.empty_leaf;\n                self.version += 1;\n                try self.applyEndsInvariant();\n                return;\n            }\n\n            var leaves: std.ArrayListUnmanaged(*const Node) = .{};\n            defer leaves.deinit(self.allocator);\n            try leaves.ensureTotalCapacity(self.allocator, items.len);\n\n            for (items) |item| {\n                const leaf = try Node.new_leaf(self.allocator, item);\n                try leaves.append(self.allocator, leaf);\n            }\n\n            self.root = try Node.merge_leaves(leaves.items, self.allocator);\n            self.version += 1;\n        }\n\n        fn rebuildMarkerCache(self: *Self) !void {\n            if (!marker_enabled) return;\n\n            self.marker_cache.clear();\n\n            const RebuildContext = struct {\n                cache: *MarkerCache,\n                current_leaf: u32 = 0,\n                current_weight: u32 = 0,\n\n                fn walker(ctx: *anyopaque, data: *const T, idx: u32) Node.WalkerResult {\n                    _ = idx;\n                    const context = @as(*@This(), @ptrCast(@alignCast(ctx)));\n\n                    const tag = std.meta.activeTag(data.*);\n\n                    var is_marker = false;\n                    inline for (T.MarkerTypes) |mt| {\n                        if (tag == mt) {\n                            is_marker = true;\n                            break;\n                        }\n                    }\n\n                    const leaf_weight = if (@hasDecl(T, \"Metrics\")) blk: {\n                        if (@hasDecl(T, \"measure\")) {\n                            const metrics = data.measure();\n                            break :blk if (@hasDecl(T.Metrics, \"weight\")) metrics.weight() else 1;\n                        }\n                        break :blk 1;\n                    } else 1;\n\n                    if (is_marker) {\n                        const gop = context.cache.positions.getOrPut(tag) catch |e| {\n                            return .{ .keep_walking = false, .err = e };\n                        };\n                        if (!gop.found_existing) {\n                            gop.value_ptr.* = .{};\n                        }\n\n                        gop.value_ptr.append(context.cache.allocator, .{\n                            .leaf_index = context.current_leaf,\n                            .global_weight = context.current_weight,\n                        }) catch |e| {\n                            return .{ .keep_walking = false, .err = e };\n                        };\n                    }\n\n                    context.current_leaf += 1;\n                    context.current_weight += leaf_weight;\n                    return .{};\n                }\n            };\n\n            var ctx = RebuildContext{ .cache = &self.marker_cache };\n            try self.walk(&ctx, RebuildContext.walker);\n\n            self.marker_cache.version = self.version;\n        }\n\n        pub fn markerCount(self: *Self, tag: std.meta.Tag(T)) u32 {\n            if (!marker_enabled) return 0;\n\n            if (self.marker_cache.version != self.version) {\n                self.rebuildMarkerCache() catch return 0;\n            }\n\n            const list = self.marker_cache.positions.get(tag) orelse return 0;\n            return @intCast(list.items.len);\n        }\n\n        pub fn getMarker(self: *Self, tag: std.meta.Tag(T), occurrence: u32) ?MarkerPosition {\n            if (!marker_enabled) return null;\n\n            if (self.marker_cache.version != self.version) {\n                self.rebuildMarkerCache() catch return null;\n            }\n\n            const list = self.marker_cache.positions.get(tag) orelse return null;\n            if (occurrence >= list.items.len) return null;\n            return list.items[occurrence];\n        }\n    };\n}\n"
  },
  {
    "path": "packages/core/src/zig/syntax-style.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst buffer = @import(\"buffer.zig\");\nconst events = @import(\"event-emitter.zig\");\n\npub const RGBA = buffer.RGBA;\n\npub const StyleDefinition = struct {\n    fg: ?RGBA,\n    bg: ?RGBA,\n    attributes: u32,\n};\n\npub const SyntaxStyleError = error{\n    OutOfMemory,\n    InvalidId,\n    StyleNotFound,\n};\n\npub const Event = enum { Destroy };\n\npub const SyntaxStyle = struct {\n    allocator: Allocator,\n    global_allocator: Allocator,\n    arena: *std.heap.ArenaAllocator,\n\n    name_to_id: std.StringHashMapUnmanaged(u32),\n    id_to_style: std.AutoHashMapUnmanaged(u32, StyleDefinition),\n    next_id: u32,\n\n    merged_cache: std.StringHashMapUnmanaged(StyleDefinition),\n\n    emitter: events.EventEmitter(Event),\n\n    pub fn init(global_allocator: Allocator) SyntaxStyleError!*SyntaxStyle {\n        const self = global_allocator.create(SyntaxStyle) catch return SyntaxStyleError.OutOfMemory;\n        errdefer global_allocator.destroy(self);\n\n        const internal_arena = global_allocator.create(std.heap.ArenaAllocator) catch return SyntaxStyleError.OutOfMemory;\n        errdefer global_allocator.destroy(internal_arena);\n        internal_arena.* = std.heap.ArenaAllocator.init(global_allocator);\n\n        const internal_allocator = internal_arena.allocator();\n\n        self.* = .{\n            .allocator = internal_allocator,\n            .global_allocator = global_allocator,\n            .arena = internal_arena,\n            .name_to_id = .{},\n            .id_to_style = .{},\n            .next_id = 1, // Start from 1, 0 can be used as \"invalid\"\n            .merged_cache = .{},\n            .emitter = events.EventEmitter(Event).init(internal_allocator),\n        };\n\n        return self;\n    }\n\n    pub fn deinit(self: *SyntaxStyle) void {\n        self.emitter.emit(.Destroy);\n        self.emitter.deinit();\n        self.arena.deinit();\n        self.global_allocator.destroy(self.arena);\n        self.global_allocator.destroy(self);\n    }\n\n    pub fn registerStyle(self: *SyntaxStyle, name: []const u8, fg: ?RGBA, bg: ?RGBA, attributes: u32) SyntaxStyleError!u32 {\n        if (self.name_to_id.get(name)) |existing_id| {\n            try self.id_to_style.put(self.allocator, existing_id, StyleDefinition{\n                .fg = fg,\n                .bg = bg,\n                .attributes = attributes,\n            });\n            return existing_id;\n        }\n\n        const id = self.next_id;\n        self.next_id += 1;\n\n        const owned_name = self.allocator.dupe(u8, name) catch return SyntaxStyleError.OutOfMemory;\n\n        try self.name_to_id.put(self.allocator, owned_name, id);\n        try self.id_to_style.put(self.allocator, id, StyleDefinition{\n            .fg = fg,\n            .bg = bg,\n            .attributes = attributes,\n        });\n\n        return id;\n    }\n\n    pub fn resolveById(self: *const SyntaxStyle, id: u32) ?StyleDefinition {\n        return self.id_to_style.get(id);\n    }\n\n    pub fn resolveByName(self: *const SyntaxStyle, name: []const u8) ?u32 {\n        return self.name_to_id.get(name);\n    }\n\n    pub fn getStyleByName(self: *const SyntaxStyle, name: []const u8) ?StyleDefinition {\n        const id = self.resolveByName(name) orelse return null;\n        return self.resolveById(id);\n    }\n\n    pub fn mergeStyles(self: *SyntaxStyle, ids: []const u32) SyntaxStyleError!StyleDefinition {\n        var cache_key_buffer: [512]u8 = undefined;\n        var cache_key_stream = std.io.fixedBufferStream(&cache_key_buffer);\n        const writer = cache_key_stream.writer();\n\n        for (ids, 0..) |id, i| {\n            if (i > 0) writer.writeByte(':') catch return SyntaxStyleError.OutOfMemory;\n            writer.print(\"{d}\", .{id}) catch return SyntaxStyleError.OutOfMemory;\n        }\n\n        const cache_key = cache_key_stream.getWritten();\n\n        if (self.merged_cache.get(cache_key)) |cached| {\n            return cached;\n        }\n\n        var merged = StyleDefinition{\n            .fg = null,\n            .bg = null,\n            .attributes = 0,\n        };\n\n        for (ids) |id| {\n            if (self.resolveById(id)) |style| {\n                if (style.fg) |fg| merged.fg = fg;\n                if (style.bg) |bg| merged.bg = bg;\n                // Attributes are OR'd together\n                merged.attributes |= style.attributes;\n            }\n        }\n\n        const owned_cache_key = self.allocator.dupe(u8, cache_key) catch return SyntaxStyleError.OutOfMemory;\n        self.merged_cache.put(self.allocator, owned_cache_key, merged) catch return SyntaxStyleError.OutOfMemory;\n\n        return merged;\n    }\n\n    pub fn clearCache(self: *SyntaxStyle) void {\n        self.merged_cache.clearRetainingCapacity();\n    }\n\n    pub fn getCacheSize(self: *const SyntaxStyle) usize {\n        return self.merged_cache.count();\n    }\n\n    pub fn getStyleCount(self: *const SyntaxStyle) usize {\n        return self.id_to_style.count();\n    }\n\n    pub fn onDestroy(self: *SyntaxStyle, ctx: *anyopaque, handle: *const fn (*anyopaque) void) SyntaxStyleError!void {\n        self.emitter.on(.Destroy, .{ .ctx = ctx, .handle = handle }) catch return SyntaxStyleError.OutOfMemory;\n    }\n\n    pub fn offDestroy(self: *SyntaxStyle, ctx: *anyopaque, handle: *const fn (*anyopaque) void) void {\n        self.emitter.off(.Destroy, .{ .ctx = ctx, .handle = handle });\n    }\n};\n"
  },
  {
    "path": "packages/core/src/zig/terminal.zig",
    "content": "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst atomic = std.atomic;\nconst assert = std.debug.assert;\nconst ansi = @import(\"ansi.zig\");\nconst utf8 = @import(\"utf8.zig\");\nconst logger = @import(\"logger.zig\");\n\nconst WidthMethod = utf8.WidthMethod;\n\n/// Terminal capability detection and management\npub const Terminal = @This();\n\npub const Capabilities = struct {\n    kitty_keyboard: bool = false,\n    kitty_graphics: bool = false,\n    rgb: bool = false,\n    unicode: WidthMethod = .unicode,\n    sgr_pixels: bool = false,\n    color_scheme_updates: bool = false,\n    explicit_width: bool = false,\n    scaled_text: bool = false,\n    sixel: bool = false,\n    focus_tracking: bool = false,\n    sync: bool = false,\n    bracketed_paste: bool = false,\n    hyperlinks: bool = false,\n    osc52: bool = false,\n    explicit_cursor_positioning: bool = false,\n};\n\npub const MouseLevel = enum {\n    none,\n    basic, // click only\n    drag, // click + drag\n    motion, // all motion\n    pixels, // pixel coordinates\n};\n\npub const CursorStyle = enum {\n    block,\n    line,\n    underline,\n    default,\n};\n\npub const MousePointerStyle = enum(u8) {\n    default = 0,\n    pointer = 1,\n    text = 2,\n    crosshair = 3,\n    move = 4,\n    not_allowed = 5,\n\n    pub fn toName(self: MousePointerStyle) []const u8 {\n        return if (self == .not_allowed) \"not-allowed\" else @tagName(self);\n    }\n};\n\npub const ClipboardTarget = enum {\n    clipboard, // \"c\"\n    primary, // \"p\"\n    secondary, // \"s\"\n    query, // \"q\"\n\n    pub fn toChar(self: ClipboardTarget) u8 {\n        return switch (self) {\n            .clipboard => 'c',\n            .primary => 'p',\n            .secondary => 's',\n            .query => 'q',\n        };\n    }\n};\n\npub const Options = struct {\n    // Kitty keyboard protocol flags (progressive enhancement):\n    // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement\n    // Bit 0 (0b1):     Disambiguate escape codes (fixes ESC timing, alt+key ambiguity, ctrl+c as event)\n    // Bit 1 (0b10):    Report event types (press/repeat/release)\n    // Bit 2 (0b100):   Report alternate keys (e.g., numpad vs regular, shifted, base layout)\n    // Bit 3 (0b1000):  Report all keys as escape codes\n    // Bit 4 (0b10000): Report text associated with key events\n    // Default 0b00101 (5) = disambiguate + alternate keys\n    // Use 0b00111 (7) to also enable event types for key release detection\n    kitty_keyboard_flags: u8 = 0b00101,\n    remote: bool = false,\n    // Optional override for environment lookups. Caller owns the map.\n    env_map: ?*const std.process.EnvMap = null,\n};\n\npub const TerminalInfo = struct {\n    name: [64]u8 = [_]u8{0} ** 64,\n    name_len: usize = 0,\n    version: [32]u8 = [_]u8{0} ** 32,\n    version_len: usize = 0,\n    from_xtversion: bool = false,\n};\n\ncaps: Capabilities = .{},\nopts: Options = .{},\nhost_env_map: ?std.process.EnvMap = null,\n\nin_tmux: bool = false,\nskip_graphics_query: bool = false,\nskip_explicit_width_query: bool = false,\ngraphics_query_pending: bool = false,\ncapability_queries_pending: bool = false,\n\nstate: struct {\n    alt_screen: bool = false,\n    kitty_keyboard: bool = false,\n    kitty_keyboard_flags: u8 = 0,\n    bracketed_paste: bool = false,\n    mouse: bool = false,\n    mouse_movement: bool = true,\n    pixel_mouse: bool = false,\n    color_scheme_updates: bool = false,\n    focus_tracking: bool = false,\n    modify_other_keys: bool = false,\n    mouse_pointer: MousePointerStyle = .default,\n    cursor: struct {\n        row: u16 = 0,\n        col: u16 = 0,\n        x: u32 = 1, // 1-based for rendering\n        y: u32 = 1, // 1-based for rendering\n        visible: bool = true,\n        style: CursorStyle = .default,\n        blinking: bool = false,\n        color: [4]f32 = .{ 1.0, 1.0, 1.0, 1.0 }, // RGBA\n    } = .{},\n} = .{},\n\nterm_info: TerminalInfo = .{},\n\npub fn init(opts: Options) Terminal {\n    var term: Terminal = .{\n        .opts = opts,\n    };\n\n    term.checkEnvironmentOverrides();\n    return term;\n}\n\npub fn deinit(self: *Terminal) void {\n    if (self.host_env_map) |*env_map| {\n        env_map.deinit();\n        self.host_env_map = null;\n    }\n    self.opts.env_map = null;\n}\n\npub fn setHostEnvVar(self: *Terminal, allocator: std.mem.Allocator, key: []const u8, value: []const u8) !void {\n    if (self.host_env_map == null) {\n        self.host_env_map = std.process.EnvMap.init(allocator);\n    }\n\n    const env_map = &self.host_env_map.?;\n    try env_map.put(key, value);\n    self.opts.env_map = env_map;\n    self.checkEnvironmentOverrides();\n}\n\npub fn resetState(self: *Terminal, tty: anytype) !void {\n    try tty.writeAll(ansi.ANSI.showCursor);\n    try tty.writeAll(ansi.ANSI.reset);\n    try tty.writeAll(ansi.ANSI.resetMousePointer);\n    self.state.mouse_pointer = .default;\n\n    if (self.state.kitty_keyboard) {\n        try self.setKittyKeyboard(tty, false, 0);\n    }\n\n    if (self.state.modify_other_keys) {\n        try self.setModifyOtherKeys(tty, false);\n    }\n\n    if (self.state.mouse) {\n        try self.setMouseMode(tty, false, self.state.mouse_movement);\n    }\n\n    if (self.state.bracketed_paste) {\n        try self.setBracketedPaste(tty, false);\n    }\n\n    if (self.state.focus_tracking) {\n        try self.setFocusTracking(tty, false);\n    }\n\n    if (self.state.alt_screen) {\n        try self.exitAltScreen(tty);\n    } else {\n        switch (builtin.os.tag) {\n            .windows => {\n                try tty.writeByte('\\r');\n                var i: u16 = 0;\n                while (i < self.state.cursor.row) : (i += 1) {\n                    try tty.writeAll(ansi.ANSI.reverseIndex);\n                }\n                try tty.writeAll(ansi.ANSI.eraseBelowCursor);\n            },\n            else => {},\n        }\n    }\n\n    if (self.state.color_scheme_updates) {\n        try self.setColorSchemeUpdates(tty, false);\n    }\n\n    self.setTerminalTitle(tty, \"\");\n}\n\npub fn enterAltScreen(self: *Terminal, tty: anytype) !void {\n    try tty.writeAll(ansi.ANSI.switchToAlternateScreen);\n    self.state.alt_screen = true;\n}\n\npub fn exitAltScreen(self: *Terminal, tty: anytype) !void {\n    try tty.writeAll(ansi.ANSI.switchToMainScreen);\n    self.state.alt_screen = false;\n}\n\npub fn queryTerminalSend(self: *Terminal, tty: anytype) !void {\n    self.checkEnvironmentOverrides();\n    self.graphics_query_pending = !self.skip_graphics_query;\n    self.capability_queries_pending = false;\n\n    // Send xtversion first (doesn't need DCS wrapping - used for tmux detection)\n    try tty.writeAll(ansi.ANSI.xtversion ++\n        ansi.ANSI.hideCursor ++\n        ansi.ANSI.saveCursorState);\n\n    if (self.in_tmux) {\n        try tty.writeAll(ansi.ANSI.capabilityQueriesTmux);\n    } else {\n        try tty.writeAll(ansi.ANSI.capabilityQueries);\n        self.capability_queries_pending = true;\n    }\n\n    if (!self.skip_explicit_width_query) {\n        try tty.writeAll(ansi.ANSI.home ++\n            ansi.ANSI.explicitWidthQuery ++\n            ansi.ANSI.cursorPositionRequest ++\n            ansi.ANSI.home ++\n            ansi.ANSI.scaledTextQuery ++\n            ansi.ANSI.cursorPositionRequest);\n    }\n\n    try tty.writeAll(ansi.ANSI.restoreCursorState);\n}\n\npub fn sendPendingQueries(self: *Terminal, tty: anytype) !bool {\n    var sent = false;\n    const is_tmux = self.in_tmux or self.isXtversionTmux();\n\n    // Re-send capability queries DCS wrapped if tmux detected via xtversion\n    // Only needed if we got xtversion response indicating tmux\n    if (self.capability_queries_pending) {\n        if (self.term_info.from_xtversion and is_tmux) {\n            try tty.writeAll(ansi.ANSI.capabilityQueriesTmux);\n            sent = true;\n        }\n        // Clear pending flag regardless - non-tmux terminals already received unwrapped queries\n        self.capability_queries_pending = false;\n    }\n\n    if (self.graphics_query_pending and !self.skip_graphics_query) {\n        if (is_tmux) {\n            try tty.writeAll(ansi.ANSI.kittyGraphicsQueryTmux);\n        } else {\n            try tty.writeAll(ansi.ANSI.kittyGraphicsQuery);\n        }\n        self.graphics_query_pending = false;\n        sent = true;\n    }\n\n    return sent;\n}\n\npub fn enableDetectedFeatures(self: *Terminal, tty: anytype, use_kitty_keyboard: bool) !void {\n    if (builtin.os.tag == .windows) {\n        // Windows-specific defaults for ConPTY\n        self.caps.rgb = true;\n        self.caps.bracketed_paste = true;\n    }\n\n    self.checkEnvironmentOverrides();\n\n    if (!self.state.modify_other_keys and !self.state.kitty_keyboard) {\n        try self.setModifyOtherKeys(tty, true);\n    }\n\n    if (self.caps.kitty_keyboard and use_kitty_keyboard) {\n        if (self.state.modify_other_keys) {\n            try self.setModifyOtherKeys(tty, false);\n        }\n        try self.setKittyKeyboard(tty, true, self.opts.kitty_keyboard_flags);\n    }\n\n    if (self.caps.unicode == .unicode and !self.caps.explicit_width) {\n        try tty.writeAll(ansi.ANSI.unicodeSet);\n    }\n\n    if (self.caps.bracketed_paste) {\n        try self.setBracketedPaste(tty, true);\n    }\n\n    if (self.caps.focus_tracking) {\n        try self.setFocusTracking(tty, true);\n    }\n\n    if (!self.state.color_scheme_updates) {\n        try self.setColorSchemeUpdates(tty, true);\n        try tty.writeAll(ansi.ANSI.colorSchemeRequest);\n    }\n}\n\nfn checkEnvironmentOverrides(self: *Terminal) void {\n    self.in_tmux = false;\n    self.skip_graphics_query = false;\n    self.skip_explicit_width_query = false;\n\n    // Always just try to enable bracketed paste, even if it was reported as not supported\n    self.caps.bracketed_paste = true;\n\n    if (self.caps.rgb) {\n        self.caps.hyperlinks = true;\n    }\n\n    if (self.opts.remote) {\n        return;\n    }\n\n    var env_map_storage: ?std.process.EnvMap = null;\n    const env_map: *const std.process.EnvMap = self.opts.env_map orelse blk: {\n        env_map_storage = std.process.getEnvMap(std.heap.page_allocator) catch |err| {\n            logger.err(\"Failed to get environment map: {}\", .{err});\n            return;\n        };\n        break :blk &env_map_storage.?;\n    };\n    defer if (env_map_storage) |*map| map.deinit();\n\n    if (!self.term_info.from_xtversion) {\n        if (env_map.get(\"TMUX\")) |_| {\n            self.in_tmux = true;\n            self.caps.unicode = .wcwidth;\n            self.caps.explicit_cursor_positioning = true;\n        } else if (env_map.get(\"TERM\")) |term| {\n            if (std.mem.startsWith(u8, term, \"tmux\")) {\n                self.in_tmux = true;\n                self.caps.unicode = .wcwidth;\n                self.caps.explicit_cursor_positioning = true;\n            } else if (std.mem.startsWith(u8, term, \"screen\")) {\n                self.skip_graphics_query = true;\n                self.caps.unicode = .wcwidth;\n                self.caps.explicit_cursor_positioning = true;\n            }\n            if (std.mem.indexOf(u8, term, \"alacritty\") != null) {\n                self.caps.explicit_cursor_positioning = true;\n            }\n        }\n    }\n\n    if (env_map.get(\"OPENTUI_GRAPHICS\")) |val| {\n        if (std.mem.eql(u8, val, \"false\") or std.mem.eql(u8, val, \"0\")) {\n            self.skip_graphics_query = true;\n        } else if (std.mem.eql(u8, val, \"true\") or std.mem.eql(u8, val, \"1\")) {\n            self.skip_graphics_query = false;\n        }\n    }\n\n    if (!self.term_info.from_xtversion) {\n        if (env_map.get(\"TERM_PROGRAM\")) |prog| {\n            const copy_len = @min(prog.len, self.term_info.name.len);\n            @memcpy(self.term_info.name[0..copy_len], prog[0..copy_len]);\n            self.term_info.name_len = copy_len;\n\n            if (env_map.get(\"TERM_PROGRAM_VERSION\")) |ver| {\n                const ver_len = @min(ver.len, self.term_info.version.len);\n                @memcpy(self.term_info.version[0..ver_len], ver[0..ver_len]);\n                self.term_info.version_len = ver_len;\n            }\n        }\n\n        if (env_map.get(\"TERM_PROGRAM\")) |prog| {\n            if (std.mem.eql(u8, prog, \"vscode\")) {\n                self.caps.kitty_keyboard = false;\n                self.caps.kitty_graphics = false;\n                self.caps.unicode = .unicode;\n            } else if (std.mem.eql(u8, prog, \"Apple_Terminal\")) {\n                self.caps.unicode = .wcwidth;\n            } else if (std.mem.eql(u8, prog, \"Alacritty\")) {\n                self.caps.explicit_cursor_positioning = true;\n            }\n        }\n\n        if (env_map.get(\"ALACRITTY_SOCKET\") != null or env_map.get(\"ALACRITTY_LOG\") != null) {\n            self.caps.explicit_cursor_positioning = true;\n            if (self.term_info.name_len == 0) {\n                const name = \"Alacritty\";\n                @memcpy(self.term_info.name[0..name.len], name);\n                self.term_info.name_len = name.len;\n            }\n        }\n    }\n\n    if (env_map.get(\"COLORTERM\")) |colorterm| {\n        if (std.mem.eql(u8, colorterm, \"truecolor\") or\n            std.mem.eql(u8, colorterm, \"24bit\"))\n        {\n            self.caps.rgb = true;\n        }\n    }\n\n    if (!self.term_info.from_xtversion) {\n        if (env_map.get(\"TERMUX_VERSION\")) |_| {\n            self.caps.unicode = .wcwidth;\n        }\n\n        if (env_map.get(\"VHS_RECORD\")) |_| {\n            self.caps.unicode = .wcwidth;\n            self.caps.kitty_keyboard = false;\n            self.caps.kitty_graphics = false;\n        }\n    }\n\n    if (env_map.get(\"OPENTUI_FORCE_WCWIDTH\")) |_| {\n        self.caps.unicode = .wcwidth;\n    }\n    if (env_map.get(\"OPENTUI_FORCE_UNICODE\")) |_| {\n        self.caps.unicode = .unicode;\n    }\n    if (env_map.get(\"OPENTUI_FORCE_NOZWJ\")) |_| {\n        self.caps.unicode = .no_zwj;\n    }\n\n    if (env_map.get(\"OPENTUI_FORCE_EXPLICIT_WIDTH\")) |val| {\n        if (std.mem.eql(u8, val, \"true\") or std.mem.eql(u8, val, \"1\")) {\n            self.caps.explicit_width = true;\n        } else if (std.mem.eql(u8, val, \"false\") or std.mem.eql(u8, val, \"0\")) {\n            self.caps.explicit_width = false;\n            self.skip_explicit_width_query = true;\n        }\n    }\n\n    if (!self.caps.hyperlinks and self.term_info.from_xtversion) {\n        if (isHyperlinkTerm(self.getTerminalName())) {\n            self.caps.hyperlinks = true;\n        }\n    }\n\n    if (!self.caps.hyperlinks and !self.term_info.from_xtversion) {\n        if (env_map.get(\"TERM\")) |term| {\n            if (isHyperlinkTerm(term)) {\n                self.caps.hyperlinks = true;\n            }\n        }\n    }\n\n    if (!self.caps.osc52 and !self.term_info.from_xtversion) {\n        if (env_map.get(\"WT_SESSION\") != null) {\n            self.caps.osc52 = true;\n        }\n\n        if (!self.caps.osc52 and (self.in_tmux or env_map.get(\"STY\") != null)) {\n            self.caps.osc52 = true;\n        }\n\n        if (!self.caps.osc52) {\n            if (env_map.get(\"TERM_PROGRAM\")) |prog| {\n                if (isOsc52Term(prog)) {\n                    self.caps.osc52 = true;\n                }\n            }\n        }\n\n        if (!self.caps.osc52) {\n            if (env_map.get(\"TERM\")) |term| {\n                if (isOsc52Term(term) or std.mem.indexOf(u8, term, \"256color\") != null or std.mem.indexOf(u8, term, \"xterm\") != null) {\n                    self.caps.osc52 = true;\n                }\n            }\n        }\n    }\n}\n\n// TODO: Allow pixel mouse mode to be enabled,\n// currently does not make sense and is not supported by higher levels\npub fn setMouseMode(self: *Terminal, tty: anytype, enable: bool, enable_movement: bool) !void {\n    if (enable) {\n        if (self.state.mouse and self.state.mouse_movement == enable_movement) return;\n    } else if (!self.state.mouse) {\n        return;\n    }\n\n    if (enable) {\n        self.state.mouse = true;\n        self.state.mouse_movement = enable_movement;\n        if (!enable_movement) {\n            // Some terminals treat ?1000/?1002/?1003 as one family and let the\n            // last sequence win. Reset any-event tracking first, then enable\n            // click/drag modes so they remain active.\n            try tty.writeAll(ansi.ANSI.disableAnyEventTracking);\n        }\n        try tty.writeAll(ansi.ANSI.enableMouseTracking);\n        try tty.writeAll(ansi.ANSI.enableButtonEventTracking);\n        if (enable_movement) {\n            try tty.writeAll(ansi.ANSI.enableAnyEventTracking);\n        }\n        try tty.writeAll(ansi.ANSI.enableSGRMouseMode);\n    } else {\n        self.state.mouse = false;\n        self.state.pixel_mouse = false;\n        try tty.writeAll(ansi.ANSI.disableAnyEventTracking);\n        try tty.writeAll(ansi.ANSI.disableButtonEventTracking);\n        try tty.writeAll(ansi.ANSI.disableMouseTracking);\n        try tty.writeAll(ansi.ANSI.disableSGRMouseMode);\n    }\n}\n\npub fn setBracketedPaste(self: *Terminal, tty: anytype, enable: bool) !void {\n    const seq = if (enable) ansi.ANSI.bracketedPasteSet else ansi.ANSI.bracketedPasteReset;\n    try tty.writeAll(seq);\n    self.state.bracketed_paste = enable;\n}\n\npub fn setFocusTracking(self: *Terminal, tty: anytype, enable: bool) !void {\n    const seq = if (enable) ansi.ANSI.focusSet else ansi.ANSI.focusReset;\n    try tty.writeAll(seq);\n    self.state.focus_tracking = enable;\n}\n\npub fn setKittyKeyboard(self: *Terminal, tty: anytype, enable: bool, flags: u8) !void {\n    if (enable) {\n        if (!self.state.kitty_keyboard) {\n            try tty.print(ansi.ANSI.csiUPush, .{flags});\n            self.state.kitty_keyboard = true;\n            self.state.kitty_keyboard_flags = flags;\n        }\n    } else {\n        if (self.state.kitty_keyboard) {\n            try tty.writeAll(ansi.ANSI.csiUPop);\n            self.state.kitty_keyboard = false;\n            self.state.kitty_keyboard_flags = 0;\n        }\n    }\n}\n\npub fn setModifyOtherKeys(self: *Terminal, tty: anytype, enable: bool) !void {\n    const seq = if (enable) ansi.ANSI.modifyOtherKeysSet else ansi.ANSI.modifyOtherKeysReset;\n    try tty.writeAll(seq);\n    self.state.modify_other_keys = enable;\n}\n\npub fn setColorSchemeUpdates(self: *Terminal, tty: anytype, enable: bool) !void {\n    const seq = if (enable) ansi.ANSI.colorSchemeSet else ansi.ANSI.colorSchemeReset;\n    try tty.writeAll(seq);\n    self.state.color_scheme_updates = enable;\n}\n\n/// Re-send all currently-active terminal mode escape sequences unconditionally.\n///\n/// When the terminal loses and regains focus (e.g. alt-tab, tab switch, minimize),\n/// some terminal emulators (notably Windows Terminal / ConPTY) strip or reset\n/// DEC private modes like mouse tracking (?1000/?1002/?1003/?1006), focus\n/// tracking (?1004), and bracketed paste (?2004). This function re-emits the\n/// enable sequences for every mode that our state tracking says is currently on,\n/// without checking whether the mode \"should\" already be enabled — because the\n/// terminal may have silently disabled it.\n///\n/// This should be called in response to the focus-in event (\\x1b[I).\n///\n/// Per the xterm ctlseqs spec (Patch #401, 2025/06/22) and the Microsoft\n/// Console Virtual Terminal Sequences documentation, the relevant DECSET\n/// private modes are:\n///   ?1000h  - Normal mouse tracking (sends button press/release)\n///   ?1002h  - Button-event tracking (adds drag reporting)\n///   ?1003h  - Any-event tracking (adds all motion reporting)\n///   ?1006h  - SGR extended mouse mode (extended coordinate encoding)\n///   ?1004h  - Focus event tracking (sends \\x1b[I / \\x1b[O)\n///   ?2004h  - Bracketed paste mode (wraps pasted text in markers)\n///   Kitty keyboard protocol (CSI > flags u) - progressive enhancement\n///   modifyOtherKeys (CSI > 4 ; 1 m) - xterm key modification\npub fn restoreTerminalModes(self: *Terminal, tty: anytype) !void {\n    // Re-enable mouse tracking modes if active\n    if (self.state.mouse) {\n        if (!self.state.mouse_movement) {\n            try tty.writeAll(ansi.ANSI.disableAnyEventTracking);\n        }\n        try tty.writeAll(ansi.ANSI.enableMouseTracking);\n        try tty.writeAll(ansi.ANSI.enableButtonEventTracking);\n        if (self.state.mouse_movement) {\n            try tty.writeAll(ansi.ANSI.enableAnyEventTracking);\n        }\n        try tty.writeAll(ansi.ANSI.enableSGRMouseMode);\n    }\n\n    // Re-enable focus tracking if active\n    if (self.state.focus_tracking) {\n        try tty.writeAll(ansi.ANSI.focusSet);\n    }\n\n    // Re-enable bracketed paste if active\n    if (self.state.bracketed_paste) {\n        try tty.writeAll(ansi.ANSI.bracketedPasteSet);\n    }\n\n    // Pop stale entry then re-push kitty keyboard protocol to avoid stack growth.\n    // Both sequences are in the same write buffer so the terminal processes them atomically.\n    if (self.state.kitty_keyboard) {\n        try tty.writeAll(ansi.ANSI.csiUPop);\n        try tty.print(ansi.ANSI.csiUPush, .{self.state.kitty_keyboard_flags});\n    }\n\n    // Re-enable modifyOtherKeys if active\n    if (self.state.modify_other_keys) {\n        try tty.writeAll(ansi.ANSI.modifyOtherKeysSet);\n    }\n}\n\n/// The responses look like these:\n/// kitty - '\\x1B[?1016;2$y\\x1B[?2027;0$y\\x1B[?2031;2$y\\x1B[?1004;1$y\\x1B[?2026;2$y\\x1B[1;2R\\x1B[1;3R\\x1BP>|kitty(0.40.1)\\x1B\\\\\\x1B[?0u\\x1B_Gi=1;EINVAL:Zero width/height not allowed\\x1B\\\\\\x1B[?62;c'\n/// ghostty - '\\x1B[?1016;1$y\\x1B[?2027;1$y\\x1B[?2031;2$y\\x1B[?1004;1$y\\x1B[?2004;2$y\\x1B[?2026;2$y\\x1B[1;1R\\x1B[1;1R\\x1BP>|ghostty 1.1.3\\x1B\\\\\\x1B[?0u\\x1B_Gi=1;OK\\x1B\\\\\\x1B[?62;22c'\n/// tmux - '\\x1B[1;1R\\x1B[1;1R\\x1BP>|tmux 3.5a\\x1B\\\\\\x1B[?1;2;4c\\x1B[?2;3;0S'\n/// vscode - '\\x1B[?1016;2$y'\n/// alacritty - '\\x1B[?1016;0$y\\x1B[?2027;0$y\\x1B[?2031;0$y\\x1B[?1004;2$y\\x1B[?2004;2$y\\x1B[?2026;2$y\\x1B[1;1R\\x1B[1;1R\\x1B[?0u\\x1B[?6c'\n///\n/// Parsing these is not complete yet\npub fn processCapabilityResponse(self: *Terminal, response: []const u8) void {\n    // DECRPM responses\n    if (std.mem.indexOf(u8, response, \"1016;2$y\")) |_| {\n        self.caps.sgr_pixels = true;\n    }\n    if (std.mem.indexOf(u8, response, \"2027;2$y\")) |_| {\n        self.caps.unicode = .unicode;\n    }\n    if (std.mem.indexOf(u8, response, \"2031;1$y\") != null or std.mem.indexOf(u8, response, \"2031;2$y\") != null) {\n        self.caps.color_scheme_updates = true;\n    }\n    if (std.mem.indexOf(u8, response, \"1004;1$y\") != null or std.mem.indexOf(u8, response, \"1004;2$y\") != null) {\n        self.caps.focus_tracking = true;\n    }\n    if (std.mem.indexOf(u8, response, \"2026;1$y\") != null or std.mem.indexOf(u8, response, \"2026;2$y\") != null) {\n        self.caps.sync = true;\n    }\n    if (std.mem.indexOf(u8, response, \"2004;1$y\") != null or std.mem.indexOf(u8, response, \"2004;2$y\") != null) {\n        self.caps.bracketed_paste = true;\n    }\n\n    // Explicit width detection - cursor position report [1;NR where N >= 2 means explicit width supported\n    // We look for ESC[1; followed by a digit >= 2\n    // This handles cases where the cursor isn't at exact home position when queries are sent\n    if (std.mem.indexOf(u8, response, \"\\x1b[1;\")) |pos| {\n        const after = response[pos + 4 ..];\n        if (after.len > 0) {\n            var end: usize = 0;\n            while (end < after.len and after[end] >= '0' and after[end] <= '9') : (end += 1) {}\n            if (end > 0 and end < after.len and after[end] == 'R') {\n                const col = std.fmt.parseInt(u16, after[0..end], 10) catch 0;\n                if (col >= 2) {\n                    self.caps.explicit_width = true;\n                }\n                if (col >= 3) {\n                    self.caps.scaled_text = true;\n                }\n            }\n        }\n    }\n\n    // Parse xtversion response: ESC P > | name version ESC \\\n    // Examples: \"\\x1BP>|kitty(0.40.1)\\x1B\\\\\" or \"\\x1BP>|ghostty 1.1.3\\x1B\\\\\" or \"\\x1BP>|tmux 3.5a\\x1B\\\\\"\n    if (std.mem.indexOf(u8, response, \"\\x1bP>|\")) |pos| {\n        const start = pos + 4; // Skip past \"\\x1BP>|\"\n        if (std.mem.indexOf(u8, response[start..], \"\\x1b\\\\\")) |end_offset| {\n            const term_str = response[start .. start + end_offset];\n            self.parseXtversion(term_str);\n        }\n    }\n\n    // Kitty detection\n    if (std.mem.indexOf(u8, response, \"kitty\")) |_| {\n        self.caps.kitty_keyboard = true;\n        self.caps.kitty_graphics = true;\n        self.caps.unicode = .unicode;\n        self.caps.rgb = true;\n        self.caps.sixel = true;\n        self.caps.bracketed_paste = true;\n        self.caps.hyperlinks = true;\n    }\n\n    // Kitty keyboard protocol detection via CSI ? u response\n    // Terminals supporting the protocol respond to CSI ? u with CSI ? <flags> u\n    // Examples: \\x1b[?0u (ghostty, alacritty), \\x1b[?1u, etc.\n    if (std.mem.indexOf(u8, response, \"\\x1b[?\") != null and std.mem.indexOf(u8, response, \"u\") != null) {\n        // Look for pattern \\x1b[?Nu where N is 0-31\n        var i: usize = 0;\n        while (i + 4 < response.len) : (i += 1) {\n            if (response[i] == '\\x1b' and i + 1 < response.len and response[i + 1] == '[' and i + 2 < response.len and response[i + 2] == '?') {\n                var num_end = i + 3;\n                while (num_end < response.len and response[num_end] >= '0' and response[num_end] <= '9') : (num_end += 1) {}\n                if (num_end > i + 3 and num_end < response.len and response[num_end] == 'u') {\n                    self.caps.kitty_keyboard = true;\n                    break;\n                }\n            }\n        }\n    }\n\n    if (std.mem.indexOf(u8, response, \"tmux\")) |_| {\n        self.caps.unicode = .wcwidth;\n        self.caps.explicit_cursor_positioning = true;\n    }\n\n    if (std.mem.indexOf(u8, response, \"alacritty\")) |_| {\n        self.caps.explicit_cursor_positioning = true;\n    }\n\n    // Sixel detection via device attributes (capability 4 in DA1 response ending with 'c')\n    if (std.mem.indexOf(u8, response, \";c\")) |pos| {\n        var start: usize = 0;\n        if (pos >= 4) {\n            start = pos;\n            while (start > 0 and response[start] != '\\x1b') {\n                start -= 1;\n            }\n\n            const da_response = response[start .. pos + 2];\n\n            if (std.mem.indexOf(u8, da_response, \"\\x1b[?\") == 0) {\n                if (std.mem.indexOf(u8, da_response, \"4;\") != null or std.mem.indexOf(u8, da_response, \";4;\") != null or std.mem.indexOf(u8, da_response, \";4c\") != null) {\n                    self.caps.sixel = true;\n                }\n            }\n        }\n    }\n\n    // Kitty graphics response: ESC_Gi=31337;OK ESC\\ or ESC_Gi=31337;EERROR... ESC\\\n    // We look for our specific query ID (31337) to avoid false positives\n    if (std.mem.indexOf(u8, response, \"\\x1b_G\")) |_| {\n        if (std.mem.indexOf(u8, response, \"i=31337\")) |_| {\n            // Got a response to our graphics query with our ID\n            // If it contains \"OK\" or even an error, the protocol is supported\n            // (errors mean the query was understood, just parameters were wrong)\n            self.caps.kitty_graphics = true;\n        }\n    }\n\n    if (!self.caps.osc52 and isOsc52Term(response)) {\n        self.caps.osc52 = true;\n    }\n\n    if (!self.caps.hyperlinks and isHyperlinkTerm(response)) {\n        self.caps.hyperlinks = true;\n    }\n}\n\nfn isOsc52Term(value: []const u8) bool {\n    return std.ascii.indexOfIgnoreCase(value, \"iterm\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"kitty\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"alacritty\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"wezterm\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"contour\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"foot\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"rio\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"ghostty\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"tmux\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"screen\") != null;\n}\n\nfn isHyperlinkTerm(value: []const u8) bool {\n    return std.ascii.indexOfIgnoreCase(value, \"ghostty\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"kitty\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"wezterm\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"alacritty\") != null or\n        std.ascii.indexOfIgnoreCase(value, \"iterm\") != null;\n}\n\npub fn getCapabilities(self: *Terminal) Capabilities {\n    return self.caps;\n}\n\npub fn setMousePointerStyle(self: *Terminal, style: MousePointerStyle) void {\n    self.state.mouse_pointer = style;\n}\n\npub fn getMousePointer(self: *Terminal) MousePointerStyle {\n    return self.state.mouse_pointer;\n}\n\npub fn setCursorPosition(self: *Terminal, x: u32, y: u32, visible: bool) void {\n    self.state.cursor.x = @max(1, x);\n    self.state.cursor.y = @max(1, y);\n    self.state.cursor.visible = visible;\n\n    // Update 0-based coordinates for terminal operations\n    self.state.cursor.col = @intCast(@max(0, x - 1));\n    self.state.cursor.row = @intCast(@max(0, y - 1));\n}\n\npub fn setCursorStyle(self: *Terminal, style: CursorStyle, blinking: bool) void {\n    self.state.cursor.style = style;\n    self.state.cursor.blinking = blinking;\n}\n\npub fn setCursorColor(self: *Terminal, color: [4]f32) void {\n    self.state.cursor.color = color;\n}\n\npub fn getCursorPosition(self: *Terminal) struct { x: u32, y: u32, visible: bool } {\n    return .{\n        .x = self.state.cursor.x,\n        .y = self.state.cursor.y,\n        .visible = self.state.cursor.visible,\n    };\n}\n\npub fn getCursorStyle(self: *Terminal) struct { style: CursorStyle, blinking: bool } {\n    return .{\n        .style = self.state.cursor.style,\n        .blinking = self.state.cursor.blinking,\n    };\n}\n\npub fn getCursorColor(self: *Terminal) [4]f32 {\n    return self.state.cursor.color;\n}\n\npub fn setKittyKeyboardFlags(self: *Terminal, flags: u8) void {\n    self.opts.kitty_keyboard_flags = flags;\n}\n\npub fn setTerminalTitle(_: *Terminal, tty: anytype, title: []const u8) void {\n    // For Windows, we might need to use different approach, but ANSI sequences work in Windows Terminal, ConPTY, etc.\n    // For other platforms, ANSI OSC sequences work reliably\n    ansi.ANSI.setTerminalTitleOutput(tty, title) catch {};\n}\n\n/// Write OSC 52 clipboard sequence to the terminal\n/// Supports tmux/screen passthrough, including nested tmux sessions\npub fn writeClipboard(self: *Terminal, tty: anytype, target: ClipboardTarget, payload: []const u8) !void {\n    if (!self.canWriteClipboard()) {\n        return error.NotSupported;\n    }\n\n    var buf: [1024]u8 = undefined;\n    var stream = std.io.fixedBufferStream(&buf);\n    const writer = stream.writer();\n\n    // Build OSC 52 sequence: ESC]52;<target>;<payload>ESC\\\n    try writer.writeAll(\"\\x1b]52;\");\n    try writer.writeByte(target.toChar());\n    try writer.writeByte(';');\n    try writer.writeAll(payload);\n    try writer.writeAll(\"\\x1b\\\\\");\n\n    const osc52 = stream.getWritten();\n\n    // Use self.in_tmux which is set by checkEnvironmentOverrides() considering\n    // env vars, xtversion response, and remote option\n    const is_tmux = self.in_tmux or self.isXtversionTmux();\n\n    if (is_tmux) {\n        // For nested tmux, we use a fixed level of 1 as we don't have access\n        // to env vars here (by design - detection already happened in checkEnvironmentOverrides)\n        // In practice, single-level wrapping works for most cases\n        var wrapped_buf: [4096]u8 = undefined;\n        var wrapped_stream = std.io.fixedBufferStream(&wrapped_buf);\n        const wrap_writer = wrapped_stream.writer();\n        for (osc52) |c| {\n            if (c == '\\x1b') {\n                try wrap_writer.writeByte('\\x1b');\n            }\n            try wrap_writer.writeByte(c);\n        }\n        const doubled = wrapped_stream.getWritten();\n\n        try tty.writeAll(ansi.ANSI.tmuxDcsStart);\n        try tty.writeAll(doubled);\n        try tty.writeAll(ansi.ANSI.tmuxDcsEnd);\n    } else if (self.opts.remote) {\n        try tty.writeAll(osc52);\n    } else {\n        var env_map_storage: ?std.process.EnvMap = null;\n        const env_map: *const std.process.EnvMap = self.opts.env_map orelse blk: {\n            env_map_storage = std.process.getEnvMap(std.heap.page_allocator) catch |err| {\n                logger.err(\"Failed to get environment map: {}\", .{err});\n                return;\n            };\n            break :blk &env_map_storage.?;\n        };\n        defer if (env_map_storage) |*map| map.deinit();\n\n        if (env_map.get(\"STY\")) |_| {\n            var wrapped_buf: [2048]u8 = undefined;\n            var wrapped_stream = std.io.fixedBufferStream(&wrapped_buf);\n            const wrapped_writer = wrapped_stream.writer();\n\n            for (osc52) |c| {\n                if (c == '\\x1b') {\n                    try wrapped_writer.writeByte('\\x1b');\n                }\n                try wrapped_writer.writeByte(c);\n            }\n            const doubled = wrapped_stream.getWritten();\n\n            try tty.writeAll(ansi.ANSI.screenDcsStart);\n            try tty.writeAll(doubled);\n            try tty.writeAll(ansi.ANSI.screenDcsEnd);\n        } else {\n            try tty.writeAll(osc52);\n        }\n    }\n}\n\n/// Check if we can write to the clipboard (TTY and OSC 52 supported)\nfn canWriteClipboard(self: *Terminal) bool {\n    // In a real TTY environment, we'd check isTTY here\n    // For now, we just check if OSC 52 is supported\n    return self.caps.osc52;\n}\n\n/// Parse xtversion response string and extract terminal name and version\n/// Examples: \"kitty(0.40.1)\", \"ghostty 1.1.3\", \"tmux 3.5a\"\nfn parseXtversion(self: *Terminal, term_str: []const u8) void {\n    if (term_str.len == 0) return;\n\n    if (std.mem.indexOf(u8, term_str, \"(\")) |paren_pos| {\n        const name_len = @min(paren_pos, self.term_info.name.len);\n        @memcpy(self.term_info.name[0..name_len], term_str[0..name_len]);\n        self.term_info.name_len = name_len;\n\n        if (std.mem.indexOf(u8, term_str[paren_pos..], \")\")) |close_offset| {\n            const ver_start = paren_pos + 1;\n            const ver_end = paren_pos + close_offset;\n            const ver_len = @min(ver_end - ver_start, self.term_info.version.len);\n            @memcpy(self.term_info.version[0..ver_len], term_str[ver_start .. ver_start + ver_len]);\n            self.term_info.version_len = ver_len;\n        }\n    } else {\n        if (std.mem.indexOf(u8, term_str, \" \")) |space_pos| {\n            const name_len = @min(space_pos, self.term_info.name.len);\n            @memcpy(self.term_info.name[0..name_len], term_str[0..name_len]);\n            self.term_info.name_len = name_len;\n\n            const ver_start = space_pos + 1;\n            const ver_len = @min(term_str.len - ver_start, self.term_info.version.len);\n            @memcpy(self.term_info.version[0..ver_len], term_str[ver_start .. ver_start + ver_len]);\n            self.term_info.version_len = ver_len;\n        } else {\n            const name_len = @min(term_str.len, self.term_info.name.len);\n            @memcpy(self.term_info.name[0..name_len], term_str[0..name_len]);\n            self.term_info.name_len = name_len;\n            self.term_info.version_len = 0;\n        }\n    }\n\n    self.term_info.from_xtversion = true;\n}\n\npub fn isXtversionTmux(self: *Terminal) bool {\n    return self.term_info.from_xtversion and std.mem.eql(u8, self.getTerminalName(), \"tmux\");\n}\n\npub fn getTerminalInfo(self: *Terminal) TerminalInfo {\n    return self.term_info;\n}\n\npub fn getTerminalName(self: *Terminal) []const u8 {\n    return self.term_info.name[0..self.term_info.name_len];\n}\n\npub fn getTerminalVersion(self: *Terminal) []const u8 {\n    return self.term_info.version[0..self.term_info.version_len];\n}\n"
  },
  {
    "path": "packages/core/src/zig/test.zig",
    "content": "const std = @import(\"std\");\n\n// Import all test modules\nconst text_buffer_tests = @import(\"tests/text-buffer_test.zig\");\nconst text_buffer_highlights_tests = @import(\"tests/text-buffer-highlights_test.zig\");\nconst text_buffer_view_tests = @import(\"tests/text-buffer-view_test.zig\");\nconst text_buffer_selection_tests = @import(\"tests/text-buffer-selection_test.zig\");\nconst text_buffer_drawing_tests = @import(\"tests/text-buffer-drawing_test.zig\");\nconst text_buffer_segment_tests = @import(\"tests/text-buffer-segment_test.zig\");\nconst text_buffer_iterators_tests = @import(\"tests/text-buffer-iterators_test.zig\");\nconst edit_buffer_tests = @import(\"tests/edit-buffer_test.zig\");\nconst edit_buffer_history_tests = @import(\"tests/edit-buffer-history_test.zig\");\nconst editor_view_tests = @import(\"tests/editor-view_test.zig\");\nconst grapheme_tests = @import(\"tests/grapheme_test.zig\");\nconst link_tests = @import(\"tests/link_test.zig\");\nconst syntax_style_tests = @import(\"tests/syntax-style_test.zig\");\nconst rope_tests = @import(\"tests/rope_test.zig\");\nconst rope_nested_tests = @import(\"tests/rope-nested_test.zig\");\nconst rope_fuzz_tests = @import(\"tests/rope_fuzz_test.zig\");\nconst utf8_tests = @import(\"tests/utf8_test.zig\");\nconst utf8_wcwidth_tests = @import(\"tests/utf8_wcwidth_test.zig\");\nconst utf8_wcwidth_cursor_tests = @import(\"tests/utf8_wcwidth_cursor_test.zig\");\nconst utf8_no_zwj_tests = @import(\"tests/utf8_no_zwj_test.zig\");\nconst event_emitter_tests = @import(\"tests/event-emitter_test.zig\");\nconst buffer_tests = @import(\"tests/buffer_test.zig\");\nconst segment_merge_tests = @import(\"tests/segment-merge.test.zig\");\nconst word_wrap_editing_tests = @import(\"tests/word-wrap-editing_test.zig\");\nconst renderer_tests = @import(\"tests/renderer_test.zig\");\nconst terminal_tests = @import(\"tests/terminal_test.zig\");\nconst mem_registry_tests = @import(\"tests/mem-registry_test.zig\");\nconst memory_leak_regression_tests = @import(\"tests/memory_leak_regression_test.zig\");\nconst wrap_cache_perf_tests = @import(\"tests/wrap-cache-perf_test.zig\");\nconst native_span_feed_tests = @import(\"tests/native-span-feed_test.zig\");\nconst buffer_methods_tests = @import(\"tests/buffer-methods_test.zig\");\n// const example_tests = @import(\"example_test.zig\");\n\n// Re-export test declarations from individual test files\n// This allows `zig test index.zig` to run all tests\ncomptime {\n    _ = text_buffer_tests;\n    _ = text_buffer_highlights_tests;\n    _ = text_buffer_view_tests;\n    _ = text_buffer_selection_tests;\n    _ = text_buffer_drawing_tests;\n    _ = text_buffer_segment_tests;\n    _ = text_buffer_iterators_tests;\n    _ = edit_buffer_tests;\n    _ = edit_buffer_history_tests;\n    _ = editor_view_tests;\n    _ = grapheme_tests;\n    _ = link_tests;\n    _ = syntax_style_tests;\n    _ = rope_tests;\n    _ = rope_nested_tests;\n    _ = rope_fuzz_tests;\n    _ = utf8_tests;\n    _ = utf8_wcwidth_tests;\n    _ = utf8_wcwidth_cursor_tests;\n    _ = utf8_no_zwj_tests;\n    _ = event_emitter_tests;\n    _ = buffer_tests;\n    _ = segment_merge_tests;\n    _ = word_wrap_editing_tests;\n    _ = renderer_tests;\n    _ = terminal_tests;\n    _ = mem_registry_tests;\n    _ = memory_leak_regression_tests;\n    _ = wrap_cache_perf_tests;\n    _ = native_span_feed_tests;\n    _ = buffer_methods_tests;\n    // _ = example_tests;\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/README.md",
    "content": "# Test Suite\n\nThis directory contains the test suite for the OpenTUI Zig components.\n\n### Run all tests:\n\n```bash\nzig build test --summary all\n```\n\n## Adding New Test Files\n\n1. Create a new `*_test.zig` file in this directory\n2. Import it in `../index.zig`:\n   ```zig\n   const new_tests = @import(\"new_test.zig\");\n   ```\n3. Update the build system if needed to include any new dependencies\n"
  },
  {
    "path": "packages/core/src/zig/tests/buffer-methods_test.zig",
    "content": "const std = @import(\"std\");\nconst buffer_mod = @import(\"../buffer.zig\");\nconst buffer_effects = @import(\"../buffer-methods.zig\");\nconst gp = @import(\"../grapheme.zig\");\n\nconst OptimizedBuffer = buffer_mod.OptimizedBuffer;\nconst RGBA = buffer_mod.RGBA;\nconst ColorTarget = buffer_effects.ColorTarget;\n\nfn expectRGBAApprox(expected: RGBA, actual: RGBA, epsilon: f32) !void {\n    const diff_r = @abs(expected[0] - actual[0]);\n    const diff_g = @abs(expected[1] - actual[1]);\n    const diff_b = @abs(expected[2] - actual[2]);\n    const diff_a = @abs(expected[3] - actual[3]);\n\n    if (diff_r > epsilon or diff_g > epsilon or diff_b > epsilon or diff_a > epsilon) {\n        std.debug.print(\"RGBA mismatch: expected {any}, got {any}\\n\", .{ expected, actual });\n        return error.TestExpectedApprox;\n    }\n}\n\nfn expectVec4fApprox(expected: @Vector(4, f32), actual: @Vector(4, f32), epsilon: f32) !void {\n    const diff = @abs(expected - actual);\n    if (@reduce(.Or, diff > @as(@Vector(4, f32), @splat(epsilon)))) {\n        std.debug.print(\"Vec4 mismatch: expected {any}, got {any}\\n\", .{ expected, actual });\n        return error.TestExpectedApprox;\n    }\n}\n\n// Identity matrix (no change)\nconst IDENTITY_MATRIX = [16]f32{\n    1.0, 0.0, 0.0, 0.0, // Red output\n    0.0, 1.0, 0.0, 0.0, // Green output\n    0.0, 0.0, 1.0, 0.0, // Blue output\n    0.0, 0.0, 0.0, 1.0, // Alpha output\n};\n\n// Sepia matrix\nconst SEPIA_MATRIX = [16]f32{\n    0.393, 0.769, 0.189, 0.0, // Red output\n    0.349, 0.686, 0.168, 0.0, // Green output\n    0.272, 0.534, 0.131, 0.0, // Blue output\n    0.0, 0.0, 0.0, 1.0, // Alpha output\n};\n\n// Grayscale matrix (luminance)\nconst GRAYSCALE_MATRIX = [16]f32{\n    0.299, 0.587, 0.114, 0.0, // Red output\n    0.299, 0.587, 0.114, 0.0, // Green output\n    0.299, 0.587, 0.114, 0.0, // Blue output\n    0.0, 0.0, 0.0, 1.0, // Alpha output\n};\n\n// Invert matrix\nconst INVERT_MATRIX = [16]f32{\n    -1.0, 0.0, 0.0, 0.0, // Red output\n    0.0, -1.0, 0.0, 0.0, // Green output\n    0.0, 0.0, -1.0, 0.0, // Blue output\n    0.0, 0.0, 0.0, 1.0, // Alpha output\n};\n\ntest \"colorMatrix - identity matrix leaves colors unchanged\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        4,\n        4,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red; // (0, 0)\n    buf.buffer.fg[5] = red; // (1, 1)\n\n    // Apply identity to specific cells: (0, 0) and (1, 1) with strength 1.0\n    // cellMask format: [x, y, strength, x, y, strength, ...]\n    const cell_mask = [_]f32{ 0.0, 0.0, 1.0, 1.0, 1.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &IDENTITY_MATRIX, &cell_mask, 1.0, ColorTarget.FG); // target=1 (FG)\n\n    // Colors should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n    try expectRGBAApprox(red, buf.buffer.fg[5], 0.0001);\n}\n\ntest \"colorMatrix - applies transformation to specified cells only\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        3,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    // Set all FG to red\n    @memset(buf.buffer.fg, red);\n\n    // Apply sepia only to cell (1, 1) with full strength\n    const cell_mask = [_]f32{ 1.0, 1.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Cell (1, 1) should be transformed (index = y * width + x = 1 * 3 + 1 = 4)\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[4], 0.001);\n\n    // Other cells should remain red\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001); // (0, 0)\n    try expectRGBAApprox(red, buf.buffer.fg[8], 0.0001); // (2, 2)\n}\n\ntest \"colorMatrix - globalStrength scales individual cell strengths\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    // Apply sepia with cell strength 1.0 but globalStrength 0.5\n    const cell_mask = [_]f32{ 0.0, 0.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 0.5, ColorTarget.FG);\n\n    // Expected: blend(original, sepia, 0.5)\n    const sepia_r = 0.393;\n    const expected_r = 1.0 + (sepia_r - 1.0) * 0.5;\n    const sepia_g = 0.349;\n    const expected_g = 0.0 + (sepia_g - 0.0) * 0.5;\n    const sepia_b = 0.272;\n    const expected_b = 0.0 + (sepia_b - 0.0) * 0.5;\n\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[0], 0.001);\n}\n\ntest \"colorMatrix - respects target parameter\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const blue = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n    buf.buffer.bg[0] = blue;\n    buf.buffer.fg[1] = red;\n    buf.buffer.bg[1] = blue;\n\n    // Apply to FG only (target = 1)\n    const cell_mask = [_]f32{ 0.0, 0.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &GRAYSCALE_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // FG should be grayscale, BG should remain blue\n    const gray_red = 0.299 * 1.0;\n    try expectRGBAApprox(.{ gray_red, gray_red, gray_red, 1.0 }, buf.buffer.fg[0], 0.001);\n    try expectRGBAApprox(blue, buf.buffer.bg[0], 0.0001);\n\n    // Reset for BG test\n    buf.buffer.fg[0] = red;\n    buf.buffer.bg[0] = blue;\n    buf.buffer.fg[1] = red;\n    buf.buffer.bg[1] = blue;\n\n    buffer_effects.colorMatrix(buf, &GRAYSCALE_MATRIX, &cell_mask, 1.0, ColorTarget.BG); // target=2 (BG)\n\n    // BG should be grayscale, FG should remain red\n    const gray_blue = 0.114 * 1.0;\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n    try expectRGBAApprox(.{ gray_blue, gray_blue, gray_blue, 1.0 }, buf.buffer.bg[0], 0.001);\n}\n\ntest \"colorMatrix - skips out-of-bounds coordinates\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        3,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[4] = red; // (1, 1)\n\n    // Apply to out-of-bounds and valid cell\n    const cell_mask = [_]f32{ 10.0, 10.0, 1.0, 1.0, 1.0, 1.0 }; // (10, 10) is OOB\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Valid cell should be transformed\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[4], 0.001);\n}\n\ntest \"colorMatrix - skips NaN and Inf coordinates\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        3,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[4] = red; // (1, 1)\n\n    // Apply with NaN and valid coordinates\n    const nan = std.math.nan(f32);\n    const cell_mask = [_]f32{ nan, 1.0, 1.0, 1.0, 1.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Valid cell should be transformed\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[4], 0.001);\n}\n\ntest \"colorMatrix - skips zero strength cells\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    // Apply with zero strength\n    const cell_mask = [_]f32{ 0.0, 0.0, 0.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Color should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n}\n\ntest \"colorMatrix - handles multiple cells in mask\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        4,\n        4,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const green = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    const blue = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n    const white = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    // Set different colors at different positions\n    buf.buffer.fg[0] = red; // (0, 0)\n    buf.buffer.fg[5] = green; // (1, 1)\n    buf.buffer.fg[10] = blue; // (2, 2)\n    buf.buffer.fg[15] = white; // (3, 3)\n\n    // Apply sepia to all four cells with varying strengths\n    const cell_mask = [_]f32{\n        0.0, 0.0, 1.0, // (0, 0) - full\n        1.0, 1.0, 0.5, // (1, 1) - half\n        2.0, 2.0, 0.0, // (2, 2) - none (skipped)\n        3.0, 3.0, 1.0, // (3, 3) - full\n    };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // (0, 0) should be fully sepia\n    const sepia_r = 0.393;\n    const sepia_g = 0.349;\n    const sepia_b = 0.272;\n    try expectRGBAApprox(.{ sepia_r, sepia_g, sepia_b, 1.0 }, buf.buffer.fg[0], 0.001);\n\n    // (1, 1) should be half sepia\n    const green_sepia_r = 0.0 + (0.769 - 0.0) * 0.5; // Matrix row 0, col 1 = 0.769\n    const green_sepia_g = 1.0 + (0.686 - 1.0) * 0.5;\n    const green_sepia_b = 0.0 + (0.534 - 0.0) * 0.5;\n    try expectRGBAApprox(.{ green_sepia_r, green_sepia_g, green_sepia_b, 1.0 }, buf.buffer.fg[5], 0.001);\n\n    // (2, 2) should be unchanged (zero strength)\n    try expectRGBAApprox(blue, buf.buffer.fg[10], 0.0001);\n\n    // (3, 3) should be fully sepia of white\n    // White * sepia matrix = sum of first 3 columns of each row\n    const white_sepia_r = 0.393 + 0.769 + 0.189; // ~1.351\n    const white_sepia_g = 0.349 + 0.686 + 0.168; // ~1.203\n    const white_sepia_b = 0.272 + 0.534 + 0.131; // ~0.937\n    try expectRGBAApprox(.{ white_sepia_r, white_sepia_g, white_sepia_b, 1.0 }, buf.buffer.fg[15], 0.001);\n}\n\ntest \"colorMatrix - truncates incomplete mask triplets\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n    buf.buffer.fg[1] = red;\n\n    // Mask with 5 elements (1 complete triplet + 2 incomplete)\n    const cell_mask = [_]f32{ 0.0, 0.0, 1.0, 1.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Only first cell should be transformed\n    const sepia_r = 0.393;\n    const sepia_g = 0.349;\n    const sepia_b = 0.272;\n    try expectRGBAApprox(.{ sepia_r, sepia_g, sepia_b, 1.0 }, buf.buffer.fg[0], 0.001);\n\n    // Second cell should be unchanged (incomplete triplet ignored)\n    try expectRGBAApprox(red, buf.buffer.fg[1], 0.0001);\n}\n\ntest \"colorMatrix - empty mask returns early\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    // Empty mask - should return early\n    const empty_mask = [0]f32{};\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &empty_mask, 1.0, ColorTarget.FG);\n\n    // Color should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n}\n\ntest \"colorMatrix - empty matrix returns early\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    // Empty matrix - should return early\n    const empty_matrix = [0]f32{};\n    const cell_mask = [_]f32{ 0.0, 0.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &empty_matrix, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Color should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n}\n\n// Test matrix that modifies alpha channel\nconst ALPHA_MODIFY_MATRIX = [16]f32{\n    1.0, 0.0, 0.0, 0.0, // Red output\n    0.0, 1.0, 0.0, 0.0, // Green output\n    0.0, 0.0, 1.0, 0.0, // Blue output\n    0.0, 0.0, 0.0, 0.5, // Alpha output (multiply by 0.5)\n};\n\ntest \"colorMatrix - alpha channel transformation\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const opaque_color = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = opaque_color;\n\n    // Apply matrix that halves alpha\n    const cell_mask = [_]f32{ 0.0, 0.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &ALPHA_MODIFY_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Alpha should be halved\n    try expectRGBAApprox(.{ 1.0, 0.0, 0.0, 0.5 }, buf.buffer.fg[0], 0.0001);\n}\n\ntest \"colorMatrix - mask with only 1 element\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    // Mask with only 1 element (incomplete triplet)\n    const cell_mask = [_]f32{0.0};\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Color should be unchanged (no complete triplets to process)\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n}\n\ntest \"colorMatrix - mask with only 2 elements\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n    buf.buffer.fg[1] = red;\n\n    // Mask with only 2 elements (incomplete triplet)\n    const cell_mask = [_]f32{ 0.0, 0.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Colors should be unchanged (no complete triplets to process)\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n    try expectRGBAApprox(red, buf.buffer.fg[1], 0.0001);\n}\n\ntest \"colorMatrix - infinity strength is skipped\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    // Apply with infinity strength (should be skipped)\n    const inf = std.math.inf(f32);\n    const cell_mask = [_]f32{ 0.0, 0.0, inf };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Color should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n}\n\ntest \"colorMatrix - non-finite global strength is skipped\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    const inf = std.math.inf(f32);\n    const cell_mask = [_]f32{ 0.0, 0.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, inf, ColorTarget.FG);\n\n    // Color should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n}\n\ntest \"colorMatrix - large buffer with SIMD and scalar mix\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    // 100 pixels = 25 SIMD batches of 4\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        100,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    // Set all to red\n    @memset(buf.buffer.fg, red);\n\n    // Apply sepia at full strength\n    buffer_effects.colorMatrixUniform(buf, &SEPIA_MATRIX, 1.0, ColorTarget.FG);\n\n    // All pixels should be transformed\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n\n    for (0..100) |i| {\n        try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[i], 0.001);\n    }\n}\n\ntest \"colorMatrix - negative coordinates are skipped\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        3,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[4] = red; // (1, 1)\n\n    // Apply with negative coordinates followed by valid\n    const cell_mask = [_]f32{ -1.0, -1.0, 1.0, 1.0, 1.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    // Valid cell should be transformed\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[4], 0.001);\n}\n\ntest \"colorMatrix - finite coordinates larger than u32 max are skipped\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        3,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[4] = red; // (1, 1)\n\n    // First triplet uses finite but out-of-range coordinates for u32 conversion.\n    // Second triplet is valid and should still be processed.\n    const huge = std.math.floatMax(f32);\n    const cell_mask = [_]f32{ huge, huge, 1.0, 1.0, 1.0, 1.0 };\n    buffer_effects.colorMatrix(buf, &SEPIA_MATRIX, &cell_mask, 1.0, ColorTarget.FG);\n\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[4], 0.001);\n}\n\n// ==================== colorMatrixUniform Tests ====================\n\ntest \"colorMatrixUniform - identity matrix leaves colors unchanged\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        4,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const green = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    const blue = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n    const white = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    // Set specific colors at different positions\n    buf.buffer.fg[0] = red;\n    buf.buffer.fg[1] = green;\n    buf.buffer.fg[2] = blue;\n    buf.buffer.fg[3] = white;\n\n    // Apply identity matrix at full strength to foreground\n    buffer_effects.colorMatrixUniform(buf, &IDENTITY_MATRIX, 1.0, ColorTarget.FG);\n\n    // Colors should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n    try expectRGBAApprox(green, buf.buffer.fg[1], 0.0001);\n    try expectRGBAApprox(blue, buf.buffer.fg[2], 0.0001);\n    try expectRGBAApprox(white, buf.buffer.fg[3], 0.0001);\n}\n\ntest \"colorMatrixUniform - zero strength has no effect\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        2,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    @memset(buf.buffer.fg, red);\n\n    // Apply sepia matrix with zero strength\n    buffer_effects.colorMatrixUniform(buf, &SEPIA_MATRIX, 0.0, ColorTarget.FG);\n\n    // Colors should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n    try expectRGBAApprox(red, buf.buffer.fg[3], 0.0001);\n}\n\ntest \"colorMatrixUniform - non-finite strength has no effect\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n    buf.buffer.fg[1] = red;\n\n    const nan = std.math.nan(f32);\n    buffer_effects.colorMatrixUniform(buf, &SEPIA_MATRIX, nan, ColorTarget.FG);\n\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n    try expectRGBAApprox(red, buf.buffer.fg[1], 0.0001);\n}\n\ntest \"colorMatrixUniform - grayscale transformation\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const green = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    const blue = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    buf.buffer.fg[0] = red;\n    buf.buffer.fg[1] = green;\n    buf.buffer.fg[2] = blue;\n\n    // Apply grayscale matrix at full strength to foreground\n    buffer_effects.colorMatrixUniform(buf, &GRAYSCALE_MATRIX, 1.0, ColorTarget.FG);\n\n    // Calculate expected grayscale values\n    // Luminance = 0.299*R + 0.587*G + 0.114*B\n    const gray_red = 0.299 * 1.0 + 0.587 * 0.0 + 0.114 * 0.0; // ~0.299\n    const gray_green = 0.299 * 0.0 + 0.587 * 1.0 + 0.114 * 0.0; // ~0.587\n    const gray_blue = 0.299 * 0.0 + 0.587 * 0.0 + 0.114 * 1.0; // ~0.114\n\n    // All channels should equal the luminance value\n    try expectRGBAApprox(.{ gray_red, gray_red, gray_red, 1.0 }, buf.buffer.fg[0], 0.001);\n    try expectRGBAApprox(.{ gray_green, gray_green, gray_green, 1.0 }, buf.buffer.fg[1], 0.001);\n    try expectRGBAApprox(.{ gray_blue, gray_blue, gray_blue, 1.0 }, buf.buffer.fg[2], 0.001);\n}\n\ntest \"colorMatrixUniform - partial strength blends with original\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n    buf.buffer.fg[1] = red;\n\n    // Apply sepia at 50% strength\n    buffer_effects.colorMatrixUniform(buf, &SEPIA_MATRIX, 0.5, ColorTarget.FG);\n\n    // Expected: blend(original, sepia_result, 0.5)\n    // Sepia of pure red: R=0.393, G=0.349, B=0.272\n    // Blend: original + (sepia - original) * 0.5\n    const expected_r = 1.0 + (0.393 - 1.0) * 0.5;\n    const expected_g = 0.0 + (0.349 - 0.0) * 0.5;\n    const expected_b = 0.0 + (0.272 - 0.0) * 0.5;\n\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[0], 0.001);\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[1], 0.001);\n}\n\ntest \"colorMatrixUniform - target affects correct buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const blue = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    buf.buffer.fg[0] = red;\n    buf.buffer.bg[0] = blue;\n    buf.buffer.fg[1] = red;\n    buf.buffer.bg[1] = blue;\n\n    // Apply to FG only (target = 1)\n    buffer_effects.colorMatrixUniform(buf, &GRAYSCALE_MATRIX, 1.0, ColorTarget.FG);\n\n    // FG should be grayscale, BG should remain blue\n    const gray_red = 0.299 * 1.0;\n    try expectRGBAApprox(.{ gray_red, gray_red, gray_red, 1.0 }, buf.buffer.fg[0], 0.001);\n    try expectRGBAApprox(blue, buf.buffer.bg[0], 0.0001);\n\n    // Reset and test BG only (target = 2)\n    buf.buffer.fg[0] = red;\n    buf.buffer.bg[0] = blue;\n    buf.buffer.fg[1] = red;\n    buf.buffer.bg[1] = blue;\n\n    buffer_effects.colorMatrixUniform(buf, &GRAYSCALE_MATRIX, 1.0, ColorTarget.BG);\n\n    // BG should be grayscale, FG should remain red\n    const gray_blue = 0.114 * 1.0;\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n    try expectRGBAApprox(.{ gray_blue, gray_blue, gray_blue, 1.0 }, buf.buffer.bg[0], 0.001);\n\n    // Reset and test Both (target = 3)\n    buf.buffer.fg[0] = red;\n    buf.buffer.bg[0] = blue;\n    buf.buffer.fg[1] = red;\n    buf.buffer.bg[1] = blue;\n\n    buffer_effects.colorMatrixUniform(buf, &GRAYSCALE_MATRIX, 1.0, ColorTarget.Both);\n\n    // Both should be grayscale\n    try expectRGBAApprox(.{ gray_red, gray_red, gray_red, 1.0 }, buf.buffer.fg[0], 0.001);\n    try expectRGBAApprox(.{ gray_blue, gray_blue, gray_blue, 1.0 }, buf.buffer.bg[0], 0.001);\n}\n\ntest \"colorMatrixUniform - handles buffer sizes not divisible by 4\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    // Test with 5 pixels (1 SIMD batch of 4 + 1 scalar remainder)\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        5,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    // Set all FG to red\n    for (0..5) |i| {\n        buf.buffer.fg[i] = red;\n    }\n\n    // Apply sepia at full strength\n    buffer_effects.colorMatrixUniform(buf, &SEPIA_MATRIX, 1.0, ColorTarget.FG);\n\n    // All pixels should be transformed (including the scalar fallback)\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n\n    for (0..5) |i| {\n        try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[i], 0.001);\n    }\n}\n\ntest \"colorMatrixUniform - empty matrix returns early\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    // Empty matrix - should return early without changes\n    const empty_matrix = [0]f32{};\n    buffer_effects.colorMatrixUniform(buf, &empty_matrix, 1.0, ColorTarget.FG);\n\n    // Color should be unchanged\n    try expectRGBAApprox(red, buf.buffer.fg[0], 0.0001);\n}\n\ntest \"colorMatrixUniform - alpha channel transformation\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const opaque_color = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const transparent_color = RGBA{ 0.0, 1.0, 0.0, 0.5 };\n\n    try buf.clear(bg, null);\n\n    buf.buffer.fg[0] = opaque_color;\n    buf.buffer.fg[1] = transparent_color;\n\n    // Apply matrix that halves alpha at full strength\n    buffer_effects.colorMatrixUniform(buf, &ALPHA_MODIFY_MATRIX, 1.0, ColorTarget.FG);\n\n    // Opaque should become semi-transparent (alpha = 1.0 * 0.5 = 0.5)\n    try expectRGBAApprox(.{ 1.0, 0.0, 0.0, 0.5 }, buf.buffer.fg[0], 0.0001);\n    // Semi-transparent should become more transparent (alpha = 0.5 * 0.5 = 0.25)\n    try expectRGBAApprox(.{ 0.0, 1.0, 0.0, 0.25 }, buf.buffer.fg[1], 0.0001);\n}\n\ntest \"colorMatrixUniform - very small buffer (less than 4 pixels)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    // Test with 2 pixels (all scalar, no SIMD)\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const green = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n    buf.buffer.fg[1] = green;\n\n    // Apply sepia at full strength\n    buffer_effects.colorMatrixUniform(buf, &SEPIA_MATRIX, 1.0, ColorTarget.FG);\n\n    // Both pixels should be transformed correctly using scalar path\n    const expected_red_r = 0.393;\n    const expected_red_g = 0.349;\n    const expected_red_b = 0.272;\n    try expectRGBAApprox(.{ expected_red_r, expected_red_g, expected_red_b, 1.0 }, buf.buffer.fg[0], 0.001);\n\n    // Green transformed: R=0.769, G=0.686, B=0.534\n    const expected_green_r = 0.769;\n    const expected_green_g = 0.686;\n    const expected_green_b = 0.534;\n    try expectRGBAApprox(.{ expected_green_r, expected_green_g, expected_green_b, 1.0 }, buf.buffer.fg[1], 0.001);\n}\n\ntest \"colorMatrixUniform - single pixel buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    // Test with 1 pixel (edge case)\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        1,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = red;\n\n    // Apply sepia at full strength\n    buffer_effects.colorMatrixUniform(buf, &SEPIA_MATRIX, 1.0, ColorTarget.FG);\n\n    // Pixel should be transformed correctly\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n    try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[0], 0.001);\n}\n\ntest \"colorMatrixUniform - values can exceed 1.0 (no clamping)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    // Matrix that amplifies colors beyond 1.0\n    const amplify_matrix = [16]f32{\n        2.0, 0.0, 0.0, 0.0, // Red output (2x)\n        0.0, 2.0, 0.0, 0.0, // Green output (2x)\n        0.0, 0.0, 2.0, 0.0, // Blue output (2x)\n        0.0, 0.0, 0.0, 1.0, // Alpha output\n    };\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const gray = RGBA{ 0.5, 0.5, 0.5, 1.0 };\n\n    try buf.clear(bg, null);\n    buf.buffer.fg[0] = gray;\n\n    // Apply amplification at full strength\n    buffer_effects.colorMatrixUniform(buf, &amplify_matrix, 1.0, ColorTarget.FG);\n\n    // Values should exceed 1.0 (no clamping)\n    try expectRGBAApprox(.{ 1.0, 1.0, 1.0, 1.0 }, buf.buffer.fg[0], 0.0001);\n}\n\ntest \"colorMatrixUniform - 3 pixel buffer (simd_end = 0, all scalar)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    // 3 pixels - simd_end will be 0, so all processed via scalar\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        1,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const red = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    try buf.clear(bg, null);\n    for (0..3) |i| {\n        buf.buffer.fg[i] = red;\n    }\n\n    // Apply sepia\n    buffer_effects.colorMatrixUniform(buf, &SEPIA_MATRIX, 1.0, ColorTarget.FG);\n\n    // All 3 should be transformed via scalar path\n    const expected_r = 0.393;\n    const expected_g = 0.349;\n    const expected_b = 0.272;\n\n    for (0..3) |i| {\n        try expectRGBAApprox(.{ expected_r, expected_g, expected_b, 1.0 }, buf.buffer.fg[i], 0.001);\n    }\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/buffer_test.zig",
    "content": "const std = @import(\"std\");\nconst buffer_mod = @import(\"../buffer.zig\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\nconst ansi = @import(\"../ansi.zig\");\n\nconst OptimizedBuffer = buffer_mod.OptimizedBuffer;\nconst TextBuffer = text_buffer.UnifiedTextBuffer;\nconst TextBufferView = text_buffer_view.UnifiedTextBufferView;\nconst RGBA = buffer_mod.RGBA;\n\nfn initBufferForOomRegression(allocator: std.mem.Allocator) !void {\n    var local_pool = gp.GraphemePool.initWithOptions(allocator, .{});\n    defer local_pool.deinit();\n\n    var local_link_pool = link.LinkPool.init(allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        allocator,\n        1,\n        1,\n        .{ .pool = &local_pool, .id = \"oom-regression\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n}\n\ntest \"OptimizedBuffer - init frees allocations on OOM\" {\n    try std.testing.checkAllAllocationFailures(std.testing.allocator, initBufferForOomRegression, .{});\n}\n\ntest \"OptimizedBuffer - init and deinit\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        10,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    try std.testing.expectEqual(@as(u32, 10), buf.getWidth());\n    try std.testing.expectEqual(@as(u32, 10), buf.getHeight());\n}\n\ntest \"OptimizedBuffer - clear fills with default char\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        5,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var y: u32 = 0;\n    while (y < 5) : (y += 1) {\n        var x: u32 = 0;\n        while (x < 5) : (x += 1) {\n            const cell = buf.get(x, y).?;\n            try std.testing.expectEqual(@as(u32, 32), cell.char);\n        }\n    }\n}\n\ntest \"OptimizedBuffer - drawText with ASCII\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.drawText(\"Hello\", 0, 0, fg, bg, 0);\n\n    const cell_h = buf.get(0, 0).?;\n    try std.testing.expectEqual(@as(u32, 'H'), cell_h.char);\n\n    const cell_e = buf.get(1, 0).?;\n    try std.testing.expectEqual(@as(u32, 'e'), cell_e.char);\n}\n\ntest \"OptimizedBuffer - repeated emoji rendering should not exhaust pool\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 1000) : (i += 1) {\n        try buf.clear(bg, null);\n        try buf.drawText(\"🌟🎨🚀\", 0, 0, fg, bg, 0);\n    }\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(cell.char));\n}\n\ntest \"OptimizedBuffer - repeated CJK rendering should not exhaust pool\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 1000) : (i += 1) {\n        try buf.clear(bg, null);\n        try buf.drawText(\"测试文字\", 0, 0, fg, bg, 0);\n    }\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(cell.char));\n}\n\ntest \"OptimizedBuffer - drawTextBuffer repeatedly should not exhaust pool\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello 🌟 World\\n测试 🎨 Test\\n🚀 Rocket\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 1000) : (i += 1) {\n        try buf.clear(bg, null);\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n}\n\ntest \"OptimizedBuffer - mixed ASCII and emoji repeated rendering\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        40,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 500) : (i += 1) {\n        try buf.clear(bg, null);\n        try buf.drawText(\"A🌟B🎨C🚀D\", 0, 0, fg, bg, 0);\n        try buf.drawText(\"测试文字处理\", 0, 1, fg, bg, 0);\n        try buf.drawText(\"Hello World!\", 0, 2, fg, bg, 0);\n    }\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expectEqual(@as(u32, 'A'), cell.char);\n}\n\ntest \"OptimizedBuffer - overwriting graphemes repeatedly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 1000) : (i += 1) {\n        try buf.drawText(\"🌟\", 0, 0, fg, bg, 0);\n        try buf.drawText(\"🎨\", 0, 0, fg, bg, 0);\n        try buf.drawText(\"🚀\", 0, 0, fg, bg, 0);\n    }\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(cell.char));\n}\n\ntest \"OptimizedBuffer - rendering to different positions\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 100) : (i += 1) {\n        try buf.clear(bg, null);\n\n        var y: u32 = 0;\n        while (y < 20) : (y += 1) {\n            var x: u32 = 0;\n            while (x < 60) : (x += 10) {\n                try buf.drawText(\"🌟\", x, y, fg, bg, 0);\n            }\n        }\n    }\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(cell.char));\n}\n\ntest \"OptimizedBuffer - large text buffer with wrapping repeated render\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n\n    var line: u32 = 0;\n    while (line < 20) : (line += 1) {\n        try text_builder.appendSlice(std.testing.allocator, \"Line \");\n        try text_builder.writer(std.testing.allocator).print(\"{d}\", .{line});\n        try text_builder.appendSlice(std.testing.allocator, \": 🌟 测试 🎨 Test 🚀\\n\");\n    }\n\n    try tb.setText(text_builder.items);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(40);\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        50,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 200) : (i += 1) {\n        try buf.clear(bg, null);\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n}\n\ntest \"OptimizedBuffer - grapheme tracker counts\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    try buf.drawText(\"🌟🎨🚀\", 0, 0, fg, bg, 0);\n\n    const count_after_draw = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count_after_draw > 0);\n    try std.testing.expect(count_after_draw <= 10);\n\n    var i: u32 = 0;\n    while (i < 100) : (i += 1) {\n        try buf.clear(bg, null);\n        try buf.drawText(\"🌟🎨🚀\", 0, 0, fg, bg, 0);\n    }\n\n    const count_after_repeated = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count_after_repeated <= 20);\n}\n\ntest \"OptimizedBuffer - alternating emojis should not leak\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 500) : (i += 1) {\n        if (i % 2 == 0) {\n            try buf.drawText(\"🌟🎨🚀\", 0, 0, fg, bg, 0);\n        } else {\n            try buf.drawText(\"🍕🍔🍟\", 0, 0, fg, bg, 0);\n        }\n    }\n\n    const count = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count <= 20);\n}\n\ntest \"OptimizedBuffer - drawTextBuffer without clear should not exhaust pool\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"🌟🎨🚀\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var i: u32 = 0;\n    while (i < 2000) : (i += 1) {\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n\n    const count = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count < 100);\n}\n\ntest \"OptimizedBuffer - many small graphemes without clear\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"• • • •\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var i: u32 = 0;\n    while (i < 5000) : (i += 1) {\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n\n    const count = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count < 200);\n}\n\ntest \"OptimizedBuffer - stress test with many graphemes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n\n    var line: u32 = 0;\n    while (line < 10) : (line += 1) {\n        try text_builder.appendSlice(std.testing.allocator, \"🌟🎨🚀🍕🍔🍟🌈🎭🎪🎨🎬🎤🎧🎼🎹🎺🎸🎻\\n\");\n    }\n\n    try tb.setText(text_builder.items);\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var i: u32 = 0;\n    while (i < 1000) : (i += 1) {\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n\n    const count = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count > 0);\n    try std.testing.expect(count < 1000);\n\n    const first_cell = buf.get(0, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(first_cell.char));\n}\n\ntest \"OptimizedBuffer - pool slot exhaustion test\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"• • • • • • • • • •\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    var i: u32 = 0;\n    while (i < 10000) : (i += 1) {\n        if (i % 100 == 0) {\n            try buf.clear(bg, null);\n        }\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(cell.char));\n\n    const count = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count > 0);\n    try std.testing.expect(count < 500);\n}\n\ntest \"OptimizedBuffer - many unique graphemes with small pool\" {\n    const tiny_slots = [_]u32{ 4, 4, 4, 4, 4 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    var render_count: u32 = 0;\n    var failure_count: u32 = 0;\n\n    while (render_count < 1000) : (render_count += 1) {\n        var text_builder: std.ArrayListUnmanaged(u8) = .{};\n        defer text_builder.deinit(std.testing.allocator);\n\n        const base_codepoint: u21 = 0x2600 + @as(u21, @intCast(render_count % 500));\n        const char_bytes = [_]u8{\n            @intCast(0xE0 | (base_codepoint >> 12)),\n            @intCast(0x80 | ((base_codepoint >> 6) & 0x3F)),\n            @intCast(0x80 | (base_codepoint & 0x3F)),\n        };\n        try text_builder.appendSlice(std.testing.allocator, &char_bytes);\n        try text_builder.appendSlice(std.testing.allocator, \" \");\n        try text_builder.appendSlice(std.testing.allocator, &char_bytes);\n\n        tb.setText(text_builder.items) catch {\n            failure_count += 1;\n            continue;\n        };\n\n        if (render_count % 50 == 0) {\n            try buf.clear(bg, null);\n            tb.reset();\n        }\n\n        buf.drawTextBuffer(view, 0, 0) catch {\n            failure_count += 1;\n            continue;\n        };\n    }\n\n    try std.testing.expect(failure_count == 0);\n}\n\ntest \"OptimizedBuffer - continuous rendering without buffer recreation\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"• Hello World •\\n• Test Line •\\n• Another Line •\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    var i: u32 = 0;\n    while (i < 50000) : (i += 1) {\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n}\n\ntest \"OptimizedBuffer - multiple buffers rendering same TextBuffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"🌟 • 测试 • 🎨\");\n\n    var buf1 = try OptimizedBuffer.init(\n        std.testing.allocator,\n        40,\n        10,\n        .{ .pool = pool, .id = \"buffer-1\" },\n    );\n    defer buf1.deinit();\n\n    var buf2 = try OptimizedBuffer.init(\n        std.testing.allocator,\n        40,\n        10,\n        .{ .pool = pool, .id = \"buffer-2\" },\n    );\n    defer buf2.deinit();\n\n    var buf3 = try OptimizedBuffer.init(\n        std.testing.allocator,\n        40,\n        10,\n        .{ .pool = pool, .id = \"buffer-3\" },\n    );\n    defer buf3.deinit();\n\n    var i: u32 = 0;\n    while (i < 5000) : (i += 1) {\n        try buf1.drawTextBuffer(view, 0, 0);\n        try buf2.drawTextBuffer(view, 0, 0);\n        try buf3.drawTextBuffer(view, 0, 0);\n    }\n}\n\ntest \"OptimizedBuffer - continuous render without clear with small pool\" {\n    const tiny_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"• Test •\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var i: u32 = 0;\n    while (i < 100) : (i += 1) {\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n}\n\ntest \"OptimizedBuffer - graphemes with scissor clipping and small pool\" {\n    const tiny_slots = [_]u32{ 3, 3, 3, 3, 3 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"• • • • •\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    try buf.pushScissorRect(0, 0, 5, 5);\n\n    var i: u32 = 0;\n    while (i < 100) : (i += 1) {\n        try buf.drawTextBuffer(view, 20, 20);\n    }\n}\n\ntest \"OptimizedBuffer - drawText with alpha blending and scissor\" {\n    const tiny_slots = [_]u32{ 3, 3, 3, 3, 3 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg_alpha = RGBA{ 0.0, 0.0, 0.0, 0.5 };\n\n    try buf.clear(bg, null);\n\n    try buf.pushScissorRect(0, 0, 10, 10);\n\n    var i: u32 = 0;\n    while (i < 200) : (i += 1) {\n        try buf.drawText(\"• • • •\", 50, 0, fg, bg_alpha, 0);\n    }\n}\n\ntest \"OptimizedBuffer - many unique graphemes with alpha and small pool\" {\n    const tiny_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg_alpha = RGBA{ 0.0, 0.0, 0.0, 0.5 };\n\n    try buf.clear(bg, null);\n\n    var i: u32 = 0;\n    while (i < 50) : (i += 1) {\n        const base_codepoint: u21 = 0x2600 + @as(u21, @intCast(i));\n        const char_bytes = [_]u8{\n            @intCast(0xE0 | (base_codepoint >> 12)),\n            @intCast(0x80 | ((base_codepoint >> 6) & 0x3F)),\n            @intCast(0x80 | (base_codepoint & 0x3F)),\n        };\n\n        var text: [4]u8 = undefined;\n        @memcpy(text[0..3], &char_bytes);\n        text[3] = ' ';\n\n        try buf.drawText(&text, @intCast(i % 70), @intCast(i / 70), fg, bg_alpha, 0);\n    }\n}\n\ntest \"OptimizedBuffer - fill buffer with many unique graphemes\" {\n    const tiny_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        40,\n        20,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    var char_idx: u32 = 0;\n    var y: u32 = 0;\n    while (y < 15) : (y += 1) {\n        var x: u32 = 0;\n        while (x < 35) : (x += 2) {\n            const base_codepoint: u21 = 0x2600 + @as(u21, @intCast(char_idx % 200));\n            const char_bytes = [_]u8{\n                @intCast(0xE0 | (base_codepoint >> 12)),\n                @intCast(0x80 | ((base_codepoint >> 6) & 0x3F)),\n                @intCast(0x80 | (base_codepoint & 0x3F)),\n            };\n\n            try buf.drawText(&char_bytes, x, y, fg, bg, 0);\n\n            char_idx += 1;\n        }\n    }\n}\n\ntest \"OptimizedBuffer - verify pool growth works correctly\" {\n    const one_slot = [_]u32{ 1, 1, 1, 1, 1 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = one_slot,\n    });\n    defer local_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    try buf.clear(bg, null);\n\n    var char_idx: u32 = 0;\n    while (char_idx < 150) : (char_idx += 1) {\n        const base_codepoint: u21 = 0x2600 + @as(u21, @intCast(char_idx));\n        const char_bytes = [_]u8{\n            @intCast(0xE0 | (base_codepoint >> 12)),\n            @intCast(0x80 | ((base_codepoint >> 6) & 0x3F)),\n            @intCast(0x80 | (base_codepoint & 0x3F)),\n        };\n\n        const x = @as(u32, @intCast((char_idx * 2) % 70));\n        const y = @as(u32, @intCast((char_idx * 2) / 70));\n\n        try buf.drawText(&char_bytes, x, y, fg, bg, 0);\n    }\n}\n\ntest \"OptimizedBuffer - repeated overwriting of same grapheme\" {\n    const tiny_slots = [_]u32{ 3, 3, 3, 3, 3 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    try buf.drawText(\"•\", 0, 0, fg, bg, 0);\n\n    var i: u32 = 0;\n    while (i < 500) : (i += 1) {\n        try buf.drawText(\"•\", 0, 0, fg, bg, 0);\n    }\n\n    try std.testing.expect(buf.grapheme_tracker.getGraphemeCount() <= 2);\n}\n\ntest \"OptimizedBuffer - two-buffer pattern should not leak\" {\n    const tiny_slots = [_]u32{ 4, 4, 4, 4, 4 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var nextBuffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = &local_pool, .id = \"next-buffer\" },\n    );\n    defer nextBuffer.deinit();\n\n    var currentBuffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = &local_pool, .id = \"current-buffer\" },\n    );\n    defer currentBuffer.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    var frame: u32 = 0;\n    while (frame < 100) : (frame += 1) {\n        try nextBuffer.drawText(\"• Test •\", 0, 0, fg, bg, 0);\n\n        const cell = nextBuffer.get(0, 0).?;\n        currentBuffer.setRaw(0, 0, cell);\n\n        try nextBuffer.clear(bg, null);\n    }\n}\n\ntest \"OptimizedBuffer - set and clear cycle should not leak\" {\n    const tiny_slots = [_]u32{ 3, 3, 3, 3, 3 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    var frame: u32 = 0;\n    while (frame < 200) : (frame += 1) {\n        try buf.drawText(\"•\", 0, 0, fg, bg, 0);\n        try buf.clear(bg, null);\n    }\n}\n\ntest \"OptimizedBuffer - repeated drawTextBuffer without clear should not leak\" {\n    const tiny_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"• Hello • World •\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"render-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var frame: u32 = 0;\n    while (frame < 500) : (frame += 1) {\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n}\n\ntest \"OptimizedBuffer - renderer two-buffer swap pattern should not leak\" {\n    const tiny_slots = [_]u32{ 3, 3, 3, 3, 3 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"• • •\");\n\n    var current = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = &local_pool, .id = \"current\" },\n    );\n    defer current.deinit();\n\n    var next = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = &local_pool, .id = \"next\" },\n    );\n    defer next.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try current.clear(bg, null);\n\n    var frame: u32 = 0;\n    while (frame < 300) : (frame += 1) {\n        try next.drawTextBuffer(view, 0, 0);\n\n        var x: u32 = 0;\n        while (x < 10) : (x += 1) {\n            if (next.get(x, 0)) |cell| {\n                current.setRaw(x, 0, cell);\n            }\n        }\n\n        try next.clear(bg, null);\n    }\n}\n\ntest \"OptimizedBuffer - set should not clear newly written adjacent grapheme continuation\" {\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{});\n    defer local_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        8,\n        1,\n        .{ .pool = &local_pool, .id = \"set-adjacent-grapheme\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.clear(bg, null);\n\n    const old_gid = try local_pool.alloc(\"🌟\");\n    const old_start = gp.packGraphemeStart(old_gid & gp.GRAPHEME_ID_MASK, 2);\n    buf.set(3, 0, .{ .char = old_start, .fg = fg, .bg = bg, .attributes = 0 });\n\n    const new_gid = try local_pool.alloc(\"🔥\");\n    const new_start = gp.packGraphemeStart(new_gid & gp.GRAPHEME_ID_MASK, 2);\n\n    // Simulate renderer's left-to-right in-place update:\n    // - x=2 writes a new grapheme (which writes continuation at x=3)\n    // - x=3 would be skipped by char-equality\n    // - x=4 overwrites an old continuation from the previous frame\n    // The overwrite at x=4 must not clear the new continuation at x=3.\n    buf.set(2, 0, .{ .char = new_start, .fg = fg, .bg = bg, .attributes = 0 });\n    buf.set(4, 0, .{ .char = ' ', .fg = fg, .bg = bg, .attributes = 0 });\n\n    const c2 = buf.get(2, 0).?;\n    const c3 = buf.get(3, 0).?;\n    const c4 = buf.get(4, 0).?;\n\n    try std.testing.expect(gp.isGraphemeChar(c2.char));\n    try std.testing.expect(gp.graphemeIdFromChar(c2.char) == (new_gid & gp.GRAPHEME_ID_MASK));\n\n    try std.testing.expect(gp.isContinuationChar(c3.char));\n    try std.testing.expect(gp.graphemeIdFromChar(c3.char) == (new_gid & gp.GRAPHEME_ID_MASK));\n\n    try std.testing.expect(c4.char == ' ');\n}\n\ntest \"OptimizedBuffer - set span cleanup keeps shared link refcounts consistent\" {\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{});\n    defer local_pool.deinit();\n\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = &local_pool, .id = \"set-span-link-refcount\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.clear(bg, null);\n\n    const link_id = try local_link_pool.alloc(\"https://example.com\");\n    const linked_attr = ansi.TextAttributes.setLinkId(0, link_id);\n\n    const gid = try local_pool.alloc(\"你\");\n    const start = gp.packGraphemeStart(gid & gp.GRAPHEME_ID_MASK, 2);\n\n    // Create three linked cells total:\n    // - a 2-cell grapheme span at x=2..3\n    // - one additional linked cell at x=6\n    buf.set(2, 0, .{ .char = start, .fg = fg, .bg = bg, .attributes = linked_attr });\n    buf.set(6, 0, .{ .char = 'X', .fg = fg, .bg = bg, .attributes = linked_attr });\n\n    try std.testing.expectEqual(@as(u32, 3), buf.link_tracker.used_ids.get(link_id).?);\n    try std.testing.expectEqual(@as(u32, 1), try local_link_pool.getRefcount(link_id));\n\n    // Overwrite the continuation cell at x=3 with a non-grapheme char.\n    // set() will run span cleanup and clear x=2..3. The independent linked\n    // cell at x=6 must remain tracked.\n    buf.set(3, 0, .{ .char = ' ', .fg = fg, .bg = bg, .attributes = 0 });\n\n    try std.testing.expectEqual(@as(u32, 1), buf.link_tracker.getLinkCount());\n    try std.testing.expectEqual(@as(u32, 1), buf.link_tracker.used_ids.get(link_id).?);\n    try std.testing.expectEqual(@as(u32, 1), try local_link_pool.getRefcount(link_id));\n}\n\ntest \"OptimizedBuffer - syncCell updates grapheme tracker for start transitions\" {\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{});\n    defer local_pool.deinit();\n\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = &local_pool, .id = \"sync-cell-grapheme-tracker\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.clear(bg, null);\n\n    const gid_old = try local_pool.alloc(\"你\");\n    const gid_new = try local_pool.alloc(\"好\");\n    const old_id = gid_old & gp.GRAPHEME_ID_MASK;\n    const new_id = gid_new & gp.GRAPHEME_ID_MASK;\n    const start_old = gp.packGraphemeStart(old_id, 2);\n    const start_new = gp.packGraphemeStart(new_id, 2);\n\n    buf.syncCell(1, 0, .{ .char = start_old, .fg = fg, .bg = bg, .attributes = 0 });\n    try std.testing.expectEqual(@as(u32, 1), buf.grapheme_tracker.getGraphemeCount());\n    try std.testing.expect(buf.grapheme_tracker.contains(old_id));\n\n    buf.syncCell(1, 0, .{ .char = start_new, .fg = fg, .bg = bg, .attributes = 0 });\n    try std.testing.expectEqual(@as(u32, 1), buf.grapheme_tracker.getGraphemeCount());\n    try std.testing.expect(!buf.grapheme_tracker.contains(old_id));\n    try std.testing.expect(buf.grapheme_tracker.contains(new_id));\n\n    buf.syncCell(1, 0, .{ .char = ' ', .fg = fg, .bg = bg, .attributes = 0 });\n    try std.testing.expectEqual(@as(u32, 0), buf.grapheme_tracker.getGraphemeCount());\n    try std.testing.expect(!buf.grapheme_tracker.contains(new_id));\n}\n\ntest \"OptimizedBuffer - sustained rendering should not leak\" {\n    const tiny_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"  • Type any text to insert\\n  • Arrow keys to move cursor\\n  • Backspace/Delete to remove text\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"render-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var frame: u32 = 0;\n    while (frame < 3000) : (frame += 1) {\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n}\n\ntest \"OptimizedBuffer - rendering with changing content should not leak\" {\n    const tiny_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer local_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"render-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var frame: u32 = 0;\n    while (frame < 100) : (frame += 1) {\n        const char_idx = frame % 10;\n        const base_codepoint: u21 = 0x2600 + @as(u21, @intCast(char_idx));\n        const char_bytes = [_]u8{\n            @intCast(0xE0 | (base_codepoint >> 12)),\n            @intCast(0x80 | ((base_codepoint >> 6) & 0x3F)),\n            @intCast(0x80 | (base_codepoint & 0x3F)),\n        };\n\n        var text: [11]u8 = undefined;\n        @memcpy(text[0..3], &char_bytes);\n        text[3] = ' ';\n        @memcpy(text[4..7], &char_bytes);\n        text[7] = ' ';\n        @memcpy(text[8..11], &char_bytes);\n\n        tb.setText(&text) catch continue;\n\n        try buf.drawTextBuffer(view, 0, 0);\n    }\n}\n\ntest \"OptimizedBuffer - multiple TextBuffers rendering simultaneously should not leak\" {\n    const one_slot = [_]u32{ 1, 1, 1, 1, 1 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = one_slot,\n    });\n    defer local_pool.deinit();\n\n    var tb1 = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb1.deinit();\n    var view1 = try TextBufferView.init(std.testing.allocator, tb1);\n    defer view1.deinit();\n\n    var tb2 = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb2.deinit();\n    var view2 = try TextBufferView.init(std.testing.allocator, tb2);\n    defer view2.deinit();\n\n    var tb3 = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb3.deinit();\n    var view3 = try TextBufferView.init(std.testing.allocator, tb3);\n    defer view3.deinit();\n\n    try tb1.setText(\"• First •\");\n    try tb2.setText(\"• Second •\");\n    try tb3.setText(\"• Third •\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        30,\n        .{ .pool = &local_pool, .id = \"main-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    var frame: u32 = 0;\n    while (frame < 500) : (frame += 1) {\n        try buf.drawTextBuffer(view1, 0, 0);\n        try buf.drawTextBuffer(view2, 0, 10);\n        try buf.drawTextBuffer(view3, 0, 20);\n    }\n}\n\ntest \"OptimizedBuffer - grapheme refcount management\" {\n    const two_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = two_slots,\n    });\n    defer local_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        5,\n        1,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    try buf.drawText(\"•\", 0, 0, fg, bg, 0);\n    const initial_cell = buf.get(0, 0).?;\n    const initial_id = gp.graphemeIdFromChar(initial_cell.char);\n    const initial_refcount = local_pool.getRefcount(initial_id) catch 0;\n\n    try std.testing.expectEqual(@as(u32, 1), initial_refcount);\n\n    var i: u32 = 0;\n    while (i < 100) : (i += 1) {\n        try buf.drawText(\"•\", 0, 0, fg, bg, 0);\n\n        const cell = buf.get(0, 0).?;\n        const id = gp.graphemeIdFromChar(cell.char);\n        const rc = local_pool.getRefcount(id) catch 999;\n        const slot = id & 0xFFFF;\n\n        try std.testing.expectEqual(@as(u32, 1), rc);\n        try std.testing.expect(slot == 0 or slot == 1);\n    }\n}\n\ntest \"OptimizedBuffer - drawTextBuffer with graphemes then clear removes all pool references\" {\n    const small_slots = [_]u32{ 4, 4, 4, 4, 4 };\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = small_slots,\n    });\n    defer local_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, &local_pool, link.initGlobalLinkPool(std.testing.allocator), .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"• Test • 🌟 • 🎨 •\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = &local_pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    try buf.drawTextBuffer(view, 0, 0);\n\n    const count_after_draw = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count_after_draw > 0);\n\n    var total_allocated_slots: u32 = 0;\n    var total_free_slots: u32 = 0;\n    for (local_pool.classes) |class| {\n        total_allocated_slots += class.num_slots;\n        total_free_slots += @intCast(class.free_list.items.len);\n    }\n    const slots_in_use_after_draw = total_allocated_slots - total_free_slots;\n    try std.testing.expect(slots_in_use_after_draw > 0);\n\n    try buf.clear(bg, null);\n\n    const count_after_clear = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expectEqual(@as(u32, 0), count_after_clear);\n\n    var total_allocated_after_clear: u32 = 0;\n    var total_free_after_clear: u32 = 0;\n    for (local_pool.classes) |class| {\n        total_allocated_after_clear += class.num_slots;\n        total_free_after_clear += @intCast(class.free_list.items.len);\n    }\n    try std.testing.expectEqual(total_allocated_after_clear, total_free_after_clear);\n\n    var y: u32 = 0;\n    while (y < 5) : (y += 1) {\n        var x: u32 = 0;\n        while (x < 20) : (x += 1) {\n            const cell = buf.get(x, y).?;\n            try std.testing.expectEqual(@as(u32, 32), cell.char);\n            try std.testing.expect(!gp.isGraphemeChar(cell.char));\n            try std.testing.expect(!gp.isContinuationChar(cell.char));\n        }\n    }\n\n    try buf.drawTextBuffer(view, 0, 0);\n    const count_after_redraw = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expect(count_after_redraw > 0);\n\n    var allocated_after_redraw: u32 = 0;\n    var free_after_redraw: u32 = 0;\n    for (local_pool.classes) |class| {\n        allocated_after_redraw += class.num_slots;\n        free_after_redraw += @intCast(class.free_list.items.len);\n    }\n    const slots_in_use_after_redraw = allocated_after_redraw - free_after_redraw;\n    try std.testing.expect(slots_in_use_after_redraw > 0);\n\n    try buf.clear(bg, null);\n    const count_after_second_clear = buf.grapheme_tracker.getGraphemeCount();\n    try std.testing.expectEqual(@as(u32, 0), count_after_second_clear);\n\n    var allocated_after_second_clear: u32 = 0;\n    var free_after_second_clear: u32 = 0;\n    for (local_pool.classes) |class| {\n        allocated_after_second_clear += class.num_slots;\n        free_after_second_clear += @intCast(class.free_list.items.len);\n    }\n    try std.testing.expectEqual(allocated_after_second_clear, free_after_second_clear);\n}\n\ntest \"OptimizedBuffer - drawTextBuffer with negative y coordinate should not panic\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\");\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        25,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    // Draw text buffer at negative y coordinate (-2)\n    // This simulates a scenario where content is scrolled partially off-screen\n    // The first 2 lines should be clipped, and lines 3, 4, 5 should be visible\n    try buf.drawTextBuffer(view, 0, -2);\n\n    // Verify that content is properly clipped when drawn at negative y\n    // Lines that are off-screen (negative y) should be skipped\n    // Line 3 should appear at y=0, Line 4 at y=1, Line 5 at y=2\n\n    // Check that Line 3 is rendered at y=0\n    const cell_y0 = buf.get(0, 0).?;\n    try std.testing.expectEqual(@as(u32, 'L'), cell_y0.char);\n\n    // Check that Line 4 is rendered at y=1\n    const cell_y1 = buf.get(0, 1).?;\n    try std.testing.expectEqual(@as(u32, 'L'), cell_y1.char);\n\n    // Check that Line 5 is rendered at y=2\n    const cell_y2 = buf.get(0, 2).?;\n    try std.testing.expectEqual(@as(u32, 'L'), cell_y2.char);\n\n    // Verify the full content of the first visible line (Line 3)\n    try std.testing.expectEqual(@as(u32, 'L'), buf.get(0, 0).?.char);\n    try std.testing.expectEqual(@as(u32, 'i'), buf.get(1, 0).?.char);\n    try std.testing.expectEqual(@as(u32, 'n'), buf.get(2, 0).?.char);\n    try std.testing.expectEqual(@as(u32, 'e'), buf.get(3, 0).?.char);\n    try std.testing.expectEqual(@as(u32, ' '), buf.get(4, 0).?.char);\n    try std.testing.expectEqual(@as(u32, '3'), buf.get(5, 0).?.char);\n}\n\ntest \"OptimizedBuffer - cells are initialized after resize grow\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        10,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    try buf.resize(20, 20);\n\n    // Verify new cells have default values (space = 32), not garbage\n    const cell = buf.get(15, 15);\n    try std.testing.expect(cell != null);\n    try std.testing.expectEqual(@as(u32, 32), cell.?.char);\n}\n\ntest \"OptimizedBuffer - link encoding round-trip\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.clear(bg, null);\n\n    // Allocate a link\n    const link_id = try local_link_pool.alloc(\"https://example.com\");\n    const attributes = ansi.TextAttributes.setLinkId(ansi.TextAttributes.BOLD, link_id);\n\n    // Draw text with link\n    try buf.drawText(\"Click\", 0, 0, fg, bg, attributes);\n\n    // Verify cell has correct char and attributes\n    const cell = buf.get(0, 0).?;\n    try std.testing.expectEqual(@as(u32, 'C'), cell.char);\n    try std.testing.expectEqual(ansi.TextAttributes.BOLD, ansi.TextAttributes.getBaseAttributes(cell.attributes));\n    try std.testing.expectEqual(link_id, ansi.TextAttributes.getLinkId(cell.attributes));\n\n    // Verify link tracker has the link\n    try std.testing.expect(buf.link_tracker.hasAny());\n    try std.testing.expectEqual(@as(u32, 1), buf.link_tracker.getLinkCount());\n}\n\ntest \"OptimizedBuffer - link tracker per-cell counting\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.clear(bg, null);\n\n    // Allocate a link\n    const link_id = try local_link_pool.alloc(\"https://example.com\");\n    const attributes = ansi.TextAttributes.setLinkId(0, link_id);\n\n    // Draw text covering 3 cells\n    try buf.drawText(\"ABC\", 0, 0, fg, bg, attributes);\n\n    // Verify link tracker has 1 unique link\n    // Pool refcount is 1 (tracker owns one ref, tracks 3 cells internally)\n    try std.testing.expectEqual(@as(u32, 1), buf.link_tracker.getLinkCount());\n    const pool_refcount = try local_link_pool.getRefcount(link_id);\n    try std.testing.expectEqual(@as(u32, 1), pool_refcount);\n\n    // Verify tracker knows about 3 cells\n    const cell_count = buf.link_tracker.used_ids.get(link_id).?;\n    try std.testing.expectEqual(@as(u32, 3), cell_count);\n\n    // Overwrite one cell without link\n    try buf.drawText(\"X\", 0, 0, fg, bg, 0);\n\n    // Tracker cell count should drop to 2, pool refcount stays 1\n    const cell_count2 = buf.link_tracker.used_ids.get(link_id).?;\n    try std.testing.expectEqual(@as(u32, 2), cell_count2);\n    const pool_refcount2 = try local_link_pool.getRefcount(link_id);\n    try std.testing.expectEqual(@as(u32, 1), pool_refcount2);\n\n    // Clear all - refcount should be 0 and link freed\n    try buf.clear(bg, null);\n    try std.testing.expectEqual(@as(u32, 0), buf.link_tracker.getLinkCount());\n}\n\ntest \"OptimizedBuffer - fillRect removes links\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.clear(bg, null);\n\n    // Allocate a link\n    const link_id = try local_link_pool.alloc(\"https://example.com\");\n    const attributes = ansi.TextAttributes.setLinkId(0, link_id);\n\n    // Draw linked text\n    try buf.drawText(\"Linked\", 0, 0, fg, bg, attributes);\n    try buf.drawText(\"Text\", 10, 0, fg, bg, attributes);\n\n    // Verify links exist\n    try std.testing.expect(ansi.TextAttributes.hasLink(buf.get(0, 0).?.attributes));\n    try std.testing.expect(ansi.TextAttributes.hasLink(buf.get(10, 0).?.attributes));\n\n    // Fill rect over first link\n    try buf.fillRect(0, 0, 6, 1, bg);\n\n    // Cells in rect should have no link\n    try std.testing.expect(!ansi.TextAttributes.hasLink(buf.get(0, 0).?.attributes));\n    try std.testing.expect(!ansi.TextAttributes.hasLink(buf.get(5, 0).?.attributes));\n\n    // Cells outside rect should preserve link\n    try std.testing.expect(ansi.TextAttributes.hasLink(buf.get(10, 0).?.attributes));\n}\n\ntest \"OptimizedBuffer - link reuse after free\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n\n    // Allocate first link\n    const link_id1 = try local_link_pool.alloc(\"https://first.com\");\n    const attr1 = ansi.TextAttributes.setLinkId(0, link_id1);\n    try buf.drawText(\"A\", 0, 0, fg, bg, attr1);\n\n    // Clear - should free the link\n    try buf.clear(bg, null);\n\n    // Allocate second link - should reuse same slot but different generation\n    const link_id2 = try local_link_pool.alloc(\"https://second.com\");\n    try std.testing.expect(link_id1 != link_id2); // Different due to generation\n\n    const attr2 = ansi.TextAttributes.setLinkId(0, link_id2);\n    try buf.drawText(\"B\", 0, 0, fg, bg, attr2);\n\n    const url = try local_link_pool.get(link_id2);\n    try std.testing.expect(std.mem.eql(u8, url, \"https://second.com\"));\n}\n\ntest \"OptimizedBuffer - alpha blending preserves overlay link not dest link\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n\n    const bg_opaque = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const bg_alpha = RGBA{ 0.5, 0.5, 0.5, 0.5 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.clear(bg_opaque, null);\n\n    // Draw underlying text with link A\n    const link_id_a = try local_link_pool.alloc(\"https://underlying.com\");\n    const attr_a = ansi.TextAttributes.setLinkId(ansi.TextAttributes.BOLD, link_id_a);\n    try buf.drawText(\"X\", 5, 0, fg, bg_opaque, attr_a);\n\n    // Verify dest cell has link A\n    const dest_cell = buf.get(5, 0).?;\n    try std.testing.expectEqual(link_id_a, ansi.TextAttributes.getLinkId(dest_cell.attributes));\n    try std.testing.expectEqual(@as(u32, 'X'), dest_cell.char);\n\n    // Draw space with alpha and link B over it (will preserve 'X' but blend colors)\n    const link_id_b = try local_link_pool.alloc(\"https://overlay.com\");\n    const attr_b = ansi.TextAttributes.setLinkId(0, link_id_b);\n    try buf.drawText(\" \", 5, 0, fg, bg_alpha, attr_b);\n\n    // Result: char should be preserved 'X', but link should be from overlay (B), not dest (A)\n    const result_cell = buf.get(5, 0).?;\n    try std.testing.expectEqual(@as(u32, 'X'), result_cell.char);\n    try std.testing.expectEqual(link_id_b, ansi.TextAttributes.getLinkId(result_cell.attributes));\n    try std.testing.expect(ansi.TextAttributes.getLinkId(result_cell.attributes) != link_id_a);\n}\n\ntest \"OptimizedBuffer - alpha blending with no link clears underlying link\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\", .link_pool = &local_link_pool },\n    );\n    defer buf.deinit();\n\n    const bg_opaque = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const bg_alpha = RGBA{ 0.5, 0.5, 0.5, 0.5 };\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    try buf.clear(bg_opaque, null);\n\n    // Draw underlying text with link\n    const link_id = try local_link_pool.alloc(\"https://underlying.com\");\n    const attr_link = ansi.TextAttributes.setLinkId(ansi.TextAttributes.BOLD, link_id);\n    try buf.drawText(\"X\", 5, 0, fg, bg_opaque, attr_link);\n\n    // Verify dest cell has link\n    const dest_cell = buf.get(5, 0).?;\n    try std.testing.expectEqual(link_id, ansi.TextAttributes.getLinkId(dest_cell.attributes));\n\n    // Draw space with alpha but NO link over it (will preserve 'X')\n    try buf.drawText(\" \", 5, 0, fg, bg_alpha, 0);\n\n    // Result: char 'X' preserved, but link should be CLEARED (0), not preserved\n    const result_cell = buf.get(5, 0).?;\n    try std.testing.expectEqual(@as(u32, 'X'), result_cell.char);\n    try std.testing.expectEqual(@as(u32, 0), ansi.TextAttributes.getLinkId(result_cell.attributes));\n\n    // Link should no longer be tracked\n    try std.testing.expect(!ansi.TextAttributes.hasLink(result_cell.attributes));\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer basic rendering\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    // Create a 3x3 intensity buffer with varying values\n    const intensities = [_]f32{\n        0.0,  0.5,  1.0,\n        0.25, 0.75, 0.0,\n        1.0,  0.0,  0.5,\n    };\n\n    buf.drawGrayscaleBuffer(2, 1, &intensities, 3, 3, null, bg);\n\n    const cell_0_0 = buf.get(2, 1).?;\n    try std.testing.expectEqual(@as(u32, 32), cell_0_0.char);\n\n    const cell_1_0 = buf.get(3, 1).?;\n    try std.testing.expect(cell_1_0.char != 32);\n    try std.testing.expect(cell_1_0.fg[0] > 0.3);\n\n    const cell_2_0 = buf.get(4, 1).?;\n    try std.testing.expect(cell_2_0.char != 32);\n    try std.testing.expect(cell_2_0.fg[0] > 0.9);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer negative position clipping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    // Create a 4x4 intensity buffer\n    const intensities = [_]f32{\n        0.5, 0.5, 0.5, 0.5,\n        0.5, 0.5, 0.5, 0.5,\n        0.5, 0.5, 0.5, 0.5,\n        0.5, 0.5, 0.5, 0.5,\n    };\n\n    buf.drawGrayscaleBuffer(-1, -1, &intensities, 4, 4, null, bg);\n\n    const cell_0_0 = buf.get(0, 0).?;\n    try std.testing.expect(cell_0_0.char != 32);\n\n    const cell_2_0 = buf.get(2, 0).?;\n    try std.testing.expect(cell_2_0.char != 32);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer negative position fully clipped\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        6,\n        3,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n    };\n\n    buf.drawGrayscaleBuffer(-10, -10, &intensities, 4, 4, null, bg);\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expectEqual(@as(u32, 32), cell.char);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer respects scissor rect\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    try buf.pushScissorRect(0, 0, 2, 2);\n\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n    };\n\n    buf.drawGrayscaleBuffer(0, 0, &intensities, 4, 4, null, bg);\n\n    const cell_0_0 = buf.get(0, 0).?;\n    const cell_1_1 = buf.get(1, 1).?;\n    try std.testing.expect(cell_0_0.char != 32);\n    try std.testing.expect(cell_1_1.char != 32);\n\n    const cell_3_3 = buf.get(3, 3).?;\n    try std.testing.expectEqual(@as(u32, 32), cell_3_3.char);\n\n    buf.popScissorRect();\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer intensity to character mapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    const intensities = [_]f32{\n        0.005,\n        0.02,\n        0.5,\n        1.0,\n    };\n\n    buf.drawGrayscaleBuffer(0, 0, &intensities, 4, 1, null, bg);\n\n    const cell_0 = buf.get(0, 0).?;\n    try std.testing.expectEqual(@as(u32, 32), cell_0.char);\n\n    const cell_1 = buf.get(1, 0).?;\n    try std.testing.expect(cell_1.char != 32);\n\n    const cell_3 = buf.get(3, 0).?;\n    try std.testing.expect(cell_3.fg[0] > 0.9);\n    try std.testing.expect(cell_3.fg[1] > 0.9);\n    try std.testing.expect(cell_3.fg[2] > 0.9);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer alpha blending preserves underlying bg\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const red_bg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    try buf.clear(red_bg, null);\n\n    const initial_cell = buf.get(1, 1).?;\n    try std.testing.expectEqual(@as(f32, 1.0), initial_cell.bg[0]);\n    try std.testing.expectEqual(@as(f32, 0.0), initial_cell.bg[1]);\n    try std.testing.expectEqual(@as(f32, 0.0), initial_cell.bg[2]);\n\n    const semi_transparent_bg = RGBA{ 0.0, 0.0, 1.0, 0.5 };\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n    };\n\n    buf.drawGrayscaleBuffer(0, 0, &intensities, 3, 3, null, semi_transparent_bg);\n\n    const cell = buf.get(1, 1).?;\n    try std.testing.expect(cell.bg[0] > 0.1);\n    try std.testing.expect(cell.bg[2] > 0.1);\n\n    try std.testing.expect(cell.fg[0] > 0.9);\n    try std.testing.expect(cell.fg[1] > 0.9);\n    try std.testing.expect(cell.fg[2] > 0.9);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer fully transparent bg preserves underlying\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const green_bg = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    try buf.clear(green_bg, null);\n\n    const transparent_bg = RGBA{ 0.0, 0.0, 1.0, 0.0 };\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n    };\n\n    buf.drawGrayscaleBuffer(0, 0, &intensities, 3, 3, null, transparent_bg);\n\n    const cell = buf.get(1, 1).?;\n    try std.testing.expectEqual(@as(f32, 0.0), cell.bg[0]);\n    try std.testing.expectEqual(@as(f32, 1.0), cell.bg[1]);\n    try std.testing.expectEqual(@as(f32, 0.0), cell.bg[2]);\n\n    try std.testing.expect(cell.fg[0] > 0.9);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer opaque bg overwrites underlying\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const red_bg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    try buf.clear(red_bg, null);\n\n    const blue_bg = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n    };\n\n    buf.drawGrayscaleBuffer(0, 0, &intensities, 3, 3, null, blue_bg);\n\n    const cell = buf.get(1, 1).?;\n    try std.testing.expectEqual(@as(f32, 0.0), cell.bg[0]);\n    try std.testing.expectEqual(@as(f32, 0.0), cell.bg[1]);\n    try std.testing.expectEqual(@as(f32, 1.0), cell.bg[2]);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer with opacity stack\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const red_bg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    try buf.clear(red_bg, null);\n\n    try buf.pushOpacity(0.5);\n\n    const blue_bg = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n    };\n\n    buf.drawGrayscaleBuffer(0, 0, &intensities, 3, 3, null, blue_bg);\n\n    buf.popOpacity();\n\n    const cell = buf.get(1, 1).?;\n    try std.testing.expect(cell.bg[0] > 0.1);\n    try std.testing.expect(cell.bg[2] > 0.1);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBufferSupersampled alpha blending\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const red_bg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    try buf.clear(red_bg, null);\n\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n    };\n\n    const semi_transparent_bg = RGBA{ 0.0, 0.0, 1.0, 0.5 };\n    buf.drawGrayscaleBufferSupersampled(0, 0, &intensities, 4, 4, null, semi_transparent_bg);\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(cell.bg[0] > 0.1);\n    try std.testing.expect(cell.bg[2] > 0.1);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBufferSupersampled fully transparent preserves bg\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const green_bg = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    try buf.clear(green_bg, null);\n\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n    };\n\n    const transparent_bg = RGBA{ 0.0, 0.0, 1.0, 0.0 };\n    buf.drawGrayscaleBufferSupersampled(0, 0, &intensities, 4, 4, null, transparent_bg);\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expectEqual(@as(f32, 0.0), cell.bg[0]);\n    try std.testing.expectEqual(@as(f32, 1.0), cell.bg[1]);\n    try std.testing.expectEqual(@as(f32, 0.0), cell.bg[2]);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBufferSupersampled respects scissor\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        6,\n        4,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(bg, null);\n\n    try buf.pushScissorRect(0, 0, 1, 1);\n\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n    };\n\n    buf.drawGrayscaleBufferSupersampled(0, 0, &intensities, 4, 4, null, bg);\n\n    const inCell = buf.get(0, 0).?;\n    const outCell = buf.get(2, 2).?;\n    try std.testing.expect(inCell.char != 32);\n    try std.testing.expectEqual(@as(u32, 32), outCell.char);\n\n    buf.popScissorRect();\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBufferSupersampled with opacity stack\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const red_bg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    try buf.clear(red_bg, null);\n\n    try buf.pushOpacity(0.5);\n\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n    };\n\n    const blue_bg = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n    buf.drawGrayscaleBufferSupersampled(0, 0, &intensities, 4, 4, null, blue_bg);\n\n    buf.popOpacity();\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(cell.bg[0] > 0.1);\n    try std.testing.expect(cell.bg[2] > 0.1);\n}\n\ntest \"OptimizedBuffer - blendColors with transparent destination\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        2,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const transparent_bg = RGBA{ 0.0, 0.0, 0.0, 0.0 };\n    try buf.clear(transparent_bg, null);\n\n    const semi_white = RGBA{ 1.0, 1.0, 1.0, 0.5 };\n    const transparent_fg = RGBA{ 0.0, 0.0, 0.0, 0.0 };\n    try buf.setCellWithAlphaBlending(0, 0, 'X', semi_white, transparent_fg, 0);\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(cell.fg[0] > 0.45);\n    try std.testing.expect(cell.fg[0] < 0.55);\n    try std.testing.expectEqual(@as(f32, 0.5), cell.fg[3]);\n}\n\ntest \"OptimizedBuffer - blend backdrop flattens transparent destination\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        2,\n        2,\n        .{ .pool = pool, .id = \"test-buffer\", .blendBackdropColor = RGBA{ 1.0, 1.0, 1.0, 1.0 } },\n    );\n    defer buf.deinit();\n\n    const transparent_bg = RGBA{ 0.0, 0.0, 0.0, 0.0 };\n    try buf.clear(transparent_bg, null);\n\n    const opaque_fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const semi_black_bg = RGBA{ 0.0, 0.0, 0.0, 0.5 };\n    try buf.setCellWithAlphaBlending(0, 0, buffer_mod.DEFAULT_SPACE_CHAR, opaque_fg, semi_black_bg, 0);\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(cell.bg[0] > 0.45);\n    try std.testing.expect(cell.bg[0] < 0.48);\n    try std.testing.expect(cell.bg[1] > 0.45);\n    try std.testing.expect(cell.bg[1] < 0.48);\n    try std.testing.expect(cell.bg[2] > 0.45);\n    try std.testing.expect(cell.bg[2] < 0.48);\n    try std.testing.expectEqual(@as(f32, 0.5), cell.bg[3]);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer with custom fg color\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const black_bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(black_bg, null);\n\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0,\n    };\n\n    const red_fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    buf.drawGrayscaleBuffer(0, 0, &intensities, 3, 3, red_fg, black_bg);\n\n    const cell = buf.get(1, 1).?;\n    try std.testing.expect(cell.fg[0] > 0.9);\n    try std.testing.expect(cell.fg[1] < 0.1);\n    try std.testing.expect(cell.fg[2] < 0.1);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBuffer custom fg with partial intensity\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const blue_bg = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n    try buf.clear(blue_bg, null);\n\n    const intensities = [_]f32{\n        0.5, 0.5, 0.5,\n        0.5, 0.5, 0.5,\n        0.5, 0.5, 0.5,\n    };\n\n    const green_fg = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    const transparent_bg = RGBA{ 0.0, 0.0, 0.0, 0.0 };\n    buf.drawGrayscaleBuffer(0, 0, &intensities, 3, 3, green_fg, transparent_bg);\n\n    const cell = buf.get(1, 1).?;\n    try std.testing.expect(cell.fg[1] > 0.2);\n    try std.testing.expect(cell.fg[2] > 0.2);\n}\n\ntest \"OptimizedBuffer - drawGrayscaleBufferSupersampled with custom fg color\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        5,\n        .{ .pool = pool, .id = \"test-buffer\" },\n    );\n    defer buf.deinit();\n\n    const black_bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try buf.clear(black_bg, null);\n\n    const intensities = [_]f32{\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n        1.0, 1.0, 1.0, 1.0,\n    };\n\n    const cyan_fg = RGBA{ 0.0, 1.0, 1.0, 1.0 };\n    buf.drawGrayscaleBufferSupersampled(0, 0, &intensities, 4, 4, cyan_fg, black_bg);\n\n    const cell = buf.get(0, 0).?;\n    try std.testing.expect(cell.fg[0] < 0.1);\n    try std.testing.expect(cell.fg[1] > 0.9);\n    try std.testing.expect(cell.fg[2] > 0.9);\n}\n\n// Overwriting a grapheme cell with the same ID but different extent bits must\n// not free the pool slot (which would allow reuse and generation bump).\ntest \"buffer - set same grapheme ID with different extents keeps slot alive\" {\n    var local_pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = .{ 1, 1, 1, 1, 1 },\n    });\n    defer local_pool.deinit();\n\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var buf = try OptimizedBuffer.init(std.testing.allocator, 10, 2, .{\n        .pool = &local_pool,\n        .link_pool = &local_link_pool,\n        .width_method = .unicode,\n    });\n    defer buf.deinit();\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    const emoji = \"👋\";\n\n    const gid = local_pool.alloc(emoji) catch @panic(\"alloc failed\");\n    const packed_w2 = gp.packGraphemeStart(gid & gp.GRAPHEME_ID_MASK, 2);\n    buf.set(0, 0, buffer_mod.Cell{ .char = packed_w2, .fg = fg, .bg = bg, .attributes = 0 });\n\n    const id_from_char = gp.graphemeIdFromChar(packed_w2);\n    try std.testing.expect(buf.grapheme_tracker.contains(id_from_char));\n\n    // Same grapheme ID, different width → different packed char\n    const packed_w1 = gp.packGraphemeStart(gid & gp.GRAPHEME_ID_MASK, 1);\n    buf.set(0, 0, buffer_mod.Cell{ .char = packed_w1, .fg = fg, .bg = bg, .attributes = 0 });\n\n    try std.testing.expect(buf.grapheme_tracker.contains(id_from_char));\n\n    const bytes = local_pool.get(gid) catch @panic(\"get failed - slot was freed\");\n    try std.testing.expectEqualSlices(u8, emoji, bytes);\n}\n\n// Exercises grapheme pool slot reuse across multiple render frames with\n// alternating dialog/form content to stress the alloc→set→render cycle.\ntest \"renderer - grapheme WrongGeneration repro with pool slot reuse\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    const renderer_mod = @import(\"../renderer.zig\");\n    var cli_renderer = try renderer_mod.CliRenderer.create(\n        std.testing.allocator,\n        40,\n        5,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    {\n        const next = cli_renderer.getNextBuffer();\n        try next.drawText(\"╭────────────────────────────────────╮\", 0, 0, fg, bg, 0);\n        try next.drawText(\"│ ◇ Select Files                    │\", 0, 1, fg, bg, 0);\n        try next.drawText(\"│ ▫ src/    ▪ file.ts                │\", 0, 2, fg, bg, 0);\n        try next.drawText(\"│ ↑↓ navigate  ⏎ select  esc close   │\", 0, 3, fg, bg, 0);\n        try next.drawText(\"╰────────────────────────────────────╯\", 0, 4, fg, bg, 0);\n        cli_renderer.render(false);\n    }\n\n    {\n        const next = cli_renderer.getNextBuffer();\n        try next.drawText(\"  Your Name                              \", 0, 0, fg, bg, 0);\n        try next.drawText(\"  John Doe                               \", 0, 1, fg, bg, 0);\n        try next.drawText(\"                                         \", 0, 2, fg, bg, 0);\n        try next.drawText(\"  Select Files                           \", 0, 3, fg, bg, 0);\n        try next.drawText(\"  Enter file path...                     \", 0, 4, fg, bg, 0);\n        cli_renderer.render(false);\n    }\n\n    {\n        const next = cli_renderer.getNextBuffer();\n        try next.drawText(\"╭────────────────────────────────────╮\", 0, 0, fg, bg, 0);\n        try next.drawText(\"│ ◇ Select Files                    │\", 0, 1, fg, bg, 0);\n        try next.drawText(\"│ ▫ src/    ▪ file.ts                │\", 0, 2, fg, bg, 0);\n        try next.drawText(\"│ ↑↓ navigate  ⏎ select  esc close   │\", 0, 3, fg, bg, 0);\n        try next.drawText(\"╰────────────────────────────────────╯\", 0, 4, fg, bg, 0);\n        cli_renderer.render(false);\n    }\n\n    {\n        const next = cli_renderer.getNextBuffer();\n        try next.drawText(\"  Your Name                              \", 0, 0, fg, bg, 0);\n        try next.drawText(\"  John Doe                               \", 0, 1, fg, bg, 0);\n        try next.drawText(\"                                         \", 0, 2, fg, bg, 0);\n        try next.drawText(\"  Select Files                           \", 0, 3, fg, bg, 0);\n        try next.drawText(\"  Enter file path...                     \", 0, 4, fg, bg, 0);\n        cli_renderer.render(false);\n    }\n\n    {\n        const next = cli_renderer.getNextBuffer();\n        try next.drawText(\"╭────────────────────────────────────╮\", 0, 0, fg, bg, 0);\n        try next.drawText(\"│ Filter: s                          │\", 0, 1, fg, bg, 0);\n        try next.drawText(\"│ ▫ src/                             │\", 0, 2, fg, bg, 0);\n        try next.drawText(\"│ ↑↓ navigate  ⏎/tab select          │\", 0, 3, fg, bg, 0);\n        try next.drawText(\"╰────────────────────────────────────╯\", 0, 4, fg, bg, 0);\n        cli_renderer.render(false);\n    }\n}\n\n// Issue #723: CJK grapheme continuation cells are destroyed when graphemes\n// shift left (e.g. after backspace). The renderer's diff loop calls\n// currentRenderBuffer.set() left-to-right, and set()'s span cleanup at\n// position N+2 destroys the continuation cell at N+1 that was just written\n// by set() at position N, because both share the same stable grapheme pool ID.\ntest \"renderer - CJK graphemes shifting left must preserve continuation cells (#723)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    const renderer_mod = @import(\"../renderer.zig\");\n    var cli_renderer = try renderer_mod.CliRenderer.create(\n        std.testing.allocator,\n        20,\n        1,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    // Frame 1: \"abcd你好世\" — CJK chars start at column 4\n    // Layout: a(0) b(1) c(2) d(3) 你(4,5) 好(6,7) 世(8,9) spaces(10..19)\n    {\n        const next = cli_renderer.getNextBuffer();\n        try next.drawText(\"abcd你好世          \", 0, 0, fg, bg, 0);\n        cli_renderer.render(false);\n    }\n\n    // Frame 2: \"abc你好世\" — backspace deleted 'd', CJK chars shift left by 1\n    // Layout: a(0) b(1) c(2) 你(3,4) 好(5,6) 世(7,8) spaces(9..19)\n    {\n        const next = cli_renderer.getNextBuffer();\n        try next.drawText(\"abc你好世           \", 0, 0, fg, bg, 0);\n        cli_renderer.render(false);\n    }\n\n    // After frame 2, currentRenderBuffer should match the frame 2 layout exactly.\n    // The bug: span cleanup in set() destroys continuation cells (positions 4, 6, 8)\n    // leaving spaces instead of proper continuation chars.\n    const current = cli_renderer.getCurrentBuffer();\n\n    // Check that position 3 is a grapheme start (你)\n    const cell3 = current.get(3, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(cell3.char));\n    try std.testing.expectEqual(@as(u32, 1), gp.charRightExtent(cell3.char));\n\n    // Check that position 4 is a continuation cell for the same grapheme (你)\n    const cell4 = current.get(4, 0).?;\n    try std.testing.expect(gp.isContinuationChar(cell4.char));\n    const id3 = gp.graphemeIdFromChar(cell3.char);\n    const id4 = gp.graphemeIdFromChar(cell4.char);\n    try std.testing.expectEqual(id3, id4);\n\n    // Check that position 5 is a grapheme start (好)\n    const cell5 = current.get(5, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(cell5.char));\n\n    // Check that position 6 is a continuation cell for the same grapheme (好)\n    const cell6 = current.get(6, 0).?;\n    try std.testing.expect(gp.isContinuationChar(cell6.char));\n    const id5 = gp.graphemeIdFromChar(cell5.char);\n    const id6 = gp.graphemeIdFromChar(cell6.char);\n    try std.testing.expectEqual(id5, id6);\n\n    // Check that position 7 is a grapheme start (世)\n    const cell7 = current.get(7, 0).?;\n    try std.testing.expect(gp.isGraphemeChar(cell7.char));\n\n    // Check that position 8 is a continuation cell for the same grapheme (世)\n    const cell8 = current.get(8, 0).?;\n    try std.testing.expect(gp.isContinuationChar(cell8.char));\n    const id7 = gp.graphemeIdFromChar(cell7.char);\n    const id8 = gp.graphemeIdFromChar(cell8.char);\n    try std.testing.expectEqual(id7, id8);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/edit-buffer-history_test.zig",
    "content": "const std = @import(\"std\");\nconst edit_buffer = @import(\"../edit-buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst EditBuffer = edit_buffer.EditBuffer;\n\ntest \"EditBuffer - basic undo/redo with insertText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n\n    try eb.insertText(\" World\");\n    var out_buffer: [100]u8 = undefined;\n    var written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello World\", out_buffer[0..written]);\n\n    const meta = try eb.undo();\n    try std.testing.expectEqualStrings(\"edit\", meta);\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello\", out_buffer[0..written]);\n\n    const meta2 = try eb.redo();\n    try std.testing.expectEqualStrings(\"current\", meta2);\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello World\", out_buffer[0..written]);\n}\n\ntest \"EditBuffer - canUndo/canRedo\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try std.testing.expect(!eb.canUndo());\n    try std.testing.expect(!eb.canRedo());\n\n    try eb.insertText(\"Test\");\n\n    try std.testing.expect(eb.canUndo());\n    try std.testing.expect(!eb.canRedo());\n\n    _ = try eb.undo();\n\n    try std.testing.expect(!eb.canUndo());\n    try std.testing.expect(eb.canRedo());\n\n    _ = try eb.redo();\n\n    try std.testing.expect(eb.canUndo());\n    try std.testing.expect(!eb.canRedo());\n}\n\ntest \"EditBuffer - undo/redo with deleteRange\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello World\");\n\n    try eb.deleteRange(.{ .row = 0, .col = 5 }, .{ .row = 0, .col = 11 });\n    var out_buffer: [100]u8 = undefined;\n    var written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello\", out_buffer[0..written]);\n\n    _ = try eb.undo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello World\", out_buffer[0..written]);\n\n    _ = try eb.redo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello\", out_buffer[0..written]);\n}\n\ntest \"EditBuffer - undo/redo with backspace\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n\n    try eb.backspace();\n    var out_buffer: [100]u8 = undefined;\n    var written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hell\", out_buffer[0..written]);\n\n    _ = try eb.undo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello\", out_buffer[0..written]);\n}\n\ntest \"EditBuffer - undo/redo with deleteForward\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n    try eb.setCursor(0, 0);\n\n    try eb.deleteForward();\n    var out_buffer: [100]u8 = undefined;\n    var written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"ello\", out_buffer[0..written]);\n\n    _ = try eb.undo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello\", out_buffer[0..written]);\n}\n\ntest \"EditBuffer - cursor position after undo\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Line 1\\nLine 2\");\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 1), cursor.row);\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    try eb.insertText(\"\\nLine 3\");\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.row);\n\n    // Undo - cursor should be clamped to valid position\n    _ = try eb.undo();\n    cursor = eb.getPrimaryCursor();\n    // Cursor should be clamped to end of line 1\n    try std.testing.expectEqual(@as(u32, 1), cursor.row);\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n}\n\ntest \"EditBuffer - lineCount after undo/redo\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Line 1\");\n    try std.testing.expectEqual(@as(u32, 1), eb.getTextBuffer().lineCount());\n\n    try eb.insertText(\"\\nLine 2\\nLine 3\");\n    try std.testing.expectEqual(@as(u32, 3), eb.getTextBuffer().lineCount());\n\n    _ = try eb.undo();\n    try std.testing.expectEqual(@as(u32, 1), eb.getTextBuffer().lineCount());\n\n    _ = try eb.redo();\n    try std.testing.expectEqual(@as(u32, 3), eb.getTextBuffer().lineCount());\n}\n\ntest \"EditBuffer - clearHistory\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n    try eb.insertText(\" World\");\n\n    try std.testing.expect(eb.canUndo());\n\n    eb.clearHistory();\n\n    try std.testing.expect(!eb.canUndo());\n    try std.testing.expect(!eb.canRedo());\n}\n\ntest \"EditBuffer - undo history branching\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"State A\");\n\n    try eb.insertText(\" -> B\");\n\n    var out_buffer: [100]u8 = undefined;\n    var written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"State A -> B\", out_buffer[0..written]);\n\n    _ = try eb.undo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"State A\", out_buffer[0..written]);\n\n    // Create new branch by editing after undo\n    try eb.insertText(\" -> C\");\n\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"State A -> C\", out_buffer[0..written]);\n\n    _ = try eb.undo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"State A\", out_buffer[0..written]);\n\n    // Redo should go to state C (the new branch)\n    _ = try eb.redo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"State A -> C\", out_buffer[0..written]);\n}\n\ntest \"EditBuffer - multiple undo/redo operations\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var out_buffer: [100]u8 = undefined;\n\n    try eb.insertText(\"A\");\n\n    try eb.insertText(\"B\");\n\n    try eb.insertText(\"C\");\n\n    var written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"ABC\", out_buffer[0..written]);\n\n    _ = try eb.undo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"AB\", out_buffer[0..written]);\n\n    _ = try eb.undo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"A\", out_buffer[0..written]);\n\n    _ = try eb.redo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"AB\", out_buffer[0..written]);\n\n    _ = try eb.redo();\n    written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"ABC\", out_buffer[0..written]);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/edit-buffer_test.zig",
    "content": "const std = @import(\"std\");\nconst edit_buffer = @import(\"../edit-buffer.zig\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\nconst iter_mod = @import(\"../text-buffer-iterators.zig\");\n\nconst EditBuffer = edit_buffer.EditBuffer;\nconst TextBufferView = text_buffer_view.TextBufferView;\nconst Cursor = edit_buffer.Cursor;\n\ntest \"EditBuffer - init and deinit\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try std.testing.expectEqual(@as(u32, 0), eb.getTextBuffer().getLength());\n    const cursor = eb.getCursor(0);\n    try std.testing.expect(cursor != null);\n    try std.testing.expectEqual(@as(u32, 0), cursor.?.row);\n    try std.testing.expectEqual(@as(u32, 0), cursor.?.col);\n}\n\ntest \"EditBuffer - next word boundary basic\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello World\");\n    try eb.setCursor(0, 0);\n\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), next_cursor.row);\n    try std.testing.expectEqual(@as(u32, 6), next_cursor.col);\n}\n\ntest \"EditBuffer - prev word boundary basic\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello World\");\n    try eb.setCursor(0, 7);\n\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor.row);\n    try std.testing.expectEqual(@as(u32, 6), prev_cursor.col);\n}\n\ntest \"EditBuffer - next word boundary across line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\\nWorld\");\n    try eb.setCursor(0, 5);\n\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 1), next_cursor.row);\n    try std.testing.expectEqual(@as(u32, 0), next_cursor.col);\n}\n\ntest \"EditBuffer - prev word boundary across line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\\nWorld\");\n    try eb.setCursor(1, 0);\n\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor.row);\n    try std.testing.expectEqual(@as(u32, 5), prev_cursor.col);\n}\n\ntest \"EditBuffer - hyphen word boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"self-contained\");\n    try eb.setCursor(0, 0);\n\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), next_cursor.row);\n    try std.testing.expectEqual(@as(u32, 5), next_cursor.col);\n}\n\ntest \"EditBuffer - multiple word boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"The quick brown fox\");\n    try eb.setCursor(0, 0);\n\n    var cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    try eb.setCursor(cursor.row, cursor.col);\n    cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 10), cursor.col);\n\n    try eb.setCursor(cursor.row, cursor.col);\n    cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 16), cursor.col);\n}\n\ntest \"EditBuffer - word boundary at end of line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n    try eb.setCursor(0, 5);\n\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), next_cursor.row);\n    try std.testing.expectEqual(@as(u32, 5), next_cursor.col);\n}\n\ntest \"EditBuffer - word boundary at start of line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n    try eb.setCursor(0, 0);\n\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor.row);\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor.col);\n}\n\ntest \"EditBuffer - getEOL basic\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello World\");\n    try eb.setCursor(0, 0);\n\n    const eol_cursor = eb.getEOL();\n    try std.testing.expectEqual(@as(u32, 0), eol_cursor.row);\n    try std.testing.expectEqual(@as(u32, 11), eol_cursor.col);\n}\n\ntest \"EditBuffer - getEOL at end of line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n    try eb.setCursor(0, 5);\n\n    const eol_cursor = eb.getEOL();\n    try std.testing.expectEqual(@as(u32, 0), eol_cursor.row);\n    try std.testing.expectEqual(@as(u32, 5), eol_cursor.col);\n}\n\ntest \"EditBuffer - getEOL multi-line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\\nWorld\\nTest\");\n    try eb.setCursor(1, 0);\n\n    const eol_cursor = eb.getEOL();\n    try std.testing.expectEqual(@as(u32, 1), eol_cursor.row);\n    try std.testing.expectEqual(@as(u32, 5), eol_cursor.col);\n}\n\ntest \"EditBuffer - getEOL empty line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\\n\\nWorld\");\n    try eb.setCursor(1, 0);\n\n    const eol_cursor = eb.getEOL();\n    try std.testing.expectEqual(@as(u32, 1), eol_cursor.row);\n    try std.testing.expectEqual(@as(u32, 0), eol_cursor.col);\n}\n\ntest \"EditBuffer - word boundary with tabs\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\\tWorld\");\n\n    try eb.setCursor(0, 12);\n\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 7), prev_cursor.col);\n\n    try eb.setCursor(0, 0);\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 7), next_cursor.col);\n}\n\ntest \"EditBuffer - word boundary with CJK graphemes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // \"你\" = 2 cols, \" \" = 1 col, \"好\" = 2 cols\n    try eb.insertText(\"你 好\");\n    try eb.setCursor(0, 0);\n\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 3), next_cursor.col);\n\n    try eb.setCursor(0, 5);\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 3), prev_cursor.col);\n}\n\ntest \"EditBuffer - word boundary mixed CJK and ASCII transition\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.setText(\"日本語abc\");\n\n    const eol = eb.getEOL();\n    try std.testing.expect(eol.col >= 3);\n\n    try eb.setCursor(0, 0);\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(eol.col - 3, next_cursor.col);\n\n    try eb.setCursor(next_cursor.row, next_cursor.col);\n    const next_cursor2 = eb.getNextWordBoundary();\n    try std.testing.expectEqual(eol.col, next_cursor2.col);\n\n    try eb.setCursor(0, eol.col);\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(eol.col - 3, prev_cursor.col);\n\n    try eb.setCursor(prev_cursor.row, prev_cursor.col);\n    const prev_cursor2 = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor2.col);\n}\n\ntest \"EditBuffer - word boundary keeps Hangul run grouped\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.setText(\"테스트test\");\n\n    const eol = eb.getEOL();\n    try std.testing.expect(eol.col >= 4);\n\n    try eb.setCursor(0, 0);\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(eol.col - 4, next_cursor.col);\n\n    try eb.setCursor(next_cursor.row, next_cursor.col);\n    const next_cursor2 = eb.getNextWordBoundary();\n    try std.testing.expectEqual(eol.col, next_cursor2.col);\n\n    try eb.setCursor(0, eol.col);\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(eol.col - 4, prev_cursor.col);\n\n    try eb.setCursor(prev_cursor.row, prev_cursor.col);\n    const prev_cursor2 = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor2.col);\n}\n\ntest \"EditBuffer - word boundary respects CJK punctuation before ASCII\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.setText(\"日本語。abc\");\n\n    const eol = eb.getEOL();\n    try std.testing.expect(eol.col >= 5);\n\n    try eb.setCursor(0, 0);\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(eol.col - 3, next_cursor.col);\n\n    try eb.setCursor(next_cursor.row, next_cursor.col);\n    const next_cursor2 = eb.getNextWordBoundary();\n    try std.testing.expectEqual(eol.col, next_cursor2.col);\n\n    try eb.setCursor(0, eol.col);\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(eol.col - 3, prev_cursor.col);\n\n    try eb.setCursor(prev_cursor.row, prev_cursor.col);\n    const prev_cursor2 = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor2.col);\n}\n\ntest \"EditBuffer - word boundary with compat ideograph and ASCII\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.setText(\"丽abc\");\n\n    const eol = eb.getEOL();\n    try std.testing.expect(eol.col >= 3);\n\n    try eb.setCursor(0, 0);\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(eol.col - 3, next_cursor.col);\n\n    try eb.setCursor(next_cursor.row, next_cursor.col);\n    const next_cursor2 = eb.getNextWordBoundary();\n    try std.testing.expectEqual(eol.col, next_cursor2.col);\n\n    try eb.setCursor(0, eol.col);\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(eol.col - 3, prev_cursor.col);\n\n    try eb.setCursor(prev_cursor.row, prev_cursor.col);\n    const prev_cursor2 = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor2.col);\n}\n\ntest \"EditBuffer - word boundary single-character script transitions\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.setText(\"a日\");\n\n    var eol = eb.getEOL();\n    try std.testing.expectEqual(@as(u32, 3), eol.col);\n\n    try eb.setCursor(0, 0);\n    var next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 1), next_cursor.col);\n\n    try eb.setCursor(next_cursor.row, next_cursor.col);\n    var next_cursor2 = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 3), next_cursor2.col);\n\n    try eb.setCursor(0, eol.col);\n    var prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 1), prev_cursor.col);\n\n    try eb.setCursor(prev_cursor.row, prev_cursor.col);\n    var prev_cursor2 = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor2.col);\n\n    try eb.setText(\"日a\");\n\n    eol = eb.getEOL();\n    try std.testing.expectEqual(@as(u32, 3), eol.col);\n\n    try eb.setCursor(0, 0);\n    next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 2), next_cursor.col);\n\n    try eb.setCursor(next_cursor.row, next_cursor.col);\n    next_cursor2 = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 3), next_cursor2.col);\n\n    try eb.setCursor(0, eol.col);\n    prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 2), prev_cursor.col);\n\n    try eb.setCursor(prev_cursor.row, prev_cursor.col);\n    prev_cursor2 = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_cursor2.col);\n}\n\ntest \"EditBuffer - word boundary with emoji\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // \"🌟\" = 2 cols, \" \" = 1 col, \"ok\" = 2 cols\n    try eb.insertText(\"🌟 ok\");\n    try eb.setCursor(0, 0);\n\n    const next_cursor = eb.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 3), next_cursor.col);\n\n    try eb.setCursor(0, 5);\n    const prev_cursor = eb.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 3), prev_cursor.col);\n}\n\ntest \"EditBuffer - moveRight past tab at start of line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\tHello\");\n    try eb.setCursor(0, 0);\n\n    eb.moveRight();\n    const cursor = eb.getCursor(0).?;\n    try std.testing.expect(cursor.col > 0);\n\n    eb.moveRight();\n    const cursor2 = eb.getCursor(0).?;\n    try std.testing.expect(cursor2.col > cursor.col);\n}\n\ntest \"EditBuffer - moveRight after typing before tab\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\tWorld\");\n    try eb.setCursor(0, 0);\n    try eb.insertText(\"Hi\");\n\n    const cursor_after_insert = eb.getCursor(0).?;\n    try std.testing.expectEqual(@as(u32, 0), cursor_after_insert.row);\n\n    eb.moveRight();\n    const cursor_after_move1 = eb.getCursor(0).?;\n    try std.testing.expect(cursor_after_move1.col > cursor_after_insert.col);\n\n    eb.moveRight();\n    const cursor_after_move2 = eb.getCursor(0).?;\n    try std.testing.expect(cursor_after_move2.col > cursor_after_move1.col);\n\n    eb.moveRight();\n    const cursor_after_move3 = eb.getCursor(0).?;\n    try std.testing.expect(cursor_after_move3.col > cursor_after_move2.col);\n}\n\ntest \"EditBuffer - moveRight between two tabs\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\t\\tHello\");\n    try eb.setCursor(0, 0);\n\n    var prev_col: u32 = 0;\n    var i: u32 = 0;\n    while (i < 10) : (i += 1) {\n        eb.moveRight();\n        const cursor = eb.getCursor(0).?;\n        try std.testing.expect(cursor.col >= prev_col);\n        prev_col = cursor.col;\n    }\n}\n\ntest \"EditBuffer - type and move around single tab\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\t\");\n    try eb.setCursor(0, 0);\n    try eb.insertText(\"a\");\n\n    var buffer: [100]u8 = undefined;\n    _ = eb.getText(&buffer);\n\n    const cursor1 = eb.getCursor(0).?;\n    try std.testing.expectEqual(@as(u32, 0), cursor1.row);\n    _ = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n\n    _ = eb.tb.getGraphemeWidthAt(0, cursor1.col);\n\n    eb.moveRight();\n    const cursor2 = eb.getCursor(0).?;\n    const line_width2 = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    const gw2 = eb.tb.getGraphemeWidthAt(0, cursor2.col);\n    try std.testing.expect(cursor2.col > cursor1.col);\n\n    // After moving right once, we're at the end of the line (col=3, line_width=3)\n    // We can't move any further\n    try std.testing.expectEqual(line_width2, cursor2.col);\n    try std.testing.expectEqual(@as(u32, 0), gw2); // No grapheme to move to\n}\n\ntest \"EditBuffer - insert text between tabs and move right\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\t\\tx\");\n    try eb.setCursor(0, 0);\n\n    eb.moveRight();\n    _ = eb.getCursor(0).?;\n\n    try eb.insertText(\"A\");\n    const after_insert = eb.getCursor(0).?;\n\n    eb.moveRight();\n    const after_move1 = eb.getCursor(0).?;\n    try std.testing.expect(after_move1.col > after_insert.col);\n\n    eb.moveRight();\n    const after_move2 = eb.getCursor(0).?;\n    try std.testing.expect(after_move2.col > after_move1.col);\n\n    eb.moveRight();\n    const after_move3 = eb.getCursor(0).?;\n    // Should reach append position (line_width) and stay there\n    try std.testing.expectEqual(after_move2.col, after_move3.col);\n}\n\ntest \"EditBuffer - insert after tab and move around\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\t\");\n    const tab_width = eb.getCursor(0).?.col;\n\n    try eb.insertText(\"x\");\n    const after_x = eb.getCursor(0).?;\n\n    eb.moveLeft();\n    const before_x = eb.getCursor(0).?;\n    try std.testing.expectEqual(tab_width, before_x.col);\n\n    eb.moveRight();\n    const back_at_x = eb.getCursor(0).?;\n    try std.testing.expectEqual(after_x.col, back_at_x.col);\n\n    // Already at append position (after 'x'), can't move further on single line\n    eb.moveRight();\n    const still_at_x = eb.getCursor(0).?;\n    try std.testing.expectEqual(back_at_x.col, still_at_x.col);\n}\n\ntest \"EditBuffer - cursor stuck after typing around tab\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"hello\\tworld\");\n    try eb.setCursor(0, 5);\n\n    eb.moveRight();\n    const pos1 = eb.getCursor(0).?;\n\n    eb.moveRight();\n    const pos2 = eb.getCursor(0).?;\n    try std.testing.expect(pos2.col > pos1.col);\n}\n\ntest \"EditBuffer - complex tab scenario\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\tx\\ty\");\n    try eb.setCursor(0, 0);\n\n    const line_width = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n\n    eb.moveRight();\n    const p1 = eb.getCursor(0).?;\n\n    eb.moveRight();\n    const p2 = eb.getCursor(0).?;\n    try std.testing.expect(p2.col > p1.col);\n\n    eb.moveRight();\n    const p3 = eb.getCursor(0).?;\n    try std.testing.expect(p3.col > p2.col);\n\n    eb.moveRight();\n    const p4 = eb.getCursor(0).?;\n    try std.testing.expect(p4.col > p3.col);\n    try std.testing.expectEqual(line_width, p4.col);\n\n    // Already at append position, can't move further\n    eb.moveRight();\n    const p5 = eb.getCursor(0).?;\n    try std.testing.expectEqual(p4.col, p5.col);\n}\n\ntest \"EditBuffer - cursor stuck at tab in middle of line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"a\\tb\");\n    try eb.setCursor(0, 1);\n\n    var buffer: [100]u8 = undefined;\n    _ = eb.getText(&buffer);\n\n    eb.moveRight();\n    const p1 = eb.getCursor(0).?;\n\n    eb.moveRight();\n    const p2 = eb.getCursor(0).?;\n    try std.testing.expect(p2.col > p1.col);\n}\n\ntest \"EditBuffer - type between tabs then move right\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\t\\t\");\n    try eb.setCursor(0, 2);\n    try eb.insertText(\"x\");\n\n    const line_width = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    const after_insert = eb.getCursor(0).?;\n\n    eb.moveRight();\n    const p1 = eb.getCursor(0).?;\n    try std.testing.expect(p1.col > after_insert.col);\n    try std.testing.expectEqual(line_width, p1.col);\n\n    // Already at append position, can't move further\n    eb.moveRight();\n    const p2 = eb.getCursor(0).?;\n    try std.testing.expectEqual(p1.col, p2.col);\n}\n\ntest \"EditBuffer - tabs only with cursor movement\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"\\t\\t\\t\");\n    try eb.setCursor(0, 0);\n\n    var prev_col: u32 = 0;\n    var i: u32 = 0;\n    while (i < 5) : (i += 1) {\n        _ = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n        _ = eb.tb.getGraphemeWidthAt(0, prev_col);\n        eb.moveRight();\n        const cursor = eb.getCursor(0).?;\n        try std.testing.expect(cursor.col >= prev_col);\n        prev_col = cursor.col;\n    }\n}\n\n// ===== getTextRange Tests =====\n\ntest \"EditBuffer - getTextRange basic ASCII\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello World\");\n\n    var buffer: [100]u8 = undefined;\n    const len = try eb.getTextRange(0, 5, &buffer);\n    try std.testing.expectEqualStrings(\"Hello\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange full text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello World\");\n\n    var buffer: [100]u8 = undefined;\n    const len = try eb.getTextRange(0, 11, &buffer);\n    try std.testing.expectEqualStrings(\"Hello World\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange with emojis\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello 👋 World\");\n\n    var buffer: [100]u8 = undefined;\n    // \"Hello \" = 6 cols, emoji = 2 cols, so emoji is at offset 6-8\n    const len = try eb.getTextRange(6, 8, &buffer);\n    try std.testing.expectEqualStrings(\"👋\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange emoji with skin tone\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Waving hand with medium skin tone\n    try eb.insertText(\"Hi 👋🏽 there\");\n\n    var buffer: [100]u8 = undefined;\n    // \"Hi \" = 3 cols, emoji = 2 cols\n    const len = try eb.getTextRange(3, 5, &buffer);\n    try std.testing.expectEqualStrings(\"👋🏽\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange flag emoji\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // USA flag 🇺🇸 (regional indicator symbols)\n    try eb.insertText(\"Flag: 🇺🇸 here\");\n\n    var buffer: [100]u8 = undefined;\n    // \"Flag: \" = 6 cols, flag = 2 cols\n    const len = try eb.getTextRange(6, 8, &buffer);\n    try std.testing.expectEqualStrings(\"🇺🇸\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange family emoji\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Family emoji (ZWJ sequence): 👨‍👩‍👧‍👦\n    try eb.insertText(\"Family: 👨‍👩‍👧‍👦 end\");\n\n    var buffer: [100]u8 = undefined;\n    // \"Family: \" = 8 cols, family emoji should be 2 cols\n    const len = try eb.getTextRange(8, 10, &buffer);\n    try std.testing.expectEqualStrings(\"👨‍👩‍👧‍👦\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange Devanagari with combining marks\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // \"नमस्ते\" (Namaste in Devanagari) - 5 display columns with zero-width combining marks\n    try eb.insertText(\"Say नमस्ते ok\");\n\n    var buffer: [100]u8 = undefined;\n    // \"Say \" = 4 cols (0-3), \"नमस्ते\" = 5 cols (4-8), \" \" = col 9\n    const len = try eb.getTextRange(4, 8, &buffer);\n    try std.testing.expectEqualStrings(\"नमस्ते\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange CJK characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // \"你好\" (Hello in Chinese) - each character is 2 cols wide\n    try eb.insertText(\"Say 你好 end\");\n\n    var buffer: [100]u8 = undefined;\n    // \"Say \" = 4 cols, 你 = 2 cols, 好 = 2 cols\n    const len = try eb.getTextRange(4, 8, &buffer);\n    try std.testing.expectEqualStrings(\"你好\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange single CJK character\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"A 日 B\");\n\n    var buffer: [100]u8 = undefined;\n    // \"A \" = 2 cols, 日 = 2 cols at offset 2-4\n    const len = try eb.getTextRange(2, 4, &buffer);\n    try std.testing.expectEqualStrings(\"日\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange across lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\\nWorld\");\n\n    var buffer: [100]u8 = undefined;\n    // \"Hello\" = 5 cols, newline = 1 weight, \"Wo\" = 2 cols\n    const len = try eb.getTextRange(3, 8, &buffer);\n    try std.testing.expectEqualStrings(\"lo\\nWo\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange with tabs\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"A\\tB\");\n\n    var buffer: [100]u8 = undefined;\n    // Should include the tab character\n    const len = try eb.getTextRange(0, 10, &buffer);\n    try std.testing.expectEqualStrings(\"A\\tB\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange partial grapheme snap to start\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // CJK character is 2 cols wide\n    try eb.insertText(\"A 好 B\");\n\n    var buffer: [100]u8 = undefined;\n    // Try to get range starting at middle of 好 (offset 3), should snap to start (offset 2)\n    const len = try eb.getTextRange(3, 5, &buffer);\n    try std.testing.expectEqualStrings(\"好 \", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange partial grapheme snap to end\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // CJK character is 2 cols wide\n    try eb.insertText(\"A 好 B\");\n\n    var buffer: [100]u8 = undefined;\n    // Try to get range ending at middle of 好 (offset 3), should snap to end (offset 4)\n    const len = try eb.getTextRange(0, 3, &buffer);\n    try std.testing.expectEqualStrings(\"A 好\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange empty range\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n\n    var buffer: [100]u8 = undefined;\n    const len = try eb.getTextRange(5, 5, &buffer);\n    try std.testing.expectEqual(@as(usize, 0), len);\n}\n\ntest \"EditBuffer - getTextRange out of bounds\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello\");\n\n    var buffer: [100]u8 = undefined;\n    const len = try eb.getTextRange(0, 1000, &buffer);\n    try std.testing.expectEqualStrings(\"Hello\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange mixed scripts\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Mix of ASCII, emoji, CJK, Devanagari\n    try eb.insertText(\"Hi 👋 世界 नमस्ते\");\n\n    var buffer: [100]u8 = undefined;\n    // Get everything\n    const total_len = try eb.getTextRange(0, 100, &buffer);\n    try std.testing.expectEqualStrings(\"Hi 👋 世界 नमस्ते\", buffer[0..total_len]);\n\n    // Get just the emoji\n    const emoji_len = try eb.getTextRange(3, 5, &buffer);\n    try std.testing.expectEqualStrings(\"👋\", buffer[0..emoji_len]);\n\n    // Get the CJK part: \"Hi \" = 3, \"👋 \" = 3, \"世界\" = 4 (cols 6-10)\n    const cjk_len = try eb.getTextRange(6, 10, &buffer);\n    try std.testing.expectEqualStrings(\"世界\", buffer[0..cjk_len]);\n}\n\ntest \"EditBuffer - getTextRange before cursor\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello World\");\n    try eb.setCursor(0, 5);\n\n    const cursor = eb.getCursor(0).?;\n    var buffer: [100]u8 = undefined;\n\n    // Get text before cursor\n    const len = try eb.getTextRange(0, cursor.offset, &buffer);\n    try std.testing.expectEqualStrings(\"Hello\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange char before cursor\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hello World\");\n    try eb.setCursor(0, 5);\n\n    const cursor = eb.getCursor(0).?;\n    var buffer: [100]u8 = undefined;\n\n    // Get last char before cursor (if cursor > 0)\n    if (cursor.offset > 0) {\n        const prev_width = eb.tb.getPrevGraphemeWidth(cursor.row, cursor.col);\n        const len = try eb.getTextRange(cursor.offset - prev_width, cursor.offset, &buffer);\n        try std.testing.expectEqualStrings(\"o\", buffer[0..len]);\n    }\n}\n\ntest \"EditBuffer - getTextRange emoji before cursor\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Hi 👋\");\n    try eb.setCursor(0, 5); // After emoji\n\n    const cursor = eb.getCursor(0).?;\n    var buffer: [100]u8 = undefined;\n\n    // Get emoji before cursor\n    const prev_width = eb.tb.getPrevGraphemeWidth(cursor.row, cursor.col);\n    const len = try eb.getTextRange(cursor.offset - prev_width, cursor.offset, &buffer);\n    try std.testing.expectEqualStrings(\"👋\", buffer[0..len]);\n}\n\ntest \"EditBuffer - getTextRange multiline with emojis\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Line1 👋\\nLine2 🎉\\nLine3\");\n\n    var buffer: [100]u8 = undefined;\n    // Get across all lines\n    const len = try eb.getTextRange(0, 100, &buffer);\n    try std.testing.expectEqualStrings(\"Line1 👋\\nLine2 🎉\\nLine3\", buffer[0..len]);\n}\n\ntest \"EditBuffer - wcwidth mode treats multi-codepoint emoji as separate chars\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Hand emoji with skin tone: U+1F44B (waving hand) + U+1F3FB (light skin tone)\n    // In wcwidth mode, these should be treated as 2 separate chars with width 2 each = 4 total\n    // In unicode/no_zwj mode, they would be 1 grapheme with width 2\n    const hand_with_skin_tone = \"👋🏻\"; // U+1F44B U+1F3FB\n\n    // Family emoji: U+1F468 (man) + U+200D (ZWJ) + U+1F469 (woman) + U+200D + U+1F467 (girl)\n    // In wcwidth mode: each visible codepoint should count separately\n    const family = \"👨‍👩‍👧\"; // man + ZWJ + woman + ZWJ + girl\n\n    // Girl with laptop: U+1F469 (woman) + U+200D (ZWJ) + U+1F4BB (laptop)\n    const girl_laptop = \"👩‍💻\"; // woman + ZWJ + laptop\n\n    try eb.setText(hand_with_skin_tone);\n    try eb.setCursor(0, 0);\n\n    // In wcwidth mode:\n    // - U+1F44B (👋) has width 2\n    // - U+1F3FB (🏻 skin tone) has width 2\n    // Total width should be 4 (not 2 as in grapheme mode)\n    const line_width_hand = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n\n    // Move right should go: col 0 -> col 2 (after first codepoint) -> col 4 (after second codepoint)\n    eb.moveRight();\n    var cursor = eb.getPrimaryCursor();\n\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n\n    // Expected behavior for wcwidth mode: treating each codepoint as separate\n    try std.testing.expectEqual(@as(u32, 4), line_width_hand);\n    try eb.setCursor(0, 0);\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col); // After first codepoint (width 2)\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col); // After second codepoint (width 2)\n\n    try eb.setText(family);\n    const line_width_family = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n\n    // Family: man (width 2) + ZWJ (width 0) + woman (width 2) + ZWJ (width 0) + girl (width 2)\n    // In wcwidth mode, total should be 6\n    try std.testing.expectEqual(@as(u32, 6), line_width_family);\n\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Should move to col 2 (after man)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveRight(); // Should move to col 4 (after woman)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    eb.moveRight(); // Should move to col 6 (after girl)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    try eb.setText(girl_laptop);\n    const line_width_laptop = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n\n    // Woman (width 2) + ZWJ (width 0) + laptop (width 2) = 4 in wcwidth mode\n    try std.testing.expectEqual(@as(u32, 4), line_width_laptop);\n\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Should move to col 2 (after woman)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveRight(); // Should move to col 4 (after laptop)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n}\n\ntest \"EditBuffer - wcwidth comprehensive emoji cursor movement and backspace\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Test string with various emoji types\n    // \"👩🏽‍💻  👨‍👩‍👧‍👦  🏳️‍🌈  🇺🇸  🇩🇪  🇯🇵  🇮🇳\"\n    const woman_tech = \"👩🏽‍💻\"; // Woman + skin tone + ZWJ + laptop = 2+2+0+2 = 6\n    const family = \"👨‍👩‍👧‍👦\"; // Man + ZWJ + Woman + ZWJ + Girl + ZWJ + Boy = 2+0+2+0+2+0+2 = 8\n    const rainbow_flag = \"🏳️‍🌈\"; // Flag + VS16 + ZWJ + Rainbow = 1+0+0+2 = 3 (white flag is width 1 in wcwidth)\n    const us_flag = \"🇺🇸\"; // Regional indicators = 1+1 = 2\n    _ = \"🇩🇪\"; // German flag (unused but documented)\n    _ = \"🇯🇵\"; // Japanese flag (unused but documented)\n    _ = \"🇮🇳\"; // Indian flag (unused but documented)\n\n    try eb.setText(woman_tech);\n    const width1 = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    try std.testing.expectEqual(@as(u32, 6), width1);\n\n    // Test moving right through all codepoints\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Past woman (width 2)\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveRight(); // Past skin tone (width 2)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    eb.moveRight(); // Past ZWJ - since ZWJ has width 0, we skip it and move to laptop\n    cursor = eb.getPrimaryCursor();\n    // ZWJ is zero-width and should be skipped - cursor jumps directly to laptop\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    // Test moving back left\n    eb.moveLeft(); // Back before laptop, skip ZWJ, land at skin tone\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col); // Skipped ZWJ, at skin tone\n\n    eb.moveLeft(); // Back before skin tone\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveLeft(); // Back to start\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    // Test backspace from end\n    try eb.setCursor(0, 6); // At end\n\n    // Get initial text\n    var buf: [100]u8 = undefined;\n    var len = eb.getText(&buf);\n    try std.testing.expectEqualStrings(woman_tech, buf[0..len]);\n\n    // Backspace from col 6 should delete laptop and move to col 4\n    try eb.backspace();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    len = eb.getText(&buf);\n\n    // Backspace from col 4: getPrevGraphemeWidth skips ZWJ and returns skin tone width (2)\n    // So we delete from col 2 to col 4, which removes both ZWJ and skin tone\n    // Cursor moves to col 2\n    try eb.backspace();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    len = eb.getText(&buf);\n\n    // Backspace from col 2 should delete woman and move to col 0\n    try eb.backspace();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    len = eb.getText(&buf);\n    try std.testing.expectEqual(@as(usize, 0), len);\n\n    try eb.setText(family);\n    const width2 = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    try std.testing.expectEqual(@as(u32, 8), width2);\n\n    // Move through all visible codepoints (ZWJs are automatically skipped)\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Man (skips following ZWJ)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveRight(); // Woman (skips preceding and following ZWJ)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    eb.moveRight(); // Girl (skips preceding and following ZWJ)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    eb.moveRight(); // Boy (skips preceding ZWJ)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 8), cursor.col);\n\n    // Move back (ZWJs are skipped)\n    eb.moveLeft(); // Back to Girl\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    eb.moveLeft(); // Back to Woman\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    eb.moveLeft(); // Back to Man\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    try eb.setText(rainbow_flag);\n    const width3 = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    try std.testing.expectEqual(@as(u32, 3), width3);\n\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // White flag (width 1, skips VS16 and ZWJ)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 1), cursor.col);\n\n    eb.moveRight(); // Rainbow (width 2, VS16 and ZWJ were skipped)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 3), cursor.col);\n\n    try eb.setText(us_flag);\n    const width4 = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    try std.testing.expectEqual(@as(u32, 2), width4);\n\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // First regional indicator\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 1), cursor.col);\n\n    eb.moveRight(); // Second regional indicator\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Move back\n    eb.moveLeft();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 1), cursor.col);\n\n    eb.moveLeft();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    const mixed_text = \"A 👩🏽‍💻 B 👨‍👩‍👧‍👦 C\";\n    try eb.setText(mixed_text);\n    const mixed_width = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    // A(1) + space(1) + woman_tech(6) + space(1) + B(1) + space(1) + family(8) + space(1) + C(1) = 21\n    try std.testing.expectEqual(@as(u32, 21), mixed_width);\n\n    // Navigate through the mixed text\n    try eb.setCursor(0, 0);\n\n    // Move to 'A'\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 1), cursor.col);\n\n    // Move past space\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Move through woman technologist (ZWJs are skipped)\n    eb.moveRight(); // woman\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    eb.moveRight(); // skin tone\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    eb.moveRight(); // laptop (ZWJ is skipped)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 8), cursor.col);\n\n    // Should be at space after woman_tech\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 9), cursor.col);\n}\n\ntest \"EditBuffer - wcwidth ZWJ does not appear in rendered text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    const woman_tech = \"👩🏽‍💻\"; // Contains ZWJ at byte position\n    try eb.setText(woman_tech);\n\n    // Get the raw bytes - ZWJ should be present in the buffer\n    var buf: [100]u8 = undefined;\n    const len = eb.getText(&buf);\n    const text_bytes = buf[0..len];\n\n    // Check that ZWJ (U+200D = 0xE2 0x80 0x8D in UTF-8) is present in bytes\n    var has_zwj = false;\n    var i: usize = 0;\n    while (i + 2 < len) : (i += 1) {\n        if (text_bytes[i] == 0xE2 and text_bytes[i + 1] == 0x80 and text_bytes[i + 2] == 0x8D) {\n            has_zwj = true;\n            break;\n        }\n    }\n    try std.testing.expect(has_zwj);\n\n    // Verify that the full text is preserved byte-for-byte\n    try std.testing.expectEqualStrings(woman_tech, text_bytes);\n\n    // But cursor movement should skip over ZWJ\n    try eb.setCursor(0, 0);\n    const line_width = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    try std.testing.expectEqual(@as(u32, 6), line_width); // 2+2+0+2\n\n    // Moving through: cursor positions should be 0, 2, 4, 6\n    // ZWJ is skipped automatically\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Woman\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveRight(); // Skin tone\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    eb.moveRight(); // Laptop (ZWJ is skipped)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n}\n\ntest \"EditBuffer - wcwidth each visible emoji requires exactly one cursor move\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Test 1: Simple laptop emoji (no ZWJ)\n    try eb.setText(\"💻\");\n    const width1 = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    try std.testing.expectEqual(@as(u32, 2), width1);\n\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Should move past laptop in ONE move\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Test 2: Woman emoji (no modifiers)\n    try eb.setText(\"👩\");\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Should move past woman in ONE move\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Test 3: Skin tone emoji alone\n    try eb.setText(\"🏽\");\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Should move past skin in ONE move\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Test 4: Woman + skin (no ZWJ yet)\n    try eb.setText(\"👩🏽\");\n    const width4 = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    try std.testing.expectEqual(@as(u32, 4), width4); // 2+2\n\n    try eb.setCursor(0, 0);\n    eb.moveRight(); // Move past woman\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveRight(); // Move past skin in ONE more move\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    // Test 5: Woman + skin + ZWJ + laptop (full technologist)\n    try eb.setText(\"👩🏽‍💻\");\n    const width5 = iter_mod.lineWidthAt(eb.tb.rope(), 0);\n    try std.testing.expectEqual(@as(u32, 6), width5); // 2+2+0+2\n\n    try eb.setCursor(0, 0);\n\n    // Should take exactly 3 moves to get to the end (woman, skin, laptop)\n    // ZWJ should be completely invisible to cursor\n    eb.moveRight(); // Move 1: woman\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveRight(); // Move 2: skin\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    eb.moveRight(); // Move 3: laptop (ZWJ should be skipped automatically)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    // Moving right again should do nothing (at end)\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    // Test moving backwards\n    eb.moveLeft(); // Should move back to before laptop (skip ZWJ), land at skin\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    eb.moveLeft(); // Should move back to before skin\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    eb.moveLeft(); // Should move back to start\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n}\n\ntest \"EditBuffer - replaceText allows undo\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Set initial text (resets everything)\n    try eb.setText(\"Initial\");\n\n    var buffer: [100]u8 = undefined;\n    var len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"Initial\", buffer[0..len]);\n\n    // Replace text with history preserved\n    try eb.replaceText(\"Modified\");\n    len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"Modified\", buffer[0..len]);\n\n    // Should be able to undo\n    try std.testing.expect(eb.canUndo());\n    _ = try eb.undo();\n\n    // Should be back to \"Initial\"\n    len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"Initial\", buffer[0..len]);\n}\n\ntest \"EditBuffer - setText clears all history\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Insert some text that creates undo history\n    try eb.insertText(\"Initial\");\n\n    // Should have undo history\n    try std.testing.expect(eb.canUndo());\n\n    var buffer: [100]u8 = undefined;\n    var len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"Initial\", buffer[0..len]);\n\n    // setText now completely resets the buffer (clears history)\n    try eb.setText(\"New\");\n\n    len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"New\", buffer[0..len]);\n\n    // History should be cleared\n    try std.testing.expect(!eb.canUndo());\n}\n\ntest \"EditBuffer - multiple replaceText with history keeps add_buffer functional\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Use replaceText to preserve history\n    try eb.replaceText(\"Line 1\");\n\n    // Insert more text using the add_buffer\n    try eb.insertText(\"\\nLine 2\");\n\n    // Replace text again (preserves history)\n    // This sets cursor to (0, 0)\n    try eb.replaceText(\"Reset\");\n\n    // Insert more text using the add_buffer (should still work)\n    // Since cursor is at (0, 0), text is inserted at the beginning\n    try eb.insertText(\" and more\");\n\n    var buffer: [100]u8 = undefined;\n    const len = eb.getText(&buffer);\n    // Text is inserted at cursor position (0, 0), so it appears before \"Reset\"\n    try std.testing.expectEqualStrings(\" and moreReset\", buffer[0..len]);\n\n    // Verify we can undo\n    try std.testing.expect(eb.canUndo());\n\n    // Move cursor to end and insert more text\n    const line_count = eb.tb.lineCount();\n    const last_line_width = iter_mod.lineWidthAt(eb.tb.rope(), line_count - 1);\n    try eb.setCursor(line_count - 1, last_line_width);\n    try eb.insertText(\" more\");\n\n    const len2 = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\" and moreReset more\", buffer[0..len2]);\n}\n\ntest \"EditBuffer - setText resets add_buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    // Insert text that uses add_buffer\n    try eb.insertText(\"First\");\n    try eb.insertText(\" Second\");\n\n    var buffer: [100]u8 = undefined;\n    var len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"First Second\", buffer[0..len]);\n\n    // setText should reset add_buffer.len to 0\n    try eb.setText(\"Reset\");\n\n    len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"Reset\", buffer[0..len]);\n\n    // After setText, add_buffer should be reset and work fine\n    // setText places cursor at (0,0), so move to end of text\n    const line_count = eb.tb.lineCount();\n    const last_line_width = iter_mod.lineWidthAt(eb.tb.rope(), line_count - 1);\n    try eb.setCursor(line_count - 1, last_line_width);\n\n    try eb.insertText(\" More\");\n\n    len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"Reset More\", buffer[0..len]);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/editor-view_test.zig",
    "content": "const std = @import(\"std\");\nconst editor_view = @import(\"../editor-view.zig\");\nconst edit_buffer = @import(\"../edit-buffer.zig\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst opt_buffer_mod = @import(\"../buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst EditorView = editor_view.EditorView;\nconst EditBuffer = edit_buffer.EditBuffer;\nconst Cursor = edit_buffer.Cursor;\nconst Viewport = text_buffer_view.Viewport;\n\ntest \"EditorView - init and deinit\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    const vp = ev.getViewport();\n    try std.testing.expect(vp != null);\n    try std.testing.expectEqual(@as(u32, 80), vp.?.width);\n    try std.testing.expectEqual(@as(u32, 24), vp.?.height);\n    try std.testing.expectEqual(@as(u32, 0), vp.?.y);\n}\n\ntest \"EditorView - ensureCursorVisible scrolls down when cursor moves below viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\nLine 11\\nLine 12\\nLine 13\\nLine 14\\nLine 15\\nLine 16\\nLine 17\\nLine 18\\nLine 19\");\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 19), cursor.row);\n\n    _ = ev.getVirtualLines();\n\n    const vp = ev.getViewport().?;\n    try std.testing.expect(vp.y > 0);\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - ensureCursorVisible scrolls up when cursor moves above viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\nLine 11\\nLine 12\\nLine 13\\nLine 14\\nLine 15\\nLine 16\\nLine 17\\nLine 18\\nLine 19\");\n\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expect(vp.y > 0);\n\n    try eb.gotoLine(0);\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n\n    vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n}\n\ntest \"EditorView - moveDown scrolls viewport automatically\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\nLine 11\\nLine 12\\nLine 13\\nLine 14\\nLine 15\\nLine 16\\nLine 17\\nLine 18\\nLine 19\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n    var vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n\n    var i: u32 = 0;\n    while (i < 15) : (i += 1) {\n        eb.moveDown();\n    }\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 15), cursor.row);\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - moveUp scrolls viewport automatically\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\nLine 11\\nLine 12\\nLine 13\\nLine 14\\nLine 15\\nLine 16\\nLine 17\\nLine 18\\nLine 19\");\n\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    const initial_y = vp.y;\n    try std.testing.expect(initial_y > 0);\n\n    var i: u32 = 0;\n    while (i < 10) : (i += 1) {\n        eb.moveUp();\n    }\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 9), cursor.row);\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(vp.y < initial_y);\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - scroll margin keeps cursor away from edges\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    ev.setScrollMargin(0.2);\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\nLine 11\\nLine 12\\nLine 13\\nLine 14\\nLine 15\\nLine 16\\nLine 17\\nLine 18\\nLine 19\");\n\n    try eb.gotoLine(5);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 5), cursor.row);\n\n    const vp = ev.getViewport().?;\n    const cursor_offset_in_viewport = cursor.row - vp.y;\n\n    try std.testing.expect(cursor_offset_in_viewport >= 2);\n    try std.testing.expect(cursor_offset_in_viewport < vp.height - 2);\n}\n\ntest \"EditorView - insertText with newlines maintains cursor visibility\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 5);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\");\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    const vp = ev.getViewport().?;\n\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - backspace at line start maintains visibility\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 5);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\");\n\n    try eb.backspace();\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    const vp = ev.getViewport().?;\n\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - deleteForward at line end maintains visibility\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 5);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\");\n\n    try eb.setCursor(8, 6);\n\n    try eb.deleteForward();\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    const vp = ev.getViewport().?;\n\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - deleteRange maintains cursor visibility\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 5);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\");\n\n    try eb.deleteRange(.{ .row = 2, .col = 0 }, .{ .row = 7, .col = 6 });\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    const vp = ev.getViewport().?;\n\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - deleteLine maintains cursor visibility\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 5);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\");\n\n    try eb.gotoLine(7);\n\n    try eb.deleteLine();\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    const vp = ev.getViewport().?;\n\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - setText resets viewport to top\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 5);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\");\n\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expect(vp.y > 0);\n\n    try eb.setText(\"New Line 0\\nNew Line 1\\nNew Line 2\");\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n}\n\ntest \"EditorView - viewport respects total line count as max offset\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\");\n\n    try eb.gotoLine(4);\n\n    const vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n}\n\ntest \"EditorView - horizontal movement doesn't affect vertical scroll\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\");\n\n    try eb.setCursor(2, 0);\n\n    const vp_before = ev.getViewport().?;\n\n    eb.moveRight();\n    eb.moveRight();\n    eb.moveRight();\n\n    const vp_after = ev.getViewport().?;\n    try std.testing.expectEqual(vp_before.y, vp_after.y);\n}\n\ntest \"EditorView - cursor at boundaries doesn't cause invalid viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.setCursor(0, 0);\n\n    var vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n\n    try eb.insertText(\"First line\");\n\n    try eb.setCursor(0, 0);\n\n    vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n\n    eb.moveLeft();\n    vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n\n    eb.moveUp();\n    vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n}\n\ntest \"EditorView - rapid cursor movements maintain visibility\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\nLine 11\\nLine 12\\nLine 13\\nLine 14\\nLine 15\\nLine 16\\nLine 17\\nLine 18\\nLine 19\\nLine 20\\nLine 21\\nLine 22\\nLine 23\\nLine 24\\nLine 25\\nLine 26\\nLine 27\\nLine 28\\nLine 29\");\n\n    try eb.gotoLine(0);\n    try eb.gotoLine(29);\n    try eb.gotoLine(15);\n    try eb.gotoLine(5);\n    try eb.gotoLine(25);\n\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n    const vp = ev.getViewport().?;\n\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - VisualCursor without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Hello World\\nSecond Line\\nThird Line\");\n\n    try eb.setCursor(1, 3);\n\n    const vcursor = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 1), vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 3), vcursor.visual_col);\n    try std.testing.expectEqual(@as(u32, 1), vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 3), vcursor.logical_col);\n}\n\ntest \"EditorView - VisualCursor with character wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb.setText(\"This is a very long line that will definitely wrap at 20 characters\");\n\n    try eb.setCursor(0, 25);\n\n    const vcursor = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 0), vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 25), vcursor.logical_col);\n    try std.testing.expect(vcursor.visual_row > 0);\n    try std.testing.expect(vcursor.visual_col <= 20);\n}\n\ntest \"EditorView - VisualCursor with word wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.word);\n\n    try eb.setText(\"Hello world this is a test of word wrapping\");\n\n    const line_count = eb.getTextBuffer().getLineCount();\n    try std.testing.expectEqual(@as(u32, 1), line_count);\n\n    _ = ev.getVisualCursor();\n}\n\ntest \"EditorView - moveUpVisual with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb.setText(\"This is a very long line that will definitely wrap multiple times at twenty characters\");\n\n    try eb.setCursor(0, 50);\n\n    const vcursor_before = ev.getVisualCursor();\n    const visual_row_before = vcursor_before.visual_row;\n\n    ev.moveUpVisual();\n\n    const vcursor_after = ev.getVisualCursor();\n\n    try std.testing.expectEqual(visual_row_before - 1, vcursor_after.visual_row);\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor_after.logical_row);\n}\n\ntest \"EditorView - moveDownVisual with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb.setText(\"This is a very long line that will definitely wrap multiple times at twenty characters\");\n\n    try eb.setCursor(0, 0);\n\n    const vcursor_before = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 0), vcursor_before.visual_row);\n\n    ev.moveDownVisual();\n\n    const vcursor_after = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 1), vcursor_after.visual_row);\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor_after.logical_row);\n}\n\ntest \"EditorView - visualToLogicalCursor conversion\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb.setText(\"12345678901234567890123456789012345\");\n\n    if (ev.visualToLogicalCursor(1, 5)) |vcursor| {\n        try std.testing.expectEqual(@as(u32, 1), vcursor.visual_row);\n        try std.testing.expectEqual(@as(u32, 0), vcursor.logical_row);\n        try std.testing.expectEqual(@as(u32, 25), vcursor.logical_col);\n    }\n}\n\ntest \"EditorView - moveUpVisual at top boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n    try eb.setText(\"Short line\");\n\n    try eb.setCursor(0, 0);\n\n    const before = ev.getPrimaryCursor();\n    ev.moveUpVisual();\n    const after = ev.getPrimaryCursor();\n\n    try std.testing.expectEqual(before.row, after.row);\n    try std.testing.expectEqual(before.col, after.col);\n}\n\ntest \"EditorView - moveDownVisual at bottom boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n    try eb.setText(\"Short line\\nSecond line\");\n\n    try eb.setCursor(1, 0);\n\n    const before = ev.getPrimaryCursor();\n    ev.moveDownVisual();\n    const after = ev.getPrimaryCursor();\n\n    try std.testing.expectEqual(before.row, after.row);\n}\n\ntest \"EditorView - VisualCursor preserves desired column across wrapped lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb.setText(\"12345678901234567890123456789012345678901234567890\");\n\n    try eb.setCursor(0, 15);\n\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    ev.moveUpVisual();\n\n    const vcursor = ev.getVisualCursor();\n\n    try std.testing.expect(vcursor.visual_col <= 20);\n}\n\ntest \"EditorView - VisualCursor with multiple logical lines and wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb.setText(\"Short line 1\\nThis is a very long line that will wrap multiple times\\nShort line 3\");\n\n    try eb.setCursor(1, 30);\n\n    const vcursor = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 1), vcursor.logical_row);\n\n    try std.testing.expect(vcursor.visual_row > 1);\n}\n\ntest \"EditorView - logicalToVisualCursor handles cursor past line end\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"Short\");\n\n    const vcursor = ev.logicalToVisualCursor(0, 100);\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor.logical_row);\n}\n\ntest \"EditorView - getTextBufferView returns correct view\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    const vp = tbv.getViewport();\n    try std.testing.expect(vp != null);\n}\n\ntest \"EditorView - getEditBuffer returns correct buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    const returned_eb = ev.getEditBuffer();\n    try std.testing.expect(returned_eb == eb);\n}\n\ntest \"EditorView - setViewportSize maintains cursor visibility\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\nLine 11\\nLine 12\\nLine 13\\nLine 14\");\n\n    try eb.gotoLine(10);\n\n    ev.setViewportSize(80, 5);\n\n    const vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 80), vp.width);\n    try std.testing.expectEqual(@as(u32, 5), vp.height);\n}\n\ntest \"EditorView - moveDownVisual across empty line preserves desired column\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"Line with some text\\n\\nAnother line with text\");\n\n    try eb.setCursor(0, 10);\n\n    const vcursor_before = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 10), vcursor_before.visual_col);\n\n    ev.moveDownVisual();\n\n    const vcursor_empty = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 1), vcursor_empty.logical_row);\n    try std.testing.expectEqual(@as(u32, 0), vcursor_empty.visual_col);\n\n    ev.moveDownVisual();\n\n    const vcursor_after = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 2), vcursor_after.logical_row);\n    try std.testing.expectEqual(@as(u32, 10), vcursor_after.visual_col);\n}\n\ntest \"EditorView - moveUpVisual across empty line preserves desired column\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"Line with some text\\n\\nAnother line with text\");\n\n    try eb.setCursor(2, 10);\n\n    const vcursor_before = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 10), vcursor_before.visual_col);\n\n    ev.moveUpVisual();\n\n    const vcursor_empty = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 1), vcursor_empty.logical_row);\n    try std.testing.expectEqual(@as(u32, 0), vcursor_empty.visual_col);\n\n    ev.moveUpVisual();\n\n    const vcursor_after = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 0), vcursor_after.logical_row);\n    try std.testing.expectEqual(@as(u32, 10), vcursor_after.visual_col);\n}\n\ntest \"EditorView - horizontal movement resets desired visual column\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"Line with some text\\n\\nAnother line with text\");\n\n    try eb.setCursor(0, 10);\n\n    const vcursor_initial = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 10), vcursor_initial.visual_col);\n\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n\n    const vcursor_after = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 2), vcursor_after.logical_row);\n    try std.testing.expectEqual(@as(u32, 10), vcursor_after.visual_col);\n\n    eb.moveRight();\n\n    const vcursor_after_right = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 11), vcursor_after_right.visual_col);\n\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n\n    const vcursor_final = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 0), vcursor_final.logical_row);\n    try std.testing.expectEqual(@as(u32, 11), vcursor_final.visual_col);\n}\n\ntest \"EditorView - inserting newlines maintains rope integrity\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\");\n\n    const rope_init = eb.getTextBuffer().rope();\n    const line_count_init = eb.getTextBuffer().lineCount();\n    try std.testing.expectEqual(@as(u32, 3), line_count_init);\n\n    try eb.insertText(\"\\n\");\n\n    const line_count_1 = eb.getTextBuffer().lineCount();\n    try std.testing.expectEqual(@as(u32, 4), line_count_1);\n\n    if (rope_init.getMarker(.linestart, 2)) |m2| {\n        if (rope_init.getMarker(.linestart, 3)) |m3| {\n            try std.testing.expect(m2.global_weight != m3.global_weight);\n        }\n    }\n\n    try eb.insertText(\"\\n\");\n\n    const line_count_2 = eb.getTextBuffer().lineCount();\n    try std.testing.expectEqual(@as(u32, 5), line_count_2);\n\n    if (rope_init.getMarker(.linestart, 3)) |m3| {\n        if (rope_init.getMarker(.linestart, 4)) |m4| {\n            try std.testing.expect(m3.global_weight != m4.global_weight);\n        }\n    }\n}\n\ntest \"EditorView - visual cursor stays in sync after scrolling and moving up\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\");\n\n    var cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.row);\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n\n    var i: u32 = 0;\n    while (i < 6) : (i += 1) {\n        try eb.insertText(\"\\n\");\n        _ = ev.getVirtualLines();\n    }\n\n    cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 10), cursor.row);\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(vp.y > 0);\n\n    const vcursor_before = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 10), vcursor_before.logical_row);\n\n    ev.moveUpVisual();\n    _ = ev.getVirtualLines();\n\n    const vcursor_after_up = ev.getVisualCursor();\n    const logical_cursor_after_up = ev.getPrimaryCursor();\n\n    try std.testing.expectEqual(@as(u32, 9), logical_cursor_after_up.row);\n    try std.testing.expectEqual(@as(u32, 9), vcursor_after_up.logical_row);\n\n    try std.testing.expect(vcursor_after_up.visual_row < vcursor_before.visual_row);\n\n    try eb.insertText(\"X\");\n    _ = ev.getVirtualLines();\n\n    const cursor_after_insert = ev.getPrimaryCursor();\n    const vcursor_after_insert = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 9), cursor_after_insert.row);\n    try std.testing.expectEqual(@as(u32, 1), cursor_after_insert.col);\n\n    try std.testing.expectEqual(@as(u32, 9), vcursor_after_insert.logical_row);\n    try std.testing.expectEqual(@as(u32, 1), vcursor_after_insert.logical_col);\n\n    var out_buffer: [200]u8 = undefined;\n    const written = eb.getText(&out_buffer);\n    const text = out_buffer[0..written];\n\n    var line_count: u32 = 0;\n    var line_start: usize = 0;\n    for (text, 0..) |c, idx| {\n        if (c == '\\n') {\n            if (line_count == 9) {\n                const line_9 = text[line_start..idx];\n                try std.testing.expect(line_9.len >= 1);\n                try std.testing.expectEqual(@as(u8, 'X'), line_9[0]);\n                break;\n            }\n            line_count += 1;\n            line_start = idx + 1;\n        }\n    }\n}\n\ntest \"EditorView - cursor positioning after wide grapheme\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"AB東CD\");\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n\n    try eb.setCursor(0, 4);\n    const cursor_after_move = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor_after_move.col);\n\n    const vcursor = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 0), vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 4), vcursor.logical_col);\n    try std.testing.expectEqual(@as(u32, 4), vcursor.visual_col);\n}\n\ntest \"EditorView - backspace after wide grapheme updates cursor correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"AB東CD\");\n\n    try eb.setCursor(0, 4);\n\n    try eb.backspace();\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    const vcursor = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 2), vcursor.logical_col);\n    try std.testing.expectEqual(@as(u32, 2), vcursor.visual_col);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"ABCD\", out_buffer[0..written]);\n}\n\ntest \"EditorView - viewport scrolling with wrapped lines: down + edit + up\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    tbv.setWrapMode(.char);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 20, .height = 10 }, true);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPPQQQQQQQQQQRRRRRRRRRRSSSSSSSSSSTTTTTTTTTTUUUUUUUUUUVVVVVVVVVVWWWWWWWWWWXXXXXXXXXXYYYYYYYYYYZZZZZZZZZZ\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    const initial_vp_y = vp.y;\n    try std.testing.expectEqual(@as(u32, 0), initial_vp_y);\n\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    _ = vp.y;\n\n    _ = ev.getVisualCursor();\n\n    try eb.insertText(\"X\");\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    _ = vp.y;\n\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    const final_vp_y = vp.y;\n\n    const vcursor_final = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor_final.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), final_vp_y);\n}\n\ntest \"EditorView - viewport scrolling with wrapped lines: aggressive down + edit + up sequence\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    tbv.setWrapMode(.char);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 20, .height = 10 }, true);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPPQQQQQQQQQQRRRRRRRRRRSSSSSSSSSSTTTTTTTTTTUUUUUUUUUUVVVVVVVVVVWWWWWWWWWWXXXXXXXXXXYYYYYYYYYYZZZZZZZZZZ\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    const total_vlines = ev.getTotalVirtualLineCount();\n    try std.testing.expect(total_vlines > 10);\n\n    var vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n\n    var i: u32 = 0;\n    while (i < 12) : (i += 1) {\n        ev.moveDownVisual();\n    }\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(vp.y > 0);\n\n    try eb.insertText(\"TEST\");\n    _ = ev.getVirtualLines();\n\n    i = 0;\n    while (i < 12) : (i += 1) {\n        ev.moveUpVisual();\n    }\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    const vcursor = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n}\n\ntest \"EditorView - viewport scrolling with wrapped lines: multiple edits and movements\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 15, 8);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    tbv.setWrapMode(.char);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 15, .height = 8 }, true);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPPQQQQQQQQQQRRRRRRRRRRSSSSSSSSSSTTTTTTTTTTUUUUUUUUUUVVVVVVVVVV\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    _ = ev.getVirtualLines();\n\n    try eb.insertText(\"A\");\n    _ = ev.getVirtualLines();\n\n    ev.moveDownVisual();\n    _ = ev.getVirtualLines();\n\n    try eb.insertText(\"B\");\n    _ = ev.getVirtualLines();\n\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    _ = ev.getVirtualLines();\n\n    const vp = ev.getViewport().?;\n    const vcursor = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n}\n\ntest \"EditorView - viewport scrolling with wrapped lines: verify viewport consistency\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    tbv.setWrapMode(.char);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 20, .height = 10 }, true);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPPQQQQQQQQQQRRRRRRRRRRSSSSSSSSSSTTTTTTTTTTUUUUUUUUUUVVVVVVVVVVWWWWWWWWWWXXXXXXXXXXYYYYYYYYYYZZZZZZZZZZ\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    const vline_count = ev.getTotalVirtualLineCount();\n    try std.testing.expect(vline_count >= 10);\n\n    var movements_down: u32 = 0;\n    var i: u32 = 0;\n    while (i < 5) : (i += 1) {\n        const vcursor_before = ev.getVisualCursor();\n        ev.moveDownVisual();\n        const vcursor_after = ev.getVisualCursor();\n        if (true) {\n            if (vcursor_after.visual_row > vcursor_before.visual_row) {\n                movements_down += 1;\n            }\n        }\n    }\n    _ = ev.getVirtualLines();\n\n    _ = ev.getViewport().?;\n    _ = ev.getVisualCursor();\n\n    try eb.insertText(\"EDITED\");\n    _ = ev.getVirtualLines();\n\n    i = 0;\n    while (i < movements_down) : (i += 1) {\n        ev.moveUpVisual();\n    }\n    _ = ev.getVirtualLines();\n\n    const vp_final = ev.getViewport().?;\n    const vcursor_final = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor_final.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), vp_final.y);\n}\n\ntest \"EditorView - viewport scrolling with wrapped lines: backspace after scroll\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    tbv.setWrapMode(.char);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 20, .height = 10 }, true);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPPQQQQQQQQQQRRRRRRRRRRSSSSSSSSSSTTTTTTTTTTUUUUUUUUUUVVVVVVVVVVWWWWWWWWWWXXXXXXXXXXYYYYYYYYYYZZZZZZZZZZ\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    _ = ev.getVirtualLines();\n\n    try eb.backspace();\n    _ = ev.getVirtualLines();\n\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    _ = ev.getVirtualLines();\n\n    const vp = ev.getViewport().?;\n    const vcursor = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n}\n\ntest \"EditorView - viewport scrolling with wrapped lines: viewport follows cursor precisely\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 5);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    tbv.setWrapMode(.char);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 20, .height = 5 }, true);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPPQQQQQQQQQQRRRRRRRRRRSSSSSSSSSSTTTTTTTTTTUUUUUUUUUUVVVVVVVVVVWWWWWWWWWWXXXXXXXXXXYYYYYYYYYYZZZZZZZZZZ\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    var i: u32 = 0;\n    while (i < 10) : (i += 1) {\n        ev.moveDownVisual();\n        _ = ev.getVirtualLines();\n\n        const vp = ev.getViewport().?;\n        const vcursor = ev.getVisualCursor();\n\n        try std.testing.expect(vcursor.visual_row >= 0);\n        try std.testing.expect(vcursor.visual_row < vp.height);\n    }\n\n    try eb.insertText(\"MIDDLE\");\n    _ = ev.getVirtualLines();\n\n    const vp_middle = ev.getViewport().?;\n    const vcursor_middle = ev.getVisualCursor();\n    try std.testing.expect(vcursor_middle.visual_row >= 0);\n    try std.testing.expect(vcursor_middle.visual_row < vp_middle.height);\n\n    i = 0;\n    while (i < 10) : (i += 1) {\n        ev.moveUpVisual();\n        _ = ev.getVirtualLines();\n\n        const vp = ev.getViewport().?;\n        const vcursor = ev.getVisualCursor();\n\n        try std.testing.expect(vcursor.visual_row >= 0);\n        try std.testing.expect(vcursor.visual_row < vp.height);\n    }\n\n    const vp_final = ev.getViewport().?;\n    const vcursor_final = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor_final.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), vp_final.y);\n}\n\ntest \"EditorView - wrapped lines: specific scenario with insert and deletions\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    tbv.setWrapMode(.char);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 20, .height = 10 }, true);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPPQQQQQQQQQQRRRRRRRRRRSSSSSSSSSSTTTTTTTTTTUUUUUUUUUUVVVVVVVVVVWWWWWWWWWWXXXXXXXXXXYYYYYYYYYYZZZZZZZZZZ\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    const vcursor_mid = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 5), vcursor_mid.visual_row);\n\n    try eb.insertText(\"XXX\");\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    const vcursor_after_insert = ev.getVisualCursor();\n    try std.testing.expect(vcursor_after_insert.visual_row >= 0);\n    try std.testing.expect(vcursor_after_insert.visual_row < vp.height);\n\n    try eb.backspace();\n    try eb.backspace();\n    try eb.backspace();\n    _ = ev.getVirtualLines();\n\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    const vcursor_final2 = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor_final2.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n}\n\ntest \"EditorView - wrapped lines: many small edits with viewport scrolling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 15, 8);\n    defer ev.deinit();\n\n    const tbv = ev.getTextBufferView();\n    tbv.setWrapMode(.char);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 15, .height = 8 }, true);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPPQQQQQQQQQQRRRRRRRRRRSSSSSSSSSSTTTTTTTTTTUUUUUUUUUUVVVVVVVVVV\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    ev.moveDownVisual();\n    ev.moveDownVisual();\n    _ = ev.getVirtualLines();\n\n    try eb.insertText(\"1\");\n    _ = ev.getVirtualLines();\n\n    ev.moveDownVisual();\n    _ = ev.getVirtualLines();\n\n    try eb.insertText(\"2\");\n    _ = ev.getVirtualLines();\n\n    ev.moveDownVisual();\n    _ = ev.getVirtualLines();\n\n    try eb.insertText(\"3\");\n    _ = ev.getVirtualLines();\n\n    ev.moveUpVisual();\n    _ = ev.getVirtualLines();\n\n    try eb.insertText(\"4\");\n    _ = ev.getVirtualLines();\n\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    ev.moveUpVisual();\n    _ = ev.getVirtualLines();\n\n    const vp2 = ev.getViewport().?;\n    const vcursor2 = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor2.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), vp2.y);\n}\n\ntest \"EditorView - horizontal scroll: cursor moves right beyond viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"This is a very long line that exceeds the viewport width of 20 characters\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.x);\n\n    try eb.setCursor(0, 50);\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(vp.x > 0);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expect(cursor.col >= vp.x);\n    try std.testing.expect(cursor.col < vp.x + vp.width);\n}\n\ntest \"EditorView - horizontal scroll: cursor moves left to beginning\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"This is a very long line that exceeds the viewport width of 20 characters\");\n\n    try eb.setCursor(0, 50);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expect(vp.x > 0);\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.x);\n}\n\ntest \"EditorView - horizontal scroll: moveRight scrolls viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.x);\n\n    var i: u32 = 0;\n    while (i < 50) : (i += 1) {\n        eb.moveRight();\n    }\n\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(vp.x > 0);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 50), cursor.col);\n    try std.testing.expect(cursor.col >= vp.x);\n    try std.testing.expect(cursor.col < vp.x + vp.width);\n}\n\ntest \"EditorView - horizontal scroll: moveLeft scrolls viewport back\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\");\n\n    try eb.setCursor(0, 50);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    const initial_x = vp.x;\n    try std.testing.expect(initial_x > 0);\n\n    var i: u32 = 0;\n    while (i < 30) : (i += 1) {\n        eb.moveLeft();\n    }\n\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(vp.x < initial_x);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expect(cursor.col >= vp.x);\n    try std.testing.expect(cursor.col < vp.x + vp.width);\n}\n\ntest \"EditorView - horizontal scroll: editing in scrolled view\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\");\n\n    try eb.setCursor(0, 50);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expect(vp.x > 0);\n\n    try eb.insertText(\"XYZ\");\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 53), cursor.col);\n    try std.testing.expect(cursor.col >= vp.x);\n    try std.testing.expect(cursor.col < vp.x + vp.width);\n\n    var out_buffer: [200]u8 = undefined;\n    const written = eb.getText(&out_buffer);\n    const text = out_buffer[0..written];\n    try std.testing.expect(std.mem.indexOf(u8, text, \"XYZ\") != null);\n}\n\ntest \"EditorView - horizontal scroll: backspace in scrolled view\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\");\n\n    try eb.setCursor(0, 50);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expect(vp.x > 0);\n\n    try eb.backspace();\n    try eb.backspace();\n    try eb.backspace();\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 47), cursor.col);\n    try std.testing.expect(cursor.col >= vp.x);\n    try std.testing.expect(cursor.col < vp.x + vp.width);\n}\n\ntest \"EditorView - horizontal scroll: short lines reset scroll\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"Short line\\nAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJ\\nAnother short\");\n\n    try eb.setCursor(1, 50);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expect(vp.x > 0);\n\n    try eb.setCursor(0, 5);\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(vp.x <= 5);\n\n    try eb.setCursor(1, 50);\n    _ = ev.getVirtualLines();\n\n    vp = ev.getViewport().?;\n    try std.testing.expect(vp.x > 0);\n}\n\ntest \"EditorView - horizontal scroll: scroll margin works\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setScrollMargin(0.2);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    var i: u32 = 0;\n    while (i < 25) : (i += 1) {\n        eb.moveRight();\n    }\n\n    _ = ev.getVirtualLines();\n\n    const vp = ev.getViewport().?;\n    const cursor = ev.getPrimaryCursor();\n\n    const cursor_offset_in_viewport = cursor.col - vp.x;\n    try std.testing.expect(cursor_offset_in_viewport >= 4);\n    try std.testing.expect(cursor_offset_in_viewport < vp.width - 4);\n}\n\ntest \"EditorView - horizontal scroll: no scrolling with wrapping enabled\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\");\n\n    try eb.setCursor(0, 50);\n    _ = ev.getVirtualLines();\n\n    const vp = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.x);\n}\n\ntest \"EditorView - horizontal scroll: cursor position correct after scrolling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\");\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    var i: u32 = 0;\n    while (i < 50) : (i += 1) {\n        eb.moveRight();\n        _ = ev.getVirtualLines();\n\n        const cursor = ev.getPrimaryCursor();\n        const vp = ev.getViewport().?;\n        const vcursor = ev.getVisualCursor();\n\n        try std.testing.expectEqual(cursor.col, vcursor.logical_col);\n        try std.testing.expect(cursor.col >= vp.x);\n        try std.testing.expect(cursor.col < vp.x + vp.width);\n    }\n}\n\ntest \"EditorView - horizontal scroll: rapid movements maintain visibility\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    try eb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\");\n\n    try eb.setCursor(0, 0);\n    try eb.setCursor(0, 80);\n    try eb.setCursor(0, 40);\n    try eb.setCursor(0, 10);\n    try eb.setCursor(0, 60);\n\n    _ = ev.getVirtualLines();\n\n    const vp = ev.getViewport().?;\n    const cursor = ev.getPrimaryCursor();\n\n    try std.testing.expectEqual(@as(u32, 60), cursor.col);\n    try std.testing.expect(cursor.col >= vp.x);\n    try std.testing.expect(cursor.col < vp.x + vp.width);\n}\n\ntest \"EditorView - horizontal scroll: goto end of long line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    const long_line = \"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJKKKKKKKKKKLLLLLLLLLLMMMMMMMMMMNNNNNNNNNNOOOOOOOOOOPPPPPPPPPP\";\n    try eb.setText(long_line);\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    try eb.setCursor(0, @intCast(long_line.len));\n    _ = ev.getVirtualLines();\n\n    const vp = ev.getViewport().?;\n    const cursor = ev.getPrimaryCursor();\n\n    try std.testing.expect(vp.x > 0);\n    try std.testing.expect(cursor.col >= vp.x);\n    try std.testing.expect(cursor.col < vp.x + vp.width);\n}\n\ntest \"EditorView - cursor at second cell of width=2 grapheme moveLeft should jump to before grapheme\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    try eb.setText(\"(emoji 🌟 and CJK 世界)\");\n\n    try eb.setCursor(0, 7);\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 7), cursor.col);\n\n    // Move right - should jump over emoji to col 9\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 9), cursor.col);\n\n    // Manually set cursor to col 8 (second cell of emoji at 7-8)\n    // TODO: setCursor should probably also snap to beginning of grapheme?\n    //       When the width/cell based cursor is visual only and EditBuffer/Rope cursor is byte based\n    try eb.setCursor(0, 8);\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 8), cursor.col);\n\n    // Should jump to col 9 (after the emoji), not col 10\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 9), cursor.col);\n\n    try eb.setCursor(0, 8);\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 8), cursor.col);\n\n    eb.moveLeft();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 6), cursor.col);\n}\n\ntest \"EditorView - cursor should be able to land after closing paren on line with wide graphemes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    try eb.setText(\"(emoji 🌟 and CJK 世界)\\nNext line\");\n\n    try eb.setCursor(0, 0);\n    var cursor = eb.getPrimaryCursor();\n\n    var i: u32 = 0;\n    while (i < 30) : (i += 1) {\n        const prev_col = cursor.col;\n        const prev_row = cursor.row;\n        eb.moveRight();\n        cursor = eb.getPrimaryCursor();\n\n        // Should not jump to next line until we've reached the end of the current line\n        if (prev_row == 0 and cursor.row == 1) {\n            // We jumped to the next line - check that we were at the end\n            const iter_mod = @import(\"../text-buffer-iterators.zig\");\n            const line_width = iter_mod.lineWidthAt(eb.getTextBuffer().rope(), 0);\n            try std.testing.expectEqual(line_width, prev_col);\n            break;\n        }\n\n        if (i > 25) {\n            break;\n        }\n    }\n\n    try std.testing.expectEqual(@as(u32, 1), cursor.row);\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n}\n\ntest \"EditorView - visual cursor should stay on same line when moving to line end with wide graphemes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    try eb.setText(\"(emoji 🌟 and CJK 世界)\\nNext line\");\n\n    try eb.setCursor(0, 0);\n\n    var i: u32 = 0;\n    while (i < 30) : (i += 1) {\n        eb.moveRight();\n        const cursor = eb.getPrimaryCursor();\n        const vcursor = ev.getVisualCursor();\n\n        // Visual cursor should stay on row 0 until we move past the line end\n        if (cursor.row == 0) {\n            try std.testing.expectEqual(@as(u32, 0), vcursor.visual_row);\n            try std.testing.expectEqual(cursor.col, vcursor.visual_col);\n        }\n\n        if (cursor.row == 1) {\n            try std.testing.expectEqual(@as(u32, 1), vcursor.visual_row);\n            break;\n        }\n\n        if (i > 25) break;\n    }\n}\n\ntest \"EditorView - placeholder with styled text renders with correct highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    const ss = @import(\"../syntax-style.zig\");\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    eb.getTextBuffer().setSyntaxStyle(style);\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    const text_part1 = \"Enter \";\n    const text_part2 = \"something\";\n    const text_part3 = \" here\";\n\n    const fg_gray = [4]f32{ 0.5, 0.5, 0.5, 1.0 };\n    const fg_blue = [4]f32{ 0.3, 0.5, 0.9, 1.0 };\n\n    const chunks = [_]text_buffer.StyledChunk{\n        .{\n            .text_ptr = text_part1.ptr,\n            .text_len = text_part1.len,\n            .fg_ptr = @ptrCast(&fg_gray),\n            .bg_ptr = null,\n            .attributes = 0,\n        },\n        .{\n            .text_ptr = text_part2.ptr,\n            .text_len = text_part2.len,\n            .fg_ptr = @ptrCast(&fg_blue),\n            .bg_ptr = null,\n            .attributes = 0,\n        },\n        .{\n            .text_ptr = text_part3.ptr,\n            .text_len = text_part3.len,\n            .fg_ptr = @ptrCast(&fg_gray),\n            .bg_ptr = null,\n            .attributes = 0,\n        },\n    };\n\n    try ev.setPlaceholderStyledText(&chunks);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = eb.getText(&out_buffer);\n    try std.testing.expectEqual(@as(usize, 0), written);\n\n    ev.updateBeforeRender();\n\n    const tbv_ptr = ev.getTextBufferView();\n\n    var opt_buffer = try opt_buffer_mod.OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        24,\n        .{ .pool = pool, .width_method = .wcwidth },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(tbv_ptr, 0, 0);\n\n    const epsilon: f32 = 0.01;\n\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'E'), cell_0.char);\n\n    const cell_6 = opt_buffer.get(6, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 's'), cell_6.char);\n\n    const cell_15 = opt_buffer.get(15, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_15.char);\n\n    const fg_0 = opt_buffer.buffer.fg[0];\n    try std.testing.expect(@abs(fg_0[0] - fg_gray[0]) < epsilon);\n    try std.testing.expect(@abs(fg_0[1] - fg_gray[1]) < epsilon);\n    try std.testing.expect(@abs(fg_0[2] - fg_gray[2]) < epsilon);\n\n    const fg_6 = opt_buffer.buffer.fg[6];\n    try std.testing.expect(@abs(fg_6[0] - fg_blue[0]) < epsilon);\n    try std.testing.expect(@abs(fg_6[1] - fg_blue[1]) < epsilon);\n    try std.testing.expect(@abs(fg_6[2] - fg_blue[2]) < epsilon);\n\n    const fg_15 = opt_buffer.buffer.fg[15];\n    try std.testing.expect(@abs(fg_15[0] - fg_gray[0]) < epsilon);\n    try std.testing.expect(@abs(fg_15[1] - fg_gray[1]) < epsilon);\n    try std.testing.expect(@abs(fg_15[2] - fg_gray[2]) < epsilon);\n}\n\ntest \"EditorView - getNextWordBoundary returns VisualCursor\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Hello World Test\");\n    try eb.setCursor(0, 0);\n\n    const next_vcursor = ev.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), next_vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 6), next_vcursor.logical_col);\n    try std.testing.expectEqual(@as(u32, 0), next_vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 6), next_vcursor.visual_col);\n}\n\ntest \"EditorView - getPrevWordBoundary returns VisualCursor\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Hello World Test\");\n    try eb.setCursor(0, 12);\n\n    const prev_vcursor = ev.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 6), prev_vcursor.logical_col);\n    try std.testing.expectEqual(@as(u32, 0), prev_vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 6), prev_vcursor.visual_col);\n}\n\ntest \"EditorView - word boundary with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb.setText(\"This is a very long line that will wrap and has multiple words\");\n    try eb.setCursor(0, 0);\n\n    const next_vcursor = ev.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), next_vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 5), next_vcursor.logical_col);\n\n    try std.testing.expect(next_vcursor.visual_col <= 20);\n}\n\ntest \"EditorView - word boundary across lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Hello\\nWorld\");\n    try eb.setCursor(0, 5);\n\n    const next_vcursor = ev.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 1), next_vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 0), next_vcursor.logical_col);\n    try std.testing.expectEqual(@as(u32, 1), next_vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), next_vcursor.visual_col);\n}\n\ntest \"EditorView - word boundary prev across lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Hello\\nWorld\");\n    try eb.setCursor(1, 0);\n\n    const prev_vcursor = ev.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 5), prev_vcursor.logical_col);\n    try std.testing.expectEqual(@as(u32, 0), prev_vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 5), prev_vcursor.visual_col);\n}\n\ntest \"EditorView - word boundary with punctuation\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"self-contained multi-word\");\n    try eb.setCursor(0, 0);\n\n    const next_vcursor = ev.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), next_vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 5), next_vcursor.logical_col);\n}\n\ntest \"EditorView - word boundary at end of buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Hello World\");\n    try eb.setCursor(0, 11);\n\n    const next_vcursor = ev.getNextWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), next_vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 11), next_vcursor.logical_col);\n}\n\ntest \"EditorView - word boundary at start of buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    try eb.insertText(\"Hello World\");\n    try eb.setCursor(0, 0);\n\n    const prev_vcursor = ev.getPrevWordBoundary();\n    try std.testing.expectEqual(@as(u32, 0), prev_vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 0), prev_vcursor.logical_col);\n}\n\ntest \"EditorView - horizontal scroll: combined vertical and horizontal scrolling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 20, 10);\n    defer ev.deinit();\n\n    const line0 = \"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJ\";\n    const repeated_line = \"\\nAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGGHHHHHHHHHHIIIIIIIIIIJJJJJJJJJJ\";\n\n    var buffer: [3000]u8 = undefined;\n    var fbs = std.io.fixedBufferStream(&buffer);\n    const writer = fbs.writer();\n    writer.writeAll(line0) catch unreachable;\n    var i: u32 = 1;\n    while (i < 20) : (i += 1) {\n        writer.writeAll(repeated_line) catch unreachable;\n    }\n\n    const text = fbs.getWritten();\n    try eb.setText(text);\n\n    try eb.setCursor(15, 60);\n    _ = ev.getVirtualLines();\n\n    const vp = ev.getViewport().?;\n    const cursor = ev.getPrimaryCursor();\n\n    try std.testing.expect(vp.y > 0);\n    try std.testing.expect(vp.x > 0);\n\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n    try std.testing.expect(cursor.col >= vp.x);\n    try std.testing.expect(cursor.col < vp.x + vp.width);\n}\n\ntest \"EditorView - deleteSelectedText single line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb_inst = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb_inst.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb_inst, 80, 24);\n    defer ev.deinit();\n\n    try eb_inst.setText(\"Hello World\");\n\n    ev.text_buffer_view.setSelection(0, 5, null, null);\n\n    const sel_before = ev.text_buffer_view.getSelection();\n    try std.testing.expect(sel_before != null);\n    try std.testing.expectEqual(@as(u32, 0), sel_before.?.start);\n    try std.testing.expectEqual(@as(u32, 5), sel_before.?.end);\n\n    try ev.deleteSelectedText();\n\n    var out_buffer: [100]u8 = undefined;\n    const written = ev.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\" World\", out_buffer[0..written]);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    const sel_after = ev.text_buffer_view.getSelection();\n    try std.testing.expect(sel_after == null);\n}\n\ntest \"EditorView - deleteSelectedText multi-line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb_inst = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb_inst.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb_inst, 80, 24);\n    defer ev.deinit();\n\n    try eb_inst.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    ev.text_buffer_view.setSelection(2, 15, null, null);\n\n    try ev.deleteSelectedText();\n\n    var out_buffer: [100]u8 = undefined;\n    const written = ev.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Liine 3\", out_buffer[0..written]);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n}\n\ntest \"EditorView - deleteSelectedText with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb_inst = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb_inst.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb_inst, 20, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.char);\n\n    try eb_inst.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    const vline_count = ev.getTotalVirtualLineCount();\n    try std.testing.expect(vline_count >= 2);\n\n    ev.text_buffer_view.setSelection(5, 15, null, null);\n\n    try ev.deleteSelectedText();\n\n    var out_buffer: [100]u8 = undefined;\n    const written = ev.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"ABCDEPQRSTUVWXYZ\", out_buffer[0..written]);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n    try std.testing.expectEqual(@as(u32, 5), cursor.col);\n}\n\ntest \"EditorView - deleteSelectedText with viewport scrolled\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb_inst = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb_inst.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb_inst, 40, 5);\n    defer ev.deinit();\n\n    try eb_inst.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\\nLine 10\\nLine 11\\nLine 12\\nLine 13\\nLine 14\\nLine 15\\nLine 16\\nLine 17\\nLine 18\\nLine 19\");\n\n    try eb_inst.gotoLine(10);\n    _ = ev.getVirtualLines();\n\n    var vp = ev.getViewport().?;\n    try std.testing.expect(vp.y > 0);\n\n    ev.text_buffer_view.setSelection(50, 70, null, null);\n\n    try ev.deleteSelectedText();\n\n    _ = ev.getVirtualLines();\n    vp = ev.getViewport().?;\n    const cursor = ev.getPrimaryCursor();\n\n    try std.testing.expect(cursor.row >= vp.y);\n    try std.testing.expect(cursor.row < vp.y + vp.height);\n}\n\ntest \"EditorView - deleteSelectedText with no selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb_inst = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb_inst.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb_inst, 80, 24);\n    defer ev.deinit();\n\n    try eb_inst.setText(\"Hello World\");\n\n    try ev.deleteSelectedText();\n\n    var out_buffer: [100]u8 = undefined;\n    const written = ev.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello World\", out_buffer[0..written]);\n}\n\ntest \"EditorView - deleteSelectedText entire line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb_inst = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb_inst.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb_inst, 80, 24);\n    defer ev.deinit();\n\n    try eb_inst.setText(\"First\\nSecond\\nThird\\n\");\n\n    ev.text_buffer_view.setSelection(5, 13, null, null);\n\n    try ev.deleteSelectedText();\n\n    var out_buffer: [100]u8 = undefined;\n    const written = ev.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"FirstThird\\n\", out_buffer[0..written]);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n    try std.testing.expectEqual(@as(u32, 5), cursor.col);\n}\n\ntest \"EditorView - deleteSelectedText respects selection with empty lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb_inst = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb_inst.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb_inst, 40, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.word);\n\n    try eb_inst.setText(\"AAAA\\n\\nBBBB\\n\\nCCCC\");\n\n    try eb_inst.setCursor(2, 0);\n\n    _ = ev.text_buffer_view.setLocalSelection(0, 2, 4, 2, null, null);\n\n    const sel = ev.text_buffer_view.getSelection();\n    try std.testing.expect(sel != null);\n\n    try std.testing.expectEqual(@as(u32, 6), sel.?.start);\n    try std.testing.expectEqual(@as(u32, 10), sel.?.end);\n\n    var selected_buffer: [100]u8 = undefined;\n    const selected_len = ev.text_buffer_view.getSelectedTextIntoBuffer(&selected_buffer);\n    const selected_text = selected_buffer[0..selected_len];\n    try std.testing.expectEqualStrings(\"BBBB\", selected_text);\n\n    try ev.deleteSelectedText();\n\n    var out_buffer: [100]u8 = undefined;\n    const written = ev.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"AAAA\\n\\n\\n\\nCCCC\", out_buffer[0..written]);\n\n    const cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.row);\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n}\n\ntest \"EditorView - word wrapping with space insertion maintains cursor sync\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 15, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.word);\n    ev.setViewport(Viewport{ .x = 0, .y = 0, .width = 15, .height = 10 }, true);\n\n    try eb.setText(\"AAAAAAAAAAAAAAAAAAA\");\n    try eb.setCursor(0, 7);\n    try eb.insertText(\" \");\n\n    const logical_cursor_after_space = eb.getPrimaryCursor();\n    const vcursor_after_space = ev.getVisualCursor();\n\n    try std.testing.expectEqual(@as(u32, 0), logical_cursor_after_space.row);\n    try std.testing.expectEqual(@as(u32, 8), logical_cursor_after_space.col);\n\n    try std.testing.expectEqual(@as(u32, 0), vcursor_after_space.logical_row);\n    try std.testing.expectEqual(@as(u32, 1), vcursor_after_space.visual_row);\n\n    try eb.backspace();\n\n    const logical_cursor_after_backspace = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), logical_cursor_after_backspace.row);\n    try std.testing.expectEqual(@as(u32, 7), logical_cursor_after_backspace.col);\n}\n\ntest \"EditorView - getVisualCursor always returns on empty buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    const vcursor = ev.getVisualCursor();\n    try std.testing.expectEqual(@as(u32, 0), vcursor.visual_row);\n    try std.testing.expectEqual(@as(u32, 0), vcursor.visual_col);\n    try std.testing.expectEqual(@as(u32, 0), vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 0), vcursor.logical_col);\n}\n\ntest \"EditorView - logicalToVisualCursor clamps row beyond last line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    try eb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    const vcursor = ev.logicalToVisualCursor(100, 0);\n    try std.testing.expectEqual(@as(u32, 2), vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 0), vcursor.logical_col);\n}\n\ntest \"EditorView - logicalToVisualCursor clamps col beyond line width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    try eb.setText(\"Hello\");\n\n    const vcursor = ev.logicalToVisualCursor(0, 100);\n    try std.testing.expectEqual(@as(u32, 0), vcursor.logical_row);\n    try std.testing.expectEqual(@as(u32, 5), vcursor.logical_col);\n    try std.testing.expectEqual(@as(u32, 5), vcursor.visual_col);\n}\n\ntest \"EditorView - placeholder shows when empty\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    const text = \"Enter text here...\";\n    const gray_color = text_buffer.RGBA{ 0.4, 0.4, 0.4, 1.0 };\n    const chunks = [_]text_buffer.StyledChunk{.{\n        .text_ptr = text.ptr,\n        .text_len = text.len,\n        .fg_ptr = @ptrCast(&gray_color),\n        .bg_ptr = null,\n        .attributes = 0,\n    }};\n    try ev.setPlaceholderStyledText(&chunks);\n\n    var out_buffer: [100]u8 = undefined;\n    const text_len = eb.getText(&out_buffer);\n    try std.testing.expectEqual(@as(usize, 0), text_len);\n\n    try std.testing.expect(ev.placeholder_buffer != null);\n    const placeholder = ev.placeholder_buffer.?;\n    try std.testing.expectEqual(@as(u32, 18), placeholder.getLength());\n}\n\ntest \"EditorView - placeholder cleared when set to empty\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    const text = \"Placeholder\";\n    const gray_color = text_buffer.RGBA{ 0.4, 0.4, 0.4, 1.0 };\n    const chunks = [_]text_buffer.StyledChunk{.{\n        .text_ptr = text.ptr,\n        .text_len = text.len,\n        .fg_ptr = @ptrCast(&gray_color),\n        .bg_ptr = null,\n        .attributes = 0,\n    }};\n    try ev.setPlaceholderStyledText(&chunks);\n\n    try std.testing.expect(ev.placeholder_buffer != null);\n\n    const empty_chunks = [_]text_buffer.StyledChunk{};\n    try ev.setPlaceholderStyledText(&empty_chunks);\n\n    try std.testing.expect(ev.placeholder_buffer == null);\n}\n\ntest \"EditorView - placeholder with styled text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    const text1 = \"Hello \";\n    const text2 = \"World\";\n    const red_color = text_buffer.RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const blue_color = text_buffer.RGBA{ 0.0, 0.0, 1.0, 1.0 };\n\n    const chunks = [_]text_buffer.StyledChunk{\n        .{\n            .text_ptr = text1.ptr,\n            .text_len = text1.len,\n            .fg_ptr = @ptrCast(&red_color),\n            .bg_ptr = null,\n            .attributes = 0,\n        },\n        .{\n            .text_ptr = text2.ptr,\n            .text_len = text2.len,\n            .fg_ptr = @ptrCast(&blue_color),\n            .bg_ptr = null,\n            .attributes = 0,\n        },\n    };\n\n    try ev.setPlaceholderStyledText(&chunks);\n\n    try std.testing.expect(ev.placeholder_buffer != null);\n    const placeholder = ev.placeholder_buffer.?;\n    try std.testing.expectEqual(@as(u32, 11), placeholder.getLength());\n}\n\ntest \"EditorView - placeholder renders to buffer when empty\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    const placeholder_text = \"Type something...\";\n    const gray_color = text_buffer.RGBA{ 0.5, 0.5, 0.5, 1.0 };\n    const placeholder_chunks = [_]text_buffer.StyledChunk{.{\n        .text_ptr = placeholder_text.ptr,\n        .text_len = placeholder_text.len,\n        .fg_ptr = @ptrCast(&gray_color),\n        .bg_ptr = null,\n        .attributes = 0,\n    }};\n    try ev.setPlaceholderStyledText(&placeholder_chunks);\n\n    try std.testing.expect(ev.placeholder_buffer != null);\n    try std.testing.expect(ev.placeholder_active);\n\n    var opt_buffer = try opt_buffer_mod.OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        10,\n        .{ .pool = pool, .width_method = .wcwidth },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawEditorView(ev, 0, 0);\n\n    var out_buffer: [1000]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expect(std.mem.startsWith(u8, result, \"Type something...\"));\n\n    try eb.insertText(\"Hello\");\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawEditorView(ev, 0, 0);\n    try std.testing.expect(!ev.placeholder_active);\n\n    const written2 = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result2 = out_buffer[0..written2];\n\n    try std.testing.expect(std.mem.startsWith(u8, result2, \"Hello\"));\n    try std.testing.expect(!std.mem.startsWith(u8, result2, \"Type something...\"));\n}\n\ntest \"EditorView - placeholder shrink clears tail and preserves background\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 10);\n    defer ev.deinit();\n\n    const long_text = \"Ask anything... \\\"Fix a TODO in the codebase\\\"\";\n    const short_text = \"Run a command... \\\"pwd\\\"\";\n    const fg = text_buffer.RGBA{ 0.6, 0.6, 0.6, 1.0 };\n    const panel_bg = text_buffer.RGBA{ 0.14, 0.14, 0.16, 1.0 };\n\n    const long_chunks = [_]text_buffer.StyledChunk{.{\n        .text_ptr = long_text.ptr,\n        .text_len = long_text.len,\n        .fg_ptr = @ptrCast(&fg),\n        .bg_ptr = null,\n        .attributes = 0,\n    }};\n    const short_chunks = [_]text_buffer.StyledChunk{.{\n        .text_ptr = short_text.ptr,\n        .text_len = short_text.len,\n        .fg_ptr = @ptrCast(&fg),\n        .bg_ptr = null,\n        .attributes = 0,\n    }};\n\n    var opt_buffer = try opt_buffer_mod.OptimizedBuffer.init(\n        std.testing.allocator,\n        120,\n        10,\n        .{ .pool = pool, .width_method = .wcwidth },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n\n    var x: u32 = 0;\n    while (x < 80) : (x += 1) {\n        opt_buffer.set(x, 0, .{ .char = 32, .fg = fg, .bg = panel_bg, .attributes = 0 });\n    }\n\n    try ev.setPlaceholderStyledText(&long_chunks);\n    try opt_buffer.drawEditorView(ev, 0, 0);\n\n    x = 0;\n    while (x < 80) : (x += 1) {\n        opt_buffer.set(x, 0, .{ .char = 32, .fg = fg, .bg = panel_bg, .attributes = 0 });\n    }\n\n    try ev.setPlaceholderStyledText(&short_chunks);\n    try opt_buffer.drawEditorView(ev, 0, 0);\n\n    var out_buffer: [1600]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const line = out_buffer[0..written];\n\n    try std.testing.expect(std.mem.indexOf(u8, line, short_text) != null);\n    try std.testing.expect(std.mem.indexOf(u8, line, \"roken tests\") == null);\n    try std.testing.expect(std.mem.indexOf(u8, line, \"TODO in the codebase\") == null);\n\n    const tail = opt_buffer.get(35, 0) orelse return error.TestUnexpectedResult;\n    try std.testing.expectEqual(@as(u32, 32), tail.char);\n    try std.testing.expectEqual(@as(f32, panel_bg[0]), tail.bg[0]);\n    try std.testing.expectEqual(@as(f32, panel_bg[1]), tail.bg[1]);\n    try std.testing.expectEqual(@as(f32, panel_bg[2]), tail.bg[2]);\n    try std.testing.expectEqual(@as(f32, panel_bg[3]), tail.bg[3]);\n}\n\ntest \"EditorView - tab indicator set and get\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    try std.testing.expect(ev.getTabIndicator() == null);\n    try std.testing.expect(ev.getTabIndicatorColor() == null);\n\n    ev.setTabIndicator('·');\n    ev.setTabIndicatorColor(.{ 0.5, 0.5, 0.5, 1.0 });\n\n    try std.testing.expectEqual(@as(u32, '·'), ev.getTabIndicator().?);\n    try std.testing.expectEqual(@as(f32, 0.5), ev.getTabIndicatorColor().?[0]);\n}\n\ntest \"EditorView - tab indicator renders in buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    eb.tb.setTabWidth(4);\n    try eb.insertText(\"A\\tB\");\n\n    ev.setTabIndicator('→');\n    ev.setTabIndicatorColor(.{ 0.3, 0.3, 0.3, 1.0 });\n\n    var opt_buffer = try opt_buffer_mod.OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        10,\n        .{ .pool = pool, .width_method = .wcwidth },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawEditorView(ev, 0, 0);\n\n    const cell_0 = opt_buffer.get(0, 0);\n    try std.testing.expect(cell_0 != null);\n    try std.testing.expectEqual(@as(u32, 'A'), cell_0.?.char);\n\n    const cell_1 = opt_buffer.get(1, 0);\n    try std.testing.expect(cell_1 != null);\n    try std.testing.expectEqual(@as(u32, '→'), cell_1.?.char);\n    try std.testing.expectEqual(@as(f32, 0.3), cell_1.?.fg[0]);\n\n    const cell_2 = opt_buffer.get(2, 0);\n    try std.testing.expect(cell_2 != null);\n    try std.testing.expectEqual(@as(u32, 32), cell_2.?.char);\n\n    const cell_3 = opt_buffer.get(3, 0);\n    try std.testing.expect(cell_3 != null);\n    try std.testing.expectEqual(@as(u32, 32), cell_3.?.char);\n\n    const cell_4 = opt_buffer.get(4, 0);\n    try std.testing.expect(cell_4 != null);\n    try std.testing.expectEqual(@as(u32, 32), cell_4.?.char);\n\n    const cell_5 = opt_buffer.get(5, 0);\n    try std.testing.expect(cell_5 != null);\n    try std.testing.expectEqual(@as(u32, 'B'), cell_5.?.char);\n}\n\ntest \"EditorView - word wrapping during editing: typing with incremental wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 17, 10);\n    defer ev.deinit();\n\n    ev.setWrapMode(.word);\n\n    // Type \"Hello world ddddddddd\" character by character\n    // Width=17\n    // \"Hello world \" = 12 chars\n    // \"Hello world ddddd\" = 17 chars (fits exactly on one line)\n    // \"Hello world dddddd\" = 18 chars (should wrap after \"world \", moving ALL d's to next line)\n    //\n    // The key issue: word wrapping should keep the break point AFTER \"world \" consistently\n    // When \"Hello world dddddd\" (18 chars) wraps, it should become:\n    //   Line 1: \"Hello world \" (12 chars)\n    //   Line 2: \"dddddd\" (6 chars)\n    // NOT:\n    //   Line 1: \"Hello world ddddd\" (17 chars)\n    //   Line 2: \"d\" (1 char)\n    const text_to_type = \"Hello world ddddddddd\";\n\n    for (text_to_type, 0..) |char, i| {\n        var char_buf: [1]u8 = .{char};\n        try eb.insertText(&char_buf);\n        _ = ev.getVirtualLines();\n\n        const vline_count = ev.getTotalVirtualLineCount();\n        const cursor = ev.getPrimaryCursor();\n\n        // \"Hello world \" = 12 chars (i=11 completes this)\n        // \"Hello world d\" through \"Hello world ddddd\" = 13-17 chars (i=12 to i=16)\n        // \"Hello world dddddd\" = 18 chars (i=17) - should wrap AFTER \"world \"\n        const current_len = i + 1;\n        if (current_len <= 17) {\n            // Should fit on 1 line\n            try std.testing.expectEqual(@as(u32, 1), vline_count);\n        } else {\n            // Should wrap AFTER \"world \", moving ALL d's to line 2\n            try std.testing.expectEqual(@as(u32, 2), vline_count);\n\n            // Cursor should still be on row 0 (single logical line that wrapped)\n            try std.testing.expectEqual(@as(u32, 0), cursor.row);\n        }\n    }\n\n    // Now we have \"Hello world ddddddddd\" (21 chars) with word wrapping at width=17\n    // Should be: \"Hello world \" (12 chars) on vline 1, \"ddddddddd\" (9 chars) on vline 2\n    var vline_count = ev.getTotalVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 2), vline_count);\n\n    // Backspace to remove d's until only 2 remain: \"Hello world dd\"\n    // We need to delete 7 d's (from 9 d's to 2 d's)\n    var i: usize = 0;\n    while (i < 7) : (i += 1) {\n        try eb.backspace();\n        _ = ev.getVirtualLines();\n    }\n\n    // After removing 7 d's, we should have \"Hello world dd\" (14 chars)\n    // This should fit on one line at width=17\n    vline_count = ev.getTotalVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), vline_count);\n\n    var cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n    try std.testing.expectEqual(@as(u32, 14), cursor.col);\n\n    // Now type more d's again - should wrap correctly after \"world \"\n    // Starting with \"Hello world dd\" (14 chars)\n    const more_ds = \"ddddddd\";\n    for (more_ds, 0..) |char, j| {\n        var char_buf: [1]u8 = .{char};\n        try eb.insertText(&char_buf);\n        _ = ev.getVirtualLines();\n\n        vline_count = ev.getTotalVirtualLineCount();\n\n        // After each d:\n        // j=0: \"Hello world ddd\" (15) - fits on 1 line\n        // j=1: \"Hello world dddd\" (16) - fits on 1 line\n        // j=2: \"Hello world ddddd\" (17) - fits exactly on 1 line\n        // j=3: \"Hello world dddddd\" (18) - should wrap AFTER \"world \", moving ALL d's to line 2\n        // j=4: \"Hello world ddddddd\" (19) - still wrapped same way\n        // j=5: \"Hello world dddddddd\" (20) - still wrapped same way\n        // j=6: \"Hello world ddddddddd\" (21) - still wrapped same way\n        const current_len = 14 + j + 1;\n        if (current_len <= 17) {\n            // Should fit on 1 line\n            try std.testing.expectEqual(@as(u32, 1), vline_count);\n        } else {\n            // Should wrap AFTER \"world \", moving ALL d's to line 2\n            // This is the key: the wrap point should stay at \"world \" boundary\n            try std.testing.expectEqual(@as(u32, 2), vline_count);\n\n            // CRITICAL: Check that first virtual line is \"Hello world \" (12 chars)\n            // and second virtual line has all the d's\n            const vlines = ev.getVirtualLines();\n            try std.testing.expect(vlines.len == 2);\n\n            // First vline should be \"Hello world \" with width 12\n            try std.testing.expectEqual(@as(u32, 12), vlines[0].width_cols);\n\n            // Second vline should have all the d's (the original \"dd\" plus newly typed d's)\n            const expected_d_count: u32 = @as(u32, 2) + @as(u32, @intCast(j + 1)); // dd + newly typed d's\n            try std.testing.expectEqual(expected_d_count, vlines[1].width_cols);\n        }\n    }\n\n    // After adding 7 more d's, we have \"Hello world ddddddddd\" (21 chars) again\n    // Should wrap after \"world \" into 2 lines\n    vline_count = ev.getTotalVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 2), vline_count);\n\n    cursor = ev.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.row);\n\n    // Verify the text is correct\n    var out_buffer: [100]u8 = undefined;\n    const written = eb.getText(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello world ddddddddd\", out_buffer[0..written]);\n}\n\ntest \"EditorView - cursor movement with emoji skin tone modifier wcwidth\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    // \"👋🏿\" is a waving hand emoji with dark skin tone modifier\n    // In wcwidth mode (tmux-style), each codepoint has width 2, total = 4 columns\n    // IMPORTANT: In wcwidth mode, each codepoint is treated as a separate char for cursor movement\n    try eb.setText(\"👋🏿\");\n\n    // Start at position 0 (before the first codepoint)\n    try eb.setCursor(0, 0);\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    // Move right once - should move past the FIRST codepoint (2 columns)\n    // In wcwidth mode, each codepoint is a separate char, so this moves to col 2\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Move right again - should move past the SECOND codepoint (2 more columns)\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    // Move left once - should move back to col 2 (before second codepoint)\n    eb.moveLeft();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Move left again - should move back to the beginning\n    eb.moveLeft();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n}\n\ntest \"EditorView - cursor movement with emoji skin tone modifier unicode\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    // \"👋🏿\" is a waving hand emoji with dark skin tone modifier\n    // In unicode mode (modern terminals), skin tone is 0-width, total = 2 columns\n    try eb.setText(\"👋🏿\");\n\n    // Start at position 0 (before the grapheme cluster)\n    try eb.setCursor(0, 0);\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    // Move right once - should move past the entire grapheme cluster (2 columns in unicode)\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Move left once - should move back to the beginning\n    eb.moveLeft();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n}\n\ntest \"EditorView - backspace emoji with skin tone modifier wcwidth\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    // \"👋🏿\" is a waving hand emoji with dark skin tone modifier\n    // In wcwidth mode, this renders as 4 columns (2+2)\n    // In wcwidth mode, each codepoint is treated as a separate char\n    try eb.setText(\"👋🏿\");\n\n    // Move cursor to col 2 (after first codepoint)\n    eb.moveRight();\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Move cursor to col 4 (after second codepoint)\n    eb.moveRight();\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 4), cursor.col);\n\n    // Get text before backspace to verify it contains the emoji\n    var buffer_before: [100]u8 = undefined;\n    const len_before = eb.getText(&buffer_before);\n    try std.testing.expectEqualStrings(\"👋🏿\", buffer_before[0..len_before]);\n\n    // First backspace should delete just the skin tone modifier (second codepoint)\n    try eb.backspace();\n\n    // Cursor should now be at position 2 (after the first codepoint)\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Text buffer should contain just the hand emoji without skin tone\n    var buffer_middle: [100]u8 = undefined;\n    const len_middle = eb.getText(&buffer_middle);\n    try std.testing.expectEqualStrings(\"👋\", buffer_middle[0..len_middle]);\n\n    // Second backspace should delete the hand emoji (first codepoint)\n    try eb.backspace();\n\n    // Cursor should now be at position 0\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    // Text buffer should be empty\n    var buffer_after: [100]u8 = undefined;\n    const len_after = eb.getText(&buffer_after);\n    try std.testing.expectEqual(@as(usize, 0), len_after);\n    try std.testing.expectEqualStrings(\"\", buffer_after[0..len_after]);\n}\n\ntest \"EditorView - backspace emoji with skin tone modifier unicode\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 80, 24);\n    defer ev.deinit();\n\n    // \"👋🏿\" is a waving hand emoji with dark skin tone modifier\n    // In unicode mode, this renders as 2 columns (modifier is 0-width)\n    try eb.setText(\"👋🏿\");\n\n    // Move cursor to AFTER the grapheme cluster (2 columns total in unicode mode)\n    eb.moveRight();\n    var cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 2), cursor.col);\n\n    // Get text before backspace to verify it contains the emoji\n    var buffer_before: [100]u8 = undefined;\n    const len_before = eb.getText(&buffer_before);\n    try std.testing.expectEqualStrings(\"👋🏿\", buffer_before[0..len_before]);\n\n    // Backspace should delete the entire grapheme cluster (both codepoints)\n    try eb.backspace();\n\n    // Cursor should now be at position 0\n    cursor = eb.getPrimaryCursor();\n    try std.testing.expectEqual(@as(u32, 0), cursor.col);\n\n    // Text buffer should be empty\n    var buffer_after: [100]u8 = undefined;\n    const len_after = eb.getText(&buffer_after);\n    try std.testing.expectEqual(@as(usize, 0), len_after);\n    try std.testing.expectEqualStrings(\"\", buffer_after[0..len_after]);\n}\n\ntest \"EditorView - mouse selection doesn't scroll when focus is within viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 40, 10);\n    defer ev.deinit();\n\n    // Create 50 lines of text\n    var i: u32 = 0;\n    while (i < 50) : (i += 1) {\n        if (i > 0) try eb.insertText(\"\\n\");\n        try eb.insertText(\"Line \");\n        var num_buf: [10]u8 = undefined;\n        const num_str = try std.fmt.bufPrint(&num_buf, \"{d}\", .{i});\n        try eb.insertText(num_str);\n    }\n\n    // Reset cursor to top\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    const vp_initial = ev.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp_initial.y);\n\n    // Simulate selection within the viewport (lines 0-5, all visible)\n    _ = ev.setLocalSelection(0, 0, 5, 5, null, null, true);\n    _ = ev.getVirtualLines();\n\n    const vp_after = ev.getViewport().?;\n\n    // Viewport should not have changed\n    try std.testing.expectEqual(vp_initial.y, vp_after.y);\n    try std.testing.expectEqual(vp_initial.x, vp_after.x);\n}\n\ntest \"EditorView - mouse selection focus outside buffer bounds clamps correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var ev = try EditorView.init(std.testing.allocator, eb, 40, 10);\n    defer ev.deinit();\n\n    // Create just 10 lines\n    var i: u32 = 0;\n    while (i < 10) : (i += 1) {\n        if (i > 0) try eb.insertText(\"\\n\");\n        try eb.insertText(\"Line \");\n        var num_buf: [10]u8 = undefined;\n        const num_str = try std.fmt.bufPrint(&num_buf, \"{d}\", .{i});\n        try eb.insertText(num_str);\n    }\n\n    try eb.setCursor(0, 0);\n    _ = ev.getVirtualLines();\n\n    // Try to select way beyond buffer (to line 100)\n    _ = ev.setLocalSelection(0, 0, 5, 100, null, null, true);\n    _ = ev.getVirtualLines();\n\n    const cursor = ev.getPrimaryCursor();\n\n    // Cursor should be clamped to last line (line 9)\n    try std.testing.expectEqual(@as(u32, 9), cursor.row);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/event-emitter_test.zig",
    "content": "const std = @import(\"std\");\nconst event_emitter = @import(\"../event-emitter.zig\");\n\nconst EventType = enum {\n    start,\n    stop,\n    update,\n};\n\nconst Counter = struct {\n    count: u32,\n\n    pub fn increment(ctx: *anyopaque) void {\n        const self: *Counter = @ptrCast(@alignCast(ctx));\n        self.count += 1;\n    }\n\n    pub fn reset(ctx: *anyopaque) void {\n        const self: *Counter = @ptrCast(@alignCast(ctx));\n        self.count = 0;\n    }\n};\n\ntest \"EventEmitter - can initialize and deinitialize\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n}\n\ntest \"EventEmitter - can add listener with on\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    var counter = Counter{ .count = 0 };\n    const listener = Emitter.Listener{\n        .ctx = &counter,\n        .handle = Counter.increment,\n    };\n\n    try emitter.on(.start, listener);\n\n    const list = emitter.listeners.get(.start);\n    try std.testing.expect(list != null);\n    try std.testing.expectEqual(@as(usize, 1), list.?.items.len);\n}\n\ntest \"EventEmitter - can add multiple listeners to same event\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    var counter1 = Counter{ .count = 0 };\n    var counter2 = Counter{ .count = 0 };\n\n    const listener1 = Emitter.Listener{\n        .ctx = &counter1,\n        .handle = Counter.increment,\n    };\n\n    const listener2 = Emitter.Listener{\n        .ctx = &counter2,\n        .handle = Counter.increment,\n    };\n\n    try emitter.on(.start, listener1);\n    try emitter.on(.start, listener2);\n\n    const list = emitter.listeners.get(.start);\n    try std.testing.expectEqual(@as(usize, 2), list.?.items.len);\n}\n\ntest \"EventEmitter - can add listeners to different events\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    var counter1 = Counter{ .count = 0 };\n    var counter2 = Counter{ .count = 0 };\n\n    const listener1 = Emitter.Listener{\n        .ctx = &counter1,\n        .handle = Counter.increment,\n    };\n\n    const listener2 = Emitter.Listener{\n        .ctx = &counter2,\n        .handle = Counter.reset,\n    };\n\n    try emitter.on(.start, listener1);\n    try emitter.on(.stop, listener2);\n\n    const start_list = emitter.listeners.get(.start);\n    const stop_list = emitter.listeners.get(.stop);\n\n    try std.testing.expectEqual(@as(usize, 1), start_list.?.items.len);\n    try std.testing.expectEqual(@as(usize, 1), stop_list.?.items.len);\n}\n\ntest \"EventEmitter - emit calls all listeners for event\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    var counter1 = Counter{ .count = 0 };\n    var counter2 = Counter{ .count = 0 };\n    var counter3 = Counter{ .count = 0 };\n\n    const listener1 = Emitter.Listener{\n        .ctx = &counter1,\n        .handle = Counter.increment,\n    };\n\n    const listener2 = Emitter.Listener{\n        .ctx = &counter2,\n        .handle = Counter.increment,\n    };\n\n    const listener3 = Emitter.Listener{\n        .ctx = &counter3,\n        .handle = Counter.increment,\n    };\n\n    try emitter.on(.start, listener1);\n    try emitter.on(.start, listener2);\n    try emitter.on(.stop, listener3);\n\n    emitter.emit(.start);\n\n    try std.testing.expectEqual(@as(u32, 1), counter1.count);\n    try std.testing.expectEqual(@as(u32, 1), counter2.count);\n    try std.testing.expectEqual(@as(u32, 0), counter3.count);\n\n    emitter.emit(.stop);\n\n    try std.testing.expectEqual(@as(u32, 1), counter1.count);\n    try std.testing.expectEqual(@as(u32, 1), counter2.count);\n    try std.testing.expectEqual(@as(u32, 1), counter3.count);\n}\n\ntest \"EventEmitter - can remove listener with off\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    var counter = Counter{ .count = 0 };\n    const listener = Emitter.Listener{\n        .ctx = &counter,\n        .handle = Counter.increment,\n    };\n\n    try emitter.on(.start, listener);\n\n    var list = emitter.listeners.get(.start);\n    try std.testing.expectEqual(@as(usize, 1), list.?.items.len);\n\n    emitter.off(.start, listener);\n\n    list = emitter.listeners.get(.start);\n    try std.testing.expectEqual(@as(usize, 0), list.?.items.len);\n}\n\ntest \"EventEmitter - off removes only matching listener by reference\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    var counter1 = Counter{ .count = 0 };\n    var counter2 = Counter{ .count = 0 };\n\n    const listener1 = Emitter.Listener{\n        .ctx = &counter1,\n        .handle = Counter.increment,\n    };\n\n    const listener2 = Emitter.Listener{\n        .ctx = &counter2,\n        .handle = Counter.increment,\n    };\n\n    try emitter.on(.start, listener1);\n    try emitter.on(.start, listener2);\n\n    var list = emitter.listeners.get(.start);\n    try std.testing.expectEqual(@as(usize, 2), list.?.items.len);\n\n    emitter.off(.start, listener1);\n\n    list = emitter.listeners.get(.start);\n    try std.testing.expectEqual(@as(usize, 1), list.?.items.len);\n\n    emitter.emit(.start);\n    try std.testing.expectEqual(@as(u32, 0), counter1.count);\n    try std.testing.expectEqual(@as(u32, 1), counter2.count);\n}\n\ntest \"EventEmitter - emit with no listeners does not crash\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    emitter.emit(.start);\n    emitter.emit(.stop);\n    emitter.emit(.update);\n}\n\ntest \"EventEmitter - multiple emits increment counter correctly\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    var counter = Counter{ .count = 0 };\n    const listener = Emitter.Listener{\n        .ctx = &counter,\n        .handle = Counter.increment,\n    };\n\n    try emitter.on(.update, listener);\n\n    emitter.emit(.update);\n    emitter.emit(.update);\n    emitter.emit(.update);\n\n    try std.testing.expectEqual(@as(u32, 3), counter.count);\n}\n\ntest \"EventEmitter - listeners are isolated per event type\" {\n    const Emitter = event_emitter.EventEmitter(EventType);\n    var emitter = Emitter.init(std.testing.allocator);\n    defer emitter.deinit();\n\n    var counter = Counter{ .count = 0 };\n    const listener = Emitter.Listener{\n        .ctx = &counter,\n        .handle = Counter.increment,\n    };\n\n    try emitter.on(.start, listener);\n\n    emitter.emit(.start);\n    try std.testing.expectEqual(@as(u32, 1), counter.count);\n\n    emitter.emit(.stop);\n    try std.testing.expectEqual(@as(u32, 1), counter.count);\n\n    emitter.emit(.start);\n    try std.testing.expectEqual(@as(u32, 2), counter.count);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/grapheme_test.zig",
    "content": "const std = @import(\"std\");\nconst gp = @import(\"../grapheme.zig\");\n\nconst GraphemePool = gp.GraphemePool;\nconst GraphemeTracker = gp.GraphemeTracker;\n\ntest \"GraphemePool - can initialize and cleanup\" {\n    // Just verify init/deinit don't crash\n    var pool = GraphemePool.init(std.testing.allocator);\n    pool.deinit();\n}\n\ntest \"GraphemePool - alloc and get small grapheme\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"a\";\n    const id = try pool.alloc(text);\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, text, retrieved);\n}\n\ntest \"GraphemePool - alloc and get emoji\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const emoji = \"🌟\";\n    const id = try pool.alloc(emoji);\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, emoji, retrieved);\n}\n\ntest \"GraphemePool - alloc and get multi-byte grapheme\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const grapheme = \"é\";\n    const id = try pool.alloc(grapheme);\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, grapheme, retrieved);\n}\n\ntest \"GraphemePool - alloc and get combining character grapheme\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const grapheme = \"e\\u{0301}\"; // e with combining acute accent\n    const id = try pool.alloc(grapheme);\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, grapheme, retrieved);\n}\n\ntest \"GraphemePool - multiple allocations\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"a\";\n    const text2 = \"b\";\n    const text3 = \"🌟\";\n\n    const id1 = try pool.alloc(text1);\n    const id2 = try pool.alloc(text2);\n    const id3 = try pool.alloc(text3);\n    try pool.incref(id1);\n    try pool.incref(id2);\n    try pool.incref(id3);\n    defer pool.decref(id1) catch {};\n    defer pool.decref(id2) catch {};\n    defer pool.decref(id3) catch {};\n\n    try std.testing.expect(id1 != id2);\n    try std.testing.expect(id2 != id3);\n    try std.testing.expect(id1 != id3);\n\n    try std.testing.expectEqualSlices(u8, text1, try pool.get(id1));\n    try std.testing.expectEqualSlices(u8, text2, try pool.get(id2));\n    try std.testing.expectEqualSlices(u8, text3, try pool.get(id3));\n}\n\ntest \"GraphemePool - handles various size graphemes\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const small = \"a\";\n    const medium = \"0123456789\";\n    const large = \"012345678901234567890123456789\";\n\n    const id_small = try pool.alloc(small);\n    const id_medium = try pool.alloc(medium);\n    const id_large = try pool.alloc(large);\n    try pool.incref(id_small);\n    try pool.incref(id_medium);\n    try pool.incref(id_large);\n    defer pool.decref(id_small) catch {};\n    defer pool.decref(id_medium) catch {};\n    defer pool.decref(id_large) catch {};\n\n    try std.testing.expectEqualSlices(u8, small, try pool.get(id_small));\n    try std.testing.expectEqualSlices(u8, medium, try pool.get(id_medium));\n    try std.testing.expectEqualSlices(u8, large, try pool.get(id_large));\n}\n\ntest \"GraphemePool - large allocation (128 bytes)\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    var buffer: [128]u8 = undefined;\n    @memset(&buffer, 'X');\n\n    const id = try pool.alloc(&buffer);\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    const retrieved = try pool.get(id);\n\n    try std.testing.expectEqual(@as(usize, 128), retrieved.len);\n    try std.testing.expectEqualSlices(u8, &buffer, retrieved);\n}\n\ntest \"GraphemePool - incref increases refcount\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"a\";\n    const id = try pool.alloc(text);\n\n    // Initial refcount is 0, increment it\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, text, retrieved);\n}\n\ntest \"GraphemePool - decref once keeps data alive\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"a\";\n    const id = try pool.alloc(text);\n\n    // Initial refcount is 0, incref to 1, incref to 2\n    try pool.incref(id);\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    // Decref from 2 to 1\n    try pool.decref(id);\n\n    // Should still be accessible (refcount is 1)\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, text, retrieved);\n}\n\ntest \"GraphemePool - decref to zero allows slot reuse\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"a\";\n    const id1 = try pool.alloc(text1);\n    try pool.incref(id1);\n\n    // Decref to zero makes slot available for reuse\n    try pool.decref(id1);\n\n    // Allocate again - should reuse the freed slot with new generation\n    const text2 = \"b\";\n    const id2 = try pool.alloc(text2);\n\n    // Old ID should fail due to generation mismatch\n    const result1 = pool.get(id1);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result1);\n\n    try pool.incref(id2);\n    const retrieved = try pool.get(id2);\n    try std.testing.expectEqualSlices(u8, text2, retrieved);\n\n    try pool.decref(id2);\n}\n\ntest \"GraphemePool - multiple incref and decref\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"test\";\n    const id = try pool.alloc(text);\n\n    // Increment refcount multiple times (starting from 0)\n    try pool.incref(id);\n    try pool.incref(id);\n    try pool.incref(id);\n\n    try pool.decref(id);\n    try pool.decref(id);\n\n    // Should still be accessible (refcount is 1)\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, text, retrieved);\n\n    // Decrement to zero\n    try pool.decref(id);\n\n    // Allocate something else to trigger reuse with new generation\n    _ = try pool.alloc(\"x\");\n\n    // Old ID should now fail due to generation mismatch\n    const result = pool.get(id);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n\n    // Cleanup not needed since allocated IDs have refcount 0\n}\n\ntest \"GraphemePool - freed IDs become invalid after reuse\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"a\";\n    const text2 = \"b\";\n\n    const id1 = try pool.alloc(text1);\n    try pool.incref(id1);\n\n    // Decref to free the slot\n    try pool.decref(id1);\n\n    // Allocate again (pool may reuse internal storage)\n    const id2 = try pool.alloc(text2);\n\n    // Old ID should be invalid due to generation mismatch\n    const result = pool.get(id1);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n\n    try pool.incref(id2);\n    const retrieved = try pool.get(id2);\n    try std.testing.expectEqualSlices(u8, text2, retrieved);\n    try pool.decref(id2);\n}\n\ntest \"GraphemePool - stale ID with wrong generation fails\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"test\";\n    const id = try pool.alloc(text);\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    // Manually create a stale ID by modifying generation\n    const stale_id = id ^ (1 << gp.SLOT_BITS); // XOR generation bits\n\n    const result = pool.get(stale_id);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n}\n\ntest \"GraphemePool - decref on zero refcount fails\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"a\";\n    const id = try pool.alloc(text);\n\n    // Refcount starts at 0, so decref should fail immediately\n    const result = pool.decref(id);\n    try std.testing.expectError(gp.GraphemePoolError.InvalidId, result);\n}\n\ntest \"GraphemePool - many allocations\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const count = 1000;\n    var ids: [count]u32 = undefined;\n\n    for (0..count) |i| {\n        var buffer: [8]u8 = undefined;\n        const slice = std.fmt.bufPrint(&buffer, \"{d}\", .{i}) catch unreachable;\n        ids[i] = try pool.alloc(slice);\n        try pool.incref(ids[i]);\n    }\n\n    for (ids, 0..count) |id, i| {\n        const retrieved = try pool.get(id);\n        var buffer: [8]u8 = undefined;\n        const slice = std.fmt.bufPrint(&buffer, \"{d}\", .{i}) catch unreachable;\n        try std.testing.expectEqualSlices(u8, slice, retrieved);\n    }\n\n    for (ids) |id| {\n        try pool.decref(id);\n    }\n}\n\ntest \"GraphemePool - allocations with varying sizes\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    var ids: std.ArrayListUnmanaged(u32) = .{};\n    defer ids.deinit(std.testing.allocator);\n\n    for (0..50) |i| {\n        const size = (i % 5) * 16 + 5; // Vary sizes: 5, 21, 37, 53, 69...\n        var buffer: [128]u8 = undefined;\n        @memset(buffer[0..size], @intCast(i % 256));\n        const id = try pool.alloc(buffer[0..size]);\n        try pool.incref(id);\n        try ids.append(std.testing.allocator, id);\n    }\n\n    for (ids.items, 0..50) |id, i| {\n        const size = (i % 5) * 16 + 5;\n        const retrieved = try pool.get(id);\n        try std.testing.expectEqual(size, retrieved.len);\n        for (retrieved) |byte| {\n            try std.testing.expectEqual(@as(u8, @intCast(i % 256)), byte);\n        }\n    }\n\n    for (ids.items) |id| {\n        try pool.decref(id);\n    }\n}\n\ntest \"GraphemePool - reuse many slots\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    for (0..100) |i| {\n        var buffer: [8]u8 = undefined;\n        const slice = std.fmt.bufPrint(&buffer, \"{d}\", .{i}) catch unreachable;\n        const id = try pool.alloc(slice);\n        try pool.incref(id);\n\n        const retrieved = try pool.get(id);\n        try std.testing.expectEqualSlices(u8, slice, retrieved);\n\n        try pool.decref(id);\n    }\n}\n\ntest \"GraphemePool - invalid ID returns error\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"test\";\n    const id = try pool.alloc(text);\n    try pool.incref(id);\n\n    // Decref to free the slot\n    try pool.decref(id);\n\n    // Now allocate again to change generation\n    const text2 = \"test2\";\n    _ = try pool.alloc(text2);\n\n    // Original ID should now be invalid due to generation mismatch\n    const result = pool.get(id);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n}\n\ntest \"GraphemePool - IDs from different pools don't interfere\" {\n    var pool1 = GraphemePool.init(std.testing.allocator);\n    defer pool1.deinit();\n\n    var pool2 = GraphemePool.init(std.testing.allocator);\n    defer pool2.deinit();\n\n    const text1 = \"pool1_data\";\n    const text2 = \"pool2_data\";\n\n    const id1 = try pool1.alloc(text1);\n    const id2 = try pool2.alloc(text2);\n    try pool1.incref(id1);\n    try pool2.incref(id2);\n    defer pool1.decref(id1) catch {};\n    defer pool2.decref(id2) catch {};\n\n    try std.testing.expectEqualSlices(u8, text1, try pool1.get(id1));\n    try std.testing.expectEqualSlices(u8, text2, try pool2.get(id2));\n\n    // Using ID from pool1 in pool2 may succeed or fail depending on internal state,\n    // but should not return pool1's data or crash\n    _ = pool2.get(id1) catch |err| {\n        try std.testing.expectEqual(gp.GraphemePoolError.InvalidId, err);\n    };\n}\n\ntest \"GraphemePool - use-after-free returns error not garbage\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"first\";\n    const id1 = try pool.alloc(text1);\n    try pool.incref(id1);\n    try pool.decref(id1);\n\n    // Allocate something else to potentially reuse the slot\n    const text2 = \"second\";\n    const id2 = try pool.alloc(text2);\n\n    // Old ID should fail due to generation mismatch, not return text2 or garbage\n    const result = pool.get(id1);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n\n    try pool.incref(id2);\n    try std.testing.expectEqualSlices(u8, text2, try pool.get(id2));\n    try pool.decref(id2);\n}\n\ntest \"GraphemePool - IDs remain unique across many allocations\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const count = 100;\n    var ids: [count]u32 = undefined;\n\n    for (0..count) |i| {\n        var buffer: [8]u8 = undefined;\n        const slice = std.fmt.bufPrint(&buffer, \"{d}\", .{i}) catch unreachable;\n        ids[i] = try pool.alloc(slice);\n        try pool.incref(ids[i]);\n    }\n\n    for (ids, 0..count) |id1, i| {\n        for (ids[i + 1 ..]) |id2| {\n            try std.testing.expect(id1 != id2);\n        }\n    }\n\n    for (ids) |id| {\n        try pool.decref(id);\n    }\n}\n\ntest \"GraphemePool - concurrent incref/decref maintains consistency\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"test\";\n    const id = try pool.alloc(text);\n\n    // Multiple incref/decref operations (starting from refcount 0)\n    try pool.incref(id);\n    try pool.incref(id);\n    try pool.incref(id);\n\n    // Should still be accessible (refcount is 3)\n    try std.testing.expectEqualSlices(u8, text, try pool.get(id));\n\n    try pool.decref(id);\n    try std.testing.expectEqualSlices(u8, text, try pool.get(id));\n\n    try pool.decref(id);\n    try std.testing.expectEqualSlices(u8, text, try pool.get(id));\n\n    // Final decref brings refcount to 0\n    try pool.decref(id);\n}\n\ntest \"GraphemePool - zero-length grapheme\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const empty: []const u8 = \"\";\n    const id = try pool.alloc(empty);\n    try pool.incref(id);\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqual(@as(usize, 0), retrieved.len);\n\n    try pool.decref(id);\n}\n\ntest \"GraphemePool - incref on stale ID fails\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"test\";\n    const id = try pool.alloc(text);\n    try pool.incref(id);\n    try pool.decref(id);\n\n    // Allocate again to invalidate old ID\n    _ = try pool.alloc(\"new\");\n\n    const result = pool.incref(id); // Old ID should fail due to wrong generation\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n}\n\ntest \"GraphemePool - decref on stale ID fails\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"test\";\n    const id = try pool.alloc(text);\n\n    // Already at refcount 0, decref should fail\n    const result = pool.decref(id);\n    try std.testing.expectError(gp.GraphemePoolError.InvalidId, result);\n}\n\ntest \"GraphemePool - bit manipulation functions\" {\n    const grapheme_char = gp.CHAR_FLAG_GRAPHEME | 0x1234;\n    try std.testing.expect(gp.isGraphemeChar(grapheme_char));\n    try std.testing.expect(!gp.isGraphemeChar(0x41)); // Plain 'A'\n\n    const cont_char = gp.CHAR_FLAG_CONTINUATION | 0x1234;\n    try std.testing.expect(gp.isContinuationChar(cont_char));\n    try std.testing.expect(!gp.isContinuationChar(0x41));\n\n    try std.testing.expect(gp.isClusterChar(grapheme_char));\n    try std.testing.expect(gp.isClusterChar(cont_char));\n    try std.testing.expect(!gp.isClusterChar(0x41));\n\n    const id: u32 = 0x12345;\n    const packed_char = gp.CHAR_FLAG_GRAPHEME | id;\n    try std.testing.expectEqual(id, gp.graphemeIdFromChar(packed_char));\n}\n\ntest \"GraphemePool - extent encoding and decoding\" {\n    const right: u32 = 2;\n    const char_with_right = (right << gp.CHAR_EXT_RIGHT_SHIFT) | gp.CHAR_FLAG_GRAPHEME;\n    try std.testing.expectEqual(right, gp.charRightExtent(char_with_right));\n\n    const left: u32 = 1;\n    const char_with_left = (left << gp.CHAR_EXT_LEFT_SHIFT) | gp.CHAR_FLAG_GRAPHEME;\n    try std.testing.expectEqual(left, gp.charLeftExtent(char_with_left));\n}\n\ntest \"GraphemePool - packGraphemeStart\" {\n    const gid: u32 = 0x1234;\n    const width: u32 = 2;\n\n    const packed_char = gp.packGraphemeStart(gid, width);\n\n    try std.testing.expect(gp.isGraphemeChar(packed_char));\n\n    try std.testing.expectEqual(gid, gp.graphemeIdFromChar(packed_char));\n\n    try std.testing.expectEqual(width - 1, gp.charRightExtent(packed_char));\n\n    try std.testing.expectEqual(@as(u32, 0), gp.charLeftExtent(packed_char));\n}\n\ntest \"GraphemePool - packContinuation\" {\n    const gid: u32 = 0x1234;\n    const left: u32 = 1;\n    const right: u32 = 2;\n\n    const packed_char = gp.packContinuation(left, right, gid);\n\n    try std.testing.expect(gp.isContinuationChar(packed_char));\n\n    try std.testing.expectEqual(gid, gp.graphemeIdFromChar(packed_char));\n\n    try std.testing.expectEqual(left, gp.charLeftExtent(packed_char));\n    try std.testing.expectEqual(right, gp.charRightExtent(packed_char));\n}\n\ntest \"GraphemePool - encodedCharWidth\" {\n    const single = @as(u32, 'A');\n    try std.testing.expectEqual(@as(u32, 1), gp.encodedCharWidth(single));\n\n    const grapheme_2 = gp.packGraphemeStart(0x1234, 2);\n    try std.testing.expectEqual(@as(u32, 2), gp.encodedCharWidth(grapheme_2));\n\n    const cont = gp.packContinuation(1, 1, 0x1234);\n    try std.testing.expectEqual(@as(u32, 3), gp.encodedCharWidth(cont));\n}\n\ntest \"GraphemeTracker - init and deinit\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    try std.testing.expect(!tracker.hasAny());\n    try std.testing.expectEqual(@as(u32, 0), tracker.getGraphemeCount());\n}\n\ntest \"GraphemeTracker - add single grapheme\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"a\";\n    const id = try pool.alloc(text);\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.add(id);\n\n    try std.testing.expect(tracker.hasAny());\n    try std.testing.expect(tracker.contains(id));\n    try std.testing.expectEqual(@as(u32, 1), tracker.getGraphemeCount());\n}\n\ntest \"GraphemeTracker - add multiple graphemes\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"a\";\n    const text2 = \"b\";\n    const text3 = \"🌟\";\n\n    const id1 = try pool.alloc(text1);\n    const id2 = try pool.alloc(text2);\n    const id3 = try pool.alloc(text3);\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.add(id1);\n    tracker.add(id2);\n    tracker.add(id3);\n\n    try std.testing.expectEqual(@as(u32, 3), tracker.getGraphemeCount());\n    try std.testing.expect(tracker.contains(id1));\n    try std.testing.expect(tracker.contains(id2));\n    try std.testing.expect(tracker.contains(id3));\n}\n\ntest \"GraphemeTracker - add same grapheme twice increfs once\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"a\";\n    const id = try pool.alloc(text);\n\n    {\n        var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n        defer tracker.deinit();\n\n        tracker.add(id);\n        tracker.add(id); // Should not incref again\n\n        try std.testing.expectEqual(@as(u32, 1), tracker.getGraphemeCount());\n        try std.testing.expectEqual(@as(u32, 2), tracker.getGraphemeCellCount());\n        try std.testing.expectEqual(@as(u32, 2), tracker.getTotalGraphemeBytes());\n\n        tracker.remove(id);\n        try std.testing.expect(tracker.contains(id));\n        try std.testing.expectEqual(@as(u32, 1), tracker.getGraphemeCellCount());\n\n        // After deinit (via defer), tracker decrefs once, bringing refcount to 0\n    }\n\n    // Allocate new item to trigger slot reuse\n    const text2 = \"b\";\n    _ = try pool.alloc(text2);\n\n    // Old ID should now be invalid due to generation change\n    const result = pool.get(id);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n}\n\ntest \"GraphemeTracker - remove grapheme\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"a\";\n    const id = try pool.alloc(text);\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.add(id);\n    try std.testing.expect(tracker.contains(id));\n\n    tracker.remove(id);\n    try std.testing.expect(!tracker.contains(id));\n    try std.testing.expectEqual(@as(u32, 0), tracker.getGraphemeCount());\n}\n\ntest \"GraphemeTracker - remove non-existent grapheme is safe\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"a\";\n    const id = try pool.alloc(text);\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    // Remove without adding - should be safe\n    tracker.remove(id);\n\n    try std.testing.expectEqual(@as(u32, 0), tracker.getGraphemeCount());\n}\n\ntest \"GraphemeTracker - clear removes all graphemes\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"a\";\n    const text2 = \"b\";\n    const id1 = try pool.alloc(text1);\n    const id2 = try pool.alloc(text2);\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.add(id1);\n    tracker.add(id2);\n    try std.testing.expectEqual(@as(u32, 2), tracker.getGraphemeCount());\n\n    tracker.clear();\n\n    try std.testing.expectEqual(@as(u32, 0), tracker.getGraphemeCount());\n    try std.testing.expect(!tracker.contains(id1));\n    try std.testing.expect(!tracker.contains(id2));\n    try std.testing.expect(!tracker.hasAny());\n}\n\ntest \"GraphemeTracker - getTotalGraphemeBytes\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"a\"; // 1 byte\n    const text2 = \"🌟\"; // 4 bytes\n    const text3 = \"test\"; // 4 bytes\n\n    const id1 = try pool.alloc(text1);\n    const id2 = try pool.alloc(text2);\n    const id3 = try pool.alloc(text3);\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.add(id1);\n    tracker.add(id2);\n    tracker.add(id3);\n\n    const total_bytes = tracker.getTotalGraphemeBytes();\n    try std.testing.expectEqual(@as(u32, 1 + 4 + 4), total_bytes);\n}\n\ntest \"GraphemeTracker - tracker keeps graphemes alive\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"test\";\n    const id = try pool.alloc(text);\n\n    {\n        var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n        defer tracker.deinit();\n\n        tracker.add(id);\n\n        // Should be accessible because tracker holds a reference (refcount is 1)\n        const retrieved = try pool.get(id);\n        try std.testing.expectEqualSlices(u8, text, retrieved);\n\n        // After tracker deinit (via defer), refcount will be 0\n    }\n\n    // Allocate new item to trigger slot reuse with new generation\n    const text2 = \"x\";\n    _ = try pool.alloc(text2);\n\n    // Old ID should fail due to generation mismatch\n    const result = pool.get(id);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n}\n\ntest \"GraphemeTracker - multiple trackers share same grapheme\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text = \"shared\";\n    const id = try pool.alloc(text);\n\n    {\n        var tracker1 = GraphemeTracker.init(std.testing.allocator, &pool);\n        defer tracker1.deinit();\n\n        {\n            var tracker2 = GraphemeTracker.init(std.testing.allocator, &pool);\n            defer tracker2.deinit();\n\n            tracker1.add(id);\n            tracker2.add(id);\n\n            try std.testing.expect(tracker1.contains(id));\n            try std.testing.expect(tracker2.contains(id));\n\n            // Should be accessible (ref count is 2 from both trackers)\n            const retrieved = try pool.get(id);\n            try std.testing.expectEqualSlices(u8, text, retrieved);\n\n            // tracker2 deinit via defer here (decrefs to 1)\n        }\n\n        // Should still be accessible (ref count is 1)\n        const retrieved2 = try pool.get(id);\n        try std.testing.expectEqualSlices(u8, text, retrieved2);\n\n        // tracker1 deinit via defer here (decrefs to 0)\n    }\n\n    // Allocate new item to trigger slot reuse with new generation\n    const text2 = \"y\";\n    _ = try pool.alloc(text2);\n\n    // Old ID should fail due to generation mismatch\n    const result = pool.get(id);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n}\n\ntest \"GraphemeTracker - stress test many graphemes\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    const count = 500;\n    var ids: [count]u32 = undefined;\n\n    // Add many graphemes\n    for (0..count) |i| {\n        var buffer: [8]u8 = undefined;\n        const slice = std.fmt.bufPrint(&buffer, \"{d}\", .{i}) catch unreachable;\n        ids[i] = try pool.alloc(slice);\n        tracker.add(ids[i]);\n    }\n\n    try std.testing.expectEqual(@as(u32, count), tracker.getGraphemeCount());\n\n    // Verify all are tracked\n    for (ids) |id| {\n        try std.testing.expect(tracker.contains(id));\n    }\n\n    // Clear should remove all\n    tracker.clear();\n    try std.testing.expectEqual(@as(u32, 0), tracker.getGraphemeCount());\n\n    for (ids) |id| {\n        try std.testing.expect(!tracker.contains(id));\n    }\n}\n\ntest \"GraphemePool - global pool init and deinit\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    const text = \"test\";\n    const id = try pool.alloc(text);\n    try pool.incref(id);\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, text, retrieved);\n\n    try pool.decref(id);\n}\n\ntest \"GraphemePool - global pool reinitialization returns same instance\" {\n    const pool1 = gp.initGlobalPool(std.testing.allocator);\n    const pool2 = gp.initGlobalPool(std.testing.allocator);\n\n    try std.testing.expectEqual(pool1, pool2);\n\n    gp.deinitGlobalPool();\n}\n\ntest \"GraphemePool - global unicode data init\" {\n\n    // Pointers should not be null (just verify they're returned)\n    // We can't easily test their validity without using them\n}\n\ntest \"GraphemePool - allocUnowned basic\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    // External memory that we manage\n    const external_text = \"external\";\n    const id = try pool.allocUnowned(external_text);\n    try pool.incref(id);\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, external_text, retrieved);\n\n    // Verify it's actually pointing to the same memory location\n    try std.testing.expectEqual(@intFromPtr(external_text.ptr), @intFromPtr(retrieved.ptr));\n\n    try pool.decref(id);\n}\n\ntest \"GraphemePool - allocUnowned multiple references\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const external_text1 = \"external1\";\n    const external_text2 = \"external2\";\n    const external_text3 = \"external3\";\n\n    const id1 = try pool.allocUnowned(external_text1);\n    const id2 = try pool.allocUnowned(external_text2);\n    const id3 = try pool.allocUnowned(external_text3);\n    try pool.incref(id1);\n    try pool.incref(id2);\n    try pool.incref(id3);\n\n    try std.testing.expectEqualSlices(u8, external_text1, try pool.get(id1));\n    try std.testing.expectEqualSlices(u8, external_text2, try pool.get(id2));\n    try std.testing.expectEqualSlices(u8, external_text3, try pool.get(id3));\n\n    // Verify they point to original memory\n    try std.testing.expectEqual(@intFromPtr(external_text1.ptr), @intFromPtr((try pool.get(id1)).ptr));\n    try std.testing.expectEqual(@intFromPtr(external_text2.ptr), @intFromPtr((try pool.get(id2)).ptr));\n    try std.testing.expectEqual(@intFromPtr(external_text3.ptr), @intFromPtr((try pool.get(id3)).ptr));\n\n    try pool.decref(id1);\n    try pool.decref(id2);\n    try pool.decref(id3);\n}\n\ntest \"GraphemePool - allocUnowned with emoji\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const external_emoji = \"🌟🎉🚀\";\n    const id = try pool.allocUnowned(external_emoji);\n    try pool.incref(id);\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, external_emoji, retrieved);\n    try std.testing.expectEqual(@intFromPtr(external_emoji.ptr), @intFromPtr(retrieved.ptr));\n\n    try pool.decref(id);\n}\n\ntest \"GraphemePool - allocUnowned refcounting\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const external_text = \"refcount_test\";\n    const id = try pool.allocUnowned(external_text);\n\n    // Increment refcount (starting from 0)\n    try pool.incref(id);\n    try pool.incref(id);\n    try pool.incref(id);\n\n    // Should still be accessible (refcount is 3)\n    try std.testing.expectEqualSlices(u8, external_text, try pool.get(id));\n\n    // Decrement\n    try pool.decref(id);\n    try std.testing.expectEqualSlices(u8, external_text, try pool.get(id));\n\n    try pool.decref(id);\n    try std.testing.expectEqualSlices(u8, external_text, try pool.get(id));\n\n    // Final decref\n    try pool.decref(id);\n}\n\ntest \"GraphemePool - mix owned and unowned allocations\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const owned_text = \"owned\";\n    const external_text = \"unowned\";\n\n    const owned_id = try pool.alloc(owned_text);\n    const unowned_id = try pool.allocUnowned(external_text);\n    try pool.incref(owned_id);\n    try pool.incref(unowned_id);\n\n    const retrieved_owned = try pool.get(owned_id);\n    const retrieved_unowned = try pool.get(unowned_id);\n\n    try std.testing.expectEqualSlices(u8, owned_text, retrieved_owned);\n    try std.testing.expectEqualSlices(u8, external_text, retrieved_unowned);\n\n    // Owned should be different memory location (copy)\n    try std.testing.expect(@intFromPtr(owned_text.ptr) != @intFromPtr(retrieved_owned.ptr));\n\n    // Unowned should be same memory location (reference)\n    try std.testing.expectEqual(@intFromPtr(external_text.ptr), @intFromPtr(retrieved_unowned.ptr));\n\n    try pool.decref(owned_id);\n    try pool.decref(unowned_id);\n}\n\ntest \"GraphemePool - allocUnowned slot reuse\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"first\";\n    const id1 = try pool.allocUnowned(text1);\n    try pool.incref(id1);\n    try pool.decref(id1);\n\n    // Allocate again - should reuse slot\n    const text2 = \"second\";\n    const id2 = try pool.allocUnowned(text2);\n\n    const result = pool.get(id1);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n\n    try pool.incref(id2);\n    const retrieved = try pool.get(id2);\n    try std.testing.expectEqualSlices(u8, text2, retrieved);\n    try std.testing.expectEqual(@intFromPtr(text2.ptr), @intFromPtr(retrieved.ptr));\n\n    try pool.decref(id2);\n}\n\ntest \"GraphemePool - allocUnowned large text\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    // Large external buffer\n    var large_buffer: [1000]u8 = undefined;\n    @memset(&large_buffer, 'X');\n    const large_slice: []const u8 = &large_buffer;\n\n    const id = try pool.allocUnowned(large_slice);\n    try pool.incref(id);\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqual(@as(usize, 1000), retrieved.len);\n    try std.testing.expectEqualSlices(u8, large_slice, retrieved);\n    try std.testing.expectEqual(@intFromPtr(large_slice.ptr), @intFromPtr(retrieved.ptr));\n\n    try pool.decref(id);\n}\n\ntest \"GraphemePool - alloc does not reuse unowned IDs\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const external_text = \"shared\";\n\n    const unowned_id = try pool.allocUnowned(external_text);\n    try pool.incref(unowned_id);\n    defer pool.decref(unowned_id) catch {};\n\n    const owned_id = try pool.alloc(external_text);\n    try pool.incref(owned_id);\n    defer pool.decref(owned_id) catch {};\n\n    try std.testing.expect(owned_id != unowned_id);\n\n    const owned_bytes = try pool.get(owned_id);\n    try std.testing.expectEqualSlices(u8, external_text, owned_bytes);\n    try std.testing.expect(@intFromPtr(owned_bytes.ptr) != @intFromPtr(external_text.ptr));\n}\n\ntest \"GraphemeTracker - with unowned allocations\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const text1 = \"external1\";\n    const text2 = \"external2\";\n\n    const id1 = try pool.allocUnowned(text1);\n    const id2 = try pool.allocUnowned(text2);\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.add(id1);\n    tracker.add(id2);\n\n    try std.testing.expectEqual(@as(u32, 2), tracker.getGraphemeCount());\n    try std.testing.expect(tracker.contains(id1));\n    try std.testing.expect(tracker.contains(id2));\n\n    // Should still get correct bytes\n    try std.testing.expectEqualSlices(u8, text1, try pool.get(id1));\n    try std.testing.expectEqualSlices(u8, text2, try pool.get(id2));\n}\n\ntest \"GraphemeTracker - mix owned and unowned\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const owned_text = \"owned_data\";\n    const external_text = \"external_data\";\n\n    const owned_id = try pool.alloc(owned_text);\n    const unowned_id = try pool.allocUnowned(external_text);\n\n    var tracker = GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.add(owned_id);\n    tracker.add(unowned_id);\n\n    try std.testing.expectEqual(@as(u32, 2), tracker.getGraphemeCount());\n\n    const total_bytes = tracker.getTotalGraphemeBytes();\n    try std.testing.expectEqual(@as(u32, owned_text.len + external_text.len), total_bytes);\n\n    try std.testing.expectEqualSlices(u8, owned_text, try pool.get(owned_id));\n    try std.testing.expectEqualSlices(u8, external_text, try pool.get(unowned_id));\n}\n\ntest \"GraphemePool - allocUnowned with stack memory\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    // Simulate stack-allocated buffer\n    var stack_buffer: [50]u8 = undefined;\n    @memcpy(stack_buffer[0..11], \"stack_based\");\n    const stack_slice = stack_buffer[0..11];\n\n    const id = try pool.allocUnowned(stack_slice);\n    try pool.incref(id);\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, \"stack_based\", retrieved);\n    try std.testing.expectEqual(@intFromPtr(stack_slice.ptr), @intFromPtr(retrieved.ptr));\n\n    try pool.decref(id);\n    // Note: In real usage, caller must ensure stack_buffer stays valid while ID is in use\n}\n\ntest \"GraphemePool - allocUnowned zero-length slice\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const empty: []const u8 = \"\";\n    const id = try pool.allocUnowned(empty);\n    try pool.incref(id);\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqual(@as(usize, 0), retrieved.len);\n\n    try pool.decref(id);\n}\n\ntest \"GraphemePool - initWithOptions with small slots_per_page\" {\n    // Create a pool with very small slots_per_page to test exhaustion\n    const small_slots = [_]u32{ 2, 2, 2, 2, 2 }; // Only 2 slots per page for each class\n    var pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = small_slots,\n    });\n    defer pool.deinit();\n\n    const id1 = try pool.alloc(\"abc\");\n    const id2 = try pool.alloc(\"def\");\n    try pool.incref(id1);\n    try pool.incref(id2);\n\n    try std.testing.expectEqualSlices(u8, \"abc\", try pool.get(id1));\n    try std.testing.expectEqualSlices(u8, \"def\", try pool.get(id2));\n\n    try pool.decref(id1);\n    try pool.decref(id2);\n}\n\ntest \"GraphemePool - alloc reuses live ID for same bytes\" {\n    const tiny_slots = [_]u32{ 1, 1, 1, 1, 1 };\n    var pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer pool.deinit();\n\n    const grapheme = \"👋\";\n\n    const id1 = try pool.alloc(grapheme);\n    try pool.incref(id1);\n\n    const id2 = try pool.alloc(grapheme);\n    try std.testing.expectEqual(id1, id2);\n    try std.testing.expectEqual(@as(u32, 1), try pool.getRefcount(id1));\n\n    try pool.decref(id1);\n\n    const id3 = try pool.alloc(grapheme);\n    try pool.incref(id3);\n    defer pool.decref(id3) catch @panic(\"Failed to decref id3\");\n\n    try std.testing.expect(id3 != id1);\n    try std.testing.expectEqualSlices(u8, grapheme, try pool.get(id3));\n\n    const id4 = try pool.alloc(grapheme);\n    try std.testing.expectEqual(id3, id4);\n}\n\ntest \"GraphemePool - small pool exhaustion and growth\" {\n    // Create a tiny pool that will need to grow\n    const tiny_slots = [_]u32{ 1, 1, 1, 1, 1 }; // Only 1 slot per page initially\n    var pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer pool.deinit();\n\n    // Allocate first item - uses initial page\n    const id1 = try pool.alloc(\"a\");\n\n    // Allocate second item - should trigger growth (new page)\n    const id2 = try pool.alloc(\"b\");\n    try pool.incref(id1);\n    try pool.incref(id2);\n\n    try std.testing.expectEqualSlices(u8, \"a\", try pool.get(id1));\n    try std.testing.expectEqualSlices(u8, \"b\", try pool.get(id2));\n\n    try pool.decref(id1);\n    try pool.decref(id2);\n}\n\ntest \"GraphemePool - small pool with refcount prevents exhaustion\" {\n    const tiny_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer pool.deinit();\n\n    // Allocate 2 items (fills the first page)\n    const id1 = try pool.alloc(\"aa\");\n    const id2 = try pool.alloc(\"bb\");\n    try pool.incref(id1);\n    try pool.incref(id2);\n\n    // Free one\n    try pool.decref(id1);\n\n    const id3 = try pool.alloc(\"cc\");\n    try pool.incref(id3);\n\n    try std.testing.expectEqualSlices(u8, \"bb\", try pool.get(id2));\n    try std.testing.expectEqualSlices(u8, \"cc\", try pool.get(id3));\n\n    // Old id1 should be invalid due to generation change\n    const result = pool.get(id1);\n    try std.testing.expectError(gp.GraphemePoolError.WrongGeneration, result);\n\n    try pool.decref(id2);\n    try pool.decref(id3);\n}\n\ntest \"GraphemePool - different size classes with small limits\" {\n    const tiny_slots = [_]u32{ 2, 2, 2, 2, 2 };\n    var pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer pool.deinit();\n\n    // Allocate different sizes (should use different classes)\n    const id_small = try pool.alloc(\"ab\"); // 2 bytes -> class 0 (8-byte slots)\n    const id_medium = try pool.alloc(\"0123456789abc\"); // 13 bytes -> class 1 (16-byte slots)\n    const id_large = try pool.alloc(\"012345678901234567890\"); // 21 bytes -> class 2 (32-byte slots)\n    try pool.incref(id_small);\n    try pool.incref(id_medium);\n    try pool.incref(id_large);\n\n    try std.testing.expectEqualSlices(u8, \"ab\", try pool.get(id_small));\n    try std.testing.expectEqualSlices(u8, \"0123456789abc\", try pool.get(id_medium));\n    try std.testing.expectEqualSlices(u8, \"012345678901234567890\", try pool.get(id_large));\n\n    try pool.decref(id_small);\n    try pool.decref(id_medium);\n    try pool.decref(id_large);\n}\n\ntest \"GraphemePool - tracker with small pool\" {\n    const tiny_slots = [_]u32{ 3, 3, 3, 3, 3 };\n    var pool = gp.GraphemePool.initWithOptions(std.testing.allocator, .{\n        .slots_per_page = tiny_slots,\n    });\n    defer pool.deinit();\n\n    var tracker = gp.GraphemeTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    // Add multiple graphemes\n    const id1 = try pool.alloc(\"🌟\");\n    const id2 = try pool.alloc(\"🎨\");\n    const id3 = try pool.alloc(\"🚀\");\n\n    tracker.add(id1);\n    tracker.add(id2);\n    tracker.add(id3);\n\n    try std.testing.expectEqual(@as(u32, 3), tracker.getGraphemeCount());\n\n    // Clear tracker should free all refs\n    tracker.clear();\n    try std.testing.expectEqual(@as(u32, 0), tracker.getGraphemeCount());\n\n    // After tracker.clear(), the graphemes have been decref'd by tracker\n    // Since alloc() starts with refcount 0, after tracker decrefs, they're freed\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/link_test.zig",
    "content": "const std = @import(\"std\");\nconst link = @import(\"../link.zig\");\n\nconst LinkPool = link.LinkPool;\nconst LinkPoolError = link.LinkPoolError;\nconst LinkTracker = link.LinkTracker;\n\ntest \"LinkPool - can initialize and cleanup\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    pool.deinit();\n}\n\ntest \"LinkPool - alloc and get URL\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const url = \"https://example.com\";\n    const id = try pool.alloc(url);\n    try pool.incref(id);\n    defer pool.decref(id) catch {};\n\n    const retrieved = try pool.get(id);\n    try std.testing.expectEqualSlices(u8, url, retrieved);\n}\n\ntest \"LinkPool - decref to zero allows slot reuse\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const id1 = try pool.alloc(\"https://first.example\");\n    try pool.incref(id1);\n    try pool.decref(id1);\n\n    const id2 = try pool.alloc(\"https://second.example\");\n    try pool.incref(id2);\n    defer pool.decref(id2) catch {};\n\n    const stale_get = pool.get(id1);\n    try std.testing.expectError(LinkPoolError.WrongGeneration, stale_get);\n\n    const stale_incref = pool.incref(id1);\n    try std.testing.expectError(LinkPoolError.WrongGeneration, stale_incref);\n\n    const stale_decref = pool.decref(id1);\n    try std.testing.expectError(LinkPoolError.WrongGeneration, stale_decref);\n\n    try std.testing.expectEqualSlices(u8, \"https://second.example\", try pool.get(id2));\n}\n\ntest \"LinkPool - decref on zero refcount fails\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const id = try pool.alloc(\"https://example.com\");\n    try std.testing.expectError(LinkPoolError.InvalidId, pool.decref(id));\n}\n\ntest \"LinkPool - alloc never returns sentinel zero ID\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const rounds: usize = 300;\n    for (0..rounds) |_| {\n        const id = try pool.alloc(\"https://example.com/rotate\");\n        try std.testing.expect(id != 0);\n\n        try pool.incref(id);\n        try pool.decref(id);\n    }\n}\n\ntest \"LinkTracker - add/remove keeps one pool ref per ID\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const id = try pool.alloc(\"https://example.com/same\");\n\n    var tracker = LinkTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.addCellRef(id);\n    tracker.addCellRef(id);\n    tracker.addCellRef(id);\n\n    try std.testing.expectEqual(@as(u32, 1), tracker.getLinkCount());\n    try std.testing.expectEqual(@as(u32, 1), try pool.getRefcount(id));\n\n    tracker.removeCellRef(id);\n    try std.testing.expectEqual(@as(u32, 1), tracker.getLinkCount());\n    try std.testing.expectEqual(@as(u32, 1), try pool.getRefcount(id));\n\n    tracker.removeCellRef(id);\n    try std.testing.expectEqual(@as(u32, 1), tracker.getLinkCount());\n    try std.testing.expectEqual(@as(u32, 1), try pool.getRefcount(id));\n\n    tracker.removeCellRef(id);\n    try std.testing.expectEqual(@as(u32, 0), tracker.getLinkCount());\n    try std.testing.expectEqual(@as(u32, 0), try pool.getRefcount(id));\n}\n\ntest \"LinkTracker - clear releases tracked IDs\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const id1 = try pool.alloc(\"https://example.com/1\");\n    const id2 = try pool.alloc(\"https://example.com/2\");\n\n    var tracker = LinkTracker.init(std.testing.allocator, &pool);\n    defer tracker.deinit();\n\n    tracker.addCellRef(id1);\n    tracker.addCellRef(id2);\n\n    try std.testing.expect(tracker.hasAny());\n    try std.testing.expectEqual(@as(u32, 2), try pool.getRefcount(id1) + try pool.getRefcount(id2));\n\n    tracker.clear();\n\n    try std.testing.expect(!tracker.hasAny());\n    try std.testing.expectEqual(@as(u32, 0), try pool.getRefcount(id1));\n    try std.testing.expectEqual(@as(u32, 0), try pool.getRefcount(id2));\n}\n\ntest \"LinkTracker - clear only decrefs once per ID with multiple cell refs\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const id = try pool.alloc(\"https://example.com/shared\");\n\n    var tracker_a = LinkTracker.init(std.testing.allocator, &pool);\n    defer tracker_a.deinit();\n\n    var tracker_b = LinkTracker.init(std.testing.allocator, &pool);\n    defer tracker_b.deinit();\n\n    tracker_a.addCellRef(id);\n    tracker_a.addCellRef(id);\n    tracker_a.addCellRef(id);\n\n    tracker_b.addCellRef(id);\n\n    try std.testing.expectEqual(@as(u32, 2), try pool.getRefcount(id));\n\n    // Clear tracker A should decref once (2 -> 1).\n    tracker_a.clear();\n\n    try std.testing.expectEqual(@as(u32, 1), try pool.getRefcount(id));\n    try std.testing.expectEqualSlices(u8, \"https://example.com/shared\", try pool.get(id));\n}\n\ntest \"LinkPool - leak repro: alloc-only IDs accumulate live slots\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const rounds: usize = 4096;\n    for (0..rounds) |i| {\n        var buf: [64]u8 = undefined;\n        const url = std.fmt.bufPrint(&buf, \"https://example.com/r{d}\", .{i}) catch unreachable;\n        _ = try pool.alloc(url);\n    }\n\n    try std.testing.expect(pool.getLiveSlotCount() > 0);\n    try std.testing.expect(pool.getFreeSlotCount() < pool.getTotalSlots());\n}\n\ntest \"LinkPool - alloc reuses live ID for same URL\" {\n    var pool = LinkPool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const url = \"https://example.com/stable\";\n\n    const id1 = try pool.alloc(url);\n    try pool.incref(id1);\n\n    const id2 = try pool.alloc(url);\n    try std.testing.expectEqual(id1, id2);\n    try std.testing.expectEqual(@as(u32, 1), try pool.getRefcount(id1));\n\n    try pool.decref(id1);\n\n    const id3 = try pool.alloc(url);\n    try pool.incref(id3);\n    defer pool.decref(id3) catch {};\n\n    try std.testing.expect(id3 != id1);\n    try std.testing.expectEqualSlices(u8, url, try pool.get(id3));\n\n    const id4 = try pool.alloc(url);\n    try std.testing.expectEqual(id3, id4);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/mem-registry_test.zig",
    "content": "const std = @import(\"std\");\nconst mem_registry = @import(\"../mem-registry.zig\");\n\nconst MemRegistry = mem_registry.MemRegistry;\nconst MemRegistryError = mem_registry.MemRegistryError;\n\ntest \"MemRegistry - init and deinit\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    try std.testing.expectEqual(@as(usize, 0), registry.getUsedSlots());\n    try std.testing.expectEqual(@as(usize, 255), registry.getFreeSlots());\n}\n\ntest \"MemRegistry - register owned memory\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text = try std.testing.allocator.dupe(u8, \"Hello, World!\");\n    const id = try registry.register(text, true);\n\n    try std.testing.expectEqual(@as(u8, 0), id);\n    try std.testing.expectEqual(@as(usize, 1), registry.getUsedSlots());\n    try std.testing.expectEqual(@as(usize, 254), registry.getFreeSlots());\n\n    const retrieved = registry.get(id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(\"Hello, World!\", retrieved.?);\n}\n\ntest \"MemRegistry - register non-owned memory\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text = \"Hello, World!\";\n    const id = try registry.register(text, false);\n\n    try std.testing.expectEqual(@as(u8, 0), id);\n    try std.testing.expectEqual(@as(usize, 1), registry.getUsedSlots());\n\n    const retrieved = registry.get(id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(\"Hello, World!\", retrieved.?);\n}\n\ntest \"MemRegistry - register multiple buffers\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = \"First\";\n    const text2 = \"Second\";\n    const text3 = \"Third\";\n\n    const id1 = try registry.register(text1, false);\n    const id2 = try registry.register(text2, false);\n    const id3 = try registry.register(text3, false);\n\n    try std.testing.expectEqual(@as(u8, 0), id1);\n    try std.testing.expectEqual(@as(u8, 1), id2);\n    try std.testing.expectEqual(@as(u8, 2), id3);\n    try std.testing.expectEqual(@as(usize, 3), registry.getUsedSlots());\n    try std.testing.expectEqual(@as(usize, 252), registry.getFreeSlots());\n\n    try std.testing.expectEqualStrings(\"First\", registry.get(id1).?);\n    try std.testing.expectEqualStrings(\"Second\", registry.get(id2).?);\n    try std.testing.expectEqualStrings(\"Third\", registry.get(id3).?);\n}\n\ntest \"MemRegistry - get invalid ID returns null\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text = \"Test\";\n    _ = try registry.register(text, false);\n\n    try std.testing.expect(registry.get(1) == null);\n    try std.testing.expect(registry.get(5) == null);\n    try std.testing.expect(registry.get(255) == null);\n}\n\ntest \"MemRegistry - replace owned buffer\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = try std.testing.allocator.dupe(u8, \"Original\");\n    const id = try registry.register(text1, true);\n\n    const text2 = try std.testing.allocator.dupe(u8, \"Replaced\");\n    try registry.replace(id, text2, true);\n\n    const retrieved = registry.get(id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(\"Replaced\", retrieved.?);\n    try std.testing.expectEqual(@as(usize, 1), registry.getUsedSlots());\n}\n\ntest \"MemRegistry - replace non-owned buffer with owned\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = \"Original\";\n    const id = try registry.register(text1, false);\n\n    const text2 = try std.testing.allocator.dupe(u8, \"Replaced\");\n    try registry.replace(id, text2, true);\n\n    const retrieved = registry.get(id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(\"Replaced\", retrieved.?);\n}\n\ntest \"MemRegistry - replace with invalid ID\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text = \"Test\";\n    const result = registry.replace(5, text, false);\n    try std.testing.expectError(MemRegistryError.InvalidMemId, result);\n}\n\ntest \"MemRegistry - clear owned buffers\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = try std.testing.allocator.dupe(u8, \"First\");\n    const text2 = try std.testing.allocator.dupe(u8, \"Second\");\n    _ = try registry.register(text1, true);\n    _ = try registry.register(text2, true);\n\n    try std.testing.expectEqual(@as(usize, 2), registry.getUsedSlots());\n\n    registry.clear();\n\n    try std.testing.expectEqual(@as(usize, 0), registry.getUsedSlots());\n    try std.testing.expectEqual(@as(usize, 255), registry.getFreeSlots());\n}\n\ntest \"MemRegistry - clear non-owned buffers\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = \"First\";\n    const text2 = \"Second\";\n    _ = try registry.register(text1, false);\n    _ = try registry.register(text2, false);\n\n    try std.testing.expectEqual(@as(usize, 2), registry.getUsedSlots());\n\n    registry.clear();\n\n    try std.testing.expectEqual(@as(usize, 0), registry.getUsedSlots());\n}\n\ntest \"MemRegistry - max capacity\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    var i: usize = 0;\n    while (i < 255) : (i += 1) {\n        const text = \"test\";\n        _ = try registry.register(text, false);\n    }\n\n    try std.testing.expectEqual(@as(usize, 255), registry.getUsedSlots());\n    try std.testing.expectEqual(@as(usize, 0), registry.getFreeSlots());\n\n    const text = \"overflow\";\n    const result = registry.register(text, false);\n    try std.testing.expectError(MemRegistryError.OutOfMemory, result);\n}\n\ntest \"MemRegistry - clear and reuse\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = \"First\";\n    const id1 = try registry.register(text1, false);\n    try std.testing.expectEqual(@as(u8, 0), id1);\n\n    registry.clear();\n\n    const text2 = \"Second\";\n    const id2 = try registry.register(text2, false);\n    try std.testing.expectEqual(@as(u8, 0), id2);\n    try std.testing.expectEqualStrings(\"Second\", registry.get(id2).?);\n}\n\ntest \"MemRegistry - mixed owned and non-owned buffers\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const owned = try std.testing.allocator.dupe(u8, \"Owned\");\n    const non_owned = \"Not Owned\";\n\n    const id1 = try registry.register(owned, true);\n    const id2 = try registry.register(non_owned, false);\n\n    try std.testing.expectEqual(@as(usize, 2), registry.getUsedSlots());\n\n    try std.testing.expectEqualStrings(\"Owned\", registry.get(id1).?);\n    try std.testing.expectEqualStrings(\"Not Owned\", registry.get(id2).?);\n\n    registry.clear();\n    try std.testing.expectEqual(@as(usize, 0), registry.getUsedSlots());\n}\n\ntest \"MemRegistry - large buffer registration\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const large_text = [_]u8{'A'} ** 10000;\n    const id = try registry.register(&large_text, false);\n\n    const retrieved = registry.get(id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqual(@as(usize, 10000), retrieved.?.len);\n}\n\ntest \"MemRegistry - empty buffer registration\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const empty = \"\";\n    const id = try registry.register(empty, false);\n\n    const retrieved = registry.get(id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqual(@as(usize, 0), retrieved.?.len);\n}\n\ntest \"MemRegistry - sequential replace operations\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = \"First\";\n    const id = try registry.register(text1, false);\n\n    const text2 = \"Second\";\n    try registry.replace(id, text2, false);\n    try std.testing.expectEqualStrings(\"Second\", registry.get(id).?);\n\n    const text3 = \"Third\";\n    try registry.replace(id, text3, false);\n    try std.testing.expectEqualStrings(\"Third\", registry.get(id).?);\n\n    try std.testing.expectEqual(@as(usize, 1), registry.getUsedSlots());\n}\n\ntest \"MemRegistry - replace owned with non-owned\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = try std.testing.allocator.dupe(u8, \"Owned\");\n    const id = try registry.register(text1, true);\n\n    const text2 = \"Not Owned\";\n    try registry.replace(id, text2, false);\n\n    const retrieved = registry.get(id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(\"Not Owned\", retrieved.?);\n    try std.testing.expectEqual(@as(usize, 1), registry.getUsedSlots());\n}\n\ntest \"MemRegistry - stress test with many registrations and clears\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    var round: usize = 0;\n    while (round < 10) : (round += 1) {\n        var i: usize = 0;\n        while (i < 50) : (i += 1) {\n            const text = \"test\";\n            _ = try registry.register(text, false);\n        }\n        try std.testing.expectEqual(@as(usize, 50), registry.getUsedSlots());\n        registry.clear();\n        try std.testing.expectEqual(@as(usize, 0), registry.getUsedSlots());\n    }\n}\n\ntest \"MemRegistry - unregister basic\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text = \"Hello\";\n    const id = try registry.register(text, false);\n\n    try std.testing.expectEqual(@as(usize, 1), registry.getUsedSlots());\n    try std.testing.expectEqualStrings(\"Hello\", registry.get(id).?);\n\n    try registry.unregister(id);\n\n    try std.testing.expectEqual(@as(usize, 0), registry.getUsedSlots());\n    try std.testing.expect(registry.get(id) == null);\n}\n\ntest \"MemRegistry - unregister owned buffer frees memory\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text = try std.testing.allocator.dupe(u8, \"Owned Buffer\");\n    const id = try registry.register(text, true);\n\n    try std.testing.expectEqual(@as(usize, 1), registry.getUsedSlots());\n\n    // Should free the memory when unregistered\n    try registry.unregister(id);\n\n    try std.testing.expectEqual(@as(usize, 0), registry.getUsedSlots());\n    try std.testing.expect(registry.get(id) == null);\n}\n\ntest \"MemRegistry - unregister invalid ID\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const result = registry.unregister(5);\n    try std.testing.expectError(MemRegistryError.InvalidMemId, result);\n}\n\ntest \"MemRegistry - unregister twice fails\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text = \"Test\";\n    const id = try registry.register(text, false);\n\n    try registry.unregister(id);\n\n    // Second unregister should fail\n    const result = registry.unregister(id);\n    try std.testing.expectError(MemRegistryError.InvalidMemId, result);\n}\n\ntest \"MemRegistry - slot reuse after unregister\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = \"First\";\n    const text2 = \"Second\";\n    const text3 = \"Third\";\n\n    const id1 = try registry.register(text1, false);\n    const id2 = try registry.register(text2, false);\n    const id3 = try registry.register(text3, false);\n\n    try std.testing.expectEqual(@as(u8, 0), id1);\n    try std.testing.expectEqual(@as(u8, 1), id2);\n    try std.testing.expectEqual(@as(u8, 2), id3);\n    try std.testing.expectEqual(@as(usize, 3), registry.getUsedSlots());\n\n    // Unregister middle slot\n    try registry.unregister(id2);\n    try std.testing.expectEqual(@as(usize, 2), registry.getUsedSlots());\n\n    // Register new buffer - should reuse slot 1\n    const text4 = \"Fourth\";\n    const id4 = try registry.register(text4, false);\n    try std.testing.expectEqual(@as(u8, 1), id4);\n    try std.testing.expectEqual(@as(usize, 3), registry.getUsedSlots());\n\n    // Verify contents\n    try std.testing.expectEqualStrings(\"First\", registry.get(id1).?);\n    try std.testing.expectEqualStrings(\"Fourth\", registry.get(id4).?);\n    try std.testing.expectEqualStrings(\"Third\", registry.get(id3).?);\n}\n\ntest \"MemRegistry - thousands of register/unregister cycles\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    // Simulate thousands of register/unregister operations\n    // This ensures slot reuse works over long periods\n    var cycle: usize = 0;\n    while (cycle < 1000) : (cycle += 1) {\n        var ids: [10]u8 = undefined;\n\n        // Register 10 buffers\n        var i: usize = 0;\n        while (i < 10) : (i += 1) {\n            const text = \"test\";\n            ids[i] = try registry.register(text, false);\n        }\n\n        try std.testing.expectEqual(@as(usize, 10), registry.getUsedSlots());\n\n        // Unregister all\n        i = 0;\n        while (i < 10) : (i += 1) {\n            try registry.unregister(ids[i]);\n        }\n\n        try std.testing.expectEqual(@as(usize, 0), registry.getUsedSlots());\n    }\n\n    // Verify we can still register after all those cycles\n    const text = \"final\";\n    const id = try registry.register(text, false);\n    try std.testing.expectEqualStrings(\"final\", registry.get(id).?);\n}\n\ntest \"MemRegistry - max capacity 255 with slot reuse\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    // Fill all 255 slots\n    // NOTE: This test ensures the registry respects the u8 ID limit (max 255 slots).\n    // If the ID type is changed from u8 to u16, this test would fail because:\n    // 1. The test fills exactly 255 slots\n    // 2. It expects OutOfMemory error on the 256th registration\n    // 3. With u16, the limit would be 65535, so no error would occur\n    var i: usize = 0;\n    var ids: [255]u8 = undefined;\n    while (i < 255) : (i += 1) {\n        const text = \"test\";\n        ids[i] = try registry.register(text, false);\n    }\n\n    try std.testing.expectEqual(@as(usize, 255), registry.getUsedSlots());\n    try std.testing.expectEqual(@as(usize, 0), registry.getFreeSlots());\n\n    // Should fail to register one more\n    const text = \"overflow\";\n    const result = registry.register(text, false);\n    try std.testing.expectError(MemRegistryError.OutOfMemory, result);\n\n    // Unregister one slot\n    try registry.unregister(ids[100]);\n    try std.testing.expectEqual(@as(usize, 254), registry.getUsedSlots());\n    try std.testing.expectEqual(@as(usize, 1), registry.getFreeSlots());\n\n    // Now we should be able to register again\n    const new_text = \"reused\";\n    const new_id = try registry.register(new_text, false);\n    try std.testing.expectEqual(@as(u8, 100), new_id); // Should reuse slot 100\n    try std.testing.expectEqualStrings(\"reused\", registry.get(new_id).?);\n}\n\ntest \"MemRegistry - replace inactive slot fails\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    const text1 = \"Original\";\n    const id = try registry.register(text1, false);\n\n    try registry.unregister(id);\n\n    // Try to replace inactive slot\n    const text2 = \"Replacement\";\n    const result = registry.replace(id, text2, false);\n    try std.testing.expectError(MemRegistryError.InvalidMemId, result);\n}\n\ntest \"MemRegistry - getFreeSlots accounts for unregistered slots\" {\n    var registry = MemRegistry.init(std.testing.allocator);\n    defer registry.deinit();\n\n    try std.testing.expectEqual(@as(usize, 255), registry.getFreeSlots());\n\n    const id1 = try registry.register(\"test1\", false);\n    const id2 = try registry.register(\"test2\", false);\n    const id3 = try registry.register(\"test3\", false);\n\n    try std.testing.expectEqual(@as(usize, 252), registry.getFreeSlots());\n\n    try registry.unregister(id2);\n    try std.testing.expectEqual(@as(usize, 253), registry.getFreeSlots());\n\n    try registry.unregister(id1);\n    try registry.unregister(id3);\n    try std.testing.expectEqual(@as(usize, 255), registry.getFreeSlots());\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/memory_leak_regression_test.zig",
    "content": "const std = @import(\"std\");\nconst gp = @import(\"../grapheme.zig\");\n\nconst GraphemePool = gp.GraphemePool;\nconst GraphemePoolError = gp.GraphemePoolError;\n\ntest \"GraphemePool - invalid class_id returns InvalidId\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    // class_id 5, 6, 7 are invalid (valid are 0-4)\n    for ([_]u32{ 5, 6, 7 }) |invalid_class_id| {\n        const invalid_id = (invalid_class_id << (gp.GENERATION_BITS + gp.SLOT_BITS));\n\n        try std.testing.expectError(GraphemePoolError.InvalidId, pool.incref(invalid_id));\n        try std.testing.expectError(GraphemePoolError.InvalidId, pool.decref(invalid_id));\n        try std.testing.expectError(GraphemePoolError.InvalidId, pool.get(invalid_id));\n        try std.testing.expectError(GraphemePoolError.InvalidId, pool.getRefcount(invalid_id));\n    }\n}\n\ntest \"GraphemePool - defer cleanup on failure path\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    var allocated_ids: std.ArrayListUnmanaged(u32) = .{};\n    defer allocated_ids.deinit(std.testing.allocator);\n\n    for (0..5) |i| {\n        var buffer: [8]u8 = undefined;\n        const slice = std.fmt.bufPrint(&buffer, \"{d}\", .{i}) catch unreachable;\n        const gid = try pool.alloc(slice);\n        try pool.incref(gid);\n        try allocated_ids.append(std.testing.allocator, gid);\n    }\n\n    // Simulate failure cleanup\n    for (allocated_ids.items) |id| {\n        try pool.decref(id);\n    }\n\n    // Force slot reuse\n    for (0..5) |_| {\n        _ = try pool.alloc(\"reuse\");\n    }\n\n    for (allocated_ids.items) |id| {\n        try std.testing.expectError(GraphemePoolError.WrongGeneration, pool.get(id));\n    }\n}\n\ntest \"GraphemePool - pending grapheme cleanup on failure\" {\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    var result_graphemes: std.ArrayListUnmanaged(u32) = .{};\n    defer result_graphemes.deinit(std.testing.allocator);\n\n    var pending_gid: ?u32 = null;\n    const success = false; // intentionally never true to test cleanup path\n\n    defer {\n        if (!success) {\n            if (pending_gid) |pgid| {\n                pool.decref(pgid) catch {};\n            }\n            for (result_graphemes.items) |gid| {\n                pool.decref(gid) catch {};\n            }\n        }\n    }\n\n    const gid1 = try pool.alloc(\"grapheme1\");\n    pending_gid = gid1;\n    try pool.incref(gid1);\n    try result_graphemes.append(std.testing.allocator, gid1);\n    pending_gid = null;\n\n    const gid2 = try pool.alloc(\"grapheme2\");\n    pending_gid = gid2;\n    try pool.incref(gid2);\n    // Simulate failure before storing - pending_gid remains set\n}\n\ntest \"encodeUnicode - cleanup on mid-operation failure\" {\n    const SimulateResult = struct {\n        success: bool,\n        captured_ids: [2]u32,\n        captured_count: usize,\n    };\n\n    const simulateEncodeUnicode = struct {\n        fn run(pool: *GraphemePool, should_fail: bool) SimulateResult {\n            var result = SimulateResult{\n                .success = false,\n                .captured_ids = undefined,\n                .captured_count = 0,\n            };\n            var pending_gid: ?u32 = null;\n            var stored_ids: [8]u32 = undefined;\n            var stored_count: usize = 0;\n\n            defer {\n                if (!result.success) {\n                    if (pending_gid) |pgid| {\n                        pool.decref(pgid) catch {};\n                    }\n                    for (stored_ids[0..stored_count]) |gid| {\n                        pool.decref(gid) catch {};\n                    }\n                }\n            }\n\n            const gid1 = pool.alloc(\"emoji1\") catch return result;\n            result.captured_ids[result.captured_count] = gid1;\n            result.captured_count += 1;\n            pending_gid = gid1;\n            pool.incref(gid1) catch return result;\n            stored_ids[stored_count] = gid1;\n            stored_count += 1;\n            pending_gid = null;\n\n            const gid2 = pool.alloc(\"emoji2\") catch return result;\n            result.captured_ids[result.captured_count] = gid2;\n            result.captured_count += 1;\n            pending_gid = gid2;\n            pool.incref(gid2) catch return result;\n\n            if (should_fail) {\n                return result;\n            }\n\n            stored_ids[stored_count] = gid2;\n            stored_count += 1;\n            pending_gid = null;\n\n            result.success = true;\n            return result;\n        }\n    }.run;\n\n    var pool = GraphemePool.init(std.testing.allocator);\n    defer pool.deinit();\n\n    const sim_result = simulateEncodeUnicode(&pool, true);\n    try std.testing.expect(!sim_result.success);\n    try std.testing.expectEqual(@as(usize, 2), sim_result.captured_count);\n\n    // Force slot reuse by allocating enough graphemes to cycle through freed slots\n    // Allocate more than captured to ensure freed slots get reused\n    for (0..4) |_| {\n        _ = try pool.alloc(\"reuse\");\n    }\n\n    // Verify cleanup: old IDs should now have wrong generation\n    for (sim_result.captured_ids[0..sim_result.captured_count]) |old_id| {\n        try std.testing.expectError(GraphemePoolError.WrongGeneration, pool.get(old_id));\n    }\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/native-span-feed_test.zig",
    "content": "const std = @import(\"std\");\nconst testing = std.testing;\nconst raw = @import(\"../native-span-feed.zig\");\n\nfn testOptions(chunk_size: u32, initial_chunks: u32, auto_commit: bool) raw.Options {\n    return testOptionsFull(chunk_size, initial_chunks, 0, auto_commit);\n}\n\nfn testOptionsFull(chunk_size: u32, initial_chunks: u32, max_bytes: u64, auto_commit: bool) raw.Options {\n    return .{\n        .chunk_size = chunk_size,\n        .initial_chunks = initial_chunks,\n        .max_bytes = max_bytes,\n        .growth_policy = @intFromEnum(raw.GrowthPolicy.grow),\n        .auto_commit_on_full = if (auto_commit) 1 else 0,\n        .span_queue_capacity = 0,\n    };\n}\n\nfn drainAllSpans(stream: *raw.Stream) u64 {\n    var buf: [256]raw.SpanInfo = undefined;\n    var total: u64 = 0;\n    while (true) {\n        const count = stream.drainSpans(&buf);\n        if (count == 0) break;\n        var i: u32 = 0;\n        while (i < count) : (i += 1) {\n            total += buf[i].len;\n            stream.markSpanConsumed(buf[i]);\n        }\n    }\n    return total;\n}\n\ntest \"Stream - create and destroy with testing allocator\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(1024, 2, true));\n    defer stream.destroy();\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u32, 2), stats.chunks);\n    try testing.expectEqual(@as(u64, 0), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 0), stats.spans_committed);\n}\n\ntest \"Stream - create with default options\" {\n    const stream = try raw.Stream.create(testing.allocator, null);\n    defer stream.destroy();\n\n    const stats = stream.getStats();\n    try testing.expect(stats.chunks >= 1);\n}\n\ntest \"Stream - write and commit produces span with correct byte count\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(1024, 2, false));\n    defer stream.destroy();\n\n    const data = \"hello world\";\n    try stream.write(data);\n    try stream.commit();\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, data.len), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 1), stats.spans_committed);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, data.len), drained);\n}\n\ntest \"Stream - write with auto_commit fills chunk and commits automatically\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, true));\n    defer stream.destroy();\n\n    const data = [_]u8{'A'} ** 64;\n    try stream.write(&data);\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 64), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 1), stats.spans_committed);\n}\n\ntest \"Stream - write spanning multiple chunks with auto_commit\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, true));\n    defer stream.destroy();\n\n    const data = [_]u8{'B'} ** 150;\n    try stream.write(&data);\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 150), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 2), stats.spans_committed);\n\n    try stream.commit();\n    const stats2 = stream.getStats();\n    try testing.expectEqual(@as(u64, 3), stats2.spans_committed);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 150), drained);\n}\n\ntest \"Stream - write returns NoSpace when auto_commit disabled and data exceeds chunk\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, false));\n    defer stream.destroy();\n\n    const data = [_]u8{'C'} ** 65;\n    const result = stream.write(&data);\n    try testing.expectError(raw.StreamError.NoSpace, result);\n}\n\ntest \"Stream - write exactly fills chunk without auto_commit succeeds\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, false));\n    defer stream.destroy();\n\n    const exact = [_]u8{'A'} ** 64;\n    try stream.write(&exact);\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 64), stats.bytes_written);\n\n    try stream.commit();\n    _ = drainAllSpans(stream);\n\n    try stream.write(\"B\");\n    try stream.commit();\n\n    const stats2 = stream.getStats();\n    try testing.expectEqual(@as(u64, 65), stats2.bytes_written);\n    try testing.expectEqual(@as(u64, 2), stats2.spans_committed);\n}\n\ntest \"Stream - written data matches drained span content\" {\n    const chunk_size: u32 = 256;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, false));\n    defer stream.destroy();\n\n    const data = \"the quick brown fox jumps over the lazy dog\";\n    try stream.write(data);\n    try stream.commit();\n\n    var buf: [16]raw.SpanInfo = undefined;\n    const count = stream.drainSpans(&buf);\n    try testing.expectEqual(@as(u32, 1), count);\n\n    const span = buf[0];\n    const slice = span.slice();\n    try testing.expectEqualStrings(data, slice);\n    stream.markSpanConsumed(buf[0]);\n}\n\ntest \"Stream - reserve and commitReserved round-trip\" {\n    const chunk_size: u32 = 256;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, false));\n    defer stream.destroy();\n\n    const info = try stream.reserve(10);\n    try testing.expect(info.len >= 10);\n\n    const dest = info.slice();\n    @memcpy(dest[0..5], \"hello\");\n\n    try stream.commitReserved(5);\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 5), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 1), stats.spans_committed);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 5), drained);\n}\n\ntest \"Stream - reserve returns Busy if already reserved\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 2, false));\n    defer stream.destroy();\n\n    _ = try stream.reserve(1);\n    const result = stream.reserve(1);\n    try testing.expectError(raw.StreamError.Busy, result);\n\n    try stream.commitReserved(0);\n}\n\ntest \"Stream - reserve returns Busy if pending data exists\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 2, false));\n    defer stream.destroy();\n\n    try stream.write(\"some data\");\n    const result = stream.reserve(1);\n    try testing.expectError(raw.StreamError.Busy, result);\n}\n\ntest \"Stream - write returns Busy while reservation is active\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 2, false));\n    defer stream.destroy();\n\n    _ = try stream.reserve(1);\n    const result = stream.write(\"data\");\n    try testing.expectError(raw.StreamError.Busy, result);\n\n    try stream.commitReserved(0);\n}\n\ntest \"Stream - write to closed stream returns Invalid\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 2, false));\n    defer stream.destroy();\n\n    try stream.close();\n    const result = stream.write(\"data\");\n    try testing.expectError(raw.StreamError.Invalid, result);\n}\n\ntest \"Stream - double close does not error\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 2, false));\n    defer stream.destroy();\n\n    try stream.close();\n    try stream.close();\n}\n\ntest \"Stream - consecutive writes without auto_commit preserves all data\" {\n    // Regression: auto_commit off must not drop pending data across writes.\n\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, false));\n    defer stream.destroy();\n\n    const first = [_]u8{'A'} ** 64;\n    try stream.write(&first);\n\n    var stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 64), stats.bytes_written);\n\n    const second = \"BBBB\";\n    try stream.write(second);\n\n    stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 68), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 1), stats.spans_committed);\n    try stream.commit();\n    stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 2), stats.spans_committed);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 68), drained);\n}\n\ntest \"Stream - write that exactly fills chunk then write more (no auto_commit)\" {\n    const chunk_size: u32 = 32;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, false));\n    defer stream.destroy();\n\n    const fill = [_]u8{'X'} ** 32;\n    try stream.write(&fill);\n\n    try stream.write(\"Y\");\n    try stream.commit();\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 33), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 2), stats.spans_committed);\n    var buf: [16]raw.SpanInfo = undefined;\n    const count = stream.drainSpans(&buf);\n    try testing.expectEqual(@as(u32, 2), count);\n\n    const span1 = buf[0].slice();\n    try testing.expectEqual(@as(usize, 32), span1.len);\n    try testing.expectEqual(@as(u8, 'X'), span1[0]);\n    try testing.expectEqual(@as(u8, 'X'), span1[31]);\n\n    const span2 = buf[1].slice();\n    try testing.expectEqualStrings(\"Y\", span2);\n\n    stream.markSpanConsumed(buf[0]);\n    stream.markSpanConsumed(buf[1]);\n}\n\ntest \"Stream - multiple chunk transitions without auto_commit\" {\n    const chunk_size: u32 = 16;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    try stream.write(\"AAAAAAAAAAAAAAAA\");\n    try stream.write(\"BBBBBBBBBBBBBBBB\");\n    try stream.write(\"CCCCCCCC\");\n\n    var stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 40), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 2), stats.spans_committed);\n    try stream.commit();\n    stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 3), stats.spans_committed);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 40), drained);\n}\n\ntest \"Stream - commit after small write should allow reuse of remaining chunk space\" {\n    // Regression: commit must not burn remaining chunk space.\n\n    const chunk_size: u32 = 256;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    try stream.write(\"0123456789\");\n    try stream.commit();\n\n    var stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 10), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 1), stats.spans_committed);\n    try testing.expectEqual(@as(u32, 1), stats.chunks);\n\n    var buf: [16]raw.SpanInfo = undefined;\n    const count = stream.drainSpans(&buf);\n    try testing.expectEqual(@as(u32, 1), count);\n    stream.markSpanConsumed(buf[0]);\n\n    try stream.write(\"abcdefghij\");\n    try stream.commit();\n\n    stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 20), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 2), stats.spans_committed);\n    try testing.expectEqual(@as(u32, 1), stats.chunks);\n\n    const count2 = stream.drainSpans(&buf);\n    try testing.expectEqual(@as(u32, 1), count2);\n    const span = buf[0];\n    try testing.expectEqual(@as(u32, 10), span.offset);\n    try testing.expectEqual(@as(u32, 10), span.len);\n    stream.markSpanConsumed(buf[0]);\n}\n\ntest \"Stream - repeated small write+commit should not force chunk growth\" {\n    // Regression: small write+commit should not force chunk growth.\n\n    const chunk_size: u32 = 1024;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 4) : (i += 1) {\n        try stream.write(\"12345678\");\n        try stream.commit();\n    }\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 32), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 4), stats.spans_committed);\n\n    try testing.expectEqual(@as(u32, 1), stats.chunks);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 32), drained);\n}\n\ntest \"Stream - max_bytes returns MaxBytes when limit is reached\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptionsFull(32, 2, 64, false));\n    defer stream.destroy();\n\n    try testing.expectEqual(@as(u32, 2), stream.getStats().chunks);\n\n    const fill1 = [_]u8{'A'} ** 32;\n    try stream.write(&fill1);\n    try stream.commit();\n\n    const fill2 = [_]u8{'B'} ** 32;\n    try stream.write(&fill2);\n    try stream.commit();\n\n    const result = stream.write(\"C\");\n    try testing.expectError(raw.StreamError.MaxBytes, result);\n}\n\ntest \"Stream - max_bytes allows reuse after draining\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptionsFull(32, 2, 64, false));\n    defer stream.destroy();\n\n    const fill1 = [_]u8{'A'} ** 32;\n    try stream.write(&fill1);\n    try stream.commit();\n    const fill2 = [_]u8{'B'} ** 32;\n    try stream.write(&fill2);\n    try stream.commit();\n\n    _ = drainAllSpans(stream);\n    const fill3 = [_]u8{'C'} ** 32;\n    try stream.write(&fill3);\n    try stream.commit();\n\n    try testing.expectEqual(@as(u64, 96), stream.getStats().bytes_written);\n    try testing.expectEqual(@as(u32, 2), stream.getStats().chunks);\n}\n\ntest \"Stream - auto_commit with max_bytes works when consumer keeps up\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptionsFull(32, 2, 64, true));\n    defer stream.destroy();\n\n    const fill1 = [_]u8{'A'} ** 32;\n    try stream.write(&fill1);\n\n    _ = drainAllSpans(stream);\n\n    const fill2 = [_]u8{'B'} ** 32;\n    try stream.write(&fill2);\n\n    _ = drainAllSpans(stream);\n\n    const fill3 = [_]u8{'C'} ** 32;\n    try stream.write(&fill3);\n\n    try testing.expectEqual(@as(u64, 96), stream.getStats().bytes_written);\n    try testing.expectEqual(@as(u32, 2), stream.getStats().chunks);\n\n    _ = drainAllSpans(stream);\n}\n\ntest \"Stream - auto_commit with max_bytes should handle write spanning chunk boundary\" {\n    // Regression: auto_commit must not fail when continuing across a boundary.\n\n    const stream = try raw.Stream.create(testing.allocator, testOptionsFull(32, 2, 64, true));\n    defer stream.destroy();\n\n    const data = [_]u8{'X'} ** 64;\n    try stream.write(&data);\n\n    try testing.expectEqual(@as(u64, 64), stream.getStats().bytes_written);\n    try testing.expect(stream.getStats().spans_committed >= 1);\n\n    _ = drainAllSpans(stream);\n}\n\ntest \"Stream - memory growth under pressure allocates new chunks\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(64, 1, true));\n    defer stream.destroy();\n\n    try testing.expectEqual(@as(u32, 1), stream.getStats().chunks);\n\n    var i: usize = 0;\n    while (i < 10) : (i += 1) {\n        const data = [_]u8{@intCast(i)} ** 64;\n        try stream.write(&data);\n    }\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 640), stats.bytes_written);\n    try testing.expect(stats.chunks >= 10);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 640), drained);\n}\n\nfn blockOptions(chunk_size: u32, initial_chunks: u32, auto_commit: bool) raw.Options {\n    return .{\n        .chunk_size = chunk_size,\n        .initial_chunks = initial_chunks,\n        .max_bytes = 0,\n        .growth_policy = @intFromEnum(raw.GrowthPolicy.block),\n        .auto_commit_on_full = if (auto_commit) 1 else 0,\n        .span_queue_capacity = 0,\n    };\n}\n\ntest \"Stream - growth_policy=block prevents new chunk allocation\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, blockOptions(chunk_size, 2, false));\n    defer stream.destroy();\n\n    try testing.expectEqual(@as(u32, 2), stream.getStats().chunks);\n\n    const fill1 = [_]u8{'A'} ** 64;\n    try stream.write(&fill1);\n    try stream.commit();\n\n    const fill2 = [_]u8{'B'} ** 64;\n    try stream.write(&fill2);\n    try stream.commit();\n\n    const result = stream.write(\"C\");\n    try testing.expectError(raw.StreamError.NoSpace, result);\n\n    try testing.expectEqual(@as(u32, 2), stream.getStats().chunks);\n}\n\ntest \"Stream - growth_policy=block allows reuse after draining\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, blockOptions(chunk_size, 2, false));\n    defer stream.destroy();\n\n    try stream.write(&([_]u8{'A'} ** 64));\n    try stream.commit();\n    try stream.write(&([_]u8{'B'} ** 64));\n    try stream.commit();\n\n    _ = drainAllSpans(stream);\n    try stream.write(&([_]u8{'C'} ** 64));\n    try stream.commit();\n\n    try testing.expectEqual(@as(u64, 192), stream.getStats().bytes_written);\n    try testing.expectEqual(@as(u32, 2), stream.getStats().chunks);\n}\n\ntest \"Stream - growth_policy=block with auto_commit returns NoSpace when pool exhausted\" {\n    const chunk_size: u32 = 32;\n    const stream = try raw.Stream.create(testing.allocator, blockOptions(chunk_size, 2, true));\n    defer stream.destroy();\n\n    try stream.write(&([_]u8{'X'} ** 64));\n\n    const result = stream.write(\"Y\");\n    try testing.expectError(raw.StreamError.NoSpace, result);\n\n    try testing.expectEqual(@as(u32, 2), stream.getStats().chunks);\n}\n\ntest \"Stream - span ring overflow returns NoSpace\" {\n    const chunk_size: u32 = 4096;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 4096) : (i += 1) {\n        try stream.write(\"x\");\n        try stream.commit();\n    }\n\n    try testing.expectEqual(@as(u32, 4096), stream.getStats().pending_spans);\n\n    try stream.write(\"y\");\n    const result = stream.commit();\n    try testing.expectError(raw.StreamError.NoSpace, result);\n}\n\ntest \"Stream - span ring recovers after draining\" {\n    const chunk_size: u32 = 4096;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 4096) : (i += 1) {\n        try stream.write(\"x\");\n        try stream.commit();\n    }\n\n    _ = drainAllSpans(stream);\n    try testing.expectEqual(@as(u32, 0), stream.getStats().pending_spans);\n\n    try stream.write(\"z\");\n    try stream.commit();\n    try testing.expectEqual(@as(u32, 1), stream.getStats().pending_spans);\n\n    _ = drainAllSpans(stream);\n}\n\ntest \"Stream - custom span_queue_capacity is respected\" {\n    var opts = testOptions(4096, 1, false);\n    opts.span_queue_capacity = 8;\n    const stream = try raw.Stream.create(testing.allocator, opts);\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 8) : (i += 1) {\n        try stream.write(\"x\");\n        try stream.commit();\n    }\n    try testing.expectEqual(@as(u32, 8), stream.getStats().pending_spans);\n\n    try stream.write(\"y\");\n    const result = stream.commit();\n    try testing.expectError(raw.StreamError.NoSpace, result);\n\n    _ = drainAllSpans(stream);\n    try testing.expectEqual(@as(u32, 0), stream.getStats().pending_spans);\n\n    try stream.write(\"z\");\n    try stream.commit();\n    try testing.expectEqual(@as(u32, 1), stream.getStats().pending_spans);\n}\n\ntest \"Stream - large span_queue_capacity works\" {\n    var opts = testOptions(4096, 1, false);\n    opts.span_queue_capacity = 8192;\n    const stream = try raw.Stream.create(testing.allocator, opts);\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 5000) : (i += 1) {\n        try stream.write(\"x\");\n        try stream.commit();\n    }\n    try testing.expectEqual(@as(u32, 5000), stream.getStats().pending_spans);\n\n    _ = drainAllSpans(stream);\n    try testing.expectEqual(@as(u32, 0), stream.getStats().pending_spans);\n}\n\ntest \"Stream - span_queue_capacity zero defaults to 4096\" {\n    var opts = testOptions(4096, 1, false);\n    opts.span_queue_capacity = 0;\n    const stream = try raw.Stream.create(testing.allocator, opts);\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 4096) : (i += 1) {\n        try stream.write(\"x\");\n        try stream.commit();\n    }\n    try testing.expectEqual(@as(u32, 4096), stream.getStats().pending_spans);\n\n    try stream.write(\"y\");\n    const result = stream.commit();\n    try testing.expectError(raw.StreamError.NoSpace, result);\n}\n\ntest \"Stream - data integrity across many chunks with auto_commit\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, true));\n    defer stream.destroy();\n\n    var source: [1024]u8 = undefined;\n    for (&source, 0..) |*b, idx| {\n        b.* = @intCast(idx % 256);\n    }\n\n    try stream.write(&source);\n    try stream.commit();\n    var received: [1024]u8 = undefined;\n    var offset: usize = 0;\n\n    var buf: [256]raw.SpanInfo = undefined;\n    while (true) {\n        const count = stream.drainSpans(&buf);\n        if (count == 0) break;\n        var i: u32 = 0;\n        while (i < count) : (i += 1) {\n            const span = buf[i];\n            const slice = span.slice();\n            @memcpy(received[offset .. offset + slice.len], slice);\n            offset += slice.len;\n            stream.markSpanConsumed(buf[i]);\n        }\n    }\n\n    try testing.expectEqual(@as(usize, 1024), offset);\n    try testing.expectEqualSlices(u8, &source, &received);\n}\n\ntest \"Stream - data integrity with reserve across multiple chunks\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    var written: [256]u8 = undefined;\n    var w_offset: usize = 0;\n\n    while (w_offset < 256) {\n        const info = try stream.reserve(1);\n        const dest = info.slice();\n        const to_write = @min(dest.len, 256 - w_offset);\n        var j: usize = 0;\n        while (j < to_write) : (j += 1) {\n            const val: u8 = @intCast((w_offset + j) % 256);\n            dest[j] = val;\n            written[w_offset + j] = val;\n        }\n        try stream.commitReserved(@intCast(to_write));\n        w_offset += to_write;\n    }\n\n    var received: [256]u8 = undefined;\n    var r_offset: usize = 0;\n\n    var buf: [64]raw.SpanInfo = undefined;\n    while (true) {\n        const count = stream.drainSpans(&buf);\n        if (count == 0) break;\n        var i: u32 = 0;\n        while (i < count) : (i += 1) {\n            const slice = buf[i].slice();\n            @memcpy(received[r_offset .. r_offset + slice.len], slice);\n            r_offset += slice.len;\n            stream.markSpanConsumed(buf[i]);\n        }\n    }\n\n    try testing.expectEqual(@as(usize, 256), r_offset);\n    try testing.expectEqualSlices(u8, &written, &received);\n}\n\ntest \"Stream - reserve on closed stream returns Invalid\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    try stream.close();\n    const result = stream.reserve(1);\n    try testing.expectError(raw.StreamError.Invalid, result);\n}\n\ntest \"Stream - commit on closed stream returns Invalid\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    try stream.close();\n    const result = stream.commit();\n    try testing.expectError(raw.StreamError.Invalid, result);\n}\n\ntest \"Stream - commitReserved on closed stream returns Invalid\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    try stream.close();\n    const result = stream.commitReserved(0);\n    try testing.expectError(raw.StreamError.Invalid, result);\n}\n\ntest \"Stream - commitReserved with len exceeding reserved returns NoSpace\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    const info = try stream.reserve(1);\n    const result = stream.commitReserved(info.len + 1);\n    try testing.expectError(raw.StreamError.NoSpace, result);\n}\n\ntest \"Stream - commitReserved without active reservation returns Invalid\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    const result = stream.commitReserved(0);\n    try testing.expectError(raw.StreamError.Invalid, result);\n}\n\ntest \"Stream - reserve with min_len larger than chunk returns NoSpace\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(64, 1, false));\n    defer stream.destroy();\n\n    const result = stream.reserve(65);\n    try testing.expectError(raw.StreamError.NoSpace, result);\n}\n\ntest \"Stream - empty write is a no-op\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    try stream.write(\"\");\n    try testing.expectEqual(@as(u64, 0), stream.getStats().bytes_written);\n}\n\ntest \"Stream - commit with no pending data is a no-op\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    try stream.commit();\n    try testing.expectEqual(@as(u64, 0), stream.getStats().spans_committed);\n}\n\ntest \"Stream - drain with no spans returns zero\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    var buf: [16]raw.SpanInfo = undefined;\n    const count = stream.drainSpans(&buf);\n    try testing.expectEqual(@as(u32, 0), count);\n}\n\ntest \"Stream - close with pending data auto-commits\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    try stream.write(\"pending data\");\n    try stream.close();\n\n    try testing.expectEqual(@as(u64, 1), stream.getStats().spans_committed);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 12), drained);\n}\n\ntest \"Stream - setOptions on closed stream returns Invalid\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    try stream.close();\n    const result = stream.setOptions(testOptions(128, 1, true));\n    try testing.expectError(raw.StreamError.Invalid, result);\n}\n\ntest \"Stream - setOptions ignores chunk_size (immutable after creation)\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(64, 1, true));\n    defer stream.destroy();\n\n    const fill1 = [_]u8{'A'} ** 64;\n    try stream.write(&fill1);\n\n    try stream.setOptions(testOptions(128, 1, true));\n\n    const fill2 = [_]u8{'B'} ** 64;\n    try stream.write(&fill2);\n\n    try stream.commit();\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 128), stats.bytes_written);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 128), drained);\n}\n\ntest \"Stream - setOptions enables auto_commit mid-stream\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(64, 2, false));\n    defer stream.destroy();\n\n    try stream.write(&([_]u8{'A'} ** 32));\n    try testing.expectEqual(@as(u64, 0), stream.getStats().spans_committed);\n    try stream.commit();\n    try testing.expectEqual(@as(u64, 1), stream.getStats().spans_committed);\n\n    _ = drainAllSpans(stream);\n    try stream.setOptions(testOptions(64, 2, true));\n    try stream.write(&([_]u8{'B'} ** 64));\n    try testing.expectEqual(@as(u64, 2), stream.getStats().spans_committed);\n\n    _ = drainAllSpans(stream);\n}\n\ntest \"Stream - pending data survives failed commit (ring full)\" {\n    const chunk_size: u32 = 4096;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 4096) : (i += 1) {\n        try stream.write(\"x\");\n        try stream.commit();\n    }\n\n    try stream.write(\"important\");\n    const result = stream.commit();\n    try testing.expectError(raw.StreamError.NoSpace, result);\n\n    _ = drainAllSpans(stream);\n    try stream.commit();\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 4096 + 9), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 4097), stats.spans_committed);\n\n    var buf: [16]raw.SpanInfo = undefined;\n    const count = stream.drainSpans(&buf);\n    try testing.expectEqual(@as(u32, 1), count);\n    const slice = buf[0].slice();\n    try testing.expectEqualStrings(\"important\", slice);\n    stream.markSpanConsumed(buf[0]);\n}\n\ntest \"Stream - close with active reservation returns Busy\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    _ = try stream.reserve(1);\n\n    const result = stream.close();\n    try testing.expectError(raw.StreamError.Busy, result);\n\n    try testing.expectEqual(false, stream.closed);\n    try stream.commitReserved(0);\n    try stream.close();\n}\n\ntest \"Stream - destroy without close commits pending data\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n\n    try stream.write(\"before destroy\");\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 14), stats.bytes_written);\n    try testing.expectEqual(@as(u64, 0), stats.spans_committed);\n\n    stream.destroy();\n}\n\ntest \"Stream - write error mid-loop preserves already-committed spans\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptionsFull(32, 2, 64, true));\n    defer stream.destroy();\n\n    const data = [_]u8{'Z'} ** 96;\n    const result = stream.write(&data);\n    try testing.expectError(raw.StreamError.MaxBytes, result);\n\n    const stats = stream.getStats();\n    try testing.expectEqual(@as(u64, 2), stats.spans_committed);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 64), drained);\n}\n\ntest \"Stream - bytes_written matches total drained across all operations\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(64, 1, true));\n    defer stream.destroy();\n\n    try stream.write(\"short\");\n    try stream.commit();\n    try stream.write(&([_]u8{'M'} ** 64));\n    try stream.write(&([_]u8{'L'} ** 200));\n\n    try stream.commit();\n\n    const stats = stream.getStats();\n    const drained = drainAllSpans(stream);\n\n    try testing.expectEqual(stats.bytes_written, drained);\n}\n\nvar data_available_count: u32 = 0;\n\nfn countingCallback(_: usize, event_id: u32, _: usize, _: u64) callconv(.c) void {\n    if (event_id == @intFromEnum(raw.EventId.DataAvailable)) {\n        data_available_count += 1;\n    }\n}\n\ntest \"Stream - write returning NoSpace emits DataAvailable exactly once\" {\n    // Regression: NoSpace path must not double-emit DataAvailable.\n    data_available_count = 0;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(64, 2, false));\n    defer stream.destroy();\n\n    stream.setCallback(&countingCallback);\n    try stream.attach();\n    data_available_count = 0;\n    const first = [_]u8{'A'} ** 64;\n    try stream.write(&first);\n    const result = stream.write(&([_]u8{'B'} ** 65));\n    try testing.expectError(raw.StreamError.NoSpace, result);\n    try testing.expectEqual(@as(u32, 1), data_available_count);\n}\n\ntest \"Stream - hasPendingSpans reflects state correctly\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    try testing.expect(!stream.hasPendingSpans());\n\n    try stream.write(\"data\");\n    try stream.commit();\n    try testing.expect(stream.hasPendingSpans());\n\n    _ = drainAllSpans(stream);\n    try testing.expect(!stream.hasPendingSpans());\n}\n\nvar drain_during_write_stream: ?*raw.Stream = null;\nvar drain_during_write_total: u64 = 0;\n\nfn drainingCallback(stream_ptr: usize, event_id: u32, _: usize, _: u64) callconv(.c) void {\n    if (event_id != @intFromEnum(raw.EventId.DataAvailable)) return;\n    const s = drain_during_write_stream orelse return;\n    if (@intFromPtr(s) != stream_ptr) return;\n\n    var buf: [64]raw.SpanInfo = undefined;\n    while (true) {\n        const count = s.drainSpans(&buf);\n        if (count == 0) break;\n        var i: u32 = 0;\n        while (i < count) : (i += 1) {\n            drain_during_write_total += buf[i].len;\n            s.markSpanConsumed(buf[i]);\n        }\n    }\n}\n\ntest \"Stream - synchronous drain during write does not corrupt state\" {\n    drain_during_write_stream = null;\n    drain_during_write_total = 0;\n\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, true));\n    defer stream.destroy();\n\n    stream.setCallback(&drainingCallback);\n    try stream.attach();\n    drain_during_write_stream = stream;\n    drain_during_write_total = 0;\n\n    const data = [_]u8{'D'} ** 256;\n    try stream.write(&data);\n\n    try stream.commit();\n    var buf: [64]raw.SpanInfo = undefined;\n    while (true) {\n        const count = stream.drainSpans(&buf);\n        if (count == 0) break;\n        var i: u32 = 0;\n        while (i < count) : (i += 1) {\n            drain_during_write_total += buf[i].len;\n            stream.markSpanConsumed(buf[i]);\n        }\n    }\n\n    try testing.expectEqual(@as(u64, 256), drain_during_write_total);\n    try testing.expectEqual(@as(u64, 256), stream.getStats().bytes_written);\n\n    drain_during_write_stream = null;\n}\n\ntest \"Stream - span ring wrapping near u32 max\" {\n    // Position near u32 max to exercise wrapping without huge loops.\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 2, false));\n    defer stream.destroy();\n\n    const near_max: u32 = std.math.maxInt(u32) - 5;\n    stream.span_ring.head = near_max;\n    stream.span_ring.tail = near_max;\n\n    var i: u32 = 0;\n    while (i < 10) : (i += 1) {\n        try stream.write(\"data\");\n        try stream.commit();\n    }\n\n    try testing.expectEqual(@as(u32, 10), stream.span_ring.count());\n\n    var buf: [16]raw.SpanInfo = undefined;\n    const count = stream.drainSpans(&buf);\n    try testing.expectEqual(@as(u32, 10), count);\n\n    try testing.expectEqual(@as(u32, 0), stream.span_ring.count());\n    try testing.expectEqual(near_max +% 10, stream.span_ring.head);\n    try testing.expectEqual(near_max +% 10, stream.span_ring.tail);\n\n    try testing.expectEqualStrings(\"data\", buf[9].slice());\n    for (buf[0..count]) |span| {\n        stream.markSpanConsumed(span);\n    }\n}\n\ntest \"Stream - commitReserved with zero length produces no span\" {\n    const stream = try raw.Stream.create(testing.allocator, testOptions(256, 1, false));\n    defer stream.destroy();\n\n    _ = try stream.reserve(1);\n    try stream.commitReserved(0);\n\n    try testing.expectEqual(@as(u64, 0), stream.getStats().spans_committed);\n    try testing.expectEqual(@as(u64, 0), stream.getStats().bytes_written);\n    try testing.expect(!stream.hasPendingSpans());\n\n    try stream.write(\"after\");\n    try stream.commit();\n\n    try testing.expectEqual(@as(u64, 1), stream.getStats().spans_committed);\n    try testing.expectEqual(@as(u64, 5), stream.getStats().bytes_written);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 5), drained);\n}\n\ntest \"Stream - write exactly chunk_size * N with auto_commit commits all, no dangling pending\" {\n    const chunk_size: u32 = 64;\n    const n: usize = 5;\n    const total = chunk_size * n;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 2, true));\n    defer stream.destroy();\n\n    const data = [_]u8{'E'} ** total;\n    try stream.write(&data);\n\n    const stats = stream.getStats();\n\n    try testing.expectEqual(@as(u64, n), stats.spans_committed);\n    try testing.expectEqual(@as(u64, total), stats.bytes_written);\n\n    try stream.commit();\n    try testing.expectEqual(@as(u64, n), stream.getStats().spans_committed);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, total), drained);\n}\n\ntest \"Stream - state buffer reallocation preserves active span refcounts\" {\n    const chunk_size: u32 = 64;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    const first = [_]u8{'F'} ** 64;\n    try stream.write(&first);\n    try stream.commit();\n\n    try testing.expectEqual(@as(u8, 1), stream.stateBuffer()[0]);\n\n    var i: usize = 0;\n    while (i < 3) : (i += 1) {\n        const filler = [_]u8{@intCast(i + 0x10)} ** 64;\n        try stream.write(&filler);\n        try stream.commit();\n    }\n\n    try testing.expect(stream.getStats().chunks >= 4);\n    try testing.expectEqual(@as(u8, 1), stream.stateBuffer()[0]);\n\n    const drained = drainAllSpans(stream);\n    try testing.expectEqual(@as(u64, 256), drained);\n\n    try testing.expectEqual(@as(u8, 0), stream.stateBuffer()[0]);\n}\n\ntest \"Stream - state_buffer caps at 255 and advances to new chunk\" {\n    // Refcount saturation should force a new chunk.\n    const chunk_size: u32 = 4096;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 260) : (i += 1) {\n        try stream.write(\"x\");\n        try stream.commit();\n    }\n\n    try testing.expectEqual(@as(u8, 255), stream.stateBuffer()[0]);\n    try testing.expect(stream.getStats().chunks >= 2);\n    var buf: [64]raw.SpanInfo = undefined;\n    var drain_count: u32 = 0;\n    while (true) {\n        const count = stream.drainSpans(&buf);\n        if (count == 0) break;\n        var j: u32 = 0;\n        while (j < count) : (j += 1) {\n            stream.markSpanConsumed(buf[j]);\n            drain_count += 1;\n\n            if (drain_count == 254) {\n                try testing.expectEqual(@as(u8, 1), stream.stateBuffer()[0]);\n            }\n        }\n    }\n\n    try testing.expectEqual(@as(u32, 260), drain_count);\n    try testing.expectEqual(@as(u8, 0), stream.stateBuffer()[0]);\n}\n\ntest \"Stream - refcount saturation must not cause data corruption\" {\n    // Regression: refcount saturation must not allow reuse that corrupts data.\n\n    const chunk_size: u32 = 256;\n    const stream = try raw.Stream.create(testing.allocator, testOptions(chunk_size, 1, false));\n    defer stream.destroy();\n\n    var i: u32 = 0;\n    while (i < 256) : (i += 1) {\n        const byte = [1]u8{@intCast(i % 256)};\n        try stream.write(&byte);\n        try stream.commit();\n    }\n\n    try testing.expectEqual(@as(u8, 255), stream.stateBuffer()[0]);\n\n    var buf: [64]raw.SpanInfo = undefined;\n    var drained: u32 = 0;\n    var data_index: u32 = 0;\n    while (drained < 255) {\n        const want: u32 = @intCast(@min(buf.len, 255 - drained));\n        const count = stream.drainSpans(buf[0..want]);\n        if (count == 0) break;\n        var j: u32 = 0;\n        while (j < count) : (j += 1) {\n            const slice = buf[j].slice();\n            try testing.expectEqual(@as(usize, 1), slice.len);\n            try testing.expectEqual(@as(u8, @intCast(data_index % 256)), slice[0]);\n            stream.markSpanConsumed(buf[j]);\n            data_index += 1;\n            drained += 1;\n        }\n    }\n    try testing.expectEqual(@as(u32, 255), drained);\n\n    try testing.expectEqual(@as(u8, 0), stream.stateBuffer()[0]);\n    const overwrite = [_]u8{'Z'} ** 128;\n    try stream.write(&overwrite);\n    try stream.commit();\n\n    const count = stream.drainSpans(&buf);\n    try testing.expect(count >= 1);\n\n    var found = false;\n    var j: u32 = 0;\n    while (j < count) : (j += 1) {\n        const slice = buf[j].slice();\n        if (slice.len == 1) {\n            try testing.expectEqual(@as(u8, 255), slice[0]);\n            found = true;\n            stream.markSpanConsumed(buf[j]);\n        } else {\n            stream.markSpanConsumed(buf[j]);\n        }\n    }\n    try testing.expect(found);\n}\n\n// Regression: addChunkLocked error paths must not leak or desync state.\n\ntest \"addChunkLocked must not leak chunk data when ArrayList append fails\" {\n    // Sweep failing allocations to ensure no leaks.\n    const chunk_size: u32 = 64;\n    const initial_chunks: u32 = 9;\n\n    var counter = std.testing.FailingAllocator.init(std.heap.page_allocator, .{});\n    {\n        const s = raw.Stream.create(counter.allocator(), testOptions(chunk_size, initial_chunks, false)) catch\n            return error.TestUnexpectedResult;\n        s.destroy();\n    }\n    const create_allocs = counter.allocations;\n\n    const configs = [_]struct { resize_fail: usize }{\n        .{ .resize_fail = std.math.maxInt(usize) },\n        .{ .resize_fail = 0 },\n    };\n\n    for (configs) |cfg| {\n        var fi: usize = 0;\n        while (fi <= create_allocs + 2) : (fi += 1) {\n            var fa = std.testing.FailingAllocator.init(testing.allocator, .{\n                .fail_index = fi,\n                .resize_fail_index = cfg.resize_fail,\n            });\n\n            const result = raw.Stream.create(fa.allocator(), testOptions(chunk_size, initial_chunks, false));\n            if (result) |stream| {\n                stream.destroy();\n            } else |_| {}\n        }\n    }\n}\n\ntest \"addChunkLocked must not leak chunk data during initial create\" {\n    // Regression: failing append must not leak chunk data.\n\n    var failing = std.testing.FailingAllocator.init(testing.allocator, .{\n        .fail_index = 4,\n    });\n\n    const result = raw.Stream.create(failing.allocator(), testOptions(64, 1, false));\n    try testing.expectError(raw.StreamError.OutOfMemory, result);\n}\n\ntest \"addChunkLocked must keep state buffer consistent with chunk count\" {\n    // Invariant: state_capacity must track chunks.items.len.\n\n    var failing = std.testing.FailingAllocator.init(std.heap.page_allocator, .{\n        .fail_index = 6,\n    });\n\n    const stream = raw.Stream.create(failing.allocator(), testOptions(64, 1, false)) catch\n        return error.TestUnexpectedResult;\n    defer stream.destroy();\n\n    stream.write(&([_]u8{'A'} ** 64)) catch return error.TestUnexpectedResult;\n    stream.commit() catch return error.TestUnexpectedResult;\n    const result = stream.write(\"x\");\n    try testing.expectError(raw.StreamError.OutOfMemory, result);\n    try testing.expect(stream.state_capacity >= stream.chunks.items.len);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/renderer_test.zig",
    "content": "const std = @import(\"std\");\nconst renderer = @import(\"../renderer.zig\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst buffer = @import(\"../buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst ss = @import(\"../syntax-style.zig\");\nconst link = @import(\"../link.zig\");\nconst ansi = @import(\"../ansi.zig\");\n\nconst CliRenderer = renderer.CliRenderer;\nconst TextBuffer = text_buffer.TextBuffer;\nconst TextBufferView = text_buffer_view.TextBufferView;\nconst OptimizedBuffer = buffer.OptimizedBuffer;\nconst RGBA = text_buffer.RGBA;\n\nfn createWithOptionsOnce(allocator: std.mem.Allocator, width: u32, height: u32) !void {\n    const pool = gp.initGlobalPool(allocator);\n    defer gp.deinitGlobalPool();\n    defer link.deinitGlobalLinkPool();\n\n    var cli_renderer = try CliRenderer.createWithOptions(allocator, width, height, pool, true, false);\n    cli_renderer.destroy();\n}\n\ntest \"renderer - createWithOptions late allocation failure cleans up\" {\n    const allocation_count = blk: {\n        var counting_allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{});\n        try createWithOptionsOnce(counting_allocator.allocator(), 80, 24);\n        break :blk counting_allocator.alloc_index;\n    };\n\n    try std.testing.expect(allocation_count > 0);\n\n    var failing_allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{\n        .fail_index = allocation_count - 1,\n    });\n\n    try std.testing.expectError(error.OutOfMemory, createWithOptionsOnce(failing_allocator.allocator(), 80, 24));\n    try std.testing.expect(failing_allocator.has_induced_failure);\n    try std.testing.expectEqual(failing_allocator.allocated_bytes, failing_allocator.freed_bytes);\n}\n\ntest \"renderer - create and destroy\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    try std.testing.expectEqual(@as(u32, 80), cli_renderer.width);\n    try std.testing.expectEqual(@as(u32, 24), cli_renderer.height);\n    try std.testing.expect(cli_renderer.testing == true);\n}\n\ntest \"renderer - simple text rendering to currentRenderBuffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n\n    cli_renderer.render(false);\n\n    const current_buffer = cli_renderer.getCurrentBuffer();\n\n    const cell_h = current_buffer.get(0, 0);\n    try std.testing.expect(cell_h != null);\n    try std.testing.expectEqual(@as(u32, 'H'), cell_h.?.char);\n\n    const cell_e = current_buffer.get(1, 0);\n    try std.testing.expect(cell_e != null);\n    try std.testing.expectEqual(@as(u32, 'e'), cell_e.?.char);\n\n    const cell_w = current_buffer.get(6, 0);\n    try std.testing.expect(cell_w != null);\n    try std.testing.expectEqual(@as(u32, 'W'), cell_w.?.char);\n}\n\ntest \"renderer - multi-line text rendering\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n    cli_renderer.render(false);\n\n    const current_buffer = cli_renderer.getCurrentBuffer();\n\n    const cell_line1 = current_buffer.get(0, 0);\n    try std.testing.expect(cell_line1 != null);\n    try std.testing.expectEqual(@as(u32, 'L'), cell_line1.?.char);\n\n    const cell_line2 = current_buffer.get(0, 1);\n    try std.testing.expect(cell_line2 != null);\n    try std.testing.expectEqual(@as(u32, 'L'), cell_line2.?.char);\n\n    const cell_line3 = current_buffer.get(0, 2);\n    try std.testing.expect(cell_line3 != null);\n    try std.testing.expectEqual(@as(u32, 'L'), cell_line3.?.char);\n}\n\ntest \"renderer - emoji (wide grapheme) rendering\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hi 👋 there\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n    cli_renderer.render(false);\n\n    const current_buffer = cli_renderer.getCurrentBuffer();\n\n    const cell_h = current_buffer.get(0, 0);\n    try std.testing.expect(cell_h != null);\n    try std.testing.expectEqual(@as(u32, 'H'), cell_h.?.char);\n\n    const cell_i = current_buffer.get(1, 0);\n    try std.testing.expect(cell_i != null);\n    try std.testing.expectEqual(@as(u32, 'i'), cell_i.?.char);\n\n    const cell_space1 = current_buffer.get(2, 0);\n    try std.testing.expect(cell_space1 != null);\n    try std.testing.expectEqual(@as(u32, ' '), cell_space1.?.char);\n\n    const cell_emoji = current_buffer.get(3, 0);\n    try std.testing.expect(cell_emoji != null);\n    try std.testing.expect(gp.isGraphemeChar(cell_emoji.?.char));\n\n    const cell_emoji_continuation = current_buffer.get(4, 0);\n    try std.testing.expect(cell_emoji_continuation != null);\n    try std.testing.expect(gp.isContinuationChar(cell_emoji_continuation.?.char));\n\n    const cell_space2 = current_buffer.get(5, 0);\n    try std.testing.expect(cell_space2 != null);\n    try std.testing.expectEqual(@as(u32, ' '), cell_space2.?.char);\n\n    const cell_t = current_buffer.get(6, 0);\n    try std.testing.expect(cell_t != null);\n    try std.testing.expectEqual(@as(u32, 't'), cell_t.?.char);\n}\n\ntest \"renderer - CJK characters rendering\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello 世界\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n    cli_renderer.render(false);\n\n    const current_buffer = cli_renderer.getCurrentBuffer();\n\n    const cell_h = current_buffer.get(0, 0);\n    try std.testing.expect(cell_h != null);\n    try std.testing.expectEqual(@as(u32, 'H'), cell_h.?.char);\n\n    const cell_space = current_buffer.get(5, 0);\n    try std.testing.expect(cell_space != null);\n    try std.testing.expectEqual(@as(u32, ' '), cell_space.?.char);\n\n    const cell_cjk1 = current_buffer.get(6, 0);\n    try std.testing.expect(cell_cjk1 != null);\n    try std.testing.expect(gp.isGraphemeChar(cell_cjk1.?.char));\n\n    const cell_cjk1_continuation = current_buffer.get(7, 0);\n    try std.testing.expect(cell_cjk1_continuation != null);\n    try std.testing.expect(gp.isContinuationChar(cell_cjk1_continuation.?.char));\n\n    const cell_cjk2 = current_buffer.get(8, 0);\n    try std.testing.expect(cell_cjk2 != null);\n    try std.testing.expect(gp.isGraphemeChar(cell_cjk2.?.char));\n\n    const cell_cjk2_continuation = current_buffer.get(9, 0);\n    try std.testing.expect(cell_cjk2_continuation != null);\n    try std.testing.expect(gp.isContinuationChar(cell_cjk2_continuation.?.char));\n}\n\ntest \"renderer - mixed ASCII, emoji, and CJK\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"A 😀 世\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n    cli_renderer.render(false);\n\n    const current_buffer = cli_renderer.getCurrentBuffer();\n\n    const cell_a = current_buffer.get(0, 0);\n    try std.testing.expect(cell_a != null);\n    try std.testing.expectEqual(@as(u32, 'A'), cell_a.?.char);\n\n    const cell_space1 = current_buffer.get(1, 0);\n    try std.testing.expect(cell_space1 != null);\n    try std.testing.expectEqual(@as(u32, ' '), cell_space1.?.char);\n\n    const cell_emoji = current_buffer.get(2, 0);\n    try std.testing.expect(cell_emoji != null);\n    try std.testing.expect(gp.isGraphemeChar(cell_emoji.?.char));\n\n    const cell_emoji_continuation = current_buffer.get(3, 0);\n    try std.testing.expect(cell_emoji_continuation != null);\n    try std.testing.expect(gp.isContinuationChar(cell_emoji_continuation.?.char));\n\n    const cell_space2 = current_buffer.get(4, 0);\n    try std.testing.expect(cell_space2 != null);\n    try std.testing.expectEqual(@as(u32, ' '), cell_space2.?.char);\n\n    const cell_cjk = current_buffer.get(5, 0);\n    try std.testing.expect(cell_cjk != null);\n    try std.testing.expect(gp.isGraphemeChar(cell_cjk.?.char));\n\n    const cell_cjk_continuation = current_buffer.get(6, 0);\n    try std.testing.expect(cell_cjk_continuation != null);\n    try std.testing.expect(gp.isContinuationChar(cell_cjk_continuation.?.char));\n}\n\ntest \"renderer - resize updates dimensions\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    try std.testing.expectEqual(@as(u32, 80), cli_renderer.width);\n    try std.testing.expectEqual(@as(u32, 24), cli_renderer.height);\n\n    try cli_renderer.resize(120, 40);\n\n    try std.testing.expectEqual(@as(u32, 120), cli_renderer.width);\n    try std.testing.expectEqual(@as(u32, 40), cli_renderer.height);\n}\n\ntest \"renderer - background color setting\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const bg_color = RGBA{ 0.1, 0.2, 0.3, 1.0 };\n    cli_renderer.setBackgroundColor(bg_color);\n\n    try std.testing.expectEqual(bg_color, cli_renderer.backgroundColor);\n    try std.testing.expectEqual(bg_color, cli_renderer.getNextBuffer().getBlendBackdropColor().?);\n\n    const transparent_bg = RGBA{ 0.25, 0.5, 0.75, 0.0 };\n    cli_renderer.setBackgroundColor(transparent_bg);\n\n    try std.testing.expectEqual(transparent_bg, cli_renderer.backgroundColor);\n    try std.testing.expectEqual(RGBA{ 0.25, 0.5, 0.75, 1.0 }, cli_renderer.getNextBuffer().getBlendBackdropColor().?);\n}\n\ntest \"renderer - empty text buffer renders correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n    cli_renderer.render(false);\n}\n\ntest \"renderer - multiple renders update currentRenderBuffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    try tb.setText(\"Hello\");\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n    cli_renderer.render(false);\n\n    var current_buffer = cli_renderer.getCurrentBuffer();\n    var first_cell = current_buffer.get(0, 0);\n    try std.testing.expect(first_cell != null);\n    try std.testing.expectEqual(@as(u32, 'H'), first_cell.?.char);\n\n    try tb.setText(\"World\");\n    const next_buffer2 = cli_renderer.getNextBuffer();\n    try next_buffer2.drawTextBuffer(view, 0, 0);\n    cli_renderer.render(false);\n\n    current_buffer = cli_renderer.getCurrentBuffer();\n    first_cell = current_buffer.get(0, 0);\n    try std.testing.expect(first_cell != null);\n    try std.testing.expectEqual(@as(u32, 'W'), first_cell.?.char);\n}\n\ntest \"renderer - 1000 frame render loop with setStyledText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        24,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    const frame_texts = [_][]const u8{\n        \"Frame ASCII\",\n        \"Frame 👋 emoji\",\n        \"Frame 世界 CJK\",\n        \"Mixed 😀 世\",\n    };\n\n    const fg_color = [4]f32{ 1.0, 0.8, 0.6, 1.0 };\n    const bg_color = [4]f32{ 0.1, 0.1, 0.2, 1.0 };\n\n    var frame: u32 = 0;\n    while (frame < 1000) : (frame += 1) {\n        const text_idx = frame % frame_texts.len;\n        const text = frame_texts[text_idx];\n\n        const chunks = [_]text_buffer.StyledChunk{.{\n            .text_ptr = text.ptr,\n            .text_len = text.len,\n            .fg_ptr = @ptrCast(&fg_color),\n            .bg_ptr = @ptrCast(&bg_color),\n            .attributes = 0,\n        }};\n\n        try tb.setStyledText(&chunks);\n        try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        try opt_buffer.drawTextBuffer(view, 0, 0);\n\n        const next_buffer = cli_renderer.getNextBuffer();\n        try next_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        next_buffer.drawFrameBuffer(0, 0, opt_buffer, null, null, null, null);\n\n        cli_renderer.render(false);\n\n        if (frame % 100 == 0) {\n            const current_buffer = cli_renderer.getCurrentBuffer();\n            const first_cell = current_buffer.get(0, 0);\n            try std.testing.expect(first_cell != null);\n            try std.testing.expect(first_cell.?.char != 32);\n\n            try std.testing.expectEqual(frame + 1, cli_renderer.renderStats.frameCount);\n        }\n    }\n\n    try std.testing.expectEqual(@as(u64, 1000), cli_renderer.renderStats.frameCount);\n\n    const current_buffer = cli_renderer.getCurrentBuffer();\n    const final_cell = current_buffer.get(0, 0);\n    try std.testing.expect(final_cell != null);\n    try std.testing.expectEqual(@as(u32, 'M'), final_cell.?.char);\n}\n\ntest \"renderer - grapheme pool refcounting with frame buffer fast path\" {\n    const limited_pool = gp.initGlobalPoolWithOptions(std.testing.allocator, .{\n        .slots_per_page = [_]u32{ 2, 2, 2, 2, 2 },\n    });\n    defer gp.deinitGlobalPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, limited_pool, link.initGlobalLinkPool(std.testing.allocator), .unicode);\n    defer tb.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        limited_pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    var frame_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        24,\n        .{ .pool = limited_pool, .width_method = .unicode, .respectAlpha = false },\n    );\n    defer frame_buffer.deinit();\n\n    const fg_color = [4]f32{ 1.0, 1.0, 1.0, 1.0 };\n    const bg_color = [4]f32{ 0.0, 0.0, 0.0, 0.0 };\n\n    const text_with_emoji = \"👋\";\n    const chunks = [_]text_buffer.StyledChunk{.{\n        .text_ptr = text_with_emoji.ptr,\n        .text_len = text_with_emoji.len,\n        .fg_ptr = @ptrCast(&fg_color),\n        .bg_ptr = @ptrCast(&bg_color),\n        .attributes = 0,\n    }};\n    try tb.setStyledText(&chunks);\n    try frame_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try frame_buffer.drawTextBuffer(view, 0, 0);\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    next_buffer.setRespectAlpha(false);\n    try next_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n\n    next_buffer.drawFrameBuffer(0, 0, frame_buffer, null, null, null, null);\n\n    try frame_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n\n    var i: usize = 0;\n    while (i < 10) : (i += 1) {\n        const new_text = \"🎉🚀💯\";\n        const new_chunks = [_]text_buffer.StyledChunk{.{\n            .text_ptr = new_text.ptr,\n            .text_len = new_text.len,\n            .fg_ptr = @ptrCast(&fg_color),\n            .bg_ptr = @ptrCast(&bg_color),\n            .attributes = 0,\n        }};\n        try tb.setStyledText(&new_chunks);\n        try frame_buffer.drawTextBuffer(view, 0, 0);\n        try frame_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    }\n\n    cli_renderer.render(false);\n\n    const current_buffer = cli_renderer.getCurrentBuffer();\n    const rendered_cell = current_buffer.get(0, 0);\n    try std.testing.expect(rendered_cell != null);\n}\n\ntest \"renderer - unchanged grapheme should not churn IDs across frames\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        4,\n        1,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    const first_next_buffer = cli_renderer.getNextBuffer();\n    try first_next_buffer.drawText(\"👋\", 0, 0, fg, bg, 0);\n    cli_renderer.render(false);\n\n    const first_output = cli_renderer.getLastOutputForTest();\n    try std.testing.expect(std.mem.indexOf(u8, first_output, \"👋\") != null);\n\n    const current_buffer = cli_renderer.getCurrentBuffer();\n    const first_cell = current_buffer.get(0, 0);\n    try std.testing.expect(first_cell != null);\n    try std.testing.expect(gp.isGraphemeChar(first_cell.?.char));\n    const first_gid = gp.graphemeIdFromChar(first_cell.?.char);\n\n    const second_next_buffer = cli_renderer.getNextBuffer();\n    try second_next_buffer.drawText(\"👋\", 0, 0, fg, bg, 0);\n\n    const second_cell = second_next_buffer.get(0, 0);\n    try std.testing.expect(second_cell != null);\n    try std.testing.expect(gp.isGraphemeChar(second_cell.?.char));\n    const second_gid = gp.graphemeIdFromChar(second_cell.?.char);\n\n    // Same grapheme content in consecutive frames should keep a stable ID,\n    // otherwise diff/write treats unchanged cells as modified every frame.\n    try std.testing.expectEqual(first_gid, second_gid);\n\n    cli_renderer.render(false);\n\n    const second_output = cli_renderer.getLastOutputForTest();\n    try std.testing.expect(std.mem.indexOf(u8, second_output, \"👋\") == null);\n}\n\ntest \"renderer - hyperlinks enabled with OSC 8 output\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const local_link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    // Enable hyperlinks capability\n    cli_renderer.terminal.caps.hyperlinks = true;\n\n    // Allocate a link\n    const link_id = try local_link_pool.alloc(\"https://example.com\");\n    const attributes = ansi.TextAttributes.setLinkId(ansi.TextAttributes.BOLD, link_id);\n\n    const next_buffer = cli_renderer.getNextBuffer();\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try next_buffer.drawText(\"Click here\", 0, 0, fg, bg, attributes);\n\n    cli_renderer.render(false);\n\n    const output = cli_renderer.getLastOutputForTest();\n\n    // Verify OSC 8 contains id parameter\n    try std.testing.expect(std.mem.indexOf(u8, output, \"\\x1b]8;id=\") != null);\n    // Verify OSC 8 contains the URL\n    try std.testing.expect(std.mem.indexOf(u8, output, \";https://example.com\\x1b\\\\\") != null);\n\n    // Verify output contains OSC 8 end sequence\n    const end_seq = \"\\x1b]8;;\\x1b\\\\\";\n    var count: usize = 0;\n    var pos: usize = 0;\n    while (std.mem.indexOf(u8, output[pos..], end_seq)) |found| {\n        count += 1;\n        pos += found + end_seq.len;\n    }\n    try std.testing.expect(count >= 1);\n}\n\ntest \"renderer - hyperlinks disabled no OSC 8 output\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const local_link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    // Hyperlinks disabled by default\n    cli_renderer.terminal.caps.hyperlinks = false;\n\n    // Allocate a link\n    const link_id = try local_link_pool.alloc(\"https://example.com\");\n    const attributes = ansi.TextAttributes.setLinkId(0, link_id);\n\n    const next_buffer = cli_renderer.getNextBuffer();\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    try next_buffer.drawText(\"Click here\", 0, 0, fg, bg, attributes);\n\n    cli_renderer.render(false);\n\n    const output = cli_renderer.getLastOutputForTest();\n\n    // Verify output does NOT contain OSC 8 sequences\n    try std.testing.expect(std.mem.indexOf(u8, output, \"]8;;\") == null);\n    try std.testing.expect(std.mem.indexOf(u8, output, \"]8;id=\") == null);\n}\n\ntest \"renderer - link transition mid-line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const local_link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    // Enable hyperlinks\n    cli_renderer.terminal.caps.hyperlinks = true;\n\n    const next_buffer = cli_renderer.getNextBuffer();\n\n    // Allocate two different links\n    const link_id1 = try local_link_pool.alloc(\"https://first.com\");\n    const link_id2 = try local_link_pool.alloc(\"https://second.com\");\n\n    const attr1 = ansi.TextAttributes.setLinkId(0, link_id1);\n    const attr2 = ansi.TextAttributes.setLinkId(0, link_id2);\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    // Draw first link\n    try next_buffer.drawText(\"First\", 0, 0, fg, bg, attr1);\n    // Draw second link\n    try next_buffer.drawText(\"Second\", 6, 0, fg, bg, attr2);\n    // Draw no link\n    try next_buffer.drawText(\"Normal\", 13, 0, fg, bg, 0);\n\n    cli_renderer.render(false);\n\n    const output = cli_renderer.getLastOutputForTest();\n\n    // Should contain both URLs\n    try std.testing.expect(std.mem.indexOf(u8, output, \"https://first.com\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, output, \"https://second.com\") != null);\n\n    // Should have multiple OSC 8 end sequences (at least 2 transitions)\n    const end_seq = \"\\x1b]8;;\\x1b\\\\\";\n    var count: usize = 0;\n    var pos: usize = 0;\n    while (std.mem.indexOf(u8, output[pos..], end_seq)) |found| {\n        count += 1;\n        pos += found + end_seq.len;\n    }\n    try std.testing.expect(count >= 2);\n}\n\ntest \"renderer - hyperlink spanning multiple rows uses same id\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    // Enable hyperlinks\n    cli_renderer.terminal.caps.hyperlinks = true;\n\n    const next_buffer = cli_renderer.getNextBuffer();\n\n    // Allocate a single link\n    const link_id = try link_pool.alloc(\"https://example.com/long-url\");\n    const attributes = ansi.TextAttributes.setLinkId(0, link_id);\n\n    const fg = RGBA{ 1.0, 1.0, 1.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    // Fill entire row 0 with linked text so link is never interrupted by empty cells\n    try next_buffer.drawText(\"01234567890123456789012345678901234567890123456789012345678901234567890123456789\", 0, 0, fg, bg, attributes);\n    // Continue the same link on row 1\n    try next_buffer.drawText(\"Here\", 0, 1, fg, bg, attributes);\n\n    cli_renderer.render(false);\n\n    const output = cli_renderer.getLastOutputForTest();\n\n    // Build expected OSC 8 open sequence with the link id\n    var buf: [256]u8 = undefined;\n    const expected_open = std.fmt.bufPrint(&buf, \"\\x1b]8;id={d};https://example.com/long-url\\x1b\\\\\", .{link_id}) catch unreachable;\n    var count: usize = 0;\n    var pos: usize = 0;\n    while (std.mem.indexOf(u8, output[pos..], expected_open)) |found| {\n        count += 1;\n        pos += found + expected_open.len;\n    }\n    // Should appear exactly once: the link stays open across rows without\n    // close/reopen at row boundaries, so terminals treat it as one contiguous link.\n    try std.testing.expectEqual(@as(usize, 1), count);\n}\n\n// ============================================================================\n// GRAPHEME CURSOR POSITIONING TESTS\n// ============================================================================\n\ntest \"renderer - default cursor style emits reset cursor ANSI\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    cli_renderer.terminal.setCursorPosition(4, 2, true);\n    cli_renderer.render(false);\n\n    const output = cli_renderer.getLastOutputForTest();\n\n    try std.testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.defaultCursorStyle) != null);\n    try std.testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.cursorBlock) == null);\n}\n\ntest \"renderer - explicit_cursor_positioning emits cursor move after wide graphemes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"👋X\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    cli_renderer.terminal.caps.explicit_cursor_positioning = true;\n    cli_renderer.terminal.caps.explicit_width = false;\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n\n    cli_renderer.render(false);\n\n    const output = cli_renderer.getLastOutputForTest();\n\n    try std.testing.expect(std.mem.indexOf(u8, output, \"\\x1b[1;3H\") != null);\n}\n\ntest \"renderer - explicit_cursor_positioning produces more cursor moves\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n    try tb.setText(\"👋🎉🚀\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer1 = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer1.destroy();\n\n    cli_renderer1.terminal.caps.explicit_cursor_positioning = false;\n    cli_renderer1.terminal.caps.explicit_width = false;\n\n    const next_buffer1 = cli_renderer1.getNextBuffer();\n    try next_buffer1.drawTextBuffer(view, 0, 0);\n    cli_renderer1.render(false);\n    const output_without = cli_renderer1.getLastOutputForTest();\n\n    var cli_renderer2 = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer2.destroy();\n\n    cli_renderer2.terminal.caps.explicit_cursor_positioning = true;\n    cli_renderer2.terminal.caps.explicit_width = false;\n\n    const next_buffer2 = cli_renderer2.getNextBuffer();\n    try next_buffer2.drawTextBuffer(view, 0, 0);\n    cli_renderer2.render(false);\n    const output_with = cli_renderer2.getLastOutputForTest();\n\n    var count_without: usize = 0;\n    var count_with: usize = 0;\n\n    var i: usize = 0;\n    while (i + 3 < output_without.len) : (i += 1) {\n        if (output_without[i] == '\\x1b' and output_without[i + 1] == '[') {\n            var j = i + 2;\n            while (j < output_without.len and ((output_without[j] >= '0' and output_without[j] <= '9') or output_without[j] == ';')) : (j += 1) {}\n            if (j < output_without.len and output_without[j] == 'H') {\n                count_without += 1;\n            }\n        }\n    }\n\n    i = 0;\n    while (i + 3 < output_with.len) : (i += 1) {\n        if (output_with[i] == '\\x1b' and output_with[i + 1] == '[') {\n            var j = i + 2;\n            while (j < output_with.len and ((output_with[j] >= '0' and output_with[j] <= '9') or output_with[j] == ';')) : (j += 1) {}\n            if (j < output_with.len and output_with[j] == 'H') {\n                count_with += 1;\n            }\n        }\n    }\n\n    try std.testing.expect(count_with > count_without);\n}\n\ntest \"renderer - explicit_cursor_positioning with CJK characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    var local_link_pool = link.LinkPool.init(std.testing.allocator);\n    defer local_link_pool.deinit();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, &local_link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"世X\");\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var cli_renderer = try CliRenderer.create(\n        std.testing.allocator,\n        80,\n        24,\n        pool,\n        true,\n    );\n    defer cli_renderer.destroy();\n\n    cli_renderer.terminal.caps.explicit_cursor_positioning = true;\n    cli_renderer.terminal.caps.explicit_width = false;\n\n    const next_buffer = cli_renderer.getNextBuffer();\n    try next_buffer.drawTextBuffer(view, 0, 0);\n\n    cli_renderer.render(false);\n\n    const output = cli_renderer.getLastOutputForTest();\n\n    try std.testing.expect(std.mem.indexOf(u8, output, \"\\x1b[1;3H\") != null);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/rope-nested_test.zig",
    "content": "const std = @import(\"std\");\nconst rope_mod = @import(\"../rope.zig\");\n\n// NOTE: This is not strictly necessary to be supported as it is not used this way in the codebase.\n// It can be removed if the rope needs to change in a way that would break this behavior.\n\nconst Chunk = struct {\n    data: []const u8,\n    width: u32,\n\n    pub const Metrics = struct {\n        total_width: u32 = 0,\n        total_bytes: u32 = 0,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.total_width += other.total_width;\n            self.total_bytes += other.total_bytes;\n        }\n    };\n\n    pub fn measure(self: *const Chunk) Metrics {\n        return .{\n            .total_width = self.width,\n            .total_bytes = @intCast(self.data.len),\n        };\n    }\n\n    pub fn empty() Chunk {\n        return .{ .data = \"\", .width = 0 };\n    }\n\n    pub fn is_empty(self: *const Chunk) bool {\n        return self.data.len == 0;\n    }\n};\n\n// Static empty chunk rope node for Line.empty()\nconst empty_chunk_leaf_node = rope_mod.Rope(Chunk).Node{\n    .leaf = .{\n        .data = Chunk.empty(),\n    },\n};\n\n// Line type containing a rope of chunks\nconst Line = struct {\n    chunks: rope_mod.Rope(Chunk),\n    line_id: u32,\n\n    pub const Metrics = struct {\n        total_width: u32 = 0,\n        total_lines: u32 = 1,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.total_width += other.total_width;\n            self.total_lines += other.total_lines;\n        }\n    };\n\n    pub fn measure(self: *const Line) Metrics {\n        const chunk_metrics = self.chunks.root.metrics();\n        return .{\n            .total_width = chunk_metrics.custom.total_width,\n            .total_lines = 1,\n        };\n    }\n\n    pub fn empty() Line {\n        // Use static empty chunk rope - safe because it's immutable\n        const ChunkRope = rope_mod.Rope(Chunk);\n        return .{\n            .chunks = .{\n                .root = &empty_chunk_leaf_node,\n                .allocator = undefined, // Never used for empty\n                .empty_leaf = &empty_chunk_leaf_node,\n                .marker_cache = ChunkRope.MarkerCache.init(undefined),\n            },\n            .line_id = 0,\n        };\n    }\n\n    pub fn is_empty(self: *const Line) bool {\n        return self.line_id == 0 and self.chunks.count() == 1;\n    }\n};\n\ntest \"Nested Rope - create line with chunks\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks = [_]Chunk{\n        .{ .data = \"Hello \", .width = 6 },\n        .{ .data = \"World\", .width = 5 },\n    };\n\n    const chunk_rope = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks);\n\n    const line = Line{\n        .chunks = chunk_rope,\n        .line_id = 1,\n    };\n\n    try std.testing.expectEqual(@as(u32, 2), line.chunks.count());\n    try std.testing.expectEqualStrings(\"Hello \", line.chunks.get(0).?.data);\n    try std.testing.expectEqualStrings(\"World\", line.chunks.get(1).?.data);\n\n    const metrics = line.measure();\n    try std.testing.expectEqual(@as(u32, 11), metrics.total_width); // 6 + 5\n}\n\ntest \"Nested Rope - rope of lines with chunks\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{\n        .{ .data = \"Line \", .width = 5 },\n        .{ .data = \"One\", .width = 3 },\n    };\n    const line1 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1),\n        .line_id = 1,\n    };\n\n    const chunks2 = [_]Chunk{\n        .{ .data = \"Line \", .width = 5 },\n        .{ .data = \"Two\", .width = 3 },\n    };\n    const line2 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2),\n        .line_id = 2,\n    };\n\n    const lines = [_]Line{ line1, line2 };\n    var line_rope = try rope_mod.Rope(Line).from_slice(allocator, &lines);\n\n    try std.testing.expectEqual(@as(u32, 2), line_rope.count());\n\n    // Access nested data\n    const first_line = line_rope.get(0).?;\n    try std.testing.expectEqual(@as(u32, 1), first_line.line_id);\n    try std.testing.expectEqual(@as(u32, 2), first_line.chunks.count());\n    try std.testing.expectEqualStrings(\"Line \", first_line.chunks.get(0).?.data);\n\n    const second_line = line_rope.get(1).?;\n    try std.testing.expectEqual(@as(u32, 2), second_line.line_id);\n    try std.testing.expectEqualStrings(\"Two\", second_line.chunks.get(1).?.data);\n}\n\ntest \"Nested Rope - insert chunk into line\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks = [_]Chunk{\n        .{ .data = \"Hello \", .width = 6 },\n        .{ .data = \"World\", .width = 5 },\n    };\n    var chunk_rope = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks);\n\n    try chunk_rope.insert(1, .{ .data = \"Beautiful \", .width = 10 });\n\n    try std.testing.expectEqual(@as(u32, 3), chunk_rope.count());\n    try std.testing.expectEqualStrings(\"Hello \", chunk_rope.get(0).?.data);\n    try std.testing.expectEqualStrings(\"Beautiful \", chunk_rope.get(1).?.data);\n    try std.testing.expectEqualStrings(\"World\", chunk_rope.get(2).?.data);\n\n    const metrics = chunk_rope.root.metrics();\n    try std.testing.expectEqual(@as(u32, 21), metrics.custom.total_width); // 6 + 10 + 5\n}\n\ntest \"Nested Rope - delete chunk from line\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks = [_]Chunk{\n        .{ .data = \"A \", .width = 2 },\n        .{ .data = \"B \", .width = 2 },\n        .{ .data = \"C\", .width = 1 },\n    };\n    var chunk_rope = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks);\n\n    try chunk_rope.delete(1);\n\n    try std.testing.expectEqual(@as(u32, 2), chunk_rope.count());\n    try std.testing.expectEqualStrings(\"A \", chunk_rope.get(0).?.data);\n    try std.testing.expectEqualStrings(\"C\", chunk_rope.get(1).?.data);\n\n    const metrics = chunk_rope.root.metrics();\n    try std.testing.expectEqual(@as(u32, 3), metrics.custom.total_width); // 2 + 1\n}\n\ntest \"Nested Rope - walk through lines and chunks\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{.{ .data = \"Line1\", .width = 5 }};\n    const chunks2 = [_]Chunk{.{ .data = \"Line2\", .width = 5 }};\n    const chunks3 = [_]Chunk{.{ .data = \"Line3\", .width = 5 }};\n\n    const lines = [_]Line{\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1), .line_id = 1 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2), .line_id = 2 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks3), .line_id = 3 },\n    };\n    var line_rope = try rope_mod.Rope(Line).from_slice(allocator, &lines);\n\n    const LineContext = struct {\n        total_lines: u32 = 0,\n        total_width: u32 = 0,\n\n        fn walker(ctx: *anyopaque, line: *const Line, index: u32) rope_mod.Rope(Line).Node.WalkerResult {\n            _ = index;\n            const self = @as(*@This(), @ptrCast(@alignCast(ctx)));\n            self.total_lines += 1;\n\n            const metrics = line.chunks.root.metrics();\n            self.total_width += metrics.custom.total_width;\n            return .{};\n        }\n    };\n\n    var ctx = LineContext{};\n    try line_rope.walk(&ctx, LineContext.walker);\n\n    try std.testing.expectEqual(@as(u32, 3), ctx.total_lines);\n    try std.testing.expectEqual(@as(u32, 15), ctx.total_width); // 5 + 5 + 5\n}\n\ntest \"Nested Rope - complex line and chunk operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{\n        .{ .data = \"First \", .width = 6 },\n        .{ .data = \"line\", .width = 4 },\n    };\n    const line1 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1),\n        .line_id = 1,\n    };\n\n    var line_rope = try rope_mod.Rope(Line).from_item(allocator, line1);\n\n    const chunks2 = [_]Chunk{.{ .data = \"Second line\", .width = 11 }};\n    const line2 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2),\n        .line_id = 2,\n    };\n    try line_rope.append(line2);\n\n    try std.testing.expectEqual(@as(u32, 2), line_rope.count());\n\n    // Access specific chunk in specific line\n    const first_line = line_rope.get(0).?;\n    try std.testing.expectEqual(@as(u32, 2), first_line.chunks.count());\n    try std.testing.expectEqualStrings(\"First \", first_line.chunks.get(0).?.data);\n\n    const metrics = line_rope.root.metrics();\n    try std.testing.expectEqual(@as(u32, 2), metrics.count);\n}\n\ntest \"Nested Rope - metrics propagate through all levels\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{\n        .{ .data = \"abc\", .width = 3 },\n        .{ .data = \"def\", .width = 3 },\n    };\n    const chunks2 = [_]Chunk{\n        .{ .data = \"12345\", .width = 5 },\n    };\n    const chunks3 = [_]Chunk{\n        .{ .data = \"x\", .width = 1 },\n        .{ .data = \"y\", .width = 1 },\n        .{ .data = \"z\", .width = 1 },\n    };\n\n    const lines = [_]Line{\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1), .line_id = 1 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2), .line_id = 2 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks3), .line_id = 3 },\n    };\n    var line_rope = try rope_mod.Rope(Line).from_slice(allocator, &lines);\n\n    const line_metrics = line_rope.root.metrics();\n    try std.testing.expectEqual(@as(u32, 3), line_metrics.count);\n\n    const line1 = line_rope.get(0).?;\n    const line1_metrics = line1.measure();\n    try std.testing.expectEqual(@as(u32, 6), line1_metrics.total_width); // 3 + 3\n\n    const line2 = line_rope.get(1).?;\n    const line2_metrics = line2.measure();\n    try std.testing.expectEqual(@as(u32, 5), line2_metrics.total_width);\n\n    const line3 = line_rope.get(2).?;\n    const line3_metrics = line3.measure();\n    try std.testing.expectEqual(@as(u32, 3), line3_metrics.total_width); // 1 + 1 + 1\n}\n\ntest \"Nested Rope - simulate text buffer edit: insert within line\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const initial_chunks = [_]Chunk{\n        .{ .data = \"Hello \", .width = 6 },\n        .{ .data = \"World\", .width = 5 },\n    };\n    var chunk_rope = try rope_mod.Rope(Chunk).from_slice(allocator, &initial_chunks);\n\n    try chunk_rope.insert(1, .{ .data = \"Beautiful \", .width = 10 });\n\n    try std.testing.expectEqual(@as(u32, 3), chunk_rope.count());\n\n    try std.testing.expectEqualStrings(\"Hello \", chunk_rope.get(0).?.data);\n    try std.testing.expectEqualStrings(\"Beautiful \", chunk_rope.get(1).?.data);\n    try std.testing.expectEqualStrings(\"World\", chunk_rope.get(2).?.data);\n\n    const metrics = chunk_rope.root.metrics();\n    try std.testing.expectEqual(@as(u32, 21), metrics.custom.total_width);\n}\n\ntest \"Nested Rope - simulate text buffer edit: delete within line\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const initial_chunks = [_]Chunk{\n        .{ .data = \"Hello \", .width = 6 },\n        .{ .data = \"Beautiful \", .width = 10 },\n        .{ .data = \"World\", .width = 5 },\n    };\n    var chunk_rope = try rope_mod.Rope(Chunk).from_slice(allocator, &initial_chunks);\n\n    try chunk_rope.delete(1);\n\n    try std.testing.expectEqual(@as(u32, 2), chunk_rope.count());\n    try std.testing.expectEqualStrings(\"Hello \", chunk_rope.get(0).?.data);\n    try std.testing.expectEqualStrings(\"World\", chunk_rope.get(1).?.data);\n\n    const metrics = chunk_rope.root.metrics();\n    try std.testing.expectEqual(@as(u32, 11), metrics.custom.total_width);\n}\n\ntest \"Nested Rope - insert line into document\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{.{ .data = \"Line 1\", .width = 6 }};\n    const chunks3 = [_]Chunk{.{ .data = \"Line 3\", .width = 6 }};\n\n    const initial_lines = [_]Line{\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1), .line_id = 1 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks3), .line_id = 3 },\n    };\n    var line_rope = try rope_mod.Rope(Line).from_slice(allocator, &initial_lines);\n\n    const chunks2 = [_]Chunk{.{ .data = \"Line 2\", .width = 6 }};\n    const line2 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2),\n        .line_id = 2,\n    };\n    try line_rope.insert(1, line2);\n\n    try std.testing.expectEqual(@as(u32, 3), line_rope.count());\n    try std.testing.expectEqual(@as(u32, 1), line_rope.get(0).?.line_id);\n    try std.testing.expectEqual(@as(u32, 2), line_rope.get(1).?.line_id);\n    try std.testing.expectEqual(@as(u32, 3), line_rope.get(2).?.line_id);\n}\n\ntest \"Nested Rope - delete line from document\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{.{ .data = \"Line 1\", .width = 6 }};\n    const chunks2 = [_]Chunk{.{ .data = \"Line 2\", .width = 6 }};\n    const chunks3 = [_]Chunk{.{ .data = \"Line 3\", .width = 6 }};\n\n    const lines = [_]Line{\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1), .line_id = 1 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2), .line_id = 2 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks3), .line_id = 3 },\n    };\n    var line_rope = try rope_mod.Rope(Line).from_slice(allocator, &lines);\n\n    try line_rope.delete(1);\n\n    try std.testing.expectEqual(@as(u32, 2), line_rope.count());\n    try std.testing.expectEqual(@as(u32, 1), line_rope.get(0).?.line_id);\n    try std.testing.expectEqual(@as(u32, 3), line_rope.get(1).?.line_id);\n}\n\ntest \"Nested Rope - modify chunks within a specific line\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const initial_chunks = [_]Chunk{\n        .{ .data = \"Hello\", .width = 5 },\n    };\n    var chunk_rope = try rope_mod.Rope(Chunk).from_slice(allocator, &initial_chunks);\n\n    try chunk_rope.insert(1, .{ .data = \" World\", .width = 6 });\n\n    const line = Line{\n        .chunks = chunk_rope,\n        .line_id = 1,\n    };\n\n    const lines = [_]Line{line};\n    var line_rope = try rope_mod.Rope(Line).from_slice(allocator, &lines);\n\n    const retrieved_line = line_rope.get(0).?;\n    try std.testing.expectEqual(@as(u32, 2), retrieved_line.chunks.count());\n    try std.testing.expectEqualStrings(\"Hello\", retrieved_line.chunks.get(0).?.data);\n    try std.testing.expectEqualStrings(\" World\", retrieved_line.chunks.get(1).?.data);\n\n    const line_metrics = retrieved_line.measure();\n    try std.testing.expectEqual(@as(u32, 11), line_metrics.total_width);\n}\n\ntest \"Nested Rope - walk all chunks in all lines\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{\n        .{ .data = \"A\", .width = 1 },\n        .{ .data = \"B\", .width = 1 },\n    };\n    const chunks2 = [_]Chunk{\n        .{ .data = \"C\", .width = 1 },\n        .{ .data = \"D\", .width = 1 },\n        .{ .data = \"E\", .width = 1 },\n    };\n\n    const lines = [_]Line{\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1), .line_id = 1 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2), .line_id = 2 },\n    };\n    var line_rope = try rope_mod.Rope(Line).from_slice(allocator, &lines);\n\n    var total_chunks: u32 = 0;\n    var total_bytes: u32 = 0;\n\n    const LineWalker = struct {\n        fn walk(ctx: *anyopaque, line: *const Line, index: u32) rope_mod.Rope(Line).Node.WalkerResult {\n            _ = index;\n            const counters = @as(*[2]u32, @ptrCast(@alignCast(ctx)));\n\n            // Count chunks in this line\n            const ChunkWalker = struct {\n                fn walk(chunk_ctx: *anyopaque, chunk: *const Chunk, chunk_idx: u32) rope_mod.Rope(Chunk).Node.WalkerResult {\n                    _ = chunk_idx;\n                    const chunk_counters = @as(*[2]u32, @ptrCast(@alignCast(chunk_ctx)));\n                    chunk_counters[0] += 1; // total_chunks\n                    chunk_counters[1] += @intCast(chunk.data.len); // total_bytes\n                    return .{};\n                }\n            };\n\n            line.chunks.walk(counters, ChunkWalker.walk) catch {};\n            return .{};\n        }\n    };\n\n    var counters = [2]u32{ total_chunks, total_bytes };\n    try line_rope.walk(&counters, LineWalker.walk);\n    total_chunks = counters[0];\n    total_bytes = counters[1];\n\n    try std.testing.expectEqual(@as(u32, 5), total_chunks); // 2 + 3\n    try std.testing.expectEqual(@as(u32, 5), total_bytes); // All single char chunks\n}\n\ntest \"Nested Rope - simulate full text buffer workflow\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{\n        .{ .data = \"Hello \", .width = 6 },\n        .{ .data = \"World\", .width = 5 },\n    };\n    const line1 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1),\n        .line_id = 1,\n    };\n\n    var document = try rope_mod.Rope(Line).from_item(allocator, line1);\n\n    const chunks2 = [_]Chunk{.{ .data = \"Goodbye\", .width = 7 }};\n    const line2 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2),\n        .line_id = 2,\n    };\n    try document.append(line2);\n\n    try std.testing.expectEqual(@as(u32, 2), document.count());\n\n    const retrieved_line1 = document.get(0).?;\n    try std.testing.expectEqual(@as(u32, 2), retrieved_line1.chunks.count());\n\n    const retrieved_line2 = document.get(1).?;\n    try std.testing.expectEqual(@as(u32, 1), retrieved_line2.chunks.count());\n\n    var modified_chunks = retrieved_line1.chunks;\n    try modified_chunks.insert(1, .{ .data = \"Beautiful \", .width = 10 });\n\n    const modified_line = Line{\n        .chunks = modified_chunks,\n        .line_id = 1,\n    };\n\n    // Note: In a real text buffer, you'd replace the line in the document\n    // For now, just verify the modified line has the new chunk\n    try std.testing.expectEqual(@as(u32, 3), modified_line.chunks.count());\n    try std.testing.expectEqualStrings(\"Beautiful \", modified_line.chunks.get(1).?.data);\n}\n\ntest \"Nested Rope - empty lines with no chunks\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const empty_chunks: []const Chunk = &[_]Chunk{};\n    const empty_line = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, empty_chunks),\n        .line_id = 1,\n    };\n\n    const chunks = [_]Chunk{.{ .data = \"Content\", .width = 7 }};\n    const content_line = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks),\n        .line_id = 2,\n    };\n\n    const lines = [_]Line{ empty_line, content_line };\n    var line_rope = try rope_mod.Rope(Line).from_slice(allocator, &lines);\n\n    try std.testing.expectEqual(@as(u32, 2), line_rope.count());\n\n    const empty = line_rope.get(0).?;\n    try std.testing.expectEqual(@as(u32, 0), empty.chunks.count());\n\n    const content = line_rope.get(1).?;\n    try std.testing.expectEqual(@as(u32, 1), content.chunks.count());\n    try std.testing.expectEqualStrings(\"Content\", content.chunks.get(0).?.data);\n}\n\ntest \"Nested Rope - large document with many lines and chunks\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var lines_array: [20]Line = undefined;\n    for (&lines_array, 0..) |*line, line_idx| {\n        var chunks_array: [3]Chunk = undefined;\n        for (&chunks_array, 0..) |*chunk, chunk_idx| {\n            chunk.* = .{\n                .data = \"X\",\n                .width = 1,\n            };\n            _ = chunk_idx;\n        }\n        line.* = Line{\n            .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks_array),\n            .line_id = @intCast(line_idx),\n        };\n    }\n\n    var document = try rope_mod.Rope(Line).from_slice(allocator, &lines_array);\n\n    try std.testing.expectEqual(@as(u32, 20), document.count());\n\n    const line_5 = document.get(5).?;\n    try std.testing.expectEqual(@as(u32, 5), line_5.line_id);\n    try std.testing.expectEqual(@as(u32, 3), line_5.chunks.count());\n\n    const line_15 = document.get(15).?;\n    try std.testing.expectEqual(@as(u32, 15), line_15.line_id);\n\n    try std.testing.expectEqualStrings(\"X\", line_5.chunks.get(0).?.data);\n}\n\ntest \"Nested Rope - walk_from specific line\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var lines_array: [5]Line = undefined;\n    for (&lines_array, 0..) |*line, i| {\n        const chunks = [_]Chunk{.{ .data = \"X\", .width = 1 }};\n        line.* = Line{\n            .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks),\n            .line_id = @intCast(i),\n        };\n    }\n    var document = try rope_mod.Rope(Line).from_slice(allocator, &lines_array);\n\n    const Context = struct {\n        count: u32 = 0,\n        first_id: ?u32 = null,\n\n        fn walker(ctx: *anyopaque, line: *const Line, index: u32) rope_mod.Rope(Line).Node.WalkerResult {\n            _ = index;\n            const self = @as(*@This(), @ptrCast(@alignCast(ctx)));\n            if (self.first_id == null) {\n                self.first_id = line.line_id;\n            }\n            self.count += 1;\n            return .{};\n        }\n    };\n\n    var ctx = Context{};\n    try document.walk_from(3, &ctx, Context.walker);\n\n    // Should walk lines 3 and 4 (indices 3 and 4)\n    try std.testing.expectEqual(@as(u32, 2), ctx.count);\n    try std.testing.expectEqual(@as(u32, 3), ctx.first_id.?);\n}\n\ntest \"Nested Rope - metrics aggregate correctly through all levels\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    // Line 1: 3 chunks, total width 10\n    const chunks1 = [_]Chunk{\n        .{ .data = \"abc\", .width = 3 },\n        .{ .data = \"def\", .width = 3 },\n        .{ .data = \"ghij\", .width = 4 },\n    };\n\n    // Line 2: 2 chunks, total width 15\n    const chunks2 = [_]Chunk{\n        .{ .data = \"12345\", .width = 5 },\n        .{ .data = \"6789012345\", .width = 10 },\n    };\n\n    const lines = [_]Line{\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1), .line_id = 1 },\n        .{ .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2), .line_id = 2 },\n    };\n    var document = try rope_mod.Rope(Line).from_slice(allocator, &lines);\n\n    const line1 = document.get(0).?;\n    const chunk_metrics1 = line1.chunks.root.metrics();\n    try std.testing.expectEqual(@as(u32, 3), chunk_metrics1.count);\n    try std.testing.expectEqual(@as(u32, 10), chunk_metrics1.custom.total_width);\n    try std.testing.expectEqual(@as(u32, 10), chunk_metrics1.custom.total_bytes);\n\n    const line2 = document.get(1).?;\n    const chunk_metrics2 = line2.chunks.root.metrics();\n    try std.testing.expectEqual(@as(u32, 2), chunk_metrics2.count);\n    try std.testing.expectEqual(@as(u32, 15), chunk_metrics2.custom.total_width);\n    try std.testing.expectEqual(@as(u32, 15), chunk_metrics2.custom.total_bytes);\n\n    const line1_metrics = line1.measure();\n    try std.testing.expectEqual(@as(u32, 10), line1_metrics.total_width);\n\n    const doc_metrics = document.root.metrics();\n    try std.testing.expectEqual(@as(u32, 2), doc_metrics.count); // 2 lines\n}\n\ntest \"Nested Rope - O(log n) access to deeply nested data\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var lines_array: [100]Line = undefined;\n    for (&lines_array, 0..) |*line, line_idx| {\n        var chunks_array: [5]Chunk = undefined;\n        for (&chunks_array, 0..) |*chunk, chunk_idx| {\n            chunk.* = .{\n                .data = \"c\",\n                .width = 1,\n            };\n            _ = chunk_idx;\n        }\n        line.* = Line{\n            .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks_array),\n            .line_id = @intCast(line_idx),\n        };\n    }\n\n    var document = try rope_mod.Rope(Line).from_slice(allocator, &lines_array);\n\n    // Access line 50, chunk 3 (deep in the tree)\n    // This tests O(log lines) + O(log chunks) access\n    const line_50 = document.get(50).?;\n    try std.testing.expectEqual(@as(u32, 50), line_50.line_id);\n\n    const chunk_3 = line_50.chunks.get(3).?;\n    try std.testing.expectEqualStrings(\"c\", chunk_3.data);\n\n    const doc_depth = document.root.depth();\n    try std.testing.expect(doc_depth < 20); // log2(100) ≈ 7, with some buffer\n\n    const line_depth = line_50.chunks.root.depth();\n    try std.testing.expect(line_depth < 10); // log2(5) ≈ 3, with buffer\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/rope_fuzz_test.zig",
    "content": "const std = @import(\"std\");\nconst rope_mod = @import(\"../rope.zig\");\n\nconst TestItem = struct {\n    value: u32,\n\n    pub fn empty() TestItem {\n        return .{ .value = 0 };\n    }\n\n    pub fn is_empty(self: *const TestItem) bool {\n        return self.value == 0;\n    }\n};\n\nfn verifyInvariants(rope: *const rope_mod.Rope(TestItem)) !void {\n    const count = rope.count();\n\n    var walked_count: u32 = 0;\n    const Context = struct {\n        count: *u32,\n        last_seen: u32 = 0,\n\n        fn walker(ctx: *anyopaque, data: *const TestItem, index: u32) rope_mod.Rope(TestItem).Node.WalkerResult {\n            _ = index;\n            _ = data;\n            const self = @as(*@This(), @ptrCast(@alignCast(ctx)));\n            self.count.* += 1;\n            return .{};\n        }\n    };\n\n    var ctx = Context{ .count = &walked_count };\n    try rope.walk(&ctx, Context.walker);\n\n    try std.testing.expectEqual(count, walked_count);\n\n    // Verify depth is logarithmic (allow slack for imbalance and sequential inserts)\n    const depth = rope.root.depth();\n    const max_expected_depth: u32 = if (count <= 1) 1 else @as(u32, @intFromFloat(@ceil(@log2(@as(f64, @floatFromInt(count)))) * 4.5));\n    try std.testing.expect(depth <= max_expected_depth);\n}\n\ntest \"Rope fuzz - random insert/delete sequence\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TestItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    var prng = std.Random.DefaultPrng.init(42);\n    const random = prng.random();\n\n    var i: usize = 0;\n    while (i < 100) : (i += 1) {\n        const op = random.intRangeAtMost(u8, 0, 2);\n        const current_count = rope.count();\n\n        switch (op) {\n            0 => {\n                const pos = if (current_count > 0) random.intRangeAtMost(u32, 0, current_count) else 0;\n                const value = random.int(u32);\n                try rope.insert(pos, .{ .value = value });\n            },\n            1 => {\n                if (current_count > 1) {\n                    const pos = random.intRangeAtMost(u32, 0, current_count - 1);\n                    try rope.delete(pos);\n                }\n            },\n            2 => {\n                if (current_count > 0) {\n                    const pos = random.intRangeAtMost(u32, 0, current_count - 1);\n                    const value = random.int(u32);\n                    try rope.replace(pos, .{ .value = value });\n                }\n            },\n            else => unreachable,\n        }\n\n        if (i % 10 == 0) {\n            try verifyInvariants(&rope);\n        }\n    }\n\n    try verifyInvariants(&rope);\n}\n\ntest \"Rope fuzz - random bulk operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TestItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    var prng = std.Random.DefaultPrng.init(123);\n    const random = prng.random();\n\n    var i: usize = 0;\n    while (i < 50) : (i += 1) {\n        const op = random.intRangeAtMost(u8, 0, 3);\n        const current_count = rope.count();\n\n        switch (op) {\n            0 => {\n                const slice_len = random.intRangeAtMost(u8, 1, 10);\n                var items: [10]TestItem = undefined;\n                for (items[0..slice_len]) |*item| {\n                    item.* = .{ .value = random.int(u32) };\n                }\n                const pos = if (current_count > 0) random.intRangeAtMost(u32, 0, current_count) else 0;\n                try rope.insert_slice(pos, items[0..slice_len]);\n            },\n            1 => {\n                if (current_count > 2) {\n                    const start = random.intRangeAtMost(u32, 0, current_count - 2);\n                    const end = random.intRangeAtMost(u32, start + 1, current_count);\n                    try rope.delete_range(start, end);\n                }\n            },\n            2 => {\n                if (current_count > 1) {\n                    const split_pos = random.intRangeAtMost(u32, 1, current_count - 1);\n                    var right_half = try rope.split(split_pos);\n                    try rope.concat(&right_half);\n                }\n            },\n            3 => {\n                const new_len = random.intRangeAtMost(u8, 1, 5);\n                var items: [5]TestItem = undefined;\n                for (items[0..new_len]) |*item| {\n                    item.* = .{ .value = random.int(u32) };\n                }\n                const new_rope = try RopeType.from_slice(arena.allocator(), items[0..new_len]);\n                try rope.concat(&new_rope);\n            },\n            else => unreachable,\n        }\n\n        if (i % 5 == 0) {\n            try verifyInvariants(&rope);\n        }\n    }\n\n    try verifyInvariants(&rope);\n}\n\ntest \"Rope fuzz - stress test with many items\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TestItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    var i: u32 = 0;\n    while (i < 1000) : (i += 1) {\n        try rope.append(.{ .value = i });\n    }\n\n    try verifyInvariants(&rope);\n\n    var prng = std.Random.DefaultPrng.init(789);\n    const random = prng.random();\n\n    var j: usize = 0;\n    while (j < 200) : (j += 1) {\n        const op = random.intRangeAtMost(u8, 0, 1);\n        const current_count = rope.count();\n\n        switch (op) {\n            0 => {\n                if (current_count > 100) {\n                    const pos = random.intRangeAtMost(u32, 0, current_count - 1);\n                    try rope.delete(pos);\n                }\n            },\n            1 => {\n                const pos = random.intRangeAtMost(u32, 0, current_count);\n                try rope.insert(pos, .{ .value = random.int(u32) });\n            },\n            else => unreachable,\n        }\n    }\n\n    try verifyInvariants(&rope);\n\n    const depth = rope.root.depth();\n    const count = rope.count();\n    const max_expected_depth: u32 = @as(u32, @intFromFloat(@ceil(@log2(@as(f64, @floatFromInt(count)))) * 4.0));\n    try std.testing.expect(depth <= max_expected_depth);\n}\n\ntest \"Rope fuzz - positional operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TestItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    var i: u32 = 0;\n    while (i < 50) : (i += 1) {\n        try rope.append(.{ .value = i });\n    }\n\n    var prng = std.Random.DefaultPrng.init(456);\n    const random = prng.random();\n\n    var position: u32 = 25;\n\n    var j: usize = 0;\n    while (j < 30) : (j += 1) {\n        const op = random.intRangeAtMost(u8, 0, 2);\n\n        switch (op) {\n            0 => {\n                try rope.insert(position, .{ .value = random.int(u32) });\n                position = position + 1;\n            },\n            1 => {\n                if (position < rope.count()) {\n                    try rope.delete(position);\n                }\n            },\n            2 => {\n                if (position < rope.count()) {\n                    try rope.replace(position, .{ .value = random.int(u32) });\n                }\n            },\n            else => unreachable,\n        }\n\n        if (random.boolean() and rope.count() > 0) {\n            position = random.intRangeAtMost(u32, 0, rope.count() - 1);\n        }\n    }\n\n    try verifyInvariants(&rope);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/rope_test.zig",
    "content": "const std = @import(\"std\");\nconst rope_mod = @import(\"../rope.zig\");\n\nconst SimpleItem = struct {\n    value: u32,\n\n    pub fn empty() SimpleItem {\n        return .{ .value = 0 };\n    }\n\n    pub fn is_empty(self: *const SimpleItem) bool {\n        return self.value == 0;\n    }\n};\n\nconst ItemWithMetrics = struct {\n    value: u32,\n    size: u32,\n\n    pub const Metrics = struct {\n        total_size: u32 = 0,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.total_size += other.total_size;\n        }\n    };\n\n    pub fn measure(self: *const ItemWithMetrics) Metrics {\n        return .{ .total_size = self.size };\n    }\n\n    pub fn empty() ItemWithMetrics {\n        return .{ .value = 0, .size = 0 };\n    }\n\n    pub fn is_empty(self: *const ItemWithMetrics) bool {\n        return self.value == 0 and self.size == 0;\n    }\n};\n\n//===== Basic Rope Tests =====\n\ntest \"Rope - can initialize with arena allocator\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n    try std.testing.expectEqual(@as(u32, 0), rope.count()); // Sentinel filtered\n}\n\ntest \"Rope - from_item creates single item rope\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 42 });\n\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n    const item = rope.get(0);\n    try std.testing.expect(item != null);\n    try std.testing.expectEqual(@as(u32, 42), item.?.value);\n}\n\ntest \"Rope - from_slice creates rope from multiple items\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);\n}\n\ntest \"Rope - get with out of bounds returns null\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try std.testing.expect(rope.get(100) == null);\n}\n\n//===== Insert Tests =====\n\ntest \"Rope - insert at beginning\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.insert(0, .{ .value = 0 });\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n    try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 1), rope.get(1).?.value);\n}\n\ntest \"Rope - insert at end\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.insert(1, .{ .value = 2 });\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n}\n\ntest \"Rope - multiple inserts\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    try rope.insert(0, .{ .value = 1 });\n    try rope.insert(1, .{ .value = 2 });\n    try rope.insert(2, .{ .value = 3 });\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count()); // Sentinel filtered\n}\n\n//===== Delete Tests =====\n\ntest \"Rope - delete at beginning\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete(0);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n    try std.testing.expectEqual(@as(u32, 2), rope.get(0).?.value);\n}\n\ntest \"Rope - delete in middle\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete(1);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(1).?.value);\n}\n\n//===== Walk Tests =====\n\ntest \"Rope - walk all items\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 10 },\n        .{ .value = 20 },\n        .{ .value = 30 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const Context = struct {\n        sum: u32 = 0,\n\n        fn walker(ctx: *anyopaque, data: *const SimpleItem, index: u32) RopeType.Node.WalkerResult {\n            _ = index;\n            const self = @as(*@This(), @ptrCast(@alignCast(ctx)));\n            self.sum += data.value;\n            return .{};\n        }\n    };\n\n    var ctx = Context{};\n    try rope.walk(&ctx, Context.walker);\n\n    try std.testing.expectEqual(@as(u32, 60), ctx.sum);\n}\n\ntest \"Rope - walk with early exit\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const Context = struct {\n        count: u32 = 0,\n\n        fn walker(ctx: *anyopaque, data: *const SimpleItem, index: u32) RopeType.Node.WalkerResult {\n            _ = index;\n            const self = @as(*@This(), @ptrCast(@alignCast(ctx)));\n            self.count += 1;\n            if (data.value == 2) {\n                return .{ .keep_walking = false };\n            }\n            return .{};\n        }\n    };\n\n    var ctx = Context{};\n    try rope.walk(&ctx, Context.walker);\n\n    try std.testing.expectEqual(@as(u32, 2), ctx.count);\n}\n\n//===== Metrics Tests =====\n\ntest \"Rope - custom metrics are tracked\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(ItemWithMetrics);\n    const items = [_]ItemWithMetrics{\n        .{ .value = 1, .size = 10 },\n        .{ .value = 2, .size = 20 },\n        .{ .value = 3, .size = 30 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const metrics = rope.root.metrics();\n    try std.testing.expectEqual(@as(u32, 3), metrics.count);\n    try std.testing.expectEqual(@as(u32, 60), metrics.custom.total_size);\n}\n\ntest \"Rope - rebalance maintains data\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var items: [20]SimpleItem = undefined;\n    for (&items, 0..) |*item, i| {\n        item.* = .{ .value = @intCast(i) };\n    }\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.rebalance(arena.allocator());\n\n    // Data should be preserved\n    try std.testing.expectEqual(@as(u32, 20), rope.count());\n    try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 10), rope.get(10).?.value);\n    try std.testing.expectEqual(@as(u32, 19), rope.get(19).?.value);\n}\n\n//===== Stress Tests =====\n\ntest \"Rope - large number of items\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var items: [100]SimpleItem = undefined;\n    for (&items, 0..) |*item, i| {\n        item.* = .{ .value = @intCast(i) };\n    }\n\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try std.testing.expectEqual(@as(u32, 100), rope.count());\n    try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 50), rope.get(50).?.value);\n    try std.testing.expectEqual(@as(u32, 99), rope.get(99).?.value);\n}\n\ntest \"Rope - many insert operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    for (0..50) |i| {\n        try rope.insert(@intCast(i), .{ .value = @intCast(i) });\n    }\n\n    try std.testing.expectEqual(@as(u32, 50), rope.count()); // Sentinel filtered\n}\n\n//===== Nested Rope Tests (Lines→Chunks Pattern) =====\n\n// Chunk type similar to what would be used in text buffer\nconst Chunk = struct {\n    data: []const u8,\n    width: u32,\n\n    pub const Metrics = struct {\n        total_width: u32 = 0,\n        total_bytes: u32 = 0,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.total_width += other.total_width;\n            self.total_bytes += other.total_bytes;\n        }\n    };\n\n    pub fn measure(self: *const Chunk) Metrics {\n        return .{\n            .total_width = self.width,\n            .total_bytes = @intCast(self.data.len),\n        };\n    }\n\n    pub fn empty() Chunk {\n        return .{ .data = \"\", .width = 0 };\n    }\n\n    pub fn is_empty(self: *const Chunk) bool {\n        return self.data.len == 0;\n    }\n};\n\n// Static empty chunk rope node for Line.empty()\nconst empty_chunk_leaf_node = rope_mod.Rope(Chunk).Node{\n    .leaf = .{\n        .data = Chunk.empty(),\n    },\n};\n\n// Line type containing a rope of chunks\nconst Line = struct {\n    chunks: rope_mod.Rope(Chunk),\n    line_id: u32,\n\n    pub const Metrics = struct {\n        total_width: u32 = 0,\n        total_lines: u32 = 1,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.total_width += other.total_width;\n            self.total_lines += other.total_lines;\n        }\n    };\n\n    pub fn measure(self: *const Line) Metrics {\n        const chunk_metrics = self.chunks.root.metrics();\n        return .{\n            .total_width = chunk_metrics.custom.total_width,\n            .total_lines = 1,\n        };\n    }\n\n    pub fn empty() Line {\n        // Use static empty chunk rope - safe because it's immutable\n        const ChunkRope = rope_mod.Rope(Chunk);\n        return .{\n            .chunks = .{\n                .root = &empty_chunk_leaf_node,\n                .allocator = undefined, // Never used for empty\n                .empty_leaf = &empty_chunk_leaf_node,\n                .marker_cache = ChunkRope.MarkerCache.init(undefined),\n            },\n            .line_id = 0,\n        };\n    }\n\n    pub fn is_empty(self: *const Line) bool {\n        return self.line_id == 0 and self.chunks.count() == 1;\n    }\n};\n\ntest \"Rope - replace item at index\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.replace(1, .{ .value = 20 });\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 20), rope.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);\n}\n\ntest \"Rope - append item\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.append(.{ .value = 2 });\n    try rope.append(.{ .value = 3 });\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);\n}\n\ntest \"Rope - prepend item\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 3 });\n\n    try rope.prepend(.{ .value = 2 });\n    try rope.prepend(.{ .value = 1 });\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);\n}\n\ntest \"Rope - concatenate two ropes\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n\n    const items1 = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    var rope1 = try RopeType.from_slice(arena.allocator(), &items1);\n\n    const items2 = [_]SimpleItem{\n        .{ .value = 3 },\n        .{ .value = 4 },\n    };\n    const rope2 = try RopeType.from_slice(arena.allocator(), &items2);\n\n    try rope1.concat(&rope2);\n\n    try std.testing.expectEqual(@as(u32, 4), rope1.count());\n    try std.testing.expectEqual(@as(u32, 1), rope1.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 2), rope1.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope1.get(2).?.value);\n    try std.testing.expectEqual(@as(u32, 4), rope1.get(3).?.value);\n}\n\n//===== Undo/Redo Tests =====\n\ntest \"Rope - basic undo operation\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.store_undo(\"initial\");\n\n    try rope.insert(1, .{ .value = 2 });\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n\n    const meta = try rope.undo(\"before undo\");\n    try std.testing.expectEqualStrings(\"initial\", meta);\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n}\n\ntest \"Rope - basic redo operation\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.store_undo(\"initial\");\n    try rope.insert(1, .{ .value = 2 });\n\n    _ = try rope.undo(\"before undo\");\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n\n    const meta = try rope.redo();\n    try std.testing.expectEqualStrings(\"before undo\", meta);\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n}\n\ntest \"Rope - multiple undo/redo operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.store_undo(\"state1\");\n    try rope.append(.{ .value = 2 });\n\n    try rope.store_undo(\"state2\");\n    try rope.append(.{ .value = 3 });\n\n    try rope.store_undo(\"state3\");\n    try rope.append(.{ .value = 4 });\n\n    try std.testing.expectEqual(@as(u32, 4), rope.count());\n\n    _ = try rope.undo(\"current\");\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n\n    _ = try rope.undo(\"current\");\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n\n    _ = try rope.redo();\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n}\n\ntest \"Rope - undo/redo with delete operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.store_undo(\"before delete\");\n    try rope.delete(1);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(1).?.value);\n\n    _ = try rope.undo(\"after delete\");\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n}\n\ntest \"Rope - undo/redo with replace operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 10 });\n\n    try rope.store_undo(\"original\");\n    try rope.replace(0, .{ .value = 20 });\n    try std.testing.expectEqual(@as(u32, 20), rope.get(0).?.value);\n\n    _ = try rope.undo(\"after replace\");\n    try std.testing.expectEqual(@as(u32, 10), rope.get(0).?.value);\n\n    _ = try rope.redo();\n    try std.testing.expectEqual(@as(u32, 20), rope.get(0).?.value);\n}\n\ntest \"Rope - can_undo and can_redo\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try std.testing.expect(!rope.can_undo());\n    try std.testing.expect(!rope.can_redo());\n\n    try rope.store_undo(\"state1\");\n    try std.testing.expect(rope.can_undo());\n    try std.testing.expect(!rope.can_redo());\n\n    _ = try rope.undo(\"current\");\n    try std.testing.expect(!rope.can_undo()); // No more undo (only one state)\n    try std.testing.expect(rope.can_redo());\n}\n\ntest \"Rope - clear history\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.store_undo(\"state1\");\n    try rope.append(.{ .value = 2 });\n    try rope.store_undo(\"state2\");\n\n    try std.testing.expect(rope.can_undo());\n\n    rope.clear_history();\n    try std.testing.expect(!rope.can_undo());\n    try std.testing.expect(!rope.can_redo());\n}\n\ntest \"Rope - undo fails when no history\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    // No history stored, undo should fail\n    const result = rope.undo(\"current\");\n    try std.testing.expectError(error.Stop, result);\n}\n\ntest \"Rope - redo fails when no redo history\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    // No redo history, redo should fail\n    const result = rope.redo();\n    try std.testing.expectError(error.Stop, result);\n}\n\ntest \"Rope - complex undo/redo workflow\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    // Build up a sequence of operations\n    try rope.store_undo(\"empty\");\n    try rope.insert(0, .{ .value = 1 });\n\n    try rope.store_undo(\"one item\");\n    try rope.append(.{ .value = 2 });\n\n    try rope.store_undo(\"two items\");\n    try rope.append(.{ .value = 3 });\n\n    try rope.store_undo(\"three items\");\n    try rope.delete(1); // Remove middle\n\n    // State: [1, 3]\n    try std.testing.expectEqual(@as(u32, 2), rope.count()); // Sentinel filtered\n\n    // Undo delete\n    _ = try rope.undo(\"current\");\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n\n    // Undo append\n    _ = try rope.undo(\"current\");\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n\n    // Redo append\n    _ = try rope.redo();\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n}\n\ntest \"Rope - undo/redo with metadata tracking\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.store_undo(\"insert operation\");\n    try rope.append(.{ .value = 2 });\n\n    try rope.store_undo(\"delete operation\");\n    try rope.delete(0);\n\n    // Undo and check metadata\n    const meta1 = try rope.undo(\"current state\");\n    try std.testing.expectEqualStrings(\"delete operation\", meta1);\n\n    const meta2 = try rope.undo(\"current state\");\n    try std.testing.expectEqualStrings(\"insert operation\", meta2);\n}\n\ntest \"Rope - undo invalidates redo after new operation\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.store_undo(\"state1\");\n    try rope.append(.{ .value = 2 });\n\n    try rope.store_undo(\"state2\");\n    try rope.append(.{ .value = 3 });\n\n    // Undo once\n    _ = try rope.undo(\"current\");\n    try std.testing.expect(rope.can_redo());\n\n    // Make a new change - this stores the old redo as a branch and clears redo\n    try rope.store_undo(\"new branch\");\n    try rope.append(.{ .value = 99 });\n\n    // Redo should NOT work anymore (it was saved as a branch)\n    try std.testing.expect(!rope.can_redo());\n\n    // But we can still undo\n    try std.testing.expect(rope.can_undo());\n}\n\ntest \"Rope - undo/redo with nested ropes\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const chunks1 = [_]Chunk{.{ .data = \"Line 1\", .width = 6 }};\n    const line1 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1),\n        .line_id = 1,\n    };\n\n    const RopeType = rope_mod.Rope(Line);\n    var rope = try RopeType.from_item(allocator, line1);\n\n    try rope.store_undo(\"before append\");\n\n    const chunks2 = [_]Chunk{.{ .data = \"Line 2\", .width = 6 }};\n    const line2 = Line{\n        .chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2),\n        .line_id = 2,\n    };\n    try rope.append(line2);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n\n    // Undo\n    _ = try rope.undo(\"after append\");\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n\n    // Redo\n    _ = try rope.redo();\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n}\n\ntest \"Rope - stress test undo/redo with many operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    // Perform 20 operations\n    for (0..20) |i| {\n        try rope.store_undo(\"operation\");\n        try rope.append(.{ .value = @intCast(i) });\n    }\n\n    try std.testing.expectEqual(@as(u32, 20), rope.count()); // Sentinel filtered\n\n    // Undo 10 operations\n    for (0..10) |_| {\n        _ = try rope.undo(\"current\");\n    }\n    try std.testing.expectEqual(@as(u32, 10), rope.count());\n\n    // Redo 5 operations\n    for (0..5) |_| {\n        _ = try rope.redo();\n    }\n    try std.testing.expectEqual(@as(u32, 15), rope.count());\n}\n\n//===== Bulk/Range Operations Tests =====\n\ntest \"Rope - split at beginning\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const right = try rope.split(0);\n\n    try std.testing.expectEqual(@as(u32, 0), rope.count()); // Sentinel filtered\n    try std.testing.expectEqual(@as(u32, 3), right.count());\n    try std.testing.expectEqual(@as(u32, 1), right.get(0).?.value);\n}\n\ntest \"Rope - split at middle\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n        .{ .value = 4 },\n        .{ .value = 5 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const right = try rope.split(2);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n\n    try std.testing.expectEqual(@as(u32, 3), right.count());\n    try std.testing.expectEqual(@as(u32, 3), right.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 4), right.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 5), right.get(2).?.value);\n}\n\ntest \"Rope - split at end\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const right = try rope.split(3);\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n    try std.testing.expectEqual(@as(u32, 0), right.count()); // Sentinel filtered\n}\n\ntest \"Rope - slice full range\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const sliced = try rope.slice(0, 3, arena.allocator());\n    defer arena.allocator().free(sliced);\n\n    try std.testing.expectEqual(@as(usize, 3), sliced.len);\n    try std.testing.expectEqual(@as(u32, 1), sliced[0].value);\n    try std.testing.expectEqual(@as(u32, 2), sliced[1].value);\n    try std.testing.expectEqual(@as(u32, 3), sliced[2].value);\n}\n\ntest \"Rope - slice partial range\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 10 },\n        .{ .value = 20 },\n        .{ .value = 30 },\n        .{ .value = 40 },\n        .{ .value = 50 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const sliced = try rope.slice(1, 4, arena.allocator());\n    defer arena.allocator().free(sliced);\n\n    try std.testing.expectEqual(@as(usize, 3), sliced.len);\n    try std.testing.expectEqual(@as(u32, 20), sliced[0].value);\n    try std.testing.expectEqual(@as(u32, 30), sliced[1].value);\n    try std.testing.expectEqual(@as(u32, 40), sliced[2].value);\n}\n\ntest \"Rope - slice empty range\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const sliced = try rope.slice(1, 1, arena.allocator());\n    defer arena.allocator().free(sliced);\n\n    try std.testing.expectEqual(@as(usize, 0), sliced.len);\n}\n\ntest \"Rope - delete_range at beginning\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n        .{ .value = 4 },\n        .{ .value = 5 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete_range(0, 2);\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n    try std.testing.expectEqual(@as(u32, 3), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 4), rope.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 5), rope.get(2).?.value);\n}\n\ntest \"Rope - delete_range in middle\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n        .{ .value = 4 },\n        .{ .value = 5 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete_range(1, 4);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 5), rope.get(1).?.value);\n}\n\ntest \"Rope - delete_range at end\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete_range(1, 3);\n\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n}\n\ntest \"Rope - delete_range empty (same indices)\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete_range(1, 1);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n}\n\ntest \"Rope - insert_slice at beginning\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 3 });\n\n    const to_insert = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    try rope.insert_slice(0, &to_insert);\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);\n}\n\ntest \"Rope - insert_slice in middle\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 4 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const to_insert = [_]SimpleItem{\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    try rope.insert_slice(1, &to_insert);\n\n    try std.testing.expectEqual(@as(u32, 4), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);\n    try std.testing.expectEqual(@as(u32, 4), rope.get(3).?.value);\n}\n\ntest \"Rope - insert_slice at end\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const to_insert = [_]SimpleItem{\n        .{ .value = 3 },\n        .{ .value = 4 },\n    };\n    try rope.insert_slice(2, &to_insert);\n\n    try std.testing.expectEqual(@as(u32, 4), rope.count());\n    try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);\n    try std.testing.expectEqual(@as(u32, 4), rope.get(3).?.value);\n}\n\ntest \"Rope - insert_slice empty array\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    const to_insert: []const SimpleItem = &[_]SimpleItem{};\n    try rope.insert_slice(0, to_insert);\n\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n}\n\ntest \"Rope - to_array with simple items\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 10 },\n        .{ .value = 20 },\n        .{ .value = 30 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const array = try rope.to_array(arena.allocator());\n    defer arena.allocator().free(array);\n\n    try std.testing.expectEqual(@as(usize, 3), array.len);\n    try std.testing.expectEqual(@as(u32, 10), array[0].value);\n    try std.testing.expectEqual(@as(u32, 20), array[1].value);\n    try std.testing.expectEqual(@as(u32, 30), array[2].value);\n}\n\ntest \"Rope - to_array empty rope\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    const array = try rope.to_array(arena.allocator());\n    defer arena.allocator().free(array);\n\n    try std.testing.expectEqual(@as(usize, 0), array.len); // Sentinel filtered\n}\n\ntest \"Rope - combined bulk operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n        .{ .value = 4 },\n        .{ .value = 5 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete_range(2, 4);\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n\n    const to_insert = [_]SimpleItem{\n        .{ .value = 30 },\n        .{ .value = 40 },\n    };\n    try rope.insert_slice(1, &to_insert);\n    try std.testing.expectEqual(@as(u32, 5), rope.count());\n\n    const array = try rope.to_array(arena.allocator());\n    defer arena.allocator().free(array);\n\n    try std.testing.expectEqual(@as(u32, 1), array[0].value);\n    try std.testing.expectEqual(@as(u32, 30), array[1].value);\n    try std.testing.expectEqual(@as(u32, 40), array[2].value);\n    try std.testing.expectEqual(@as(u32, 2), array[3].value);\n    try std.testing.expectEqual(@as(u32, 5), array[4].value);\n}\n\ntest \"Rope - undo/redo with bulk operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    // Store state\n    try rope.store_undo(\"before bulk\");\n\n    // Bulk insert\n    const to_insert = [_]SimpleItem{\n        .{ .value = 10 },\n        .{ .value = 20 },\n    };\n    try rope.insert_slice(1, &to_insert);\n    try std.testing.expectEqual(@as(u32, 5), rope.count());\n\n    // Undo\n    _ = try rope.undo(\"after bulk\");\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n\n    // Redo\n    _ = try rope.redo();\n    try std.testing.expectEqual(@as(u32, 5), rope.count());\n}\n\n//===== Edge Case Tests =====\n\ntest \"Rope - slice with start > end returns empty\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const sliced = try rope.slice(2, 1, arena.allocator());\n    defer arena.allocator().free(sliced);\n\n    try std.testing.expectEqual(@as(usize, 0), sliced.len);\n}\n\ntest \"Rope - slice beyond bounds\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    // Should only get items that exist\n    const sliced = try rope.slice(0, 100, arena.allocator());\n    defer arena.allocator().free(sliced);\n\n    try std.testing.expectEqual(@as(usize, 2), sliced.len);\n}\n\ntest \"Rope - delete_range with start > end does nothing\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete_range(2, 1);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n}\n\ntest \"Rope - insert_slice beyond count appends\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    const to_insert = [_]SimpleItem{\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    try rope.insert_slice(100, &to_insert);\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n    try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);\n    try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);\n}\n\ntest \"Rope - replace at out of bounds does nothing\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.replace(100, .{ .value = 999 });\n\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n    try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);\n}\n\ntest \"Rope - delete at out of bounds\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    // This should handle gracefully (delete beyond bounds)\n    try rope.delete(100);\n\n    // Count unchanged\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n}\n\ntest \"Rope - split at zero creates empty left\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const right = try rope.split(0);\n\n    try std.testing.expectEqual(@as(u32, 0), rope.count()); // Sentinel filtered\n    try std.testing.expectEqual(@as(u32, 2), right.count());\n}\n\ntest \"Rope - split beyond count\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const right = try rope.split(100);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.count());\n    try std.testing.expectEqual(@as(u32, 0), right.count()); // Sentinel filtered\n}\n\ntest \"Rope - multiple undo without operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.store_undo(\"state1\");\n    try rope.store_undo(\"state2\");\n\n    // Two undos back to back\n    _ = try rope.undo(\"current\");\n    _ = try rope.undo(\"current\");\n\n    // Should fail on third\n    const result = rope.undo(\"current\");\n    try std.testing.expectError(error.Stop, result);\n}\n\ntest \"Rope - stress test with 1000 items\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var items: [1000]SimpleItem = undefined;\n    for (&items, 0..) |*item, i| {\n        item.* = .{ .value = @intCast(i) };\n    }\n\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try std.testing.expectEqual(@as(u32, 1000), rope.count());\n    try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value);\n    try std.testing.expectEqual(@as(u32, 500), rope.get(500).?.value);\n    try std.testing.expectEqual(@as(u32, 999), rope.get(999).?.value);\n\n    const depth = rope.root.depth();\n    try std.testing.expect(depth < 20); // log2(1000) ≈ 10, allow some slack\n}\n\ntest \"Rope - delete_range entire rope\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try rope.delete_range(0, 3);\n\n    // Should be empty (sentinel filtered)\n    try std.testing.expectEqual(@as(u32, 0), rope.count());\n}\n\ntest \"Rope - to_array single item\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 42 });\n\n    const array = try rope.to_array(arena.allocator());\n    defer arena.allocator().free(array);\n\n    try std.testing.expectEqual(@as(usize, 1), array.len);\n    try std.testing.expectEqual(@as(u32, 42), array[0].value);\n}\n\ntest \"Rope - concat with empty rope\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope1 = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n    const rope2 = try RopeType.init(arena.allocator());\n\n    try rope1.concat(&rope2);\n\n    try std.testing.expectEqual(@as(u32, 1), rope1.count()); // original only (empty filtered)\n}\n\ntest \"Rope - redo after modifying tree fails\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    try rope.store_undo(\"state1\");\n    try rope.append(.{ .value = 2 });\n\n    _ = try rope.undo(\"current\");\n\n    // Manually modify the rope (breaking the redo contract)\n    try rope.append(.{ .value = 3 });\n\n    // Redo should fail because tree was modified\n    const result = rope.redo();\n    try std.testing.expectError(error.Stop, result);\n}\n\ntest \"Rope - rebalance extremely unbalanced tree\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    for (0..50) |i| {\n        try rope.append(.{ .value = @intCast(i) });\n    }\n\n    const depth_before = rope.root.depth();\n\n    // Rebalance\n    try rope.rebalance(arena.allocator());\n\n    const depth_after = rope.root.depth();\n\n    // Should be more balanced now\n    try std.testing.expect(depth_after <= depth_before);\n    try std.testing.expect(depth_after < 15); // log2(50) ≈ 6\n\n    // Data should be preserved\n    try std.testing.expectEqual(@as(u32, 50), rope.count());\n    try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value); // Fixed index\n}\n\n//===== Weight-aware Tests =====\n\n// Type with custom weight for testing weight-based operations\nconst WeightedItem = struct {\n    value: u32,\n    weight: u32,\n\n    pub const Metrics = struct {\n        total_weight: u32 = 0,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.total_weight += other.total_weight;\n        }\n\n        pub fn weight(self: *const Metrics) u32 {\n            return self.total_weight;\n        }\n    };\n\n    pub fn measure(self: *const WeightedItem) Metrics {\n        return .{ .total_weight = self.weight };\n    }\n\n    pub fn empty() WeightedItem {\n        return .{ .value = 0, .weight = 0 };\n    }\n\n    pub fn is_empty(self: *const WeightedItem) bool {\n        return self.value == 0 and self.weight == 0;\n    }\n};\n\n// Leaf split function for testing (callback format)\nconst WeightedRope = rope_mod.Rope(WeightedItem);\n\nfn splitWeightedItemCallback(\n    ctx: ?*anyopaque,\n    allocator: std.mem.Allocator,\n    leaf: *const WeightedItem,\n    weight_in_leaf: u32,\n) error{ OutOfBounds, OutOfMemory }!WeightedRope.Node.LeafSplitResult {\n    _ = ctx;\n    _ = allocator;\n    if (weight_in_leaf == 0) {\n        return .{\n            .left = WeightedItem.empty(),\n            .right = leaf.*,\n        };\n    } else if (weight_in_leaf >= leaf.weight) {\n        return .{\n            .left = leaf.*,\n            .right = WeightedItem.empty(),\n        };\n    }\n\n    // Split proportionally\n    return .{\n        .left = .{ .value = leaf.value, .weight = weight_in_leaf },\n        .right = .{ .value = leaf.value + 1000, .weight = leaf.weight - weight_in_leaf },\n    };\n}\n\n// Helper to create the callback struct\nfn makeWeightedSplitter() WeightedRope.Node.LeafSplitFn {\n    return .{\n        .ctx = null,\n        .splitFn = splitWeightedItemCallback,\n    };\n}\n\ntest \"Rope - totalWeight returns correct weight\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const items = [_]WeightedItem{\n        .{ .value = 1, .weight = 10 },\n        .{ .value = 2, .weight = 20 },\n        .{ .value = 3, .weight = 30 },\n    };\n\n    var rope = try WeightedRope.from_slice(arena.allocator(), &items);\n    try std.testing.expectEqual(@as(u32, 60), rope.totalWeight());\n}\n\ntest \"Rope - split_at_weight at boundary\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const items = [_]WeightedItem{\n        .{ .value = 1, .weight = 10 },\n        .{ .value = 2, .weight = 20 },\n        .{ .value = 3, .weight = 30 },\n    };\n\n    const rope = try WeightedRope.from_slice(arena.allocator(), &items);\n\n    // Split at weight 30 (boundary between second and third item)\n    const splitter = makeWeightedSplitter();\n    const result = try WeightedRope.Node.split_at_weight(rope.root, 30, arena.allocator(), rope.empty_leaf, &splitter);\n\n    // Left should have weight 30 (first two items)\n    try std.testing.expectEqual(@as(u32, 30), result.left.metrics().weight());\n\n    // Right should have weight 30 (third item)\n    try std.testing.expectEqual(@as(u32, 30), result.right.metrics().weight());\n}\n\ntest \"Rope - split_at_weight inside leaf\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const rope = try WeightedRope.from_item(arena.allocator(), .{ .value = 1, .weight = 100 });\n\n    // Split at weight 40 (inside the single leaf)\n    const splitter = makeWeightedSplitter();\n    const result = try WeightedRope.Node.split_at_weight(rope.root, 40, arena.allocator(), rope.empty_leaf, &splitter);\n\n    // Left should have weight 40\n    try std.testing.expectEqual(@as(u32, 40), result.left.metrics().weight());\n\n    // Right should have weight 60\n    try std.testing.expectEqual(@as(u32, 60), result.right.metrics().weight());\n}\n\ntest \"Rope - splitByWeight\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const items = [_]WeightedItem{\n        .{ .value = 1, .weight = 10 },\n        .{ .value = 2, .weight = 20 },\n        .{ .value = 3, .weight = 30 },\n    };\n\n    var rope = try WeightedRope.from_slice(arena.allocator(), &items);\n    try std.testing.expectEqual(@as(u32, 60), rope.totalWeight());\n\n    // Split at weight 30\n    const splitter = makeWeightedSplitter();\n    const right_half = try rope.splitByWeight(30, &splitter);\n\n    // Left half should have weight 30\n    try std.testing.expectEqual(@as(u32, 30), rope.totalWeight());\n\n    // Right half should have weight 30\n    try std.testing.expectEqual(@as(u32, 30), right_half.totalWeight());\n}\n\ntest \"Rope - deleteRangeByWeight\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const items = [_]WeightedItem{\n        .{ .value = 1, .weight = 10 },\n        .{ .value = 2, .weight = 20 },\n        .{ .value = 3, .weight = 30 },\n        .{ .value = 4, .weight = 40 },\n    };\n\n    var rope = try WeightedRope.from_slice(arena.allocator(), &items);\n    try std.testing.expectEqual(@as(u32, 100), rope.totalWeight());\n\n    // Delete weight range [10, 30) - removes the second item (weight 20)\n    const splitter = makeWeightedSplitter();\n    try rope.deleteRangeByWeight(10, 30, &splitter);\n\n    // Should have removed weight 20\n    try std.testing.expectEqual(@as(u32, 80), rope.totalWeight());\n}\n\ntest \"Rope - insertSliceByWeight\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const items = [_]WeightedItem{\n        .{ .value = 1, .weight = 10 },\n        .{ .value = 3, .weight = 30 },\n    };\n\n    var rope = try WeightedRope.from_slice(arena.allocator(), &items);\n    try std.testing.expectEqual(@as(u32, 40), rope.totalWeight());\n\n    // Insert at weight 10 (after first item)\n    const insert_items = [_]WeightedItem{\n        .{ .value = 2, .weight = 20 },\n    };\n    const splitter = makeWeightedSplitter();\n    try rope.insertSliceByWeight(10, &insert_items, &splitter);\n\n    // Should have added weight 20\n    try std.testing.expectEqual(@as(u32, 60), rope.totalWeight());\n}\n\ntest \"Rope - findByWeight\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n    const items = [_]WeightedItem{\n        .{ .value = 1, .weight = 10 },\n        .{ .value = 2, .weight = 20 },\n        .{ .value = 3, .weight = 30 },\n    };\n\n    var rope = try WeightedRope.from_slice(arena.allocator(), &items);\n\n    // Find leaf containing weight 0 (first item)\n    const result0 = rope.findByWeight(0);\n    try std.testing.expect(result0 != null);\n    try std.testing.expectEqual(@as(u32, 1), result0.?.leaf.value);\n    try std.testing.expectEqual(@as(u32, 0), result0.?.start_weight);\n\n    // Find leaf containing weight 15 (second item)\n    const result15 = rope.findByWeight(15);\n    try std.testing.expect(result15 != null);\n    try std.testing.expectEqual(@as(u32, 2), result15.?.leaf.value);\n    try std.testing.expectEqual(@as(u32, 10), result15.?.start_weight);\n\n    // Find leaf containing weight 35 (third item)\n    const result35 = rope.findByWeight(35);\n    try std.testing.expect(result35 != null);\n    try std.testing.expectEqual(@as(u32, 3), result35.?.leaf.value);\n    try std.testing.expectEqual(@as(u32, 30), result35.?.start_weight);\n\n    // Out of bounds\n    const result100 = rope.findByWeight(100);\n    try std.testing.expect(result100 == null);\n}\n\n//===== Integrated Marker Tracking Tests (Union Types) =====\n\n// Simple union type for testing automatic marker tracking\nconst TokenType = union(enum) {\n    word: u32,\n    space: u32,\n    newline: void, // Marker type\n\n    // Define which tags are markers (only track these!)\n    pub const MarkerTypes = &[_]std.meta.Tag(TokenType){.newline};\n\n    pub const Metrics = struct {\n        width: u32 = 0,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.width += other.width;\n        }\n\n        pub fn weight(self: *const Metrics) u32 {\n            return self.width;\n        }\n    };\n\n    pub fn measure(self: *const TokenType) Metrics {\n        return switch (self.*) {\n            .word => |w| .{ .width = w },\n            .space => |s| .{ .width = s },\n            .newline => .{ .width = 0 },\n        };\n    }\n\n    pub fn empty() TokenType {\n        return .{ .space = 0 };\n    }\n\n    pub fn is_empty(self: *const TokenType) bool {\n        return switch (self.*) {\n            .space => |s| s == 0,\n            else => false,\n        };\n    }\n};\n\ntest \"Rope - automatic marker tracking with union type\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n\n    // Create rope with marker tracking enabled\n    const tokens = [_]TokenType{\n        .{ .word = 5 }, // \"Hello\"\n        .{ .space = 1 }, // \" \"\n        .{ .word = 5 }, // \"World\"\n        .{ .newline = {} }, // Line break marker\n        .{ .word = 6 }, // \"Second\"\n        .{ .space = 1 }, // \" \"\n        .{ .word = 4 }, // \"Line\"\n        .{ .newline = {} }, // Line break marker\n        .{ .word = 5 }, // \"Third\"\n    };\n\n    var rope = try RopeType.from_slice(arena.allocator(), &tokens);\n\n    // O(1) lookup: find newline markers (only .newline is tracked, not .word or .space)\n    try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));\n\n    // Get first newline (end of line 0)\n    const nl0 = rope.getMarker(.newline, 0);\n    try std.testing.expect(nl0 != null);\n    try std.testing.expectEqual(@as(u32, 3), nl0.?.leaf_index); // After word, space, word\n    try std.testing.expectEqual(@as(u32, 11), nl0.?.global_weight); // 5 + 1 + 5\n\n    // Get second newline (end of line 1)\n    const nl1 = rope.getMarker(.newline, 1);\n    try std.testing.expect(nl1 != null);\n    try std.testing.expectEqual(@as(u32, 7), nl1.?.leaf_index);\n    try std.testing.expectEqual(@as(u32, 22), nl1.?.global_weight); // 11 + 6 + 1 + 4\n\n    // Word and space are NOT markers - should return 0\n    try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.word));\n    try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.space));\n}\n\ntest \"Rope - marker tracking with empty rope\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n    var rope = try RopeType.init(arena.allocator());\n\n    try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.newline));\n    try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.word)); // Not a marker type\n    try std.testing.expect(rope.getMarker(.newline, 0) == null);\n}\n\ntest \"Rope - marker tracking requires rebuild\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n    const tokens = [_]TokenType{\n        .{ .word = 5 },\n        .{ .newline = {} },\n    };\n\n    var rope = try RopeType.from_slice(arena.allocator(), &tokens);\n\n    // Markers are automatically tracked in the tree\n    try std.testing.expectEqual(@as(u32, 1), rope.markerCount(.newline));\n    try std.testing.expect(rope.getMarker(.newline, 0) != null);\n}\n\ntest \"Rope - marker tracking with many markers\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n\n    // Create 100 lines\n    var tokens_array: [199]TokenType = undefined; // 100 words + 99 newlines\n    for (0..100) |i| {\n        if (i > 0) {\n            tokens_array[i * 2 - 1] = .{ .newline = {} };\n        }\n        tokens_array[i * 2] = .{ .word = 5 };\n    }\n\n    var rope = try RopeType.from_slice(arena.allocator(), &tokens_array);\n\n    // Should have 99 newlines (only newlines are tracked as markers)\n    try std.testing.expectEqual(@as(u32, 99), rope.markerCount(.newline));\n\n    // Test O(1) random access to specific lines\n    const nl50 = rope.getMarker(.newline, 50).?;\n    try std.testing.expectEqual(@as(u32, 101), nl50.leaf_index); // word, nl, word, nl, ... (50th newline is at index 101)\n    try std.testing.expectEqual(@as(u32, 255), nl50.global_weight); // 51 words * 5 width\n\n    const nl98 = rope.getMarker(.newline, 98).?;\n    try std.testing.expectEqual(@as(u32, 197), nl98.leaf_index);\n}\n//===== Debug toText Tests =====\n\ntest \"Rope - toText shows basic structure\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const debug_text = try rope.toText(arena.allocator());\n\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"[root\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"branch\") != null or std.mem.indexOf(u8, debug_text, \"leaf\") != null);\n}\n\ntest \"Rope - toText shows empty rope\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    const debug_text = try rope.toText(arena.allocator());\n\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"[root\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"[empty]\") != null);\n}\n\ntest \"Rope - toText with union type shows tags\" {\n    const TestSegment = union(enum) {\n        text: struct { width: u32 },\n        brk: void,\n        linestart: void,\n\n        pub const MarkerTypes = &[_]std.meta.Tag(@This()){ .brk, .linestart };\n\n        pub const Metrics = struct {\n            width: u32 = 0,\n\n            pub fn add(self: *Metrics, other: Metrics) void {\n                self.width += other.width;\n            }\n\n            pub fn weight(self: *const Metrics) u32 {\n                return self.width;\n            }\n        };\n\n        pub fn measure(self: *const @This()) Metrics {\n            return switch (self.*) {\n                .text => |t| Metrics{ .width = t.width },\n                .brk => Metrics{ .width = 1 },\n                .linestart => Metrics{ .width = 0 },\n            };\n        }\n\n        pub fn empty() @This() {\n            return .{ .text = .{ .width = 0 } };\n        }\n\n        pub fn is_empty(self: *const @This()) bool {\n            return switch (self.*) {\n                .text => |t| t.width == 0,\n                else => false,\n            };\n        }\n    };\n\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const TestRope = rope_mod.Rope(TestSegment);\n    var rope = try TestRope.from_slice(arena.allocator(), &[_]TestSegment{\n        .linestart,\n        .{ .text = .{ .width = 5 } },\n        .brk,\n        .linestart,\n        .{ .text = .{ .width = 10 } },\n    });\n\n    const debug_text = try rope.toText(arena.allocator());\n\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"text\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"brk\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"linestart\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"w5\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"w10\") != null);\n}\n\ntest \"Rope - toText with nested structure\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n\n    // Create a larger rope that will have branches\n    var items: [10]SimpleItem = undefined;\n    for (&items, 0..) |*item, i| {\n        item.* = .{ .value = @intCast(i) };\n    }\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const debug_text = try rope.toText(arena.allocator());\n\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"[root\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"[branch\") != null);\n}\n\ntest \"Rope - toText after insertions shows updated structure\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    const before = try rope.toText(arena.allocator());\n    try std.testing.expect(std.mem.indexOf(u8, before, \"[root\") != null);\n\n    try rope.append(.{ .value = 2 });\n    try rope.append(.{ .value = 3 });\n\n    const after = try rope.toText(arena.allocator());\n    try std.testing.expect(std.mem.indexOf(u8, after, \"[root\") != null);\n    try std.testing.expect(after.len >= before.len);\n}\n\ntest \"Rope - toText with custom metrics shows width info\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(ItemWithMetrics);\n    const items = [_]ItemWithMetrics{\n        .{ .value = 1, .size = 100 },\n        .{ .value = 2, .size = 200 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    const debug_text = try rope.toText(arena.allocator());\n\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"[root\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"w\") != null);\n}\n\ntest \"Rope - toText shows single leaf\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 42 });\n\n    const debug_text = try rope.toText(arena.allocator());\n\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"[root\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"[leaf\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, debug_text, \"]\") != null);\n}\n\ntest \"Rope - marker cache MUST update after delete operations\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n\n    // Create: word(5) newline word(5) newline word(5)\n    // 3 lines total, 2 newlines\n    const tokens = [_]TokenType{\n        .{ .word = 5 },\n        .{ .newline = {} },\n        .{ .word = 5 },\n        .{ .newline = {} },\n        .{ .word = 5 },\n    };\n\n    var rope = try RopeType.from_slice(arena.allocator(), &tokens);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));\n\n    try rope.delete(4);\n\n    try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));\n\n    // The critical test: marker positions MUST be correct after delete!\n    const nl1_after = rope.getMarker(.newline, 1);\n    try std.testing.expect(nl1_after != null);\n\n    // After deleting the last word at index 4, the second newline should be at index 3\n    // (was at index 3 before, stays at 3 after deleting index 4)\n    try std.testing.expectEqual(@as(u32, 3), nl1_after.?.leaf_index);\n}\n\ntest \"Rope - marker cache MUST update after undo\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n\n    // Create: word(10) newline word(5)\n    const tokens = [_]TokenType{\n        .{ .word = 10 },\n        .{ .newline = {} },\n        .{ .word = 5 },\n    };\n\n    var rope = try RopeType.from_slice(arena.allocator(), &tokens);\n\n    // Initial state: 1 newline at weight 10\n    const nl_before = rope.getMarker(.newline, 0);\n    try std.testing.expect(nl_before != null);\n    try std.testing.expectEqual(@as(u32, 10), nl_before.?.global_weight);\n\n    // Store undo point\n    try rope.store_undo(\"before delete\");\n\n    // Delete part of first word: delete range [0, 1) removes first word\n    try rope.delete_range(0, 1);\n\n    // After delete: newline should be at weight 0 (no word before it)\n    const nl_after_delete = rope.getMarker(.newline, 0);\n    try std.testing.expect(nl_after_delete != null);\n    try std.testing.expectEqual(@as(u32, 0), nl_after_delete.?.global_weight);\n\n    // Undo the delete\n    _ = try rope.undo(\"after delete\");\n\n    // CRITICAL: After undo, marker cache MUST be recalculated!\n    // Newline should be back at weight 10\n    const nl_after_undo = rope.getMarker(.newline, 0);\n    try std.testing.expect(nl_after_undo != null);\n    try std.testing.expectEqual(@as(u32, 10), nl_after_undo.?.global_weight);\n}\n\ntest \"Rope - marker cache MUST update after redo\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n\n    const tokens = [_]TokenType{\n        .{ .word = 10 },\n        .{ .newline = {} },\n        .{ .word = 5 },\n    };\n\n    var rope = try RopeType.from_slice(arena.allocator(), &tokens);\n\n    try rope.store_undo(\"initial\");\n    try rope.delete_range(0, 1);\n\n    const nl_after_delete = rope.getMarker(.newline, 0);\n    try std.testing.expectEqual(@as(u32, 0), nl_after_delete.?.global_weight);\n\n    // Undo\n    _ = try rope.undo(\"after delete\");\n    const nl_after_undo = rope.getMarker(.newline, 0);\n    try std.testing.expectEqual(@as(u32, 10), nl_after_undo.?.global_weight);\n\n    // Redo\n    _ = try rope.redo();\n\n    // CRITICAL: After redo, marker cache MUST be recalculated!\n    const nl_after_redo = rope.getMarker(.newline, 0);\n    try std.testing.expect(nl_after_redo != null);\n    try std.testing.expectEqual(@as(u32, 0), nl_after_redo.?.global_weight);\n}\n\ntest \"Rope - marker cache survives multiple undo/redo cycles\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n\n    var rope = try RopeType.from_slice(arena.allocator(), &[_]TokenType{\n        .{ .word = 5 },\n        .{ .newline = {} },\n        .{ .word = 5 },\n    });\n\n    try rope.store_undo(\"state1\");\n    try rope.append(.{ .newline = {} });\n    try rope.append(.{ .word = 5 });\n\n    // Should have 2 newlines now\n    try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));\n    const nl1_orig = rope.getMarker(.newline, 1);\n    try std.testing.expectEqual(@as(u32, 10), nl1_orig.?.global_weight);\n\n    try rope.store_undo(\"state2\");\n    try rope.delete(0); // Delete first word\n\n    // Markers should update: first newline now at weight 0\n    const nl0_after_delete = rope.getMarker(.newline, 0);\n    try std.testing.expectEqual(@as(u32, 0), nl0_after_delete.?.global_weight);\n\n    // Undo twice\n    _ = try rope.undo(\"current\");\n    _ = try rope.undo(\"current\");\n\n    // Back to original: 1 newline at weight 5\n    try std.testing.expectEqual(@as(u32, 1), rope.markerCount(.newline));\n    const nl_final = rope.getMarker(.newline, 0);\n    try std.testing.expectEqual(@as(u32, 5), nl_final.?.global_weight);\n\n    // Redo twice\n    _ = try rope.redo();\n    _ = try rope.redo();\n\n    // Should match the post-delete state\n    const nl0_redo = rope.getMarker(.newline, 0);\n    try std.testing.expectEqual(@as(u32, 0), nl0_redo.?.global_weight);\n}\n\n//===== Configurable Undo Depth Tests =====\n\ntest \"Rope - weight-based balancing with custom weight function\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    // Create items with different sizes\n    const items = [_]WeightedItem{\n        .{ .value = 1, .weight = 100 }, // Large item\n        .{ .value = 2, .weight = 10 }, // Small item\n        .{ .value = 3, .weight = 200 }, // Very large item\n        .{ .value = 4, .weight = 50 }, // Medium item\n    };\n\n    var rope = try WeightedRope.from_slice(arena.allocator(), &items);\n\n    // Check that metrics are tracked\n    const metrics = rope.root.metrics();\n    try std.testing.expectEqual(@as(u32, 4), metrics.count);\n    try std.testing.expectEqual(@as(u32, 360), metrics.custom.total_weight);\n    try std.testing.expectEqual(@as(u32, 360), metrics.weight()); // Should use weight()\n}\n\ntest \"Rope - unlimited undo depth by default\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    // Store many undo states\n    for (0..100) |i| {\n        try rope.store_undo(\"state\");\n        try rope.append(.{ .value = @intCast(i) });\n    }\n\n    // Should have all 100 states\n    try std.testing.expectEqual(@as(usize, 100), rope.undo_depth);\n    try std.testing.expect(rope.can_undo());\n}\n\ntest \"Rope - max_undo_depth limits history\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.initWithConfig(arena.allocator(), .{ .max_undo_depth = 10 });\n\n    // Store 20 undo states\n    for (0..20) |i| {\n        try rope.store_undo(\"state\");\n        try rope.append(.{ .value = @intCast(i) });\n    }\n\n    // Should only keep 10 states\n    try std.testing.expectEqual(@as(usize, 10), rope.undo_depth);\n    try std.testing.expect(rope.can_undo());\n\n    // Can undo at most 10 times (may be less due to how history works)\n    var undo_count: usize = 0;\n    while (rope.can_undo()) : (undo_count += 1) {\n        _ = rope.undo(\"current\") catch break;\n    }\n    // Should have undone at least some operations, but not more than 10\n    try std.testing.expect(undo_count > 0);\n    try std.testing.expect(undo_count <= 10);\n}\n\ntest \"Rope - trimUndoHistory works correctly\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.initWithConfig(arena.allocator(), .{ .max_undo_depth = 5 });\n\n    for (0..10) |i| {\n        try rope.store_undo(\"state\");\n        try rope.append(.{ .value = @intCast(i) });\n\n        try std.testing.expect(rope.undo_depth <= 5);\n    }\n\n    try std.testing.expectEqual(@as(usize, 5), rope.undo_depth);\n}\n\ntest \"Rope - weight-based join_balanced respects weight ratio\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const left_items = [_]WeightedItem{\n        .{ .value = 1, .weight = 1000 },\n        .{ .value = 2, .weight = 1000 },\n        .{ .value = 3, .weight = 1000 },\n    };\n    var rope_left = try WeightedRope.from_slice(arena.allocator(), &left_items);\n\n    const right_items = [_]WeightedItem{\n        .{ .value = 4, .weight = 100 },\n    };\n    const rope_right = try WeightedRope.from_slice(arena.allocator(), &right_items);\n\n    try rope_left.concat(&rope_right);\n\n    try std.testing.expectEqual(@as(u32, 4), rope_left.count());\n\n    try std.testing.expect(rope_left.root.is_balanced());\n}\n\ntest \"Rope - integration weight-based balancing with history limits\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    var rope = try WeightedRope.initWithConfig(arena.allocator(), .{ .max_undo_depth = 5 });\n\n    var expected_count: u32 = 0;\n    for (0..10) |i| {\n        try rope.store_undo(\"insert\");\n        try rope.append(.{\n            .value = @intCast(i),\n            .weight = @intCast((i + 1) * 10),\n        });\n        expected_count += 1;\n    }\n\n    try std.testing.expectEqual(expected_count, rope.count());\n\n    try rope.insert(5, .{ .value = 999, .weight = 50 });\n    expected_count += 1;\n\n    try std.testing.expectEqual(expected_count, rope.count());\n\n    try std.testing.expect(rope.undo_depth <= 5);\n\n    try std.testing.expect(rope.root.is_balanced());\n}\n\ntest \"Rope - clear removes all items\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    const items = [_]SimpleItem{\n        .{ .value = 1 },\n        .{ .value = 2 },\n        .{ .value = 3 },\n    };\n    var rope = try RopeType.from_slice(arena.allocator(), &items);\n\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n\n    rope.clear();\n\n    try std.testing.expectEqual(@as(u32, 0), rope.count());\n}\n\ntest \"Rope - clear on empty rope\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.init(arena.allocator());\n\n    try std.testing.expectEqual(@as(u32, 0), rope.count());\n\n    rope.clear();\n\n    try std.testing.expectEqual(@as(u32, 0), rope.count());\n}\n\ntest \"Rope - clear then insert works\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });\n\n    rope.clear();\n    try std.testing.expectEqual(@as(u32, 0), rope.count());\n\n    try rope.append(.{ .value = 42 });\n    try std.testing.expectEqual(@as(u32, 1), rope.count());\n    try std.testing.expectEqual(@as(u32, 42), rope.get(0).?.value);\n}\n\ntest \"Rope - clear with markers resets marker cache\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(TokenType);\n    const tokens = [_]TokenType{\n        .{ .word = 5 },\n        .{ .newline = {} },\n        .{ .word = 5 },\n        .{ .newline = {} },\n    };\n\n    var rope = try RopeType.from_slice(arena.allocator(), &tokens);\n    try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));\n\n    rope.clear();\n\n    try std.testing.expectEqual(@as(u32, 0), rope.count());\n    try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.newline));\n}\n\ntest \"Rope - integration all features working together\" {\n    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);\n    defer arena.deinit();\n\n    const RopeType = rope_mod.Rope(SimpleItem);\n    var rope = try RopeType.initWithConfig(arena.allocator(), .{ .max_undo_depth = 3 });\n\n    try std.testing.expectEqual(@as(u32, 0), rope.count());\n\n    try rope.store_undo(\"empty\");\n    try rope.append(.{ .value = 1 });\n\n    try rope.store_undo(\"one\");\n    try rope.append(.{ .value = 2 });\n\n    try rope.store_undo(\"two\");\n    try rope.append(.{ .value = 3 });\n\n    try rope.store_undo(\"three\");\n    try rope.append(.{ .value = 4 });\n\n    try std.testing.expectEqual(@as(u32, 4), rope.count());\n\n    try std.testing.expectEqual(@as(usize, 3), rope.undo_depth);\n\n    const val = rope.get(2);\n    try std.testing.expectEqual(@as(u32, 3), val.?.value);\n\n    _ = try rope.undo(\"current\");\n    try std.testing.expectEqual(@as(u32, 3), rope.count());\n\n    const Context = struct {\n        count: u32 = 0,\n        fn walker(ctx: *anyopaque, data: *const SimpleItem, index: u32) RopeType.Node.WalkerResult {\n            _ = data;\n            _ = index;\n            const self = @as(*@This(), @ptrCast(@alignCast(ctx)));\n            self.count += 1;\n            return .{};\n        }\n    };\n    var ctx = Context{};\n    try rope.walk(&ctx, Context.walker);\n    try std.testing.expectEqual(@as(u32, 3), ctx.count);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/segment-merge.test.zig",
    "content": "const std = @import(\"std\");\nconst EditBuffer = @import(\"../edit-buffer.zig\").EditBuffer;\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\ntest \"EditBuffer - sequential character insertion merges segments\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"h\");\n    try eb.insertText(\"e\");\n    try eb.insertText(\"l\");\n    try eb.insertText(\"l\");\n    try eb.insertText(\"o\");\n\n    const count = eb.tb.rope().count();\n\n    var buffer: [1024]u8 = undefined;\n    const len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"hello\", buffer[0..len]);\n\n    try std.testing.expect(count <= 4);\n    try std.testing.expect(count >= 2);\n}\n\ntest \"EditBuffer - merging preserves text correctness\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    const text = \"The quick brown fox jumps over the lazy dog\";\n    for (text) |c| {\n        var char_buf: [1]u8 = .{c};\n        try eb.insertText(&char_buf);\n    }\n\n    var buffer: [1024]u8 = undefined;\n    const len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(text, buffer[0..len]);\n}\n\ntest \"EditBuffer - non-contiguous segments do not merge\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"abc\");\n    try eb.setCursor(0, 0);\n    try eb.insertText(\"xyz\");\n\n    var buffer: [1024]u8 = undefined;\n    const len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"xyzabc\", buffer[0..len]);\n}\n\ntest \"EditBuffer - merging works across newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"a\");\n    try eb.insertText(\"b\");\n    try eb.insertText(\"c\");\n    try eb.insertText(\"\\n\");\n    try eb.insertText(\"d\");\n    try eb.insertText(\"e\");\n    try eb.insertText(\"f\");\n\n    var buffer: [1024]u8 = undefined;\n    const len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"abc\\ndef\", buffer[0..len]);\n\n    const line_count = eb.tb.lineCount();\n    try std.testing.expectEqual(@as(u32, 2), line_count);\n}\n\ntest \"EditBuffer - merging with unicode characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"你\");\n    try eb.insertText(\"好\");\n    try eb.insertText(\"世\");\n    try eb.insertText(\"界\");\n\n    var buffer: [1024]u8 = undefined;\n    const len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"你好世界\", buffer[0..len]);\n}\n\ntest \"EditBuffer - merging after delete and re-insert\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"hello\");\n    try eb.backspace();\n    try eb.insertText(\"p\");\n\n    var buffer: [1024]u8 = undefined;\n    const len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"hellp\", buffer[0..len]);\n}\n\ntest \"EditBuffer - empty buffer then type\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.insertText(\"t\");\n    try eb.insertText(\"e\");\n    try eb.insertText(\"s\");\n    try eb.insertText(\"t\");\n\n    var buffer: [1024]u8 = undefined;\n    const len = eb.getText(&buffer);\n    try std.testing.expectEqualStrings(\"test\", buffer[0..len]);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/syntax-style_test.zig",
    "content": "const std = @import(\"std\");\nconst syntax_style = @import(\"../syntax-style.zig\");\n\nconst SyntaxStyle = syntax_style.SyntaxStyle;\nconst StyleDefinition = syntax_style.StyleDefinition;\nconst RGBA = syntax_style.RGBA;\n\ntest \"SyntaxStyle - init and deinit\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    try std.testing.expectEqual(@as(usize, 0), style.getStyleCount());\n}\n\ntest \"SyntaxStyle - multiple independent instances\" {\n    const style1 = try SyntaxStyle.init(std.testing.allocator);\n    defer style1.deinit();\n\n    const style2 = try SyntaxStyle.init(std.testing.allocator);\n    defer style2.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    _ = try style1.registerStyle(\"test\", fg, null, 0);\n\n    try std.testing.expectEqual(@as(usize, 1), style1.getStyleCount());\n    try std.testing.expectEqual(@as(usize, 0), style2.getStyleCount());\n}\n\ntest \"SyntaxStyle - register simple style\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const id = try style.registerStyle(\"keyword\", fg, null, 0);\n\n    try std.testing.expect(id > 0);\n    try std.testing.expectEqual(@as(usize, 1), style.getStyleCount());\n}\n\ntest \"SyntaxStyle - register style with fg and bg\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const id = try style.registerStyle(\"string\", fg, bg, 0);\n\n    try std.testing.expect(id > 0);\n    try std.testing.expectEqual(@as(usize, 1), style.getStyleCount());\n}\n\ntest \"SyntaxStyle - register style with attributes\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const attributes: u32 = 0b0001; // Bold\n    const id = try style.registerStyle(\"bold-keyword\", fg, null, attributes);\n\n    try std.testing.expect(id > 0);\n\n    const resolved = style.resolveById(id).?;\n    try std.testing.expectEqual(attributes, resolved.attributes);\n}\n\ntest \"SyntaxStyle - register style with all attributes\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const attributes: u32 = 0b1111; // Bold, italic, underline, dim\n    const id = try style.registerStyle(\"all-attrs\", fg, null, attributes);\n\n    try std.testing.expect(id > 0);\n\n    const resolved = style.resolveById(id).?;\n    try std.testing.expectEqual(attributes, resolved.attributes);\n}\n\ntest \"SyntaxStyle - register style without colors\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const id = try style.registerStyle(\"plain\", null, null, 0);\n\n    try std.testing.expect(id > 0);\n\n    const resolved = style.resolveById(id).?;\n    try std.testing.expect(resolved.fg == null);\n    try std.testing.expect(resolved.bg == null);\n}\n\ntest \"SyntaxStyle - register multiple styles\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg1 = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const fg2 = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    const fg3 = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"keyword\", fg1, null, 0);\n    const id2 = try style.registerStyle(\"string\", fg2, null, 0);\n    const id3 = try style.registerStyle(\"comment\", fg3, null, 0);\n\n    try std.testing.expect(id1 != id2);\n    try std.testing.expect(id2 != id3);\n    try std.testing.expect(id1 != id3);\n    try std.testing.expectEqual(@as(usize, 3), style.getStyleCount());\n}\n\ntest \"SyntaxStyle - register same name returns same ID\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg1 = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const fg2 = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"keyword\", fg1, null, 0);\n    const id2 = try style.registerStyle(\"keyword\", fg2, null, 0);\n\n    try std.testing.expectEqual(id1, id2);\n    try std.testing.expectEqual(@as(usize, 1), style.getStyleCount());\n\n    const resolved = style.resolveById(id2).?;\n    try std.testing.expectEqual(fg2[0], resolved.fg.?[0]);\n    try std.testing.expectEqual(fg2[1], resolved.fg.?[1]);\n}\n\ntest \"SyntaxStyle - register style with special characters\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    _ = try style.registerStyle(\"keyword.control\", fg, null, 0);\n    _ = try style.registerStyle(\"variable.parameter\", fg, null, 0);\n    _ = try style.registerStyle(\"meta.tag.xml\", fg, null, 0);\n\n    try std.testing.expectEqual(@as(usize, 3), style.getStyleCount());\n}\n\ntest \"SyntaxStyle - register many styles\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const count = 100;\n    var ids: [count]u32 = undefined;\n\n    for (0..count) |i| {\n        var name_buffer: [32]u8 = undefined;\n        const name = try std.fmt.bufPrint(&name_buffer, \"style-{d}\", .{i});\n\n        const fg = RGBA{ @as(f32, @floatFromInt(i)) / 100.0, 0.0, 0.0, 1.0 };\n        ids[i] = try style.registerStyle(name, fg, null, 0);\n    }\n\n    try std.testing.expectEqual(@as(usize, count), style.getStyleCount());\n\n    for (ids, 0..count) |id1, i| {\n        for (ids[i + 1 ..]) |id2| {\n            try std.testing.expect(id1 != id2);\n        }\n    }\n}\n\ntest \"SyntaxStyle - resolveById returns correct style\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const bg = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n    const attributes: u32 = 0b0011; // Bold + italic\n\n    const id = try style.registerStyle(\"test\", fg, bg, attributes);\n    const resolved = style.resolveById(id).?;\n\n    try std.testing.expectEqual(fg[0], resolved.fg.?[0]);\n    try std.testing.expectEqual(bg[0], resolved.bg.?[0]);\n    try std.testing.expectEqual(attributes, resolved.attributes);\n}\n\ntest \"SyntaxStyle - resolveById invalid ID returns null\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const resolved = style.resolveById(9999);\n    try std.testing.expect(resolved == null);\n}\n\ntest \"SyntaxStyle - resolveById zero returns null\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const resolved = style.resolveById(0);\n    try std.testing.expect(resolved == null);\n}\n\ntest \"SyntaxStyle - resolveByName returns correct ID\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const registered_id = try style.registerStyle(\"keyword\", fg, null, 0);\n\n    const resolved_id = style.resolveByName(\"keyword\").?;\n    try std.testing.expectEqual(registered_id, resolved_id);\n}\n\ntest \"SyntaxStyle - resolveByName non-existent returns null\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const resolved = style.resolveByName(\"nonexistent\");\n    try std.testing.expect(resolved == null);\n}\n\ntest \"SyntaxStyle - resolveByName is case-sensitive\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    _ = try style.registerStyle(\"keyword\", fg, null, 0);\n\n    try std.testing.expect(style.resolveByName(\"keyword\") != null);\n    try std.testing.expect(style.resolveByName(\"Keyword\") == null);\n    try std.testing.expect(style.resolveByName(\"KEYWORD\") == null);\n}\n\ntest \"SyntaxStyle - resolve multiple styles\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg1 = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const fg2 = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"keyword\", fg1, null, 0);\n    const id2 = try style.registerStyle(\"string\", fg2, null, 0);\n\n    try std.testing.expectEqual(id1, style.resolveByName(\"keyword\").?);\n    try std.testing.expectEqual(id2, style.resolveByName(\"string\").?);\n}\n\ntest \"SyntaxStyle - merge single style\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const attributes: u32 = 0b0001;\n\n    const id = try style.registerStyle(\"keyword\", fg, null, attributes);\n\n    const ids = [_]u32{id};\n    const merged = try style.mergeStyles(&ids);\n\n    try std.testing.expectEqual(fg[0], merged.fg.?[0]);\n    try std.testing.expectEqual(attributes, merged.attributes);\n}\n\ntest \"SyntaxStyle - merge two styles\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg1 = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const fg2 = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    const bg2 = RGBA{ 0.0, 0.0, 0.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"base\", fg1, null, 0b0001); // Bold\n    const id2 = try style.registerStyle(\"modifier\", fg2, bg2, 0b0010); // Italic\n\n    const ids = [_]u32{ id1, id2 };\n    const merged = try style.mergeStyles(&ids);\n\n    try std.testing.expectEqual(fg2[0], merged.fg.?[0]);\n    try std.testing.expectEqual(bg2[0], merged.bg.?[0]);\n    try std.testing.expectEqual(@as(u32, 0b0011), merged.attributes);\n}\n\ntest \"SyntaxStyle - merge three styles\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg1 = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const fg2 = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n    const fg3 = RGBA{ 0.0, 0.0, 1.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"s1\", fg1, null, 0b0001); // Bold\n    const id2 = try style.registerStyle(\"s2\", fg2, null, 0b0010); // Italic\n    const id3 = try style.registerStyle(\"s3\", fg3, null, 0b0100); // Underline\n\n    const ids = [_]u32{ id1, id2, id3 };\n    const merged = try style.mergeStyles(&ids);\n\n    try std.testing.expectEqual(fg3[0], merged.fg.?[0]);\n    try std.testing.expectEqual(@as(u32, 0b0111), merged.attributes);\n}\n\ntest \"SyntaxStyle - merge empty array\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const ids: []const u32 = &[_]u32{};\n    const merged = try style.mergeStyles(ids);\n\n    try std.testing.expect(merged.fg == null);\n    try std.testing.expect(merged.bg == null);\n    try std.testing.expectEqual(@as(u32, 0), merged.attributes);\n}\n\ntest \"SyntaxStyle - merge with invalid ID skips it\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const id1 = try style.registerStyle(\"valid\", fg, null, 0b0001);\n\n    const ids = [_]u32{ id1, 9999 }; // 9999 is invalid\n    const merged = try style.mergeStyles(&ids);\n\n    try std.testing.expectEqual(fg[0], merged.fg.?[0]);\n    try std.testing.expectEqual(@as(u32, 0b0001), merged.attributes);\n}\n\ntest \"SyntaxStyle - merge caches results\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg1 = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const fg2 = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"s1\", fg1, null, 0);\n    const id2 = try style.registerStyle(\"s2\", fg2, null, 0);\n\n    const ids = [_]u32{ id1, id2 };\n\n    const merged1 = try style.mergeStyles(&ids);\n    const merged2 = try style.mergeStyles(&ids);\n\n    try std.testing.expectEqual(merged1.fg.?[0], merged2.fg.?[0]);\n    try std.testing.expect(style.getCacheSize() > 0);\n}\n\ntest \"SyntaxStyle - merge different order produces different results\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg1 = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const fg2 = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"s1\", fg1, null, 0);\n    const id2 = try style.registerStyle(\"s2\", fg2, null, 0);\n\n    const ids1 = [_]u32{ id1, id2 };\n    const ids2 = [_]u32{ id2, id1 };\n\n    const merged1 = try style.mergeStyles(&ids1);\n    const merged2 = try style.mergeStyles(&ids2);\n\n    try std.testing.expect(merged1.fg.?[0] != merged2.fg.?[0]);\n}\n\ntest \"SyntaxStyle - clearCache empties cache\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg1 = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const fg2 = RGBA{ 0.0, 1.0, 0.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"s1\", fg1, null, 0);\n    const id2 = try style.registerStyle(\"s2\", fg2, null, 0);\n\n    const ids = [_]u32{ id1, id2 };\n    _ = try style.mergeStyles(&ids);\n\n    try std.testing.expect(style.getCacheSize() > 0);\n\n    style.clearCache();\n\n    try std.testing.expectEqual(@as(usize, 0), style.getCacheSize());\n}\n\ntest \"SyntaxStyle - clearCache preserves styles\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    _ = try style.registerStyle(\"keyword\", fg, null, 0);\n    _ = try style.registerStyle(\"string\", fg, null, 0);\n\n    const count_before = style.getStyleCount();\n    style.clearCache();\n    const count_after = style.getStyleCount();\n\n    try std.testing.expectEqual(count_before, count_after);\n}\n\ntest \"SyntaxStyle - getCacheSize returns correct count\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    try std.testing.expectEqual(@as(usize, 0), style.getCacheSize());\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const id1 = try style.registerStyle(\"s1\", fg, null, 0);\n    const id2 = try style.registerStyle(\"s2\", fg, null, 0);\n\n    const ids1 = [_]u32{id1};\n    const ids2 = [_]u32{id2};\n    const ids_both = [_]u32{ id1, id2 };\n\n    _ = try style.mergeStyles(&ids1);\n    try std.testing.expectEqual(@as(usize, 1), style.getCacheSize());\n\n    _ = try style.mergeStyles(&ids2);\n    try std.testing.expectEqual(@as(usize, 2), style.getCacheSize());\n\n    _ = try style.mergeStyles(&ids_both);\n    try std.testing.expectEqual(@as(usize, 3), style.getCacheSize());\n}\n\ntest \"SyntaxStyle - very long style name\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    var long_name: [1000]u8 = undefined;\n    @memset(&long_name, 'a');\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const id = try style.registerStyle(&long_name, fg, null, 0);\n\n    try std.testing.expect(id > 0);\n    try std.testing.expectEqual(id, style.resolveByName(&long_name).?);\n}\n\ntest \"SyntaxStyle - empty string style name\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const id = try style.registerStyle(\"\", fg, null, 0);\n\n    try std.testing.expect(id > 0);\n    try std.testing.expectEqual(id, style.resolveByName(\"\").?);\n}\n\ntest \"SyntaxStyle - unicode style names\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    const id1 = try style.registerStyle(\"关键字\", fg, null, 0);\n    const id2 = try style.registerStyle(\"キーワード\", fg, null, 0);\n    const id3 = try style.registerStyle(\"🔑\", fg, null, 0);\n\n    try std.testing.expectEqual(@as(usize, 3), style.getStyleCount());\n    try std.testing.expect(id1 != id2);\n    try std.testing.expect(id2 != id3);\n}\n\ntest \"SyntaxStyle - all color channels\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 0.1, 0.2, 0.3, 0.4 };\n    const bg = RGBA{ 0.5, 0.6, 0.7, 0.8 };\n\n    const id = try style.registerStyle(\"test\", fg, bg, 0);\n    const resolved = style.resolveById(id).?;\n\n    try std.testing.expectEqual(fg[0], resolved.fg.?[0]);\n    try std.testing.expectEqual(fg[1], resolved.fg.?[1]);\n    try std.testing.expectEqual(fg[2], resolved.fg.?[2]);\n    try std.testing.expectEqual(fg[3], resolved.fg.?[3]);\n\n    try std.testing.expectEqual(bg[0], resolved.bg.?[0]);\n    try std.testing.expectEqual(bg[1], resolved.bg.?[1]);\n    try std.testing.expectEqual(bg[2], resolved.bg.?[2]);\n    try std.testing.expectEqual(bg[3], resolved.bg.?[3]);\n}\n\ntest \"SyntaxStyle - stress test many registrations\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const count = 1000;\n    for (0..count) |i| {\n        var name_buffer: [32]u8 = undefined;\n        const name = try std.fmt.bufPrint(&name_buffer, \"style-{d}\", .{i});\n\n        const fg = RGBA{ @as(f32, @floatFromInt(i % 256)) / 255.0, 0.0, 0.0, 1.0 };\n        _ = try style.registerStyle(name, fg, null, 0);\n    }\n\n    try std.testing.expectEqual(@as(usize, count), style.getStyleCount());\n}\n\ntest \"SyntaxStyle - stress test many merges\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const id1 = try style.registerStyle(\"s1\", fg, null, 0);\n    const id2 = try style.registerStyle(\"s2\", fg, null, 0);\n    const id3 = try style.registerStyle(\"s3\", fg, null, 0);\n\n    for (0..100) |_| {\n        const ids = [_]u32{ id1, id2, id3 };\n        _ = try style.mergeStyles(&ids);\n    }\n\n    try std.testing.expectEqual(@as(usize, 1), style.getCacheSize());\n}\n\ntest \"SyntaxStyle - merge many styles at once\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const count = 50;\n    var ids: [count]u32 = undefined;\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    for (0..count) |i| {\n        var name_buffer: [32]u8 = undefined;\n        const name = try std.fmt.bufPrint(&name_buffer, \"s{d}\", .{i});\n        ids[i] = try style.registerStyle(name, fg, null, @as(u8, @intCast(i % 4)));\n    }\n\n    const merged = try style.mergeStyles(&ids);\n\n    try std.testing.expect(merged.attributes != 0);\n}\n\ntest \"SyntaxStyle - multiple init/deinit cycles\" {\n    for (0..10) |_| {\n        const style = try SyntaxStyle.init(std.testing.allocator);\n        const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n        _ = try style.registerStyle(\"test\", fg, null, 0);\n        style.deinit();\n    }\n}\n\ntest \"SyntaxStyle - register and resolve after clear cache\" {\n    const style = try SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    const fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    const id = try style.registerStyle(\"keyword\", fg, null, 0);\n\n    const ids = [_]u32{id};\n    _ = try style.mergeStyles(&ids);\n\n    style.clearCache();\n\n    try std.testing.expectEqual(id, style.resolveByName(\"keyword\").?);\n    const merged = try style.mergeStyles(&ids);\n    try std.testing.expectEqual(fg[0], merged.fg.?[0]);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/terminal_test.zig",
    "content": "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst testing = std.testing;\nconst ansi = @import(\"../ansi.zig\");\nconst Terminal = @import(\"../terminal.zig\");\nconst utf8 = @import(\"../utf8.zig\");\n\ntest \"parseXtversion - kitty format\" {\n    var term = Terminal.init(.{});\n    const response = \"\\x1bP>|kitty(0.40.1)\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqualStrings(\"kitty\", term.getTerminalName());\n    try testing.expectEqualStrings(\"0.40.1\", term.getTerminalVersion());\n    try testing.expect(term.term_info.from_xtversion);\n    try testing.expect(term.caps.osc52);\n}\n\ntest \"parseXtversion - ghostty format\" {\n    var term = Terminal.init(.{});\n    const response = \"\\x1bP>|ghostty 1.1.3\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqualStrings(\"ghostty\", term.getTerminalName());\n    try testing.expectEqualStrings(\"1.1.3\", term.getTerminalVersion());\n    try testing.expect(term.term_info.from_xtversion);\n    try testing.expect(term.caps.osc52);\n}\n\ntest \"parseXtversion - tmux format\" {\n    var term = Terminal.init(.{});\n    const response = \"\\x1bP>|tmux 3.5a\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqualStrings(\"tmux\", term.getTerminalName());\n    try testing.expectEqualStrings(\"3.5a\", term.getTerminalVersion());\n    try testing.expect(term.term_info.from_xtversion);\n    try testing.expect(term.caps.osc52);\n}\n\ntest \"parseXtversion - with prefix data\" {\n    var term = Terminal.init(.{});\n    const response = \"\\x1b[1;1R\\x1bP>|tmux 3.5a\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqualStrings(\"tmux\", term.getTerminalName());\n    try testing.expectEqualStrings(\"3.5a\", term.getTerminalVersion());\n    try testing.expect(term.term_info.from_xtversion);\n}\n\ntest \"parseXtversion - full kitty response\" {\n    var term = Terminal.init(.{});\n    const response = \"\\x1b[?1016;2$y\\x1b[?2027;0$y\\x1b[?2031;2$y\\x1b[?1004;1$y\\x1b[?2026;2$y\\x1b[1;2R\\x1b[1;3R\\x1bP>|kitty(0.40.1)\\x1b\\\\\\x1b[?0u\\x1b_Gi=1;EINVAL:Zero width/height not allowed\\x1b\\\\\\x1b[?62;c\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqualStrings(\"kitty\", term.getTerminalName());\n    try testing.expectEqualStrings(\"0.40.1\", term.getTerminalVersion());\n    try testing.expect(term.term_info.from_xtversion);\n    try testing.expect(term.caps.kitty_keyboard);\n    try testing.expect(term.caps.kitty_graphics);\n    try testing.expect(term.caps.osc52);\n}\n\ntest \"parseXtversion - full ghostty response\" {\n    var term = Terminal.init(.{});\n    const response = \"\\x1b[?1016;1$y\\x1b[?2027;1$y\\x1b[?2031;2$y\\x1b[?1004;1$y\\x1b[?2004;2$y\\x1b[?2026;2$y\\x1b[1;1R\\x1b[1;1R\\x1bP>|ghostty 1.1.3\\x1b\\\\\\x1b[?0u\\x1b_Gi=1;OK\\x1b\\\\\\x1b[?62;22c\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqualStrings(\"ghostty\", term.getTerminalName());\n    try testing.expectEqualStrings(\"1.1.3\", term.getTerminalVersion());\n    try testing.expect(term.term_info.from_xtversion);\n}\n\ntest \"environment variables - should be overridden by xtversion\" {\n    var term = Terminal.init(.{});\n\n    // First check environment (simulated by setting values directly)\n    term.term_info.name_len = 6;\n    @memcpy(term.term_info.name[0..6], \"vscode\");\n    term.term_info.version_len = 5;\n    @memcpy(term.term_info.version[0..5], \"1.0.0\");\n    term.term_info.from_xtversion = false;\n\n    try testing.expectEqualStrings(\"vscode\", term.getTerminalName());\n    try testing.expectEqualStrings(\"1.0.0\", term.getTerminalVersion());\n    try testing.expect(!term.term_info.from_xtversion);\n\n    // Now process xtversion response - should override\n    const response = \"\\x1bP>|kitty(0.40.1)\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqualStrings(\"kitty\", term.getTerminalName());\n    try testing.expectEqualStrings(\"0.40.1\", term.getTerminalVersion());\n    try testing.expect(term.term_info.from_xtversion);\n}\n\ntest \"remote ignores env overrides but accepts capability responses\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n    try env.put(\"TMUX\", \"/tmp/tmux-1000/default,12345,0\");\n    try env.put(\"TERM_PROGRAM\", \"iTerm.app\");\n    try env.put(\"WT_SESSION\", \"test-session\");\n\n    var term = Terminal.init(.{ .remote = true, .env_map = &env });\n\n    try testing.expect(!term.in_tmux);\n    try testing.expect(!term.caps.osc52);\n    try testing.expect(!term.caps.explicit_cursor_positioning);\n\n    term.processCapabilityResponse(\"\\x1bP>|kitty(0.40.1)\\x1b\\\\\");\n    try testing.expect(term.caps.osc52);\n}\n\ntest \"setHostEnvVar applies env overrides in shared library mode\" {\n    var term = Terminal.init(.{});\n    defer term.deinit();\n\n    try term.setHostEnvVar(testing.allocator, \"TERM\", \"screen\");\n    try testing.expect(term.skip_graphics_query);\n    try testing.expect(term.caps.unicode == .wcwidth);\n    try testing.expect(term.caps.explicit_cursor_positioning);\n\n    try term.setHostEnvVar(testing.allocator, \"OPENTUI_FORCE_UNICODE\", \"1\");\n    try testing.expect(term.caps.unicode == .unicode);\n\n    try term.setHostEnvVar(testing.allocator, \"OPENTUI_GRAPHICS\", \"0\");\n    try testing.expect(term.skip_graphics_query);\n}\n\ntest \"parseXtversion - terminal name only\" {\n    var term = Terminal.init(.{});\n    const response = \"\\x1bP>|wezterm\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqualStrings(\"wezterm\", term.getTerminalName());\n    try testing.expectEqualStrings(\"\", term.getTerminalVersion());\n    try testing.expect(term.term_info.from_xtversion);\n    try testing.expect(term.caps.osc52);\n}\n\ntest \"parseXtversion - empty response\" {\n    var term = Terminal.init(.{});\n\n    const initial_name_len = term.term_info.name_len;\n    const initial_version_len = term.term_info.version_len;\n\n    const response = \"\\x1bP>|\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expectEqual(initial_name_len, term.term_info.name_len);\n    try testing.expectEqual(initial_version_len, term.term_info.version_len);\n}\n\n// Test buffer for capturing terminal output\nconst TestWriter = struct {\n    buffer: std.ArrayListUnmanaged(u8),\n    allocator: std.mem.Allocator,\n\n    pub fn init(allocator: std.mem.Allocator) TestWriter {\n        return .{ .buffer = .{}, .allocator = allocator };\n    }\n\n    pub fn deinit(self: *TestWriter) void {\n        self.buffer.deinit(self.allocator);\n    }\n\n    pub fn writeAll(self: *TestWriter, data: []const u8) !void {\n        try self.buffer.appendSlice(self.allocator, data);\n    }\n\n    pub fn print(self: *TestWriter, comptime fmt: []const u8, args: anytype) !void {\n        try self.buffer.writer(self.allocator).print(fmt, args);\n    }\n\n    pub fn getWritten(self: *TestWriter) []const u8 {\n        return self.buffer.items;\n    }\n\n    pub fn reset(self: *TestWriter) void {\n        self.buffer.clearRetainingCapacity();\n    }\n};\n\ntest \"queryTerminalSend - sends unwrapped queries when not in tmux\" {\n    // Note: This test may fail if running inside tmux since checkEnvironmentOverrides\n    // reads TMUX/TERM env vars. We test the logic directly instead.\n    var term = Terminal.init(.{});\n\n    // Skip test if actually running in tmux\n    if (term.in_tmux) return error.SkipZigTest;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.queryTerminalSend(&writer);\n\n    const output = writer.getWritten();\n\n    // Should contain xtversion\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b[>0q\") != null);\n\n    // Should contain unwrapped DECRQM queries (single ESC)\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b[?1016$p\") != null);\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b[?2027$p\") != null);\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b[?u\") != null);\n\n    // Should NOT contain tmux DCS wrapper\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1bPtmux;\") == null);\n\n    // Should mark capability queries as pending\n    try testing.expect(term.capability_queries_pending);\n}\n\ntest \"queryTerminalSend - sends DCS wrapped queries when in tmux\" {\n    // Note: This test checks logic when in_tmux is true.\n    // We can't easily force in_tmux=true since checkEnvironmentOverrides resets it,\n    // so we test this via sendPendingQueries tests instead.\n    var term = Terminal.init(.{});\n\n    // Only run the DCS wrapping test if actually in tmux\n    if (!term.in_tmux) return error.SkipZigTest;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.queryTerminalSend(&writer);\n\n    const output = writer.getWritten();\n\n    // Should contain xtversion (unwrapped - used for detection)\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b[>0q\") != null);\n\n    // Should contain tmux DCS wrapper start and doubled ESC for queries\n    // wrapForTmux wraps all queries together with one DCS envelope\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1bPtmux;\\x1b\\x1b[?1016$p\") != null);\n\n    // Should NOT mark capability queries as pending (already sent wrapped)\n    try testing.expect(!term.capability_queries_pending);\n}\n\ntest \"sendPendingQueries - sends wrapped queries after tmux detected via xtversion\" {\n    var term = Terminal.init(.{});\n    term.in_tmux = false;\n    term.capability_queries_pending = true;\n    term.graphics_query_pending = true;\n\n    // Simulate tmux detected via xtversion\n    term.term_info.from_xtversion = true;\n    term.term_info.name_len = 4;\n    @memcpy(term.term_info.name[0..4], \"tmux\");\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    const did_send = try term.sendPendingQueries(&writer);\n\n    try testing.expect(did_send);\n\n    const output = writer.getWritten();\n\n    // Should send DCS wrapped capability queries (wrapForTmux wraps all queries together)\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1bPtmux;\\x1b\\x1b[?1016$p\") != null);\n\n    // Should send DCS wrapped graphics query\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1bPtmux;\\x1b\\x1b_G\") != null);\n\n    // Should clear pending flags\n    try testing.expect(!term.capability_queries_pending);\n    try testing.expect(!term.graphics_query_pending);\n}\n\ntest \"sendPendingQueries - sends unwrapped graphics query for non-tmux terminal\" {\n    var term = Terminal.init(.{});\n    term.in_tmux = false;\n    term.capability_queries_pending = true;\n    term.graphics_query_pending = true;\n\n    // Simulate non-tmux terminal detected via xtversion\n    term.term_info.from_xtversion = true;\n    term.term_info.name_len = 5;\n    @memcpy(term.term_info.name[0..5], \"kitty\");\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    const did_send = try term.sendPendingQueries(&writer);\n\n    try testing.expect(did_send);\n\n    const output = writer.getWritten();\n\n    // Should NOT send DCS wrapped capability queries (not tmux)\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1bPtmux;\") == null);\n\n    // Should send unwrapped graphics query\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b_Gi=31337\") != null);\n\n    // Should clear pending flags\n    try testing.expect(!term.capability_queries_pending);\n    try testing.expect(!term.graphics_query_pending);\n}\n\ntest \"sendPendingQueries - sends unwrapped graphics query even without xtversion response\" {\n    // This covers terminals that support kitty graphics but don't respond to xtversion.\n    // The graphics query should still be sent (unwrapped) so we can detect graphics support.\n    var term = Terminal.init(.{});\n    term.in_tmux = false;\n    term.term_info.from_xtversion = false;\n    term.capability_queries_pending = true;\n    term.graphics_query_pending = true;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    const did_send = try term.sendPendingQueries(&writer);\n\n    try testing.expect(did_send);\n\n    const output = writer.getWritten();\n\n    // Should send unwrapped graphics query (not tmux, so no DCS wrapper)\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b_Gi=31337\") != null);\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1bPtmux;\") == null);\n\n    // Should clear graphics pending flag\n    try testing.expect(!term.graphics_query_pending);\n\n    // Capability queries should NOT be re-sent (no xtversion means we don't know if tmux,\n    // but they were already sent unwrapped in queryTerminalSend)\n    try testing.expect(!term.capability_queries_pending);\n}\n\ntest \"sendPendingQueries - skips graphics when skip_graphics_query is set\" {\n    var term = Terminal.init(.{});\n    term.in_tmux = true;\n    term.skip_graphics_query = true;\n    term.graphics_query_pending = true;\n    term.capability_queries_pending = false;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    const did_send = try term.sendPendingQueries(&writer);\n\n    try testing.expect(!did_send);\n\n    const output = writer.getWritten();\n    try testing.expect(std.mem.indexOf(u8, output, \"Gi=31337\") == null);\n}\n\ntest \"isXtversionTmux - detects tmux from xtversion\" {\n    var term = Terminal.init(.{});\n\n    // Not from xtversion\n    term.term_info.from_xtversion = false;\n    term.term_info.name_len = 4;\n    @memcpy(term.term_info.name[0..4], \"tmux\");\n    try testing.expect(!term.isXtversionTmux());\n\n    // From xtversion but not tmux\n    term.term_info.from_xtversion = true;\n    term.term_info.name_len = 5;\n    @memcpy(term.term_info.name[0..5], \"kitty\");\n    try testing.expect(!term.isXtversionTmux());\n\n    // From xtversion and is tmux\n    term.term_info.name_len = 4;\n    @memcpy(term.term_info.name[0..4], \"tmux\");\n    try testing.expect(term.isXtversionTmux());\n}\n\n// ============================================================================\n// GRAPHEME CURSOR POSITIONING CAPABILITY TESTS\n// ============================================================================\n\ntest \"processCapabilityResponse - tmux sets explicit_cursor_positioning\" {\n    var term: Terminal = .{};\n\n    term.caps.explicit_cursor_positioning = false;\n    term.caps.unicode = .unicode;\n\n    const response = \"\\x1bP>|tmux 3.5a\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expect(term.caps.explicit_cursor_positioning);\n    try testing.expectEqual(utf8.WidthMethod.wcwidth, term.caps.unicode);\n}\n\ntest \"processCapabilityResponse - alacritty sets explicit_cursor_positioning\" {\n    var term: Terminal = .{};\n\n    term.caps.explicit_cursor_positioning = false;\n\n    const response = \"\\x1bP>|alacritty 0.13.0\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expect(term.caps.explicit_cursor_positioning);\n}\n\ntest \"processCapabilityResponse - kitty does not set explicit_cursor_positioning\" {\n    var term: Terminal = .{};\n\n    term.caps.explicit_cursor_positioning = false;\n\n    const response = \"\\x1bP>|kitty(0.40.1)\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expect(!term.caps.explicit_cursor_positioning);\n}\n\ntest \"processCapabilityResponse - ghostty does not set explicit_cursor_positioning\" {\n    var term: Terminal = .{};\n\n    term.caps.explicit_cursor_positioning = false;\n\n    const response = \"\\x1bP>|ghostty 1.1.3\\x1b\\\\\";\n    term.processCapabilityResponse(response);\n\n    try testing.expect(!term.caps.explicit_cursor_positioning);\n}\n\n// ============================================================================\n// CLIPBOARD (OSC 52) TESTS\n// ============================================================================\n\ntest \"writeClipboard - generates basic OSC52 sequence\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n\n    var term = Terminal.init(.{ .env_map = &env });\n    term.caps.osc52 = true;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.writeClipboard(&writer, .clipboard, \"aGVsbG8=\");\n\n    const output = writer.getWritten();\n    // Should be: ESC]52;c;aGVsbG8=ESC\\\n    try testing.expectEqualStrings(\"\\x1b]52;c;aGVsbG8=\\x1b\\\\\", output);\n}\n\ntest \"writeClipboard - supports different targets\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n\n    var term = Terminal.init(.{ .env_map = &env });\n    term.caps.osc52 = true;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.writeClipboard(&writer, .primary, \"test\");\n    try testing.expect(std.mem.indexOf(u8, writer.getWritten(), \"\\x1b]52;p;\") != null);\n\n    writer.reset();\n    try term.writeClipboard(&writer, .secondary, \"test\");\n    try testing.expect(std.mem.indexOf(u8, writer.getWritten(), \"\\x1b]52;s;\") != null);\n\n    writer.reset();\n    try term.writeClipboard(&writer, .query, \"test\");\n    try testing.expect(std.mem.indexOf(u8, writer.getWritten(), \"\\x1b]52;q;\") != null);\n}\n\ntest \"writeClipboard - returns error when OSC52 not supported\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var term = Terminal.init(.{});\n    term.caps.osc52 = false;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    const result = term.writeClipboard(&writer, .clipboard, \"test\");\n    try testing.expectError(error.NotSupported, result);\n}\n\ntest \"writeClipboard - wraps in DCS passthrough for tmux\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n    try env.put(\"TMUX\", \"/tmp/tmux-1000/default,12345,0\");\n\n    var term = Terminal.init(.{ .env_map = &env });\n    term.caps.osc52 = true;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.writeClipboard(&writer, .clipboard, \"test\");\n\n    const output = writer.getWritten();\n    // Should start with tmux DCS wrapper\n    try testing.expect(std.mem.startsWith(u8, output, \"\\x1bPtmux;\"));\n    // Should end with DCS terminator\n    try testing.expect(std.mem.endsWith(u8, output, \"\\x1b\\\\\"));\n    // Should have doubled ESC characters inside\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b\\x1b\") != null);\n}\n\ntest \"writeClipboard - wraps in DCS passthrough for GNU Screen\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n    try env.put(\"STY\", \"12345.pts-0.hostname\");\n\n    var term = Terminal.init(.{ .env_map = &env });\n    term.caps.osc52 = true;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.writeClipboard(&writer, .clipboard, \"test\");\n\n    const output = writer.getWritten();\n    // Should start with DCS (but not tmux prefix)\n    try testing.expect(std.mem.startsWith(u8, output, \"\\x1bP\"));\n    try testing.expect(!std.mem.startsWith(u8, output, \"\\x1bPtmux;\"));\n    // Should end with DCS terminator\n    try testing.expect(std.mem.endsWith(u8, output, \"\\x1b\\\\\"));\n    // Should have doubled ESC characters\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b\\x1b\") != null);\n}\n\ntest \"writeClipboard - handles tmux sessions\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n    try env.put(\"TMUX\", \"/tmp/tmux-1000/default,12345,0\");\n\n    var term = Terminal.init(.{ .env_map = &env });\n    term.caps.osc52 = true;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.writeClipboard(&writer, .clipboard, \"test\");\n\n    const output = writer.getWritten();\n    // Should have tmux DCS wrapper\n    try testing.expect(std.mem.startsWith(u8, output, \"\\x1bPtmux;\"));\n    // Should end with DCS terminator\n    try testing.expect(std.mem.endsWith(u8, output, \"\\x1b\\\\\"));\n    // Should have doubled ESC characters\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b\\x1b\") != null);\n}\n\ntest \"caps.osc52 - clipboard capability flag\" {\n    var term = Terminal.init(.{});\n\n    term.caps.osc52 = false;\n    try testing.expect(!term.caps.osc52);\n\n    term.caps.osc52 = true;\n    try testing.expect(term.caps.osc52);\n}\n\nfn countSubstring(haystack: []const u8, needle: []const u8) usize {\n    var count: usize = 0;\n    var i: usize = 0;\n    while (i < haystack.len) {\n        if (std.mem.startsWith(u8, haystack[i..], needle)) {\n            count += 1;\n            i += needle.len;\n        } else {\n            i += 1;\n        }\n    }\n    return count;\n}\n\ntest \"queryTerminalSend - skips OSC 66 queries when OPENTUI_FORCE_EXPLICIT_WIDTH=false\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n    try env.put(\"OPENTUI_FORCE_EXPLICIT_WIDTH\", \"false\");\n\n    var term = Terminal.init(.{ .env_map = &env });\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.queryTerminalSend(&writer);\n\n    const output = writer.getWritten();\n\n    // Should not contain OSC 66 queries\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b]66;\") == null);\n\n    // Should still contain other queries\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b[>0q\") != null); // xtversion\n\n    // Verify the flag was set correctly\n    try testing.expect(term.skip_explicit_width_query);\n    try testing.expect(!term.caps.explicit_width);\n}\n\ntest \"queryTerminalSend - sends OSC 66 queries by default\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n\n    var term = Terminal.init(.{ .env_map = &env });\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.queryTerminalSend(&writer);\n\n    const output = writer.getWritten();\n\n    // Should contain OSC 66 explicit width query\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b]66;w=1; \\x1b\\\\\") != null);\n\n    // Should contain OSC 66 scaled text query\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b]66;s=2; \\x1b\\\\\") != null);\n\n    // Verify the flag was not set\n    try testing.expect(!term.skip_explicit_width_query);\n}\n\ntest \"queryTerminalSend - sends OSC 66 queries when OPENTUI_FORCE_EXPLICIT_WIDTH=true\" {\n    if (builtin.os.tag == .windows) return error.SkipZigTest;\n\n    var env = std.process.EnvMap.init(testing.allocator);\n    defer env.deinit();\n    try env.put(\"OPENTUI_FORCE_EXPLICIT_WIDTH\", \"true\");\n\n    var term = Terminal.init(.{ .env_map = &env });\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.queryTerminalSend(&writer);\n\n    const output = writer.getWritten();\n\n    // Should contain OSC 66 queries\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b]66;w=1; \\x1b\\\\\") != null);\n    try testing.expect(std.mem.indexOf(u8, output, \"\\x1b]66;s=2; \\x1b\\\\\") != null);\n\n    // Verify the capability was forced on\n    try testing.expect(term.caps.explicit_width);\n    try testing.expect(!term.skip_explicit_width_query);\n}\n\ntest \"setMouseMode - enable without movement keeps click/drag only\" {\n    var term = Terminal.init(.{});\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.setMouseMode(&writer, true, false);\n\n    const output = writer.getWritten();\n    const idx_disable_any = std.mem.indexOf(u8, output, ansi.ANSI.disableAnyEventTracking).?;\n    const idx_enable_mouse = std.mem.indexOf(u8, output, ansi.ANSI.enableMouseTracking).?;\n    const idx_enable_button = std.mem.indexOf(u8, output, ansi.ANSI.enableButtonEventTracking).?;\n    const idx_enable_sgr = std.mem.indexOf(u8, output, ansi.ANSI.enableSGRMouseMode).?;\n    try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.enableAnyEventTracking) == null);\n    try testing.expect(idx_disable_any < idx_enable_mouse);\n    try testing.expect(idx_enable_mouse < idx_enable_button);\n    try testing.expect(idx_enable_button < idx_enable_sgr);\n\n    try testing.expect(term.state.mouse);\n    try testing.expect(!term.state.mouse_movement);\n}\n\ntest \"setMouseMode - enable with movement enables any-event tracking\" {\n    var term = Terminal.init(.{});\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.setMouseMode(&writer, true, true);\n\n    const output = writer.getWritten();\n    const idx_enable_mouse = std.mem.indexOf(u8, output, ansi.ANSI.enableMouseTracking).?;\n    const idx_enable_button = std.mem.indexOf(u8, output, ansi.ANSI.enableButtonEventTracking).?;\n    const idx_enable_any = std.mem.indexOf(u8, output, ansi.ANSI.enableAnyEventTracking).?;\n    const idx_enable_sgr = std.mem.indexOf(u8, output, ansi.ANSI.enableSGRMouseMode).?;\n    try testing.expect(idx_enable_mouse < idx_enable_button);\n    try testing.expect(idx_enable_button < idx_enable_any);\n    try testing.expect(idx_enable_any < idx_enable_sgr);\n    try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.disableAnyEventTracking) == null);\n\n    try testing.expect(term.state.mouse);\n    try testing.expect(term.state.mouse_movement);\n}\n\ntest \"restoreTerminalModes - respects mouse movement setting\" {\n    var term = Terminal.init(.{});\n    term.state.mouse = true;\n    term.state.mouse_movement = false;\n\n    var writer = TestWriter.init(testing.allocator);\n    defer writer.deinit();\n\n    try term.restoreTerminalModes(&writer);\n\n    const output = writer.getWritten();\n    const idx_disable_any = std.mem.indexOf(u8, output, ansi.ANSI.disableAnyEventTracking).?;\n    const idx_enable_mouse = std.mem.indexOf(u8, output, ansi.ANSI.enableMouseTracking).?;\n    const idx_enable_button = std.mem.indexOf(u8, output, ansi.ANSI.enableButtonEventTracking).?;\n    const idx_enable_sgr = std.mem.indexOf(u8, output, ansi.ANSI.enableSGRMouseMode).?;\n    try testing.expect(idx_disable_any < idx_enable_mouse);\n    try testing.expect(idx_enable_mouse < idx_enable_button);\n    try testing.expect(idx_enable_button < idx_enable_sgr);\n    try testing.expect(std.mem.indexOf(u8, output, ansi.ANSI.enableAnyEventTracking) == null);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/text-buffer-drawing_test.zig",
    "content": "const std = @import(\"std\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst buffer = @import(\"../buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\nconst ss = @import(\"../syntax-style.zig\");\n\nconst TextBuffer = text_buffer.TextBuffer;\nconst TextBufferView = text_buffer_view.TextBufferView;\nconst OptimizedBuffer = buffer.OptimizedBuffer;\nconst RGBA = text_buffer.RGBA;\nconst WrapMode = text_buffer.WrapMode;\nconst StyledChunk = text_buffer.StyledChunk;\n\ntest \"drawTextBuffer - simple single line text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expect(std.mem.startsWith(u8, result, \"Hello World\"));\n}\n\ntest \"drawTextBuffer - empty text buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n}\n\ntest \"drawTextBuffer - multiple lines without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len == 3);\n}\n\ntest \"drawTextBuffer - text wrapping at word boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"This is a long line that should wrap at word boundaries\");\n    view.setWrapMode(.word);\n    view.setWrapWidth(15);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        15,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len > 1);\n}\n\ntest \"drawTextBuffer - text wrapping at character boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\");\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len == 4);\n}\n\ntest \"drawTextBuffer - no wrapping with none mode\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"This is a very long line that extends beyond the buffer width\");\n    view.setWrapMode(.word);\n    view.setWrapWidth(null);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len == 1);\n}\n\ntest \"drawTextBuffer - wrapped text with multiple lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"First long line that wraps\\nSecond long line that also wraps\\nThird line\");\n    view.setWrapMode(.word);\n    view.setWrapWidth(15);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        15,\n        15,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len >= 3);\n}\n\ntest \"drawTextBuffer - unicode characters with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello 世界 🌟 Test wrapping\");\n    view.setWrapMode(.word);\n    view.setWrapWidth(15);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        15,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len > 0);\n}\n\ntest \"drawTextBuffer - wrapping preserves wide characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"測試測試測試測試測試\");\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len > 1);\n}\n\ntest \"drawTextBuffer - word wrap does not split multi-byte UTF-8 characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"🌟 Unicode test: こんにちは世界 Hello World 你好世界\");\n    view.setWrapMode(.word);\n    view.setWrapWidth(35);\n    view.updateVirtualLines();\n\n    const vlines = view.getVirtualLines();\n\n    for (vlines) |vline| {\n        var line_buffer: [200]u8 = undefined;\n        const line_start_offset = vline.col_offset;\n        const line_end_offset = line_start_offset + vline.width_cols;\n        const extracted = tb.getTextRange(line_start_offset, line_end_offset, &line_buffer);\n\n        const is_valid_utf8 = std.unicode.utf8ValidateSlice(line_buffer[0..extracted]);\n        try std.testing.expect(is_valid_utf8);\n    }\n\n    try std.testing.expect(vlines.len == 2);\n\n    var full_buffer: [200]u8 = undefined;\n    const line0_len = tb.getTextRange(vlines[0].col_offset, vlines[0].col_offset + vlines[0].width_cols, &full_buffer);\n    const line0_text = full_buffer[0..line0_len];\n\n    const line1_len = tb.getTextRange(vlines[1].col_offset, vlines[1].col_offset + vlines[1].width_cols, &full_buffer);\n    const line1_text = full_buffer[0..line1_len];\n\n    const line0_ends_with_kai = std.mem.endsWith(u8, line0_text, \"界\");\n    const line1_starts_with_kai = std.mem.startsWith(u8, line1_text, \"界\");\n\n    try std.testing.expect(!(line0_ends_with_kai and line1_starts_with_kai));\n}\n\ntest \"drawTextBuffer - wrapped text with offset position\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Short line that wraps nicely\");\n    view.setWrapMode(.word);\n    view.setWrapWidth(10);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        20,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 5, 5);\n\n    const cell = opt_buffer.get(5, 5);\n    try std.testing.expect(cell != null);\n    try std.testing.expect(cell.?.char != 32);\n}\n\ntest \"drawTextBuffer - clipping with scrolled view\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\\nLine 4\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len >= 4);\n}\n\ntest \"drawTextBuffer - wrapping with very narrow width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello\");\n    view.setWrapMode(.char);\n    view.setWrapWidth(3);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        3,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len == 2);\n}\n\ntest \"drawTextBuffer - word wrap doesn't break mid-word\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n    view.setWrapMode(.word);\n    view.setWrapWidth(8);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        8,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len == 2);\n}\n\ntest \"drawTextBuffer - empty lines render correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\n\\nLine 3\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len == 3);\n}\n\ntest \"drawTextBuffer - wrapping with tabs\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello\\tWorld\\tTest\");\n    view.setWrapMode(.word);\n    view.setWrapWidth(15);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        15,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n}\n\ntest \"drawTextBuffer - very long unwrapped line clipping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var long_text: std.ArrayListUnmanaged(u8) = .{};\n    defer long_text.deinit(std.testing.allocator);\n    try long_text.appendNTimes(std.testing.allocator, 'A', 200);\n\n    try tb.setText(long_text.items);\n    view.setWrapMode(.word);\n    view.setWrapWidth(null);\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len == 1);\n}\n\ntest \"drawTextBuffer - wrap mode transitions\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"This is a test line for wrapping\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(null);\n    view.updateVirtualLines();\n    const no_wrap_lines = view.getVirtualLines().len;\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    view.updateVirtualLines();\n    const char_lines = view.getVirtualLines().len;\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(10);\n    view.updateVirtualLines();\n    const word_lines = view.getVirtualLines().len;\n\n    try std.testing.expect(no_wrap_lines == 1);\n    try std.testing.expect(char_lines > 1);\n    try std.testing.expect(word_lines > 1);\n}\n\ntest \"drawTextBuffer - changing wrap width updates virtual lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAAAAAAAAAAAAAAAAAAAAAAAAAAA\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    view.updateVirtualLines();\n    const lines_10 = view.getVirtualLines().len;\n\n    view.setWrapWidth(20);\n    view.updateVirtualLines();\n    const lines_20 = view.getVirtualLines().len;\n\n    view.setWrapWidth(5);\n    view.updateVirtualLines();\n    const lines_5 = view.getVirtualLines().len;\n\n    try std.testing.expect(lines_10 > lines_20);\n    try std.testing.expect(lines_5 > lines_10);\n}\n\ntest \"drawTextBuffer - wrapping with mixed ASCII and Unicode\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABC測試DEF試験GHI\");\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len > 1);\n}\n\ntest \"setStyledText - basic rendering with single chunk\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    const text = \"Hello World\";\n    const fg_color = [4]f32{ 1.0, 1.0, 1.0, 1.0 };\n\n    const chunks = [_]StyledChunk{.{\n        .text_ptr = text.ptr,\n        .text_len = text.len,\n        .fg_ptr = @ptrCast(&fg_color),\n        .bg_ptr = null,\n        .attributes = 0,\n    }};\n\n    try tb.setStyledText(&chunks);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    const result = out_buffer[0..written];\n\n    try std.testing.expectEqualStrings(\"Hello World\", result);\n}\n\ntest \"setStyledText - multiple chunks render correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    const text0 = \"Hello \";\n    const text1 = \"World\";\n    const fg_color = [4]f32{ 1.0, 1.0, 1.0, 1.0 };\n\n    const chunks = [_]StyledChunk{\n        .{ .text_ptr = text0.ptr, .text_len = text0.len, .fg_ptr = @ptrCast(&fg_color), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = text1.ptr, .text_len = text1.len, .fg_ptr = @ptrCast(&fg_color), .bg_ptr = null, .attributes = 0 },\n    };\n\n    try tb.setStyledText(&chunks);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    const result = out_buffer[0..written];\n\n    try std.testing.expectEqualStrings(\"Hello World\", result);\n}\n\n// Viewport Tests\n\ntest \"viewport - basic vertical scrolling limits returned lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\");\n\n    view.setViewport(.{ .x = 0, .y = 2, .width = 20, .height = 5 });\n\n    const visible_lines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 5), visible_lines.len);\n    try std.testing.expectEqual(@as(usize, 2), visible_lines[0].source_line);\n    try std.testing.expectEqual(@as(usize, 6), visible_lines[4].source_line);\n}\n\ntest \"viewport - vertical scrolling at start boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\");\n\n    view.setViewport(.{ .x = 0, .y = 0, .width = 20, .height = 3 });\n\n    const visible_lines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 3), visible_lines.len);\n    try std.testing.expectEqual(@as(usize, 0), visible_lines[0].source_line);\n    try std.testing.expectEqual(@as(usize, 2), visible_lines[2].source_line);\n}\n\ntest \"viewport - vertical scrolling at end boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\");\n\n    view.setViewport(.{ .x = 0, .y = 3, .width = 20, .height = 3 });\n\n    const visible_lines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), visible_lines.len);\n    try std.testing.expectEqual(@as(usize, 3), visible_lines[0].source_line);\n    try std.testing.expectEqual(@as(usize, 4), visible_lines[1].source_line);\n}\n\ntest \"viewport - vertical scrolling beyond content\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\");\n\n    view.setViewport(.{ .x = 0, .y = 10, .width = 20, .height = 5 });\n\n    const visible_lines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 0), visible_lines.len);\n}\n\ntest \"viewport - with wrapping vertical scrolling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"This is a long line that will wrap\\nShort\\nAnother long line that wraps\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(15);\n    view.updateVirtualLines();\n\n    const total_vlines = view.getVirtualLineCount();\n    try std.testing.expect(total_vlines > 3);\n\n    view.setViewport(.{ .x = 0, .y = 2, .width = 15, .height = 3 });\n\n    const visible_lines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 3), visible_lines.len);\n}\n\ntest \"viewport - getCachedLineInfo returns only viewport lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\");\n\n    view.setViewport(.{ .x = 0, .y = 1, .width = 20, .height = 3 });\n\n    const line_info = view.getCachedLineInfo();\n\n    try std.testing.expectEqual(@as(usize, 3), line_info.line_start_cols.len);\n    try std.testing.expectEqual(@as(usize, 3), line_info.line_width_cols.len);\n}\n\ntest \"viewport - changing viewport updates returned lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\");\n\n    view.setViewport(.{ .x = 0, .y = 0, .width = 20, .height = 2 });\n    const lines1 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), lines1.len);\n    try std.testing.expectEqual(@as(usize, 0), lines1[0].source_line);\n\n    view.setViewport(.{ .x = 0, .y = 3, .width = 20, .height = 2 });\n    const lines2 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), lines2.len);\n    try std.testing.expectEqual(@as(usize, 3), lines2[0].source_line);\n\n    view.setViewport(.{ .x = 0, .y = 1, .width = 20, .height = 4 });\n    const lines3 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 4), lines3.len);\n    try std.testing.expectEqual(@as(usize, 1), lines3[0].source_line);\n}\n\ntest \"viewport - null viewport returns all lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\");\n\n    const all_lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 5), all_lines.len);\n\n    view.setViewport(.{ .x = 0, .y = 1, .width = 20, .height = 2 });\n    const viewport_lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), viewport_lines.len);\n\n    view.setViewport(null);\n    const all_lines_again = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 5), all_lines_again.len);\n}\n\ntest \"viewport - setViewportSize convenience method\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\");\n\n    view.setViewportSize(20, 2);\n    const vp1 = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp1.x);\n    try std.testing.expectEqual(@as(u32, 0), vp1.y);\n    try std.testing.expectEqual(@as(u32, 20), vp1.width);\n    try std.testing.expectEqual(@as(u32, 2), vp1.height);\n\n    view.setViewport(.{ .x = 5, .y = 1, .width = 20, .height = 2 });\n\n    view.setViewportSize(30, 3);\n    const vp2 = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 5), vp2.x);\n    try std.testing.expectEqual(@as(u32, 1), vp2.y);\n    try std.testing.expectEqual(@as(u32, 30), vp2.width);\n    try std.testing.expectEqual(@as(u32, 3), vp2.height);\n}\n\ntest \"viewport - stores horizontal offset value with no wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n\n    view.setViewport(.{ .x = 5, .y = 0, .width = 10, .height = 1 });\n\n    const vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 5), vp.x);\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n    try std.testing.expectEqual(@as(u32, 10), vp.width);\n    try std.testing.expectEqual(@as(u32, 1), vp.height);\n\n    const lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), lines.len);\n}\n\ntest \"viewport - preserves horizontal offset when changing vertical (no wrap)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJ\\nKLMNOPQRST\\nUVWXYZ1234\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n\n    view.setViewport(.{ .x = 3, .y = 0, .width = 8, .height = 2 });\n\n    var vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 3), vp.x);\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n\n    view.setViewport(.{ .x = 3, .y = 1, .width = 8, .height = 2 });\n\n    vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 3), vp.x);\n    try std.testing.expectEqual(@as(u32, 1), vp.y);\n\n    const visible_lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), visible_lines.len);\n    try std.testing.expectEqual(@as(usize, 1), visible_lines[0].source_line);\n}\n\ntest \"viewport - can set large horizontal offset (no wrap)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Short\\nLonger line here\\nTiny\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n\n    view.setViewport(.{ .x = 10, .y = 0, .width = 10, .height = 3 });\n\n    const vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 10), vp.x);\n\n    const visible_lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 3), visible_lines.len);\n}\n\ntest \"viewport - horizontal and vertical offset combined (no wrap)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0: ABCDEFGHIJ\\nLine 1: KLMNOPQRST\\nLine 2: UVWXYZ1234\\nLine 3: 567890ABCD\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n\n    view.setViewport(.{ .x = 8, .y = 1, .width = 15, .height = 2 });\n\n    const vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 8), vp.x);\n    try std.testing.expectEqual(@as(u32, 1), vp.y);\n\n    const visible_lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), visible_lines.len);\n    try std.testing.expectEqual(@as(usize, 1), visible_lines[0].source_line);\n    try std.testing.expectEqual(@as(usize, 2), visible_lines[1].source_line);\n}\n\ntest \"viewport - horizontal scrolling only for no-wrap mode\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const long_text = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n    try tb.setText(long_text);\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 10, .y = 0, .width = 15, .height = 1 });\n    view.updateVirtualLines();\n\n    var vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 10), vp.x);\n\n    var lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), lines.len);\n\n    view.setWrapMode(.char);\n    view.setViewport(.{ .x = 10, .y = 0, .width = 15, .height = 5 });\n    view.updateVirtualLines();\n\n    vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 10), vp.x);\n\n    lines = view.getVirtualLines();\n    try std.testing.expect(lines.len > 1);\n}\n\ntest \"viewport - horizontal offset irrelevant with wrapping enabled\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"This is a very long line that will wrap into multiple virtual lines\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(20);\n    view.updateVirtualLines();\n\n    const total_vlines = view.getVirtualLineCount();\n    try std.testing.expect(total_vlines > 1);\n\n    view.setViewport(.{ .x = 5, .y = 1, .width = 15, .height = 2 });\n\n    const vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 5), vp.x);\n    try std.testing.expectEqual(@as(u32, 1), vp.y);\n    try std.testing.expectEqual(@as(u32, 15), vp.width);\n\n    const visible_lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), visible_lines.len);\n}\n\ntest \"viewport - zero width or height\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\");\n\n    view.setViewport(.{ .x = 0, .y = 0, .width = 20, .height = 0 });\n    const lines1 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 0), lines1.len);\n\n    view.setViewport(.{ .x = 0, .y = 0, .width = 0, .height = 2 });\n    const lines2 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), lines2.len);\n}\n\ntest \"viewport - viewport sets wrap width automatically\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDD\");\n\n    view.setWrapMode(.char);\n\n    view.setViewport(.{ .x = 0, .y = 0, .width = 10, .height = 5 });\n    view.updateVirtualLines();\n\n    const vline_count_10 = view.getVirtualLineCount();\n\n    view.setViewport(.{ .x = 0, .y = 0, .width = 20, .height = 5 });\n    view.updateVirtualLines();\n\n    const vline_count_20 = view.getVirtualLineCount();\n\n    try std.testing.expect(vline_count_10 > vline_count_20);\n}\n\ntest \"viewport - moving viewport dynamically (no wrap)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789\\nABCDEFGHIJ\\nKLMNOPQRST\\nUVWXYZ!@#$\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n\n    view.setViewport(.{ .x = 0, .y = 0, .width = 5, .height = 2 });\n    var vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.x);\n    try std.testing.expectEqual(@as(u32, 0), vp.y);\n    const lines1 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), lines1.len);\n    try std.testing.expectEqual(@as(usize, 0), lines1[0].source_line);\n\n    view.setViewport(.{ .x = 0, .y = 1, .width = 5, .height = 2 });\n    vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 0), vp.x);\n    try std.testing.expectEqual(@as(u32, 1), vp.y);\n    const lines2 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), lines2.len);\n    try std.testing.expectEqual(@as(usize, 1), lines2[0].source_line);\n\n    view.setViewport(.{ .x = 3, .y = 1, .width = 5, .height = 2 });\n    vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 3), vp.x);\n    try std.testing.expectEqual(@as(u32, 1), vp.y);\n    const lines3 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), lines3.len);\n\n    view.setViewport(.{ .x = 5, .y = 2, .width = 5, .height = 2 });\n    vp = view.getViewport().?;\n    try std.testing.expectEqual(@as(u32, 5), vp.x);\n    try std.testing.expectEqual(@as(u32, 2), vp.y);\n    const lines4 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), lines4.len);\n    try std.testing.expectEqual(@as(usize, 2), lines4[0].source_line);\n}\n\ntest \"loadFile - loads and renders file correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const test_content = \"ABC\\nDEF\";\n    const tmpdir = std.testing.tmpDir(.{});\n    var tmp = tmpdir;\n    defer tmp.cleanup();\n\n    const file = try tmp.dir.createFile(\"test.txt\", .{});\n    try file.writeAll(test_content);\n    file.close();\n\n    const dir_path = try tmp.dir.realpathAlloc(std.testing.allocator, \".\");\n    defer std.testing.allocator.free(dir_path);\n\n    const file_path = try std.fs.path.join(std.testing.allocator, &[_][]const u8{ dir_path, \"test.txt\" });\n    defer std.testing.allocator.free(file_path);\n\n    try tb.loadFile(file_path);\n\n    const line_count = tb.getLineCount();\n    try std.testing.expectEqual(@as(u32, 2), line_count);\n\n    const char_count = tb.getLength();\n    try std.testing.expectEqual(@as(u32, 6), char_count);\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var render_buffer: [200]u8 = undefined;\n    const render_written = try opt_buffer.writeResolvedChars(&render_buffer, false);\n    const render_result = render_buffer[0..render_written];\n\n    try std.testing.expect(std.mem.startsWith(u8, render_result, \"ABC\"));\n}\n\ntest \"drawTextBuffer - horizontal viewport offset renders correctly without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789ABCDEFGHIJ\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 5, .y = 0, .width = 10, .height = 1 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expect(std.mem.startsWith(u8, result, \"56789ABCDE\"));\n}\n\ntest \"drawTextBuffer - horizontal viewport offset with multiple lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNO\\n0123456789!@#$%\\nXYZ[\\\\]^_`{|}~\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 3, .y = 0, .width = 8, .height = 3 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        8,\n        3,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expect(std.mem.indexOf(u8, result, \"DEFGHIJK\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, result, \"3456789!\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, result, \"[\\\\]^_`{|\") != null);\n}\n\ntest \"drawTextBuffer - combined horizontal and vertical viewport offsets\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line0ABCDEFGHIJ\\nLine1KLMNOPQRST\\nLine2UVWXYZ0123\\nLine3456789!@#$\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 5, .y = 1, .width = 10, .height = 2 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        2,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expect(std.mem.indexOf(u8, result, \"KLMNOPQRST\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, result, \"UVWXYZ0123\") != null);\n}\n\ntest \"drawTextBuffer - horizontal viewport stops rendering at viewport width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 5, .y = 0, .width = 10, .height = 1 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expectEqualStrings(\"56789ABCDE\", result[0..10]);\n\n    const cell_9 = opt_buffer.get(9, 0);\n    try std.testing.expect(cell_9 != null);\n    try std.testing.expectEqual(@as(u32, 'E'), cell_9.?.char);\n}\n\ntest \"drawTextBuffer - horizontal viewport with small buffer renders only viewport width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 10, .y = 0, .width = 5, .height = 1 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const cell_0 = opt_buffer.get(0, 0);\n    try std.testing.expect(cell_0 != null);\n    try std.testing.expectEqual(@as(u32, 'K'), cell_0.?.char);\n\n    const cell_4 = opt_buffer.get(4, 0);\n    try std.testing.expect(cell_4 != null);\n    try std.testing.expectEqual(@as(u32, 'O'), cell_4.?.char);\n\n    const cell_5 = opt_buffer.get(5, 0);\n    try std.testing.expect(cell_5 != null);\n    try std.testing.expectEqual(@as(u32, 32), cell_5.?.char);\n\n    const cell_6 = opt_buffer.get(6, 0);\n    try std.testing.expect(cell_6 != null);\n    try std.testing.expectEqual(@as(u32, 32), cell_6.?.char);\n}\n\ntest \"drawTextBuffer - horizontal viewport width limits rendering (efficiency test)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var long_line: std.ArrayListUnmanaged(u8) = .{};\n    defer long_line.deinit(std.testing.allocator);\n    try long_line.appendNTimes(std.testing.allocator, 'A', 1000);\n\n    try tb.setText(long_line.items);\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 100, .y = 0, .width = 10, .height = 1 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        50,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var non_space_count: u32 = 0;\n    var i: u32 = 0;\n    while (i < 50) : (i += 1) {\n        if (opt_buffer.get(i, 0)) |cell| {\n            if (cell.char == 'A') {\n                non_space_count += 1;\n            }\n        }\n    }\n\n    try std.testing.expectEqual(@as(u32, 10), non_space_count);\n}\n\ntest \"drawTextBuffer - overwriting wide grapheme with ASCII leaves no ghost chars\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try tb.setText(\"世界\");\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const first_cell = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expect(gp.isGraphemeChar(first_cell.char));\n    try std.testing.expectEqual(@as(u32, 2), gp.encodedCharWidth(first_cell.char));\n\n    const second_cell = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expect(gp.isContinuationChar(second_cell.char));\n\n    try tb.setText(\"ABC\");\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const cell_a = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'A'), cell_a.char);\n    try std.testing.expect(!gp.isGraphemeChar(cell_a.char));\n    try std.testing.expect(!gp.isContinuationChar(cell_a.char));\n\n    const cell_b = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'B'), cell_b.char);\n    try std.testing.expect(!gp.isGraphemeChar(cell_b.char));\n    try std.testing.expect(!gp.isContinuationChar(cell_b.char));\n\n    const cell_c = opt_buffer.get(2, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'C'), cell_c.char);\n    try std.testing.expect(!gp.isGraphemeChar(cell_c.char));\n    try std.testing.expect(!gp.isContinuationChar(cell_c.char));\n\n    var out_buffer: [100]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n    try std.testing.expect(std.mem.startsWith(u8, result, \"ABC\"));\n}\n\ntest \"drawTextBuffer - syntax style destroy does not crash\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var style = try ss.SyntaxStyle.init(std.testing.allocator);\n    tb.setSyntaxStyle(style);\n\n    const style_id = try style.registerStyle(\"test\", .{ 1.0, 0.0, 0.0, 1.0 }, null, 0);\n    try tb.setText(\"Hello World\");\n    try tb.addHighlightByCharRange(0, 5, style_id, 1, 0);\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n    try std.testing.expect(std.mem.startsWith(u8, result, \"Hello World\"));\n\n    style.deinit();\n\n    try std.testing.expect(tb.getSyntaxStyle() == null);\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const written2 = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result2 = out_buffer[0..written2];\n    try std.testing.expect(std.mem.startsWith(u8, result2, \"Hello World\"));\n}\n\ntest \"drawTextBuffer - tabs are rendered as spaces (empty cells)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    tb.setTabWidth(4);\n\n    try tb.setText(\"A\\tB\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'A'), cell_0.char);\n\n    const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_1.char);\n\n    const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_2.char);\n\n    const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_3.char);\n\n    const cell_4 = opt_buffer.get(4, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_4.char);\n\n    // With static tabs: A at col 0, tab takes 4 cols (1-4), B at col 5\n    const cell_5 = opt_buffer.get(5, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'B'), cell_5.char);\n}\n\ntest \"drawTextBuffer - tab indicator renders with correct color\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    tb.setTabWidth(4);\n    try tb.setText(\"A\\tB\");\n\n    view.setTabIndicator(@as(u32, '→'));\n    view.setTabIndicatorColor(RGBA{ 0.25, 0.25, 0.25, 1.0 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'A'), cell_0.char);\n\n    const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '→'), cell_1.char);\n    try std.testing.expectEqual(@as(f32, 0.25), cell_1.fg[0]);\n    try std.testing.expectEqual(@as(f32, 0.25), cell_1.fg[1]);\n    try std.testing.expectEqual(@as(f32, 0.25), cell_1.fg[2]);\n\n    const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_2.char);\n\n    const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_3.char);\n\n    const cell_4 = opt_buffer.get(4, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_4.char);\n\n    // With static tabs: A at col 0, tab takes 4 cols (1-4), B at col 5\n    const cell_5 = opt_buffer.get(5, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'B'), cell_5.char);\n}\n\ntest \"drawTextBuffer - tab without indicator renders as spaces\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    tb.setTabWidth(4);\n    try tb.setText(\"A\\tB\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        20,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'A'), cell_0.char);\n\n    const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_1.char);\n\n    const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_2.char);\n\n    const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_3.char);\n\n    const cell_4 = opt_buffer.get(4, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 32), cell_4.char);\n\n    // With static tabs: A at col 0, tab takes 4 cols (1-4), B at col 5\n    const cell_5 = opt_buffer.get(5, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'B'), cell_5.char);\n}\n\ntest \"drawTextBuffer - mixed ASCII and Unicode with emoji renders completely\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"- ✅ All 881 native tests passs\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        50,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '-'), cell_0.char);\n\n    const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_1.char);\n\n    const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n    try std.testing.expect(gp.isGraphemeChar(cell_2.char));\n    const width_2 = gp.encodedCharWidth(cell_2.char);\n    try std.testing.expectEqual(@as(u32, 2), width_2);\n\n    const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expect(gp.isContinuationChar(cell_3.char));\n\n    const cell_4 = opt_buffer.get(4, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_4.char);\n\n    const cell_5 = opt_buffer.get(5, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'A'), cell_5.char);\n\n    const cell_6 = opt_buffer.get(6, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'l'), cell_6.char);\n\n    const cell_7 = opt_buffer.get(7, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'l'), cell_7.char);\n\n    const cell_8 = opt_buffer.get(8, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_8.char);\n\n    const cell_9 = opt_buffer.get(9, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '8'), cell_9.char);\n\n    const cell_10 = opt_buffer.get(10, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '8'), cell_10.char);\n\n    const cell_11 = opt_buffer.get(11, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '1'), cell_11.char);\n\n    const cell_12 = opt_buffer.get(12, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_12.char);\n\n    const cell_13 = opt_buffer.get(13, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'n'), cell_13.char);\n\n    const cell_14 = opt_buffer.get(14, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'a'), cell_14.char);\n\n    const cell_15 = opt_buffer.get(15, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 't'), cell_15.char);\n\n    const cell_16 = opt_buffer.get(16, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'i'), cell_16.char);\n\n    const cell_17 = opt_buffer.get(17, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'v'), cell_17.char);\n\n    const cell_18 = opt_buffer.get(18, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'e'), cell_18.char);\n\n    const cell_19 = opt_buffer.get(19, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_19.char);\n\n    const cell_20 = opt_buffer.get(20, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 't'), cell_20.char);\n\n    const cell_21 = opt_buffer.get(21, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'e'), cell_21.char);\n\n    const cell_22 = opt_buffer.get(22, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 's'), cell_22.char);\n\n    const cell_23 = opt_buffer.get(23, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 't'), cell_23.char);\n\n    const cell_24 = opt_buffer.get(24, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 's'), cell_24.char);\n\n    const cell_25 = opt_buffer.get(25, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_25.char);\n\n    const cell_26 = opt_buffer.get(26, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'p'), cell_26.char);\n\n    const cell_27 = opt_buffer.get(27, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'a'), cell_27.char);\n\n    const cell_28 = opt_buffer.get(28, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 's'), cell_28.char);\n\n    const cell_29 = opt_buffer.get(29, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 's'), cell_29.char);\n\n    const cell_30 = opt_buffer.get(30, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 's'), cell_30.char);\n\n    var out_buffer: [500]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expect(std.mem.indexOf(u8, result, \"- ✅ All 881 native tests passs\") != null);\n\n    const plain_text = tb.getPlainTextIntoBuffer(&out_buffer);\n    const plain_result = out_buffer[0..plain_text];\n    try std.testing.expectEqualStrings(\"- ✅ All 881 native tests passs\", plain_result);\n}\n\ntest \"viewport width = 31 exactly - last character rendering\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"- ✅ All 881 native tests passs\");\n\n    // Set viewport width to EXACTLY 31 (the display width needed)\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 31, .height = 1 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        50,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    // BUG CHECK: The last 's' at cell 30 should be present\n    const cell_30 = opt_buffer.get(30, 0);\n    if (cell_30) |c| {\n        try std.testing.expectEqual(@as(u32, 's'), c.char);\n    } else {\n        return error.TestFailed;\n    }\n}\n\ntest \"drawTextBuffer - complex multilingual text with diverse scripts and emojis\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const text =\n        \\\\# The Celestial Journey of संस्कृति 🌟🔮✨\n        \\\\In the beginning, there was नमस्ते 🙏 and the ancient wisdom of the ॐ symbol echoing through dimensions. The travelers 🧑‍🚀👨‍🚀👩‍🚀 embarked on their quest through the cosmos, guided by the mysterious རྒྱ་མཚོ and the luminous 🌈🦄🧚‍♀️ beings of light. They encountered the great देवनागरी scribes who wrote in flowing अक्षर characters, documenting everything in their sacred texts 📜📖✍️.\n        \\\\## Chapter प्रथम: The Eastern Gardens 🏯🎋🌸\n        \\\\The journey led them to the mystical lands where 漢字 (kanji) danced with ひらがな and カタカナ across ancient scrolls 📯🎴🎎. In the gardens of Seoul, they found 한글 inscriptions speaking of 사랑 (love) and 평화 (peace) 💝🕊️☮️. The monks meditated under the bodhi tree 🧘‍♂️🌳, contemplating the nature of धर्म while drinking matcha 🍵 and eating 餃子 dumplings 🥟.\n        \\\\Strange creatures emerged from the mist: 🦥🦦🦧🦨🦩🦚🦜🦝🦞🦟. They spoke in riddles about the प्राचीन (ancient) ways and the नवीन (new) paths forward. \"भविष्य में क्या है?\" they asked, while the ໂຫຍ່າກເຈົ້າ whispered secrets in Lao script 🤫🗣️💬.\n        \\\\## The संगम (Confluence) of Scripts 🌊📝🎭\n        \\\\At the great confluence, they witnessed the merger of བོད་ཡིག (Tibetan), ગુજરાતી (Gujarati), and தமிழ் (Tamil) scripts flowing together like rivers 🏞️🌊💧. The scholars debated about ਪੰਜਾਬੀ philosophy while juggling 🤹‍♂️🎪🎨 colorful orbs that represented different తెలుగు concepts.\n        \\\\The marketplace buzzed with activity 🏪🛒💰: merchants sold বাংলা spices 🌶️🧄🧅, ಕನ್ನಡ silks 🧵👘, and മലയാളം handicrafts 🎨🖼️. Children played with toys shaped like 🦖🦕🐉🐲 while their parents bargained using ancient ଓଡ଼ିଆ numerals and gestures 🤝🤲👐.\n        \\\\## The Festival of ๑๐๐ Lanterns 🏮🎆🎇\n        \\\\During the grand festival, they lit exactly ๑๐๐ (100 in Thai numerals) lanterns 🏮🕯️💡 that floated into the night sky like ascending ความหวัง (hopes). The celebration featured dancers 💃🕺🩰 performing classical moves from भरतनाट्यम tradition, their मुद्रा hand gestures telling stories of प्रेम and वीरता.\n        \\\\Musicians played unusual instruments: the 🎻🎺🎷🎸🪕🪘 ensemble created harmonies that resonated with the वेद chants and མཆོད་རྟེན bells 🔔⛩️. The audience sat mesmerized 😵‍💫🤯✨, some sipping on bubble tea 🧋 while others enjoyed मिठाई sweets 🍬🍭🧁.\n        \\\\## The འཕྲུལ་དེབ (Machine) Age Arrives ⚙️🤖🦾\n        \\\\As modernity crept in, the ancient འཁོར་ལོ (wheel) gave way to 🚗🚕🚙🚌🚎 vehicles and eventually to 🚀🛸🛰️ spacecraft. The યુવાન (youth) learned to code in Python 🐍💻⌨️, but still honored their గురువు (teachers) who taught them the old ways of ज्ञान acquisition 🧠📚🎓.\n        \\\\The সমাজ (society) transformed: robots 🤖🦾🦿 worked alongside humans 👨‍💼👩‍💼👨‍🔬👩‍🔬, and AI learned to read སྐད (languages) from across the planet 🌍🌎🌏. Yet somehow, the essence of मानवता remained intact, preserved in the கவிதை (poetry) and the ກາບແກ້ວ stories passed down through generations 👴👵👨‍👩‍👧‍👦.\n        \\\\## The Final ಅಧ್ಯಾಯ (Chapter) 🌅🌄🌠\n        \\\\As the sun set over the പർവ്വതങ്ങൾ (mountains) 🏔️⛰️🗻, our travelers realized that every script, every symbol—from ا to ㄱ to অ to अ—represented not just sounds, but entire civilizations' worth of विचार (thoughts) and ಕನಸು (dreams) 💭💤🌌.\n        \\\\They gathered around the final campfire 🔥🏕️, sharing stories in ภาษา (languages) both ancient and new. Someone brought out a guitar 🎸 and started singing in ગીત form, while others prepared ආහාර (food) 🍛🍲🥘 seasoned with love ❤️💕💖 and memories 📸🎞️📹.\n        \\\\And so they learned that whether written in দেবনাগরী, 中文, 한글, or ไทย, the human experience transcends boundaries 🌐🤝🌈. The weird emojis 🦩🧿🪬🫀🫁🧠 and complex scripts were all part of the same beautiful བསྟན་པ (teaching): that diversity is our greatest strength 💪✊🙌.\n        \\\\The end. समाप्त. 끝. จบ. முடிவு. ముగింపు. সমাপ্তি. ഒടുക്കം. ಅಂತ್ಯ. અંત. 🎬🎭🎪✨🌟⭐\n        \\\\\n    ;\n\n    try tb.setText(text);\n\n    // Test with word wrapping\n    view.setWrapMode(.word);\n    view.setWrapWidth(80);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        100,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    // Verify the text buffer can handle complex multilingual content\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expect(virtual_lines.len > 0);\n\n    // Test that we can get the plain text back\n    var plain_buffer: [10000]u8 = undefined;\n    const plain_len = tb.getPlainTextIntoBuffer(&plain_buffer);\n    const plain_text = plain_buffer[0..plain_len];\n\n    // Verify some key multilingual content is present\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"संस्कृति\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"नमस्ते\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"漢字\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"한글\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"தமிழ்\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"বাংলা\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"ಕನ್ನಡ\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"മലയാളം\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"🌟\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, plain_text, \"🙏\") != null);\n\n    // Test with no wrapping\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.updateVirtualLines();\n\n    const no_wrap_lines = view.getVirtualLines();\n    // Should have one line per actual newline in the text\n    try std.testing.expect(no_wrap_lines.len > 10);\n\n    // Test with character wrapping on narrow width\n    view.setWrapMode(.char);\n    view.setWrapWidth(40);\n    view.updateVirtualLines();\n\n    const char_wrap_lines = view.getVirtualLines();\n    // Should wrap into many more lines\n    try std.testing.expect(char_wrap_lines.len > virtual_lines.len);\n\n    // Test viewport scrolling through the content\n    view.setWrapMode(.word);\n    view.setWrapWidth(80);\n    view.setViewport(.{ .x = 0, .y = 10, .width = 80, .height = 20 });\n    view.updateVirtualLines();\n\n    const viewport_lines = view.getVirtualLines();\n    try std.testing.expect(viewport_lines.len <= 20);\n\n    // Verify rendering doesn't crash with complex emoji sequences\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    // Test that line count is reasonable\n    const line_count = tb.getLineCount();\n    try std.testing.expect(line_count > 15);\n}\n\ntest \"setStyledText - highlight positioning with Unicode text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    // Text: \"Say नमस्ते please.\"\n    // Layout: \"Say \" (4 cols) + \"नमस्ते\" (4 cols) + \" \" (1 col) + \"please\" (6 cols) + \".\" (1 col)\n    // We highlight \"please\" with a green background to verify correct positioning\n    const text_part1 = \"Say \";\n    const text_part2 = \"नमस्ते\";\n    const text_part3 = \" \";\n    const text_part4 = \"please\";\n    const text_part5 = \".\";\n\n    const fg_normal = [4]f32{ 1.0, 1.0, 1.0, 1.0 };\n    const bg_highlight = [4]f32{ 0.0, 1.0, 0.0, 1.0 }; // Green background\n\n    const chunks = [_]StyledChunk{\n        .{ .text_ptr = text_part1.ptr, .text_len = text_part1.len, .fg_ptr = @ptrCast(&fg_normal), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = text_part2.ptr, .text_len = text_part2.len, .fg_ptr = @ptrCast(&fg_normal), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = text_part3.ptr, .text_len = text_part3.len, .fg_ptr = @ptrCast(&fg_normal), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = text_part4.ptr, .text_len = text_part4.len, .fg_ptr = @ptrCast(&fg_normal), .bg_ptr = @ptrCast(&bg_highlight), .attributes = 0 },\n        .{ .text_ptr = text_part5.ptr, .text_len = text_part5.len, .fg_ptr = @ptrCast(&fg_normal), .bg_ptr = null, .attributes = 0 },\n    };\n\n    try tb.setStyledText(&chunks);\n\n    // Verify the text content\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    const result = out_buffer[0..written];\n    try std.testing.expectEqualStrings(\"Say नमस्ते please.\", result);\n\n    // Calculate expected positions using measureText\n    const part1_width = tb.measureText(text_part1);\n    const part2_width = tb.measureText(text_part2);\n    const part3_width = tb.measureText(text_part3);\n    const please_start_col = part1_width + part2_width + part3_width;\n\n    // Render to buffer and check colors\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        30,\n        5,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    // Check that \"please\" (6 characters) all have the green background\n    const epsilon: f32 = 0.01;\n    var i: u32 = 0;\n    while (i < 6) : (i += 1) {\n        const cell_col = please_start_col + i;\n        const cell = opt_buffer.get(cell_col, 0) orelse return error.TestFailed;\n\n        // Verify green background (R=0, G=1, B=0)\n        try std.testing.expect(@abs(cell.bg[0] - 0.0) < epsilon);\n        try std.testing.expect(@abs(cell.bg[1] - 1.0) < epsilon);\n        try std.testing.expect(@abs(cell.bg[2] - 0.0) < epsilon);\n    }\n\n    // Check that text before \"please\" does NOT have green background\n    i = 0;\n    while (i < please_start_col) : (i += 1) {\n        const cell = opt_buffer.get(i, 0) orelse unreachable;\n        const has_green_bg = @abs(cell.bg[1] - 1.0) < epsilon and @abs(cell.bg[0] - 0.0) < epsilon;\n        try std.testing.expect(!has_green_bg);\n    }\n\n    // Check that \".\" after \"please\" does NOT have green background\n    const period_col = please_start_col + 6;\n    const period_cell = opt_buffer.get(period_col, 0) orelse unreachable;\n    const has_green_bg = @abs(period_cell.bg[1] - 1.0) < epsilon and @abs(period_cell.bg[0] - 0.0) < epsilon;\n    try std.testing.expect(!has_green_bg);\n}\n\ntest \"drawTextBuffer - multiple syntax highlights with various horizontal viewport offsets\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    // Register different color styles\n    const red_style = try style.registerStyle(\"red\", RGBA{ 1.0, 0.0, 0.0, 1.0 }, null, 0);\n    const green_style = try style.registerStyle(\"green\", RGBA{ 0.0, 1.0, 0.0, 1.0 }, null, 0);\n    const blue_style = try style.registerStyle(\"blue\", RGBA{ 0.0, 0.0, 1.0, 1.0 }, null, 0);\n    const yellow_style = try style.registerStyle(\"yellow\", RGBA{ 1.0, 1.0, 0.0, 1.0 }, null, 0);\n\n    // Text: \"const x = function(y) { return y * 2; }\"\n    const test_text = \"const x = function(y) { return y * 2; }\";\n    // Positions (0-indexed):\n    // \"const\" is at 0-5 (exclusive end, so 0,1,2,3,4)\n    // \"function\" is at 10-18 (chars 10-17)\n    // \"return\" is at 24-30 (chars 24-29)\n    // \"2\" is at 35-36 (char 35)\n\n    try tb.setText(test_text);\n\n    try tb.addHighlightByCharRange(0, 5, red_style, 1, 0); // \"const\"\n    try tb.addHighlightByCharRange(10, 18, green_style, 1, 0); // \"function\"\n    try tb.addHighlightByCharRange(24, 30, blue_style, 1, 0); // \"return\"\n    try tb.addHighlightByCharRange(35, 36, yellow_style, 1, 0); // \"2\"\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n\n    const epsilon: f32 = 0.01;\n\n    // Test 1: Viewport at x=0 (no scroll)\n    {\n        view.setViewport(.{ .x = 0, .y = 0, .width = 40, .height = 1 });\n        var opt_buffer = try OptimizedBuffer.init(std.testing.allocator, 40, 1, .{ .pool = pool, .width_method = .unicode });\n        defer opt_buffer.deinit();\n\n        try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        try opt_buffer.drawTextBuffer(view, 0, 0);\n\n        // Check \"const\" is red\n        const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'c'), cell_0.char);\n        try std.testing.expect(@abs(cell_0.fg[0] - 1.0) < epsilon); // Red\n\n        const cell_4 = opt_buffer.get(4, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 't'), cell_4.char);\n        try std.testing.expect(@abs(cell_4.fg[0] - 1.0) < epsilon); // Red\n\n        // Check \"function\" is green\n        const cell_10 = opt_buffer.get(10, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'f'), cell_10.char);\n        try std.testing.expect(@abs(cell_10.fg[1] - 1.0) < epsilon); // Green\n\n        const cell_17 = opt_buffer.get(17, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'n'), cell_17.char);\n        try std.testing.expect(@abs(cell_17.fg[1] - 1.0) < epsilon); // Green\n    }\n\n    // Test 2: Viewport scrolled to x=3 (showing \"st x = fun...\")\n    {\n        view.setViewport(.{ .x = 3, .y = 0, .width = 20, .height = 1 });\n        var opt_buffer = try OptimizedBuffer.init(std.testing.allocator, 20, 1, .{ .pool = pool, .width_method = .unicode });\n        defer opt_buffer.deinit();\n\n        try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        try opt_buffer.drawTextBuffer(view, 0, 0);\n\n        // Buffer shows characters 3-22 from source: \"st x = function(y) {\"\n        // Position 0: 's' (source 3) - should be RED (part of \"const\" 0-5)\n        // Position 1: 't' (source 4) - should be RED (part of \"const\" 0-5)\n        // Position 2: ' ' (source 5) - NOT red (outside \"const\")\n        // Position 7: 'f' (source 10) - should be GREEN (start of \"function\" 10-18)\n        // Position 14: 'n' (source 17) - should be GREEN (part of \"function\")\n\n        const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 's'), cell_0.char);\n        try std.testing.expect(@abs(cell_0.fg[0] - 1.0) < epsilon); // Red\n        try std.testing.expect(@abs(cell_0.fg[1] - 0.0) < epsilon);\n        try std.testing.expect(@abs(cell_0.fg[2] - 0.0) < epsilon);\n\n        const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 't'), cell_1.char);\n        try std.testing.expect(@abs(cell_1.fg[0] - 1.0) < epsilon); // Red\n        try std.testing.expect(@abs(cell_1.fg[1] - 0.0) < epsilon);\n        try std.testing.expect(@abs(cell_1.fg[2] - 0.0) < epsilon);\n\n        const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, ' '), cell_2.char);\n        try std.testing.expect(@abs(cell_2.fg[0] - 1.0) < epsilon); // White (default)\n        try std.testing.expect(@abs(cell_2.fg[1] - 1.0) < epsilon);\n        try std.testing.expect(@abs(cell_2.fg[2] - 1.0) < epsilon);\n\n        const cell_7 = opt_buffer.get(7, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'f'), cell_7.char);\n        try std.testing.expect(@abs(cell_7.fg[0] - 0.0) < epsilon); // Green\n        try std.testing.expect(@abs(cell_7.fg[1] - 1.0) < epsilon);\n        try std.testing.expect(@abs(cell_7.fg[2] - 0.0) < epsilon);\n\n        const cell_14 = opt_buffer.get(14, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'n'), cell_14.char);\n        try std.testing.expect(@abs(cell_14.fg[0] - 0.0) < epsilon); // Green\n        try std.testing.expect(@abs(cell_14.fg[1] - 1.0) < epsilon);\n        try std.testing.expect(@abs(cell_14.fg[2] - 0.0) < epsilon);\n    }\n\n    // Test 4: Viewport scrolled to x=30 (showing \"y * 2; }\" based on 40 char text)\n    {\n        view.setViewport(.{ .x = 30, .y = 0, .width = 20, .height = 1 });\n        var opt_buffer = try OptimizedBuffer.init(std.testing.allocator, 20, 1, .{ .pool = pool, .width_method = .unicode });\n        defer opt_buffer.deinit();\n\n        try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        try opt_buffer.drawTextBuffer(view, 0, 0);\n\n        // Actual rendering shows: \" y * 2; }\"\n        // Source chars 30-38 are shown\n        // Position 0: ' ' (source 30) - white\n        // Position 5: '2' (source 35) - should be YELLOW (highlighted 35-36)\n\n        const cell_5 = opt_buffer.get(5, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, '2'), cell_5.char);\n        try std.testing.expect(@abs(cell_5.fg[0] - 1.0) < epsilon); // Yellow\n        try std.testing.expect(@abs(cell_5.fg[1] - 1.0) < epsilon);\n        try std.testing.expect(@abs(cell_5.fg[2] - 0.0) < epsilon);\n    }\n}\n\ntest \"drawTextBuffer - syntax highlighting with horizontal viewport offset\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    // Register a red style\n    const red_style_id = try style.registerStyle(\"keyword\", RGBA{ 1.0, 0.0, 0.0, 1.0 }, null, 0);\n\n    // Text: \"const x = 1\"\n    // Highlight \"const\" (characters 0-5) in red\n    try tb.setText(\"const x = 1\");\n    try tb.addHighlightByCharRange(0, 5, red_style_id, 1, 0);\n\n    // Set viewport to skip first 3 characters, showing \"st x = 1\"\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 3, .y = 0, .width = 10, .height = 1 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const epsilon: f32 = 0.01;\n    const red_fg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n\n    // Check that 's' at buffer position 0 is RED\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 's'), cell_0.char);\n    const is_red_0 = @abs(cell_0.fg[0] - red_fg[0]) < epsilon and\n        @abs(cell_0.fg[1] - red_fg[1]) < epsilon and\n        @abs(cell_0.fg[2] - red_fg[2]) < epsilon;\n    try std.testing.expect(is_red_0);\n\n    // Check that 't' at buffer position 1 is RED\n    const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 't'), cell_1.char);\n    const is_red_1 = @abs(cell_1.fg[0] - red_fg[0]) < epsilon and\n        @abs(cell_1.fg[1] - red_fg[1]) < epsilon and\n        @abs(cell_1.fg[2] - red_fg[2]) < epsilon;\n    try std.testing.expect(is_red_1);\n\n    // Check that ' ' at buffer position 2 is NOT RED\n    const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_2.char);\n    const is_red_2 = @abs(cell_2.fg[0] - red_fg[0]) < epsilon and\n        @abs(cell_2.fg[1] - red_fg[1]) < epsilon and\n        @abs(cell_2.fg[2] - red_fg[2]) < epsilon;\n    try std.testing.expect(!is_red_2);\n\n    // Check that 'x' at buffer position 3 is NOT RED\n    const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'x'), cell_3.char);\n    const is_red_3 = @abs(cell_3.fg[0] - red_fg[0]) < epsilon and\n        @abs(cell_3.fg[1] - red_fg[1]) < epsilon and\n        @abs(cell_3.fg[2] - red_fg[2]) < epsilon;\n    try std.testing.expect(!is_red_3);\n}\n\ntest \"drawTextBuffer - setStyledText with multiple colors and horizontal scrolling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    // Simulate what code renderable does with setStyledText\n    // Text will be: \"const x = function(y) { return y * 2; }\"\n    // But split into colored chunks like syntax highlighting\n\n    const chunk1_text = \"const\";\n    const chunk2_text = \" x = \";\n    const chunk3_text = \"function\";\n    const chunk4_text = \"(y) { \";\n    const chunk5_text = \"return\";\n    const chunk6_text = \" y * \";\n    const chunk7_text = \"2\";\n    const chunk8_text = \"; }\";\n\n    const red_color = [4]f32{ 1.0, 0.0, 0.0, 1.0 };\n    const white_color = [4]f32{ 1.0, 1.0, 1.0, 1.0 };\n    const green_color = [4]f32{ 0.0, 1.0, 0.0, 1.0 };\n    const blue_color = [4]f32{ 0.0, 0.0, 1.0, 1.0 };\n    const yellow_color = [4]f32{ 1.0, 1.0, 0.0, 1.0 };\n\n    const chunks = [_]StyledChunk{\n        .{ .text_ptr = chunk1_text.ptr, .text_len = chunk1_text.len, .fg_ptr = @ptrCast(&red_color), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = chunk2_text.ptr, .text_len = chunk2_text.len, .fg_ptr = @ptrCast(&white_color), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = chunk3_text.ptr, .text_len = chunk3_text.len, .fg_ptr = @ptrCast(&green_color), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = chunk4_text.ptr, .text_len = chunk4_text.len, .fg_ptr = @ptrCast(&white_color), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = chunk5_text.ptr, .text_len = chunk5_text.len, .fg_ptr = @ptrCast(&blue_color), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = chunk6_text.ptr, .text_len = chunk6_text.len, .fg_ptr = @ptrCast(&white_color), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = chunk7_text.ptr, .text_len = chunk7_text.len, .fg_ptr = @ptrCast(&yellow_color), .bg_ptr = null, .attributes = 0 },\n        .{ .text_ptr = chunk8_text.ptr, .text_len = chunk8_text.len, .fg_ptr = @ptrCast(&white_color), .bg_ptr = null, .attributes = 0 },\n    };\n\n    try tb.setStyledText(&chunks);\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n\n    const epsilon: f32 = 0.01;\n\n    // Helper to check if color matches\n    const isRed = struct {\n        fn check(fg: RGBA, eps: f32) bool {\n            return @abs(fg[0] - 1.0) < eps and @abs(fg[1] - 0.0) < eps and @abs(fg[2] - 0.0) < eps;\n        }\n    }.check;\n\n    const isGreen = struct {\n        fn check(fg: RGBA, eps: f32) bool {\n            return @abs(fg[0] - 0.0) < eps and @abs(fg[1] - 1.0) < eps and @abs(fg[2] - 0.0) < eps;\n        }\n    }.check;\n\n    const isBlue = struct {\n        fn check(fg: RGBA, eps: f32) bool {\n            return @abs(fg[0] - 0.0) < eps and @abs(fg[1] - 0.0) < eps and @abs(fg[2] - 1.0) < eps;\n        }\n    }.check;\n\n    const isYellow = struct {\n        fn check(fg: RGBA, eps: f32) bool {\n            return @abs(fg[0] - 1.0) < eps and @abs(fg[1] - 1.0) < eps and @abs(fg[2] - 0.0) < eps;\n        }\n    }.check;\n\n    const isWhite = struct {\n        fn check(fg: RGBA, eps: f32) bool {\n            return @abs(fg[0] - 1.0) < eps and @abs(fg[1] - 1.0) < eps and @abs(fg[2] - 1.0) < eps;\n        }\n    }.check;\n\n    // Test at x=0 (no scroll)\n    {\n        view.setViewport(.{ .x = 0, .y = 0, .width = 40, .height = 1 });\n        var opt_buffer = try OptimizedBuffer.init(std.testing.allocator, 40, 1, .{ .pool = pool, .width_method = .unicode });\n        defer opt_buffer.deinit();\n\n        try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        try opt_buffer.drawTextBuffer(view, 0, 0);\n\n        const cell_0 = opt_buffer.get(0, 0) orelse unreachable; // 'c' from \"const\"\n        try std.testing.expectEqual(@as(u32, 'c'), cell_0.char);\n        try std.testing.expect(isRed(cell_0.fg, epsilon));\n\n        const cell_10 = opt_buffer.get(10, 0) orelse unreachable; // 'f' from \"function\"\n        try std.testing.expectEqual(@as(u32, 'f'), cell_10.char);\n        try std.testing.expect(isGreen(cell_10.fg, epsilon));\n    }\n\n    // Test at x=5 (scrolled past \"const\")\n    {\n        view.setViewport(.{ .x = 5, .y = 0, .width = 20, .height = 1 });\n        var opt_buffer = try OptimizedBuffer.init(std.testing.allocator, 20, 1, .{ .pool = pool, .width_method = .unicode });\n        defer opt_buffer.deinit();\n\n        try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        try opt_buffer.drawTextBuffer(view, 0, 0);\n\n        // At x=5, showing chars 5-24: \" x = function(y) { \"\n        // Position 0: ' ' (source 5) - should be white\n        // Position 5: 'f' (source 10) - should be GREEN\n        const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, ' '), cell_0.char);\n        try std.testing.expect(isWhite(cell_0.fg, epsilon));\n\n        const cell_5 = opt_buffer.get(5, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'f'), cell_5.char);\n        try std.testing.expect(isGreen(cell_5.fg, epsilon));\n    }\n\n    // Test at x=15 (in middle of \"function\")\n    {\n        view.setViewport(.{ .x = 15, .y = 0, .width = 20, .height = 1 });\n        var opt_buffer = try OptimizedBuffer.init(std.testing.allocator, 20, 1, .{ .pool = pool, .width_method = .unicode });\n        defer opt_buffer.deinit();\n\n        try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        try opt_buffer.drawTextBuffer(view, 0, 0);\n\n        // At x=15, showing chars 15-34: \"ion(y) { return y * \"\n        // \"const x = function...\"\n        //  0123456789012345678...\n        // Position 0: 'i' (source 15) - should be GREEN (part of \"function\" 10-18)\n        // Position 1: 'o' (source 16) - should be GREEN\n        // Position 2: 'n' (source 17) - should be GREEN\n        // Position 3: '(' (source 18) - should be WHITE (end of \"function\")\n        const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'i'), cell_0.char);\n        try std.testing.expect(isGreen(cell_0.fg, epsilon));\n\n        const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'o'), cell_1.char);\n        try std.testing.expect(isGreen(cell_1.fg, epsilon));\n\n        const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'n'), cell_2.char);\n        try std.testing.expect(isGreen(cell_2.fg, epsilon));\n\n        const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, '('), cell_3.char);\n        try std.testing.expect(isWhite(cell_3.fg, epsilon));\n\n        // Position 9: 'r' (source 24) - should be BLUE (start of \"return\" 24-30)\n        const cell_9 = opt_buffer.get(9, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'r'), cell_9.char);\n        try std.testing.expect(isBlue(cell_9.fg, epsilon));\n    }\n\n    // Test at x=25 (past \"return\")\n    {\n        view.setViewport(.{ .x = 25, .y = 0, .width = 20, .height = 1 });\n        var opt_buffer = try OptimizedBuffer.init(std.testing.allocator, 20, 1, .{ .pool = pool, .width_method = .unicode });\n        defer opt_buffer.deinit();\n\n        try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n        try opt_buffer.drawTextBuffer(view, 0, 0);\n\n        // At x=25, showing chars 25-44: \"eturn y * 2; }\"\n        // Position 0: 'e' (source 25) - should be BLUE (part of \"return\" 24-30)\n        // Position 4: 'n' (source 29) - should be BLUE\n        // Position 5: ' ' (source 30) - should be WHITE (end of \"return\")\n        // Position 10: '2' (source 35) - should be YELLOW\n        const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'e'), cell_0.char);\n        try std.testing.expect(isBlue(cell_0.fg, epsilon));\n\n        const cell_4 = opt_buffer.get(4, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, 'n'), cell_4.char);\n        try std.testing.expect(isBlue(cell_4.fg, epsilon));\n\n        const cell_5 = opt_buffer.get(5, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, ' '), cell_5.char);\n        try std.testing.expect(isWhite(cell_5.fg, epsilon));\n\n        const cell_10 = opt_buffer.get(10, 0) orelse unreachable;\n        try std.testing.expectEqual(@as(u32, '2'), cell_10.char);\n        try std.testing.expect(isYellow(cell_10.fg, epsilon));\n    }\n}\n\ntest \"drawTextBuffer - selection with horizontal viewport offset\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    // Text: \"0123456789ABCDEFGHIJ\"\n    // We'll set viewport to x=5, showing \"56789ABCDE\"\n    // Then we'll select characters 7-12 (which are \"789AB\")\n    // Expected: in the rendered buffer, \"789AB\" should be highlighted\n    try tb.setText(\"0123456789ABCDEFGHIJ\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setViewport(.{ .x = 5, .y = 0, .width = 10, .height = 1 });\n\n    // Select characters at positions 7-12 in the original text (\"789AB\")\n    view.setSelection(7, 12, RGBA{ 1.0, 1.0, 0.0, 1.0 }, RGBA{ 0.0, 0.0, 0.0, 1.0 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    // The viewport shows positions 5-14 of the text\n    // Characters 7-11 (0-indexed) should be highlighted\n    // In the buffer:\n    // Position 0: '5' - not highlighted\n    // Position 1: '6' - not highlighted\n    // Position 2: '7' - HIGHLIGHTED (char pos 7)\n    // Position 3: '8' - HIGHLIGHTED (char pos 8)\n    // Position 4: '9' - HIGHLIGHTED (char pos 9)\n    // Position 5: 'A' - HIGHLIGHTED (char pos 10)\n    // Position 6: 'B' - HIGHLIGHTED (char pos 11)\n    // Position 7: 'C' - not highlighted (char pos 12, selection end is exclusive)\n    // Position 8: 'D' - not highlighted\n    // Position 9: 'E' - not highlighted\n\n    const epsilon: f32 = 0.01;\n    const yellow_bg = RGBA{ 1.0, 1.0, 0.0, 1.0 };\n\n    // Check non-highlighted cells\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '5'), cell_0.char);\n    const has_yellow_0 = @abs(cell_0.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_0.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_0.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(!has_yellow_0);\n\n    const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '6'), cell_1.char);\n    const has_yellow_1 = @abs(cell_1.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_1.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_1.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(!has_yellow_1);\n\n    // Check highlighted cells\n    const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '7'), cell_2.char);\n    const has_yellow_2 = @abs(cell_2.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_2.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_2.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(has_yellow_2);\n\n    const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '8'), cell_3.char);\n    const has_yellow_3 = @abs(cell_3.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_3.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_3.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(has_yellow_3);\n\n    const cell_6 = opt_buffer.get(6, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'B'), cell_6.char);\n    const has_yellow_6 = @abs(cell_6.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_6.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_6.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(has_yellow_6);\n\n    // Check cells after selection\n    const cell_7 = opt_buffer.get(7, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'C'), cell_7.char);\n    const has_yellow_7 = @abs(cell_7.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_7.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_7.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(!has_yellow_7);\n}\n\ntest \"drawTextBuffer - syntax highlight respects truncation\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    const red_style = try style.registerStyle(\"red\", RGBA{ 1.0, 0.0, 0.0, 1.0 }, null, 0);\n    const green_style = try style.registerStyle(\"green\", RGBA{ 0.0, 1.0, 0.0, 1.0 }, null, 0);\n\n    try tb.setText(\"0123456789ABCDEFGHIJ\");\n    try tb.addHighlightByCharRange(4, 7, red_style, 1, 0); // highlight \"456\"\n    try tb.addHighlightByCharRange(16, 20, green_style, 1, 0); // highlight \"GHIJ\"\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setTruncate(true);\n    view.setViewport(.{ .x = 0, .y = 0, .width = 10, .height = 1 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const epsilon: f32 = 0.01;\n\n    const prefix_cell = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '1'), prefix_cell.char);\n    try std.testing.expect(@abs(prefix_cell.fg[0] - 1.0) < epsilon);\n    try std.testing.expect(@abs(prefix_cell.fg[1] - 1.0) < epsilon);\n    try std.testing.expect(@abs(prefix_cell.fg[2] - 1.0) < epsilon);\n\n    const ellipsis_cell = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '.'), ellipsis_cell.char);\n    try std.testing.expect(@abs(ellipsis_cell.fg[0] - 1.0) < epsilon);\n    try std.testing.expect(@abs(ellipsis_cell.fg[1] - 1.0) < epsilon);\n    try std.testing.expect(@abs(ellipsis_cell.fg[2] - 1.0) < epsilon);\n\n    const suffix_cell = opt_buffer.get(6, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'G'), suffix_cell.char);\n    try std.testing.expect(@abs(suffix_cell.fg[0] - 0.0) < epsilon);\n    try std.testing.expect(@abs(suffix_cell.fg[1] - 1.0) < epsilon);\n    try std.testing.expect(@abs(suffix_cell.fg[2] - 0.0) < epsilon);\n}\n\ntest \"drawTextBuffer - highlight spanning ellipsis continues on suffix\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n    tb.setSyntaxStyle(style);\n\n    const magenta_style = try style.registerStyle(\"magenta\", RGBA{ 1.0, 0.0, 1.0, 1.0 }, null, 0);\n    const green_style = try style.registerStyle(\"green\", RGBA{ 0.0, 1.0, 0.0, 1.0 }, null, 0);\n\n    try tb.setText(\"0123456789ABCDEFGHIJ\");\n    try tb.addHighlightByCharRange(2, 18, magenta_style, 1, 0); // spans through ellipsis\n    try tb.addHighlightByCharRange(18, 20, green_style, 2, 0); // suffix highlight\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setTruncate(true);\n    view.setViewport(.{ .x = 0, .y = 0, .width = 10, .height = 1 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const epsilon: f32 = 0.01;\n\n    const ellipsis_cell = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '.'), ellipsis_cell.char);\n    try std.testing.expect(@abs(ellipsis_cell.fg[0] - 1.0) < epsilon);\n    try std.testing.expect(@abs(ellipsis_cell.fg[1] - 1.0) < epsilon);\n    try std.testing.expect(@abs(ellipsis_cell.fg[2] - 1.0) < epsilon);\n\n    const suffix_magenta = opt_buffer.get(6, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'G'), suffix_magenta.char);\n    try std.testing.expect(@abs(suffix_magenta.fg[0] - 1.0) < epsilon);\n    try std.testing.expect(@abs(suffix_magenta.fg[1] - 0.0) < epsilon);\n    try std.testing.expect(@abs(suffix_magenta.fg[2] - 1.0) < epsilon);\n\n    const suffix_green = opt_buffer.get(8, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'I'), suffix_green.char);\n    try std.testing.expect(@abs(suffix_green.fg[0] - 0.0) < epsilon);\n    try std.testing.expect(@abs(suffix_green.fg[1] - 1.0) < epsilon);\n    try std.testing.expect(@abs(suffix_green.fg[2] - 0.0) < epsilon);\n}\n\ntest \"drawTextBuffer - selection respects truncation\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    // Text: \"0123456789ABCDEFGHIJ\" (len 20)\n    // With width 10, truncation should render: \"012...GHIJ\"\n    try tb.setText(\"0123456789ABCDEFGHIJ\");\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setTruncate(true);\n    view.setViewport(.{ .x = 0, .y = 0, .width = 10, .height = 1 });\n\n    // Select across the ellipsis and suffix\n    view.setSelection(2, 19, RGBA{ 1.0, 1.0, 0.0, 1.0 }, RGBA{ 0.0, 0.0, 0.0, 1.0 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const epsilon: f32 = 0.01;\n    const yellow_bg = RGBA{ 1.0, 1.0, 0.0, 1.0 };\n\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '0'), cell_0.char);\n    const has_yellow_0 = @abs(cell_0.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_0.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_0.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(!has_yellow_0);\n\n    const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '.'), cell_3.char);\n    const has_yellow_3 = @abs(cell_3.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_3.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_3.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(has_yellow_3);\n\n    const cell_6 = opt_buffer.get(6, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'G'), cell_6.char);\n    const has_yellow_6 = @abs(cell_6.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_6.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_6.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(has_yellow_6);\n\n    const cell_8 = opt_buffer.get(8, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'I'), cell_8.char);\n    const has_yellow_8 = @abs(cell_8.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_8.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_8.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(has_yellow_8);\n\n    const cell_9 = opt_buffer.get(9, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'J'), cell_9.char);\n    const has_yellow_9 = @abs(cell_9.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(cell_9.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(cell_9.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(!has_yellow_9);\n}\n\ntest \"drawTextBuffer - truncation selection does not overshoot multiline\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\n        \"abcdefghijABCDEFGHIJ\\n\" ++\n            \"klmnopqrstKLMNOPQRST\",\n    );\n\n    view.setWrapMode(.none);\n    view.setWrapWidth(null);\n    view.setTruncate(true);\n    view.setViewport(.{ .x = 0, .y = 0, .width = 10, .height = 2 });\n\n    // Select from line 1 col 2 through line 2 col 5 (exclusive)\n    view.setSelection(2, 26, RGBA{ 1.0, 1.0, 0.0, 1.0 }, RGBA{ 0.0, 0.0, 0.0, 1.0 });\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        2,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const epsilon: f32 = 0.01;\n    const yellow_bg = RGBA{ 1.0, 1.0, 0.0, 1.0 };\n\n    const line2_cell_0 = opt_buffer.get(0, 1) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'k'), line2_cell_0.char);\n    const has_yellow_line2_0 = @abs(line2_cell_0.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(line2_cell_0.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(line2_cell_0.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(has_yellow_line2_0);\n\n    const line2_cell_2 = opt_buffer.get(2, 1) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'm'), line2_cell_2.char);\n    const has_yellow_line2_2 = @abs(line2_cell_2.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(line2_cell_2.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(line2_cell_2.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(has_yellow_line2_2);\n\n    const line2_cell_6 = opt_buffer.get(6, 1) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, 'Q'), line2_cell_6.char);\n    const has_yellow_line2_6 = @abs(line2_cell_6.bg[0] - yellow_bg[0]) < epsilon and\n        @abs(line2_cell_6.bg[1] - yellow_bg[1]) < epsilon and\n        @abs(line2_cell_6.bg[2] - yellow_bg[2]) < epsilon;\n    try std.testing.expect(!has_yellow_line2_6);\n}\n\ntest \"drawTextBuffer - Chinese text with wrapping no stray bytes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const text =\n        \\\\前后端分离 - TypeScript逻辑 + Go TUI界面\n        \\\\组件化设计 - 基于tview的可复用组件\n        \\\\渐进式交互 - 逐步披露避免信息过载\n        \\\\智能上下文 - 基于项目状态动态生成问题\n        \\\\丰富的问题类型 - 支持6种不同的交互形式\n        \\\\完整的验证 - 实时输入验证和错误处理\n    ;\n\n    try tb.setText(text);\n\n    // Try word wrapping with a width that might split multibyte chars\n    view.setWrapMode(.word);\n    view.setWrapWidth(35);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        40,\n        20,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    // Write the rendered buffer to check for stray bytes\n    var out_buffer: [2000]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    // Verify the output is valid UTF-8\n    try std.testing.expect(std.unicode.utf8ValidateSlice(result));\n\n    // Verify that the original text is contained in the output (with possible spaces/newlines from wrapping)\n    try std.testing.expect(std.mem.indexOf(u8, result, \"完整的验证\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, result, \"实时输入验证和错误处理\") != null);\n\n    // Check specific problematic line - should NOT contain stray bytes\n    // The line should be present correctly (possibly wrapped with spaces)\n    // But there should be NO stray å character or partial UTF-8 sequences\n    try std.testing.expect(std.mem.indexOf(u8, result, \"å式\") == null); // This should NOT appear\n    try std.testing.expect(std.mem.indexOf(u8, result, \"å\") == null); // No stray partial bytes\n\n    // Verify the problematic characters appear correctly\n    try std.testing.expect(std.mem.indexOf(u8, result, \"形式\") != null);\n}\n\ntest \"drawTextBuffer - Chinese text WITHOUT wrapping no duplicate chunks\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const text =\n        \\\\前后端分离 - TypeScript逻辑 + Go TUI界面\n        \\\\组件化设计 - 基于tview的可复用组件\n        \\\\渐进式交互 - 逐步披露避免信息过载\n        \\\\智能上下文 - 基于项目状态动态生成问题\n        \\\\丰富的问题类型 - 支持6种不同的交互形式\n        \\\\完整的验证 - 实时输入验证和错误处理\n    ;\n\n    try tb.setText(text);\n\n    // Word wrap mode but with wide width so nothing actually wraps\n    view.setWrapMode(.word);\n    view.setWrapWidth(80);\n    view.updateVirtualLines();\n\n    const vlines = view.getVirtualLines();\n\n    // Check each virtual line - should have exactly ONE chunk when width is large enough\n    for (vlines) |vline| {\n        // Each line should have exactly ONE chunk when not actually wrapping\n        try std.testing.expectEqual(@as(usize, 1), vline.chunks.items.len);\n    }\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        80,\n        10,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    // Write the rendered buffer\n    var out_buffer: [2000]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    // Verify the output is valid UTF-8\n    try std.testing.expect(std.unicode.utf8ValidateSlice(result));\n\n    // Should NOT contain stray bytes\n    try std.testing.expect(std.mem.indexOf(u8, result, \"å\") == null);\n\n    // All text should be present\n    try std.testing.expect(std.mem.indexOf(u8, result, \"完整的验证 - 实时输入验证和错误处理\") != null);\n}\n\ntest \"drawTextBuffer - Chinese text with CHAR wrapping no stray bytes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const text =\n        \\\\前后端分离 - TypeScript逻辑 + Go TUI界面\n        \\\\组件化设计 - 基于tview的可复用组件\n        \\\\渐进式交互 - 逐步披露避免信息过载\n        \\\\智能上下文 - 基于项目状态动态生成问题\n        \\\\丰富的问题类型 - 支持6种不同的交互形式\n        \\\\完整的验证 - 实时输入验证和错误处理\n    ;\n\n    try tb.setText(text);\n\n    // Char wrapping with a width that might split multibyte chars\n    view.setWrapMode(.char);\n    view.setWrapWidth(35);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        35,\n        20,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    // Write the rendered buffer to check for stray bytes\n    var out_buffer: [2000]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    // Verify the output is valid UTF-8\n    try std.testing.expect(std.unicode.utf8ValidateSlice(result));\n\n    // Should NOT contain stray bytes\n    try std.testing.expect(std.mem.indexOf(u8, result, \"å\") == null);\n\n    // Verify the problematic characters appear correctly\n    try std.testing.expect(std.mem.indexOf(u8, result, \"形式\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, result, \"完整的验证\") != null);\n}\n\ntest \"drawTextBuffer - word wrap CJK mixed text without break points\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"한글,English,中文,日本語,混合,Test,測試,テスト,가나다,ABC,一二三,あいう,라마바,DEF,四五六,えおか\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(20);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        30,\n        20,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var out_buffer: [1000]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    const vlines = view.getVirtualLines();\n    try std.testing.expect(vlines.len > 1);\n\n    var y: u32 = 0;\n    while (y < vlines.len) : (y += 1) {\n        const first_cell = opt_buffer.get(0, y);\n        if (first_cell) |cell| {\n            try std.testing.expect(!gp.isContinuationChar(cell.char));\n        }\n    }\n\n    try std.testing.expect(std.unicode.utf8ValidateSlice(result));\n}\n\ntest \"drawTextBuffer - word wrap CJK text preserves UTF-8 boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"한글,English,中文,日本語,混合,Test,測試,テスト,가나다,ABC,一二三,あいう,라마바,DEF,四五六,えおか\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(20);\n    view.updateVirtualLines();\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        30,\n        20,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    var out_buffer: [1000]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expect(std.unicode.utf8ValidateSlice(result));\n    try std.testing.expect(std.mem.indexOf(u8, result, \"ä\") == null);\n\n    var i: usize = 0;\n    while (i < result.len) : (i += 1) {\n        if (result[i] == 0xE4) {\n            if (i + 1 >= result.len) {\n                return error.TestFailed;\n            }\n            const next_byte = result[i + 1];\n            if (next_byte < 0x80 or next_byte > 0xBF) {\n                return error.TestFailed;\n            }\n        }\n    }\n\n    const vlines = view.getVirtualLines();\n    var y: u32 = 0;\n    while (y < vlines.len) : (y += 1) {\n        const first_cell = opt_buffer.get(0, y);\n        if (first_cell) |cell| {\n            try std.testing.expect(!gp.isContinuationChar(cell.char));\n        }\n    }\n}\n\ntest \"drawTextBuffer - Thai ว่ grapheme in quotes occupies one cell\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\\\"ว่\\\"\");\n\n    var opt_buffer = try OptimizedBuffer.init(\n        std.testing.allocator,\n        10,\n        1,\n        .{ .pool = pool, .width_method = .unicode },\n    );\n    defer opt_buffer.deinit();\n\n    try opt_buffer.clear(.{ 0.0, 0.0, 0.0, 1.0 }, 32);\n    try opt_buffer.drawTextBuffer(view, 0, 0);\n\n    const cell_0 = opt_buffer.get(0, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '\"'), cell_0.char);\n\n    const cell_1 = opt_buffer.get(1, 0) orelse unreachable;\n    try std.testing.expect(cell_1.char != ' ');\n    try std.testing.expect(cell_1.char != '\"');\n\n    const cell_2 = opt_buffer.get(2, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, '\"'), cell_2.char);\n\n    const cell_3 = opt_buffer.get(3, 0) orelse unreachable;\n    try std.testing.expectEqual(@as(u32, ' '), cell_3.char);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = try opt_buffer.writeResolvedChars(&out_buffer, false);\n    const result = out_buffer[0..written];\n\n    try std.testing.expect(std.mem.indexOf(u8, result, \"\\\"ว่\\\"\") != null);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/text-buffer-highlights_test.zig",
    "content": "const std = @import(\"std\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\nconst ss = @import(\"../syntax-style.zig\");\n\nconst TextBuffer = text_buffer.UnifiedTextBuffer;\nconst RGBA = text_buffer.RGBA;\nconst Highlight = text_buffer.Highlight;\n\ntest \"TextBuffer coords - addHighlightByCoords\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\\nWorld\");\n\n    try tb.addHighlightByCoords(0, 1, 0, 5, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 5), highlights[0].col_end);\n}\n\ntest \"TextBuffer coords - addHighlightByCoords multi-line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\\nWorld\");\n\n    try tb.addHighlightByCoords(0, 3, 1, 3, 1, 1, 0);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    const line1_highlights = tb.getLineHighlights(1);\n\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(usize, 1), line1_highlights.len);\n}\n\n// ===== Highlight System Tests =====\n\ntest \"TextBuffer highlights - add single highlight to line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlight(0, 0, 5, 1, 0, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 0), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 5), highlights[0].col_end);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].style_id);\n}\n\ntest \"TextBuffer highlights - add multiple highlights to same line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlight(0, 0, 5, 1, 0, 0);\n    try tb.addHighlight(0, 6, 11, 2, 0, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 2), highlights.len);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].style_id);\n    try std.testing.expectEqual(@as(u32, 2), highlights[1].style_id);\n}\n\ntest \"TextBuffer highlights - add highlights to multiple lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    try tb.addHighlight(0, 0, 6, 1, 0, 0);\n    try tb.addHighlight(1, 0, 6, 2, 0, 0);\n    try tb.addHighlight(2, 0, 6, 3, 0, 0);\n\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(0).len);\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(1).len);\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(2).len);\n}\n\ntest \"TextBuffer highlights - remove highlights by reference\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\");\n\n    try tb.addHighlight(0, 0, 3, 1, 0, 100);\n    try tb.addHighlight(0, 3, 6, 2, 0, 200);\n    try tb.addHighlight(1, 0, 6, 3, 0, 100);\n\n    tb.removeHighlightsByRef(100);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    const line1_highlights = tb.getLineHighlights(1);\n\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(u32, 2), line0_highlights[0].style_id);\n    try std.testing.expectEqual(@as(usize, 0), line1_highlights.len);\n}\n\ntest \"TextBuffer highlights - clear line highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\");\n\n    try tb.addHighlight(0, 0, 6, 1, 0, 0);\n    try tb.addHighlight(0, 6, 10, 2, 0, 0);\n\n    tb.clearLineHighlights(0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBuffer highlights - clear all highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    try tb.addHighlight(0, 0, 6, 1, 0, 0);\n    try tb.addHighlight(1, 0, 6, 2, 0, 0);\n    try tb.addHighlight(2, 0, 6, 3, 0, 0);\n\n    tb.clearAllHighlights();\n\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(0).len);\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(1).len);\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(2).len);\n}\n\ntest \"TextBuffer highlights - get highlights from non-existent line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line 1\");\n\n    // Get highlights from line that doesn't have any\n    const highlights = tb.getLineHighlights(10);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBuffer highlights - overlapping highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlight(0, 0, 8, 1, 0, 0);\n    try tb.addHighlight(0, 5, 11, 2, 0, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 2), highlights.len);\n}\n\ntest \"TextBuffer highlights - reset clears highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n    try tb.addHighlight(0, 0, 5, 1, 0, 0);\n\n    tb.reset();\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBuffer highlights - setSyntaxStyle and getSyntaxStyle\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var syntax_style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer syntax_style.deinit();\n\n    try std.testing.expect(tb.getSyntaxStyle() == null);\n\n    tb.setSyntaxStyle(syntax_style);\n    try std.testing.expect(tb.getSyntaxStyle() != null);\n\n    tb.setSyntaxStyle(null);\n    try std.testing.expect(tb.getSyntaxStyle() == null);\n}\n\ntest \"TextBuffer highlights - integration with SyntaxStyle\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var syntax_style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer syntax_style.deinit();\n\n    const keyword_id = try syntax_style.registerStyle(\"keyword\", RGBA{ 1.0, 0.0, 0.0, 1.0 }, null, 0);\n    const string_id = try syntax_style.registerStyle(\"string\", RGBA{ 0.0, 1.0, 0.0, 1.0 }, null, 0);\n    const comment_id = try syntax_style.registerStyle(\"comment\", RGBA{ 0.5, 0.5, 0.5, 1.0 }, null, 0);\n\n    try tb.setText(\"function hello() // comment\");\n    tb.setSyntaxStyle(syntax_style);\n\n    try tb.addHighlight(0, 0, 8, keyword_id, 1, 0);\n    try tb.addHighlight(0, 9, 14, string_id, 1, 0);\n    try tb.addHighlight(0, 17, 27, comment_id, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 3), highlights.len);\n\n    const style = tb.getSyntaxStyle().?;\n    try std.testing.expect(style.resolveById(keyword_id) != null);\n    try std.testing.expect(style.resolveById(string_id) != null);\n    try std.testing.expect(style.resolveById(comment_id) != null);\n}\n\ntest \"TextBuffer highlights - style spans computed correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"0123456789\");\n\n    try tb.addHighlight(0, 0, 3, 1, 1, 0);\n    try tb.addHighlight(0, 5, 8, 2, 1, 0);\n\n    const spans = tb.getLineSpans(0);\n    try std.testing.expect(spans.len > 0);\n\n    // Should have spans for: [0-3 style:1], [3-5 style:0/default], [5-8 style:2], ...\n    var found_style1 = false;\n    var found_style2 = false;\n    for (spans) |span| {\n        if (span.style_id == 1) found_style1 = true;\n        if (span.style_id == 2) found_style2 = true;\n    }\n    try std.testing.expect(found_style1);\n    try std.testing.expect(found_style2);\n}\n\ntest \"TextBuffer highlights - priority handling in spans\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"0123456789\");\n\n    try tb.addHighlight(0, 0, 8, 1, 1, 0);\n    try tb.addHighlight(0, 3, 6, 2, 5, 0);\n\n    const spans = tb.getLineSpans(0);\n    try std.testing.expect(spans.len > 0);\n\n    // In range 3-6, style 2 should win due to higher priority\n    var found_high_priority = false;\n    for (spans) |span| {\n        if (span.col >= 3 and span.col < 6 and span.style_id == 2) {\n            found_high_priority = true;\n        }\n    }\n    try std.testing.expect(found_high_priority);\n}\n\n// ===== Character Range Highlight Tests =====\n\ntest \"TextBuffer char range highlights - single line highlight\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlightByCharRange(0, 5, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 0), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 5), highlights[0].col_end);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].style_id);\n}\n\ntest \"TextBuffer char range highlights - multi-line highlight\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // \"Hello\" = 5 chars (0-4, newlines not counted in offsets)\n    // \"World\" = 5 chars (5-9, newlines not counted in offsets)\n    // \"Test\" = 4 chars (10-13, newlines not counted in offsets)\n    try tb.setText(\"Hello\\nWorld\\nTest\");\n\n    // Highlight from middle of line 0 to middle of line 1 (chars 3-9, not counting newlines)\n    // char 3 = 'l' in \"Hello\", char 9 = 'd' in \"World\" (last char)\n    try tb.addHighlightByCharRange(3, 9, 1, 1, 0);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    const line1_highlights = tb.getLineHighlights(1);\n\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(usize, 1), line1_highlights.len);\n\n    // Line 0: highlight from col 3 to end (col 5)\n    try std.testing.expectEqual(@as(u32, 3), line0_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 5), line0_highlights[0].col_end);\n\n    // Line 1: highlight from start (col 0) to col 4 (chars 5,6,7,8 = cols 0,1,2,3)\n    try std.testing.expectEqual(@as(u32, 0), line1_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 4), line1_highlights[0].col_end);\n}\n\ntest \"TextBuffer char range highlights - spanning three lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line1\\nLine2\\nLine3\");\n\n    try tb.addHighlightByCharRange(3, 13, 1, 1, 0);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    const line1_highlights = tb.getLineHighlights(1);\n    const line2_highlights = tb.getLineHighlights(2);\n\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(usize, 1), line1_highlights.len);\n    try std.testing.expectEqual(@as(usize, 1), line2_highlights.len);\n\n    try std.testing.expectEqual(@as(u32, 3), line0_highlights[0].col_start);\n\n    try std.testing.expectEqual(@as(u32, 0), line1_highlights[0].col_start);\n\n    try std.testing.expectEqual(@as(u32, 0), line2_highlights[0].col_start);\n}\n\ntest \"TextBuffer char range highlights - exact line boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"AAAA\\nBBBB\\nCCCC\");\n\n    // Highlight entire first line (chars 0-4, excluding newline)\n    try tb.addHighlightByCharRange(0, 4, 1, 1, 0);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(u32, 0), line0_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 4), line0_highlights[0].col_end);\n\n    // Line 1 should have no highlights\n    const line1_highlights = tb.getLineHighlights(1);\n    try std.testing.expectEqual(@as(usize, 0), line1_highlights.len);\n}\n\ntest \"TextBuffer char range highlights - empty range\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    // Empty range (start == end) should add no highlights\n    try tb.addHighlightByCharRange(5, 5, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBuffer char range highlights - invalid range\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    // Invalid range (start > end) should add no highlights\n    try tb.addHighlightByCharRange(10, 5, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBuffer char range highlights - out of bounds range\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\");\n\n    // Range extends beyond text length - should handle gracefully\n    try tb.addHighlightByCharRange(3, 100, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 3), highlights[0].col_start);\n}\n\ntest \"TextBuffer char range highlights - multiple non-overlapping ranges\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"function hello() { return 42; }\");\n\n    try tb.addHighlightByCharRange(0, 8, 1, 1, 0);\n    try tb.addHighlightByCharRange(9, 14, 2, 1, 0);\n    try tb.addHighlightByCharRange(19, 25, 3, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 3), highlights.len);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].style_id);\n    try std.testing.expectEqual(@as(u32, 2), highlights[1].style_id);\n    try std.testing.expectEqual(@as(u32, 3), highlights[2].style_id);\n}\n\ntest \"TextBuffer char range highlights - with reference ID for removal\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line1\\nLine2\\nLine3\");\n\n    try tb.addHighlightByCharRange(0, 5, 1, 1, 100);\n    try tb.addHighlightByCharRange(6, 11, 2, 1, 100);\n\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(0).len);\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(1).len);\n\n    tb.removeHighlightsByRef(100);\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(0).len);\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(1).len);\n}\n\ntest \"TextBuffer char range highlights - priority handling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"0123456789\");\n\n    try tb.addHighlightByCharRange(0, 8, 1, 1, 0);\n    try tb.addHighlightByCharRange(3, 6, 2, 5, 0);\n\n    const spans = tb.getLineSpans(0);\n    try std.testing.expect(spans.len > 0);\n\n    // Higher priority should win in overlap region\n    var found_high_priority = false;\n    for (spans) |span| {\n        if (span.col >= 3 and span.col < 6 and span.style_id == 2) {\n            found_high_priority = true;\n        }\n    }\n    try std.testing.expect(found_high_priority);\n}\n\ntest \"TextBuffer char range highlights - unicode text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello 世界 🌟\");\n\n    const text_len = tb.getLength();\n    try tb.addHighlightByCharRange(0, text_len, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n}\n\ntest \"TextBuffer char range highlights - preserved after setText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n    try tb.addHighlightByCharRange(0, 5, 1, 1, 0);\n\n    // Set new text - with clear() highlights are now preserved\n    try tb.setText(\"New Text\");\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n\n    // To clear highlights, caller must explicitly call clearAllHighlights\n    tb.clearAllHighlights();\n    const cleared_highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), cleared_highlights.len);\n}\n\ntest \"TextBuffer char range highlights - multi-width chars before highlight\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"前后端分离 @git-committer\");\n    try tb.addHighlightByCharRange(11, 25, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 11), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 25), highlights[0].col_end);\n}\n\ntest \"TextBuffer char range highlights - multi-width chars between highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"abc前后端def\");\n    try tb.addHighlightByCharRange(9, 12, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 9), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 12), highlights[0].col_end);\n}\n\ntest \"TextBuffer char range highlights - emoji grapheme clusters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"前🌟test\");\n    try tb.addHighlightByCharRange(4, 8, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 4), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 8), highlights[0].col_end);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/text-buffer-iterators_test.zig",
    "content": "const std = @import(\"std\");\nconst testing = std.testing;\nconst iter_mod = @import(\"../text-buffer-iterators.zig\");\nconst seg_mod = @import(\"../text-buffer-segment.zig\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst Segment = seg_mod.Segment;\nconst UnifiedRope = seg_mod.UnifiedRope;\nconst LineInfo = iter_mod.LineInfo;\nconst TextChunk = seg_mod.TextChunk;\nconst TextBuffer = text_buffer.UnifiedTextBuffer;\n\ntest \"walkLines - empty rope\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n\n    const Context = struct {\n        count: u32 = 0,\n        first_line: ?LineInfo = null,\n\n        fn callback(ctx_ptr: *anyopaque, line_info: LineInfo) void {\n            const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n            if (ctx.count == 0) {\n                ctx.first_line = line_info;\n            }\n            ctx.count += 1;\n        }\n    };\n\n    var ctx = Context{};\n    iter_mod.walkLines(&rope, &ctx, Context.callback, true);\n\n    try testing.expectEqual(@as(u32, 1), ctx.count);\n}\n\ntest \"walkLines - single text segment\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    });\n\n    const Context = struct {\n        lines: std.ArrayListUnmanaged(LineInfo),\n        allocator: std.mem.Allocator,\n\n        fn callback(ctx_ptr: *anyopaque, line_info: LineInfo) void {\n            const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n            ctx.lines.append(ctx.allocator, line_info) catch {};\n        }\n    };\n\n    var ctx = Context{ .lines = .{}, .allocator = allocator };\n    defer ctx.lines.deinit(allocator);\n\n    iter_mod.walkLines(&rope, &ctx, Context.callback, true);\n\n    try testing.expectEqual(@as(usize, 1), ctx.lines.items.len);\n    try testing.expectEqual(@as(u32, 10), ctx.lines.items[0].width_cols);\n}\n\ntest \"walkLines - text + break + text\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    });\n    try rope.append(Segment{ .brk = {} });\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 10,\n            .byte_end = 15,\n            .width = 5,\n            .flags = 0,\n        },\n    });\n\n    const Context = struct {\n        lines: std.ArrayListUnmanaged(LineInfo),\n        allocator: std.mem.Allocator,\n\n        fn callback(ctx_ptr: *anyopaque, line_info: LineInfo) void {\n            const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n            ctx.lines.append(ctx.allocator, line_info) catch {};\n        }\n    };\n\n    var ctx = Context{ .lines = .{}, .allocator = allocator };\n    defer ctx.lines.deinit(allocator);\n\n    iter_mod.walkLines(&rope, &ctx, Context.callback, true);\n\n    try testing.expectEqual(@as(usize, 2), ctx.lines.items.len);\n\n    try testing.expectEqual(@as(u32, 0), ctx.lines.items[0].line_idx);\n    try testing.expectEqual(@as(u32, 10), ctx.lines.items[0].width_cols);\n    try testing.expectEqual(@as(u32, 0), ctx.lines.items[0].col_offset);\n\n    try testing.expectEqual(@as(u32, 1), ctx.lines.items[1].line_idx);\n    try testing.expectEqual(@as(u32, 5), ctx.lines.items[1].width_cols);\n    try testing.expectEqual(@as(u32, 11), ctx.lines.items[1].col_offset);\n}\n\ntest \"walkLines - exclude newlines in offset\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    });\n    try rope.append(Segment{ .brk = {} });\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 10,\n            .byte_end = 15,\n            .width = 5,\n            .flags = 0,\n        },\n    });\n\n    const Context = struct {\n        lines: std.ArrayListUnmanaged(LineInfo),\n        allocator: std.mem.Allocator,\n\n        fn callback(ctx_ptr: *anyopaque, line_info: LineInfo) void {\n            const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n            ctx.lines.append(ctx.allocator, line_info) catch {};\n        }\n    };\n\n    var ctx = Context{ .lines = .{}, .allocator = allocator };\n    defer ctx.lines.deinit(allocator);\n\n    iter_mod.walkLines(&rope, &ctx, Context.callback, false);\n\n    try testing.expectEqual(@as(usize, 2), ctx.lines.items.len);\n\n    try testing.expectEqual(@as(u32, 0), ctx.lines.items[0].line_idx);\n    try testing.expectEqual(@as(u32, 10), ctx.lines.items[0].width_cols);\n    try testing.expectEqual(@as(u32, 0), ctx.lines.items[0].col_offset);\n\n    try testing.expectEqual(@as(u32, 1), ctx.lines.items[1].line_idx);\n    try testing.expectEqual(@as(u32, 5), ctx.lines.items[1].width_cols);\n    try testing.expectEqual(@as(u32, 10), ctx.lines.items[1].col_offset);\n}\n\ntest \"coordsToOffset - valid coordinates\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    });\n    try rope.append(Segment{ .brk = {} });\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 10,\n            .byte_end = 15,\n            .width = 5,\n            .flags = 0,\n        },\n    });\n\n    const offset1 = iter_mod.coordsToOffset(&rope, 0, 5);\n    try testing.expect(offset1 != null);\n    try testing.expectEqual(@as(u32, 5), offset1.?);\n\n    const offset2 = iter_mod.coordsToOffset(&rope, 1, 0);\n    try testing.expect(offset2 != null);\n    try testing.expectEqual(@as(u32, 11), offset2.?);\n\n    const offset3 = iter_mod.coordsToOffset(&rope, 1, 3);\n    try testing.expect(offset3 != null);\n    try testing.expectEqual(@as(u32, 14), offset3.?);\n}\n\ntest \"offsetToCoords - valid offsets\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    });\n    try rope.append(Segment{ .brk = {} });\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 10,\n            .byte_end = 15,\n            .width = 5,\n            .flags = 0,\n        },\n    });\n\n    const coords1 = iter_mod.offsetToCoords(&rope, 5);\n    try testing.expect(coords1 != null);\n    try testing.expectEqual(@as(u32, 0), coords1.?.row);\n    try testing.expectEqual(@as(u32, 5), coords1.?.col);\n\n    const coords2 = iter_mod.offsetToCoords(&rope, 10);\n    try testing.expect(coords2 != null);\n    try testing.expectEqual(@as(u32, 0), coords2.?.row);\n    try testing.expectEqual(@as(u32, 10), coords2.?.col);\n\n    const coords2b = iter_mod.offsetToCoords(&rope, 11);\n    try testing.expect(coords2b != null);\n    try testing.expectEqual(@as(u32, 1), coords2b.?.row);\n    try testing.expectEqual(@as(u32, 0), coords2b.?.col);\n\n    const coords3 = iter_mod.offsetToCoords(&rope, 14);\n    try testing.expect(coords3 != null);\n    try testing.expectEqual(@as(u32, 1), coords3.?.row);\n    try testing.expectEqual(@as(u32, 3), coords3.?.col);\n}\n\ntest \"Helper functions\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    });\n    try rope.append(Segment{ .brk = {} });\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 10,\n            .byte_end = 15,\n            .width = 5,\n            .flags = 0,\n        },\n    });\n\n    try testing.expectEqual(@as(u32, 2), iter_mod.getLineCount(&rope));\n    try testing.expectEqual(@as(u32, 10), iter_mod.getMaxLineWidth(&rope));\n    try testing.expectEqual(@as(u32, 15), iter_mod.getTotalWidth(&rope));\n}\n\ntest \"coordsToOffset and offsetToCoords - round trip\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    });\n    try rope.append(Segment{ .brk = {} });\n    try rope.append(Segment{ .linestart = {} });\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 10,\n            .byte_end = 18,\n            .width = 8,\n            .flags = 0,\n        },\n    });\n\n    const test_cases = [_]struct { row: u32, col: u32 }{\n        .{ .row = 0, .col = 0 },\n        .{ .row = 0, .col = 5 },\n        .{ .row = 0, .col = 9 },\n        .{ .row = 1, .col = 0 },\n        .{ .row = 1, .col = 4 },\n        .{ .row = 1, .col = 7 },\n    };\n\n    for (test_cases) |tc| {\n        const offset = iter_mod.coordsToOffset(&rope, tc.row, tc.col);\n        try testing.expect(offset != null);\n\n        const coords = iter_mod.offsetToCoords(&rope, offset.?);\n        try testing.expect(coords != null);\n        try testing.expectEqual(tc.row, coords.?.row);\n        try testing.expectEqual(tc.col, coords.?.col);\n    }\n}\n\ntest \"getGraphemeWidthAt - ASCII text\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 2, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 4, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 10, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - emoji and wide characters\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"a😀b\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 4, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - multiple chunks\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 4, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 6, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 10, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 11, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - empty line\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\");\n\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - at chunk boundary\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"abcdef\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - after break segment\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"abc\\ndef\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 1, 0, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - ASCII text\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\");\n\n    try testing.expectEqual(@as(u32, 0), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 2, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 4, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - emoji and wide characters\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"a😀b\");\n\n    try testing.expectEqual(@as(u32, 0), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 4, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - at chunk boundary\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"abcdef\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 4, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - emoji at chunk boundary\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"a😀b\");\n\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - multiple chunks\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello 😀\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 6, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 8, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - empty line\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\");\n\n    try testing.expectEqual(@as(u32, 0), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - col beyond line width\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"abc\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 100, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - multiline\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"abc\\n😀xyz\");\n\n    try testing.expectEqual(@as(u32, 0), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 0), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 1, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 1, 2, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 1, 3, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - CJK characters (Chinese)\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"a世界b\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 6, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - various emoji including star\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"🌟🎉\");\n\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 2, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 4, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - tab characters\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n    tb.setTabWidth(4);\n\n    try tb.setText(\"a\\tb\\t\\tc\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 4), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 4), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 6, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 4), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 10, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 14, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt - tab with different tab_width\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"x\\ty\");\n\n    tb.setTabWidth(2);\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 1, 2, .unicode));\n\n    tb.setTabWidth(8);\n    try testing.expectEqual(@as(u32, 8), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 1, 8, .unicode));\n}\n\ntest \"getGraphemeWidthAt - middle of wide character\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"世\");\n\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    const result = iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod());\n    _ = result;\n}\n\ntest \"getGraphemeWidthAt - invalid row\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"test\");\n\n    try testing.expectEqual(@as(u32, 0), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 5, 0, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - CJK characters\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"a世界b\");\n\n    try testing.expectEqual(@as(u32, 0), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 6, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - star emoji\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"x🌟y\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 3, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 4, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - tabs\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n    tb.setTabWidth(4);\n\n    try tb.setText(\"a\\tb\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 4), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 5, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 6, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getPrevGraphemeWidth - invalid row\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"test\");\n\n    try testing.expectEqual(@as(u32, 0), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 10, 5, tb.tabWidth(), tb.widthMethod()));\n}\n\ntest \"getGraphemeWidthAt and getPrevGraphemeWidth - mixed content\" {\n    const pool = gp.initGlobalPool(testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n    tb.setTabWidth(4);\n\n    try tb.setText(\"Hi\\t世🌟!\");\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 0, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 4), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 2, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 6, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 8, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getGraphemeWidthAt(tb.rope(), tb.memRegistry(), 0, 10, tb.tabWidth(), tb.widthMethod()));\n\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 1, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 1), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 2, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 4), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 6, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 8, tb.tabWidth(), tb.widthMethod()));\n    try testing.expectEqual(@as(u32, 2), iter_mod.getPrevGraphemeWidth(tb.rope(), tb.memRegistry(), 0, 10, tb.tabWidth(), tb.widthMethod()));\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/text-buffer-segment_test.zig",
    "content": "const std = @import(\"std\");\nconst testing = std.testing;\nconst seg_mod = @import(\"../text-buffer-segment.zig\");\n\nconst Segment = seg_mod.Segment;\nconst UnifiedRope = seg_mod.UnifiedRope;\nconst TextChunk = seg_mod.TextChunk;\n\ntest \"Segment.measure - text chunk\" {\n    const chunk = TextChunk{\n        .mem_id = 0,\n        .byte_start = 0,\n        .byte_end = 10,\n        .width = 10,\n        .flags = TextChunk.Flags.ASCII_ONLY,\n    };\n    const seg = Segment{ .text = chunk };\n    const metrics = seg.measure();\n\n    try testing.expectEqual(@as(u32, 10), metrics.total_width);\n    try testing.expectEqual(@as(u32, 10), metrics.max_line_width);\n    try testing.expect(metrics.ascii_only);\n}\n\ntest \"Segment.measure - break\" {\n    const seg = Segment{ .brk = {} };\n    const metrics = seg.measure();\n\n    try testing.expectEqual(@as(u32, 0), metrics.total_width);\n    try testing.expectEqual(@as(u32, 0), metrics.max_line_width);\n    try testing.expect(metrics.ascii_only);\n}\n\ntest \"Segment.empty and is_empty\" {\n    const seg = Segment.empty();\n    try testing.expect(seg.is_empty());\n}\n\ntest \"Segment.isBreak and isText\" {\n    const text_seg = Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    };\n    try testing.expect(text_seg.isText());\n    try testing.expect(!text_seg.isBreak());\n\n    const brk_seg = Segment{ .brk = {} };\n    try testing.expect(brk_seg.isBreak());\n    try testing.expect(!brk_seg.isText());\n}\n\ntest \"Segment.asText\" {\n    const chunk = TextChunk{\n        .mem_id = 0,\n        .byte_start = 0,\n        .byte_end = 10,\n        .width = 10,\n        .flags = 0,\n    };\n    const text_seg = Segment{ .text = chunk };\n    const retrieved = text_seg.asText();\n    try testing.expect(retrieved != null);\n    try testing.expectEqual(@as(u32, 10), retrieved.?.width);\n\n    const brk_seg = Segment{ .brk = {} };\n    try testing.expect(brk_seg.asText() == null);\n}\n\ntest \"Metrics.add - two text segments\" {\n    var left = Segment.Metrics{\n        .total_width = 10,\n        .max_line_width = 10,\n        .ascii_only = true,\n    };\n\n    const right = Segment.Metrics{\n        .total_width = 5,\n        .max_line_width = 5,\n        .ascii_only = true,\n    };\n\n    left.add(right);\n\n    try testing.expectEqual(@as(u32, 15), left.total_width);\n    try testing.expectEqual(@as(u32, 10), left.max_line_width);\n    try testing.expect(left.ascii_only);\n}\n\ntest \"Metrics.add - text, break, text\" {\n    var left = Segment.Metrics{\n        .total_width = 10,\n        .max_line_width = 10,\n        .ascii_only = true,\n    };\n\n    const middle = Segment.Metrics{\n        .total_width = 0,\n        .max_line_width = 0,\n        .ascii_only = true,\n    };\n\n    left.add(middle);\n\n    try testing.expectEqual(@as(u32, 10), left.total_width);\n    try testing.expectEqual(@as(u32, 10), left.max_line_width);\n\n    const right = Segment.Metrics{\n        .total_width = 5,\n        .max_line_width = 5,\n        .ascii_only = true,\n    };\n\n    left.add(right);\n\n    try testing.expectEqual(@as(u32, 15), left.total_width);\n    try testing.expectEqual(@as(u32, 10), left.max_line_width);\n}\n\ntest \"Metrics.add - multiple breaks\" {\n    var metrics = Segment.Metrics{\n        .total_width = 10,\n        .max_line_width = 10,\n        .ascii_only = true,\n    };\n\n    metrics.add(Segment.Metrics{\n        .total_width = 0,\n        .max_line_width = 0,\n        .ascii_only = true,\n    });\n\n    metrics.add(Segment.Metrics{\n        .total_width = 20,\n        .max_line_width = 20,\n        .ascii_only = true,\n    });\n\n    try testing.expectEqual(@as(u32, 30), metrics.total_width);\n    try testing.expectEqual(@as(u32, 20), metrics.max_line_width);\n\n    metrics.add(Segment.Metrics{\n        .total_width = 0,\n        .max_line_width = 0,\n        .ascii_only = true,\n    });\n\n    metrics.add(Segment.Metrics{\n        .total_width = 5,\n        .max_line_width = 5,\n        .ascii_only = true,\n    });\n\n    try testing.expectEqual(@as(u32, 35), metrics.total_width);\n    try testing.expectEqual(@as(u32, 20), metrics.max_line_width);\n}\n\ntest \"Metrics.add - non-ASCII propagation\" {\n    var left = Segment.Metrics{\n        .total_width = 10,\n        .max_line_width = 10,\n        .ascii_only = true,\n    };\n\n    const right = Segment.Metrics{\n        .total_width = 5,\n        .max_line_width = 5,\n        .ascii_only = false,\n    };\n\n    left.add(right);\n    try testing.expect(!left.ascii_only);\n}\n\ntest \"UnifiedRope - basic operations\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n\n    const text1 = Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = TextChunk.Flags.ASCII_ONLY,\n        },\n    };\n    try rope.append(text1);\n\n    const brk = Segment{ .brk = {} };\n    try rope.append(brk);\n\n    const text2 = Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 10,\n            .byte_end = 15,\n            .width = 5,\n            .flags = TextChunk.Flags.ASCII_ONLY,\n        },\n    };\n    try rope.append(text2);\n\n    const metrics = rope.root.metrics();\n    try testing.expectEqual(@as(u32, 5), rope.count());\n    try testing.expectEqual(@as(u32, 15), metrics.custom.total_width);\n    try testing.expectEqual(@as(u32, 10), metrics.custom.max_line_width);\n}\n\ntest \"UnifiedRope - empty rope metrics\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const rope = try UnifiedRope.init(allocator);\n    const metrics = rope.root.metrics();\n\n    try testing.expectEqual(@as(u32, 1), rope.count());\n    try testing.expectEqual(@as(u32, 0), metrics.custom.total_width);\n}\n\ntest \"UnifiedRope - single text segment\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 20,\n            .width = 20,\n            .flags = 0,\n        },\n    });\n\n    const metrics = rope.root.metrics();\n    try testing.expectEqual(@as(u32, 2), rope.count());\n    try testing.expectEqual(@as(u32, 20), metrics.custom.total_width);\n    try testing.expectEqual(@as(u32, 20), metrics.custom.max_line_width);\n}\n\ntest \"UnifiedRope - multiple lines with varying widths\" {\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    var rope = try UnifiedRope.init(allocator);\n\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 10,\n            .width = 10,\n            .flags = 0,\n        },\n    });\n    try rope.append(Segment{ .brk = {} });\n\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 10,\n            .byte_end = 40,\n            .width = 30,\n            .flags = 0,\n        },\n    });\n    try rope.append(Segment{ .brk = {} });\n\n    try rope.append(Segment{\n        .text = TextChunk{\n            .mem_id = 0,\n            .byte_start = 40,\n            .byte_end = 55,\n            .width = 15,\n            .flags = 0,\n        },\n    });\n\n    const metrics = rope.root.metrics();\n    try testing.expectEqual(@as(u32, 8), rope.count());\n    try testing.expectEqual(@as(u32, 55), metrics.custom.total_width);\n    try testing.expectEqual(@as(u32, 30), metrics.custom.max_line_width);\n}\n\nfn combineMetrics(left: Segment.Metrics, right: Segment.Metrics) Segment.Metrics {\n    var result = left;\n    result.add(right);\n    return result;\n}\n\ntest \"combineMetrics helper function\" {\n    const left = Segment.Metrics{\n        .total_width = 10,\n        .max_line_width = 10,\n        .ascii_only = true,\n    };\n\n    const right = Segment.Metrics{\n        .total_width = 5,\n        .max_line_width = 5,\n        .ascii_only = true,\n    };\n\n    const combined = combineMetrics(left, right);\n\n    try testing.expectEqual(@as(u32, 15), combined.total_width);\n    try testing.expectEqual(@as(u32, 10), combined.max_line_width);\n    try testing.expect(combined.ascii_only);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/text-buffer-selection_test.zig",
    "content": "const std = @import(\"std\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst buffer = @import(\"../buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst TextBuffer = text_buffer.TextBuffer;\nconst TextBufferView = text_buffer_view.TextBufferView;\nconst RGBA = text_buffer.RGBA;\n\ntest \"Selection - basic selection without wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    _ = view.setLocalSelection(2, 0, 7, 0, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 2), start);\n    try std.testing.expectEqual(@as(u32, 7), end);\n}\n\ntest \"Selection - with wrapped lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n\n    _ = view.setLocalSelection(5, 0, 5, 1, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 5), start);\n    try std.testing.expectEqual(@as(u32, 15), end);\n}\n\ntest \"Selection - no selection returns all bits set\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info);\n}\n\ntest \"Selection - with newline characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    _ = view.setLocalSelection(2, 1, 4, 2, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expect(std.mem.indexOf(u8, text, \"ne 2\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"\\n\") != null);\n}\n\ntest \"Selection - across empty lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\n\\nLine 4\");\n\n    _ = view.setLocalSelection(0, 0, 2, 2, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    // With newline-aware offsets: Line 0 (0-5) + newline (6) + Line 1 (7-12) + newline (13) + Line 2 empty (14)\n    // Selection to (row=2, col=2) with empty line 2 clamps to col=0, so end=14\n    try std.testing.expectEqual(@as(u32, 14), end);\n}\n\ntest \"Selection - ending in empty line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\n\\nLine 3\");\n\n    _ = view.setLocalSelection(0, 0, 3, 1, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    try std.testing.expectEqual(@as(u32, 0), start);\n}\n\ntest \"Selection - spanning multiple lines completely\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"First\\nSecond\\nThird\");\n\n    _ = view.setLocalSelection(0, 1, 6, 1, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"Second\", text);\n}\n\ntest \"Selection - including multiple line breaks\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"A\\nB\\nC\\nD\");\n\n    _ = view.setLocalSelection(0, 1, 1, 2, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expect(std.mem.indexOf(u8, text, \"\\n\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"B\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"C\") != null);\n}\n\ntest \"Selection - at line boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line1\\nLine2\\nLine3\");\n\n    _ = view.setLocalSelection(4, 0, 2, 1, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expect(std.mem.indexOf(u8, text, \"1\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"\\n\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Li\") != null);\n}\n\ntest \"Selection - empty text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\");\n\n    _ = view.setLocalSelection(0, 0, 0, 0, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info);\n}\n\ntest \"Selection - single character\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"A\");\n\n    _ = view.setLocalSelection(0, 0, 1, 0, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 1), end);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n    try std.testing.expectEqualStrings(\"A\", text);\n}\n\ntest \"Selection - zero-width selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    _ = view.setLocalSelection(5, 0, 5, 0, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info);\n}\n\ntest \"Selection - beyond text bounds\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hi\");\n\n    _ = view.setLocalSelection(0, 0, 10, 0, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 2), end);\n}\n\ntest \"Selection - clear selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    _ = view.setLocalSelection(0, 0, 5, 0, null, null);\n    var packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    view.resetLocalSelection();\n    packed_info = view.packSelectionInfo();\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info);\n}\n\ntest \"Selection - at wrap boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    _ = view.setLocalSelection(9, 0, 1, 1, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 9), start);\n    try std.testing.expectEqual(@as(u32, 11), end);\n}\n\ntest \"Selection - spanning multiple wrapped lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    try std.testing.expectEqual(@as(u32, 3), view.getVirtualLineCount());\n\n    _ = view.setLocalSelection(2, 0, 8, 2, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 2), start);\n    try std.testing.expectEqual(@as(u32, 28), end);\n}\n\ntest \"Selection - changes when wrap width changes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    _ = view.setLocalSelection(5, 0, 5, 1, null, null);\n\n    var packed_info = view.packSelectionInfo();\n    var start = @as(u32, @intCast(packed_info >> 32));\n    var end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 5), start);\n    try std.testing.expectEqual(@as(u32, 15), end);\n\n    view.setWrapWidth(5);\n    _ = view.setLocalSelection(5, 0, 5, 1, null, null);\n\n    packed_info = view.packSelectionInfo();\n    start = @as(u32, @intCast(packed_info >> 32));\n    end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n}\n\ntest \"Selection - with newlines and wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNO\\nPQRSTUVWXYZ\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expect(vline_count >= 3);\n\n    _ = view.setLocalSelection(5, 0, 5, 2, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n}\n\ntest \"Selection - getSelectedText simple\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n    view.setSelection(6, 11, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"World\", text);\n}\n\ntest \"Selection - getSelectedText with newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n    // With rope weight system: Line 0 (0-5) + newline (6) + Line 1 (7-12) + newline (13) + Line 2 (14-19)\n    // Selection [0, 9) includes: \"Line 1\" (0-5) + newline (6) + \"Li\" (7-8) = 9 chars total\n    view.setSelection(0, 9, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"Line 1\\nLi\", text);\n}\n\ntest \"Selection - spanning multiple lines with getSelectedText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Red\\nBlue\");\n    // Rope offsets: \"Red\" (0-2) + newline (3) + \"Blue\" (4-7)\n    // Selection [2, 5) = \"d\" (2) + newline (3) + \"B\" (4) = 3 chars\n    view.setSelection(2, 5, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"d\\nB\", text);\n}\n\ntest \"Selection - with graphemes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello 🌍 World\");\n\n    view.setSelection(0, 8, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Hello\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"🌍\") != null);\n}\n\ntest \"Selection - wide emoji at boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello 🌍 World\");\n\n    // Select \"Hello 🌍\" which is 7 columns: H(0),e(1),l(2),l(3),o(4),space(5),🌍(6-7)\n    // Note: 🌍 is a 2-wide character\n    // Selection [0, 7) should include the emoji because it starts at column 6\n    view.setSelection(0, 7, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"Hello 🌍\", text);\n}\n\ntest \"Selection - wide emoji BEFORE selection start should be excluded\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello 🌍 World\");\n\n    // Layout: H(0) e(1) l(2) l(3) o(4) space(5) 🌍(6-7) space(8) W(9) o(10) r(11) l(12) d(13)\n    // Select [7, 10) - starts at col 7 (second cell of emoji), ends at col 10\n    // When selection starts at second cell of width=2 grapheme, snap backward to include it\n    // So selection should include emoji (snaps to col 6), space, and W\n    // Should NOT include 'o' at col 10 because selection is [7, 10) exclusive end\n    view.setSelection(7, 10, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"🌍 W\", text);\n}\n\ntest \"Selection - start at second cell of width=2 grapheme should snap backward to include it\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AB🌍CD\");\n\n    view.setSelection(3, 5, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"🌍C\", text);\n}\n\ntest \"Selection - end at first cell of width=2 grapheme should snap forward to include it\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AB🌍CD\");\n\n    view.setSelection(1, 3, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"B🌍\", text);\n}\n\ntest \"Selection - both boundaries at cells of width=2 graphemes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"A🌍B🌎C\");\n\n    view.setSelection(2, 5, null, null);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"🌍B🌎\", text);\n}\n\ntest \"Selection - updateSelection extends existing selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    // Set initial selection from 0 to 5\n    view.setSelection(0, 5, null, null);\n\n    var packed_info = view.packSelectionInfo();\n    var start = @as(u32, @intCast(packed_info >> 32));\n    var end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 5), end);\n\n    // Update to extend end to 11\n    view.updateSelection(11, null, null);\n\n    packed_info = view.packSelectionInfo();\n    start = @as(u32, @intCast(packed_info >> 32));\n    end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 11), end);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n    try std.testing.expectEqualStrings(\"Hello World\", text);\n}\n\ntest \"Selection - updateSelection with no existing selection does nothing\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    // No selection set\n    const packed_info_before = view.packSelectionInfo();\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info_before);\n\n    // Try to update - should do nothing\n    view.updateSelection(5, null, null);\n\n    const packed_info_after = view.packSelectionInfo();\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info_after);\n}\n\ntest \"Selection - updateSelection can shrink selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    view.setSelection(0, 11, null, null);\n\n    // Shrink to 5\n    view.updateSelection(5, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 5), end);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n    try std.testing.expectEqualStrings(\"Hello\", text);\n}\n\ntest \"Selection - updateLocalSelection extends focus position\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    // Set initial local selection from (0,0) to (5,0)\n    _ = view.setLocalSelection(0, 0, 5, 0, null, null);\n\n    var packed_info = view.packSelectionInfo();\n    var start = @as(u32, @intCast(packed_info >> 32));\n    var end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 5), end);\n\n    // Update focus to (11,0) - should keep anchor at (0,0)\n    const changed = view.updateLocalSelection(0, 0, 11, 0, null, null);\n    try std.testing.expect(changed);\n\n    packed_info = view.packSelectionInfo();\n    start = @as(u32, @intCast(packed_info >> 32));\n    end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 11), end);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n    try std.testing.expectEqualStrings(\"Hello World\", text);\n}\n\ntest \"Selection - updateLocalSelection with no existing selection falls back to setLocalSelection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    // No selection set - updateLocalSelection now falls back to setLocalSelection\n    const changed = view.updateLocalSelection(0, 0, 5, 0, null, null);\n    try std.testing.expect(changed);\n\n    const packed_info = view.packSelectionInfo();\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 5), end);\n}\n\ntest \"Selection - updateLocalSelection can shrink selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    _ = view.setLocalSelection(0, 0, 11, 0, null, null);\n\n    // Shrink focus to 5\n    const changed = view.updateLocalSelection(0, 0, 5, 0, null, null);\n    try std.testing.expect(changed);\n\n    const packed_info = view.packSelectionInfo();\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 5), end);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n    try std.testing.expectEqualStrings(\"Hello\", text);\n}\n\ntest \"Selection - updateLocalSelection across multiple lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    // Start selection at (2, 0)\n    _ = view.setLocalSelection(2, 0, 2, 0, null, null);\n\n    // Extend to (4, 1) - should select from \"ne 1\\nLine\"\n    const changed = view.updateLocalSelection(2, 0, 4, 1, null, null);\n    try std.testing.expect(changed);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    try std.testing.expect(std.mem.indexOf(u8, text, \"ne 1\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"\\n\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Line\") != null);\n}\n\ntest \"Selection - updateLocalSelection backward selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World!\");\n\n    // Set anchor at (11, 0) - after \"World\"\n    _ = view.setLocalSelection(11, 0, 11, 0, null, null);\n\n    // Move focus backward to (6, 0) - start of \"World\"\n    // Backward selection adds +1 to make it inclusive, so [6, 12) = \"World!\"\n    const changed = view.updateLocalSelection(11, 0, 6, 0, null, null);\n    try std.testing.expect(changed);\n\n    const packed_info = view.packSelectionInfo();\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 6), start);\n    try std.testing.expectEqual(@as(u32, 12), end);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n    try std.testing.expectEqualStrings(\"World!\", text);\n}\n\ntest \"Selection - updateLocalSelection with wrapped lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n\n    // Start at (0, 0)\n    _ = view.setLocalSelection(0, 0, 0, 0, null, null);\n\n    // Extend to second wrapped line (5, 1)\n    const changed = view.updateLocalSelection(0, 0, 5, 1, null, null);\n    try std.testing.expect(changed);\n\n    const packed_info = view.packSelectionInfo();\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 15), end);\n\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n    try std.testing.expectEqualStrings(\"ABCDEFGHIJKLMNO\", text);\n}\n\ntest \"Selection - updateLocalSelection with same focus position maintains selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    _ = view.setLocalSelection(0, 0, 5, 0, null, null);\n\n    // Update to same focus position - selection should remain the same\n    _ = view.updateLocalSelection(0, 0, 5, 0, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 0), start);\n    try std.testing.expectEqual(@as(u32, 5), end);\n}\n\ntest \"Selection - updateLocalSelection preserves anchor correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    // Set anchor at (3, 1) - middle of \"Line 2\" (at 'e' in \"Line\")\n    _ = view.setLocalSelection(3, 1, 3, 1, null, null);\n\n    // Update focus multiple times - last one to (6, 2) which is end of \"Line 3\"\n    _ = view.updateLocalSelection(3, 1, 6, 1, null, null);\n    _ = view.updateLocalSelection(3, 1, 2, 2, null, null);\n    _ = view.updateLocalSelection(3, 1, 6, 2, null, null);\n\n    // Final selection should still have anchor at (3, 1)\n    var out_buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&out_buffer);\n    const text = out_buffer[0..len];\n\n    // Should include \"e 2\\nLine 3\" since anchor is at col 3 of line 1 and focus at end of line 2\n    try std.testing.expect(std.mem.indexOf(u8, text, \"e 2\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"\\nLine 3\") != null);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/text-buffer-selection_viewport_test.zig",
    "content": "const std = @import(\"std\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst TextBuffer = text_buffer.TextBuffer;\nconst TextBufferView = text_buffer_view.TextBufferView;\nconst Viewport = text_buffer_view.Viewport;\n\n// ===== Viewport-Aware Selection Tests =====\n\ntest \"Selection - vertical viewport selection without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 0\\nLine 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\nLine 6\\nLine 7\\nLine 8\\nLine 9\");\n\n    view.setViewport(Viewport{ .x = 0, .y = 5, .width = 10, .height = 5 });\n\n    _ = view.setLocalSelection(0, 0, 2, 2, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Line 5\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Line 6\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Li\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Line 7\") == null);\n}\n\ntest \"Selection - horizontal viewport selection without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\");\n\n    view.setViewport(Viewport{ .x = 10, .y = 0, .width = 10, .height = 1 });\n\n    _ = view.setLocalSelection(0, 0, 5, 0, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"KLMNO\", text);\n}\n\ntest \"Selection - wrapping mode ignores horizontal viewport offset\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    view.setViewport(Viewport{ .x = 10, .y = 0, .width = 10, .height = 3 });\n\n    _ = view.setLocalSelection(0, 0, 5, 0, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"ABCDE\", text);\n}\n\ntest \"Selection - vertical viewport with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 4), vline_count);\n\n    view.setViewport(Viewport{ .x = 0, .y = 1, .width = 10, .height = 2 });\n\n    _ = view.setLocalSelection(0, 0, 5, 1, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"KLMNOPQRSTUVWXY\", text);\n}\n\ntest \"Selection - across empty line with viewport offset\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line0\\n\\nLine2\\nLine3\\nLine4\");\n\n    view.setViewport(Viewport{ .x = 0, .y = 1, .width = 10, .height = 3 });\n\n    _ = view.setLocalSelection(0, 0, 3, 2, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Line2\") != null);\n    try std.testing.expect(std.mem.indexOf(u8, text, \"Lin\") != null);\n}\n\ntest \"Selection - viewport offset with multi-line selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAA\\nBBB\\nCCC\\nDDD\\nEEE\\nFFF\\nGGG\\nHHH\");\n\n    view.setViewport(Viewport{ .x = 0, .y = 2, .width = 10, .height = 4 });\n\n    _ = view.setLocalSelection(0, 0, 3, 0, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"CCC\", text);\n}\n\ntest \"Selection - combined horizontal and vertical viewport offsets\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\\n0123456789ABCDEFGHIJKLMNOP\\nQRSTUVWXYZ0123456789ABCDEF\");\n\n    view.setViewport(Viewport{ .x = 5, .y = 1, .width = 10, .height = 2 });\n\n    _ = view.setLocalSelection(0, 0, 5, 0, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"56789\", text);\n}\n\ntest \"Selection - viewport without offsets behaves as before\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    view.setViewport(Viewport{ .x = 0, .y = 0, .width = 20, .height = 5 });\n\n    _ = view.setLocalSelection(2, 0, 7, 0, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"llo W\", text);\n}\n\ntest \"Selection - no viewport behaves as before\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    _ = view.setLocalSelection(2, 0, 7, 0, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"llo W\", text);\n}\n\ntest \"Selection - VALIDATION: verify selection range matches extracted text with viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line0\\nLine1\\nLine2\\nLine3\\nLine4\\nLine5\\nLine6\\nLine7\\nLine8\\nLine9\");\n\n    view.setViewport(Viewport{ .x = 0, .y = 5, .width = 10, .height = 5 });\n\n    _ = view.setLocalSelection(0, 0, 5, 0, null, null);\n\n    const selection = view.getSelection();\n    try std.testing.expect(selection != null);\n\n    var selected_buffer: [100]u8 = undefined;\n    const selected_len = view.getSelectedTextIntoBuffer(&selected_buffer);\n    const selected_text = selected_buffer[0..selected_len];\n\n    try std.testing.expectEqualStrings(\"Line5\", selected_text);\n\n    const expected_start: u32 = 30; // Start of line 5\n    const expected_end: u32 = 35; // First 5 chars of line 5\n\n    try std.testing.expectEqual(expected_start, selection.?.start);\n    try std.testing.expectEqual(expected_end, selection.?.end);\n}\n\ntest \"Selection - VALIDATION: multi-line selection range with viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAA\\nBBB\\nCCC\\nDDD\\nEEE\\nFFF\\nGGG\\nHHH\");\n\n    view.setViewport(Viewport{ .x = 0, .y = 3, .width = 10, .height = 5 });\n\n    _ = view.setLocalSelection(0, 0, 3, 2, null, null);\n\n    var selected_buffer: [100]u8 = undefined;\n    const selected_len = view.getSelectedTextIntoBuffer(&selected_buffer);\n    const selected_text = selected_buffer[0..selected_len];\n\n    try std.testing.expectEqualStrings(\"DDD\\nEEE\\nFFF\", selected_text);\n\n    const selection = view.getSelection();\n    try std.testing.expect(selection != null);\n\n    const expected_start: u32 = 12; // Start of line 3\n    const expected_end: u32 = 23; // End of \"FFF\" on line 5\n\n    try std.testing.expectEqual(expected_start, selection.?.start);\n    try std.testing.expectEqual(expected_end, selection.?.end);\n}\n\ntest \"Selection - RENDER TEST: selection highlights correct cells with viewport scroll\" {\n    const buffer_mod = @import(\"../buffer.zig\");\n    const RGBA = buffer_mod.RGBA;\n\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAA\\nBBB\\nCCC\\nDDD\\nEEE\\nFFF\\nGGG\\nHHH\");\n\n    view.setViewport(Viewport{ .x = 0, .y = 3, .width = 10, .height = 5 });\n\n    const red_bg = RGBA{ 1.0, 0.0, 0.0, 1.0 };\n    _ = view.setLocalSelection(0, 0, 3, 0, red_bg, null);\n\n    var render_buffer = try buffer_mod.OptimizedBuffer.init(std.testing.allocator, pool, 20, 10, .unicode);\n    defer render_buffer.deinit();\n\n    try render_buffer.drawTextBuffer(view, 0, 0);\n\n    var x: u32 = 0;\n    while (x < 3) : (x += 1) {\n        const cell = render_buffer.get(x, 0);\n        try std.testing.expect(cell != null);\n\n        const bg = cell.?.bg;\n        try std.testing.expectApproxEqAbs(@as(f32, 1.0), bg[0], 0.01); // Red\n        try std.testing.expectApproxEqAbs(@as(f32, 0.0), bg[1], 0.01); // Green\n        try std.testing.expectApproxEqAbs(@as(f32, 0.0), bg[2], 0.01); // Blue\n    }\n\n    const cell_3 = render_buffer.get(3, 0);\n    try std.testing.expect(cell_3 != null);\n    const bg_3 = cell_3.?.bg;\n    try std.testing.expect(bg_3[0] < 0.5); // Not red\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/text-buffer-view_test.zig",
    "content": "const std = @import(\"std\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst iter_mod = @import(\"../text-buffer-iterators.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst TextBuffer = text_buffer.UnifiedTextBuffer;\nconst TextBufferView = text_buffer_view.UnifiedTextBufferView;\nconst RGBA = text_buffer.RGBA;\n\ntest \"TextBufferView wrapping - no wrap returns same line count\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    const no_wrap_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), no_wrap_count);\n\n    view.setWrapWidth(null);\n    const still_no_wrap = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), still_no_wrap);\n}\n\ntest \"TextBufferView wrapping - simple wrap splits line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    const no_wrap_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), no_wrap_count);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 2), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - wrap at exact boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 1), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - preserves newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Short\\nAnother short line\\nLast\");\n\n    const no_wrap_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 3), no_wrap_count);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(50);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 3), wrapped_count);\n}\n\ntest \"TextBufferView selection - basic selection without wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    _ = view.setLocalSelection(2, 0, 7, 0, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 2), start);\n    try std.testing.expectEqual(@as(u32, 7), end);\n}\n\ntest \"TextBufferView selection - with wrapped lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n\n    _ = view.setLocalSelection(5, 0, 5, 1, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 5), start);\n    try std.testing.expectEqual(@as(u32, 15), end);\n}\n\ntest \"TextBufferView selection - no selection returns all bits set\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info);\n}\n\ntest \"TextBufferView word wrapping - basic word wrap at space\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(8);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 2), wrapped_count);\n}\n\ntest \"TextBufferView word wrapping - long word exceeds width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 3), wrapped_count);\n}\n\ntest \"TextBufferView getSelectedTextIntoBuffer - simple selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n    view.setSelection(6, 11, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"World\", text);\n}\n\ntest \"TextBufferView getSelectedTextIntoBuffer - with newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    view.setSelection(0, 9, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"Line 1\\nLi\", text);\n}\n\ntest \"TextBufferView getCachedLineInfo - with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(7);\n    const line_count = view.getVirtualLineCount();\n    const line_info = view.getCachedLineInfo();\n\n    try std.testing.expectEqual(@as(usize, line_count), line_info.line_start_cols.len);\n    try std.testing.expectEqual(@as(usize, line_count), line_info.line_width_cols.len);\n\n    for (line_info.line_width_cols, 0..) |width, i| {\n        if (i < line_info.line_width_cols.len - 1) {\n            try std.testing.expect(width <= 7);\n        }\n    }\n}\n\ntest \"TextBufferView virtual line spans - with highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    try tb.addHighlight(0, 5, 15, 1, 1, 0);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n\n    const vline0_info = view.getVirtualLineSpans(0);\n    const vline1_info = view.getVirtualLineSpans(1);\n\n    try std.testing.expectEqual(@as(usize, 0), vline0_info.source_line);\n    try std.testing.expectEqual(@as(usize, 0), vline1_info.source_line);\n\n    try std.testing.expectEqual(@as(u32, 0), vline0_info.col_offset);\n    try std.testing.expectEqual(@as(u32, 10), vline1_info.col_offset);\n\n    try std.testing.expect(vline0_info.spans.len > 0);\n    try std.testing.expect(vline1_info.spans.len > 0);\n}\n\ntest \"TextBufferView updates after buffer setText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"First text\");\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n    const count1 = view.getVirtualLineCount();\n\n    try tb.setText(\"New text that is much longer\");\n\n    const count2 = view.getVirtualLineCount();\n\n    try std.testing.expect(count2 > count1);\n}\n\ntest \"TextBufferView wrapping - multiple wrap lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 3), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - long line with newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\\nShort\");\n\n    const no_wrap_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 2), no_wrap_count);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 3), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - change wrap width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    var wrapped_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 2), wrapped_count);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n    wrapped_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 4), wrapped_count);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(20);\n    wrapped_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), wrapped_count);\n\n    view.setWrapWidth(null);\n    wrapped_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - grapheme at exact boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"12345678🌟\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 1), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - grapheme split across boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"123456789🌟ABC\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 2), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - CJK characters at boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"测试文字处理\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 2), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - mixed width characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AB测试CD\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(6);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 2), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - single wide character exceeds width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"🌟\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(1);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 1), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - multiple consecutive wide characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"🌟🌟🌟🌟🌟\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(6);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 2), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - zero width characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"e\\u{0301}e\\u{0301}e\\u{0301}\"); // é é é using combining acute\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(2);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 1);\n}\n\ntest \"TextBufferView word wrapping - multiple words\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"The quick brown fox jumps\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(15);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 2);\n}\n\ntest \"TextBufferView word wrapping - hyphenated words\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"self-contained multi-line\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(12);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 2);\n}\n\ntest \"TextBufferView word wrapping - punctuation boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello,World.Test\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(8);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 2);\n}\n\ntest \"TextBufferView word wrapping - tab boundary width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    // \"AB\" = 2 cols, tab = 2 cols, \"CD\" = 2 cols\n    try tb.setText(\"AB\\tCD\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(4);\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n    try std.testing.expectEqual(@as(u32, 4), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 2), vlines[1].width_cols);\n}\n\ntest \"TextBufferView word wrapping - emoji boundary width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    // \"AB\" = 2 cols, \"🌟\" = 2 cols, space = 1 col, \"CD\" = 2 cols\n    try tb.setText(\"AB🌟 CD\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(5);\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n    try std.testing.expectEqual(@as(u32, 5), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 2), vlines[1].width_cols);\n}\n\ntest \"TextBufferView word wrapping - CJK boundary width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    // \"AB\" = 2 cols, \"好\" = 2 cols, space = 1 col, \"CD\" = 2 cols\n    try tb.setText(\"AB好 CD\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(5);\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n    try std.testing.expectEqual(@as(u32, 5), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 2), vlines[1].width_cols);\n}\n\n\ntest \"TextBufferView word wrapping - compare char vs word mode\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello wonderful world\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    const char_wrapped_count = view.getVirtualLineCount();\n\n    view.setWrapMode(.word);\n    const word_wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(char_wrapped_count >= 2);\n    try std.testing.expect(word_wrapped_count >= 2);\n}\n\ntest \"TextBufferView word wrapping - empty lines preserved\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"First line\\n\\nSecond line\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(8);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 3);\n}\n\ntest \"TextBufferView word wrapping - slash as boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"path/to/file\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(8);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 2);\n}\n\ntest \"TextBufferView word wrapping - brackets as boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"array[index]value\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(10);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 2);\n}\n\ntest \"TextBufferView word wrapping - single character at boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"a b c d e f\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(4);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 3);\n}\n\ntest \"TextBufferView word wrapping - fragmented rope with word boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const text = \"hello my good friend\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n\n    const seg_mod = @import(\"../text-buffer-segment.zig\");\n    const Segment = seg_mod.Segment;\n\n    const chunk1 = tb.createChunk(mem_id, 0, 14); // \"hello my good \"\n    const chunk2 = tb.createChunk(mem_id, 14, 15); // \"f\"\n    const chunk3 = tb.createChunk(mem_id, 15, 20); // \"riend\"\n\n    var segments: std.ArrayListUnmanaged(Segment) = .{};\n    defer segments.deinit(std.testing.allocator);\n\n    try segments.append(std.testing.allocator, Segment{ .linestart = {} });\n    try segments.append(std.testing.allocator, Segment{ .text = chunk1 });\n    try segments.append(std.testing.allocator, Segment{ .text = chunk2 });\n    try segments.append(std.testing.allocator, Segment{ .text = chunk3 });\n\n    try tb.rope().setSegments(segments.items);\n\n    view.virtual_lines_dirty = true;\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    try std.testing.expectEqual(@as(u32, 14), vlines[0].width_cols);\n\n    try std.testing.expectEqual(@as(u32, 6), vlines[1].width_cols);\n}\n\ntest \"TextBufferView wrapping - very narrow width (1 char)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDE\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(1);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 5), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - very narrow width (2 chars)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEF\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(2);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 3), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - switch between char and word mode\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello world test\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(8);\n\n    view.setWrapMode(.char);\n    const char_count = view.getVirtualLineCount();\n\n    view.setWrapMode(.word);\n    const word_count = view.getVirtualLineCount();\n\n    try std.testing.expect(char_count >= 2);\n    try std.testing.expect(word_count >= 2);\n}\n\ntest \"TextBufferView wrapping - multiple consecutive newlines with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJ\\n\\n\\nKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 6);\n}\n\ntest \"TextBufferView wrapping - only spaces should not create extra lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"          \"); // 10 spaces\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expectEqual(@as(u32, 2), wrapped_count);\n}\n\ntest \"TextBufferView wrapping - mixed tabs and spaces\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AB\\tCD\\tEF\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 1);\n}\n\ntest \"TextBufferView wrapping - unicode emoji with varying widths\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"A🌟B🎨C🚀D\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count >= 2);\n}\n\ntest \"TextBufferView wrapping - getVirtualLines reflects current wrap state\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    var vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 4), vlines.len);\n\n    view.setWrapWidth(null);\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n}\n\ntest \"TextBufferView selection - multi-line selection without wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    _ = view.setLocalSelection(2, 0, 4, 1, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n}\n\ntest \"TextBufferView selection - selection at wrap boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    _ = view.setLocalSelection(9, 0, 1, 1, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 9), start);\n    try std.testing.expectEqual(@as(u32, 11), end);\n}\n\ntest \"TextBufferView selection - spanning multiple wrapped lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    try std.testing.expectEqual(@as(u32, 3), view.getVirtualLineCount());\n\n    _ = view.setLocalSelection(2, 0, 8, 2, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    const start = @as(u32, @intCast(packed_info >> 32));\n    const end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 2), start);\n    try std.testing.expectEqual(@as(u32, 28), end);\n}\n\ntest \"TextBufferView selection - changes when wrap width changes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    _ = view.setLocalSelection(5, 0, 5, 1, null, null);\n\n    var packed_info = view.packSelectionInfo();\n    var start = @as(u32, @intCast(packed_info >> 32));\n    var end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n    try std.testing.expectEqual(@as(u32, 5), start);\n    try std.testing.expectEqual(@as(u32, 15), end);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n    _ = view.setLocalSelection(5, 0, 5, 1, null, null);\n\n    packed_info = view.packSelectionInfo();\n    start = @as(u32, @intCast(packed_info >> 32));\n    end = @as(u32, @intCast(packed_info & 0xFFFFFFFF));\n\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n}\n\ntest \"TextBufferView selection - empty selection with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJ\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n\n    _ = view.setLocalSelection(2, 0, 2, 0, null, null);\n\n    const packed_info = view.packSelectionInfo();\n\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info);\n}\n\ntest \"TextBufferView selection - with newlines and wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNO\\nPQRSTUVWXYZ\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expect(vline_count >= 3);\n\n    _ = view.setLocalSelection(5, 0, 5, 2, null, null);\n\n    const packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n}\n\ntest \"TextBufferView selection - reset clears selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    _ = view.setLocalSelection(0, 0, 5, 0, null, null);\n    var packed_info = view.packSelectionInfo();\n    try std.testing.expect(packed_info != 0xFFFFFFFF_FFFFFFFF);\n\n    view.resetLocalSelection();\n    packed_info = view.packSelectionInfo();\n    try std.testing.expectEqual(@as(u64, 0xFFFFFFFF_FFFFFFFF), packed_info);\n}\n\ntest \"TextBufferView selection - spanning multiple lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Red\\nBlue\");\n\n    view.setSelection(2, 5, null, null);\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getSelectedTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"d\\nB\", text);\n}\n\ntest \"TextBufferView line info - empty buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(usize, 1), line_info.line_start_cols.len);\n    try std.testing.expectEqual(@as(u32, 0), line_info.line_start_cols[0]);\n    try std.testing.expectEqual(@as(u32, 0), line_info.line_width_cols[0]);\n}\n\ntest \"TextBufferView line info - simple text without newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(u32, 0), line_info.line_start_cols[0]);\n    try std.testing.expect(line_info.line_width_cols[0] > 0);\n}\n\ntest \"TextBufferView line info - text ending with newline\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\\n\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 2), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(u32, 0), line_info.line_start_cols[0]);\n    try std.testing.expect(line_info.line_width_cols[0] > 0);\n    try std.testing.expect(line_info.line_width_cols[1] >= 0);\n}\n\ntest \"TextBufferView line info - consecutive newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\n\\nLine 3\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 3), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(u32, 0), line_info.line_start_cols[0]);\n}\n\ntest \"TextBufferView line info - only newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\\n\\n\\n\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 4), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    for (line_info.line_width_cols) |width| {\n        try std.testing.expect(width >= 0);\n    }\n}\n\ntest \"TextBufferView line info - wide characters (Unicode)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello 世界 🌟\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expect(line_info.line_width_cols[0] > 0);\n}\n\ntest \"TextBufferView line info - very long lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const longText = [_]u8{'A'} ** 1000;\n    try tb.setText(&longText);\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expect(line_info.line_width_cols[0] > 0);\n}\n\ntest \"TextBufferView line info - buffer with only whitespace\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"   \\n \\n \");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 3), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    for (line_info.line_width_cols) |width| {\n        try std.testing.expect(width >= 0);\n    }\n}\n\ntest \"TextBufferView line info - single character lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"A\\nB\\nC\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 3), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    for (line_info.line_width_cols) |width| {\n        try std.testing.expect(width > 0);\n    }\n}\n\ntest \"TextBufferView line info - complex Unicode combining characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"café\\nnaïve\\nrésumé\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 3), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    for (line_info.line_width_cols) |width| {\n        try std.testing.expect(width > 0);\n    }\n}\n\ntest \"TextBufferView line info - extremely long single line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const extremelyLongText = [_]u8{'A'} ** 10000;\n    try tb.setText(&extremelyLongText);\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expect(line_info.line_width_cols[0] > 0);\n}\n\ntest \"TextBufferView line info - extremely long line with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    // Create extremely long text with 10000 'A' characters\n    const extremelyLongText = [_]u8{'A'} ** 10000;\n    try tb.setText(&extremelyLongText);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(80);\n    const wrapped_count = view.getVirtualLineCount();\n\n    try std.testing.expect(wrapped_count > 100);\n}\n\ntest \"TextBufferView getPlainTextIntoBuffer - simple text without newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getPlainTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"Hello World\", text);\n}\n\ntest \"TextBufferView getPlainTextIntoBuffer - text with newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getPlainTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"Line 1\\nLine 2\\nLine 3\", text);\n}\n\ntest \"TextBufferView getPlainTextIntoBuffer - text with only newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\\n\\n\\n\");\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getPlainTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"\\n\\n\\n\", text);\n}\n\ntest \"TextBufferView getPlainTextIntoBuffer - empty lines between content\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"First\\n\\nThird\");\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getPlainTextIntoBuffer(&buffer);\n    const text = buffer[0..len];\n\n    try std.testing.expectEqualStrings(\"First\\n\\nThird\", text);\n}\n\ntest \"TextBufferView line info - text starting with newline\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\\nHello World\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 2), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(u32, 0), line_info.line_start_cols[0]);\n\n    try std.testing.expectEqual(@as(u32, 1), line_info.line_start_cols[1]);\n}\n\ntest \"TextBufferView line info - lines with different widths\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n    try text_builder.appendSlice(std.testing.allocator, \"Short\\n\");\n    try text_builder.appendNTimes(std.testing.allocator, 'A', 50);\n    try text_builder.appendSlice(std.testing.allocator, \"\\nMedium\");\n    const text = text_builder.items;\n\n    try tb.setText(text);\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 3), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expect(line_info.line_width_cols[0] < line_info.line_width_cols[1]);\n    try std.testing.expect(line_info.line_width_cols[1] > line_info.line_width_cols[2]);\n}\n\ntest \"TextBufferView line info - alternating empty and content lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\\nContent\\n\\nMore\\n\\n\");\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 6), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    for (line_info.line_width_cols) |width| {\n        try std.testing.expect(width >= 0);\n    }\n}\n\ntest \"TextBufferView line info - thousands of lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n\n    var i: u32 = 0;\n    while (i < 999) : (i += 1) {\n        try text_builder.writer(std.testing.allocator).print(\"Line {}\\n\", .{i});\n    }\n    try text_builder.writer(std.testing.allocator).print(\"Line {}\", .{i});\n\n    try tb.setText(text_builder.items);\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1000), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(u32, 0), line_info.line_start_cols[0]);\n\n    var line_idx: u32 = 1;\n    while (line_idx < 1000) : (line_idx += 1) {\n        try std.testing.expect(line_info.line_start_cols[line_idx] > line_info.line_start_cols[line_idx - 1]);\n    }\n}\n\ntest \"TextBufferView highlights - add single highlight to line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlight(0, 0, 5, 1, 0, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 0), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 5), highlights[0].col_end);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].style_id);\n}\n\ntest \"TextBufferView highlights - add multiple highlights to same line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlight(0, 0, 5, 1, 0, 0);\n    try tb.addHighlight(0, 6, 11, 2, 0, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 2), highlights.len);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].style_id);\n    try std.testing.expectEqual(@as(u32, 2), highlights[1].style_id);\n}\n\ntest \"TextBufferView highlights - add highlights to multiple lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    try tb.addHighlight(0, 0, 6, 1, 0, 0);\n    try tb.addHighlight(1, 0, 6, 2, 0, 0);\n    try tb.addHighlight(2, 0, 6, 3, 0, 0);\n\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(0).len);\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(1).len);\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(2).len);\n}\n\ntest \"TextBufferView highlights - remove highlights by reference\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\");\n\n    try tb.addHighlight(0, 0, 3, 1, 0, 100);\n    try tb.addHighlight(0, 3, 6, 2, 0, 200);\n    try tb.addHighlight(1, 0, 6, 3, 0, 100);\n\n    tb.removeHighlightsByRef(100);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    const line1_highlights = tb.getLineHighlights(1);\n\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(u32, 2), line0_highlights[0].style_id);\n    try std.testing.expectEqual(@as(usize, 0), line1_highlights.len);\n}\n\ntest \"TextBufferView highlights - clear line highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\");\n\n    try tb.addHighlight(0, 0, 6, 1, 0, 0);\n    try tb.addHighlight(0, 6, 10, 2, 0, 0);\n\n    tb.clearLineHighlights(0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBufferView highlights - clear all highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    try tb.addHighlight(0, 0, 6, 1, 0, 0);\n    try tb.addHighlight(1, 0, 6, 2, 0, 0);\n    try tb.addHighlight(2, 0, 6, 3, 0, 0);\n\n    tb.clearAllHighlights();\n\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(0).len);\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(1).len);\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(2).len);\n}\n\ntest \"TextBufferView highlights - overlapping highlights\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlight(0, 0, 8, 1, 0, 0);\n    try tb.addHighlight(0, 5, 11, 2, 0, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 2), highlights.len);\n}\n\ntest \"TextBufferView highlights - style spans computed correctly\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789\");\n\n    try tb.addHighlight(0, 0, 3, 1, 1, 0);\n    try tb.addHighlight(0, 5, 8, 2, 1, 0);\n\n    const spans = tb.getLineSpans(0);\n    try std.testing.expect(spans.len > 0);\n\n    var found_style1 = false;\n    var found_style2 = false;\n    for (spans) |span| {\n        if (span.style_id == 1) found_style1 = true;\n        if (span.style_id == 2) found_style2 = true;\n    }\n    try std.testing.expect(found_style1);\n    try std.testing.expect(found_style2);\n}\n\ntest \"TextBufferView highlights - priority handling in spans\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789\");\n\n    try tb.addHighlight(0, 0, 8, 1, 1, 0);\n    try tb.addHighlight(0, 3, 6, 2, 5, 0);\n\n    const spans = tb.getLineSpans(0);\n    try std.testing.expect(spans.len > 0);\n\n    var found_high_priority = false;\n    for (spans) |span| {\n        if (span.col >= 3 and span.col < 6 and span.style_id == 2) {\n            found_high_priority = true;\n        }\n    }\n    try std.testing.expect(found_high_priority);\n}\n\ntest \"TextBufferView char range highlights - single line highlight\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlightByCharRange(0, 5, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 0), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 5), highlights[0].col_end);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].style_id);\n}\n\ntest \"TextBufferView char range highlights - multi-line highlight\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello\\nWorld\\nTest\");\n\n    try tb.addHighlightByCharRange(3, 9, 1, 1, 0);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    const line1_highlights = tb.getLineHighlights(1);\n\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(usize, 1), line1_highlights.len);\n\n    try std.testing.expectEqual(@as(u32, 3), line0_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 5), line0_highlights[0].col_end);\n\n    try std.testing.expectEqual(@as(u32, 0), line1_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 4), line1_highlights[0].col_end);\n}\n\ntest \"TextBufferView char range highlights - spanning three lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line1\\nLine2\\nLine3\");\n\n    try tb.addHighlightByCharRange(3, 13, 1, 1, 0);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    const line1_highlights = tb.getLineHighlights(1);\n    const line2_highlights = tb.getLineHighlights(2);\n\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(usize, 1), line1_highlights.len);\n    try std.testing.expectEqual(@as(usize, 1), line2_highlights.len);\n\n    try std.testing.expectEqual(@as(u32, 3), line0_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 0), line1_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 0), line2_highlights[0].col_start);\n}\n\ntest \"TextBufferView char range highlights - empty range\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlightByCharRange(5, 5, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBufferView char range highlights - multiple non-overlapping ranges\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"function hello() { return 42; }\");\n\n    try tb.addHighlightByCharRange(0, 8, 1, 1, 0);\n    try tb.addHighlightByCharRange(9, 14, 2, 1, 0);\n    try tb.addHighlightByCharRange(19, 25, 3, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 3), highlights.len);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].style_id);\n    try std.testing.expectEqual(@as(u32, 2), highlights[1].style_id);\n    try std.testing.expectEqual(@as(u32, 3), highlights[2].style_id);\n}\n\ntest \"TextBufferView char range highlights - with reference ID for removal\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line1\\nLine2\\nLine3\");\n\n    try tb.addHighlightByCharRange(0, 5, 1, 1, 100);\n    try tb.addHighlightByCharRange(6, 11, 2, 1, 100);\n\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(0).len);\n    try std.testing.expectEqual(@as(usize, 1), tb.getLineHighlights(1).len);\n\n    tb.removeHighlightsByRef(100);\n\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(0).len);\n    try std.testing.expectEqual(@as(usize, 0), tb.getLineHighlights(1).len);\n}\n\ntest \"TextBufferView highlights - work correctly with wrapped lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    try tb.addHighlight(0, 5, 15, 1, 1, 0);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n\n    const vline0_info = view.getVirtualLineSpans(0);\n    const vline1_info = view.getVirtualLineSpans(1);\n\n    try std.testing.expectEqual(@as(usize, 0), vline0_info.source_line);\n    try std.testing.expectEqual(@as(usize, 0), vline1_info.source_line);\n\n    try std.testing.expectEqual(@as(u32, 0), vline0_info.col_offset);\n    try std.testing.expectEqual(@as(u32, 10), vline1_info.col_offset);\n\n    try std.testing.expect(vline0_info.spans.len > 0);\n    try std.testing.expect(vline1_info.spans.len > 0);\n}\n\ntest \"TextBufferView measureForDimensions - does not modify cache\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    // Set wrap mode but don't call updateVirtualLines\n    view.setWrapMode(.char);\n    view.setWrapWidth(100); // Large width, no wrapping expected\n\n    // Measure with different width WITHOUT updating cache\n    const result = try view.measureForDimensions(10, 10);\n\n    // Should have 2 lines for width 10\n    try std.testing.expectEqual(@as(u32, 2), result.line_count);\n    try std.testing.expectEqual(@as(u32, 10), result.width_cols_max);\n\n    // Now check that the actual cached virtual lines are NOT changed\n    const actual_count = view.getVirtualLineCount();\n    // Should be 1 line because wrap_width is 100\n    try std.testing.expectEqual(@as(u32, 1), actual_count);\n}\n\ntest \"TextBufferView measureForDimensions - cache invalidates after updateVirtualLines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAAAA\");\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n\n    const result1 = try view.measureForDimensions(5, 10);\n    try std.testing.expectEqual(@as(u32, 1), result1.line_count);\n    try std.testing.expectEqual(@as(u32, 5), result1.width_cols_max);\n\n    try tb.setText(\"AAAAAAAAAA\");\n\n    // This clears the dirty flag, which would cause a false cache hit\n    // if we keyed on dirty instead of epoch.\n    _ = view.getVirtualLineCount();\n\n    const result2 = try view.measureForDimensions(5, 10);\n    try std.testing.expectEqual(@as(u32, 2), result2.line_count);\n    try std.testing.expectEqual(@as(u32, 5), result2.width_cols_max);\n}\n\ntest \"TextBufferView measureForDimensions - width 0 uses intrinsic line widths\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"abc\\ndefghij\");\n    view.setWrapMode(.char);\n\n    const result = try view.measureForDimensions(0, 24);\n    try std.testing.expectEqual(tb.getLineCount(), result.line_count);\n    try std.testing.expectEqual(iter_mod.getMaxLineWidth(tb.rope()), result.width_cols_max);\n}\n\ntest \"TextBufferView measureForDimensions - no wrap matches multi-segment line widths\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAAA\");\n    try tb.append(\"BBBB\");\n    view.setWrapMode(.none);\n\n    const line_info = view.getCachedLineInfo();\n    var expected_max: u32 = 0;\n    for (line_info.line_width_cols) |w| {\n        expected_max = @max(expected_max, w);\n    }\n\n    const result = try view.measureForDimensions(80, 24);\n    try std.testing.expectEqual(expected_max, result.width_cols_max);\n    try std.testing.expectEqual(@as(u32, @intCast(line_info.line_width_cols.len)), result.line_count);\n}\n\ntest \"TextBufferView measureForDimensions - cache invalidates on switchToBuffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var other_tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer other_tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAAAAA\");\n    view.setWrapMode(.char);\n\n    const result1 = try view.measureForDimensions(10, 10);\n    try std.testing.expectEqual(@as(u32, 6), result1.width_cols_max);\n\n    try other_tb.setText(\"BBBBBBBBBB\");\n    try std.testing.expectEqual(tb.getContentEpoch(), other_tb.getContentEpoch());\n\n    view.switchToBuffer(other_tb);\n\n    const result2 = try view.measureForDimensions(10, 10);\n    try std.testing.expectEqual(@as(u32, 10), result2.width_cols_max);\n    try std.testing.expectEqual(@as(u32, 1), result2.line_count);\n}\n\ntest \"TextBufferView measureForDimensions - char wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n    view.setWrapMode(.char);\n\n    // Test different widths\n    const result1 = try view.measureForDimensions(10, 10);\n    try std.testing.expectEqual(@as(u32, 2), result1.line_count);\n    try std.testing.expectEqual(@as(u32, 10), result1.width_cols_max);\n\n    const result2 = try view.measureForDimensions(5, 10);\n    try std.testing.expectEqual(@as(u32, 4), result2.line_count);\n    try std.testing.expectEqual(@as(u32, 5), result2.width_cols_max);\n\n    const result3 = try view.measureForDimensions(20, 10);\n    try std.testing.expectEqual(@as(u32, 1), result3.line_count);\n    try std.testing.expectEqual(@as(u32, 20), result3.width_cols_max);\n}\n\ntest \"TextBufferView measureForDimensions - no wrap mode\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello\\nWorld\\nTest\");\n    view.setWrapMode(.none);\n\n    // With no wrap, width shouldn't matter\n    const result = try view.measureForDimensions(3, 10);\n    try std.testing.expectEqual(@as(u32, 3), result.line_count);\n    // width_cols_max should be the longest line\n    try std.testing.expect(result.width_cols_max >= 4);\n}\n\ntest \"TextBufferView measureForDimensions - word wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello wonderful world\");\n    view.setWrapMode(.word);\n\n    const result = try view.measureForDimensions(10, 10);\n    // Should wrap at word boundaries\n    try std.testing.expect(result.line_count >= 2);\n    try std.testing.expect(result.width_cols_max <= 10);\n}\n\ntest \"TextBufferView measureForDimensions - empty buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"\");\n    view.setWrapMode(.char);\n\n    const result = try view.measureForDimensions(10, 10);\n    try std.testing.expectEqual(@as(u32, 1), result.line_count);\n    try std.testing.expectEqual(@as(u32, 0), result.width_cols_max);\n}\n\ntest \"TextBufferView truncation - basic truncate single line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setTruncate(true);\n    view.setWrapMode(.none);\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 10, .height = 5 });\n\n    const vlines = view.getVirtualLines();\n\n    // With truncation, line should be truncated to viewport width\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n    // Width should be reduced (prefix + suffix, ellipsis handled separately)\n    try std.testing.expect(vlines[0].width_cols <= 10);\n}\n\ntest \"TextBufferView truncation - multiline with truncate\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\\nShortLine\\nAnotherVeryLongLineHere\");\n\n    view.setTruncate(true);\n    view.setWrapMode(.none);\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 12, .height = 5 });\n\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 3), vlines.len);\n\n    // First line should be truncated\n    try std.testing.expect(vlines[0].width_cols <= 12);\n    // Second line is short, should not be truncated\n    try std.testing.expectEqual(@as(u32, 9), vlines[1].width_cols);\n    // Third line should be truncated\n    try std.testing.expect(vlines[2].width_cols <= 12);\n}\n\ntest \"TextBufferView truncation - with wrapping disabled\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789ABCDEFGHIJ\");\n\n    view.setTruncate(true);\n    view.setWrapMode(.none);\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 15, .height = 1 });\n\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n    // Should be truncated to fit viewport\n    try std.testing.expect(vlines[0].width_cols <= 15);\n}\n\ntest \"TextBufferView truncation - toggle truncate on and off\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    view.setWrapMode(.none);\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 10, .height = 1 });\n\n    // Without truncation\n    view.setTruncate(false);\n    var vlines = view.getVirtualLines();\n    const width_no_truncate = vlines[0].width_cols;\n\n    // With truncation\n    view.setTruncate(true);\n    vlines = view.getVirtualLines();\n    const width_with_truncate = vlines[0].width_cols;\n\n    try std.testing.expectEqual(@as(u32, 26), width_no_truncate);\n    try std.testing.expect(width_with_truncate <= 10);\n}\n\ntest \"TextBufferView truncation - very small viewport\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    view.setTruncate(true);\n    view.setWrapMode(.none);\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 3, .height = 1 });\n\n    const vlines = view.getVirtualLines();\n\n    // With width=3, only room for \"...\" - should clear the line\n    try std.testing.expectEqual(@as(u32, 0), vlines[0].width_cols);\n}\n\ntest \"TextBufferView truncation - verify ellipsis chunk injection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789ABCDEFGHIJ\");\n\n    view.setTruncate(true);\n    view.setWrapMode(.none);\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 10, .height = 1 });\n\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n    try std.testing.expectEqual(@as(u32, 10), vlines[0].width_cols);\n\n    // Should have 3 chunks: prefix, ellipsis, suffix\n    try std.testing.expectEqual(@as(usize, 3), vlines[0].chunks.items.len);\n\n    // Verify the middle chunk is the ellipsis\n    const ellipsis_chunk = vlines[0].chunks.items[1];\n    try std.testing.expectEqual(@as(u32, 3), ellipsis_chunk.width);\n\n    // Get the ellipsis text to verify it's \"...\"\n    const ellipsis_text = ellipsis_chunk.chunk.getBytes(tb.memRegistry());\n    try std.testing.expectEqualStrings(\"...\", ellipsis_text);\n}\n\ntest \"TextBufferView truncation - works with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    view.setTruncate(true);\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 15, .height = 5 });\n\n    const vlines = view.getVirtualLines();\n\n    // With char wrap at 10, should wrap into multiple lines first\n    // Then truncation should apply to lines exceeding viewport width\n    try std.testing.expect(vlines.len >= 2);\n}\n\ntest \"TextBufferView truncation - verify prefix and suffix content\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"0123456789ABCDEFGHIJ\");\n\n    view.setTruncate(true);\n    view.setWrapMode(.none);\n    view.setViewport(text_buffer_view.Viewport{ .x = 0, .y = 0, .width = 10, .height = 1 });\n\n    const vlines = view.getVirtualLines();\n    const chunks = vlines[0].chunks.items;\n\n    // Should have 3 chunks: prefix, ellipsis, suffix\n    try std.testing.expectEqual(@as(usize, 3), chunks.len);\n\n    // Middle chunk (ellipsis)\n    const ellipsis_bytes = chunks[1].chunk.getBytes(tb.memRegistry());\n\n    // Verify ellipsis is correct\n    try std.testing.expectEqualStrings(\"...\", ellipsis_bytes);\n\n    // Verify total width matches viewport\n    try std.testing.expectEqual(@as(u32, 10), vlines[0].width_cols);\n}\n\ntest \"TextBufferView measureForDimensions - multiple lines with different widths\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Short\\nAVeryLongLineHere\\nMedium\");\n    view.setWrapMode(.char);\n\n    const result = try view.measureForDimensions(10, 10);\n    // \"Short\" (1 line), \"AVeryLongLineHere\" (2 lines), \"Medium\" (1 line) = 4 lines\n    try std.testing.expectEqual(@as(u32, 4), result.line_count);\n    try std.testing.expectEqual(@as(u32, 10), result.width_cols_max);\n}\n\ntest \"TextBufferView highlights - multiple highlights on wrapped line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    try tb.addHighlight(0, 2, 8, 1, 1, 0);\n    try tb.addHighlight(0, 12, 18, 2, 1, 0);\n    try tb.addHighlight(0, 22, 26, 3, 1, 0);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expect(vline_count >= 3);\n\n    for (0..vline_count) |i| {\n        const vline_info = view.getVirtualLineSpans(i);\n        try std.testing.expectEqual(@as(usize, 0), vline_info.source_line);\n        try std.testing.expectEqual(@as(u32, @intCast(i * 10)), vline_info.col_offset);\n    }\n}\n\ntest \"TextBufferView highlights - with emojis and wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AB🌟CD🎨EF🚀GH\");\n\n    try tb.addHighlight(0, 2, 8, 1, 1, 0);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(6);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expect(vline_count >= 2);\n\n    const vline0_info = view.getVirtualLineSpans(0);\n    const vline1_info = view.getVirtualLineSpans(1);\n\n    try std.testing.expectEqual(@as(usize, 0), vline0_info.source_line);\n    try std.testing.expectEqual(@as(usize, 0), vline1_info.source_line);\n\n    try std.testing.expectEqual(@as(u32, 0), vline0_info.col_offset);\n    try std.testing.expect(vline1_info.col_offset == 6);\n\n    try std.testing.expect(vline0_info.spans.len > 0);\n}\n\ntest \"TextBufferView highlights - with CJK characters and wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AB测试CD文字EF\");\n\n    try tb.addHighlight(0, 2, 6, 1, 1, 0);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(6);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expect(vline_count >= 2);\n\n    for (0..vline_count) |i| {\n        const vline_info = view.getVirtualLineSpans(i);\n        try std.testing.expectEqual(@as(usize, 0), vline_info.source_line);\n\n        if (i == 0) {\n            try std.testing.expectEqual(@as(u32, 0), vline_info.col_offset);\n        } else if (i == 1) {\n            try std.testing.expectEqual(@as(u32, 6), vline_info.col_offset);\n        }\n    }\n}\n\ntest \"TextBufferView highlights - mixed ASCII and wide chars with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello🌟世界\");\n\n    try tb.addHighlight(0, 5, 11, 1, 1, 0);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(7);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expect(vline_count >= 2);\n\n    const vline0_info = view.getVirtualLineSpans(0);\n    const vline1_info = view.getVirtualLineSpans(1);\n\n    try std.testing.expectEqual(@as(usize, 0), vline0_info.source_line);\n    try std.testing.expectEqual(@as(usize, 0), vline1_info.source_line);\n\n    try std.testing.expectEqual(@as(u32, 0), vline0_info.col_offset);\n    try std.testing.expectEqual(@as(u32, 7), vline1_info.col_offset);\n\n    try std.testing.expect(vline0_info.spans.len > 0);\n    try std.testing.expect(vline1_info.spans.len > 0);\n}\n\ntest \"TextBufferView highlights - emoji at wrap boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCD🌟EFGH\");\n\n    try tb.addHighlight(0, 3, 7, 1, 1, 0);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(5);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expect(vline_count >= 2);\n\n    const vline0_info = view.getVirtualLineSpans(0);\n    const vline1_info = view.getVirtualLineSpans(1);\n\n    try std.testing.expectEqual(@as(u32, 0), vline0_info.col_offset);\n    try std.testing.expect(vline1_info.col_offset >= 4);\n}\n\ntest \"TextBufferView highlights - emojis without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AB🌟CD🎨EF\");\n\n    try tb.addHighlight(0, 2, 8, 1, 1, 0);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), vline_count);\n\n    const spans = tb.getLineSpans(0);\n    try std.testing.expect(spans.len > 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 2), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 8), highlights[0].col_end);\n}\n\ntest \"TextBufferView highlights - CJK without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AB测试CD\");\n\n    try tb.addHighlight(0, 2, 6, 1, 1, 0);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), vline_count);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 2), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 6), highlights[0].col_end);\n\n    const spans = tb.getLineSpans(0);\n    try std.testing.expect(spans.len > 0);\n}\n\ntest \"TextBufferView highlights - mixed width graphemes without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"A🌟B测C试D\");\n\n    try tb.addHighlight(0, 1, 4, 1, 1, 0);\n    try tb.addHighlight(0, 4, 7, 2, 1, 0);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), vline_count);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 2), highlights.len);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 4), highlights[0].col_end);\n    try std.testing.expectEqual(@as(u32, 4), highlights[1].col_start);\n    try std.testing.expectEqual(@as(u32, 7), highlights[1].col_end);\n\n    const spans = tb.getLineSpans(0);\n    try std.testing.expect(spans.len > 0);\n}\n\ntest \"TextBufferView highlights - emoji at start without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"🌟ABCD\");\n\n    try tb.addHighlight(0, 0, 3, 1, 1, 0);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), vline_count);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 0), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 3), highlights[0].col_end);\n}\n\ntest \"TextBufferView highlights - emoji at end without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCD🌟\");\n\n    try tb.addHighlight(0, 3, 6, 1, 1, 0);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), vline_count);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 3), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 6), highlights[0].col_end);\n}\n\ntest \"TextBufferView highlights - consecutive emojis without wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"A🌟🎨🚀B\");\n\n    try tb.addHighlight(0, 1, 7, 1, 1, 0);\n\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 1), vline_count);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 1), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 7), highlights[0].col_end);\n}\n\ntest \"TextBufferView accessor methods - getVirtualLines and getLines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\");\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), virtual_lines.len);\n\n    try std.testing.expectEqual(@as(u32, 2), tb.lineCount());\n\n    try std.testing.expect(virtual_lines[0].chunks.items.len > 0);\n}\n\ntest \"TextBufferView accessor methods - with wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    const virtual_lines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), virtual_lines.len);\n\n    try std.testing.expectEqual(@as(u32, 1), tb.lineCount());\n}\n\ntest \"TextBufferView virtual lines - match real lines when no wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    try std.testing.expectEqual(@as(u32, 3), view.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(usize, 3), line_info.line_start_cols.len);\n    try std.testing.expectEqual(@as(usize, 3), line_info.line_width_cols.len);\n}\n\ntest \"TextBufferView virtual lines - updated when wrap width set\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    try std.testing.expectEqual(@as(u32, 1), view.getVirtualLineCount());\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n}\n\ntest \"TextBufferView virtual lines - reset to match real lines when wrap removed\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\\nShort\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n    try std.testing.expectEqual(@as(u32, 3), view.getVirtualLineCount());\n\n    view.setWrapWidth(null);\n\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(usize, 2), line_info.line_start_cols.len);\n    try std.testing.expectEqual(@as(usize, 2), line_info.line_width_cols.len);\n}\n\ntest \"TextBufferView virtual lines - multi-line text without wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"First line\\n\\nThird line with more text\\n\");\n\n    try std.testing.expectEqual(@as(u32, 4), view.getVirtualLineCount());\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(usize, 4), line_info.line_start_cols.len);\n    try std.testing.expectEqual(@as(usize, 4), line_info.line_width_cols.len);\n\n    // Verify the line starts are monotonically non-decreasing (empty lines have same start)\n    try std.testing.expect(line_info.line_start_cols[0] == 0);\n    try std.testing.expect(line_info.line_start_cols[1] >= line_info.line_start_cols[0]);\n    try std.testing.expect(line_info.line_start_cols[2] >= line_info.line_start_cols[1]);\n    try std.testing.expect(line_info.line_start_cols[3] >= line_info.line_start_cols[2]);\n}\n\ntest \"TextBufferView line info - line starts and widths consistency\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(7);\n    const line_count = view.getVirtualLineCount();\n    const line_info = view.getCachedLineInfo();\n\n    try std.testing.expectEqual(@as(usize, line_count), line_info.line_start_cols.len);\n    try std.testing.expectEqual(@as(usize, line_count), line_info.line_width_cols.len);\n\n    for (line_info.line_width_cols, 0..) |width, i| {\n        if (i < line_info.line_width_cols.len - 1) {\n            try std.testing.expect(width <= 7);\n        }\n    }\n}\n\ntest \"TextBufferView line info - line starts monotonically increasing\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n\n    var i: u32 = 0;\n    while (i < 99) : (i += 1) {\n        try text_builder.writer(std.testing.allocator).print(\"Line {}\\n\", .{i});\n    }\n    try text_builder.writer(std.testing.allocator).print(\"Line {}\", .{i});\n\n    try tb.setText(text_builder.items);\n\n    const line_count = view.getVirtualLineCount();\n    try std.testing.expectEqual(@as(u32, 100), line_count);\n\n    const line_info = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(u32, 0), line_info.line_start_cols[0]);\n\n    var line_idx: u32 = 1;\n    while (line_idx < 100) : (line_idx += 1) {\n        try std.testing.expect(line_info.line_start_cols[line_idx] >= line_info.line_start_cols[line_idx - 1]);\n    }\n}\n\ntest \"TextBufferView - highlights preserved after wrap width change\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    try tb.addHighlight(0, 0, 10, 1, 0, 0);\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n}\n\ntest \"TextBufferView - get highlights from non-existent line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Line 1\");\n\n    const highlights = tb.getLineHighlights(10);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBufferView - char range highlights out of bounds\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello\");\n\n    try tb.addHighlightByCharRange(3, 100, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 3), highlights[0].col_start);\n}\n\ntest \"TextBufferView - char range highlights invalid range\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n\n    try tb.addHighlightByCharRange(10, 5, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), highlights.len);\n}\n\ntest \"TextBufferView - char range highlights exact line boundaries\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"AAAA\\nBBBB\\nCCCC\");\n\n    try tb.addHighlightByCharRange(0, 4, 1, 1, 0);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(u32, 0), line0_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 4), line0_highlights[0].col_end);\n\n    const line1_highlights = tb.getLineHighlights(1);\n    try std.testing.expectEqual(@as(usize, 0), line1_highlights.len);\n}\n\ntest \"TextBufferView - char range highlights unicode text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello 世界 🌟\");\n\n    const text_len = tb.getLength();\n    try tb.addHighlightByCharRange(0, text_len, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n}\n\ntest \"TextBufferView automatic updates - view reflects buffer changes immediately\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello\");\n    try std.testing.expectEqual(@as(u32, 1), view.getVirtualLineCount());\n\n    var buffer: [100]u8 = undefined;\n    const len1 = view.getPlainTextIntoBuffer(&buffer);\n    try std.testing.expectEqualStrings(\"Hello\", buffer[0..len1]);\n\n    try tb.setText(\"Hello\\nWorld\");\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n\n    const len2 = view.getPlainTextIntoBuffer(&buffer);\n    try std.testing.expectEqualStrings(\"Hello\\nWorld\", buffer[0..len2]);\n}\n\ntest \"TextBufferView automatic updates - multiple views update independently\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view1 = try TextBufferView.init(std.testing.allocator, tb);\n    defer view1.deinit();\n\n    var view2 = try TextBufferView.init(std.testing.allocator, tb);\n    defer view2.deinit();\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    try std.testing.expectEqual(@as(u32, 1), view1.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 1), view2.getVirtualLineCount());\n\n    view1.setWrapMode(.char);\n    view1.setWrapWidth(10);\n    view2.setWrapMode(.char);\n    view2.setWrapWidth(5);\n\n    try std.testing.expectEqual(@as(u32, 2), view1.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 4), view2.getVirtualLineCount());\n\n    try tb.setText(\"Short\");\n\n    try std.testing.expectEqual(@as(u32, 1), view1.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 1), view2.getVirtualLineCount());\n}\n\ntest \"TextBufferView automatic updates - view destroyed doesn't affect others\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view1 = try TextBufferView.init(std.testing.allocator, tb);\n    defer view1.deinit();\n\n    var view2 = try TextBufferView.init(std.testing.allocator, tb);\n\n    try tb.setText(\"Hello\");\n    try std.testing.expectEqual(@as(u32, 1), view1.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 1), view2.getVirtualLineCount());\n\n    view2.deinit();\n\n    try tb.setText(\"Hello\\nWorld\");\n    try std.testing.expectEqual(@as(u32, 2), view1.getVirtualLineCount());\n}\n\ntest \"TextBufferView automatic updates - with wrapping across buffer changes\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    view.setWrapMode(.char);\n    view.setWrapWidth(10);\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n    try std.testing.expectEqual(@as(u32, 2), view.getVirtualLineCount());\n\n    const info1 = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(usize, 2), info1.line_start_cols.len);\n\n    try tb.setText(\"Short\");\n    try std.testing.expectEqual(@as(u32, 1), view.getVirtualLineCount());\n\n    const info2 = view.getCachedLineInfo();\n    try std.testing.expectEqual(@as(usize, 1), info2.line_start_cols.len);\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\");\n    const vline_count = view.getVirtualLineCount();\n    try std.testing.expect(vline_count >= 3);\n\n    const info3 = view.getCachedLineInfo();\n    try std.testing.expect(info3.line_start_cols.len >= 3);\n}\n\ntest \"TextBufferView automatic updates - reset clears content and marks views dirty\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n    try std.testing.expectEqual(@as(u32, 1), view.getVirtualLineCount());\n\n    tb.reset();\n    try std.testing.expectEqual(@as(u32, 1), view.getVirtualLineCount());\n\n    try tb.setText(\"\");\n    try std.testing.expectEqual(@as(u32, 1), view.getVirtualLineCount());\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getPlainTextIntoBuffer(&buffer);\n    try std.testing.expectEqual(@as(usize, 0), len);\n}\n\ntest \"TextBufferView automatic updates - view updates work with selection\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try tb.setText(\"Hello World\");\n    view.setSelection(0, 5, null, null);\n\n    var buffer: [100]u8 = undefined;\n    var len = view.getSelectedTextIntoBuffer(&buffer);\n    try std.testing.expectEqualStrings(\"Hello\", buffer[0..len]);\n\n    try tb.setText(\"Hi\");\n\n    len = view.getPlainTextIntoBuffer(&buffer);\n    try std.testing.expectEqualStrings(\"Hi\", buffer[0..len]);\n}\n\ntest \"TextBufferView automatic updates - multiple views with different wrap settings\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view_nowrap = try TextBufferView.init(std.testing.allocator, tb);\n    defer view_nowrap.deinit();\n\n    var view_wrap10 = try TextBufferView.init(std.testing.allocator, tb);\n    defer view_wrap10.deinit();\n    view_wrap10.setWrapMode(.char);\n    view_wrap10.setWrapWidth(10);\n\n    var view_wrap5 = try TextBufferView.init(std.testing.allocator, tb);\n    defer view_wrap5.deinit();\n    view_wrap5.setWrapMode(.char);\n    view_wrap5.setWrapWidth(5);\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRST\");\n\n    try std.testing.expectEqual(@as(u32, 1), view_nowrap.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 2), view_wrap10.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 4), view_wrap5.getVirtualLineCount());\n    try tb.setText(\"Short\");\n\n    try std.testing.expectEqual(@as(u32, 1), view_nowrap.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 1), view_wrap10.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 1), view_wrap5.getVirtualLineCount());\n\n    try tb.setText(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n\n    try std.testing.expectEqual(@as(u32, 1), view_nowrap.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 3), view_wrap10.getVirtualLineCount());\n    try std.testing.expectEqual(@as(u32, 6), view_wrap5.getVirtualLineCount());\n}\n\ntest \"TextBufferView - tab indicator set and get\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    try std.testing.expect(view.getTabIndicator() == null);\n    try std.testing.expect(view.getTabIndicatorColor() == null);\n\n    view.setTabIndicator(@as(u32, '·'));\n    view.setTabIndicatorColor(RGBA{ 0.4, 0.4, 0.4, 1.0 });\n\n    try std.testing.expectEqual(@as(u32, '·'), view.getTabIndicator().?);\n    try std.testing.expectEqual(@as(f32, 0.4), view.getTabIndicatorColor().?[0]);\n}\n\ntest \"TextBufferView findVisualLineIndex - finds correct line for wrapped text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    // Same text as in the failing test - wraps into 7 virtual lines\n    try tb.setText(\"This is a very long line that will definitely wrap into multiple visual lines when the viewport is small\");\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(20);\n\n    // Test findVisualLineIndex for various logical columns\n    // Column 0 should be in visual line 0\n    const idx0 = view.findVisualLineIndex(0, 0);\n    try std.testing.expectEqual(@as(u32, 0), idx0);\n\n    // Column 20 should be in visual line 1 (starts at col 20)\n    const idx20 = view.findVisualLineIndex(0, 20);\n    try std.testing.expectEqual(@as(u32, 1), idx20);\n\n    // Column 35 should be in visual line 2 (starts at col 35)\n    const idx35 = view.findVisualLineIndex(0, 35);\n    try std.testing.expectEqual(@as(u32, 2), idx35);\n\n    // Column 50 is the last column of visual line 2\n    const idx50 = view.findVisualLineIndex(0, 50);\n    try std.testing.expectEqual(@as(u32, 2), idx50);\n\n    // Column 51 should be in visual line 3 (starts at col 51)\n    const idx51 = view.findVisualLineIndex(0, 51);\n    try std.testing.expectEqual(@as(u32, 3), idx51);\n}\n\ntest \"TextBufferView word wrapping - chunk at exact wrap boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const text = \"hello world ddddddddd\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n\n    const seg_mod = @import(\"../text-buffer-segment.zig\");\n    const Segment = seg_mod.Segment;\n\n    var segments: std.ArrayListUnmanaged(Segment) = .{};\n    defer segments.deinit(std.testing.allocator);\n\n    try segments.append(std.testing.allocator, Segment{ .linestart = {} });\n\n    const chunk1 = tb.createChunk(mem_id, 0, 17);\n    try segments.append(std.testing.allocator, Segment{ .text = chunk1 });\n\n    const chunk2 = tb.createChunk(mem_id, 17, 21);\n    try segments.append(std.testing.allocator, Segment{ .text = chunk2 });\n\n    try tb.rope().setSegments(segments.items);\n    view.virtual_lines_dirty = true;\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(17);\n\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n    try std.testing.expectEqual(@as(u32, 12), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 9), vlines[1].width_cols);\n}\n\ntest \"TextBufferView word wrapping - does not split 'uses' across lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const text =\n        \"So: the per‑repo config is the baseline; the -c flags are a “don’t depend on baseline” guard for commands where output consistency matters. \" ++\n        \"Revert uses checkout, which is less about output formatting and already respects the repo config, so it didn’t get the extra guard. \" ++\n        \"If you want stricter consistency, we can add -c core.autocrlf=false there too.\";\n\n    try tb.setText(text);\n    view.setWrapMode(.word);\n\n    var split_found = false;\n\n    var width: u32 = 100;\n    while (width >= 80) : (width -= 1) {\n        view.setWrapWidth(width);\n\n        const vlines = view.getVirtualLines();\n        var i: usize = 0;\n        while (i + 1 < vlines.len) : (i += 1) {\n            var line_buf: [1024]u8 = undefined;\n            var next_line_buf: [1024]u8 = undefined;\n\n            const line_len = tb.getTextRange(vlines[i].col_offset, vlines[i].col_offset + vlines[i].width_cols, &line_buf);\n            const next_line_len = tb.getTextRange(\n                vlines[i + 1].col_offset,\n                vlines[i + 1].col_offset + vlines[i + 1].width_cols,\n                &next_line_buf,\n            );\n\n            const line = std.mem.trim(u8, line_buf[0..line_len], \" \\t\");\n            const next_line = std.mem.trim(u8, next_line_buf[0..next_line_len], \" \\t\");\n\n            const split_u = std.mem.endsWith(u8, line, \"Revert u\") and std.mem.startsWith(u8, next_line, \"ses checkout\");\n            const split_us = std.mem.endsWith(u8, line, \"Revert us\") and std.mem.startsWith(u8, next_line, \"es checkout\");\n            const split_use = std.mem.endsWith(u8, line, \"Revert use\") and std.mem.startsWith(u8, next_line, \"s checkout\");\n\n            if (split_u or split_us or split_use) {\n                split_found = true;\n                break;\n            }\n        }\n\n        if (split_found or width == 80) {\n            break;\n        }\n    }\n\n    try std.testing.expect(!split_found);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/text-buffer_test.zig",
    "content": "const std = @import(\"std\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\nconst iter_mod = @import(\"../text-buffer-iterators.zig\");\n\nconst TextBuffer = text_buffer.UnifiedTextBuffer;\n\ntest \"TextBuffer init - creates empty buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try std.testing.expectEqual(@as(u32, 0), tb.getLength());\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount()); // Empty buffer has 1 empty line (invariant)\n}\n\ntest \"TextBuffer line info - empty buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\");\n\n    try std.testing.expectEqual(@as(u32, 0), tb.getLength());\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 1), tb.rope().count());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.lineWidthAt(tb.rope(), 0));\n}\n\ntest \"TextBuffer line info - simple text without newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello World\";\n    try tb.setText(text);\n\n    try std.testing.expectEqual(@as(u32, 11), tb.getLength());\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 2), tb.rope().count());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqual(@as(usize, 11), written);\n    try std.testing.expectEqualStrings(text, out_buffer[0..written]);\n}\n\ntest \"TextBuffer line info - single newline\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\\nWorld\");\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) > 0);\n}\n\ntest \"TextBuffer line info - multiple lines separated by newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Line 1\\nLine 2\\nLine 3\";\n    try tb.setText(text);\n\n    try std.testing.expectEqual(@as(u32, 18), tb.getLength());\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 8), tb.rope().count());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 7), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 14), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) > 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqual(@as(usize, 20), written);\n    try std.testing.expectEqualStrings(text, out_buffer[0..written]);\n}\n\ntest \"TextBuffer line info - text ending with newline\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Line 1\\nLine 2\\n\";\n    try tb.setText(text);\n\n    // Trailing newline creates an empty 3rd line (matches editor semantics)\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 7), tb.rope().count());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 7), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 14), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) >= 0); // Empty line\n}\n\ntest \"TextBuffer line info - consecutive newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line 1\\n\\nLine 3\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 7), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 8), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n}\n\ntest \"TextBuffer line info - text starting with newline\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\\nHello World\");\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 1), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n}\n\ntest \"TextBuffer line info - only newlines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\\n\\n\\n\");\n\n    try std.testing.expectEqual(@as(u32, 4), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 1), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 2), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n    try std.testing.expectEqual(@as(u32, 3), iter_mod.coordsToOffset(tb.rope(), 3, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 3) >= 0);\n}\n\ntest \"TextBuffer line info - wide characters (Unicode)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello 世界 🌟\";\n    try tb.setText(text);\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(text, out_buffer[0..written]);\n}\n\ntest \"TextBuffer line info - empty lines between content\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"First\\n\\nThird\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 7), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n}\n\ntest \"TextBuffer line info - very long lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Create a long text with 1000 'A' characters\n    const longText = [_]u8{'A'} ** 1000;\n    try tb.setText(&longText);\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n}\n\ntest \"TextBuffer line info - lines with different widths\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Create text with different line lengths\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n    try text_builder.appendSlice(std.testing.allocator, \"Short\\n\");\n    try text_builder.appendNTimes(std.testing.allocator, 'A', 50);\n    try text_builder.appendSlice(std.testing.allocator, \"\\nMedium\");\n    const text = text_builder.items;\n    try tb.setText(text);\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) < iter_mod.lineWidthAt(tb.rope(), 1));\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) > iter_mod.lineWidthAt(tb.rope(), 2));\n}\n\ntest \"TextBuffer line info - text without styling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // setText now handles all text at once without styling\n    try tb.setText(\"Red\\nBlue\");\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 4), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n}\n\ntest \"TextBuffer line info - buffer with only whitespace\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"   \\n \\n \");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 4), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) >= 0);\n}\n\ntest \"TextBuffer line info - single character lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"A\\nB\\nC\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 2), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 4), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) > 0);\n}\n\ntest \"TextBuffer line info - mixed content with special characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Normal\\n123\\n!@#\\n测试\\n\");\n\n    try std.testing.expectEqual(@as(u32, 5), tb.getLineCount()); // line_count (4 lines + empty line at end)\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 3) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 4) >= 0);\n}\n\ntest \"TextBuffer line info - buffer resize operations\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    // Create a small buffer that will need to resize\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Add text that will cause multiple resizes\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n    try text_builder.appendNTimes(std.testing.allocator, 'A', 100);\n    try text_builder.appendSlice(std.testing.allocator, \"\\n\");\n    try text_builder.appendNTimes(std.testing.allocator, 'B', 100);\n    const longText = text_builder.items;\n    try tb.setText(longText);\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n}\n\ntest \"TextBuffer line info - thousands of lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Create text with 1000 lines\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n\n    var i: u32 = 0;\n    while (i < 999) : (i += 1) {\n        try text_builder.writer(std.testing.allocator).print(\"Line {}\\n\", .{i});\n    }\n    // Last line without newline\n    try text_builder.writer(std.testing.allocator).print(\"Line {}\", .{i});\n\n    try tb.setText(text_builder.items);\n\n    try std.testing.expectEqual(@as(u32, 1000), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n\n    // Check that line starts are monotonically increasing\n    var line_idx: u32 = 1;\n    while (line_idx < 1000) : (line_idx += 1) {\n        try std.testing.expect(iter_mod.coordsToOffset(tb.rope(), line_idx, 0).? > iter_mod.coordsToOffset(tb.rope(), line_idx - 1, 0).?);\n    }\n}\n\ntest \"TextBuffer line info - alternating empty and content lines\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\\nContent\\n\\nMore\\n\\n\");\n\n    try std.testing.expectEqual(@as(u32, 6), tb.getLineCount());\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 3) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 4) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 5) >= 0);\n}\n\ntest \"TextBuffer line info - complex Unicode combining characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"café\\nnaïve\\nrésumé\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) > 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) > 0);\n}\n\ntest \"TextBuffer line info - simple multi-line text\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Test\\nText\");\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 5), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) >= 0);\n}\n\ntest \"TextBuffer line info - unicode width method\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello 世界 🌟\");\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n}\n\ntest \"TextBuffer line info - unicode mixed content with special characters\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Normal\\n123\\n!@#\\n测试\\n\");\n\n    try std.testing.expectEqual(@as(u32, 5), tb.getLineCount()); // line_count (4 lines + empty line at end)\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 2) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 3) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 4) >= 0);\n}\n\ntest \"TextBuffer line info - unicode text without styling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // setText now handles all text at once without styling\n    try tb.setText(\"Red\\nBlue\");\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 4), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) >= 0);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 1) >= 0);\n}\n\ntest \"TextBuffer line info - extremely long single line\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Create extremely long text with 10000 'A' characters\n    const extremelyLongText = [_]u8{'A'} ** 10000;\n    try tb.setText(&extremelyLongText);\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expect(iter_mod.lineWidthAt(tb.rope(), 0) > 0);\n}\n\ntest \"TextBuffer unicode - multi-line with extraction\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello 世界\\n🚀 Emoji\\nΑλφα\";\n    try tb.setText(text);\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(text, out_buffer[0..written]);\n}\n\ntest \"TextBuffer reset - clears all content\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Some text\\nMore text\");\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n\n    tb.reset();\n    try std.testing.expectEqual(@as(u32, 0), tb.getLength());\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n}\n\ntest \"TextBuffer line iteration - walkLines callback\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"First\\nSecond\\nThird\";\n    try tb.setText(text);\n\n    const Context = struct {\n        lines: std.ArrayListUnmanaged(iter_mod.LineInfo),\n        allocator: std.mem.Allocator,\n\n        fn callback(ctx_ptr: *anyopaque, line_info: iter_mod.LineInfo) void {\n            const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n            ctx.lines.append(ctx.allocator, line_info) catch {};\n        }\n    };\n\n    var ctx = Context{ .lines = .{}, .allocator = std.testing.allocator };\n    defer ctx.lines.deinit(std.testing.allocator);\n\n    iter_mod.walkLines(tb.rope(), &ctx, Context.callback, true);\n\n    try std.testing.expectEqual(@as(usize, 3), ctx.lines.items.len);\n    try std.testing.expectEqual(@as(u32, 0), ctx.lines.items[0].line_idx);\n    try std.testing.expectEqual(@as(u32, 5), ctx.lines.items[0].width_cols);\n\n    try std.testing.expectEqual(@as(u32, 1), ctx.lines.items[1].line_idx);\n    try std.testing.expectEqual(@as(u32, 6), ctx.lines.items[1].width_cols);\n\n    try std.testing.expectEqual(@as(u32, 2), ctx.lines.items[2].line_idx);\n    try std.testing.expectEqual(@as(u32, 5), ctx.lines.items[2].width_cols);\n}\n\ntest \"TextBuffer line queries - comprehensive rope coordinate checks\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"First\\nSecond\\nThird\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 5), iter_mod.lineWidthAt(tb.rope(), 0));\n\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.lineWidthAt(tb.rope(), 1));\n\n    try std.testing.expectEqual(@as(u32, 13), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n    try std.testing.expectEqual(@as(u32, 5), iter_mod.lineWidthAt(tb.rope(), 2));\n\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.getMaxLineWidth(tb.rope()));\n}\n\n// ===== View Registration Tests =====\n\ntest \"TextBuffer view registration - multiple views can be created\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const id1 = try tb.registerView();\n    const id2 = try tb.registerView();\n    const id3 = try tb.registerView();\n\n    try std.testing.expect(id1 != id2);\n    try std.testing.expect(id2 != id3);\n    try std.testing.expect(id1 != id3);\n\n    tb.unregisterView(id1);\n    tb.unregisterView(id2);\n    tb.unregisterView(id3);\n}\n\ntest \"TextBuffer view registration - views marked dirty on setText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const id1 = try tb.registerView();\n    defer tb.unregisterView(id1);\n\n    try std.testing.expect(tb.isViewDirty(id1));\n\n    tb.clearViewDirty(id1);\n    try std.testing.expect(!tb.isViewDirty(id1));\n\n    try tb.setText(\"Hello World\");\n    try std.testing.expect(tb.isViewDirty(id1));\n\n    tb.clearViewDirty(id1);\n    try std.testing.expect(!tb.isViewDirty(id1));\n\n    try tb.setText(\"New text\");\n    try std.testing.expect(tb.isViewDirty(id1));\n}\n\ntest \"TextBuffer view registration - views marked dirty on reset\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const id1 = try tb.registerView();\n    defer tb.unregisterView(id1);\n\n    tb.clearViewDirty(id1);\n    try std.testing.expect(!tb.isViewDirty(id1));\n\n    tb.reset();\n    try std.testing.expect(tb.isViewDirty(id1));\n}\n\ntest \"TextBuffer view registration - ID reuse after unregister\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const id1 = try tb.registerView();\n    tb.unregisterView(id1);\n\n    const id2 = try tb.registerView();\n    defer tb.unregisterView(id2);\n\n    try std.testing.expectEqual(id1, id2);\n\n    try std.testing.expect(tb.isViewDirty(id2));\n}\n\ntest \"TextBuffer view registration - multiple views all marked dirty on setText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const id1 = try tb.registerView();\n    defer tb.unregisterView(id1);\n\n    const id2 = try tb.registerView();\n    defer tb.unregisterView(id2);\n\n    const id3 = try tb.registerView();\n    defer tb.unregisterView(id3);\n\n    tb.clearViewDirty(id1);\n    tb.clearViewDirty(id2);\n    tb.clearViewDirty(id3);\n\n    try std.testing.expect(!tb.isViewDirty(id1));\n    try std.testing.expect(!tb.isViewDirty(id2));\n    try std.testing.expect(!tb.isViewDirty(id3));\n\n    try tb.setText(\"Test\");\n\n    try std.testing.expect(tb.isViewDirty(id1));\n    try std.testing.expect(tb.isViewDirty(id2));\n    try std.testing.expect(tb.isViewDirty(id3));\n}\n\n// ===== Memory Registry Tests =====\n\ntest \"TextBuffer memory registry - register and get buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello World\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n\n    const retrieved = tb.getMemBuffer(mem_id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(text, retrieved.?);\n}\n\ntest \"TextBuffer memory registry - multiple buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text1 = \"First buffer\";\n    const text2 = \"Second buffer\";\n    const text3 = \"Third buffer\";\n\n    const id1 = try tb.registerMemBuffer(text1, false);\n    const id2 = try tb.registerMemBuffer(text2, false);\n    const id3 = try tb.registerMemBuffer(text3, false);\n\n    try std.testing.expect(id1 != id2);\n    try std.testing.expect(id2 != id3);\n    try std.testing.expect(id1 != id3);\n\n    try std.testing.expectEqualStrings(text1, tb.getMemBuffer(id1).?);\n    try std.testing.expectEqualStrings(text2, tb.getMemBuffer(id2).?);\n    try std.testing.expectEqualStrings(text3, tb.getMemBuffer(id3).?);\n}\n\ntest \"TextBuffer memory registry - invalid ID returns null\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Try to get buffer with ID that doesn't exist\n    const result = tb.getMemBuffer(99);\n    try std.testing.expect(result == null);\n}\n\ntest \"TextBuffer memory registry - addLine from single buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello World\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n\n    // Add line from buffer\n    try tb.addLine(mem_id, 0, 5); // \"Hello\"\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 5), tb.getLength());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer memory registry - addLine from multiple buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text1 = \"First line\";\n    const text2 = \"Second line\";\n    const text3 = \"Third line\";\n\n    const id1 = try tb.registerMemBuffer(text1, false);\n    const id2 = try tb.registerMemBuffer(text2, false);\n    const id3 = try tb.registerMemBuffer(text3, false);\n\n    try tb.addLine(id1, 0, 10);\n    try tb.addLine(id2, 0, 11);\n    try tb.addLine(id3, 0, 10);\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"First line\\nSecond line\\nThird line\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer memory registry - addLine with invalid mem_id\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Try to add line with invalid mem_id\n    const result = tb.addLine(99, 0, 5);\n    try std.testing.expectError(text_buffer.TextBufferError.InvalidMemId, result);\n}\n\ntest \"TextBuffer memory registry - mixed with setText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Initial text\");\n    try std.testing.expectEqual(@as(u32, 12), tb.getLength());\n\n    const text = \"New text\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n    try tb.addLine(mem_id, 0, 8);\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n}\n\ntest \"TextBuffer memory registry - reset clears memory buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n    try tb.addLine(mem_id, 0, 5);\n\n    tb.reset();\n\n    // Old mem_id should no longer be valid\n    try std.testing.expect(tb.getMemBuffer(mem_id) == null);\n    try std.testing.expectEqual(@as(u32, 0), tb.getLength());\n}\n\ntest \"TextBuffer clear - preserves memory buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello World\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n    try tb.addLine(mem_id, 0, 5); // \"Hello\"\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 5), tb.getLength());\n\n    // Clear should empty the buffer but preserve memory registry\n    tb.clear();\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount()); // Empty buffer has 1 empty line\n    try std.testing.expectEqual(@as(u32, 0), tb.getLength());\n\n    // mem_id should still be valid\n    const retrieved = tb.getMemBuffer(mem_id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(text, retrieved.?);\n\n    // We can re-use the same mem_id after clear\n    try tb.addLine(mem_id, 6, 11); // \"World\"\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 5), tb.getLength());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"World\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer setText - preserves previously registered memory buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Register a memory buffer\n    const old_text = \"Previous content\";\n    const old_mem_id = try tb.registerMemBuffer(old_text, false);\n\n    // Set some text using setText (which now calls clear() not reset())\n    try tb.setText(\"New text content\");\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n\n    // The old mem_id should still be valid after setText\n    const retrieved = tb.getMemBuffer(old_mem_id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(old_text, retrieved.?);\n\n    // We can still use the old mem_id\n    tb.clear();\n    try tb.addLine(old_mem_id, 0, 8); // \"Previous\"\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Previous\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer setStyledText - preserves previously registered memory buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Register a memory buffer before setStyledText\n    const preserved_text = \"Preserved data\";\n    const preserved_mem_id = try tb.registerMemBuffer(preserved_text, false);\n\n    // Use setStyledText (which now calls clear() not reset())\n    const chunk1_text = \"Styled \";\n    const chunk2_text = \"Text\";\n    const chunks = [_]text_buffer.StyledChunk{\n        .{\n            .text_ptr = chunk1_text.ptr,\n            .text_len = chunk1_text.len,\n            .fg_ptr = null,\n            .bg_ptr = null,\n            .attributes = 0,\n        },\n        .{\n            .text_ptr = chunk2_text.ptr,\n            .text_len = chunk2_text.len,\n            .fg_ptr = null,\n            .bg_ptr = null,\n            .attributes = 0,\n        },\n    };\n    try tb.setStyledText(&chunks);\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n\n    // The preserved mem_id should still be valid\n    const retrieved = tb.getMemBuffer(preserved_mem_id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(preserved_text, retrieved.?);\n\n    // We can use the preserved buffer\n    tb.clear();\n    try tb.addLine(preserved_mem_id, 0, 9); // \"Preserved\"\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Preserved\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer clear vs reset - memory registry behavior\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Test buffer\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n    try tb.addLine(mem_id, 0, 4); // \"Test\"\n\n    // clear() preserves memory buffers\n    tb.clear();\n    try std.testing.expect(tb.getMemBuffer(mem_id) != null);\n    try std.testing.expectEqual(@as(u32, 0), tb.getLength());\n\n    // Restore content\n    try tb.addLine(mem_id, 5, 11); // \"buffer\"\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n\n    // reset() clears memory buffers\n    tb.reset();\n    try std.testing.expect(tb.getMemBuffer(mem_id) == null);\n    try std.testing.expectEqual(@as(u32, 0), tb.getLength());\n}\n\ntest \"TextBuffer memory registry - partial buffer slices\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const full_text = \"0123456789ABCDEFGHIJ\";\n    const mem_id = try tb.registerMemBuffer(full_text, false);\n\n    try tb.addLine(mem_id, 0, 5); // \"01234\"\n    try tb.addLine(mem_id, 5, 10); // \"56789\"\n    try tb.addLine(mem_id, 10, 20); // \"ABCDEFGHIJ\"\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"01234\\n56789\\nABCDEFGHIJ\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer memory registry - unicode text from buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text1 = \"Hello 世界\";\n    const text2 = \"🌟 Test\";\n\n    const id1 = try tb.registerMemBuffer(text1, false);\n    const id2 = try tb.registerMemBuffer(text2, false);\n\n    try tb.addLine(id1, 0, @intCast(text1.len));\n    try tb.addLine(id2, 0, @intCast(text2.len));\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    const expected = \"Hello 世界\\n🌟 Test\";\n    try std.testing.expectEqualStrings(expected, out_buffer[0..written]);\n}\n\ntest \"TextBuffer memory registry - getByteSize with multiple buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text1 = \"Hello\"; // 5 bytes\n    const text2 = \"World\"; // 5 bytes\n\n    const id1 = try tb.registerMemBuffer(text1, false);\n    const id2 = try tb.registerMemBuffer(text2, false);\n\n    try tb.addLine(id1, 0, 5);\n    try tb.addLine(id2, 0, 5);\n\n    const byte_size = tb.getByteSize();\n    try std.testing.expectEqual(@as(u32, 11), byte_size);\n}\n\ntest \"TextBuffer memory registry - views marked dirty on addLine\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const view_id = try tb.registerView();\n    defer tb.unregisterView(view_id);\n\n    tb.clearViewDirty(view_id);\n    try std.testing.expect(!tb.isViewDirty(view_id));\n\n    const text = \"Hello\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n    try tb.addLine(mem_id, 0, 5);\n\n    try std.testing.expect(tb.isViewDirty(view_id));\n}\n\ntest \"TextBuffer memory registry - empty chunk handling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello World\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n\n    // Add line with empty slice (start == end)\n    try tb.addLine(mem_id, 5, 5);\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), tb.getLength());\n}\n\ntest \"TextBuffer memory registry - buffer limit of 255\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Register 255 buffers (the maximum for u8)\n    var i: u32 = 0;\n    while (i < 255) : (i += 1) {\n        const text = \"Buffer\";\n        _ = try tb.registerMemBuffer(text, false);\n    }\n\n    // Try to register 256th buffer - should fail\n    const result = tb.registerMemBuffer(\"One more\", false);\n    try std.testing.expectError(text_buffer.TextBufferError.OutOfMemory, result);\n}\n\ntest \"TextBuffer memory registry - owned buffer memory management\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Allocate a buffer that the TextBuffer should own and free\n    const owned_text = try std.testing.allocator.dupe(u8, \"Owned text\");\n    const mem_id = try tb.registerMemBuffer(owned_text, true);\n\n    try tb.addLine(mem_id, 0, 10);\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n\n    // tb.deinit() should free the owned buffer\n    // If there's a memory leak, the test allocator will catch it\n}\n\ntest \"TextBuffer memory registry - byte range out of bounds\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Hello\"; // Only 5 bytes\n    const mem_id = try tb.registerMemBuffer(text, false);\n\n    // This should panic in debug mode or cause undefined behavior\n    // We can't easily test this without catching panics, but we can document it\n    // try tb.addLine(mem_id, 0, 100); // Would access out of bounds\n\n    // Test that valid range works\n    try tb.addLine(mem_id, 0, 5);\n    try std.testing.expectEqual(@as(u32, 5), tb.getLength());\n}\n\ntest \"TextBuffer memory registry - character range highlights across buffers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text1 = \"Line One\";\n    const text2 = \"Line Two\";\n\n    const id1 = try tb.registerMemBuffer(text1, false);\n    const id2 = try tb.registerMemBuffer(text2, false);\n\n    try tb.addLine(id1, 0, 8);\n    try tb.addLine(id2, 0, 8);\n\n    // Add highlight spanning both lines (from different buffers)\n    try tb.addHighlightByCharRange(3, 11, 1, 1, 0);\n\n    const line0_highlights = tb.getLineHighlights(0);\n    const line1_highlights = tb.getLineHighlights(1);\n\n    try std.testing.expectEqual(@as(usize, 1), line0_highlights.len);\n    try std.testing.expectEqual(@as(usize, 1), line1_highlights.len);\n}\n\ntest \"TextBuffer memory registry - empty buffer registration\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const empty_text = \"\";\n    const mem_id = try tb.registerMemBuffer(empty_text, false);\n\n    const retrieved = tb.getMemBuffer(mem_id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqual(@as(usize, 0), retrieved.?.len);\n}\n\ntest \"TextBuffer memory registry - same buffer registered multiple times\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Shared buffer\";\n\n    // Register the same buffer multiple times (different IDs)\n    const id1 = try tb.registerMemBuffer(text, false);\n    const id2 = try tb.registerMemBuffer(text, false);\n    const id3 = try tb.registerMemBuffer(text, false);\n\n    // IDs should be different\n    try std.testing.expect(id1 != id2);\n    try std.testing.expect(id2 != id3);\n\n    // Use different slices of the same registered buffer\n    try tb.addLine(id1, 0, 6); // \"Shared\"\n    try tb.addLine(id2, 7, 13); // \"buffer\"\n    try tb.addLine(id3, 0, 13); // \"Shared buffer\"\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Shared\\nbuffer\\nShared buffer\", out_buffer[0..written]);\n}\n\n// ===== setText SIMD Line Break Tests =====\n\ntest \"TextBuffer setText - CRLF line endings (Windows)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line1\\r\\nLine2\\r\\nLine3\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 12), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Line1\\nLine2\\nLine3\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer setText - mixed line endings (LF, CRLF, CR)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Unix\\nWindows\\r\\nOldMac\\rEnd\");\n\n    try std.testing.expectEqual(@as(u32, 4), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Unix\\nWindows\\nOldMac\\nEnd\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer setText - text ending with CRLF\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello World\\r\\n\");\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 12), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.lineWidthAt(tb.rope(), 1)); // Empty line\n}\n\ntest \"TextBuffer setText - consecutive CRLF sequences\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line1\\r\\n\\r\\nLine3\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n    try std.testing.expectEqual(@as(u32, 7), iter_mod.coordsToOffset(tb.rope(), 2, 0).?);\n}\n\ntest \"TextBuffer setText - only CRLF sequences\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\\r\\n\\r\\n\\r\\n\");\n\n    try std.testing.expectEqual(@as(u32, 4), tb.getLineCount());\n\n    // All lines should be empty\n    for (0..4) |i| {\n        try std.testing.expectEqual(@as(u32, 0), iter_mod.lineWidthAt(tb.rope(), @intCast(i)));\n    }\n}\n\ntest \"TextBuffer setText - text starting with CRLF\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"\\r\\nHello World\");\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?); // Empty first line\n    try std.testing.expectEqual(@as(u32, 1), iter_mod.coordsToOffset(tb.rope(), 1, 0).?);\n}\n\ntest \"TextBuffer setText - CR without LF\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line1\\rLine2\\rLine3\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Line1\\nLine2\\nLine3\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer setText - very long line with SIMD processing\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Create a text longer than 16 bytes (SIMD vector size) to test SIMD path\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n\n    try text_builder.appendNTimes(std.testing.allocator, 'A', 100);\n    try text_builder.appendSlice(std.testing.allocator, \"\\r\\n\");\n    try text_builder.appendNTimes(std.testing.allocator, 'B', 100);\n    try text_builder.appendSlice(std.testing.allocator, \"\\n\");\n    try text_builder.appendNTimes(std.testing.allocator, 'C', 100);\n\n    try tb.setText(text_builder.items);\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 100), iter_mod.lineWidthAt(tb.rope(), 0));\n    try std.testing.expectEqual(@as(u32, 100), iter_mod.lineWidthAt(tb.rope(), 1));\n    try std.testing.expectEqual(@as(u32, 100), iter_mod.lineWidthAt(tb.rope(), 2));\n}\n\ntest \"TextBuffer setText - unicode content with various line endings\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello 世界\\r\\n🌟 Test\\nEnd\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello 世界\\n🌟 Test\\nEnd\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer setText - multiple consecutive different line endings\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Mix of \\n, \\r\\n, \\r in sequence\n    try tb.setText(\"A\\n\\r\\n\\rB\");\n\n    // \"A\", \"\", \"\", \"B\"\n    try std.testing.expectEqual(@as(u32, 4), tb.getLineCount());\n}\n\ntest \"TextBuffer setText - SIMD boundary conditions\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Create text with newlines at SIMD vector boundaries (16 bytes)\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n\n    // 15 chars + \\n = exactly 16 bytes\n    try text_builder.appendNTimes(std.testing.allocator, 'X', 15);\n    try text_builder.appendSlice(std.testing.allocator, \"\\n\");\n    // 15 more chars + \\n\n    try text_builder.appendNTimes(std.testing.allocator, 'Y', 15);\n    try text_builder.appendSlice(std.testing.allocator, \"\\n\");\n    // Final line\n    try text_builder.appendNTimes(std.testing.allocator, 'Z', 10);\n\n    try tb.setText(text_builder.items);\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 15), iter_mod.lineWidthAt(tb.rope(), 0));\n    try std.testing.expectEqual(@as(u32, 15), iter_mod.lineWidthAt(tb.rope(), 1));\n    try std.testing.expectEqual(@as(u32, 10), iter_mod.lineWidthAt(tb.rope(), 2));\n}\n\ntest \"TextBuffer setText - CRLF at SIMD boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Create text where \\r is at end of SIMD vector and \\n is at start of next\n    var text_builder: std.ArrayListUnmanaged(u8) = .{};\n    defer text_builder.deinit(std.testing.allocator);\n\n    // 15 chars + \\r = 16 bytes, then \\n at position 16\n    try text_builder.appendNTimes(std.testing.allocator, 'A', 15);\n    try text_builder.appendSlice(std.testing.allocator, \"\\r\\n\");\n    try text_builder.appendSlice(std.testing.allocator, \"Next line\");\n\n    try tb.setText(text_builder.items);\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 15), iter_mod.lineWidthAt(tb.rope(), 0));\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    const expected_len = 15 + 1 + 9;\n    try std.testing.expectEqual(expected_len, written);\n}\n\ntest \"TextBuffer setText - line with multiple u16-sized chunks (SKIPPED)\" {\n    return error.SkipZigTest;\n}\n\ntest \"TextBuffer setText - validate rope structure is correct\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try text_buffer.UnifiedTextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    try tb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    const line_count = tb.lineCount();\n    try std.testing.expectEqual(@as(u32, 3), line_count);\n\n    const break_count = tb.rope().markerCount(.brk);\n    try std.testing.expectEqual(@as(u32, 2), break_count);\n\n    const linestart_count = tb.rope().markerCount(.linestart);\n    try std.testing.expectEqual(@as(u32, 3), linestart_count);\n\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.lineWidthAt(tb.rope(), 0));\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.lineWidthAt(tb.rope(), 1));\n    try std.testing.expectEqual(@as(u32, 6), iter_mod.lineWidthAt(tb.rope(), 2));\n\n    const total_weight = tb.rope().totalWeight();\n    try std.testing.expectEqual(@as(u32, 20), total_weight);\n}\n\ntest \"TextBuffer setText - then deleteRange via EditBuffer - validate markers\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    const edit_buffer = @import(\"../edit-buffer.zig\");\n    var eb = try edit_buffer.EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    try eb.setText(\"Line 1\\nLine 2\\nLine 3\");\n\n    try eb.deleteRange(.{ .row = 2, .col = 0 }, .{ .row = 2, .col = 6 });\n\n    try std.testing.expectEqual(@as(u32, 2), eb.getTextBuffer().lineCount());\n    try std.testing.expectEqual(@as(u32, 2), eb.getTextBuffer().rope().markerCount(.brk));\n    try std.testing.expectEqual(@as(u32, 2), eb.getTextBuffer().rope().markerCount(.linestart));\n}\n\ntest \"TextBuffer setStyledText - repeated calls with SyntaxStyle (crash reproduction)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Create a SyntaxStyle (similar to what Text.ts does)\n    const ss = @import(\"../syntax-style.zig\");\n    const style = try ss.SyntaxStyle.init(std.testing.allocator);\n    defer style.deinit();\n\n    tb.setSyntaxStyle(style);\n\n    const iterations = 10000;\n    const initial_arena = tb.getArenaAllocatedBytes();\n\n    // Simulate what styled-text-demo does - call setStyledText repeatedly\n    var iteration: u32 = 0;\n    while (iteration < iterations) : (iteration += 1) {\n        // Create styled chunks similar to the demo\n        const text1 = \"System Stats: \";\n        const text2 = \"Frame: \";\n        var frame_buf: [32]u8 = undefined;\n        const frame_text = try std.fmt.bufPrint(&frame_buf, \"{}\", .{iteration});\n\n        const chunks = [_]text_buffer.StyledChunk{\n            .{\n                .text_ptr = text1.ptr,\n                .text_len = text1.len,\n                .fg_ptr = null,\n                .bg_ptr = null,\n                .attributes = 1, // bold\n            },\n            .{\n                .text_ptr = text2.ptr,\n                .text_len = text2.len,\n                .fg_ptr = null,\n                .bg_ptr = null,\n                .attributes = 0,\n            },\n            .{\n                .text_ptr = frame_text.ptr,\n                .text_len = frame_text.len,\n                .fg_ptr = null,\n                .bg_ptr = null,\n                .attributes = 0,\n            },\n        };\n\n        try tb.setStyledText(&chunks);\n        try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    }\n\n    const final_arena = tb.getArenaAllocatedBytes();\n    const arena_growth = final_arena - initial_arena;\n\n    // Arena should not grow significantly - setStyledText should reuse memory\n    // Max 50KB growth is reasonable for rope structure\n    const max_expected_growth = 50000;\n    try std.testing.expect(arena_growth < max_expected_growth);\n}\n\ntest \"addHighlightByCharRange - single line highlight should not extend to EOL\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Try moving your cursor through the [VIRTUAL] markers below:\";\n    try tb.setText(text);\n\n    try tb.addHighlightByCharRange(35, 44, 1, 1, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 1), highlights.len);\n    try std.testing.expectEqual(@as(u32, 35), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 44), highlights[0].col_end);\n\n    try std.testing.expect(highlights[0].col_end < 59);\n    try std.testing.expect(highlights[0].col_end == 44);\n}\n\ntest \"addHighlightByCharRange - multiple highlights on same line should have correct bounds\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Text [MARK1] and [MARK2] here\";\n    try tb.setText(text);\n\n    try tb.addHighlightByCharRange(5, 12, 1, 1, 0);\n    try tb.addHighlightByCharRange(17, 24, 1, 2, 0);\n\n    const highlights = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 2), highlights.len);\n\n    try std.testing.expectEqual(@as(u32, 5), highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 12), highlights[0].col_end);\n\n    try std.testing.expectEqual(@as(u32, 17), highlights[1].col_start);\n    try std.testing.expectEqual(@as(u32, 24), highlights[1].col_end);\n}\n\ntest \"addHighlightByCharRange - highlight after newline should not span to EOL\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Line1\\nLine2 with [MARK] text\\nLine3\";\n    try tb.setText(text);\n\n    const line1_char_offset: u32 = 5;\n    const mark_start = line1_char_offset + 11;\n    const mark_end = line1_char_offset + 17;\n\n    try tb.addHighlightByCharRange(mark_start, mark_end, 1, 1, 0);\n\n    const hl0 = tb.getLineHighlights(0);\n    try std.testing.expectEqual(@as(usize, 0), hl0.len);\n\n    const hl1 = tb.getLineHighlights(1);\n    try std.testing.expectEqual(@as(usize, 1), hl1.len);\n\n    try std.testing.expectEqual(@as(u32, 11), hl1[0].col_start);\n    try std.testing.expectEqual(@as(u32, 17), hl1[0].col_end);\n\n    const line1_text = \"Line2 with [MARK] text\";\n    try std.testing.expect(hl1[0].col_end < line1_text.len);\n}\n\ntest \"addHighlightByCharRange - extmarks demo scenario reproduction\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const full_text =\n        \\\\Welcome to the Extmarks Demo!\n        \\\\\n        \\\\This demo showcases virtual extmarks - text ranges that the cursor jumps over.\n        \\\\\n        \\\\Try moving your cursor through the [VIRTUAL] markers below:\n        \\\\- Use arrow keys to navigate\n    ;\n    try tb.setText(full_text);\n\n    const line4_char_offset: u32 = 107;\n    const virtual_start = line4_char_offset + 35;\n    const virtual_end = line4_char_offset + 44;\n\n    try tb.addHighlightByCharRange(virtual_start, virtual_end, 1, 1, 0);\n\n    const line4_highlights = tb.getLineHighlights(4);\n    try std.testing.expectEqual(@as(usize, 1), line4_highlights.len);\n    try std.testing.expectEqual(@as(u32, 35), line4_highlights[0].col_start);\n    try std.testing.expectEqual(@as(u32, 44), line4_highlights[0].col_end);\n\n    const line4_text = \"Try moving your cursor through the [VIRTUAL] markers below:\";\n    try std.testing.expect(line4_highlights[0].col_end == 44);\n    try std.testing.expect(line4_highlights[0].col_end < line4_text.len);\n}\n\n// ===== TextBuffer.append() Tests =====\n\ntest \"TextBuffer append - to empty buffer\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.append(\"Hello\");\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 5), tb.getLength());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - to non-empty buffer, no newline\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\");\n    try tb.append(\" World\");\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n    try std.testing.expectEqual(@as(u32, 11), tb.getLength());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello World\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - creating new line with LF\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\");\n    try tb.append(\"\\nWorld\");\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello\\nWorld\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - multiple lines with various endings\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"A\\nB\");\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n\n    try tb.append(\"\\nC\\nD\\n\");\n\n    try std.testing.expectEqual(@as(u32, 5), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"A\\nB\\nC\\nD\\n\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - CRLF line endings\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.append(\"Line1\\r\\nLine2\\r\\nLine3\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    // CRLF should be normalized to LF\n    try std.testing.expectEqualStrings(\"Line1\\nLine2\\nLine3\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - mixed line endings\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Unix\\n\");\n    try tb.append(\"Windows\\r\\nOldMac\\rEnd\");\n\n    try std.testing.expectEqual(@as(u32, 4), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Unix\\nWindows\\nOldMac\\nEnd\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - empty string is no-op\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello\");\n    const initial_length = tb.getLength();\n    const initial_line_count = tb.getLineCount();\n\n    try tb.append(\"\");\n\n    try std.testing.expectEqual(initial_length, tb.getLength());\n    try std.testing.expectEqual(initial_line_count, tb.getLineCount());\n}\n\ntest \"TextBuffer append - unicode content\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Hello \");\n    try tb.append(\"世界 🌟\");\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Hello 世界 🌟\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - streaming/chunked append vs ground truth\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Append in chunks\n    try tb.append(\"First\");\n    try tb.append(\"\\nLine2\");\n    try tb.append(\"\\n\");\n    try tb.append(\"Line3\");\n    try tb.append(\" end\");\n\n    // Build expected ground truth\n    var expected: std.ArrayListUnmanaged(u8) = .{};\n    defer expected.deinit(std.testing.allocator);\n    try expected.appendSlice(std.testing.allocator, \"First\");\n    try expected.appendSlice(std.testing.allocator, \"\\nLine2\");\n    try expected.appendSlice(std.testing.allocator, \"\\n\");\n    try expected.appendSlice(std.testing.allocator, \"Line3\");\n    try expected.appendSlice(std.testing.allocator, \" end\");\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(expected.items, out_buffer[0..written]);\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n}\n\ntest \"TextBuffer append - large streaming append\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    // Simulate streaming large content\n    var i: u32 = 0;\n    while (i < 100) : (i += 1) {\n        var buf: [32]u8 = undefined;\n        const line = try std.fmt.bufPrint(&buf, \"Line {}\\n\", .{i});\n        try tb.append(line);\n    }\n\n    try std.testing.expectEqual(@as(u32, 101), tb.getLineCount()); // 100 lines + empty final line\n\n    // Verify first and last lines can be extracted correctly\n    try std.testing.expectEqual(@as(u32, 0), iter_mod.coordsToOffset(tb.rope(), 0, 0).?);\n    try std.testing.expect(iter_mod.coordsToOffset(tb.rope(), 99, 0).? > 0);\n}\n\ntest \"TextBuffer appendFromMemId - basic functionality\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Alpha\\nBeta\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n\n    try tb.appendFromMemId(mem_id);\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Alpha\\nBeta\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer appendFromMemId - append to existing content\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const text = \"Gamma\";\n    const mem_id = try tb.registerMemBuffer(text, false);\n\n    try tb.setText(\"Alpha\\nBeta\");\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n\n    try tb.appendFromMemId(mem_id);\n\n    try std.testing.expectEqual(@as(u32, 2), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Alpha\\nBetaGamma\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer appendFromMemId - invalid mem_id\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const result = tb.appendFromMemId(99);\n    try std.testing.expectError(text_buffer.TextBufferError.InvalidMemId, result);\n}\n\ntest \"TextBuffer append - marker invariants maintained\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.append(\"Line1\\n\");\n    try tb.append(\"Line2\\n\");\n    try tb.append(\"Line3\");\n\n    const line_count = tb.getLineCount();\n    try std.testing.expectEqual(@as(u32, 3), line_count);\n\n    // Verify marker counts\n    const linestart_count = tb.rope().markerCount(.linestart);\n    try std.testing.expectEqual(line_count, linestart_count);\n\n    const break_count = tb.rope().markerCount(.brk);\n    try std.testing.expectEqual(@as(u32, 2), break_count); // 2 newlines\n}\n\ntest \"TextBuffer append - memory registry preserved\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const preserved_text = \"Preserved\";\n    const preserved_id = try tb.registerMemBuffer(preserved_text, false);\n\n    try tb.append(\"First\\n\");\n    try tb.append(\"Second\\n\");\n    try tb.append(\"Third\");\n\n    // Preserved buffer should still be accessible\n    const retrieved = tb.getMemBuffer(preserved_id);\n    try std.testing.expect(retrieved != null);\n    try std.testing.expectEqualStrings(preserved_text, retrieved.?);\n}\n\ntest \"TextBuffer append - views marked dirty\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    const view_id = try tb.registerView();\n    defer tb.unregisterView(view_id);\n\n    tb.clearViewDirty(view_id);\n    try std.testing.expect(!tb.isViewDirty(view_id));\n\n    try tb.append(\"New content\");\n\n    try std.testing.expect(tb.isViewDirty(view_id));\n}\n\ntest \"TextBuffer append - append after clear\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Initial content\");\n    tb.clear();\n\n    try tb.append(\"After clear\");\n\n    try std.testing.expectEqual(@as(u32, 1), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"After clear\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - consecutive empty line handling\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"Line1\\n\");\n    try tb.append(\"\\n\");\n    try tb.append(\"Line3\");\n\n    try std.testing.expectEqual(@as(u32, 3), tb.getLineCount());\n\n    var out_buffer: [100]u8 = undefined;\n    const written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Line1\\n\\nLine3\", out_buffer[0..written]);\n}\n\ntest \"TextBuffer append - mixed append and setText\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .unicode);\n    defer tb.deinit();\n\n    try tb.setText(\"First\");\n    try tb.append(\" appended\");\n\n    var out_buffer: [100]u8 = undefined;\n    var written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"First appended\", out_buffer[0..written]);\n\n    try tb.setText(\"Reset\");\n    try tb.append(\" again\");\n\n    written = tb.getPlainTextIntoBuffer(&out_buffer);\n    try std.testing.expectEqualStrings(\"Reset again\", out_buffer[0..written]);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/unicode-width-map.zon",
    "content": ".{\n    .{ .codepoint = \"U+0000\", .width = 0 },\n    .{ .codepoint = \"U+0010\", .width = 0 },\n    .{ .codepoint = \"U+0020\", .width = 1 },\n    .{ .codepoint = \"U+0021\", .width = 1 },\n    .{ .codepoint = \"U+0022\", .width = 1 },\n    .{ .codepoint = \"U+0023\", .width = 1 },\n    .{ .codepoint = \"U+0024\", .width = 1 },\n    .{ .codepoint = \"U+0025\", .width = 1 },\n    .{ .codepoint = \"U+0026\", .width = 1 },\n    .{ .codepoint = \"U+0027\", .width = 1 },\n    .{ .codepoint = \"U+0028\", .width = 1 },\n    .{ .codepoint = \"U+0029\", .width = 1 },\n    .{ .codepoint = \"U+002A\", .width = 1 },\n    .{ .codepoint = \"U+002B\", .width = 1 },\n    .{ .codepoint = \"U+002C\", .width = 1 },\n    .{ .codepoint = \"U+002D\", .width = 1 },\n    .{ .codepoint = \"U+002E\", .width = 1 },\n    .{ .codepoint = \"U+002F\", .width = 1 },\n    .{ .codepoint = \"U+0030\", .width = 1 },\n    .{ .codepoint = \"U+0031\", .width = 1 },\n    .{ .codepoint = \"U+0032\", .width = 1 },\n    .{ .codepoint = \"U+0033\", .width = 1 },\n    .{ .codepoint = \"U+0034\", .width = 1 },\n    .{ .codepoint = \"U+0035\", .width = 1 },\n    .{ .codepoint = \"U+0036\", .width = 1 },\n    .{ .codepoint = \"U+0037\", .width = 1 },\n    .{ .codepoint = \"U+0038\", .width = 1 },\n    .{ .codepoint = \"U+0039\", .width = 1 },\n    .{ .codepoint = \"U+003A\", .width = 1 },\n    .{ .codepoint = \"U+003B\", .width = 1 },\n    .{ .codepoint = \"U+003C\", .width = 1 },\n    .{ .codepoint = \"U+003D\", .width = 1 },\n    .{ .codepoint = \"U+003E\", .width = 1 },\n    .{ .codepoint = \"U+003F\", .width = 1 },\n    .{ .codepoint = \"U+0040\", .width = 1 },\n    .{ .codepoint = \"U+0041\", .width = 1 },\n    .{ .codepoint = \"U+0042\", .width = 1 },\n    .{ .codepoint = \"U+0043\", .width = 1 },\n    .{ .codepoint = \"U+0044\", .width = 1 },\n    .{ .codepoint = \"U+0045\", .width = 1 },\n    .{ .codepoint = \"U+0046\", .width = 1 },\n    .{ .codepoint = \"U+0047\", .width = 1 },\n    .{ .codepoint = \"U+0048\", .width = 1 },\n    .{ .codepoint = \"U+0049\", .width = 1 },\n    .{ .codepoint = \"U+004A\", .width = 1 },\n    .{ .codepoint = \"U+004B\", .width = 1 },\n    .{ .codepoint = \"U+004C\", .width = 1 },\n    .{ .codepoint = \"U+004D\", .width = 1 },\n    .{ .codepoint = \"U+004E\", .width = 1 },\n    .{ .codepoint = \"U+004F\", .width = 1 },\n    .{ .codepoint = \"U+0050\", .width = 1 },\n    .{ .codepoint = \"U+0051\", .width = 1 },\n    .{ .codepoint = \"U+0052\", .width = 1 },\n    .{ .codepoint = \"U+0053\", .width = 1 },\n    .{ .codepoint = \"U+0054\", .width = 1 },\n    .{ .codepoint = \"U+0055\", .width = 1 },\n    .{ .codepoint = \"U+0056\", .width = 1 },\n    .{ .codepoint = \"U+0057\", .width = 1 },\n    .{ .codepoint = \"U+0058\", .width = 1 },\n    .{ .codepoint = \"U+0059\", .width = 1 },\n    .{ .codepoint = \"U+005A\", .width = 1 },\n    .{ .codepoint = \"U+005B\", .width = 1 },\n    .{ .codepoint = \"U+005C\", .width = 1 },\n    .{ .codepoint = \"U+005D\", .width = 1 },\n    .{ .codepoint = \"U+005E\", .width = 1 },\n    .{ .codepoint = \"U+005F\", .width = 1 },\n    .{ .codepoint = \"U+0060\", .width = 1 },\n    .{ .codepoint = \"U+0061\", .width = 1 },\n    .{ .codepoint = \"U+0062\", .width = 1 },\n    .{ .codepoint = \"U+0063\", .width = 1 },\n    .{ .codepoint = \"U+0064\", .width = 1 },\n    .{ .codepoint = \"U+0065\", .width = 1 },\n    .{ .codepoint = \"U+0066\", .width = 1 },\n    .{ .codepoint = \"U+0067\", .width = 1 },\n    .{ .codepoint = \"U+0068\", .width = 1 },\n    .{ .codepoint = \"U+0069\", .width = 1 },\n    .{ .codepoint = \"U+006A\", .width = 1 },\n    .{ .codepoint = \"U+006B\", .width = 1 },\n    .{ .codepoint = \"U+006C\", .width = 1 },\n    .{ .codepoint = \"U+006D\", .width = 1 },\n    .{ .codepoint = \"U+006E\", .width = 1 },\n    .{ .codepoint = \"U+006F\", .width = 1 },\n    .{ .codepoint = \"U+0070\", .width = 1 },\n    .{ .codepoint = \"U+0071\", .width = 1 },\n    .{ .codepoint = \"U+0072\", .width = 1 },\n    .{ .codepoint = \"U+0073\", .width = 1 },\n    .{ .codepoint = \"U+0074\", .width = 1 },\n    .{ .codepoint = \"U+0075\", .width = 1 },\n    .{ .codepoint = \"U+0076\", .width = 1 },\n    .{ .codepoint = \"U+0077\", .width = 1 },\n    .{ .codepoint = \"U+0078\", .width = 1 },\n    .{ .codepoint = \"U+0079\", .width = 1 },\n    .{ .codepoint = \"U+007A\", .width = 1 },\n    .{ .codepoint = \"U+007B\", .width = 1 },\n    .{ .codepoint = \"U+007C\", .width = 1 },\n    .{ .codepoint = \"U+007D\", .width = 1 },\n    .{ .codepoint = \"U+007E\", .width = 1 },\n    .{ .codepoint = \"U+0080\", .width = 0 },\n    .{ .codepoint = \"U+0090\", .width = 0 },\n    .{ .codepoint = \"U+00A0\", .width = 1 },\n    .{ .codepoint = \"U+00A1\", .width = 1 },\n    .{ .codepoint = \"U+00A2\", .width = 1 },\n    .{ .codepoint = \"U+00A3\", .width = 1 },\n    .{ .codepoint = \"U+00A4\", .width = 1 },\n    .{ .codepoint = \"U+00A5\", .width = 1 },\n    .{ .codepoint = \"U+00A6\", .width = 1 },\n    .{ .codepoint = \"U+00A7\", .width = 1 },\n    .{ .codepoint = \"U+00A8\", .width = 1 },\n    .{ .codepoint = \"U+00A9\", .width = 1 },\n    .{ .codepoint = \"U+00AA\", .width = 1 },\n    .{ .codepoint = \"U+00AB\", .width = 1 },\n    .{ .codepoint = \"U+00AC\", .width = 1 },\n    .{ .codepoint = \"U+00AD\", .width = 1 },\n    .{ .codepoint = \"U+00AE\", .width = 1 },\n    .{ .codepoint = \"U+00AF\", .width = 1 },\n    .{ .codepoint = \"U+00B0\", .width = 1 },\n    .{ .codepoint = \"U+00B1\", .width = 1 },\n    .{ .codepoint = \"U+00B2\", .width = 1 },\n    .{ .codepoint = \"U+00B3\", .width = 1 },\n    .{ .codepoint = \"U+00B4\", .width = 1 },\n    .{ .codepoint = \"U+00B5\", .width = 1 },\n    .{ .codepoint = \"U+00B6\", .width = 1 },\n    .{ .codepoint = \"U+00B7\", .width = 1 },\n    .{ .codepoint = \"U+00B8\", .width = 1 },\n    .{ .codepoint = \"U+00B9\", .width = 1 },\n    .{ .codepoint = \"U+00BA\", .width = 1 },\n    .{ .codepoint = \"U+00BB\", .width = 1 },\n    .{ .codepoint = \"U+00BC\", .width = 1 },\n    .{ .codepoint = \"U+00BD\", .width = 1 },\n    .{ .codepoint = \"U+00BE\", .width = 1 },\n    .{ .codepoint = \"U+00BF\", .width = 1 },\n    .{ .codepoint = \"U+00C0\", .width = 1 },\n    .{ .codepoint = \"U+00C1\", .width = 1 },\n    .{ .codepoint = \"U+00C2\", .width = 1 },\n    .{ .codepoint = \"U+00C3\", .width = 1 },\n    .{ .codepoint = \"U+00C4\", .width = 1 },\n    .{ .codepoint = \"U+00C5\", .width = 1 },\n    .{ .codepoint = \"U+00C6\", .width = 1 },\n    .{ .codepoint = \"U+00C7\", .width = 1 },\n    .{ .codepoint = \"U+00C8\", .width = 1 },\n    .{ .codepoint = \"U+00C9\", .width = 1 },\n    .{ .codepoint = \"U+00CA\", .width = 1 },\n    .{ .codepoint = \"U+00CB\", .width = 1 },\n    .{ .codepoint = \"U+00CC\", .width = 1 },\n    .{ .codepoint = \"U+00CD\", .width = 1 },\n    .{ .codepoint = \"U+00CE\", .width = 1 },\n    .{ .codepoint = \"U+00CF\", .width = 1 },\n    .{ .codepoint = \"U+00D0\", .width = 1 },\n    .{ .codepoint = \"U+00D1\", .width = 1 },\n    .{ .codepoint = \"U+00D2\", .width = 1 },\n    .{ .codepoint = \"U+00D3\", .width = 1 },\n    .{ .codepoint = \"U+00D4\", .width = 1 },\n    .{ .codepoint = \"U+00D5\", .width = 1 },\n    .{ .codepoint = \"U+00D6\", .width = 1 },\n    .{ .codepoint = \"U+00D7\", .width = 1 },\n    .{ .codepoint = \"U+00D8\", .width = 1 },\n    .{ .codepoint = \"U+00D9\", .width = 1 },\n    .{ .codepoint = \"U+00DA\", .width = 1 },\n    .{ .codepoint = \"U+00DB\", .width = 1 },\n    .{ .codepoint = \"U+00DC\", .width = 1 },\n    .{ .codepoint = \"U+00DD\", .width = 1 },\n    .{ .codepoint = \"U+00DE\", .width = 1 },\n    .{ .codepoint = \"U+00DF\", .width = 1 },\n    .{ .codepoint = \"U+00E0\", .width = 1 },\n    .{ .codepoint = \"U+00E1\", .width = 1 },\n    .{ .codepoint = \"U+00E2\", .width = 1 },\n    .{ .codepoint = \"U+00E3\", .width = 1 },\n    .{ .codepoint = \"U+00E4\", .width = 1 },\n    .{ .codepoint = \"U+00E5\", .width = 1 },\n    .{ .codepoint = \"U+00E6\", .width = 1 },\n    .{ .codepoint = \"U+00E7\", .width = 1 },\n    .{ .codepoint = \"U+00E8\", .width = 1 },\n    .{ .codepoint = \"U+00E9\", .width = 1 },\n    .{ .codepoint = \"U+00EA\", .width = 1 },\n    .{ .codepoint = \"U+00EB\", .width = 1 },\n    .{ .codepoint = \"U+00EC\", .width = 1 },\n    .{ .codepoint = \"U+00ED\", .width = 1 },\n    .{ .codepoint = \"U+00EE\", .width = 1 },\n    .{ .codepoint = \"U+00EF\", .width = 1 },\n    .{ .codepoint = \"U+00F0\", .width = 1 },\n    .{ .codepoint = \"U+00F1\", .width = 1 },\n    .{ .codepoint = \"U+00F2\", .width = 1 },\n    .{ .codepoint = \"U+00F3\", .width = 1 },\n    .{ .codepoint = \"U+00F4\", .width = 1 },\n    .{ .codepoint = \"U+00F5\", .width = 1 },\n    .{ .codepoint = \"U+00F6\", .width = 1 },\n    .{ .codepoint = \"U+00F7\", .width = 1 },\n    .{ .codepoint = \"U+00F8\", .width = 1 },\n    .{ .codepoint = \"U+00F9\", .width = 1 },\n    .{ .codepoint = \"U+00FA\", .width = 1 },\n    .{ .codepoint = \"U+00FB\", .width = 1 },\n    .{ .codepoint = \"U+00FC\", .width = 1 },\n    .{ .codepoint = \"U+00FD\", .width = 1 },\n    .{ .codepoint = \"U+00FE\", .width = 1 },\n    .{ .codepoint = \"U+00FF\", .width = 1 },\n    .{ .codepoint = \"U+0100\", .width = 1 },\n    .{ .codepoint = \"U+0101\", .width = 1 },\n    .{ .codepoint = \"U+0102\", .width = 1 },\n    .{ .codepoint = \"U+0103\", .width = 1 },\n    .{ .codepoint = \"U+0104\", .width = 1 },\n    .{ .codepoint = \"U+0105\", .width = 1 },\n    .{ .codepoint = \"U+0106\", .width = 1 },\n    .{ .codepoint = \"U+0107\", .width = 1 },\n    .{ .codepoint = \"U+0108\", .width = 1 },\n    .{ .codepoint = \"U+0109\", .width = 1 },\n    .{ .codepoint = \"U+010A\", .width = 1 },\n    .{ .codepoint = \"U+010B\", .width = 1 },\n    .{ .codepoint = \"U+010C\", .width = 1 },\n    .{ .codepoint = \"U+010D\", .width = 1 },\n    .{ .codepoint = \"U+010E\", .width = 1 },\n    .{ .codepoint = \"U+010F\", .width = 1 },\n    .{ .codepoint = \"U+0110\", .width = 1 },\n    .{ .codepoint = \"U+0111\", .width = 1 },\n    .{ .codepoint = \"U+0112\", .width = 1 },\n    .{ .codepoint = \"U+0113\", .width = 1 },\n    .{ .codepoint = \"U+0114\", .width = 1 },\n    .{ .codepoint = \"U+0115\", .width = 1 },\n    .{ .codepoint = \"U+0116\", .width = 1 },\n    .{ .codepoint = \"U+0117\", .width = 1 },\n    .{ .codepoint = \"U+0118\", .width = 1 },\n    .{ .codepoint = \"U+0119\", .width = 1 },\n    .{ .codepoint = \"U+011A\", .width = 1 },\n    .{ .codepoint = \"U+011B\", .width = 1 },\n    .{ .codepoint = \"U+011C\", .width = 1 },\n    .{ .codepoint = \"U+011D\", .width = 1 },\n    .{ .codepoint = \"U+011E\", .width = 1 },\n    .{ .codepoint = \"U+011F\", .width = 1 },\n    .{ .codepoint = \"U+0120\", .width = 1 },\n    .{ .codepoint = \"U+0121\", .width = 1 },\n    .{ .codepoint = \"U+0122\", .width = 1 },\n    .{ .codepoint = \"U+0123\", .width = 1 },\n    .{ .codepoint = \"U+0124\", .width = 1 },\n    .{ .codepoint = \"U+0125\", .width = 1 },\n    .{ .codepoint = \"U+0126\", .width = 1 },\n    .{ .codepoint = \"U+0127\", .width = 1 },\n    .{ .codepoint = \"U+0128\", .width = 1 },\n    .{ .codepoint = \"U+0129\", .width = 1 },\n    .{ .codepoint = \"U+012A\", .width = 1 },\n    .{ .codepoint = \"U+012B\", .width = 1 },\n    .{ .codepoint = \"U+012C\", .width = 1 },\n    .{ .codepoint = \"U+012D\", .width = 1 },\n    .{ .codepoint = \"U+012E\", .width = 1 },\n    .{ .codepoint = \"U+012F\", .width = 1 },\n    .{ .codepoint = \"U+0130\", .width = 1 },\n    .{ .codepoint = \"U+0131\", .width = 1 },\n    .{ .codepoint = \"U+0132\", .width = 1 },\n    .{ .codepoint = \"U+0133\", .width = 1 },\n    .{ .codepoint = \"U+0134\", .width = 1 },\n    .{ .codepoint = \"U+0135\", .width = 1 },\n    .{ .codepoint = \"U+0136\", .width = 1 },\n    .{ .codepoint = \"U+0137\", .width = 1 },\n    .{ .codepoint = \"U+0138\", .width = 1 },\n    .{ .codepoint = \"U+0139\", .width = 1 },\n    .{ .codepoint = \"U+013A\", .width = 1 },\n    .{ .codepoint = \"U+013B\", .width = 1 },\n    .{ .codepoint = \"U+013C\", .width = 1 },\n    .{ .codepoint = \"U+013D\", .width = 1 },\n    .{ .codepoint = \"U+013E\", .width = 1 },\n    .{ .codepoint = \"U+013F\", .width = 1 },\n    .{ .codepoint = \"U+0140\", .width = 1 },\n    .{ .codepoint = \"U+0141\", .width = 1 },\n    .{ .codepoint = \"U+0142\", .width = 1 },\n    .{ .codepoint = \"U+0143\", .width = 1 },\n    .{ .codepoint = \"U+0144\", .width = 1 },\n    .{ .codepoint = \"U+0145\", .width = 1 },\n    .{ .codepoint = \"U+0146\", .width = 1 },\n    .{ .codepoint = \"U+0147\", .width = 1 },\n    .{ .codepoint = \"U+0148\", .width = 1 },\n    .{ .codepoint = \"U+0149\", .width = 1 },\n    .{ .codepoint = \"U+014A\", .width = 1 },\n    .{ .codepoint = \"U+014B\", .width = 1 },\n    .{ .codepoint = \"U+014C\", .width = 1 },\n    .{ .codepoint = \"U+014D\", .width = 1 },\n    .{ .codepoint = \"U+014E\", .width = 1 },\n    .{ .codepoint = \"U+014F\", .width = 1 },\n    .{ .codepoint = \"U+0150\", .width = 1 },\n    .{ .codepoint = \"U+0151\", .width = 1 },\n    .{ .codepoint = \"U+0152\", .width = 1 },\n    .{ .codepoint = \"U+0153\", .width = 1 },\n    .{ .codepoint = \"U+0154\", .width = 1 },\n    .{ .codepoint = \"U+0155\", .width = 1 },\n    .{ .codepoint = \"U+0156\", .width = 1 },\n    .{ .codepoint = \"U+0157\", .width = 1 },\n    .{ .codepoint = \"U+0158\", .width = 1 },\n    .{ .codepoint = \"U+0159\", .width = 1 },\n    .{ .codepoint = \"U+015A\", .width = 1 },\n    .{ .codepoint = \"U+015B\", .width = 1 },\n    .{ .codepoint = \"U+015C\", .width = 1 },\n    .{ .codepoint = \"U+015D\", .width = 1 },\n    .{ .codepoint = \"U+015E\", .width = 1 },\n    .{ .codepoint = \"U+015F\", .width = 1 },\n    .{ .codepoint = \"U+0160\", .width = 1 },\n    .{ .codepoint = \"U+0161\", .width = 1 },\n    .{ .codepoint = \"U+0162\", .width = 1 },\n    .{ .codepoint = \"U+0163\", .width = 1 },\n    .{ .codepoint = \"U+0164\", .width = 1 },\n    .{ .codepoint = \"U+0165\", .width = 1 },\n    .{ .codepoint = \"U+0166\", .width = 1 },\n    .{ .codepoint = \"U+0167\", .width = 1 },\n    .{ .codepoint = \"U+0168\", .width = 1 },\n    .{ .codepoint = \"U+0169\", .width = 1 },\n    .{ .codepoint = \"U+016A\", .width = 1 },\n    .{ .codepoint = \"U+016B\", .width = 1 },\n    .{ .codepoint = \"U+016C\", .width = 1 },\n    .{ .codepoint = \"U+016D\", .width = 1 },\n    .{ .codepoint = \"U+016E\", .width = 1 },\n    .{ .codepoint = \"U+016F\", .width = 1 },\n    .{ .codepoint = \"U+0170\", .width = 1 },\n    .{ .codepoint = \"U+0171\", .width = 1 },\n    .{ .codepoint = \"U+0172\", .width = 1 },\n    .{ .codepoint = \"U+0173\", .width = 1 },\n    .{ .codepoint = \"U+0174\", .width = 1 },\n    .{ .codepoint = \"U+0175\", .width = 1 },\n    .{ .codepoint = \"U+0176\", .width = 1 },\n    .{ .codepoint = \"U+0177\", .width = 1 },\n    .{ .codepoint = \"U+0178\", .width = 1 },\n    .{ .codepoint = \"U+0179\", .width = 1 },\n    .{ .codepoint = \"U+017A\", .width = 1 },\n    .{ .codepoint = \"U+017B\", .width = 1 },\n    .{ .codepoint = \"U+017C\", .width = 1 },\n    .{ .codepoint = \"U+017D\", .width = 1 },\n    .{ .codepoint = \"U+017E\", .width = 1 },\n    .{ .codepoint = \"U+017F\", .width = 1 },\n    .{ .codepoint = \"U+0180\", .width = 1 },\n    .{ .codepoint = \"U+0181\", .width = 1 },\n    .{ .codepoint = \"U+0182\", .width = 1 },\n    .{ .codepoint = \"U+0183\", .width = 1 },\n    .{ .codepoint = \"U+0184\", .width = 1 },\n    .{ .codepoint = \"U+0185\", .width = 1 },\n    .{ .codepoint = \"U+0186\", .width = 1 },\n    .{ .codepoint = \"U+0187\", .width = 1 },\n    .{ .codepoint = \"U+0188\", .width = 1 },\n    .{ .codepoint = \"U+0189\", .width = 1 },\n    .{ .codepoint = \"U+018A\", .width = 1 },\n    .{ .codepoint = \"U+018B\", .width = 1 },\n    .{ .codepoint = \"U+018C\", .width = 1 },\n    .{ .codepoint = \"U+018D\", .width = 1 },\n    .{ .codepoint = \"U+018E\", .width = 1 },\n    .{ .codepoint = \"U+018F\", .width = 1 },\n    .{ .codepoint = \"U+0190\", .width = 1 },\n    .{ .codepoint = \"U+0191\", .width = 1 },\n    .{ .codepoint = \"U+0192\", .width = 1 },\n    .{ .codepoint = \"U+0193\", .width = 1 },\n    .{ .codepoint = \"U+0194\", .width = 1 },\n    .{ .codepoint = \"U+0195\", .width = 1 },\n    .{ .codepoint = \"U+0196\", .width = 1 },\n    .{ .codepoint = \"U+0197\", .width = 1 },\n    .{ .codepoint = \"U+0198\", .width = 1 },\n    .{ .codepoint = \"U+0199\", .width = 1 },\n    .{ .codepoint = \"U+019A\", .width = 1 },\n    .{ .codepoint = \"U+019B\", .width = 1 },\n    .{ .codepoint = \"U+019C\", .width = 1 },\n    .{ .codepoint = \"U+019D\", .width = 1 },\n    .{ .codepoint = \"U+019E\", .width = 1 },\n    .{ .codepoint = \"U+019F\", .width = 1 },\n    .{ .codepoint = \"U+01A0\", .width = 1 },\n    .{ .codepoint = \"U+01A1\", .width = 1 },\n    .{ .codepoint = \"U+01A2\", .width = 1 },\n    .{ .codepoint = \"U+01A3\", .width = 1 },\n    .{ .codepoint = \"U+01A4\", .width = 1 },\n    .{ .codepoint = \"U+01A5\", .width = 1 },\n    .{ .codepoint = \"U+01A6\", .width = 1 },\n    .{ .codepoint = \"U+01A7\", .width = 1 },\n    .{ .codepoint = \"U+01A8\", .width = 1 },\n    .{ .codepoint = \"U+01A9\", .width = 1 },\n    .{ .codepoint = \"U+01AA\", .width = 1 },\n    .{ .codepoint = \"U+01AB\", .width = 1 },\n    .{ .codepoint = \"U+01AC\", .width = 1 },\n    .{ .codepoint = \"U+01AD\", .width = 1 },\n    .{ .codepoint = \"U+01AE\", .width = 1 },\n    .{ .codepoint = \"U+01AF\", .width = 1 },\n    .{ .codepoint = \"U+01B0\", .width = 1 },\n    .{ .codepoint = \"U+01B1\", .width = 1 },\n    .{ .codepoint = \"U+01B2\", .width = 1 },\n    .{ .codepoint = \"U+01B3\", .width = 1 },\n    .{ .codepoint = \"U+01B4\", .width = 1 },\n    .{ .codepoint = \"U+01B5\", .width = 1 },\n    .{ .codepoint = \"U+01B6\", .width = 1 },\n    .{ .codepoint = \"U+01B7\", .width = 1 },\n    .{ .codepoint = \"U+01B8\", .width = 1 },\n    .{ .codepoint = \"U+01B9\", .width = 1 },\n    .{ .codepoint = \"U+01BA\", .width = 1 },\n    .{ .codepoint = \"U+01BB\", .width = 1 },\n    .{ .codepoint = \"U+01BC\", .width = 1 },\n    .{ .codepoint = \"U+01BD\", .width = 1 },\n    .{ .codepoint = \"U+01BE\", .width = 1 },\n    .{ .codepoint = \"U+01BF\", .width = 1 },\n    .{ .codepoint = \"U+01C0\", .width = 1 },\n    .{ .codepoint = \"U+01C1\", .width = 1 },\n    .{ .codepoint = \"U+01C2\", .width = 1 },\n    .{ .codepoint = \"U+01C3\", .width = 1 },\n    .{ .codepoint = \"U+01C4\", .width = 1 },\n    .{ .codepoint = \"U+01C5\", .width = 1 },\n    .{ .codepoint = \"U+01C6\", .width = 1 },\n    .{ .codepoint = \"U+01C7\", .width = 1 },\n    .{ .codepoint = \"U+01C8\", .width = 1 },\n    .{ .codepoint = \"U+01C9\", .width = 1 },\n    .{ .codepoint = \"U+01CA\", .width = 1 },\n    .{ .codepoint = \"U+01CB\", .width = 1 },\n    .{ .codepoint = \"U+01CC\", .width = 1 },\n    .{ .codepoint = \"U+01CD\", .width = 1 },\n    .{ .codepoint = \"U+01CE\", .width = 1 },\n    .{ .codepoint = \"U+01CF\", .width = 1 },\n    .{ .codepoint = \"U+01D0\", .width = 1 },\n    .{ .codepoint = \"U+01D1\", .width = 1 },\n    .{ .codepoint = \"U+01D2\", .width = 1 },\n    .{ .codepoint = \"U+01D3\", .width = 1 },\n    .{ .codepoint = \"U+01D4\", .width = 1 },\n    .{ .codepoint = \"U+01D5\", .width = 1 },\n    .{ .codepoint = \"U+01D6\", .width = 1 },\n    .{ .codepoint = \"U+01D7\", .width = 1 },\n    .{ .codepoint = \"U+01D8\", .width = 1 },\n    .{ .codepoint = \"U+01D9\", .width = 1 },\n    .{ .codepoint = \"U+01DA\", .width = 1 },\n    .{ .codepoint = \"U+01DB\", .width = 1 },\n    .{ .codepoint = \"U+01DC\", .width = 1 },\n    .{ .codepoint = \"U+01DD\", .width = 1 },\n    .{ .codepoint = \"U+01DE\", .width = 1 },\n    .{ .codepoint = \"U+01DF\", .width = 1 },\n    .{ .codepoint = \"U+01E0\", .width = 1 },\n    .{ .codepoint = \"U+01E1\", .width = 1 },\n    .{ .codepoint = \"U+01E2\", .width = 1 },\n    .{ .codepoint = \"U+01E3\", .width = 1 },\n    .{ .codepoint = \"U+01E4\", .width = 1 },\n    .{ .codepoint = \"U+01E5\", .width = 1 },\n    .{ .codepoint = \"U+01E6\", .width = 1 },\n    .{ .codepoint = \"U+01E7\", .width = 1 },\n    .{ .codepoint = \"U+01E8\", .width = 1 },\n    .{ .codepoint = \"U+01E9\", .width = 1 },\n    .{ .codepoint = \"U+01EA\", .width = 1 },\n    .{ .codepoint = \"U+01EB\", .width = 1 },\n    .{ .codepoint = \"U+01EC\", .width = 1 },\n    .{ .codepoint = \"U+01ED\", .width = 1 },\n    .{ .codepoint = \"U+01EE\", .width = 1 },\n    .{ .codepoint = \"U+01EF\", .width = 1 },\n    .{ .codepoint = \"U+01F0\", .width = 1 },\n    .{ .codepoint = \"U+01F1\", .width = 1 },\n    .{ .codepoint = \"U+01F2\", .width = 1 },\n    .{ .codepoint = \"U+01F3\", .width = 1 },\n    .{ .codepoint = \"U+01F4\", .width = 1 },\n    .{ .codepoint = \"U+01F5\", .width = 1 },\n    .{ .codepoint = \"U+01F6\", .width = 1 },\n    .{ .codepoint = \"U+01F7\", .width = 1 },\n    .{ .codepoint = \"U+01F8\", .width = 1 },\n    .{ .codepoint = \"U+01F9\", .width = 1 },\n    .{ .codepoint = \"U+01FA\", .width = 1 },\n    .{ .codepoint = \"U+01FB\", .width = 1 },\n    .{ .codepoint = \"U+01FC\", .width = 1 },\n    .{ .codepoint = \"U+01FD\", .width = 1 },\n    .{ .codepoint = \"U+01FE\", .width = 1 },\n    .{ .codepoint = \"U+01FF\", .width = 1 },\n    .{ .codepoint = \"U+0200\", .width = 1 },\n    .{ .codepoint = \"U+0201\", .width = 1 },\n    .{ .codepoint = \"U+0202\", .width = 1 },\n    .{ .codepoint = \"U+0203\", .width = 1 },\n    .{ .codepoint = \"U+0204\", .width = 1 },\n    .{ .codepoint = \"U+0205\", .width = 1 },\n    .{ .codepoint = \"U+0206\", .width = 1 },\n    .{ .codepoint = \"U+0207\", .width = 1 },\n    .{ .codepoint = \"U+0208\", .width = 1 },\n    .{ .codepoint = \"U+0209\", .width = 1 },\n    .{ .codepoint = \"U+020A\", .width = 1 },\n    .{ .codepoint = \"U+020B\", .width = 1 },\n    .{ .codepoint = \"U+020C\", .width = 1 },\n    .{ .codepoint = \"U+020D\", .width = 1 },\n    .{ .codepoint = \"U+020E\", .width = 1 },\n    .{ .codepoint = \"U+020F\", .width = 1 },\n    .{ .codepoint = \"U+0210\", .width = 1 },\n    .{ .codepoint = \"U+0211\", .width = 1 },\n    .{ .codepoint = \"U+0212\", .width = 1 },\n    .{ .codepoint = \"U+0213\", .width = 1 },\n    .{ .codepoint = \"U+0214\", .width = 1 },\n    .{ .codepoint = \"U+0215\", .width = 1 },\n    .{ .codepoint = \"U+0216\", .width = 1 },\n    .{ .codepoint = \"U+0217\", .width = 1 },\n    .{ .codepoint = \"U+0218\", .width = 1 },\n    .{ .codepoint = \"U+0219\", .width = 1 },\n    .{ .codepoint = \"U+021A\", .width = 1 },\n    .{ .codepoint = \"U+021B\", .width = 1 },\n    .{ .codepoint = \"U+021C\", .width = 1 },\n    .{ .codepoint = \"U+021D\", .width = 1 },\n    .{ .codepoint = \"U+021E\", .width = 1 },\n    .{ .codepoint = \"U+021F\", .width = 1 },\n    .{ .codepoint = \"U+0220\", .width = 1 },\n    .{ .codepoint = \"U+0221\", .width = 1 },\n    .{ .codepoint = \"U+0222\", .width = 1 },\n    .{ .codepoint = \"U+0223\", .width = 1 },\n    .{ .codepoint = \"U+0224\", .width = 1 },\n    .{ .codepoint = \"U+0225\", .width = 1 },\n    .{ .codepoint = \"U+0226\", .width = 1 },\n    .{ .codepoint = \"U+0227\", .width = 1 },\n    .{ .codepoint = \"U+0228\", .width = 1 },\n    .{ .codepoint = \"U+0229\", .width = 1 },\n    .{ .codepoint = \"U+022A\", .width = 1 },\n    .{ .codepoint = \"U+022B\", .width = 1 },\n    .{ .codepoint = \"U+022C\", .width = 1 },\n    .{ .codepoint = \"U+022D\", .width = 1 },\n    .{ .codepoint = \"U+022E\", .width = 1 },\n    .{ .codepoint = \"U+022F\", .width = 1 },\n    .{ .codepoint = \"U+0230\", .width = 1 },\n    .{ .codepoint = \"U+0231\", .width = 1 },\n    .{ .codepoint = \"U+0232\", .width = 1 },\n    .{ .codepoint = \"U+0233\", .width = 1 },\n    .{ .codepoint = \"U+0234\", .width = 1 },\n    .{ .codepoint = \"U+0235\", .width = 1 },\n    .{ .codepoint = \"U+0236\", .width = 1 },\n    .{ .codepoint = \"U+0237\", .width = 1 },\n    .{ .codepoint = \"U+0238\", .width = 1 },\n    .{ .codepoint = \"U+0239\", .width = 1 },\n    .{ .codepoint = \"U+023A\", .width = 1 },\n    .{ .codepoint = \"U+023B\", .width = 1 },\n    .{ .codepoint = \"U+023C\", .width = 1 },\n    .{ .codepoint = \"U+023D\", .width = 1 },\n    .{ .codepoint = \"U+023E\", .width = 1 },\n    .{ .codepoint = \"U+023F\", .width = 1 },\n    .{ .codepoint = \"U+0240\", .width = 1 },\n    .{ .codepoint = \"U+0241\", .width = 1 },\n    .{ .codepoint = \"U+0242\", .width = 1 },\n    .{ .codepoint = \"U+0243\", .width = 1 },\n    .{ .codepoint = \"U+0244\", .width = 1 },\n    .{ .codepoint = \"U+0245\", .width = 1 },\n    .{ .codepoint = \"U+0246\", .width = 1 },\n    .{ .codepoint = \"U+0247\", .width = 1 },\n    .{ .codepoint = \"U+0248\", .width = 1 },\n    .{ .codepoint = \"U+0249\", .width = 1 },\n    .{ .codepoint = \"U+024A\", .width = 1 },\n    .{ .codepoint = \"U+024B\", .width = 1 },\n    .{ .codepoint = \"U+024C\", .width = 1 },\n    .{ .codepoint = \"U+024D\", .width = 1 },\n    .{ .codepoint = \"U+024E\", .width = 1 },\n    .{ .codepoint = \"U+024F\", .width = 1 },\n    .{ .codepoint = \"U+2000\", .width = 1 },\n    .{ .codepoint = \"U+2001\", .width = 1 },\n    .{ .codepoint = \"U+2002\", .width = 1 },\n    .{ .codepoint = \"U+2003\", .width = 1 },\n    .{ .codepoint = \"U+2004\", .width = 1 },\n    .{ .codepoint = \"U+2005\", .width = 1 },\n    .{ .codepoint = \"U+2006\", .width = 1 },\n    .{ .codepoint = \"U+2007\", .width = 1 },\n    .{ .codepoint = \"U+2008\", .width = 1 },\n    .{ .codepoint = \"U+2009\", .width = 1 },\n    .{ .codepoint = \"U+200A\", .width = 1 },\n    .{ .codepoint = \"U+2010\", .width = 1 },\n    .{ .codepoint = \"U+2011\", .width = 1 },\n    .{ .codepoint = \"U+2012\", .width = 1 },\n    .{ .codepoint = \"U+2013\", .width = 1 },\n    .{ .codepoint = \"U+2014\", .width = 1 },\n    .{ .codepoint = \"U+2015\", .width = 1 },\n    .{ .codepoint = \"U+2016\", .width = 1 },\n    .{ .codepoint = \"U+2017\", .width = 1 },\n    .{ .codepoint = \"U+2018\", .width = 1 },\n    .{ .codepoint = \"U+2019\", .width = 1 },\n    .{ .codepoint = \"U+201A\", .width = 1 },\n    .{ .codepoint = \"U+201B\", .width = 1 },\n    .{ .codepoint = \"U+201C\", .width = 1 },\n    .{ .codepoint = \"U+201D\", .width = 1 },\n    .{ .codepoint = \"U+201E\", .width = 1 },\n    .{ .codepoint = \"U+201F\", .width = 1 },\n    .{ .codepoint = \"U+2020\", .width = 1 },\n    .{ .codepoint = \"U+2021\", .width = 1 },\n    .{ .codepoint = \"U+2022\", .width = 1 },\n    .{ .codepoint = \"U+2023\", .width = 1 },\n    .{ .codepoint = \"U+2024\", .width = 1 },\n    .{ .codepoint = \"U+2025\", .width = 1 },\n    .{ .codepoint = \"U+2026\", .width = 1 },\n    .{ .codepoint = \"U+2027\", .width = 1 },\n    .{ .codepoint = \"U+202F\", .width = 1 },\n    .{ .codepoint = \"U+2030\", .width = 1 },\n    .{ .codepoint = \"U+2031\", .width = 1 },\n    .{ .codepoint = \"U+2032\", .width = 1 },\n    .{ .codepoint = \"U+2033\", .width = 1 },\n    .{ .codepoint = \"U+2034\", .width = 1 },\n    .{ .codepoint = \"U+2035\", .width = 1 },\n    .{ .codepoint = \"U+2036\", .width = 1 },\n    .{ .codepoint = \"U+2037\", .width = 1 },\n    .{ .codepoint = \"U+2038\", .width = 1 },\n    .{ .codepoint = \"U+2039\", .width = 1 },\n    .{ .codepoint = \"U+203A\", .width = 1 },\n    .{ .codepoint = \"U+203B\", .width = 1 },\n    .{ .codepoint = \"U+203C\", .width = 2 },\n    .{ .codepoint = \"U+203D\", .width = 1 },\n    .{ .codepoint = \"U+203E\", .width = 1 },\n    .{ .codepoint = \"U+203F\", .width = 1 },\n    .{ .codepoint = \"U+2040\", .width = 1 },\n    .{ .codepoint = \"U+2041\", .width = 1 },\n    .{ .codepoint = \"U+2042\", .width = 1 },\n    .{ .codepoint = \"U+2043\", .width = 1 },\n    .{ .codepoint = \"U+2044\", .width = 1 },\n    .{ .codepoint = \"U+2045\", .width = 1 },\n    .{ .codepoint = \"U+2046\", .width = 1 },\n    .{ .codepoint = \"U+2047\", .width = 1 },\n    .{ .codepoint = \"U+2048\", .width = 1 },\n    .{ .codepoint = \"U+2049\", .width = 2 },\n    .{ .codepoint = \"U+204A\", .width = 1 },\n    .{ .codepoint = \"U+204B\", .width = 1 },\n    .{ .codepoint = \"U+204C\", .width = 1 },\n    .{ .codepoint = \"U+204D\", .width = 1 },\n    .{ .codepoint = \"U+204E\", .width = 1 },\n    .{ .codepoint = \"U+204F\", .width = 1 },\n    .{ .codepoint = \"U+2050\", .width = 1 },\n    .{ .codepoint = \"U+2051\", .width = 1 },\n    .{ .codepoint = \"U+2052\", .width = 1 },\n    .{ .codepoint = \"U+2053\", .width = 1 },\n    .{ .codepoint = \"U+2054\", .width = 1 },\n    .{ .codepoint = \"U+2055\", .width = 1 },\n    .{ .codepoint = \"U+2056\", .width = 1 },\n    .{ .codepoint = \"U+2057\", .width = 1 },\n    .{ .codepoint = \"U+2058\", .width = 1 },\n    .{ .codepoint = \"U+2059\", .width = 1 },\n    .{ .codepoint = \"U+205A\", .width = 1 },\n    .{ .codepoint = \"U+205B\", .width = 1 },\n    .{ .codepoint = \"U+205C\", .width = 1 },\n    .{ .codepoint = \"U+205D\", .width = 1 },\n    .{ .codepoint = \"U+205E\", .width = 1 },\n    .{ .codepoint = \"U+205F\", .width = 1 },\n    .{ .codepoint = \"U+2070\", .width = 1 },\n    .{ .codepoint = \"U+2071\", .width = 1 },\n    .{ .codepoint = \"U+2072\", .width = 1 },\n    .{ .codepoint = \"U+2073\", .width = 1 },\n    .{ .codepoint = \"U+2074\", .width = 1 },\n    .{ .codepoint = \"U+2075\", .width = 1 },\n    .{ .codepoint = \"U+2076\", .width = 1 },\n    .{ .codepoint = \"U+2077\", .width = 1 },\n    .{ .codepoint = \"U+2078\", .width = 1 },\n    .{ .codepoint = \"U+2079\", .width = 1 },\n    .{ .codepoint = \"U+207A\", .width = 1 },\n    .{ .codepoint = \"U+207B\", .width = 1 },\n    .{ .codepoint = \"U+207C\", .width = 1 },\n    .{ .codepoint = \"U+207D\", .width = 1 },\n    .{ .codepoint = \"U+207E\", .width = 1 },\n    .{ .codepoint = \"U+207F\", .width = 1 },\n    .{ .codepoint = \"U+2080\", .width = 1 },\n    .{ .codepoint = \"U+2081\", .width = 1 },\n    .{ .codepoint = \"U+2082\", .width = 1 },\n    .{ .codepoint = \"U+2083\", .width = 1 },\n    .{ .codepoint = \"U+2084\", .width = 1 },\n    .{ .codepoint = \"U+2085\", .width = 1 },\n    .{ .codepoint = \"U+2086\", .width = 1 },\n    .{ .codepoint = \"U+2087\", .width = 1 },\n    .{ .codepoint = \"U+2088\", .width = 1 },\n    .{ .codepoint = \"U+2089\", .width = 1 },\n    .{ .codepoint = \"U+208A\", .width = 1 },\n    .{ .codepoint = \"U+208B\", .width = 1 },\n    .{ .codepoint = \"U+208C\", .width = 1 },\n    .{ .codepoint = \"U+208D\", .width = 1 },\n    .{ .codepoint = \"U+208E\", .width = 1 },\n    .{ .codepoint = \"U+208F\", .width = 1 },\n    .{ .codepoint = \"U+2090\", .width = 1 },\n    .{ .codepoint = \"U+2091\", .width = 1 },\n    .{ .codepoint = \"U+2092\", .width = 1 },\n    .{ .codepoint = \"U+2093\", .width = 1 },\n    .{ .codepoint = \"U+2094\", .width = 1 },\n    .{ .codepoint = \"U+2095\", .width = 1 },\n    .{ .codepoint = \"U+2096\", .width = 1 },\n    .{ .codepoint = \"U+2097\", .width = 1 },\n    .{ .codepoint = \"U+2098\", .width = 1 },\n    .{ .codepoint = \"U+2099\", .width = 1 },\n    .{ .codepoint = \"U+209A\", .width = 1 },\n    .{ .codepoint = \"U+209B\", .width = 1 },\n    .{ .codepoint = \"U+209C\", .width = 1 },\n    .{ .codepoint = \"U+209D\", .width = 1 },\n    .{ .codepoint = \"U+209E\", .width = 1 },\n    .{ .codepoint = \"U+209F\", .width = 1 },\n    .{ .codepoint = \"U+20A0\", .width = 1 },\n    .{ .codepoint = \"U+20A1\", .width = 1 },\n    .{ .codepoint = \"U+20A2\", .width = 1 },\n    .{ .codepoint = \"U+20A3\", .width = 1 },\n    .{ .codepoint = \"U+20A4\", .width = 1 },\n    .{ .codepoint = \"U+20A5\", .width = 1 },\n    .{ .codepoint = \"U+20A6\", .width = 1 },\n    .{ .codepoint = \"U+20A7\", .width = 1 },\n    .{ .codepoint = \"U+20A8\", .width = 1 },\n    .{ .codepoint = \"U+20A9\", .width = 1 },\n    .{ .codepoint = \"U+20AA\", .width = 1 },\n    .{ .codepoint = \"U+20AB\", .width = 1 },\n    .{ .codepoint = \"U+20AC\", .width = 1 },\n    .{ .codepoint = \"U+20AD\", .width = 1 },\n    .{ .codepoint = \"U+20AE\", .width = 1 },\n    .{ .codepoint = \"U+20AF\", .width = 1 },\n    .{ .codepoint = \"U+20B0\", .width = 1 },\n    .{ .codepoint = \"U+20B1\", .width = 1 },\n    .{ .codepoint = \"U+20B2\", .width = 1 },\n    .{ .codepoint = \"U+20B3\", .width = 1 },\n    .{ .codepoint = \"U+20B4\", .width = 1 },\n    .{ .codepoint = \"U+20B5\", .width = 1 },\n    .{ .codepoint = \"U+20B6\", .width = 1 },\n    .{ .codepoint = \"U+20B7\", .width = 1 },\n    .{ .codepoint = \"U+20B8\", .width = 1 },\n    .{ .codepoint = \"U+20B9\", .width = 1 },\n    .{ .codepoint = \"U+20BA\", .width = 1 },\n    .{ .codepoint = \"U+20BB\", .width = 1 },\n    .{ .codepoint = \"U+20BC\", .width = 1 },\n    .{ .codepoint = \"U+20BD\", .width = 1 },\n    .{ .codepoint = \"U+20BE\", .width = 1 },\n    .{ .codepoint = \"U+20BF\", .width = 1 },\n    .{ .codepoint = \"U+20C0\", .width = 1 },\n    .{ .codepoint = \"U+20C1\", .width = 1 },\n    .{ .codepoint = \"U+20C2\", .width = 1 },\n    .{ .codepoint = \"U+20C3\", .width = 1 },\n    .{ .codepoint = \"U+20C4\", .width = 1 },\n    .{ .codepoint = \"U+20C5\", .width = 1 },\n    .{ .codepoint = \"U+20C6\", .width = 1 },\n    .{ .codepoint = \"U+20C7\", .width = 1 },\n    .{ .codepoint = \"U+20C8\", .width = 1 },\n    .{ .codepoint = \"U+20C9\", .width = 1 },\n    .{ .codepoint = \"U+20CA\", .width = 1 },\n    .{ .codepoint = \"U+20CB\", .width = 1 },\n    .{ .codepoint = \"U+20CC\", .width = 1 },\n    .{ .codepoint = \"U+20CD\", .width = 1 },\n    .{ .codepoint = \"U+20CE\", .width = 1 },\n    .{ .codepoint = \"U+20CF\", .width = 1 },\n    .{ .codepoint = \"U+2100\", .width = 1 },\n    .{ .codepoint = \"U+2101\", .width = 1 },\n    .{ .codepoint = \"U+2102\", .width = 1 },\n    .{ .codepoint = \"U+2103\", .width = 1 },\n    .{ .codepoint = \"U+2104\", .width = 1 },\n    .{ .codepoint = \"U+2105\", .width = 1 },\n    .{ .codepoint = \"U+2106\", .width = 1 },\n    .{ .codepoint = \"U+2107\", .width = 1 },\n    .{ .codepoint = \"U+2108\", .width = 1 },\n    .{ .codepoint = \"U+2109\", .width = 1 },\n    .{ .codepoint = \"U+210A\", .width = 1 },\n    .{ .codepoint = \"U+210B\", .width = 1 },\n    .{ .codepoint = \"U+210C\", .width = 1 },\n    .{ .codepoint = \"U+210D\", .width = 1 },\n    .{ .codepoint = \"U+210E\", .width = 1 },\n    .{ .codepoint = \"U+210F\", .width = 1 },\n    .{ .codepoint = \"U+2110\", .width = 1 },\n    .{ .codepoint = \"U+2111\", .width = 1 },\n    .{ .codepoint = \"U+2112\", .width = 1 },\n    .{ .codepoint = \"U+2113\", .width = 1 },\n    .{ .codepoint = \"U+2114\", .width = 1 },\n    .{ .codepoint = \"U+2115\", .width = 1 },\n    .{ .codepoint = \"U+2116\", .width = 1 },\n    .{ .codepoint = \"U+2117\", .width = 1 },\n    .{ .codepoint = \"U+2118\", .width = 1 },\n    .{ .codepoint = \"U+2119\", .width = 1 },\n    .{ .codepoint = \"U+211A\", .width = 1 },\n    .{ .codepoint = \"U+211B\", .width = 1 },\n    .{ .codepoint = \"U+211C\", .width = 1 },\n    .{ .codepoint = \"U+211D\", .width = 1 },\n    .{ .codepoint = \"U+211E\", .width = 1 },\n    .{ .codepoint = \"U+211F\", .width = 1 },\n    .{ .codepoint = \"U+2120\", .width = 1 },\n    .{ .codepoint = \"U+2121\", .width = 1 },\n    .{ .codepoint = \"U+2122\", .width = 1 },\n    .{ .codepoint = \"U+2123\", .width = 1 },\n    .{ .codepoint = \"U+2124\", .width = 1 },\n    .{ .codepoint = \"U+2125\", .width = 1 },\n    .{ .codepoint = \"U+2126\", .width = 1 },\n    .{ .codepoint = \"U+2127\", .width = 1 },\n    .{ .codepoint = \"U+2128\", .width = 1 },\n    .{ .codepoint = \"U+2129\", .width = 1 },\n    .{ .codepoint = \"U+212A\", .width = 1 },\n    .{ .codepoint = \"U+212B\", .width = 1 },\n    .{ .codepoint = \"U+212C\", .width = 1 },\n    .{ .codepoint = \"U+212D\", .width = 1 },\n    .{ .codepoint = \"U+212E\", .width = 1 },\n    .{ .codepoint = \"U+212F\", .width = 1 },\n    .{ .codepoint = \"U+2130\", .width = 1 },\n    .{ .codepoint = \"U+2131\", .width = 1 },\n    .{ .codepoint = \"U+2132\", .width = 1 },\n    .{ .codepoint = \"U+2133\", .width = 1 },\n    .{ .codepoint = \"U+2134\", .width = 1 },\n    .{ .codepoint = \"U+2135\", .width = 1 },\n    .{ .codepoint = \"U+2136\", .width = 1 },\n    .{ .codepoint = \"U+2137\", .width = 1 },\n    .{ .codepoint = \"U+2138\", .width = 1 },\n    .{ .codepoint = \"U+2139\", .width = 1 },\n    .{ .codepoint = \"U+213A\", .width = 1 },\n    .{ .codepoint = \"U+213B\", .width = 1 },\n    .{ .codepoint = \"U+213C\", .width = 1 },\n    .{ .codepoint = \"U+213D\", .width = 1 },\n    .{ .codepoint = \"U+213E\", .width = 1 },\n    .{ .codepoint = \"U+213F\", .width = 1 },\n    .{ .codepoint = \"U+2140\", .width = 1 },\n    .{ .codepoint = \"U+2141\", .width = 1 },\n    .{ .codepoint = \"U+2142\", .width = 1 },\n    .{ .codepoint = \"U+2143\", .width = 1 },\n    .{ .codepoint = \"U+2144\", .width = 1 },\n    .{ .codepoint = \"U+2145\", .width = 1 },\n    .{ .codepoint = \"U+2146\", .width = 1 },\n    .{ .codepoint = \"U+2147\", .width = 1 },\n    .{ .codepoint = \"U+2148\", .width = 1 },\n    .{ .codepoint = \"U+2149\", .width = 1 },\n    .{ .codepoint = \"U+214A\", .width = 1 },\n    .{ .codepoint = \"U+214B\", .width = 1 },\n    .{ .codepoint = \"U+214C\", .width = 1 },\n    .{ .codepoint = \"U+214D\", .width = 1 },\n    .{ .codepoint = \"U+214E\", .width = 1 },\n    .{ .codepoint = \"U+214F\", .width = 1 },\n    .{ .codepoint = \"U+2190\", .width = 1 },\n    .{ .codepoint = \"U+2191\", .width = 1 },\n    .{ .codepoint = \"U+2192\", .width = 1 },\n    .{ .codepoint = \"U+2193\", .width = 1 },\n    .{ .codepoint = \"U+2194\", .width = 1 },\n    .{ .codepoint = \"U+2195\", .width = 1 },\n    .{ .codepoint = \"U+2196\", .width = 1 },\n    .{ .codepoint = \"U+2197\", .width = 1 },\n    .{ .codepoint = \"U+2198\", .width = 1 },\n    .{ .codepoint = \"U+2199\", .width = 1 },\n    .{ .codepoint = \"U+219A\", .width = 1 },\n    .{ .codepoint = \"U+219B\", .width = 1 },\n    .{ .codepoint = \"U+219C\", .width = 1 },\n    .{ .codepoint = \"U+219D\", .width = 1 },\n    .{ .codepoint = \"U+219E\", .width = 1 },\n    .{ .codepoint = \"U+219F\", .width = 1 },\n    .{ .codepoint = \"U+21A0\", .width = 1 },\n    .{ .codepoint = \"U+21A1\", .width = 1 },\n    .{ .codepoint = \"U+21A2\", .width = 1 },\n    .{ .codepoint = \"U+21A3\", .width = 1 },\n    .{ .codepoint = \"U+21A4\", .width = 1 },\n    .{ .codepoint = \"U+21A5\", .width = 1 },\n    .{ .codepoint = \"U+21A6\", .width = 1 },\n    .{ .codepoint = \"U+21A7\", .width = 1 },\n    .{ .codepoint = \"U+21A8\", .width = 1 },\n    .{ .codepoint = \"U+21A9\", .width = 1 },\n    .{ .codepoint = \"U+21AA\", .width = 1 },\n    .{ .codepoint = \"U+21AB\", .width = 1 },\n    .{ .codepoint = \"U+21AC\", .width = 1 },\n    .{ .codepoint = \"U+21AD\", .width = 1 },\n    .{ .codepoint = \"U+21AE\", .width = 1 },\n    .{ .codepoint = \"U+21AF\", .width = 1 },\n    .{ .codepoint = \"U+21B0\", .width = 1 },\n    .{ .codepoint = \"U+21B1\", .width = 1 },\n    .{ .codepoint = \"U+21B2\", .width = 1 },\n    .{ .codepoint = \"U+21B3\", .width = 1 },\n    .{ .codepoint = \"U+21B4\", .width = 1 },\n    .{ .codepoint = \"U+21B5\", .width = 1 },\n    .{ .codepoint = \"U+21B6\", .width = 1 },\n    .{ .codepoint = \"U+21B7\", .width = 1 },\n    .{ .codepoint = \"U+21B8\", .width = 1 },\n    .{ .codepoint = \"U+21B9\", .width = 1 },\n    .{ .codepoint = \"U+21BA\", .width = 1 },\n    .{ .codepoint = \"U+21BB\", .width = 1 },\n    .{ .codepoint = \"U+21BC\", .width = 1 },\n    .{ .codepoint = \"U+21BD\", .width = 1 },\n    .{ .codepoint = \"U+21BE\", .width = 1 },\n    .{ .codepoint = \"U+21BF\", .width = 1 },\n    .{ .codepoint = \"U+21C0\", .width = 1 },\n    .{ .codepoint = \"U+21C1\", .width = 1 },\n    .{ .codepoint = \"U+21C2\", .width = 1 },\n    .{ .codepoint = \"U+21C3\", .width = 1 },\n    .{ .codepoint = \"U+21C4\", .width = 1 },\n    .{ .codepoint = \"U+21C5\", .width = 1 },\n    .{ .codepoint = \"U+21C6\", .width = 1 },\n    .{ .codepoint = \"U+21C7\", .width = 1 },\n    .{ .codepoint = \"U+21C8\", .width = 1 },\n    .{ .codepoint = \"U+21C9\", .width = 1 },\n    .{ .codepoint = \"U+21CA\", .width = 1 },\n    .{ .codepoint = \"U+21CB\", .width = 1 },\n    .{ .codepoint = \"U+21CC\", .width = 1 },\n    .{ .codepoint = \"U+21CD\", .width = 1 },\n    .{ .codepoint = \"U+21CE\", .width = 1 },\n    .{ .codepoint = \"U+21CF\", .width = 1 },\n    .{ .codepoint = \"U+21D0\", .width = 1 },\n    .{ .codepoint = \"U+21D1\", .width = 1 },\n    .{ .codepoint = \"U+21D2\", .width = 1 },\n    .{ .codepoint = \"U+21D3\", .width = 1 },\n    .{ .codepoint = \"U+21D4\", .width = 1 },\n    .{ .codepoint = \"U+21D5\", .width = 1 },\n    .{ .codepoint = \"U+21D6\", .width = 1 },\n    .{ .codepoint = \"U+21D7\", .width = 1 },\n    .{ .codepoint = \"U+21D8\", .width = 1 },\n    .{ .codepoint = \"U+21D9\", .width = 1 },\n    .{ .codepoint = \"U+21DA\", .width = 1 },\n    .{ .codepoint = \"U+21DB\", .width = 1 },\n    .{ .codepoint = \"U+21DC\", .width = 1 },\n    .{ .codepoint = \"U+21DD\", .width = 1 },\n    .{ .codepoint = \"U+21DE\", .width = 1 },\n    .{ .codepoint = \"U+21DF\", .width = 1 },\n    .{ .codepoint = \"U+21E0\", .width = 1 },\n    .{ .codepoint = \"U+21E1\", .width = 1 },\n    .{ .codepoint = \"U+21E2\", .width = 1 },\n    .{ .codepoint = \"U+21E3\", .width = 1 },\n    .{ .codepoint = \"U+21E4\", .width = 1 },\n    .{ .codepoint = \"U+21E5\", .width = 1 },\n    .{ .codepoint = \"U+21E6\", .width = 1 },\n    .{ .codepoint = \"U+21E7\", .width = 1 },\n    .{ .codepoint = \"U+21E8\", .width = 1 },\n    .{ .codepoint = \"U+21E9\", .width = 1 },\n    .{ .codepoint = \"U+21EA\", .width = 1 },\n    .{ .codepoint = \"U+21EB\", .width = 1 },\n    .{ .codepoint = \"U+21EC\", .width = 1 },\n    .{ .codepoint = \"U+21ED\", .width = 1 },\n    .{ .codepoint = \"U+21EE\", .width = 1 },\n    .{ .codepoint = \"U+21EF\", .width = 1 },\n    .{ .codepoint = \"U+21F0\", .width = 1 },\n    .{ .codepoint = \"U+21F1\", .width = 1 },\n    .{ .codepoint = \"U+21F2\", .width = 1 },\n    .{ .codepoint = \"U+21F3\", .width = 1 },\n    .{ .codepoint = \"U+21F4\", .width = 1 },\n    .{ .codepoint = \"U+21F5\", .width = 1 },\n    .{ .codepoint = \"U+21F6\", .width = 1 },\n    .{ .codepoint = \"U+21F7\", .width = 1 },\n    .{ .codepoint = \"U+21F8\", .width = 1 },\n    .{ .codepoint = \"U+21F9\", .width = 1 },\n    .{ .codepoint = \"U+21FA\", .width = 1 },\n    .{ .codepoint = \"U+21FB\", .width = 1 },\n    .{ .codepoint = \"U+21FC\", .width = 1 },\n    .{ .codepoint = \"U+21FD\", .width = 1 },\n    .{ .codepoint = \"U+21FE\", .width = 1 },\n    .{ .codepoint = \"U+21FF\", .width = 1 },\n    .{ .codepoint = \"U+2200\", .width = 1 },\n    .{ .codepoint = \"U+2201\", .width = 1 },\n    .{ .codepoint = \"U+2202\", .width = 1 },\n    .{ .codepoint = \"U+2203\", .width = 1 },\n    .{ .codepoint = \"U+2204\", .width = 1 },\n    .{ .codepoint = \"U+2205\", .width = 1 },\n    .{ .codepoint = \"U+2206\", .width = 1 },\n    .{ .codepoint = \"U+2207\", .width = 1 },\n    .{ .codepoint = \"U+2208\", .width = 1 },\n    .{ .codepoint = \"U+2209\", .width = 1 },\n    .{ .codepoint = \"U+220A\", .width = 1 },\n    .{ .codepoint = \"U+220B\", .width = 1 },\n    .{ .codepoint = \"U+220C\", .width = 1 },\n    .{ .codepoint = \"U+220D\", .width = 1 },\n    .{ .codepoint = \"U+220E\", .width = 1 },\n    .{ .codepoint = \"U+220F\", .width = 1 },\n    .{ .codepoint = \"U+2210\", .width = 1 },\n    .{ .codepoint = \"U+2211\", .width = 1 },\n    .{ .codepoint = \"U+2212\", .width = 1 },\n    .{ .codepoint = \"U+2213\", .width = 1 },\n    .{ .codepoint = \"U+2214\", .width = 1 },\n    .{ .codepoint = \"U+2215\", .width = 1 },\n    .{ .codepoint = \"U+2216\", .width = 1 },\n    .{ .codepoint = \"U+2217\", .width = 1 },\n    .{ .codepoint = \"U+2218\", .width = 1 },\n    .{ .codepoint = \"U+2219\", .width = 1 },\n    .{ .codepoint = \"U+221A\", .width = 1 },\n    .{ .codepoint = \"U+221B\", .width = 1 },\n    .{ .codepoint = \"U+221C\", .width = 1 },\n    .{ .codepoint = \"U+221D\", .width = 1 },\n    .{ .codepoint = \"U+221E\", .width = 1 },\n    .{ .codepoint = \"U+221F\", .width = 1 },\n    .{ .codepoint = \"U+2220\", .width = 1 },\n    .{ .codepoint = \"U+2221\", .width = 1 },\n    .{ .codepoint = \"U+2222\", .width = 1 },\n    .{ .codepoint = \"U+2223\", .width = 1 },\n    .{ .codepoint = \"U+2224\", .width = 1 },\n    .{ .codepoint = \"U+2225\", .width = 1 },\n    .{ .codepoint = \"U+2226\", .width = 1 },\n    .{ .codepoint = \"U+2227\", .width = 1 },\n    .{ .codepoint = \"U+2228\", .width = 1 },\n    .{ .codepoint = \"U+2229\", .width = 1 },\n    .{ .codepoint = \"U+222A\", .width = 1 },\n    .{ .codepoint = \"U+222B\", .width = 1 },\n    .{ .codepoint = \"U+222C\", .width = 1 },\n    .{ .codepoint = \"U+222D\", .width = 1 },\n    .{ .codepoint = \"U+222E\", .width = 1 },\n    .{ .codepoint = \"U+222F\", .width = 1 },\n    .{ .codepoint = \"U+2230\", .width = 1 },\n    .{ .codepoint = \"U+2231\", .width = 1 },\n    .{ .codepoint = \"U+2232\", .width = 1 },\n    .{ .codepoint = \"U+2233\", .width = 1 },\n    .{ .codepoint = \"U+2234\", .width = 1 },\n    .{ .codepoint = \"U+2235\", .width = 1 },\n    .{ .codepoint = \"U+2236\", .width = 1 },\n    .{ .codepoint = \"U+2237\", .width = 1 },\n    .{ .codepoint = \"U+2238\", .width = 1 },\n    .{ .codepoint = \"U+2239\", .width = 1 },\n    .{ .codepoint = \"U+223A\", .width = 1 },\n    .{ .codepoint = \"U+223B\", .width = 1 },\n    .{ .codepoint = \"U+223C\", .width = 1 },\n    .{ .codepoint = \"U+223D\", .width = 1 },\n    .{ .codepoint = \"U+223E\", .width = 1 },\n    .{ .codepoint = \"U+223F\", .width = 1 },\n    .{ .codepoint = \"U+2240\", .width = 1 },\n    .{ .codepoint = \"U+2241\", .width = 1 },\n    .{ .codepoint = \"U+2242\", .width = 1 },\n    .{ .codepoint = \"U+2243\", .width = 1 },\n    .{ .codepoint = \"U+2244\", .width = 1 },\n    .{ .codepoint = \"U+2245\", .width = 1 },\n    .{ .codepoint = \"U+2246\", .width = 1 },\n    .{ .codepoint = \"U+2247\", .width = 1 },\n    .{ .codepoint = \"U+2248\", .width = 1 },\n    .{ .codepoint = \"U+2249\", .width = 1 },\n    .{ .codepoint = \"U+224A\", .width = 1 },\n    .{ .codepoint = \"U+224B\", .width = 1 },\n    .{ .codepoint = \"U+224C\", .width = 1 },\n    .{ .codepoint = \"U+224D\", .width = 1 },\n    .{ .codepoint = \"U+224E\", .width = 1 },\n    .{ .codepoint = \"U+224F\", .width = 1 },\n    .{ .codepoint = \"U+2250\", .width = 1 },\n    .{ .codepoint = \"U+2251\", .width = 1 },\n    .{ .codepoint = \"U+2252\", .width = 1 },\n    .{ .codepoint = \"U+2253\", .width = 1 },\n    .{ .codepoint = \"U+2254\", .width = 1 },\n    .{ .codepoint = \"U+2255\", .width = 1 },\n    .{ .codepoint = \"U+2256\", .width = 1 },\n    .{ .codepoint = \"U+2257\", .width = 1 },\n    .{ .codepoint = \"U+2258\", .width = 1 },\n    .{ .codepoint = \"U+2259\", .width = 1 },\n    .{ .codepoint = \"U+225A\", .width = 1 },\n    .{ .codepoint = \"U+225B\", .width = 1 },\n    .{ .codepoint = \"U+225C\", .width = 1 },\n    .{ .codepoint = \"U+225D\", .width = 1 },\n    .{ .codepoint = \"U+225E\", .width = 1 },\n    .{ .codepoint = \"U+225F\", .width = 1 },\n    .{ .codepoint = \"U+2260\", .width = 1 },\n    .{ .codepoint = \"U+2261\", .width = 1 },\n    .{ .codepoint = \"U+2262\", .width = 1 },\n    .{ .codepoint = \"U+2263\", .width = 1 },\n    .{ .codepoint = \"U+2264\", .width = 1 },\n    .{ .codepoint = \"U+2265\", .width = 1 },\n    .{ .codepoint = \"U+2266\", .width = 1 },\n    .{ .codepoint = \"U+2267\", .width = 1 },\n    .{ .codepoint = \"U+2268\", .width = 1 },\n    .{ .codepoint = \"U+2269\", .width = 1 },\n    .{ .codepoint = \"U+226A\", .width = 1 },\n    .{ .codepoint = \"U+226B\", .width = 1 },\n    .{ .codepoint = \"U+226C\", .width = 1 },\n    .{ .codepoint = \"U+226D\", .width = 1 },\n    .{ .codepoint = \"U+226E\", .width = 1 },\n    .{ .codepoint = \"U+226F\", .width = 1 },\n    .{ .codepoint = \"U+2270\", .width = 1 },\n    .{ .codepoint = \"U+2271\", .width = 1 },\n    .{ .codepoint = \"U+2272\", .width = 1 },\n    .{ .codepoint = \"U+2273\", .width = 1 },\n    .{ .codepoint = \"U+2274\", .width = 1 },\n    .{ .codepoint = \"U+2275\", .width = 1 },\n    .{ .codepoint = \"U+2276\", .width = 1 },\n    .{ .codepoint = \"U+2277\", .width = 1 },\n    .{ .codepoint = \"U+2278\", .width = 1 },\n    .{ .codepoint = \"U+2279\", .width = 1 },\n    .{ .codepoint = \"U+227A\", .width = 1 },\n    .{ .codepoint = \"U+227B\", .width = 1 },\n    .{ .codepoint = \"U+227C\", .width = 1 },\n    .{ .codepoint = \"U+227D\", .width = 1 },\n    .{ .codepoint = \"U+227E\", .width = 1 },\n    .{ .codepoint = \"U+227F\", .width = 1 },\n    .{ .codepoint = \"U+2280\", .width = 1 },\n    .{ .codepoint = \"U+2281\", .width = 1 },\n    .{ .codepoint = \"U+2282\", .width = 1 },\n    .{ .codepoint = \"U+2283\", .width = 1 },\n    .{ .codepoint = \"U+2284\", .width = 1 },\n    .{ .codepoint = \"U+2285\", .width = 1 },\n    .{ .codepoint = \"U+2286\", .width = 1 },\n    .{ .codepoint = \"U+2287\", .width = 1 },\n    .{ .codepoint = \"U+2288\", .width = 1 },\n    .{ .codepoint = \"U+2289\", .width = 1 },\n    .{ .codepoint = \"U+228A\", .width = 1 },\n    .{ .codepoint = \"U+228B\", .width = 1 },\n    .{ .codepoint = \"U+228C\", .width = 1 },\n    .{ .codepoint = \"U+228D\", .width = 1 },\n    .{ .codepoint = \"U+228E\", .width = 1 },\n    .{ .codepoint = \"U+228F\", .width = 1 },\n    .{ .codepoint = \"U+2290\", .width = 1 },\n    .{ .codepoint = \"U+2291\", .width = 1 },\n    .{ .codepoint = \"U+2292\", .width = 1 },\n    .{ .codepoint = \"U+2293\", .width = 1 },\n    .{ .codepoint = \"U+2294\", .width = 1 },\n    .{ .codepoint = \"U+2295\", .width = 1 },\n    .{ .codepoint = \"U+2296\", .width = 1 },\n    .{ .codepoint = \"U+2297\", .width = 1 },\n    .{ .codepoint = \"U+2298\", .width = 1 },\n    .{ .codepoint = \"U+2299\", .width = 1 },\n    .{ .codepoint = \"U+229A\", .width = 1 },\n    .{ .codepoint = \"U+229B\", .width = 1 },\n    .{ .codepoint = \"U+229C\", .width = 1 },\n    .{ .codepoint = \"U+229D\", .width = 1 },\n    .{ .codepoint = \"U+229E\", .width = 1 },\n    .{ .codepoint = \"U+229F\", .width = 1 },\n    .{ .codepoint = \"U+22A0\", .width = 1 },\n    .{ .codepoint = \"U+22A1\", .width = 1 },\n    .{ .codepoint = \"U+22A2\", .width = 1 },\n    .{ .codepoint = \"U+22A3\", .width = 1 },\n    .{ .codepoint = \"U+22A4\", .width = 1 },\n    .{ .codepoint = \"U+22A5\", .width = 1 },\n    .{ .codepoint = \"U+22A6\", .width = 1 },\n    .{ .codepoint = \"U+22A7\", .width = 1 },\n    .{ .codepoint = \"U+22A8\", .width = 1 },\n    .{ .codepoint = \"U+22A9\", .width = 1 },\n    .{ .codepoint = \"U+22AA\", .width = 1 },\n    .{ .codepoint = \"U+22AB\", .width = 1 },\n    .{ .codepoint = \"U+22AC\", .width = 1 },\n    .{ .codepoint = \"U+22AD\", .width = 1 },\n    .{ .codepoint = \"U+22AE\", .width = 1 },\n    .{ .codepoint = \"U+22AF\", .width = 1 },\n    .{ .codepoint = \"U+22B0\", .width = 1 },\n    .{ .codepoint = \"U+22B1\", .width = 1 },\n    .{ .codepoint = \"U+22B2\", .width = 1 },\n    .{ .codepoint = \"U+22B3\", .width = 1 },\n    .{ .codepoint = \"U+22B4\", .width = 1 },\n    .{ .codepoint = \"U+22B5\", .width = 1 },\n    .{ .codepoint = \"U+22B6\", .width = 1 },\n    .{ .codepoint = \"U+22B7\", .width = 1 },\n    .{ .codepoint = \"U+22B8\", .width = 1 },\n    .{ .codepoint = \"U+22B9\", .width = 1 },\n    .{ .codepoint = \"U+22BA\", .width = 1 },\n    .{ .codepoint = \"U+22BB\", .width = 1 },\n    .{ .codepoint = \"U+22BC\", .width = 1 },\n    .{ .codepoint = \"U+22BD\", .width = 1 },\n    .{ .codepoint = \"U+22BE\", .width = 1 },\n    .{ .codepoint = \"U+22BF\", .width = 1 },\n    .{ .codepoint = \"U+22C0\", .width = 1 },\n    .{ .codepoint = \"U+22C1\", .width = 1 },\n    .{ .codepoint = \"U+22C2\", .width = 1 },\n    .{ .codepoint = \"U+22C3\", .width = 1 },\n    .{ .codepoint = \"U+22C4\", .width = 1 },\n    .{ .codepoint = \"U+22C5\", .width = 1 },\n    .{ .codepoint = \"U+22C6\", .width = 1 },\n    .{ .codepoint = \"U+22C7\", .width = 1 },\n    .{ .codepoint = \"U+22C8\", .width = 1 },\n    .{ .codepoint = \"U+22C9\", .width = 1 },\n    .{ .codepoint = \"U+22CA\", .width = 1 },\n    .{ .codepoint = \"U+22CB\", .width = 1 },\n    .{ .codepoint = \"U+22CC\", .width = 1 },\n    .{ .codepoint = \"U+22CD\", .width = 1 },\n    .{ .codepoint = \"U+22CE\", .width = 1 },\n    .{ .codepoint = \"U+22CF\", .width = 1 },\n    .{ .codepoint = \"U+22D0\", .width = 1 },\n    .{ .codepoint = \"U+22D1\", .width = 1 },\n    .{ .codepoint = \"U+22D2\", .width = 1 },\n    .{ .codepoint = \"U+22D3\", .width = 1 },\n    .{ .codepoint = \"U+22D4\", .width = 1 },\n    .{ .codepoint = \"U+22D5\", .width = 1 },\n    .{ .codepoint = \"U+22D6\", .width = 1 },\n    .{ .codepoint = \"U+22D7\", .width = 1 },\n    .{ .codepoint = \"U+22D8\", .width = 1 },\n    .{ .codepoint = \"U+22D9\", .width = 1 },\n    .{ .codepoint = \"U+22DA\", .width = 1 },\n    .{ .codepoint = \"U+22DB\", .width = 1 },\n    .{ .codepoint = \"U+22DC\", .width = 1 },\n    .{ .codepoint = \"U+22DD\", .width = 1 },\n    .{ .codepoint = \"U+22DE\", .width = 1 },\n    .{ .codepoint = \"U+22DF\", .width = 1 },\n    .{ .codepoint = \"U+22E0\", .width = 1 },\n    .{ .codepoint = \"U+22E1\", .width = 1 },\n    .{ .codepoint = \"U+22E2\", .width = 1 },\n    .{ .codepoint = \"U+22E3\", .width = 1 },\n    .{ .codepoint = \"U+22E4\", .width = 1 },\n    .{ .codepoint = \"U+22E5\", .width = 1 },\n    .{ .codepoint = \"U+22E6\", .width = 1 },\n    .{ .codepoint = \"U+22E7\", .width = 1 },\n    .{ .codepoint = \"U+22E8\", .width = 1 },\n    .{ .codepoint = \"U+22E9\", .width = 1 },\n    .{ .codepoint = \"U+22EA\", .width = 1 },\n    .{ .codepoint = \"U+22EB\", .width = 1 },\n    .{ .codepoint = \"U+22EC\", .width = 1 },\n    .{ .codepoint = \"U+22ED\", .width = 1 },\n    .{ .codepoint = \"U+22EE\", .width = 1 },\n    .{ .codepoint = \"U+22EF\", .width = 1 },\n    .{ .codepoint = \"U+22F0\", .width = 1 },\n    .{ .codepoint = \"U+22F1\", .width = 1 },\n    .{ .codepoint = \"U+22F2\", .width = 1 },\n    .{ .codepoint = \"U+22F3\", .width = 1 },\n    .{ .codepoint = \"U+22F4\", .width = 1 },\n    .{ .codepoint = \"U+22F5\", .width = 1 },\n    .{ .codepoint = \"U+22F6\", .width = 1 },\n    .{ .codepoint = \"U+22F7\", .width = 1 },\n    .{ .codepoint = \"U+22F8\", .width = 1 },\n    .{ .codepoint = \"U+22F9\", .width = 1 },\n    .{ .codepoint = \"U+22FA\", .width = 1 },\n    .{ .codepoint = \"U+22FB\", .width = 1 },\n    .{ .codepoint = \"U+22FC\", .width = 1 },\n    .{ .codepoint = \"U+22FD\", .width = 1 },\n    .{ .codepoint = \"U+22FE\", .width = 1 },\n    .{ .codepoint = \"U+22FF\", .width = 1 },\n    .{ .codepoint = \"U+2300\", .width = 1 },\n    .{ .codepoint = \"U+2301\", .width = 1 },\n    .{ .codepoint = \"U+2302\", .width = 1 },\n    .{ .codepoint = \"U+2303\", .width = 1 },\n    .{ .codepoint = \"U+2304\", .width = 1 },\n    .{ .codepoint = \"U+2305\", .width = 1 },\n    .{ .codepoint = \"U+2306\", .width = 1 },\n    .{ .codepoint = \"U+2307\", .width = 1 },\n    .{ .codepoint = \"U+2308\", .width = 1 },\n    .{ .codepoint = \"U+2309\", .width = 1 },\n    .{ .codepoint = \"U+230A\", .width = 1 },\n    .{ .codepoint = \"U+230B\", .width = 1 },\n    .{ .codepoint = \"U+230C\", .width = 1 },\n    .{ .codepoint = \"U+230D\", .width = 1 },\n    .{ .codepoint = \"U+230E\", .width = 1 },\n    .{ .codepoint = \"U+230F\", .width = 1 },\n    .{ .codepoint = \"U+2310\", .width = 1 },\n    .{ .codepoint = \"U+2311\", .width = 1 },\n    .{ .codepoint = \"U+2312\", .width = 1 },\n    .{ .codepoint = \"U+2313\", .width = 1 },\n    .{ .codepoint = \"U+2314\", .width = 1 },\n    .{ .codepoint = \"U+2315\", .width = 1 },\n    .{ .codepoint = \"U+2316\", .width = 1 },\n    .{ .codepoint = \"U+2317\", .width = 1 },\n    .{ .codepoint = \"U+2318\", .width = 1 },\n    .{ .codepoint = \"U+2319\", .width = 1 },\n    .{ .codepoint = \"U+231A\", .width = 2 },\n    .{ .codepoint = \"U+231B\", .width = 2 },\n    .{ .codepoint = \"U+231C\", .width = 1 },\n    .{ .codepoint = \"U+231D\", .width = 1 },\n    .{ .codepoint = \"U+231E\", .width = 1 },\n    .{ .codepoint = \"U+231F\", .width = 1 },\n    .{ .codepoint = \"U+2320\", .width = 1 },\n    .{ .codepoint = \"U+2321\", .width = 1 },\n    .{ .codepoint = \"U+2322\", .width = 1 },\n    .{ .codepoint = \"U+2323\", .width = 1 },\n    .{ .codepoint = \"U+2324\", .width = 1 },\n    .{ .codepoint = \"U+2325\", .width = 1 },\n    .{ .codepoint = \"U+2326\", .width = 1 },\n    .{ .codepoint = \"U+2327\", .width = 1 },\n    .{ .codepoint = \"U+2328\", .width = 1 },\n    .{ .codepoint = \"U+2329\", .width = 2 },\n    .{ .codepoint = \"U+232A\", .width = 2 },\n    .{ .codepoint = \"U+232B\", .width = 1 },\n    .{ .codepoint = \"U+232C\", .width = 1 },\n    .{ .codepoint = \"U+232D\", .width = 1 },\n    .{ .codepoint = \"U+232E\", .width = 1 },\n    .{ .codepoint = \"U+232F\", .width = 1 },\n    .{ .codepoint = \"U+2330\", .width = 1 },\n    .{ .codepoint = \"U+2331\", .width = 1 },\n    .{ .codepoint = \"U+2332\", .width = 1 },\n    .{ .codepoint = \"U+2333\", .width = 1 },\n    .{ .codepoint = \"U+2334\", .width = 1 },\n    .{ .codepoint = \"U+2335\", .width = 1 },\n    .{ .codepoint = \"U+2336\", .width = 1 },\n    .{ .codepoint = \"U+2337\", .width = 1 },\n    .{ .codepoint = \"U+2338\", .width = 1 },\n    .{ .codepoint = \"U+2339\", .width = 1 },\n    .{ .codepoint = \"U+233A\", .width = 1 },\n    .{ .codepoint = \"U+233B\", .width = 1 },\n    .{ .codepoint = \"U+233C\", .width = 1 },\n    .{ .codepoint = \"U+233D\", .width = 1 },\n    .{ .codepoint = \"U+233E\", .width = 1 },\n    .{ .codepoint = \"U+233F\", .width = 1 },\n    .{ .codepoint = \"U+2340\", .width = 1 },\n    .{ .codepoint = \"U+2341\", .width = 1 },\n    .{ .codepoint = \"U+2342\", .width = 1 },\n    .{ .codepoint = \"U+2343\", .width = 1 },\n    .{ .codepoint = \"U+2344\", .width = 1 },\n    .{ .codepoint = \"U+2345\", .width = 1 },\n    .{ .codepoint = \"U+2346\", .width = 1 },\n    .{ .codepoint = \"U+2347\", .width = 1 },\n    .{ .codepoint = \"U+2348\", .width = 1 },\n    .{ .codepoint = \"U+2349\", .width = 1 },\n    .{ .codepoint = \"U+234A\", .width = 1 },\n    .{ .codepoint = \"U+234B\", .width = 1 },\n    .{ .codepoint = \"U+234C\", .width = 1 },\n    .{ .codepoint = \"U+234D\", .width = 1 },\n    .{ .codepoint = \"U+234E\", .width = 1 },\n    .{ .codepoint = \"U+234F\", .width = 1 },\n    .{ .codepoint = \"U+2350\", .width = 1 },\n    .{ .codepoint = \"U+2351\", .width = 1 },\n    .{ .codepoint = \"U+2352\", .width = 1 },\n    .{ .codepoint = \"U+2353\", .width = 1 },\n    .{ .codepoint = \"U+2354\", .width = 1 },\n    .{ .codepoint = \"U+2355\", .width = 1 },\n    .{ .codepoint = \"U+2356\", .width = 1 },\n    .{ .codepoint = \"U+2357\", .width = 1 },\n    .{ .codepoint = \"U+2358\", .width = 1 },\n    .{ .codepoint = \"U+2359\", .width = 1 },\n    .{ .codepoint = \"U+235A\", .width = 1 },\n    .{ .codepoint = \"U+235B\", .width = 1 },\n    .{ .codepoint = \"U+235C\", .width = 1 },\n    .{ .codepoint = \"U+235D\", .width = 1 },\n    .{ .codepoint = \"U+235E\", .width = 1 },\n    .{ .codepoint = \"U+235F\", .width = 1 },\n    .{ .codepoint = \"U+2360\", .width = 1 },\n    .{ .codepoint = \"U+2361\", .width = 1 },\n    .{ .codepoint = \"U+2362\", .width = 1 },\n    .{ .codepoint = \"U+2363\", .width = 1 },\n    .{ .codepoint = \"U+2364\", .width = 1 },\n    .{ .codepoint = \"U+2365\", .width = 1 },\n    .{ .codepoint = \"U+2366\", .width = 1 },\n    .{ .codepoint = \"U+2367\", .width = 1 },\n    .{ .codepoint = \"U+2368\", .width = 1 },\n    .{ .codepoint = \"U+2369\", .width = 1 },\n    .{ .codepoint = \"U+236A\", .width = 1 },\n    .{ .codepoint = \"U+236B\", .width = 1 },\n    .{ .codepoint = \"U+236C\", .width = 1 },\n    .{ .codepoint = \"U+236D\", .width = 1 },\n    .{ .codepoint = \"U+236E\", .width = 1 },\n    .{ .codepoint = \"U+236F\", .width = 1 },\n    .{ .codepoint = \"U+2370\", .width = 1 },\n    .{ .codepoint = \"U+2371\", .width = 1 },\n    .{ .codepoint = \"U+2372\", .width = 1 },\n    .{ .codepoint = \"U+2373\", .width = 1 },\n    .{ .codepoint = \"U+2374\", .width = 1 },\n    .{ .codepoint = \"U+2375\", .width = 1 },\n    .{ .codepoint = \"U+2376\", .width = 1 },\n    .{ .codepoint = \"U+2377\", .width = 1 },\n    .{ .codepoint = \"U+2378\", .width = 1 },\n    .{ .codepoint = \"U+2379\", .width = 1 },\n    .{ .codepoint = \"U+237A\", .width = 1 },\n    .{ .codepoint = \"U+237B\", .width = 1 },\n    .{ .codepoint = \"U+237C\", .width = 1 },\n    .{ .codepoint = \"U+237D\", .width = 1 },\n    .{ .codepoint = \"U+237E\", .width = 1 },\n    .{ .codepoint = \"U+237F\", .width = 1 },\n    .{ .codepoint = \"U+2380\", .width = 1 },\n    .{ .codepoint = \"U+2381\", .width = 1 },\n    .{ .codepoint = \"U+2382\", .width = 1 },\n    .{ .codepoint = \"U+2383\", .width = 1 },\n    .{ .codepoint = \"U+2384\", .width = 1 },\n    .{ .codepoint = \"U+2385\", .width = 1 },\n    .{ .codepoint = \"U+2386\", .width = 1 },\n    .{ .codepoint = \"U+2387\", .width = 1 },\n    .{ .codepoint = \"U+2388\", .width = 1 },\n    .{ .codepoint = \"U+2389\", .width = 1 },\n    .{ .codepoint = \"U+238A\", .width = 1 },\n    .{ .codepoint = \"U+238B\", .width = 1 },\n    .{ .codepoint = \"U+238C\", .width = 1 },\n    .{ .codepoint = \"U+238D\", .width = 1 },\n    .{ .codepoint = \"U+238E\", .width = 1 },\n    .{ .codepoint = \"U+238F\", .width = 1 },\n    .{ .codepoint = \"U+2390\", .width = 1 },\n    .{ .codepoint = \"U+2391\", .width = 1 },\n    .{ .codepoint = \"U+2392\", .width = 1 },\n    .{ .codepoint = \"U+2393\", .width = 1 },\n    .{ .codepoint = \"U+2394\", .width = 1 },\n    .{ .codepoint = \"U+2395\", .width = 1 },\n    .{ .codepoint = \"U+2396\", .width = 1 },\n    .{ .codepoint = \"U+2397\", .width = 1 },\n    .{ .codepoint = \"U+2398\", .width = 1 },\n    .{ .codepoint = \"U+2399\", .width = 1 },\n    .{ .codepoint = \"U+239A\", .width = 1 },\n    .{ .codepoint = \"U+239B\", .width = 1 },\n    .{ .codepoint = \"U+239C\", .width = 1 },\n    .{ .codepoint = \"U+239D\", .width = 1 },\n    .{ .codepoint = \"U+239E\", .width = 1 },\n    .{ .codepoint = \"U+239F\", .width = 1 },\n    .{ .codepoint = \"U+23A0\", .width = 1 },\n    .{ .codepoint = \"U+23A1\", .width = 1 },\n    .{ .codepoint = \"U+23A2\", .width = 1 },\n    .{ .codepoint = \"U+23A3\", .width = 1 },\n    .{ .codepoint = \"U+23A4\", .width = 1 },\n    .{ .codepoint = \"U+23A5\", .width = 1 },\n    .{ .codepoint = \"U+23A6\", .width = 1 },\n    .{ .codepoint = \"U+23A7\", .width = 1 },\n    .{ .codepoint = \"U+23A8\", .width = 1 },\n    .{ .codepoint = \"U+23A9\", .width = 1 },\n    .{ .codepoint = \"U+23AA\", .width = 1 },\n    .{ .codepoint = \"U+23AB\", .width = 1 },\n    .{ .codepoint = \"U+23AC\", .width = 1 },\n    .{ .codepoint = \"U+23AD\", .width = 1 },\n    .{ .codepoint = \"U+23AE\", .width = 1 },\n    .{ .codepoint = \"U+23AF\", .width = 1 },\n    .{ .codepoint = \"U+23B0\", .width = 1 },\n    .{ .codepoint = \"U+23B1\", .width = 1 },\n    .{ .codepoint = \"U+23B2\", .width = 1 },\n    .{ .codepoint = \"U+23B3\", .width = 1 },\n    .{ .codepoint = \"U+23B4\", .width = 1 },\n    .{ .codepoint = \"U+23B5\", .width = 1 },\n    .{ .codepoint = \"U+23B6\", .width = 1 },\n    .{ .codepoint = \"U+23B7\", .width = 1 },\n    .{ .codepoint = \"U+23B8\", .width = 1 },\n    .{ .codepoint = \"U+23B9\", .width = 1 },\n    .{ .codepoint = \"U+23BA\", .width = 1 },\n    .{ .codepoint = \"U+23BB\", .width = 1 },\n    .{ .codepoint = \"U+23BC\", .width = 1 },\n    .{ .codepoint = \"U+23BD\", .width = 1 },\n    .{ .codepoint = \"U+23BE\", .width = 1 },\n    .{ .codepoint = \"U+23BF\", .width = 1 },\n    .{ .codepoint = \"U+23C0\", .width = 1 },\n    .{ .codepoint = \"U+23C1\", .width = 1 },\n    .{ .codepoint = \"U+23C2\", .width = 1 },\n    .{ .codepoint = \"U+23C3\", .width = 1 },\n    .{ .codepoint = \"U+23C4\", .width = 1 },\n    .{ .codepoint = \"U+23C5\", .width = 1 },\n    .{ .codepoint = \"U+23C6\", .width = 1 },\n    .{ .codepoint = \"U+23C7\", .width = 1 },\n    .{ .codepoint = \"U+23C8\", .width = 1 },\n    .{ .codepoint = \"U+23C9\", .width = 1 },\n    .{ .codepoint = \"U+23CA\", .width = 1 },\n    .{ .codepoint = \"U+23CB\", .width = 1 },\n    .{ .codepoint = \"U+23CC\", .width = 1 },\n    .{ .codepoint = \"U+23CD\", .width = 1 },\n    .{ .codepoint = \"U+23CE\", .width = 1 },\n    .{ .codepoint = \"U+23CF\", .width = 1 },\n    .{ .codepoint = \"U+23D0\", .width = 1 },\n    .{ .codepoint = \"U+23D1\", .width = 1 },\n    .{ .codepoint = \"U+23D2\", .width = 1 },\n    .{ .codepoint = \"U+23D3\", .width = 1 },\n    .{ .codepoint = \"U+23D4\", .width = 1 },\n    .{ .codepoint = \"U+23D5\", .width = 1 },\n    .{ .codepoint = \"U+23D6\", .width = 1 },\n    .{ .codepoint = \"U+23D7\", .width = 1 },\n    .{ .codepoint = \"U+23D8\", .width = 1 },\n    .{ .codepoint = \"U+23D9\", .width = 1 },\n    .{ .codepoint = \"U+23DA\", .width = 1 },\n    .{ .codepoint = \"U+23DB\", .width = 1 },\n    .{ .codepoint = \"U+23DC\", .width = 1 },\n    .{ .codepoint = \"U+23DD\", .width = 1 },\n    .{ .codepoint = \"U+23DE\", .width = 1 },\n    .{ .codepoint = \"U+23DF\", .width = 1 },\n    .{ .codepoint = \"U+23E0\", .width = 1 },\n    .{ .codepoint = \"U+23E1\", .width = 1 },\n    .{ .codepoint = \"U+23E2\", .width = 1 },\n    .{ .codepoint = \"U+23E3\", .width = 1 },\n    .{ .codepoint = \"U+23E4\", .width = 1 },\n    .{ .codepoint = \"U+23E5\", .width = 1 },\n    .{ .codepoint = \"U+23E6\", .width = 1 },\n    .{ .codepoint = \"U+23E7\", .width = 1 },\n    .{ .codepoint = \"U+23E8\", .width = 1 },\n    .{ .codepoint = \"U+23E9\", .width = 2 },\n    .{ .codepoint = \"U+23EA\", .width = 2 },\n    .{ .codepoint = \"U+23EB\", .width = 2 },\n    .{ .codepoint = \"U+23EC\", .width = 2 },\n    .{ .codepoint = \"U+23ED\", .width = 1 },\n    .{ .codepoint = \"U+23EE\", .width = 1 },\n    .{ .codepoint = \"U+23EF\", .width = 1 },\n    .{ .codepoint = \"U+23F0\", .width = 2 },\n    .{ .codepoint = \"U+23F1\", .width = 1 },\n    .{ .codepoint = \"U+23F2\", .width = 1 },\n    .{ .codepoint = \"U+23F3\", .width = 2 },\n    .{ .codepoint = \"U+23F4\", .width = 1 },\n    .{ .codepoint = \"U+23F5\", .width = 1 },\n    .{ .codepoint = \"U+23F6\", .width = 1 },\n    .{ .codepoint = \"U+23F7\", .width = 1 },\n    .{ .codepoint = \"U+23F8\", .width = 1 },\n    .{ .codepoint = \"U+23F9\", .width = 1 },\n    .{ .codepoint = \"U+23FA\", .width = 1 },\n    .{ .codepoint = \"U+23FB\", .width = 1 },\n    .{ .codepoint = \"U+23FC\", .width = 1 },\n    .{ .codepoint = \"U+23FD\", .width = 1 },\n    .{ .codepoint = \"U+23FE\", .width = 1 },\n    .{ .codepoint = \"U+23FF\", .width = 1 },\n    .{ .codepoint = \"U+2500\", .width = 1 },\n    .{ .codepoint = \"U+2501\", .width = 1 },\n    .{ .codepoint = \"U+2502\", .width = 1 },\n    .{ .codepoint = \"U+2503\", .width = 1 },\n    .{ .codepoint = \"U+2504\", .width = 1 },\n    .{ .codepoint = \"U+2505\", .width = 1 },\n    .{ .codepoint = \"U+2506\", .width = 1 },\n    .{ .codepoint = \"U+2507\", .width = 1 },\n    .{ .codepoint = \"U+2508\", .width = 1 },\n    .{ .codepoint = \"U+2509\", .width = 1 },\n    .{ .codepoint = \"U+250A\", .width = 1 },\n    .{ .codepoint = \"U+250B\", .width = 1 },\n    .{ .codepoint = \"U+250C\", .width = 1 },\n    .{ .codepoint = \"U+250D\", .width = 1 },\n    .{ .codepoint = \"U+250E\", .width = 1 },\n    .{ .codepoint = \"U+250F\", .width = 1 },\n    .{ .codepoint = \"U+2510\", .width = 1 },\n    .{ .codepoint = \"U+2511\", .width = 1 },\n    .{ .codepoint = \"U+2512\", .width = 1 },\n    .{ .codepoint = \"U+2513\", .width = 1 },\n    .{ .codepoint = \"U+2514\", .width = 1 },\n    .{ .codepoint = \"U+2515\", .width = 1 },\n    .{ .codepoint = \"U+2516\", .width = 1 },\n    .{ .codepoint = \"U+2517\", .width = 1 },\n    .{ .codepoint = \"U+2518\", .width = 1 },\n    .{ .codepoint = \"U+2519\", .width = 1 },\n    .{ .codepoint = \"U+251A\", .width = 1 },\n    .{ .codepoint = \"U+251B\", .width = 1 },\n    .{ .codepoint = \"U+251C\", .width = 1 },\n    .{ .codepoint = \"U+251D\", .width = 1 },\n    .{ .codepoint = \"U+251E\", .width = 1 },\n    .{ .codepoint = \"U+251F\", .width = 1 },\n    .{ .codepoint = \"U+2520\", .width = 1 },\n    .{ .codepoint = \"U+2521\", .width = 1 },\n    .{ .codepoint = \"U+2522\", .width = 1 },\n    .{ .codepoint = \"U+2523\", .width = 1 },\n    .{ .codepoint = \"U+2524\", .width = 1 },\n    .{ .codepoint = \"U+2525\", .width = 1 },\n    .{ .codepoint = \"U+2526\", .width = 1 },\n    .{ .codepoint = \"U+2527\", .width = 1 },\n    .{ .codepoint = \"U+2528\", .width = 1 },\n    .{ .codepoint = \"U+2529\", .width = 1 },\n    .{ .codepoint = \"U+252A\", .width = 1 },\n    .{ .codepoint = \"U+252B\", .width = 1 },\n    .{ .codepoint = \"U+252C\", .width = 1 },\n    .{ .codepoint = \"U+252D\", .width = 1 },\n    .{ .codepoint = \"U+252E\", .width = 1 },\n    .{ .codepoint = \"U+252F\", .width = 1 },\n    .{ .codepoint = \"U+2530\", .width = 1 },\n    .{ .codepoint = \"U+2531\", .width = 1 },\n    .{ .codepoint = \"U+2532\", .width = 1 },\n    .{ .codepoint = \"U+2533\", .width = 1 },\n    .{ .codepoint = \"U+2534\", .width = 1 },\n    .{ .codepoint = \"U+2535\", .width = 1 },\n    .{ .codepoint = \"U+2536\", .width = 1 },\n    .{ .codepoint = \"U+2537\", .width = 1 },\n    .{ .codepoint = \"U+2538\", .width = 1 },\n    .{ .codepoint = \"U+2539\", .width = 1 },\n    .{ .codepoint = \"U+253A\", .width = 1 },\n    .{ .codepoint = \"U+253B\", .width = 1 },\n    .{ .codepoint = \"U+253C\", .width = 1 },\n    .{ .codepoint = \"U+253D\", .width = 1 },\n    .{ .codepoint = \"U+253E\", .width = 1 },\n    .{ .codepoint = \"U+253F\", .width = 1 },\n    .{ .codepoint = \"U+2540\", .width = 1 },\n    .{ .codepoint = \"U+2541\", .width = 1 },\n    .{ .codepoint = \"U+2542\", .width = 1 },\n    .{ .codepoint = \"U+2543\", .width = 1 },\n    .{ .codepoint = \"U+2544\", .width = 1 },\n    .{ .codepoint = \"U+2545\", .width = 1 },\n    .{ .codepoint = \"U+2546\", .width = 1 },\n    .{ .codepoint = \"U+2547\", .width = 1 },\n    .{ .codepoint = \"U+2548\", .width = 1 },\n    .{ .codepoint = \"U+2549\", .width = 1 },\n    .{ .codepoint = \"U+254A\", .width = 1 },\n    .{ .codepoint = \"U+254B\", .width = 1 },\n    .{ .codepoint = \"U+254C\", .width = 1 },\n    .{ .codepoint = \"U+254D\", .width = 1 },\n    .{ .codepoint = \"U+254E\", .width = 1 },\n    .{ .codepoint = \"U+254F\", .width = 1 },\n    .{ .codepoint = \"U+2550\", .width = 1 },\n    .{ .codepoint = \"U+2551\", .width = 1 },\n    .{ .codepoint = \"U+2552\", .width = 1 },\n    .{ .codepoint = \"U+2553\", .width = 1 },\n    .{ .codepoint = \"U+2554\", .width = 1 },\n    .{ .codepoint = \"U+2555\", .width = 1 },\n    .{ .codepoint = \"U+2556\", .width = 1 },\n    .{ .codepoint = \"U+2557\", .width = 1 },\n    .{ .codepoint = \"U+2558\", .width = 1 },\n    .{ .codepoint = \"U+2559\", .width = 1 },\n    .{ .codepoint = \"U+255A\", .width = 1 },\n    .{ .codepoint = \"U+255B\", .width = 1 },\n    .{ .codepoint = \"U+255C\", .width = 1 },\n    .{ .codepoint = \"U+255D\", .width = 1 },\n    .{ .codepoint = \"U+255E\", .width = 1 },\n    .{ .codepoint = \"U+255F\", .width = 1 },\n    .{ .codepoint = \"U+2560\", .width = 1 },\n    .{ .codepoint = \"U+2561\", .width = 1 },\n    .{ .codepoint = \"U+2562\", .width = 1 },\n    .{ .codepoint = \"U+2563\", .width = 1 },\n    .{ .codepoint = \"U+2564\", .width = 1 },\n    .{ .codepoint = \"U+2565\", .width = 1 },\n    .{ .codepoint = \"U+2566\", .width = 1 },\n    .{ .codepoint = \"U+2567\", .width = 1 },\n    .{ .codepoint = \"U+2568\", .width = 1 },\n    .{ .codepoint = \"U+2569\", .width = 1 },\n    .{ .codepoint = \"U+256A\", .width = 1 },\n    .{ .codepoint = \"U+256B\", .width = 1 },\n    .{ .codepoint = \"U+256C\", .width = 1 },\n    .{ .codepoint = \"U+256D\", .width = 1 },\n    .{ .codepoint = \"U+256E\", .width = 1 },\n    .{ .codepoint = \"U+256F\", .width = 1 },\n    .{ .codepoint = \"U+2570\", .width = 1 },\n    .{ .codepoint = \"U+2571\", .width = 1 },\n    .{ .codepoint = \"U+2572\", .width = 1 },\n    .{ .codepoint = \"U+2573\", .width = 1 },\n    .{ .codepoint = \"U+2574\", .width = 1 },\n    .{ .codepoint = \"U+2575\", .width = 1 },\n    .{ .codepoint = \"U+2576\", .width = 1 },\n    .{ .codepoint = \"U+2577\", .width = 1 },\n    .{ .codepoint = \"U+2578\", .width = 1 },\n    .{ .codepoint = \"U+2579\", .width = 1 },\n    .{ .codepoint = \"U+257A\", .width = 1 },\n    .{ .codepoint = \"U+257B\", .width = 1 },\n    .{ .codepoint = \"U+257C\", .width = 1 },\n    .{ .codepoint = \"U+257D\", .width = 1 },\n    .{ .codepoint = \"U+257E\", .width = 1 },\n    .{ .codepoint = \"U+257F\", .width = 1 },\n    .{ .codepoint = \"U+2580\", .width = 1 },\n    .{ .codepoint = \"U+2581\", .width = 1 },\n    .{ .codepoint = \"U+2582\", .width = 1 },\n    .{ .codepoint = \"U+2583\", .width = 1 },\n    .{ .codepoint = \"U+2584\", .width = 1 },\n    .{ .codepoint = \"U+2585\", .width = 1 },\n    .{ .codepoint = \"U+2586\", .width = 1 },\n    .{ .codepoint = \"U+2587\", .width = 1 },\n    .{ .codepoint = \"U+2588\", .width = 1 },\n    .{ .codepoint = \"U+2589\", .width = 1 },\n    .{ .codepoint = \"U+258A\", .width = 1 },\n    .{ .codepoint = \"U+258B\", .width = 1 },\n    .{ .codepoint = \"U+258C\", .width = 1 },\n    .{ .codepoint = \"U+258D\", .width = 1 },\n    .{ .codepoint = \"U+258E\", .width = 1 },\n    .{ .codepoint = \"U+258F\", .width = 1 },\n    .{ .codepoint = \"U+2590\", .width = 1 },\n    .{ .codepoint = \"U+2591\", .width = 1 },\n    .{ .codepoint = \"U+2592\", .width = 1 },\n    .{ .codepoint = \"U+2593\", .width = 1 },\n    .{ .codepoint = \"U+2594\", .width = 1 },\n    .{ .codepoint = \"U+2595\", .width = 1 },\n    .{ .codepoint = \"U+2596\", .width = 1 },\n    .{ .codepoint = \"U+2597\", .width = 1 },\n    .{ .codepoint = \"U+2598\", .width = 1 },\n    .{ .codepoint = \"U+2599\", .width = 1 },\n    .{ .codepoint = \"U+259A\", .width = 1 },\n    .{ .codepoint = \"U+259B\", .width = 1 },\n    .{ .codepoint = \"U+259C\", .width = 1 },\n    .{ .codepoint = \"U+259D\", .width = 1 },\n    .{ .codepoint = \"U+259E\", .width = 1 },\n    .{ .codepoint = \"U+259F\", .width = 1 },\n    .{ .codepoint = \"U+25A0\", .width = 1 },\n    .{ .codepoint = \"U+25A1\", .width = 1 },\n    .{ .codepoint = \"U+25A2\", .width = 1 },\n    .{ .codepoint = \"U+25A3\", .width = 1 },\n    .{ .codepoint = \"U+25A4\", .width = 1 },\n    .{ .codepoint = \"U+25A5\", .width = 1 },\n    .{ .codepoint = \"U+25A6\", .width = 1 },\n    .{ .codepoint = \"U+25A7\", .width = 1 },\n    .{ .codepoint = \"U+25A8\", .width = 1 },\n    .{ .codepoint = \"U+25A9\", .width = 1 },\n    .{ .codepoint = \"U+25AA\", .width = 1 },\n    .{ .codepoint = \"U+25AB\", .width = 1 },\n    .{ .codepoint = \"U+25AC\", .width = 1 },\n    .{ .codepoint = \"U+25AD\", .width = 1 },\n    .{ .codepoint = \"U+25AE\", .width = 1 },\n    .{ .codepoint = \"U+25AF\", .width = 1 },\n    .{ .codepoint = \"U+25B0\", .width = 1 },\n    .{ .codepoint = \"U+25B1\", .width = 1 },\n    .{ .codepoint = \"U+25B2\", .width = 1 },\n    .{ .codepoint = \"U+25B3\", .width = 1 },\n    .{ .codepoint = \"U+25B4\", .width = 1 },\n    .{ .codepoint = \"U+25B5\", .width = 1 },\n    .{ .codepoint = \"U+25B6\", .width = 1 },\n    .{ .codepoint = \"U+25B7\", .width = 1 },\n    .{ .codepoint = \"U+25B8\", .width = 1 },\n    .{ .codepoint = \"U+25B9\", .width = 1 },\n    .{ .codepoint = \"U+25BA\", .width = 1 },\n    .{ .codepoint = \"U+25BB\", .width = 1 },\n    .{ .codepoint = \"U+25BC\", .width = 1 },\n    .{ .codepoint = \"U+25BD\", .width = 1 },\n    .{ .codepoint = \"U+25BE\", .width = 1 },\n    .{ .codepoint = \"U+25BF\", .width = 1 },\n    .{ .codepoint = \"U+25C0\", .width = 1 },\n    .{ .codepoint = \"U+25C1\", .width = 1 },\n    .{ .codepoint = \"U+25C2\", .width = 1 },\n    .{ .codepoint = \"U+25C3\", .width = 1 },\n    .{ .codepoint = \"U+25C4\", .width = 1 },\n    .{ .codepoint = \"U+25C5\", .width = 1 },\n    .{ .codepoint = \"U+25C6\", .width = 1 },\n    .{ .codepoint = \"U+25C7\", .width = 1 },\n    .{ .codepoint = \"U+25C8\", .width = 1 },\n    .{ .codepoint = \"U+25C9\", .width = 1 },\n    .{ .codepoint = \"U+25CA\", .width = 1 },\n    .{ .codepoint = \"U+25CB\", .width = 1 },\n    .{ .codepoint = \"U+25CC\", .width = 1 },\n    .{ .codepoint = \"U+25CD\", .width = 1 },\n    .{ .codepoint = \"U+25CE\", .width = 1 },\n    .{ .codepoint = \"U+25CF\", .width = 1 },\n    .{ .codepoint = \"U+25D0\", .width = 1 },\n    .{ .codepoint = \"U+25D1\", .width = 1 },\n    .{ .codepoint = \"U+25D2\", .width = 1 },\n    .{ .codepoint = \"U+25D3\", .width = 1 },\n    .{ .codepoint = \"U+25D4\", .width = 1 },\n    .{ .codepoint = \"U+25D5\", .width = 1 },\n    .{ .codepoint = \"U+25D6\", .width = 1 },\n    .{ .codepoint = \"U+25D7\", .width = 1 },\n    .{ .codepoint = \"U+25D8\", .width = 1 },\n    .{ .codepoint = \"U+25D9\", .width = 1 },\n    .{ .codepoint = \"U+25DA\", .width = 1 },\n    .{ .codepoint = \"U+25DB\", .width = 1 },\n    .{ .codepoint = \"U+25DC\", .width = 1 },\n    .{ .codepoint = \"U+25DD\", .width = 1 },\n    .{ .codepoint = \"U+25DE\", .width = 1 },\n    .{ .codepoint = \"U+25DF\", .width = 1 },\n    .{ .codepoint = \"U+25E0\", .width = 1 },\n    .{ .codepoint = \"U+25E1\", .width = 1 },\n    .{ .codepoint = \"U+25E2\", .width = 1 },\n    .{ .codepoint = \"U+25E3\", .width = 1 },\n    .{ .codepoint = \"U+25E4\", .width = 1 },\n    .{ .codepoint = \"U+25E5\", .width = 1 },\n    .{ .codepoint = \"U+25E6\", .width = 1 },\n    .{ .codepoint = \"U+25E7\", .width = 1 },\n    .{ .codepoint = \"U+25E8\", .width = 1 },\n    .{ .codepoint = \"U+25E9\", .width = 1 },\n    .{ .codepoint = \"U+25EA\", .width = 1 },\n    .{ .codepoint = \"U+25EB\", .width = 1 },\n    .{ .codepoint = \"U+25EC\", .width = 1 },\n    .{ .codepoint = \"U+25ED\", .width = 1 },\n    .{ .codepoint = \"U+25EE\", .width = 1 },\n    .{ .codepoint = \"U+25EF\", .width = 1 },\n    .{ .codepoint = \"U+25F0\", .width = 1 },\n    .{ .codepoint = \"U+25F1\", .width = 1 },\n    .{ .codepoint = \"U+25F2\", .width = 1 },\n    .{ .codepoint = \"U+25F3\", .width = 1 },\n    .{ .codepoint = \"U+25F4\", .width = 1 },\n    .{ .codepoint = \"U+25F5\", .width = 1 },\n    .{ .codepoint = \"U+25F6\", .width = 1 },\n    .{ .codepoint = \"U+25F7\", .width = 1 },\n    .{ .codepoint = \"U+25F8\", .width = 1 },\n    .{ .codepoint = \"U+25F9\", .width = 1 },\n    .{ .codepoint = \"U+25FA\", .width = 1 },\n    .{ .codepoint = \"U+25FB\", .width = 1 },\n    .{ .codepoint = \"U+25FC\", .width = 1 },\n    .{ .codepoint = \"U+25FD\", .width = 2 },\n    .{ .codepoint = \"U+25FE\", .width = 2 },\n    .{ .codepoint = \"U+25FF\", .width = 1 },\n    .{ .codepoint = \"U+2600\", .width = 1 },\n    .{ .codepoint = \"U+2601\", .width = 1 },\n    .{ .codepoint = \"U+2602\", .width = 1 },\n    .{ .codepoint = \"U+2603\", .width = 1 },\n    .{ .codepoint = \"U+2604\", .width = 1 },\n    .{ .codepoint = \"U+2605\", .width = 1 },\n    .{ .codepoint = \"U+2606\", .width = 1 },\n    .{ .codepoint = \"U+2607\", .width = 1 },\n    .{ .codepoint = \"U+2608\", .width = 1 },\n    .{ .codepoint = \"U+2609\", .width = 1 },\n    .{ .codepoint = \"U+260A\", .width = 1 },\n    .{ .codepoint = \"U+260B\", .width = 1 },\n    .{ .codepoint = \"U+260C\", .width = 1 },\n    .{ .codepoint = \"U+260D\", .width = 1 },\n    .{ .codepoint = \"U+260E\", .width = 1 },\n    .{ .codepoint = \"U+260F\", .width = 1 },\n    .{ .codepoint = \"U+2610\", .width = 1 },\n    .{ .codepoint = \"U+2611\", .width = 1 },\n    .{ .codepoint = \"U+2612\", .width = 1 },\n    .{ .codepoint = \"U+2613\", .width = 1 },\n    .{ .codepoint = \"U+2614\", .width = 2 },\n    .{ .codepoint = \"U+2615\", .width = 2 },\n    .{ .codepoint = \"U+2616\", .width = 1 },\n    .{ .codepoint = \"U+2617\", .width = 1 },\n    .{ .codepoint = \"U+2618\", .width = 1 },\n    .{ .codepoint = \"U+2619\", .width = 1 },\n    .{ .codepoint = \"U+261A\", .width = 1 },\n    .{ .codepoint = \"U+261B\", .width = 1 },\n    .{ .codepoint = \"U+261C\", .width = 1 },\n    .{ .codepoint = \"U+261D\", .width = 1 },\n    .{ .codepoint = \"U+261E\", .width = 1 },\n    .{ .codepoint = \"U+261F\", .width = 1 },\n    .{ .codepoint = \"U+2620\", .width = 1 },\n    .{ .codepoint = \"U+2621\", .width = 1 },\n    .{ .codepoint = \"U+2622\", .width = 2 },\n    .{ .codepoint = \"U+2623\", .width = 2 },\n    .{ .codepoint = \"U+2624\", .width = 1 },\n    .{ .codepoint = \"U+2625\", .width = 1 },\n    .{ .codepoint = \"U+2626\", .width = 1 },\n    .{ .codepoint = \"U+2627\", .width = 1 },\n    .{ .codepoint = \"U+2628\", .width = 1 },\n    .{ .codepoint = \"U+2629\", .width = 1 },\n    .{ .codepoint = \"U+262A\", .width = 1 },\n    .{ .codepoint = \"U+262B\", .width = 1 },\n    .{ .codepoint = \"U+262C\", .width = 1 },\n    .{ .codepoint = \"U+262D\", .width = 1 },\n    .{ .codepoint = \"U+262E\", .width = 1 },\n    .{ .codepoint = \"U+262F\", .width = 1 },\n    .{ .codepoint = \"U+2630\", .width = 2 },\n    .{ .codepoint = \"U+2631\", .width = 2 },\n    .{ .codepoint = \"U+2632\", .width = 2 },\n    .{ .codepoint = \"U+2633\", .width = 2 },\n    .{ .codepoint = \"U+2634\", .width = 2 },\n    .{ .codepoint = \"U+2635\", .width = 2 },\n    .{ .codepoint = \"U+2636\", .width = 2 },\n    .{ .codepoint = \"U+2637\", .width = 2 },\n    .{ .codepoint = \"U+2638\", .width = 1 },\n    .{ .codepoint = \"U+2639\", .width = 1 },\n    .{ .codepoint = \"U+263A\", .width = 1 },\n    .{ .codepoint = \"U+263B\", .width = 1 },\n    .{ .codepoint = \"U+263C\", .width = 1 },\n    .{ .codepoint = \"U+263D\", .width = 1 },\n    .{ .codepoint = \"U+263E\", .width = 1 },\n    .{ .codepoint = \"U+263F\", .width = 1 },\n    .{ .codepoint = \"U+2640\", .width = 1 },\n    .{ .codepoint = \"U+2641\", .width = 1 },\n    .{ .codepoint = \"U+2642\", .width = 1 },\n    .{ .codepoint = \"U+2643\", .width = 1 },\n    .{ .codepoint = \"U+2644\", .width = 1 },\n    .{ .codepoint = \"U+2645\", .width = 1 },\n    .{ .codepoint = \"U+2646\", .width = 1 },\n    .{ .codepoint = \"U+2647\", .width = 1 },\n    .{ .codepoint = \"U+2648\", .width = 2 },\n    .{ .codepoint = \"U+2649\", .width = 2 },\n    .{ .codepoint = \"U+264A\", .width = 2 },\n    .{ .codepoint = \"U+264B\", .width = 2 },\n    .{ .codepoint = \"U+264C\", .width = 2 },\n    .{ .codepoint = \"U+264D\", .width = 2 },\n    .{ .codepoint = \"U+264E\", .width = 2 },\n    .{ .codepoint = \"U+264F\", .width = 2 },\n    .{ .codepoint = \"U+2650\", .width = 2 },\n    .{ .codepoint = \"U+2651\", .width = 2 },\n    .{ .codepoint = \"U+2652\", .width = 2 },\n    .{ .codepoint = \"U+2653\", .width = 2 },\n    .{ .codepoint = \"U+2654\", .width = 1 },\n    .{ .codepoint = \"U+2655\", .width = 1 },\n    .{ .codepoint = \"U+2656\", .width = 1 },\n    .{ .codepoint = \"U+2657\", .width = 1 },\n    .{ .codepoint = \"U+2658\", .width = 1 },\n    .{ .codepoint = \"U+2659\", .width = 1 },\n    .{ .codepoint = \"U+265A\", .width = 1 },\n    .{ .codepoint = \"U+265B\", .width = 1 },\n    .{ .codepoint = \"U+265C\", .width = 1 },\n    .{ .codepoint = \"U+265D\", .width = 1 },\n    .{ .codepoint = \"U+265E\", .width = 1 },\n    .{ .codepoint = \"U+265F\", .width = 1 },\n    .{ .codepoint = \"U+2660\", .width = 1 },\n    .{ .codepoint = \"U+2661\", .width = 1 },\n    .{ .codepoint = \"U+2662\", .width = 1 },\n    .{ .codepoint = \"U+2663\", .width = 1 },\n    .{ .codepoint = \"U+2664\", .width = 1 },\n    .{ .codepoint = \"U+2665\", .width = 1 },\n    .{ .codepoint = \"U+2666\", .width = 1 },\n    .{ .codepoint = \"U+2667\", .width = 1 },\n    .{ .codepoint = \"U+2668\", .width = 1 },\n    .{ .codepoint = \"U+2669\", .width = 1 },\n    .{ .codepoint = \"U+266A\", .width = 1 },\n    .{ .codepoint = \"U+266B\", .width = 1 },\n    .{ .codepoint = \"U+266C\", .width = 1 },\n    .{ .codepoint = \"U+266D\", .width = 1 },\n    .{ .codepoint = \"U+266E\", .width = 1 },\n    .{ .codepoint = \"U+266F\", .width = 1 },\n    .{ .codepoint = \"U+2670\", .width = 1 },\n    .{ .codepoint = \"U+2671\", .width = 1 },\n    .{ .codepoint = \"U+2672\", .width = 1 },\n    .{ .codepoint = \"U+2673\", .width = 1 },\n    .{ .codepoint = \"U+2674\", .width = 1 },\n    .{ .codepoint = \"U+2675\", .width = 1 },\n    .{ .codepoint = \"U+2676\", .width = 1 },\n    .{ .codepoint = \"U+2677\", .width = 1 },\n    .{ .codepoint = \"U+2678\", .width = 1 },\n    .{ .codepoint = \"U+2679\", .width = 1 },\n    .{ .codepoint = \"U+267A\", .width = 1 },\n    .{ .codepoint = \"U+267B\", .width = 1 },\n    .{ .codepoint = \"U+267C\", .width = 1 },\n    .{ .codepoint = \"U+267D\", .width = 1 },\n    .{ .codepoint = \"U+267E\", .width = 1 },\n    .{ .codepoint = \"U+267F\", .width = 2 },\n    .{ .codepoint = \"U+2680\", .width = 1 },\n    .{ .codepoint = \"U+2681\", .width = 1 },\n    .{ .codepoint = \"U+2682\", .width = 1 },\n    .{ .codepoint = \"U+2683\", .width = 1 },\n    .{ .codepoint = \"U+2684\", .width = 1 },\n    .{ .codepoint = \"U+2685\", .width = 1 },\n    .{ .codepoint = \"U+2686\", .width = 1 },\n    .{ .codepoint = \"U+2687\", .width = 1 },\n    .{ .codepoint = \"U+2688\", .width = 1 },\n    .{ .codepoint = \"U+2689\", .width = 1 },\n    .{ .codepoint = \"U+268A\", .width = 2 },\n    .{ .codepoint = \"U+268B\", .width = 2 },\n    .{ .codepoint = \"U+268C\", .width = 2 },\n    .{ .codepoint = \"U+268D\", .width = 2 },\n    .{ .codepoint = \"U+268E\", .width = 2 },\n    .{ .codepoint = \"U+268F\", .width = 2 },\n    .{ .codepoint = \"U+2690\", .width = 1 },\n    .{ .codepoint = \"U+2691\", .width = 1 },\n    .{ .codepoint = \"U+2692\", .width = 1 },\n    .{ .codepoint = \"U+2693\", .width = 2 },\n    .{ .codepoint = \"U+2694\", .width = 1 },\n    .{ .codepoint = \"U+2695\", .width = 1 },\n    .{ .codepoint = \"U+2696\", .width = 1 },\n    .{ .codepoint = \"U+2697\", .width = 1 },\n    .{ .codepoint = \"U+2698\", .width = 1 },\n    .{ .codepoint = \"U+2699\", .width = 1 },\n    .{ .codepoint = \"U+269A\", .width = 1 },\n    .{ .codepoint = \"U+269B\", .width = 2 },\n    .{ .codepoint = \"U+269C\", .width = 1 },\n    .{ .codepoint = \"U+269D\", .width = 1 },\n    .{ .codepoint = \"U+269E\", .width = 1 },\n    .{ .codepoint = \"U+269F\", .width = 1 },\n    .{ .codepoint = \"U+26A0\", .width = 2 },\n    .{ .codepoint = \"U+26A1\", .width = 2 },\n    .{ .codepoint = \"U+26A2\", .width = 1 },\n    .{ .codepoint = \"U+26A3\", .width = 1 },\n    .{ .codepoint = \"U+26A4\", .width = 1 },\n    .{ .codepoint = \"U+26A5\", .width = 1 },\n    .{ .codepoint = \"U+26A6\", .width = 1 },\n    .{ .codepoint = \"U+26A7\", .width = 1 },\n    .{ .codepoint = \"U+26A8\", .width = 1 },\n    .{ .codepoint = \"U+26A9\", .width = 1 },\n    .{ .codepoint = \"U+26AA\", .width = 2 },\n    .{ .codepoint = \"U+26AB\", .width = 2 },\n    .{ .codepoint = \"U+26AC\", .width = 1 },\n    .{ .codepoint = \"U+26AD\", .width = 1 },\n    .{ .codepoint = \"U+26AE\", .width = 1 },\n    .{ .codepoint = \"U+26AF\", .width = 1 },\n    .{ .codepoint = \"U+26B0\", .width = 1 },\n    .{ .codepoint = \"U+26B1\", .width = 1 },\n    .{ .codepoint = \"U+26B2\", .width = 1 },\n    .{ .codepoint = \"U+26B3\", .width = 1 },\n    .{ .codepoint = \"U+26B4\", .width = 1 },\n    .{ .codepoint = \"U+26B5\", .width = 1 },\n    .{ .codepoint = \"U+26B6\", .width = 1 },\n    .{ .codepoint = \"U+26B7\", .width = 1 },\n    .{ .codepoint = \"U+26B8\", .width = 1 },\n    .{ .codepoint = \"U+26B9\", .width = 1 },\n    .{ .codepoint = \"U+26BA\", .width = 1 },\n    .{ .codepoint = \"U+26BB\", .width = 1 },\n    .{ .codepoint = \"U+26BC\", .width = 1 },\n    .{ .codepoint = \"U+26BD\", .width = 2 },\n    .{ .codepoint = \"U+26BE\", .width = 2 },\n    .{ .codepoint = \"U+26BF\", .width = 1 },\n    .{ .codepoint = \"U+26C0\", .width = 1 },\n    .{ .codepoint = \"U+26C1\", .width = 1 },\n    .{ .codepoint = \"U+26C2\", .width = 1 },\n    .{ .codepoint = \"U+26C3\", .width = 1 },\n    .{ .codepoint = \"U+26C4\", .width = 2 },\n    .{ .codepoint = \"U+26C5\", .width = 2 },\n    .{ .codepoint = \"U+26C6\", .width = 1 },\n    .{ .codepoint = \"U+26C7\", .width = 1 },\n    .{ .codepoint = \"U+26C8\", .width = 1 },\n    .{ .codepoint = \"U+26C9\", .width = 1 },\n    .{ .codepoint = \"U+26CA\", .width = 1 },\n    .{ .codepoint = \"U+26CB\", .width = 1 },\n    .{ .codepoint = \"U+26CC\", .width = 1 },\n    .{ .codepoint = \"U+26CD\", .width = 1 },\n    .{ .codepoint = \"U+26CE\", .width = 2 },\n    .{ .codepoint = \"U+26CF\", .width = 1 },\n    .{ .codepoint = \"U+26D0\", .width = 1 },\n    .{ .codepoint = \"U+26D1\", .width = 2 },\n    .{ .codepoint = \"U+26D2\", .width = 1 },\n    .{ .codepoint = \"U+26D3\", .width = 1 },\n    .{ .codepoint = \"U+26D4\", .width = 2 },\n    .{ .codepoint = \"U+26D5\", .width = 1 },\n    .{ .codepoint = \"U+26D6\", .width = 1 },\n    .{ .codepoint = \"U+26D7\", .width = 1 },\n    .{ .codepoint = \"U+26D8\", .width = 1 },\n    .{ .codepoint = \"U+26D9\", .width = 1 },\n    .{ .codepoint = \"U+26DA\", .width = 1 },\n    .{ .codepoint = \"U+26DB\", .width = 1 },\n    .{ .codepoint = \"U+26DC\", .width = 1 },\n    .{ .codepoint = \"U+26DD\", .width = 1 },\n    .{ .codepoint = \"U+26DE\", .width = 1 },\n    .{ .codepoint = \"U+26DF\", .width = 1 },\n    .{ .codepoint = \"U+26E0\", .width = 1 },\n    .{ .codepoint = \"U+26E1\", .width = 1 },\n    .{ .codepoint = \"U+26E2\", .width = 1 },\n    .{ .codepoint = \"U+26E3\", .width = 1 },\n    .{ .codepoint = \"U+26E4\", .width = 1 },\n    .{ .codepoint = \"U+26E5\", .width = 1 },\n    .{ .codepoint = \"U+26E6\", .width = 1 },\n    .{ .codepoint = \"U+26E7\", .width = 1 },\n    .{ .codepoint = \"U+26E8\", .width = 1 },\n    .{ .codepoint = \"U+26E9\", .width = 1 },\n    .{ .codepoint = \"U+26EA\", .width = 2 },\n    .{ .codepoint = \"U+26EB\", .width = 1 },\n    .{ .codepoint = \"U+26EC\", .width = 1 },\n    .{ .codepoint = \"U+26ED\", .width = 1 },\n    .{ .codepoint = \"U+26EE\", .width = 1 },\n    .{ .codepoint = \"U+26EF\", .width = 1 },\n    .{ .codepoint = \"U+26F0\", .width = 1 },\n    .{ .codepoint = \"U+26F1\", .width = 1 },\n    .{ .codepoint = \"U+26F2\", .width = 2 },\n    .{ .codepoint = \"U+26F3\", .width = 2 },\n    .{ .codepoint = \"U+26F4\", .width = 1 },\n    .{ .codepoint = \"U+26F5\", .width = 2 },\n    .{ .codepoint = \"U+26F6\", .width = 1 },\n    .{ .codepoint = \"U+26F7\", .width = 1 },\n    .{ .codepoint = \"U+26F8\", .width = 1 },\n    .{ .codepoint = \"U+26F9\", .width = 1 },\n    .{ .codepoint = \"U+26FA\", .width = 2 },\n    .{ .codepoint = \"U+26FB\", .width = 1 },\n    .{ .codepoint = \"U+26FC\", .width = 1 },\n    .{ .codepoint = \"U+26FD\", .width = 2 },\n    .{ .codepoint = \"U+26FE\", .width = 1 },\n    .{ .codepoint = \"U+26FF\", .width = 1 },\n    .{ .codepoint = \"U+2700\", .width = 1 },\n    .{ .codepoint = \"U+2701\", .width = 1 },\n    .{ .codepoint = \"U+2702\", .width = 1 },\n    .{ .codepoint = \"U+2703\", .width = 1 },\n    .{ .codepoint = \"U+2704\", .width = 1 },\n    .{ .codepoint = \"U+2705\", .width = 2 },\n    .{ .codepoint = \"U+2706\", .width = 1 },\n    .{ .codepoint = \"U+2707\", .width = 1 },\n    .{ .codepoint = \"U+2708\", .width = 1 },\n    .{ .codepoint = \"U+2709\", .width = 1 },\n    .{ .codepoint = \"U+270A\", .width = 2 },\n    .{ .codepoint = \"U+270B\", .width = 2 },\n    .{ .codepoint = \"U+270C\", .width = 1 },\n    .{ .codepoint = \"U+270D\", .width = 1 },\n    .{ .codepoint = \"U+270E\", .width = 1 },\n    .{ .codepoint = \"U+270F\", .width = 1 },\n    .{ .codepoint = \"U+2710\", .width = 1 },\n    .{ .codepoint = \"U+2711\", .width = 1 },\n    .{ .codepoint = \"U+2712\", .width = 1 },\n    .{ .codepoint = \"U+2713\", .width = 1 },\n    .{ .codepoint = \"U+2714\", .width = 1 },\n    .{ .codepoint = \"U+2715\", .width = 1 },\n    .{ .codepoint = \"U+2716\", .width = 1 },\n    .{ .codepoint = \"U+2717\", .width = 1 },\n    .{ .codepoint = \"U+2718\", .width = 1 },\n    .{ .codepoint = \"U+2719\", .width = 1 },\n    .{ .codepoint = \"U+271A\", .width = 1 },\n    .{ .codepoint = \"U+271B\", .width = 1 },\n    .{ .codepoint = \"U+271C\", .width = 1 },\n    .{ .codepoint = \"U+271D\", .width = 1 },\n    .{ .codepoint = \"U+271E\", .width = 1 },\n    .{ .codepoint = \"U+271F\", .width = 1 },\n    .{ .codepoint = \"U+2720\", .width = 1 },\n    .{ .codepoint = \"U+2721\", .width = 1 },\n    .{ .codepoint = \"U+2722\", .width = 1 },\n    .{ .codepoint = \"U+2723\", .width = 1 },\n    .{ .codepoint = \"U+2724\", .width = 1 },\n    .{ .codepoint = \"U+2725\", .width = 1 },\n    .{ .codepoint = \"U+2726\", .width = 1 },\n    .{ .codepoint = \"U+2727\", .width = 1 },\n    .{ .codepoint = \"U+2728\", .width = 2 },\n    .{ .codepoint = \"U+2729\", .width = 1 },\n    .{ .codepoint = \"U+272A\", .width = 1 },\n    .{ .codepoint = \"U+272B\", .width = 1 },\n    .{ .codepoint = \"U+272C\", .width = 1 },\n    .{ .codepoint = \"U+272D\", .width = 1 },\n    .{ .codepoint = \"U+272E\", .width = 1 },\n    .{ .codepoint = \"U+272F\", .width = 1 },\n    .{ .codepoint = \"U+2730\", .width = 1 },\n    .{ .codepoint = \"U+2731\", .width = 1 },\n    .{ .codepoint = \"U+2732\", .width = 1 },\n    .{ .codepoint = \"U+2733\", .width = 1 },\n    .{ .codepoint = \"U+2734\", .width = 1 },\n    .{ .codepoint = \"U+2735\", .width = 1 },\n    .{ .codepoint = \"U+2736\", .width = 1 },\n    .{ .codepoint = \"U+2737\", .width = 1 },\n    .{ .codepoint = \"U+2738\", .width = 1 },\n    .{ .codepoint = \"U+2739\", .width = 1 },\n    .{ .codepoint = \"U+273A\", .width = 1 },\n    .{ .codepoint = \"U+273B\", .width = 1 },\n    .{ .codepoint = \"U+273C\", .width = 1 },\n    .{ .codepoint = \"U+273D\", .width = 1 },\n    .{ .codepoint = \"U+273E\", .width = 1 },\n    .{ .codepoint = \"U+273F\", .width = 1 },\n    .{ .codepoint = \"U+2740\", .width = 1 },\n    .{ .codepoint = \"U+2741\", .width = 1 },\n    .{ .codepoint = \"U+2742\", .width = 1 },\n    .{ .codepoint = \"U+2743\", .width = 1 },\n    .{ .codepoint = \"U+2744\", .width = 1 },\n    .{ .codepoint = \"U+2745\", .width = 1 },\n    .{ .codepoint = \"U+2746\", .width = 1 },\n    .{ .codepoint = \"U+2747\", .width = 1 },\n    .{ .codepoint = \"U+2748\", .width = 1 },\n    .{ .codepoint = \"U+2749\", .width = 1 },\n    .{ .codepoint = \"U+274A\", .width = 1 },\n    .{ .codepoint = \"U+274B\", .width = 1 },\n    .{ .codepoint = \"U+274C\", .width = 2 },\n    .{ .codepoint = \"U+274D\", .width = 1 },\n    .{ .codepoint = \"U+274E\", .width = 2 },\n    .{ .codepoint = \"U+274F\", .width = 1 },\n    .{ .codepoint = \"U+2750\", .width = 1 },\n    .{ .codepoint = \"U+2751\", .width = 1 },\n    .{ .codepoint = \"U+2752\", .width = 1 },\n    .{ .codepoint = \"U+2753\", .width = 2 },\n    .{ .codepoint = \"U+2754\", .width = 2 },\n    .{ .codepoint = \"U+2755\", .width = 2 },\n    .{ .codepoint = \"U+2756\", .width = 1 },\n    .{ .codepoint = \"U+2757\", .width = 2 },\n    .{ .codepoint = \"U+2758\", .width = 1 },\n    .{ .codepoint = \"U+2759\", .width = 1 },\n    .{ .codepoint = \"U+275A\", .width = 1 },\n    .{ .codepoint = \"U+275B\", .width = 1 },\n    .{ .codepoint = \"U+275C\", .width = 1 },\n    .{ .codepoint = \"U+275D\", .width = 1 },\n    .{ .codepoint = \"U+275E\", .width = 1 },\n    .{ .codepoint = \"U+275F\", .width = 1 },\n    .{ .codepoint = \"U+2760\", .width = 2 },\n    .{ .codepoint = \"U+2761\", .width = 2 },\n    .{ .codepoint = \"U+2762\", .width = 2 },\n    .{ .codepoint = \"U+2763\", .width = 2 },\n    .{ .codepoint = \"U+2764\", .width = 2 },\n    .{ .codepoint = \"U+2765\", .width = 2 },\n    .{ .codepoint = \"U+2766\", .width = 2 },\n    .{ .codepoint = \"U+2767\", .width = 2 },\n    .{ .codepoint = \"U+2768\", .width = 1 },\n    .{ .codepoint = \"U+2769\", .width = 1 },\n    .{ .codepoint = \"U+276A\", .width = 1 },\n    .{ .codepoint = \"U+276B\", .width = 1 },\n    .{ .codepoint = \"U+276C\", .width = 1 },\n    .{ .codepoint = \"U+276D\", .width = 1 },\n    .{ .codepoint = \"U+276E\", .width = 1 },\n    .{ .codepoint = \"U+276F\", .width = 1 },\n    .{ .codepoint = \"U+2770\", .width = 1 },\n    .{ .codepoint = \"U+2771\", .width = 1 },\n    .{ .codepoint = \"U+2772\", .width = 1 },\n    .{ .codepoint = \"U+2773\", .width = 1 },\n    .{ .codepoint = \"U+2774\", .width = 1 },\n    .{ .codepoint = \"U+2775\", .width = 1 },\n    .{ .codepoint = \"U+2776\", .width = 1 },\n    .{ .codepoint = \"U+2777\", .width = 1 },\n    .{ .codepoint = \"U+2778\", .width = 1 },\n    .{ .codepoint = \"U+2779\", .width = 1 },\n    .{ .codepoint = \"U+277A\", .width = 1 },\n    .{ .codepoint = \"U+277B\", .width = 1 },\n    .{ .codepoint = \"U+277C\", .width = 1 },\n    .{ .codepoint = \"U+277D\", .width = 1 },\n    .{ .codepoint = \"U+277E\", .width = 1 },\n    .{ .codepoint = \"U+277F\", .width = 1 },\n    .{ .codepoint = \"U+2780\", .width = 1 },\n    .{ .codepoint = \"U+2781\", .width = 1 },\n    .{ .codepoint = \"U+2782\", .width = 1 },\n    .{ .codepoint = \"U+2783\", .width = 1 },\n    .{ .codepoint = \"U+2784\", .width = 1 },\n    .{ .codepoint = \"U+2785\", .width = 1 },\n    .{ .codepoint = \"U+2786\", .width = 1 },\n    .{ .codepoint = \"U+2787\", .width = 1 },\n    .{ .codepoint = \"U+2788\", .width = 1 },\n    .{ .codepoint = \"U+2789\", .width = 1 },\n    .{ .codepoint = \"U+278A\", .width = 1 },\n    .{ .codepoint = \"U+278B\", .width = 1 },\n    .{ .codepoint = \"U+278C\", .width = 1 },\n    .{ .codepoint = \"U+278D\", .width = 1 },\n    .{ .codepoint = \"U+278E\", .width = 1 },\n    .{ .codepoint = \"U+278F\", .width = 1 },\n    .{ .codepoint = \"U+2790\", .width = 1 },\n    .{ .codepoint = \"U+2791\", .width = 1 },\n    .{ .codepoint = \"U+2792\", .width = 1 },\n    .{ .codepoint = \"U+2793\", .width = 1 },\n    .{ .codepoint = \"U+2794\", .width = 1 },\n    .{ .codepoint = \"U+2795\", .width = 2 },\n    .{ .codepoint = \"U+2796\", .width = 2 },\n    .{ .codepoint = \"U+2797\", .width = 2 },\n    .{ .codepoint = \"U+2798\", .width = 1 },\n    .{ .codepoint = \"U+2799\", .width = 1 },\n    .{ .codepoint = \"U+279A\", .width = 1 },\n    .{ .codepoint = \"U+279B\", .width = 1 },\n    .{ .codepoint = \"U+279C\", .width = 1 },\n    .{ .codepoint = \"U+279D\", .width = 1 },\n    .{ .codepoint = \"U+279E\", .width = 1 },\n    .{ .codepoint = \"U+279F\", .width = 1 },\n    .{ .codepoint = \"U+27A0\", .width = 1 },\n    .{ .codepoint = \"U+27A1\", .width = 1 },\n    .{ .codepoint = \"U+27A2\", .width = 1 },\n    .{ .codepoint = \"U+27A3\", .width = 1 },\n    .{ .codepoint = \"U+27A4\", .width = 1 },\n    .{ .codepoint = \"U+27A5\", .width = 1 },\n    .{ .codepoint = \"U+27A6\", .width = 1 },\n    .{ .codepoint = \"U+27A7\", .width = 1 },\n    .{ .codepoint = \"U+27A8\", .width = 1 },\n    .{ .codepoint = \"U+27A9\", .width = 1 },\n    .{ .codepoint = \"U+27AA\", .width = 1 },\n    .{ .codepoint = \"U+27AB\", .width = 1 },\n    .{ .codepoint = \"U+27AC\", .width = 1 },\n    .{ .codepoint = \"U+27AD\", .width = 1 },\n    .{ .codepoint = \"U+27AE\", .width = 1 },\n    .{ .codepoint = \"U+27AF\", .width = 1 },\n    .{ .codepoint = \"U+27B0\", .width = 2 },\n    .{ .codepoint = \"U+27B1\", .width = 1 },\n    .{ .codepoint = \"U+27B2\", .width = 1 },\n    .{ .codepoint = \"U+27B3\", .width = 1 },\n    .{ .codepoint = \"U+27B4\", .width = 1 },\n    .{ .codepoint = \"U+27B5\", .width = 1 },\n    .{ .codepoint = \"U+27B6\", .width = 1 },\n    .{ .codepoint = \"U+27B7\", .width = 1 },\n    .{ .codepoint = \"U+27B8\", .width = 1 },\n    .{ .codepoint = \"U+27B9\", .width = 1 },\n    .{ .codepoint = \"U+27BA\", .width = 1 },\n    .{ .codepoint = \"U+27BB\", .width = 1 },\n    .{ .codepoint = \"U+27BC\", .width = 1 },\n    .{ .codepoint = \"U+27BD\", .width = 1 },\n    .{ .codepoint = \"U+27BE\", .width = 1 },\n    .{ .codepoint = \"U+27BF\", .width = 2 },\n    .{ .codepoint = \"U+3000\", .width = 2 },\n    .{ .codepoint = \"U+3001\", .width = 2 },\n    .{ .codepoint = \"U+3002\", .width = 2 },\n    .{ .codepoint = \"U+3003\", .width = 2 },\n    .{ .codepoint = \"U+3004\", .width = 2 },\n    .{ .codepoint = \"U+3005\", .width = 2 },\n    .{ .codepoint = \"U+3006\", .width = 2 },\n    .{ .codepoint = \"U+3007\", .width = 2 },\n    .{ .codepoint = \"U+3008\", .width = 2 },\n    .{ .codepoint = \"U+3009\", .width = 2 },\n    .{ .codepoint = \"U+300A\", .width = 2 },\n    .{ .codepoint = \"U+300B\", .width = 2 },\n    .{ .codepoint = \"U+300C\", .width = 2 },\n    .{ .codepoint = \"U+300D\", .width = 2 },\n    .{ .codepoint = \"U+300E\", .width = 2 },\n    .{ .codepoint = \"U+300F\", .width = 2 },\n    .{ .codepoint = \"U+3010\", .width = 2 },\n    .{ .codepoint = \"U+3011\", .width = 2 },\n    .{ .codepoint = \"U+3012\", .width = 2 },\n    .{ .codepoint = \"U+3013\", .width = 2 },\n    .{ .codepoint = \"U+3014\", .width = 2 },\n    .{ .codepoint = \"U+3015\", .width = 2 },\n    .{ .codepoint = \"U+3016\", .width = 2 },\n    .{ .codepoint = \"U+3017\", .width = 2 },\n    .{ .codepoint = \"U+3018\", .width = 2 },\n    .{ .codepoint = \"U+3019\", .width = 2 },\n    .{ .codepoint = \"U+301A\", .width = 2 },\n    .{ .codepoint = \"U+301B\", .width = 2 },\n    .{ .codepoint = \"U+301C\", .width = 2 },\n    .{ .codepoint = \"U+301D\", .width = 2 },\n    .{ .codepoint = \"U+301E\", .width = 2 },\n    .{ .codepoint = \"U+301F\", .width = 2 },\n    .{ .codepoint = \"U+3020\", .width = 2 },\n    .{ .codepoint = \"U+3021\", .width = 2 },\n    .{ .codepoint = \"U+3022\", .width = 2 },\n    .{ .codepoint = \"U+3023\", .width = 2 },\n    .{ .codepoint = \"U+3024\", .width = 2 },\n    .{ .codepoint = \"U+3025\", .width = 2 },\n    .{ .codepoint = \"U+3026\", .width = 2 },\n    .{ .codepoint = \"U+3027\", .width = 2 },\n    .{ .codepoint = \"U+3028\", .width = 2 },\n    .{ .codepoint = \"U+3029\", .width = 2 },\n    .{ .codepoint = \"U+3030\", .width = 2 },\n    .{ .codepoint = \"U+3031\", .width = 2 },\n    .{ .codepoint = \"U+3032\", .width = 2 },\n    .{ .codepoint = \"U+3033\", .width = 2 },\n    .{ .codepoint = \"U+3034\", .width = 2 },\n    .{ .codepoint = \"U+3035\", .width = 2 },\n    .{ .codepoint = \"U+3036\", .width = 2 },\n    .{ .codepoint = \"U+3037\", .width = 2 },\n    .{ .codepoint = \"U+3038\", .width = 2 },\n    .{ .codepoint = \"U+3039\", .width = 2 },\n    .{ .codepoint = \"U+303A\", .width = 2 },\n    .{ .codepoint = \"U+303B\", .width = 2 },\n    .{ .codepoint = \"U+303C\", .width = 2 },\n    .{ .codepoint = \"U+303D\", .width = 2 },\n    .{ .codepoint = \"U+303E\", .width = 2 },\n    .{ .codepoint = \"U+303F\", .width = 1 },\n    .{ .codepoint = \"U+3040\", .width = 1 },\n    .{ .codepoint = \"U+3041\", .width = 2 },\n    .{ .codepoint = \"U+3042\", .width = 2 },\n    .{ .codepoint = \"U+3043\", .width = 2 },\n    .{ .codepoint = \"U+3044\", .width = 2 },\n    .{ .codepoint = \"U+3045\", .width = 2 },\n    .{ .codepoint = \"U+3046\", .width = 2 },\n    .{ .codepoint = \"U+3047\", .width = 2 },\n    .{ .codepoint = \"U+3048\", .width = 2 },\n    .{ .codepoint = \"U+3049\", .width = 2 },\n    .{ .codepoint = \"U+304A\", .width = 2 },\n    .{ .codepoint = \"U+304B\", .width = 2 },\n    .{ .codepoint = \"U+304C\", .width = 2 },\n    .{ .codepoint = \"U+304D\", .width = 2 },\n    .{ .codepoint = \"U+304E\", .width = 2 },\n    .{ .codepoint = \"U+304F\", .width = 2 },\n    .{ .codepoint = \"U+3050\", .width = 2 },\n    .{ .codepoint = \"U+3051\", .width = 2 },\n    .{ .codepoint = \"U+3052\", .width = 2 },\n    .{ .codepoint = \"U+3053\", .width = 2 },\n    .{ .codepoint = \"U+3054\", .width = 2 },\n    .{ .codepoint = \"U+3055\", .width = 2 },\n    .{ .codepoint = \"U+3056\", .width = 2 },\n    .{ .codepoint = \"U+3057\", .width = 2 },\n    .{ .codepoint = \"U+3058\", .width = 2 },\n    .{ .codepoint = \"U+3059\", .width = 2 },\n    .{ .codepoint = \"U+305A\", .width = 2 },\n    .{ .codepoint = \"U+305B\", .width = 2 },\n    .{ .codepoint = \"U+305C\", .width = 2 },\n    .{ .codepoint = \"U+305D\", .width = 2 },\n    .{ .codepoint = \"U+305E\", .width = 2 },\n    .{ .codepoint = \"U+305F\", .width = 2 },\n    .{ .codepoint = \"U+3060\", .width = 2 },\n    .{ .codepoint = \"U+3061\", .width = 2 },\n    .{ .codepoint = \"U+3062\", .width = 2 },\n    .{ .codepoint = \"U+3063\", .width = 2 },\n    .{ .codepoint = \"U+3064\", .width = 2 },\n    .{ .codepoint = \"U+3065\", .width = 2 },\n    .{ .codepoint = \"U+3066\", .width = 2 },\n    .{ .codepoint = \"U+3067\", .width = 2 },\n    .{ .codepoint = \"U+3068\", .width = 2 },\n    .{ .codepoint = \"U+3069\", .width = 2 },\n    .{ .codepoint = \"U+306A\", .width = 2 },\n    .{ .codepoint = \"U+306B\", .width = 2 },\n    .{ .codepoint = \"U+306C\", .width = 2 },\n    .{ .codepoint = \"U+306D\", .width = 2 },\n    .{ .codepoint = \"U+306E\", .width = 2 },\n    .{ .codepoint = \"U+306F\", .width = 2 },\n    .{ .codepoint = \"U+3070\", .width = 2 },\n    .{ .codepoint = \"U+3071\", .width = 2 },\n    .{ .codepoint = \"U+3072\", .width = 2 },\n    .{ .codepoint = \"U+3073\", .width = 2 },\n    .{ .codepoint = \"U+3074\", .width = 2 },\n    .{ .codepoint = \"U+3075\", .width = 2 },\n    .{ .codepoint = \"U+3076\", .width = 2 },\n    .{ .codepoint = \"U+3077\", .width = 2 },\n    .{ .codepoint = \"U+3078\", .width = 2 },\n    .{ .codepoint = \"U+3079\", .width = 2 },\n    .{ .codepoint = \"U+307A\", .width = 2 },\n    .{ .codepoint = \"U+307B\", .width = 2 },\n    .{ .codepoint = \"U+307C\", .width = 2 },\n    .{ .codepoint = \"U+307D\", .width = 2 },\n    .{ .codepoint = \"U+307E\", .width = 2 },\n    .{ .codepoint = \"U+307F\", .width = 2 },\n    .{ .codepoint = \"U+3080\", .width = 2 },\n    .{ .codepoint = \"U+3081\", .width = 2 },\n    .{ .codepoint = \"U+3082\", .width = 2 },\n    .{ .codepoint = \"U+3083\", .width = 2 },\n    .{ .codepoint = \"U+3084\", .width = 2 },\n    .{ .codepoint = \"U+3085\", .width = 2 },\n    .{ .codepoint = \"U+3086\", .width = 2 },\n    .{ .codepoint = \"U+3087\", .width = 2 },\n    .{ .codepoint = \"U+3088\", .width = 2 },\n    .{ .codepoint = \"U+3089\", .width = 2 },\n    .{ .codepoint = \"U+308A\", .width = 2 },\n    .{ .codepoint = \"U+308B\", .width = 2 },\n    .{ .codepoint = \"U+308C\", .width = 2 },\n    .{ .codepoint = \"U+308D\", .width = 2 },\n    .{ .codepoint = \"U+308E\", .width = 2 },\n    .{ .codepoint = \"U+308F\", .width = 2 },\n    .{ .codepoint = \"U+3090\", .width = 2 },\n    .{ .codepoint = \"U+3091\", .width = 2 },\n    .{ .codepoint = \"U+3092\", .width = 2 },\n    .{ .codepoint = \"U+3093\", .width = 2 },\n    .{ .codepoint = \"U+3094\", .width = 2 },\n    .{ .codepoint = \"U+3095\", .width = 2 },\n    .{ .codepoint = \"U+3096\", .width = 2 },\n    .{ .codepoint = \"U+3097\", .width = 1 },\n    .{ .codepoint = \"U+3098\", .width = 1 },\n    .{ .codepoint = \"U+309B\", .width = 2 },\n    .{ .codepoint = \"U+309C\", .width = 2 },\n    .{ .codepoint = \"U+309D\", .width = 2 },\n    .{ .codepoint = \"U+309E\", .width = 2 },\n    .{ .codepoint = \"U+309F\", .width = 2 },\n    .{ .codepoint = \"U+30A0\", .width = 2 },\n    .{ .codepoint = \"U+30A1\", .width = 2 },\n    .{ .codepoint = \"U+30A2\", .width = 2 },\n    .{ .codepoint = \"U+30A3\", .width = 2 },\n    .{ .codepoint = \"U+30A4\", .width = 2 },\n    .{ .codepoint = \"U+30A5\", .width = 2 },\n    .{ .codepoint = \"U+30A6\", .width = 2 },\n    .{ .codepoint = \"U+30A7\", .width = 2 },\n    .{ .codepoint = \"U+30A8\", .width = 2 },\n    .{ .codepoint = \"U+30A9\", .width = 2 },\n    .{ .codepoint = \"U+30AA\", .width = 2 },\n    .{ .codepoint = \"U+30AB\", .width = 2 },\n    .{ .codepoint = \"U+30AC\", .width = 2 },\n    .{ .codepoint = \"U+30AD\", .width = 2 },\n    .{ .codepoint = \"U+30AE\", .width = 2 },\n    .{ .codepoint = \"U+30AF\", .width = 2 },\n    .{ .codepoint = \"U+30B0\", .width = 2 },\n    .{ .codepoint = \"U+30B1\", .width = 2 },\n    .{ .codepoint = \"U+30B2\", .width = 2 },\n    .{ .codepoint = \"U+30B3\", .width = 2 },\n    .{ .codepoint = \"U+30B4\", .width = 2 },\n    .{ .codepoint = \"U+30B5\", .width = 2 },\n    .{ .codepoint = \"U+30B6\", .width = 2 },\n    .{ .codepoint = \"U+30B7\", .width = 2 },\n    .{ .codepoint = \"U+30B8\", .width = 2 },\n    .{ .codepoint = \"U+30B9\", .width = 2 },\n    .{ .codepoint = \"U+30BA\", .width = 2 },\n    .{ .codepoint = \"U+30BB\", .width = 2 },\n    .{ .codepoint = \"U+30BC\", .width = 2 },\n    .{ .codepoint = \"U+30BD\", .width = 2 },\n    .{ .codepoint = \"U+30BE\", .width = 2 },\n    .{ .codepoint = \"U+30BF\", .width = 2 },\n    .{ .codepoint = \"U+30C0\", .width = 2 },\n    .{ .codepoint = \"U+30C1\", .width = 2 },\n    .{ .codepoint = \"U+30C2\", .width = 2 },\n    .{ .codepoint = \"U+30C3\", .width = 2 },\n    .{ .codepoint = \"U+30C4\", .width = 2 },\n    .{ .codepoint = \"U+30C5\", .width = 2 },\n    .{ .codepoint = \"U+30C6\", .width = 2 },\n    .{ .codepoint = \"U+30C7\", .width = 2 },\n    .{ .codepoint = \"U+30C8\", .width = 2 },\n    .{ .codepoint = \"U+30C9\", .width = 2 },\n    .{ .codepoint = \"U+30CA\", .width = 2 },\n    .{ .codepoint = \"U+30CB\", .width = 2 },\n    .{ .codepoint = \"U+30CC\", .width = 2 },\n    .{ .codepoint = \"U+30CD\", .width = 2 },\n    .{ .codepoint = \"U+30CE\", .width = 2 },\n    .{ .codepoint = \"U+30CF\", .width = 2 },\n    .{ .codepoint = \"U+30D0\", .width = 2 },\n    .{ .codepoint = \"U+30D1\", .width = 2 },\n    .{ .codepoint = \"U+30D2\", .width = 2 },\n    .{ .codepoint = \"U+30D3\", .width = 2 },\n    .{ .codepoint = \"U+30D4\", .width = 2 },\n    .{ .codepoint = \"U+30D5\", .width = 2 },\n    .{ .codepoint = \"U+30D6\", .width = 2 },\n    .{ .codepoint = \"U+30D7\", .width = 2 },\n    .{ .codepoint = \"U+30D8\", .width = 2 },\n    .{ .codepoint = \"U+30D9\", .width = 2 },\n    .{ .codepoint = \"U+30DA\", .width = 2 },\n    .{ .codepoint = \"U+30DB\", .width = 2 },\n    .{ .codepoint = \"U+30DC\", .width = 2 },\n    .{ .codepoint = \"U+30DD\", .width = 2 },\n    .{ .codepoint = \"U+30DE\", .width = 2 },\n    .{ .codepoint = \"U+30DF\", .width = 2 },\n    .{ .codepoint = \"U+30E0\", .width = 2 },\n    .{ .codepoint = \"U+30E1\", .width = 2 },\n    .{ .codepoint = \"U+30E2\", .width = 2 },\n    .{ .codepoint = \"U+30E3\", .width = 2 },\n    .{ .codepoint = \"U+30E4\", .width = 2 },\n    .{ .codepoint = \"U+30E5\", .width = 2 },\n    .{ .codepoint = \"U+30E6\", .width = 2 },\n    .{ .codepoint = \"U+30E7\", .width = 2 },\n    .{ .codepoint = \"U+30E8\", .width = 2 },\n    .{ .codepoint = \"U+30E9\", .width = 2 },\n    .{ .codepoint = \"U+30EA\", .width = 2 },\n    .{ .codepoint = \"U+30EB\", .width = 2 },\n    .{ .codepoint = \"U+30EC\", .width = 2 },\n    .{ .codepoint = \"U+30ED\", .width = 2 },\n    .{ .codepoint = \"U+30EE\", .width = 2 },\n    .{ .codepoint = \"U+30EF\", .width = 2 },\n    .{ .codepoint = \"U+30F0\", .width = 2 },\n    .{ .codepoint = \"U+30F1\", .width = 2 },\n    .{ .codepoint = \"U+30F2\", .width = 2 },\n    .{ .codepoint = \"U+30F3\", .width = 2 },\n    .{ .codepoint = \"U+30F4\", .width = 2 },\n    .{ .codepoint = \"U+30F5\", .width = 2 },\n    .{ .codepoint = \"U+30F6\", .width = 2 },\n    .{ .codepoint = \"U+30F7\", .width = 2 },\n    .{ .codepoint = \"U+30F8\", .width = 2 },\n    .{ .codepoint = \"U+30F9\", .width = 2 },\n    .{ .codepoint = \"U+30FA\", .width = 2 },\n    .{ .codepoint = \"U+30FB\", .width = 2 },\n    .{ .codepoint = \"U+30FC\", .width = 2 },\n    .{ .codepoint = \"U+30FD\", .width = 2 },\n    .{ .codepoint = \"U+30FE\", .width = 2 },\n    .{ .codepoint = \"U+30FF\", .width = 2 },\n    .{ .codepoint = \"U+3100\", .width = 1 },\n    .{ .codepoint = \"U+3101\", .width = 1 },\n    .{ .codepoint = \"U+3102\", .width = 1 },\n    .{ .codepoint = \"U+3103\", .width = 1 },\n    .{ .codepoint = \"U+3104\", .width = 1 },\n    .{ .codepoint = \"U+3105\", .width = 2 },\n    .{ .codepoint = \"U+3106\", .width = 2 },\n    .{ .codepoint = \"U+3107\", .width = 2 },\n    .{ .codepoint = \"U+3108\", .width = 2 },\n    .{ .codepoint = \"U+3109\", .width = 2 },\n    .{ .codepoint = \"U+310A\", .width = 2 },\n    .{ .codepoint = \"U+310B\", .width = 2 },\n    .{ .codepoint = \"U+310C\", .width = 2 },\n    .{ .codepoint = \"U+310D\", .width = 2 },\n    .{ .codepoint = \"U+310E\", .width = 2 },\n    .{ .codepoint = \"U+310F\", .width = 2 },\n    .{ .codepoint = \"U+3110\", .width = 2 },\n    .{ .codepoint = \"U+3111\", .width = 2 },\n    .{ .codepoint = \"U+3112\", .width = 2 },\n    .{ .codepoint = \"U+3113\", .width = 2 },\n    .{ .codepoint = \"U+3114\", .width = 2 },\n    .{ .codepoint = \"U+3115\", .width = 2 },\n    .{ .codepoint = \"U+3116\", .width = 2 },\n    .{ .codepoint = \"U+3117\", .width = 2 },\n    .{ .codepoint = \"U+3118\", .width = 2 },\n    .{ .codepoint = \"U+3119\", .width = 2 },\n    .{ .codepoint = \"U+311A\", .width = 2 },\n    .{ .codepoint = \"U+311B\", .width = 2 },\n    .{ .codepoint = \"U+311C\", .width = 2 },\n    .{ .codepoint = \"U+311D\", .width = 2 },\n    .{ .codepoint = \"U+311E\", .width = 2 },\n    .{ .codepoint = \"U+311F\", .width = 2 },\n    .{ .codepoint = \"U+3120\", .width = 2 },\n    .{ .codepoint = \"U+3121\", .width = 2 },\n    .{ .codepoint = \"U+3122\", .width = 2 },\n    .{ .codepoint = \"U+3123\", .width = 2 },\n    .{ .codepoint = \"U+3124\", .width = 2 },\n    .{ .codepoint = \"U+3125\", .width = 2 },\n    .{ .codepoint = \"U+3126\", .width = 2 },\n    .{ .codepoint = \"U+3127\", .width = 2 },\n    .{ .codepoint = \"U+3128\", .width = 2 },\n    .{ .codepoint = \"U+3129\", .width = 2 },\n    .{ .codepoint = \"U+312A\", .width = 2 },\n    .{ .codepoint = \"U+312B\", .width = 2 },\n    .{ .codepoint = \"U+312C\", .width = 2 },\n    .{ .codepoint = \"U+312D\", .width = 2 },\n    .{ .codepoint = \"U+312E\", .width = 2 },\n    .{ .codepoint = \"U+312F\", .width = 2 },\n    .{ .codepoint = \"U+4E00\", .width = 2 },\n    .{ .codepoint = \"U+4E01\", .width = 2 },\n    .{ .codepoint = \"U+4E02\", .width = 2 },\n    .{ .codepoint = \"U+4E03\", .width = 2 },\n    .{ .codepoint = \"U+4E04\", .width = 2 },\n    .{ .codepoint = \"U+4E05\", .width = 2 },\n    .{ .codepoint = \"U+4E06\", .width = 2 },\n    .{ .codepoint = \"U+4E07\", .width = 2 },\n    .{ .codepoint = \"U+4E08\", .width = 2 },\n    .{ .codepoint = \"U+4E09\", .width = 2 },\n    .{ .codepoint = \"U+4E0A\", .width = 2 },\n    .{ .codepoint = \"U+4E0B\", .width = 2 },\n    .{ .codepoint = \"U+4E0C\", .width = 2 },\n    .{ .codepoint = \"U+4E0D\", .width = 2 },\n    .{ .codepoint = \"U+4E0E\", .width = 2 },\n    .{ .codepoint = \"U+4E0F\", .width = 2 },\n    .{ .codepoint = \"U+4E10\", .width = 2 },\n    .{ .codepoint = \"U+4E11\", .width = 2 },\n    .{ .codepoint = \"U+4E12\", .width = 2 },\n    .{ .codepoint = \"U+4E13\", .width = 2 },\n    .{ .codepoint = \"U+4E14\", .width = 2 },\n    .{ .codepoint = \"U+4E15\", .width = 2 },\n    .{ .codepoint = \"U+4E16\", .width = 2 },\n    .{ .codepoint = \"U+4E17\", .width = 2 },\n    .{ .codepoint = \"U+4E18\", .width = 2 },\n    .{ .codepoint = \"U+4E19\", .width = 2 },\n    .{ .codepoint = \"U+4E1A\", .width = 2 },\n    .{ .codepoint = \"U+4E1B\", .width = 2 },\n    .{ .codepoint = \"U+4E1C\", .width = 2 },\n    .{ .codepoint = \"U+4E1D\", .width = 2 },\n    .{ .codepoint = \"U+4E1E\", .width = 2 },\n    .{ .codepoint = \"U+4E1F\", .width = 2 },\n    .{ .codepoint = \"U+4E20\", .width = 2 },\n    .{ .codepoint = \"U+4E21\", .width = 2 },\n    .{ .codepoint = \"U+4E22\", .width = 2 },\n    .{ .codepoint = \"U+4E23\", .width = 2 },\n    .{ .codepoint = \"U+4E24\", .width = 2 },\n    .{ .codepoint = \"U+4E25\", .width = 2 },\n    .{ .codepoint = \"U+4E26\", .width = 2 },\n    .{ .codepoint = \"U+4E27\", .width = 2 },\n    .{ .codepoint = \"U+4E28\", .width = 2 },\n    .{ .codepoint = \"U+4E29\", .width = 2 },\n    .{ .codepoint = \"U+4E2A\", .width = 2 },\n    .{ .codepoint = \"U+4E2B\", .width = 2 },\n    .{ .codepoint = \"U+4E2C\", .width = 2 },\n    .{ .codepoint = \"U+4E2D\", .width = 2 },\n    .{ .codepoint = \"U+4E2E\", .width = 2 },\n    .{ .codepoint = \"U+4E2F\", .width = 2 },\n    .{ .codepoint = \"U+4E30\", .width = 2 },\n    .{ .codepoint = \"U+4E31\", .width = 2 },\n    .{ .codepoint = \"U+4E32\", .width = 2 },\n    .{ .codepoint = \"U+4E33\", .width = 2 },\n    .{ .codepoint = \"U+4E34\", .width = 2 },\n    .{ .codepoint = \"U+4E35\", .width = 2 },\n    .{ .codepoint = \"U+4E36\", .width = 2 },\n    .{ .codepoint = \"U+4E37\", .width = 2 },\n    .{ .codepoint = \"U+4E38\", .width = 2 },\n    .{ .codepoint = \"U+4E39\", .width = 2 },\n    .{ .codepoint = \"U+4E3A\", .width = 2 },\n    .{ .codepoint = \"U+4E3B\", .width = 2 },\n    .{ .codepoint = \"U+4E3C\", .width = 2 },\n    .{ .codepoint = \"U+4E3D\", .width = 2 },\n    .{ .codepoint = \"U+4E3E\", .width = 2 },\n    .{ .codepoint = \"U+4E3F\", .width = 2 },\n    .{ .codepoint = \"U+4E40\", .width = 2 },\n    .{ .codepoint = \"U+4E41\", .width = 2 },\n    .{ .codepoint = \"U+4E42\", .width = 2 },\n    .{ .codepoint = \"U+4E43\", .width = 2 },\n    .{ .codepoint = \"U+4E44\", .width = 2 },\n    .{ .codepoint = \"U+4E45\", .width = 2 },\n    .{ .codepoint = \"U+4E46\", .width = 2 },\n    .{ .codepoint = \"U+4E47\", .width = 2 },\n    .{ .codepoint = \"U+4E48\", .width = 2 },\n    .{ .codepoint = \"U+4E49\", .width = 2 },\n    .{ .codepoint = \"U+4E4A\", .width = 2 },\n    .{ .codepoint = \"U+4E4B\", .width = 2 },\n    .{ .codepoint = \"U+4E4C\", .width = 2 },\n    .{ .codepoint = \"U+4E4D\", .width = 2 },\n    .{ .codepoint = \"U+4E4E\", .width = 2 },\n    .{ .codepoint = \"U+4E4F\", .width = 2 },\n    .{ .codepoint = \"U+4E50\", .width = 2 },\n    .{ .codepoint = \"U+4E51\", .width = 2 },\n    .{ .codepoint = \"U+4E52\", .width = 2 },\n    .{ .codepoint = \"U+4E53\", .width = 2 },\n    .{ .codepoint = \"U+4E54\", .width = 2 },\n    .{ .codepoint = \"U+4E55\", .width = 2 },\n    .{ .codepoint = \"U+4E56\", .width = 2 },\n    .{ .codepoint = \"U+4E57\", .width = 2 },\n    .{ .codepoint = \"U+4E58\", .width = 2 },\n    .{ .codepoint = \"U+4E59\", .width = 2 },\n    .{ .codepoint = \"U+4E5A\", .width = 2 },\n    .{ .codepoint = \"U+4E5B\", .width = 2 },\n    .{ .codepoint = \"U+4E5C\", .width = 2 },\n    .{ .codepoint = \"U+4E5D\", .width = 2 },\n    .{ .codepoint = \"U+4E5E\", .width = 2 },\n    .{ .codepoint = \"U+4E5F\", .width = 2 },\n    .{ .codepoint = \"U+4E60\", .width = 2 },\n    .{ .codepoint = \"U+4E61\", .width = 2 },\n    .{ .codepoint = \"U+4E62\", .width = 2 },\n    .{ .codepoint = \"U+4E63\", .width = 2 },\n    .{ .codepoint = \"U+4E64\", .width = 2 },\n    .{ .codepoint = \"U+4E65\", .width = 2 },\n    .{ .codepoint = \"U+4E66\", .width = 2 },\n    .{ .codepoint = \"U+4E67\", .width = 2 },\n    .{ .codepoint = \"U+4E68\", .width = 2 },\n    .{ .codepoint = \"U+4E69\", .width = 2 },\n    .{ .codepoint = \"U+4E6A\", .width = 2 },\n    .{ .codepoint = \"U+4E6B\", .width = 2 },\n    .{ .codepoint = \"U+4E6C\", .width = 2 },\n    .{ .codepoint = \"U+4E6D\", .width = 2 },\n    .{ .codepoint = \"U+4E6E\", .width = 2 },\n    .{ .codepoint = \"U+4E6F\", .width = 2 },\n    .{ .codepoint = \"U+4E70\", .width = 2 },\n    .{ .codepoint = \"U+4E71\", .width = 2 },\n    .{ .codepoint = \"U+4E72\", .width = 2 },\n    .{ .codepoint = \"U+4E73\", .width = 2 },\n    .{ .codepoint = \"U+4E74\", .width = 2 },\n    .{ .codepoint = \"U+4E75\", .width = 2 },\n    .{ .codepoint = \"U+4E76\", .width = 2 },\n    .{ .codepoint = \"U+4E77\", .width = 2 },\n    .{ .codepoint = \"U+4E78\", .width = 2 },\n    .{ .codepoint = \"U+4E79\", .width = 2 },\n    .{ .codepoint = \"U+4E7A\", .width = 2 },\n    .{ .codepoint = \"U+4E7B\", .width = 2 },\n    .{ .codepoint = \"U+4E7C\", .width = 2 },\n    .{ .codepoint = \"U+4E7D\", .width = 2 },\n    .{ .codepoint = \"U+4E7E\", .width = 2 },\n    .{ .codepoint = \"U+4E7F\", .width = 2 },\n    .{ .codepoint = \"U+4E80\", .width = 2 },\n    .{ .codepoint = \"U+4E81\", .width = 2 },\n    .{ .codepoint = \"U+4E82\", .width = 2 },\n    .{ .codepoint = \"U+4E83\", .width = 2 },\n    .{ .codepoint = \"U+4E84\", .width = 2 },\n    .{ .codepoint = \"U+4E85\", .width = 2 },\n    .{ .codepoint = \"U+4E86\", .width = 2 },\n    .{ .codepoint = \"U+4E87\", .width = 2 },\n    .{ .codepoint = \"U+4E88\", .width = 2 },\n    .{ .codepoint = \"U+4E89\", .width = 2 },\n    .{ .codepoint = \"U+4E8A\", .width = 2 },\n    .{ .codepoint = \"U+4E8B\", .width = 2 },\n    .{ .codepoint = \"U+4E8C\", .width = 2 },\n    .{ .codepoint = \"U+4E8D\", .width = 2 },\n    .{ .codepoint = \"U+4E8E\", .width = 2 },\n    .{ .codepoint = \"U+4E8F\", .width = 2 },\n    .{ .codepoint = \"U+4E90\", .width = 2 },\n    .{ .codepoint = \"U+4E91\", .width = 2 },\n    .{ .codepoint = \"U+4E92\", .width = 2 },\n    .{ .codepoint = \"U+4E93\", .width = 2 },\n    .{ .codepoint = \"U+4E94\", .width = 2 },\n    .{ .codepoint = \"U+4E95\", .width = 2 },\n    .{ .codepoint = \"U+4E96\", .width = 2 },\n    .{ .codepoint = \"U+4E97\", .width = 2 },\n    .{ .codepoint = \"U+4E98\", .width = 2 },\n    .{ .codepoint = \"U+4E99\", .width = 2 },\n    .{ .codepoint = \"U+4E9A\", .width = 2 },\n    .{ .codepoint = \"U+4E9B\", .width = 2 },\n    .{ .codepoint = \"U+4E9C\", .width = 2 },\n    .{ .codepoint = \"U+4E9D\", .width = 2 },\n    .{ .codepoint = \"U+4E9E\", .width = 2 },\n    .{ .codepoint = \"U+4E9F\", .width = 2 },\n    .{ .codepoint = \"U+4EA0\", .width = 2 },\n    .{ .codepoint = \"U+4EA1\", .width = 2 },\n    .{ .codepoint = \"U+4EA2\", .width = 2 },\n    .{ .codepoint = \"U+4EA3\", .width = 2 },\n    .{ .codepoint = \"U+4EA4\", .width = 2 },\n    .{ .codepoint = \"U+4EA5\", .width = 2 },\n    .{ .codepoint = \"U+4EA6\", .width = 2 },\n    .{ .codepoint = \"U+4EA7\", .width = 2 },\n    .{ .codepoint = \"U+4EA8\", .width = 2 },\n    .{ .codepoint = \"U+4EA9\", .width = 2 },\n    .{ .codepoint = \"U+4EAA\", .width = 2 },\n    .{ .codepoint = \"U+4EAB\", .width = 2 },\n    .{ .codepoint = \"U+4EAC\", .width = 2 },\n    .{ .codepoint = \"U+4EAD\", .width = 2 },\n    .{ .codepoint = \"U+4EAE\", .width = 2 },\n    .{ .codepoint = \"U+4EAF\", .width = 2 },\n    .{ .codepoint = \"U+4EB0\", .width = 2 },\n    .{ .codepoint = \"U+4EB1\", .width = 2 },\n    .{ .codepoint = \"U+4EB2\", .width = 2 },\n    .{ .codepoint = \"U+4EB3\", .width = 2 },\n    .{ .codepoint = \"U+4EB4\", .width = 2 },\n    .{ .codepoint = \"U+4EB5\", .width = 2 },\n    .{ .codepoint = \"U+4EB6\", .width = 2 },\n    .{ .codepoint = \"U+4EB7\", .width = 2 },\n    .{ .codepoint = \"U+4EB8\", .width = 2 },\n    .{ .codepoint = \"U+4EB9\", .width = 2 },\n    .{ .codepoint = \"U+4EBA\", .width = 2 },\n    .{ .codepoint = \"U+4EBB\", .width = 2 },\n    .{ .codepoint = \"U+4EBC\", .width = 2 },\n    .{ .codepoint = \"U+4EBD\", .width = 2 },\n    .{ .codepoint = \"U+4EBE\", .width = 2 },\n    .{ .codepoint = \"U+4EBF\", .width = 2 },\n    .{ .codepoint = \"U+4EC0\", .width = 2 },\n    .{ .codepoint = \"U+4EC1\", .width = 2 },\n    .{ .codepoint = \"U+4EC2\", .width = 2 },\n    .{ .codepoint = \"U+4EC3\", .width = 2 },\n    .{ .codepoint = \"U+4EC4\", .width = 2 },\n    .{ .codepoint = \"U+4EC5\", .width = 2 },\n    .{ .codepoint = \"U+4EC6\", .width = 2 },\n    .{ .codepoint = \"U+4EC7\", .width = 2 },\n    .{ .codepoint = \"U+4EC8\", .width = 2 },\n    .{ .codepoint = \"U+4EC9\", .width = 2 },\n    .{ .codepoint = \"U+4ECA\", .width = 2 },\n    .{ .codepoint = \"U+4ECB\", .width = 2 },\n    .{ .codepoint = \"U+4ECC\", .width = 2 },\n    .{ .codepoint = \"U+4ECD\", .width = 2 },\n    .{ .codepoint = \"U+4ECE\", .width = 2 },\n    .{ .codepoint = \"U+4ECF\", .width = 2 },\n    .{ .codepoint = \"U+4ED0\", .width = 2 },\n    .{ .codepoint = \"U+4ED1\", .width = 2 },\n    .{ .codepoint = \"U+4ED2\", .width = 2 },\n    .{ .codepoint = \"U+4ED3\", .width = 2 },\n    .{ .codepoint = \"U+4ED4\", .width = 2 },\n    .{ .codepoint = \"U+4ED5\", .width = 2 },\n    .{ .codepoint = \"U+4ED6\", .width = 2 },\n    .{ .codepoint = \"U+4ED7\", .width = 2 },\n    .{ .codepoint = \"U+4ED8\", .width = 2 },\n    .{ .codepoint = \"U+4ED9\", .width = 2 },\n    .{ .codepoint = \"U+4EDA\", .width = 2 },\n    .{ .codepoint = \"U+4EDB\", .width = 2 },\n    .{ .codepoint = \"U+4EDC\", .width = 2 },\n    .{ .codepoint = \"U+4EDD\", .width = 2 },\n    .{ .codepoint = \"U+4EDE\", .width = 2 },\n    .{ .codepoint = \"U+4EDF\", .width = 2 },\n    .{ .codepoint = \"U+4EE0\", .width = 2 },\n    .{ .codepoint = \"U+4EE1\", .width = 2 },\n    .{ .codepoint = \"U+4EE2\", .width = 2 },\n    .{ .codepoint = \"U+4EE3\", .width = 2 },\n    .{ .codepoint = \"U+4EE4\", .width = 2 },\n    .{ .codepoint = \"U+4EE5\", .width = 2 },\n    .{ .codepoint = \"U+4EE6\", .width = 2 },\n    .{ .codepoint = \"U+4EE7\", .width = 2 },\n    .{ .codepoint = \"U+4EE8\", .width = 2 },\n    .{ .codepoint = \"U+4EE9\", .width = 2 },\n    .{ .codepoint = \"U+4EEA\", .width = 2 },\n    .{ .codepoint = \"U+4EEB\", .width = 2 },\n    .{ .codepoint = \"U+4EEC\", .width = 2 },\n    .{ .codepoint = \"U+4EED\", .width = 2 },\n    .{ .codepoint = \"U+4EEE\", .width = 2 },\n    .{ .codepoint = \"U+4EEF\", .width = 2 },\n    .{ .codepoint = \"U+4EF0\", .width = 2 },\n    .{ .codepoint = \"U+4EF1\", .width = 2 },\n    .{ .codepoint = \"U+4EF2\", .width = 2 },\n    .{ .codepoint = \"U+4EF3\", .width = 2 },\n    .{ .codepoint = \"U+4EF4\", .width = 2 },\n    .{ .codepoint = \"U+4EF5\", .width = 2 },\n    .{ .codepoint = \"U+4EF6\", .width = 2 },\n    .{ .codepoint = \"U+4EF7\", .width = 2 },\n    .{ .codepoint = \"U+4EF8\", .width = 2 },\n    .{ .codepoint = \"U+4EF9\", .width = 2 },\n    .{ .codepoint = \"U+4EFA\", .width = 2 },\n    .{ .codepoint = \"U+4EFB\", .width = 2 },\n    .{ .codepoint = \"U+4EFC\", .width = 2 },\n    .{ .codepoint = \"U+4EFD\", .width = 2 },\n    .{ .codepoint = \"U+4EFE\", .width = 2 },\n    .{ .codepoint = \"U+4EFF\", .width = 2 },\n    .{ .codepoint = \"U+AC00\", .width = 2 },\n    .{ .codepoint = \"U+AC01\", .width = 2 },\n    .{ .codepoint = \"U+AC02\", .width = 2 },\n    .{ .codepoint = \"U+AC03\", .width = 2 },\n    .{ .codepoint = \"U+AC04\", .width = 2 },\n    .{ .codepoint = \"U+AC05\", .width = 2 },\n    .{ .codepoint = \"U+AC06\", .width = 2 },\n    .{ .codepoint = \"U+AC07\", .width = 2 },\n    .{ .codepoint = \"U+AC08\", .width = 2 },\n    .{ .codepoint = \"U+AC09\", .width = 2 },\n    .{ .codepoint = \"U+AC0A\", .width = 2 },\n    .{ .codepoint = \"U+AC0B\", .width = 2 },\n    .{ .codepoint = \"U+AC0C\", .width = 2 },\n    .{ .codepoint = \"U+AC0D\", .width = 2 },\n    .{ .codepoint = \"U+AC0E\", .width = 2 },\n    .{ .codepoint = \"U+AC0F\", .width = 2 },\n    .{ .codepoint = \"U+AC10\", .width = 2 },\n    .{ .codepoint = \"U+AC11\", .width = 2 },\n    .{ .codepoint = \"U+AC12\", .width = 2 },\n    .{ .codepoint = \"U+AC13\", .width = 2 },\n    .{ .codepoint = \"U+AC14\", .width = 2 },\n    .{ .codepoint = \"U+AC15\", .width = 2 },\n    .{ .codepoint = \"U+AC16\", .width = 2 },\n    .{ .codepoint = \"U+AC17\", .width = 2 },\n    .{ .codepoint = \"U+AC18\", .width = 2 },\n    .{ .codepoint = \"U+AC19\", .width = 2 },\n    .{ .codepoint = \"U+AC1A\", .width = 2 },\n    .{ .codepoint = \"U+AC1B\", .width = 2 },\n    .{ .codepoint = \"U+AC1C\", .width = 2 },\n    .{ .codepoint = \"U+AC1D\", .width = 2 },\n    .{ .codepoint = \"U+AC1E\", .width = 2 },\n    .{ .codepoint = \"U+AC1F\", .width = 2 },\n    .{ .codepoint = \"U+AC20\", .width = 2 },\n    .{ .codepoint = \"U+AC21\", .width = 2 },\n    .{ .codepoint = \"U+AC22\", .width = 2 },\n    .{ .codepoint = \"U+AC23\", .width = 2 },\n    .{ .codepoint = \"U+AC24\", .width = 2 },\n    .{ .codepoint = \"U+AC25\", .width = 2 },\n    .{ .codepoint = \"U+AC26\", .width = 2 },\n    .{ .codepoint = \"U+AC27\", .width = 2 },\n    .{ .codepoint = \"U+AC28\", .width = 2 },\n    .{ .codepoint = \"U+AC29\", .width = 2 },\n    .{ .codepoint = \"U+AC2A\", .width = 2 },\n    .{ .codepoint = \"U+AC2B\", .width = 2 },\n    .{ .codepoint = \"U+AC2C\", .width = 2 },\n    .{ .codepoint = \"U+AC2D\", .width = 2 },\n    .{ .codepoint = \"U+AC2E\", .width = 2 },\n    .{ .codepoint = \"U+AC2F\", .width = 2 },\n    .{ .codepoint = \"U+AC30\", .width = 2 },\n    .{ .codepoint = \"U+AC31\", .width = 2 },\n    .{ .codepoint = \"U+AC32\", .width = 2 },\n    .{ .codepoint = \"U+AC33\", .width = 2 },\n    .{ .codepoint = \"U+AC34\", .width = 2 },\n    .{ .codepoint = \"U+AC35\", .width = 2 },\n    .{ .codepoint = \"U+AC36\", .width = 2 },\n    .{ .codepoint = \"U+AC37\", .width = 2 },\n    .{ .codepoint = \"U+AC38\", .width = 2 },\n    .{ .codepoint = \"U+AC39\", .width = 2 },\n    .{ .codepoint = \"U+AC3A\", .width = 2 },\n    .{ .codepoint = \"U+AC3B\", .width = 2 },\n    .{ .codepoint = \"U+AC3C\", .width = 2 },\n    .{ .codepoint = \"U+AC3D\", .width = 2 },\n    .{ .codepoint = \"U+AC3E\", .width = 2 },\n    .{ .codepoint = \"U+AC3F\", .width = 2 },\n    .{ .codepoint = \"U+AC40\", .width = 2 },\n    .{ .codepoint = \"U+AC41\", .width = 2 },\n    .{ .codepoint = \"U+AC42\", .width = 2 },\n    .{ .codepoint = \"U+AC43\", .width = 2 },\n    .{ .codepoint = \"U+AC44\", .width = 2 },\n    .{ .codepoint = \"U+AC45\", .width = 2 },\n    .{ .codepoint = \"U+AC46\", .width = 2 },\n    .{ .codepoint = \"U+AC47\", .width = 2 },\n    .{ .codepoint = \"U+AC48\", .width = 2 },\n    .{ .codepoint = \"U+AC49\", .width = 2 },\n    .{ .codepoint = \"U+AC4A\", .width = 2 },\n    .{ .codepoint = \"U+AC4B\", .width = 2 },\n    .{ .codepoint = \"U+AC4C\", .width = 2 },\n    .{ .codepoint = \"U+AC4D\", .width = 2 },\n    .{ .codepoint = \"U+AC4E\", .width = 2 },\n    .{ .codepoint = \"U+AC4F\", .width = 2 },\n    .{ .codepoint = \"U+AC50\", .width = 2 },\n    .{ .codepoint = \"U+AC51\", .width = 2 },\n    .{ .codepoint = \"U+AC52\", .width = 2 },\n    .{ .codepoint = \"U+AC53\", .width = 2 },\n    .{ .codepoint = \"U+AC54\", .width = 2 },\n    .{ .codepoint = \"U+AC55\", .width = 2 },\n    .{ .codepoint = \"U+AC56\", .width = 2 },\n    .{ .codepoint = \"U+AC57\", .width = 2 },\n    .{ .codepoint = \"U+AC58\", .width = 2 },\n    .{ .codepoint = \"U+AC59\", .width = 2 },\n    .{ .codepoint = \"U+AC5A\", .width = 2 },\n    .{ .codepoint = \"U+AC5B\", .width = 2 },\n    .{ .codepoint = \"U+AC5C\", .width = 2 },\n    .{ .codepoint = \"U+AC5D\", .width = 2 },\n    .{ .codepoint = \"U+AC5E\", .width = 2 },\n    .{ .codepoint = \"U+AC5F\", .width = 2 },\n    .{ .codepoint = \"U+AC60\", .width = 2 },\n    .{ .codepoint = \"U+AC61\", .width = 2 },\n    .{ .codepoint = \"U+AC62\", .width = 2 },\n    .{ .codepoint = \"U+AC63\", .width = 2 },\n    .{ .codepoint = \"U+AC64\", .width = 2 },\n    .{ .codepoint = \"U+AC65\", .width = 2 },\n    .{ .codepoint = \"U+AC66\", .width = 2 },\n    .{ .codepoint = \"U+AC67\", .width = 2 },\n    .{ .codepoint = \"U+AC68\", .width = 2 },\n    .{ .codepoint = \"U+AC69\", .width = 2 },\n    .{ .codepoint = \"U+AC6A\", .width = 2 },\n    .{ .codepoint = \"U+AC6B\", .width = 2 },\n    .{ .codepoint = \"U+AC6C\", .width = 2 },\n    .{ .codepoint = \"U+AC6D\", .width = 2 },\n    .{ .codepoint = \"U+AC6E\", .width = 2 },\n    .{ .codepoint = \"U+AC6F\", .width = 2 },\n    .{ .codepoint = \"U+AC70\", .width = 2 },\n    .{ .codepoint = \"U+AC71\", .width = 2 },\n    .{ .codepoint = \"U+AC72\", .width = 2 },\n    .{ .codepoint = \"U+AC73\", .width = 2 },\n    .{ .codepoint = \"U+AC74\", .width = 2 },\n    .{ .codepoint = \"U+AC75\", .width = 2 },\n    .{ .codepoint = \"U+AC76\", .width = 2 },\n    .{ .codepoint = \"U+AC77\", .width = 2 },\n    .{ .codepoint = \"U+AC78\", .width = 2 },\n    .{ .codepoint = \"U+AC79\", .width = 2 },\n    .{ .codepoint = \"U+AC7A\", .width = 2 },\n    .{ .codepoint = \"U+AC7B\", .width = 2 },\n    .{ .codepoint = \"U+AC7C\", .width = 2 },\n    .{ .codepoint = \"U+AC7D\", .width = 2 },\n    .{ .codepoint = \"U+AC7E\", .width = 2 },\n    .{ .codepoint = \"U+AC7F\", .width = 2 },\n    .{ .codepoint = \"U+AC80\", .width = 2 },\n    .{ .codepoint = \"U+AC81\", .width = 2 },\n    .{ .codepoint = \"U+AC82\", .width = 2 },\n    .{ .codepoint = \"U+AC83\", .width = 2 },\n    .{ .codepoint = \"U+AC84\", .width = 2 },\n    .{ .codepoint = \"U+AC85\", .width = 2 },\n    .{ .codepoint = \"U+AC86\", .width = 2 },\n    .{ .codepoint = \"U+AC87\", .width = 2 },\n    .{ .codepoint = \"U+AC88\", .width = 2 },\n    .{ .codepoint = \"U+AC89\", .width = 2 },\n    .{ .codepoint = \"U+AC8A\", .width = 2 },\n    .{ .codepoint = \"U+AC8B\", .width = 2 },\n    .{ .codepoint = \"U+AC8C\", .width = 2 },\n    .{ .codepoint = \"U+AC8D\", .width = 2 },\n    .{ .codepoint = \"U+AC8E\", .width = 2 },\n    .{ .codepoint = \"U+AC8F\", .width = 2 },\n    .{ .codepoint = \"U+AC90\", .width = 2 },\n    .{ .codepoint = \"U+AC91\", .width = 2 },\n    .{ .codepoint = \"U+AC92\", .width = 2 },\n    .{ .codepoint = \"U+AC93\", .width = 2 },\n    .{ .codepoint = \"U+AC94\", .width = 2 },\n    .{ .codepoint = \"U+AC95\", .width = 2 },\n    .{ .codepoint = \"U+AC96\", .width = 2 },\n    .{ .codepoint = \"U+AC97\", .width = 2 },\n    .{ .codepoint = \"U+AC98\", .width = 2 },\n    .{ .codepoint = \"U+AC99\", .width = 2 },\n    .{ .codepoint = \"U+AC9A\", .width = 2 },\n    .{ .codepoint = \"U+AC9B\", .width = 2 },\n    .{ .codepoint = \"U+AC9C\", .width = 2 },\n    .{ .codepoint = \"U+AC9D\", .width = 2 },\n    .{ .codepoint = \"U+AC9E\", .width = 2 },\n    .{ .codepoint = \"U+AC9F\", .width = 2 },\n    .{ .codepoint = \"U+ACA0\", .width = 2 },\n    .{ .codepoint = \"U+ACA1\", .width = 2 },\n    .{ .codepoint = \"U+ACA2\", .width = 2 },\n    .{ .codepoint = \"U+ACA3\", .width = 2 },\n    .{ .codepoint = \"U+ACA4\", .width = 2 },\n    .{ .codepoint = \"U+ACA5\", .width = 2 },\n    .{ .codepoint = \"U+ACA6\", .width = 2 },\n    .{ .codepoint = \"U+ACA7\", .width = 2 },\n    .{ .codepoint = \"U+ACA8\", .width = 2 },\n    .{ .codepoint = \"U+ACA9\", .width = 2 },\n    .{ .codepoint = \"U+ACAA\", .width = 2 },\n    .{ .codepoint = \"U+ACAB\", .width = 2 },\n    .{ .codepoint = \"U+ACAC\", .width = 2 },\n    .{ .codepoint = \"U+ACAD\", .width = 2 },\n    .{ .codepoint = \"U+ACAE\", .width = 2 },\n    .{ .codepoint = \"U+ACAF\", .width = 2 },\n    .{ .codepoint = \"U+ACB0\", .width = 2 },\n    .{ .codepoint = \"U+ACB1\", .width = 2 },\n    .{ .codepoint = \"U+ACB2\", .width = 2 },\n    .{ .codepoint = \"U+ACB3\", .width = 2 },\n    .{ .codepoint = \"U+ACB4\", .width = 2 },\n    .{ .codepoint = \"U+ACB5\", .width = 2 },\n    .{ .codepoint = \"U+ACB6\", .width = 2 },\n    .{ .codepoint = \"U+ACB7\", .width = 2 },\n    .{ .codepoint = \"U+ACB8\", .width = 2 },\n    .{ .codepoint = \"U+ACB9\", .width = 2 },\n    .{ .codepoint = \"U+ACBA\", .width = 2 },\n    .{ .codepoint = \"U+ACBB\", .width = 2 },\n    .{ .codepoint = \"U+ACBC\", .width = 2 },\n    .{ .codepoint = \"U+ACBD\", .width = 2 },\n    .{ .codepoint = \"U+ACBE\", .width = 2 },\n    .{ .codepoint = \"U+ACBF\", .width = 2 },\n    .{ .codepoint = \"U+ACC0\", .width = 2 },\n    .{ .codepoint = \"U+ACC1\", .width = 2 },\n    .{ .codepoint = \"U+ACC2\", .width = 2 },\n    .{ .codepoint = \"U+ACC3\", .width = 2 },\n    .{ .codepoint = \"U+ACC4\", .width = 2 },\n    .{ .codepoint = \"U+ACC5\", .width = 2 },\n    .{ .codepoint = \"U+ACC6\", .width = 2 },\n    .{ .codepoint = \"U+ACC7\", .width = 2 },\n    .{ .codepoint = \"U+ACC8\", .width = 2 },\n    .{ .codepoint = \"U+ACC9\", .width = 2 },\n    .{ .codepoint = \"U+ACCA\", .width = 2 },\n    .{ .codepoint = \"U+ACCB\", .width = 2 },\n    .{ .codepoint = \"U+ACCC\", .width = 2 },\n    .{ .codepoint = \"U+ACCD\", .width = 2 },\n    .{ .codepoint = \"U+ACCE\", .width = 2 },\n    .{ .codepoint = \"U+ACCF\", .width = 2 },\n    .{ .codepoint = \"U+ACD0\", .width = 2 },\n    .{ .codepoint = \"U+ACD1\", .width = 2 },\n    .{ .codepoint = \"U+ACD2\", .width = 2 },\n    .{ .codepoint = \"U+ACD3\", .width = 2 },\n    .{ .codepoint = \"U+ACD4\", .width = 2 },\n    .{ .codepoint = \"U+ACD5\", .width = 2 },\n    .{ .codepoint = \"U+ACD6\", .width = 2 },\n    .{ .codepoint = \"U+ACD7\", .width = 2 },\n    .{ .codepoint = \"U+ACD8\", .width = 2 },\n    .{ .codepoint = \"U+ACD9\", .width = 2 },\n    .{ .codepoint = \"U+ACDA\", .width = 2 },\n    .{ .codepoint = \"U+ACDB\", .width = 2 },\n    .{ .codepoint = \"U+ACDC\", .width = 2 },\n    .{ .codepoint = \"U+ACDD\", .width = 2 },\n    .{ .codepoint = \"U+ACDE\", .width = 2 },\n    .{ .codepoint = \"U+ACDF\", .width = 2 },\n    .{ .codepoint = \"U+ACE0\", .width = 2 },\n    .{ .codepoint = \"U+ACE1\", .width = 2 },\n    .{ .codepoint = \"U+ACE2\", .width = 2 },\n    .{ .codepoint = \"U+ACE3\", .width = 2 },\n    .{ .codepoint = \"U+ACE4\", .width = 2 },\n    .{ .codepoint = \"U+ACE5\", .width = 2 },\n    .{ .codepoint = \"U+ACE6\", .width = 2 },\n    .{ .codepoint = \"U+ACE7\", .width = 2 },\n    .{ .codepoint = \"U+ACE8\", .width = 2 },\n    .{ .codepoint = \"U+ACE9\", .width = 2 },\n    .{ .codepoint = \"U+ACEA\", .width = 2 },\n    .{ .codepoint = \"U+ACEB\", .width = 2 },\n    .{ .codepoint = \"U+ACEC\", .width = 2 },\n    .{ .codepoint = \"U+ACED\", .width = 2 },\n    .{ .codepoint = \"U+ACEE\", .width = 2 },\n    .{ .codepoint = \"U+ACEF\", .width = 2 },\n    .{ .codepoint = \"U+ACF0\", .width = 2 },\n    .{ .codepoint = \"U+ACF1\", .width = 2 },\n    .{ .codepoint = \"U+ACF2\", .width = 2 },\n    .{ .codepoint = \"U+ACF3\", .width = 2 },\n    .{ .codepoint = \"U+ACF4\", .width = 2 },\n    .{ .codepoint = \"U+ACF5\", .width = 2 },\n    .{ .codepoint = \"U+ACF6\", .width = 2 },\n    .{ .codepoint = \"U+ACF7\", .width = 2 },\n    .{ .codepoint = \"U+ACF8\", .width = 2 },\n    .{ .codepoint = \"U+ACF9\", .width = 2 },\n    .{ .codepoint = \"U+ACFA\", .width = 2 },\n    .{ .codepoint = \"U+ACFB\", .width = 2 },\n    .{ .codepoint = \"U+ACFC\", .width = 2 },\n    .{ .codepoint = \"U+ACFD\", .width = 2 },\n    .{ .codepoint = \"U+ACFE\", .width = 2 },\n    .{ .codepoint = \"U+ACFF\", .width = 2 },\n    .{ .codepoint = \"U+1F300\", .width = 2 },\n    .{ .codepoint = \"U+1F301\", .width = 2 },\n    .{ .codepoint = \"U+1F302\", .width = 2 },\n    .{ .codepoint = \"U+1F303\", .width = 2 },\n    .{ .codepoint = \"U+1F304\", .width = 2 },\n    .{ .codepoint = \"U+1F305\", .width = 2 },\n    .{ .codepoint = \"U+1F306\", .width = 2 },\n    .{ .codepoint = \"U+1F307\", .width = 2 },\n    .{ .codepoint = \"U+1F308\", .width = 2 },\n    .{ .codepoint = \"U+1F309\", .width = 2 },\n    .{ .codepoint = \"U+1F30A\", .width = 2 },\n    .{ .codepoint = \"U+1F30B\", .width = 2 },\n    .{ .codepoint = \"U+1F30C\", .width = 2 },\n    .{ .codepoint = \"U+1F30D\", .width = 2 },\n    .{ .codepoint = \"U+1F30E\", .width = 2 },\n    .{ .codepoint = \"U+1F30F\", .width = 2 },\n    .{ .codepoint = \"U+1F310\", .width = 2 },\n    .{ .codepoint = \"U+1F311\", .width = 2 },\n    .{ .codepoint = \"U+1F312\", .width = 2 },\n    .{ .codepoint = \"U+1F313\", .width = 2 },\n    .{ .codepoint = \"U+1F314\", .width = 2 },\n    .{ .codepoint = \"U+1F315\", .width = 2 },\n    .{ .codepoint = \"U+1F316\", .width = 2 },\n    .{ .codepoint = \"U+1F317\", .width = 2 },\n    .{ .codepoint = \"U+1F318\", .width = 2 },\n    .{ .codepoint = \"U+1F319\", .width = 2 },\n    .{ .codepoint = \"U+1F31A\", .width = 2 },\n    .{ .codepoint = \"U+1F31B\", .width = 2 },\n    .{ .codepoint = \"U+1F31C\", .width = 2 },\n    .{ .codepoint = \"U+1F31D\", .width = 2 },\n    .{ .codepoint = \"U+1F31E\", .width = 2 },\n    .{ .codepoint = \"U+1F31F\", .width = 2 },\n    .{ .codepoint = \"U+1F320\", .width = 2 },\n    .{ .codepoint = \"U+1F321\", .width = 1 },\n    .{ .codepoint = \"U+1F322\", .width = 1 },\n    .{ .codepoint = \"U+1F323\", .width = 1 },\n    .{ .codepoint = \"U+1F324\", .width = 1 },\n    .{ .codepoint = \"U+1F325\", .width = 1 },\n    .{ .codepoint = \"U+1F326\", .width = 1 },\n    .{ .codepoint = \"U+1F327\", .width = 1 },\n    .{ .codepoint = \"U+1F328\", .width = 1 },\n    .{ .codepoint = \"U+1F329\", .width = 1 },\n    .{ .codepoint = \"U+1F32A\", .width = 1 },\n    .{ .codepoint = \"U+1F32B\", .width = 1 },\n    .{ .codepoint = \"U+1F32C\", .width = 1 },\n    .{ .codepoint = \"U+1F32D\", .width = 2 },\n    .{ .codepoint = \"U+1F32E\", .width = 2 },\n    .{ .codepoint = \"U+1F32F\", .width = 2 },\n    .{ .codepoint = \"U+1F330\", .width = 2 },\n    .{ .codepoint = \"U+1F331\", .width = 2 },\n    .{ .codepoint = \"U+1F332\", .width = 2 },\n    .{ .codepoint = \"U+1F333\", .width = 2 },\n    .{ .codepoint = \"U+1F334\", .width = 2 },\n    .{ .codepoint = \"U+1F335\", .width = 2 },\n    .{ .codepoint = \"U+1F336\", .width = 1 },\n    .{ .codepoint = \"U+1F337\", .width = 2 },\n    .{ .codepoint = \"U+1F338\", .width = 2 },\n    .{ .codepoint = \"U+1F339\", .width = 2 },\n    .{ .codepoint = \"U+1F33A\", .width = 2 },\n    .{ .codepoint = \"U+1F33B\", .width = 2 },\n    .{ .codepoint = \"U+1F33C\", .width = 2 },\n    .{ .codepoint = \"U+1F33D\", .width = 2 },\n    .{ .codepoint = \"U+1F33E\", .width = 2 },\n    .{ .codepoint = \"U+1F33F\", .width = 2 },\n    .{ .codepoint = \"U+1F340\", .width = 2 },\n    .{ .codepoint = \"U+1F341\", .width = 2 },\n    .{ .codepoint = \"U+1F342\", .width = 2 },\n    .{ .codepoint = \"U+1F343\", .width = 2 },\n    .{ .codepoint = \"U+1F344\", .width = 2 },\n    .{ .codepoint = \"U+1F345\", .width = 2 },\n    .{ .codepoint = \"U+1F346\", .width = 2 },\n    .{ .codepoint = \"U+1F347\", .width = 2 },\n    .{ .codepoint = \"U+1F348\", .width = 2 },\n    .{ .codepoint = \"U+1F349\", .width = 2 },\n    .{ .codepoint = \"U+1F34A\", .width = 2 },\n    .{ .codepoint = \"U+1F34B\", .width = 2 },\n    .{ .codepoint = \"U+1F34C\", .width = 2 },\n    .{ .codepoint = \"U+1F34D\", .width = 2 },\n    .{ .codepoint = \"U+1F34E\", .width = 2 },\n    .{ .codepoint = \"U+1F34F\", .width = 2 },\n    .{ .codepoint = \"U+1F350\", .width = 2 },\n    .{ .codepoint = \"U+1F351\", .width = 2 },\n    .{ .codepoint = \"U+1F352\", .width = 2 },\n    .{ .codepoint = \"U+1F353\", .width = 2 },\n    .{ .codepoint = \"U+1F354\", .width = 2 },\n    .{ .codepoint = \"U+1F355\", .width = 2 },\n    .{ .codepoint = \"U+1F356\", .width = 2 },\n    .{ .codepoint = \"U+1F357\", .width = 2 },\n    .{ .codepoint = \"U+1F358\", .width = 2 },\n    .{ .codepoint = \"U+1F359\", .width = 2 },\n    .{ .codepoint = \"U+1F35A\", .width = 2 },\n    .{ .codepoint = \"U+1F35B\", .width = 2 },\n    .{ .codepoint = \"U+1F35C\", .width = 2 },\n    .{ .codepoint = \"U+1F35D\", .width = 2 },\n    .{ .codepoint = \"U+1F35E\", .width = 2 },\n    .{ .codepoint = \"U+1F35F\", .width = 2 },\n    .{ .codepoint = \"U+1F360\", .width = 2 },\n    .{ .codepoint = \"U+1F361\", .width = 2 },\n    .{ .codepoint = \"U+1F362\", .width = 2 },\n    .{ .codepoint = \"U+1F363\", .width = 2 },\n    .{ .codepoint = \"U+1F364\", .width = 2 },\n    .{ .codepoint = \"U+1F365\", .width = 2 },\n    .{ .codepoint = \"U+1F366\", .width = 2 },\n    .{ .codepoint = \"U+1F367\", .width = 2 },\n    .{ .codepoint = \"U+1F368\", .width = 2 },\n    .{ .codepoint = \"U+1F369\", .width = 2 },\n    .{ .codepoint = \"U+1F36A\", .width = 2 },\n    .{ .codepoint = \"U+1F36B\", .width = 2 },\n    .{ .codepoint = \"U+1F36C\", .width = 2 },\n    .{ .codepoint = \"U+1F36D\", .width = 2 },\n    .{ .codepoint = \"U+1F36E\", .width = 2 },\n    .{ .codepoint = \"U+1F36F\", .width = 2 },\n    .{ .codepoint = \"U+1F370\", .width = 2 },\n    .{ .codepoint = \"U+1F371\", .width = 2 },\n    .{ .codepoint = \"U+1F372\", .width = 2 },\n    .{ .codepoint = \"U+1F373\", .width = 2 },\n    .{ .codepoint = \"U+1F374\", .width = 2 },\n    .{ .codepoint = \"U+1F375\", .width = 2 },\n    .{ .codepoint = \"U+1F376\", .width = 2 },\n    .{ .codepoint = \"U+1F377\", .width = 2 },\n    .{ .codepoint = \"U+1F378\", .width = 2 },\n    .{ .codepoint = \"U+1F379\", .width = 2 },\n    .{ .codepoint = \"U+1F37A\", .width = 2 },\n    .{ .codepoint = \"U+1F37B\", .width = 2 },\n    .{ .codepoint = \"U+1F37C\", .width = 2 },\n    .{ .codepoint = \"U+1F37D\", .width = 1 },\n    .{ .codepoint = \"U+1F37E\", .width = 2 },\n    .{ .codepoint = \"U+1F37F\", .width = 2 },\n    .{ .codepoint = \"U+1F380\", .width = 2 },\n    .{ .codepoint = \"U+1F381\", .width = 2 },\n    .{ .codepoint = \"U+1F382\", .width = 2 },\n    .{ .codepoint = \"U+1F383\", .width = 2 },\n    .{ .codepoint = \"U+1F384\", .width = 2 },\n    .{ .codepoint = \"U+1F385\", .width = 2 },\n    .{ .codepoint = \"U+1F386\", .width = 2 },\n    .{ .codepoint = \"U+1F387\", .width = 2 },\n    .{ .codepoint = \"U+1F388\", .width = 2 },\n    .{ .codepoint = \"U+1F389\", .width = 2 },\n    .{ .codepoint = \"U+1F38A\", .width = 2 },\n    .{ .codepoint = \"U+1F38B\", .width = 2 },\n    .{ .codepoint = \"U+1F38C\", .width = 2 },\n    .{ .codepoint = \"U+1F38D\", .width = 2 },\n    .{ .codepoint = \"U+1F38E\", .width = 2 },\n    .{ .codepoint = \"U+1F38F\", .width = 2 },\n    .{ .codepoint = \"U+1F390\", .width = 2 },\n    .{ .codepoint = \"U+1F391\", .width = 2 },\n    .{ .codepoint = \"U+1F392\", .width = 2 },\n    .{ .codepoint = \"U+1F393\", .width = 2 },\n    .{ .codepoint = \"U+1F394\", .width = 1 },\n    .{ .codepoint = \"U+1F395\", .width = 1 },\n    .{ .codepoint = \"U+1F396\", .width = 1 },\n    .{ .codepoint = \"U+1F397\", .width = 1 },\n    .{ .codepoint = \"U+1F398\", .width = 1 },\n    .{ .codepoint = \"U+1F399\", .width = 1 },\n    .{ .codepoint = \"U+1F39A\", .width = 1 },\n    .{ .codepoint = \"U+1F39B\", .width = 1 },\n    .{ .codepoint = \"U+1F39C\", .width = 1 },\n    .{ .codepoint = \"U+1F39D\", .width = 1 },\n    .{ .codepoint = \"U+1F39E\", .width = 1 },\n    .{ .codepoint = \"U+1F39F\", .width = 1 },\n    .{ .codepoint = \"U+1F3A0\", .width = 2 },\n    .{ .codepoint = \"U+1F3A1\", .width = 2 },\n    .{ .codepoint = \"U+1F3A2\", .width = 2 },\n    .{ .codepoint = \"U+1F3A3\", .width = 2 },\n    .{ .codepoint = \"U+1F3A4\", .width = 2 },\n    .{ .codepoint = \"U+1F3A5\", .width = 2 },\n    .{ .codepoint = \"U+1F3A6\", .width = 2 },\n    .{ .codepoint = \"U+1F3A7\", .width = 2 },\n    .{ .codepoint = \"U+1F3A8\", .width = 2 },\n    .{ .codepoint = \"U+1F3A9\", .width = 2 },\n    .{ .codepoint = \"U+1F3AA\", .width = 2 },\n    .{ .codepoint = \"U+1F3AB\", .width = 2 },\n    .{ .codepoint = \"U+1F3AC\", .width = 2 },\n    .{ .codepoint = \"U+1F3AD\", .width = 2 },\n    .{ .codepoint = \"U+1F3AE\", .width = 2 },\n    .{ .codepoint = \"U+1F3AF\", .width = 2 },\n    .{ .codepoint = \"U+1F3B0\", .width = 2 },\n    .{ .codepoint = \"U+1F3B1\", .width = 2 },\n    .{ .codepoint = \"U+1F3B2\", .width = 2 },\n    .{ .codepoint = \"U+1F3B3\", .width = 2 },\n    .{ .codepoint = \"U+1F3B4\", .width = 2 },\n    .{ .codepoint = \"U+1F3B5\", .width = 2 },\n    .{ .codepoint = \"U+1F3B6\", .width = 2 },\n    .{ .codepoint = \"U+1F3B7\", .width = 2 },\n    .{ .codepoint = \"U+1F3B8\", .width = 2 },\n    .{ .codepoint = \"U+1F3B9\", .width = 2 },\n    .{ .codepoint = \"U+1F3BA\", .width = 2 },\n    .{ .codepoint = \"U+1F3BB\", .width = 2 },\n    .{ .codepoint = \"U+1F3BC\", .width = 2 },\n    .{ .codepoint = \"U+1F3BD\", .width = 2 },\n    .{ .codepoint = \"U+1F3BE\", .width = 2 },\n    .{ .codepoint = \"U+1F3BF\", .width = 2 },\n    .{ .codepoint = \"U+1F3C0\", .width = 2 },\n    .{ .codepoint = \"U+1F3C1\", .width = 2 },\n    .{ .codepoint = \"U+1F3C2\", .width = 2 },\n    .{ .codepoint = \"U+1F3C3\", .width = 2 },\n    .{ .codepoint = \"U+1F3C4\", .width = 2 },\n    .{ .codepoint = \"U+1F3C5\", .width = 2 },\n    .{ .codepoint = \"U+1F3C6\", .width = 2 },\n    .{ .codepoint = \"U+1F3C7\", .width = 2 },\n    .{ .codepoint = \"U+1F3C8\", .width = 2 },\n    .{ .codepoint = \"U+1F3C9\", .width = 2 },\n    .{ .codepoint = \"U+1F3CA\", .width = 2 },\n    .{ .codepoint = \"U+1F3CB\", .width = 1 },\n    .{ .codepoint = \"U+1F3CC\", .width = 1 },\n    .{ .codepoint = \"U+1F3CD\", .width = 1 },\n    .{ .codepoint = \"U+1F3CE\", .width = 1 },\n    .{ .codepoint = \"U+1F3CF\", .width = 2 },\n    .{ .codepoint = \"U+1F3D0\", .width = 2 },\n    .{ .codepoint = \"U+1F3D1\", .width = 2 },\n    .{ .codepoint = \"U+1F3D2\", .width = 2 },\n    .{ .codepoint = \"U+1F3D3\", .width = 2 },\n    .{ .codepoint = \"U+1F3D4\", .width = 1 },\n    .{ .codepoint = \"U+1F3D5\", .width = 1 },\n    .{ .codepoint = \"U+1F3D6\", .width = 1 },\n    .{ .codepoint = \"U+1F3D7\", .width = 1 },\n    .{ .codepoint = \"U+1F3D8\", .width = 1 },\n    .{ .codepoint = \"U+1F3D9\", .width = 1 },\n    .{ .codepoint = \"U+1F3DA\", .width = 1 },\n    .{ .codepoint = \"U+1F3DB\", .width = 1 },\n    .{ .codepoint = \"U+1F3DC\", .width = 1 },\n    .{ .codepoint = \"U+1F3DD\", .width = 1 },\n    .{ .codepoint = \"U+1F3DE\", .width = 1 },\n    .{ .codepoint = \"U+1F3DF\", .width = 1 },\n    .{ .codepoint = \"U+1F3E0\", .width = 2 },\n    .{ .codepoint = \"U+1F3E1\", .width = 2 },\n    .{ .codepoint = \"U+1F3E2\", .width = 2 },\n    .{ .codepoint = \"U+1F3E3\", .width = 2 },\n    .{ .codepoint = \"U+1F3E4\", .width = 2 },\n    .{ .codepoint = \"U+1F3E5\", .width = 2 },\n    .{ .codepoint = \"U+1F3E6\", .width = 2 },\n    .{ .codepoint = \"U+1F3E7\", .width = 2 },\n    .{ .codepoint = \"U+1F3E8\", .width = 2 },\n    .{ .codepoint = \"U+1F3E9\", .width = 2 },\n    .{ .codepoint = \"U+1F3EA\", .width = 2 },\n    .{ .codepoint = \"U+1F3EB\", .width = 2 },\n    .{ .codepoint = \"U+1F3EC\", .width = 2 },\n    .{ .codepoint = \"U+1F3ED\", .width = 2 },\n    .{ .codepoint = \"U+1F3EE\", .width = 2 },\n    .{ .codepoint = \"U+1F3EF\", .width = 2 },\n    .{ .codepoint = \"U+1F3F0\", .width = 2 },\n    .{ .codepoint = \"U+1F3F1\", .width = 1 },\n    .{ .codepoint = \"U+1F3F2\", .width = 1 },\n    .{ .codepoint = \"U+1F3F3\", .width = 1 },\n    .{ .codepoint = \"U+1F3F4\", .width = 2 },\n    .{ .codepoint = \"U+1F3F5\", .width = 1 },\n    .{ .codepoint = \"U+1F3F6\", .width = 1 },\n    .{ .codepoint = \"U+1F3F7\", .width = 1 },\n    .{ .codepoint = \"U+1F3F8\", .width = 2 },\n    .{ .codepoint = \"U+1F3F9\", .width = 2 },\n    .{ .codepoint = \"U+1F3FA\", .width = 2 },\n    .{ .codepoint = \"U+1F3FB\", .width = 2 },\n    .{ .codepoint = \"U+1F3FC\", .width = 2 },\n    .{ .codepoint = \"U+1F3FD\", .width = 2 },\n    .{ .codepoint = \"U+1F3FE\", .width = 2 },\n    .{ .codepoint = \"U+1F3FF\", .width = 2 },\n    .{ .codepoint = \"U+1F400\", .width = 2 },\n    .{ .codepoint = \"U+1F401\", .width = 2 },\n    .{ .codepoint = \"U+1F402\", .width = 2 },\n    .{ .codepoint = \"U+1F403\", .width = 2 },\n    .{ .codepoint = \"U+1F404\", .width = 2 },\n    .{ .codepoint = \"U+1F405\", .width = 2 },\n    .{ .codepoint = \"U+1F406\", .width = 2 },\n    .{ .codepoint = \"U+1F407\", .width = 2 },\n    .{ .codepoint = \"U+1F408\", .width = 2 },\n    .{ .codepoint = \"U+1F409\", .width = 2 },\n    .{ .codepoint = \"U+1F40A\", .width = 2 },\n    .{ .codepoint = \"U+1F40B\", .width = 2 },\n    .{ .codepoint = \"U+1F40C\", .width = 2 },\n    .{ .codepoint = \"U+1F40D\", .width = 2 },\n    .{ .codepoint = \"U+1F40E\", .width = 2 },\n    .{ .codepoint = \"U+1F40F\", .width = 2 },\n    .{ .codepoint = \"U+1F410\", .width = 2 },\n    .{ .codepoint = \"U+1F411\", .width = 2 },\n    .{ .codepoint = \"U+1F412\", .width = 2 },\n    .{ .codepoint = \"U+1F413\", .width = 2 },\n    .{ .codepoint = \"U+1F414\", .width = 2 },\n    .{ .codepoint = \"U+1F415\", .width = 2 },\n    .{ .codepoint = \"U+1F416\", .width = 2 },\n    .{ .codepoint = \"U+1F417\", .width = 2 },\n    .{ .codepoint = \"U+1F418\", .width = 2 },\n    .{ .codepoint = \"U+1F419\", .width = 2 },\n    .{ .codepoint = \"U+1F41A\", .width = 2 },\n    .{ .codepoint = \"U+1F41B\", .width = 2 },\n    .{ .codepoint = \"U+1F41C\", .width = 2 },\n    .{ .codepoint = \"U+1F41D\", .width = 2 },\n    .{ .codepoint = \"U+1F41E\", .width = 2 },\n    .{ .codepoint = \"U+1F41F\", .width = 2 },\n    .{ .codepoint = \"U+1F420\", .width = 2 },\n    .{ .codepoint = \"U+1F421\", .width = 2 },\n    .{ .codepoint = \"U+1F422\", .width = 2 },\n    .{ .codepoint = \"U+1F423\", .width = 2 },\n    .{ .codepoint = \"U+1F424\", .width = 2 },\n    .{ .codepoint = \"U+1F425\", .width = 2 },\n    .{ .codepoint = \"U+1F426\", .width = 2 },\n    .{ .codepoint = \"U+1F427\", .width = 2 },\n    .{ .codepoint = \"U+1F428\", .width = 2 },\n    .{ .codepoint = \"U+1F429\", .width = 2 },\n    .{ .codepoint = \"U+1F42A\", .width = 2 },\n    .{ .codepoint = \"U+1F42B\", .width = 2 },\n    .{ .codepoint = \"U+1F42C\", .width = 2 },\n    .{ .codepoint = \"U+1F42D\", .width = 2 },\n    .{ .codepoint = \"U+1F42E\", .width = 2 },\n    .{ .codepoint = \"U+1F42F\", .width = 2 },\n    .{ .codepoint = \"U+1F430\", .width = 2 },\n    .{ .codepoint = \"U+1F431\", .width = 2 },\n    .{ .codepoint = \"U+1F432\", .width = 2 },\n    .{ .codepoint = \"U+1F433\", .width = 2 },\n    .{ .codepoint = \"U+1F434\", .width = 2 },\n    .{ .codepoint = \"U+1F435\", .width = 2 },\n    .{ .codepoint = \"U+1F436\", .width = 2 },\n    .{ .codepoint = \"U+1F437\", .width = 2 },\n    .{ .codepoint = \"U+1F438\", .width = 2 },\n    .{ .codepoint = \"U+1F439\", .width = 2 },\n    .{ .codepoint = \"U+1F43A\", .width = 2 },\n    .{ .codepoint = \"U+1F43B\", .width = 2 },\n    .{ .codepoint = \"U+1F43C\", .width = 2 },\n    .{ .codepoint = \"U+1F43D\", .width = 2 },\n    .{ .codepoint = \"U+1F43E\", .width = 2 },\n    .{ .codepoint = \"U+1F43F\", .width = 1 },\n    .{ .codepoint = \"U+1F440\", .width = 2 },\n    .{ .codepoint = \"U+1F441\", .width = 1 },\n    .{ .codepoint = \"U+1F442\", .width = 2 },\n    .{ .codepoint = \"U+1F443\", .width = 2 },\n    .{ .codepoint = \"U+1F444\", .width = 2 },\n    .{ .codepoint = \"U+1F445\", .width = 2 },\n    .{ .codepoint = \"U+1F446\", .width = 2 },\n    .{ .codepoint = \"U+1F447\", .width = 2 },\n    .{ .codepoint = \"U+1F448\", .width = 2 },\n    .{ .codepoint = \"U+1F449\", .width = 2 },\n    .{ .codepoint = \"U+1F44A\", .width = 2 },\n    .{ .codepoint = \"U+1F44B\", .width = 2 },\n    .{ .codepoint = \"U+1F44C\", .width = 2 },\n    .{ .codepoint = \"U+1F44D\", .width = 2 },\n    .{ .codepoint = \"U+1F44E\", .width = 2 },\n    .{ .codepoint = \"U+1F44F\", .width = 2 },\n    .{ .codepoint = \"U+1F450\", .width = 2 },\n    .{ .codepoint = \"U+1F451\", .width = 2 },\n    .{ .codepoint = \"U+1F452\", .width = 2 },\n    .{ .codepoint = \"U+1F453\", .width = 2 },\n    .{ .codepoint = \"U+1F454\", .width = 2 },\n    .{ .codepoint = \"U+1F455\", .width = 2 },\n    .{ .codepoint = \"U+1F456\", .width = 2 },\n    .{ .codepoint = \"U+1F457\", .width = 2 },\n    .{ .codepoint = \"U+1F458\", .width = 2 },\n    .{ .codepoint = \"U+1F459\", .width = 2 },\n    .{ .codepoint = \"U+1F45A\", .width = 2 },\n    .{ .codepoint = \"U+1F45B\", .width = 2 },\n    .{ .codepoint = \"U+1F45C\", .width = 2 },\n    .{ .codepoint = \"U+1F45D\", .width = 2 },\n    .{ .codepoint = \"U+1F45E\", .width = 2 },\n    .{ .codepoint = \"U+1F45F\", .width = 2 },\n    .{ .codepoint = \"U+1F460\", .width = 2 },\n    .{ .codepoint = \"U+1F461\", .width = 2 },\n    .{ .codepoint = \"U+1F462\", .width = 2 },\n    .{ .codepoint = \"U+1F463\", .width = 2 },\n    .{ .codepoint = \"U+1F464\", .width = 2 },\n    .{ .codepoint = \"U+1F465\", .width = 2 },\n    .{ .codepoint = \"U+1F466\", .width = 2 },\n    .{ .codepoint = \"U+1F467\", .width = 2 },\n    .{ .codepoint = \"U+1F468\", .width = 2 },\n    .{ .codepoint = \"U+1F469\", .width = 2 },\n    .{ .codepoint = \"U+1F46A\", .width = 2 },\n    .{ .codepoint = \"U+1F46B\", .width = 2 },\n    .{ .codepoint = \"U+1F46C\", .width = 2 },\n    .{ .codepoint = \"U+1F46D\", .width = 2 },\n    .{ .codepoint = \"U+1F46E\", .width = 2 },\n    .{ .codepoint = \"U+1F46F\", .width = 2 },\n    .{ .codepoint = \"U+1F470\", .width = 2 },\n    .{ .codepoint = \"U+1F471\", .width = 2 },\n    .{ .codepoint = \"U+1F472\", .width = 2 },\n    .{ .codepoint = \"U+1F473\", .width = 2 },\n    .{ .codepoint = \"U+1F474\", .width = 2 },\n    .{ .codepoint = \"U+1F475\", .width = 2 },\n    .{ .codepoint = \"U+1F476\", .width = 2 },\n    .{ .codepoint = \"U+1F477\", .width = 2 },\n    .{ .codepoint = \"U+1F478\", .width = 2 },\n    .{ .codepoint = \"U+1F479\", .width = 2 },\n    .{ .codepoint = \"U+1F47A\", .width = 2 },\n    .{ .codepoint = \"U+1F47B\", .width = 2 },\n    .{ .codepoint = \"U+1F47C\", .width = 2 },\n    .{ .codepoint = \"U+1F47D\", .width = 2 },\n    .{ .codepoint = \"U+1F47E\", .width = 2 },\n    .{ .codepoint = \"U+1F47F\", .width = 2 },\n    .{ .codepoint = \"U+1F480\", .width = 2 },\n    .{ .codepoint = \"U+1F481\", .width = 2 },\n    .{ .codepoint = \"U+1F482\", .width = 2 },\n    .{ .codepoint = \"U+1F483\", .width = 2 },\n    .{ .codepoint = \"U+1F484\", .width = 2 },\n    .{ .codepoint = \"U+1F485\", .width = 2 },\n    .{ .codepoint = \"U+1F486\", .width = 2 },\n    .{ .codepoint = \"U+1F487\", .width = 2 },\n    .{ .codepoint = \"U+1F488\", .width = 2 },\n    .{ .codepoint = \"U+1F489\", .width = 2 },\n    .{ .codepoint = \"U+1F48A\", .width = 2 },\n    .{ .codepoint = \"U+1F48B\", .width = 2 },\n    .{ .codepoint = \"U+1F48C\", .width = 2 },\n    .{ .codepoint = \"U+1F48D\", .width = 2 },\n    .{ .codepoint = \"U+1F48E\", .width = 2 },\n    .{ .codepoint = \"U+1F48F\", .width = 2 },\n    .{ .codepoint = \"U+1F490\", .width = 2 },\n    .{ .codepoint = \"U+1F491\", .width = 2 },\n    .{ .codepoint = \"U+1F492\", .width = 2 },\n    .{ .codepoint = \"U+1F493\", .width = 2 },\n    .{ .codepoint = \"U+1F494\", .width = 2 },\n    .{ .codepoint = \"U+1F495\", .width = 2 },\n    .{ .codepoint = \"U+1F496\", .width = 2 },\n    .{ .codepoint = \"U+1F497\", .width = 2 },\n    .{ .codepoint = \"U+1F498\", .width = 2 },\n    .{ .codepoint = \"U+1F499\", .width = 2 },\n    .{ .codepoint = \"U+1F49A\", .width = 2 },\n    .{ .codepoint = \"U+1F49B\", .width = 2 },\n    .{ .codepoint = \"U+1F49C\", .width = 2 },\n    .{ .codepoint = \"U+1F49D\", .width = 2 },\n    .{ .codepoint = \"U+1F49E\", .width = 2 },\n    .{ .codepoint = \"U+1F49F\", .width = 2 },\n    .{ .codepoint = \"U+1F4A0\", .width = 2 },\n    .{ .codepoint = \"U+1F4A1\", .width = 2 },\n    .{ .codepoint = \"U+1F4A2\", .width = 2 },\n    .{ .codepoint = \"U+1F4A3\", .width = 2 },\n    .{ .codepoint = \"U+1F4A4\", .width = 2 },\n    .{ .codepoint = \"U+1F4A5\", .width = 2 },\n    .{ .codepoint = \"U+1F4A6\", .width = 2 },\n    .{ .codepoint = \"U+1F4A7\", .width = 2 },\n    .{ .codepoint = \"U+1F4A8\", .width = 2 },\n    .{ .codepoint = \"U+1F4A9\", .width = 2 },\n    .{ .codepoint = \"U+1F4AA\", .width = 2 },\n    .{ .codepoint = \"U+1F4AB\", .width = 2 },\n    .{ .codepoint = \"U+1F4AC\", .width = 2 },\n    .{ .codepoint = \"U+1F4AD\", .width = 2 },\n    .{ .codepoint = \"U+1F4AE\", .width = 2 },\n    .{ .codepoint = \"U+1F4AF\", .width = 2 },\n    .{ .codepoint = \"U+1F4B0\", .width = 2 },\n    .{ .codepoint = \"U+1F4B1\", .width = 2 },\n    .{ .codepoint = \"U+1F4B2\", .width = 2 },\n    .{ .codepoint = \"U+1F4B3\", .width = 2 },\n    .{ .codepoint = \"U+1F4B4\", .width = 2 },\n    .{ .codepoint = \"U+1F4B5\", .width = 2 },\n    .{ .codepoint = \"U+1F4B6\", .width = 2 },\n    .{ .codepoint = \"U+1F4B7\", .width = 2 },\n    .{ .codepoint = \"U+1F4B8\", .width = 2 },\n    .{ .codepoint = \"U+1F4B9\", .width = 2 },\n    .{ .codepoint = \"U+1F4BA\", .width = 2 },\n    .{ .codepoint = \"U+1F4BB\", .width = 2 },\n    .{ .codepoint = \"U+1F4BC\", .width = 2 },\n    .{ .codepoint = \"U+1F4BD\", .width = 2 },\n    .{ .codepoint = \"U+1F4BE\", .width = 2 },\n    .{ .codepoint = \"U+1F4BF\", .width = 2 },\n    .{ .codepoint = \"U+1F4C0\", .width = 2 },\n    .{ .codepoint = \"U+1F4C1\", .width = 2 },\n    .{ .codepoint = \"U+1F4C2\", .width = 2 },\n    .{ .codepoint = \"U+1F4C3\", .width = 2 },\n    .{ .codepoint = \"U+1F4C4\", .width = 2 },\n    .{ .codepoint = \"U+1F4C5\", .width = 2 },\n    .{ .codepoint = \"U+1F4C6\", .width = 2 },\n    .{ .codepoint = \"U+1F4C7\", .width = 2 },\n    .{ .codepoint = \"U+1F4C8\", .width = 2 },\n    .{ .codepoint = \"U+1F4C9\", .width = 2 },\n    .{ .codepoint = \"U+1F4CA\", .width = 2 },\n    .{ .codepoint = \"U+1F4CB\", .width = 2 },\n    .{ .codepoint = \"U+1F4CC\", .width = 2 },\n    .{ .codepoint = \"U+1F4CD\", .width = 2 },\n    .{ .codepoint = \"U+1F4CE\", .width = 2 },\n    .{ .codepoint = \"U+1F4CF\", .width = 2 },\n    .{ .codepoint = \"U+1F4D0\", .width = 2 },\n    .{ .codepoint = \"U+1F4D1\", .width = 2 },\n    .{ .codepoint = \"U+1F4D2\", .width = 2 },\n    .{ .codepoint = \"U+1F4D3\", .width = 2 },\n    .{ .codepoint = \"U+1F4D4\", .width = 2 },\n    .{ .codepoint = \"U+1F4D5\", .width = 2 },\n    .{ .codepoint = \"U+1F4D6\", .width = 2 },\n    .{ .codepoint = \"U+1F4D7\", .width = 2 },\n    .{ .codepoint = \"U+1F4D8\", .width = 2 },\n    .{ .codepoint = \"U+1F4D9\", .width = 2 },\n    .{ .codepoint = \"U+1F4DA\", .width = 2 },\n    .{ .codepoint = \"U+1F4DB\", .width = 2 },\n    .{ .codepoint = \"U+1F4DC\", .width = 2 },\n    .{ .codepoint = \"U+1F4DD\", .width = 2 },\n    .{ .codepoint = \"U+1F4DE\", .width = 2 },\n    .{ .codepoint = \"U+1F4DF\", .width = 2 },\n    .{ .codepoint = \"U+1F4E0\", .width = 2 },\n    .{ .codepoint = \"U+1F4E1\", .width = 2 },\n    .{ .codepoint = \"U+1F4E2\", .width = 2 },\n    .{ .codepoint = \"U+1F4E3\", .width = 2 },\n    .{ .codepoint = \"U+1F4E4\", .width = 2 },\n    .{ .codepoint = \"U+1F4E5\", .width = 2 },\n    .{ .codepoint = \"U+1F4E6\", .width = 2 },\n    .{ .codepoint = \"U+1F4E7\", .width = 2 },\n    .{ .codepoint = \"U+1F4E8\", .width = 2 },\n    .{ .codepoint = \"U+1F4E9\", .width = 2 },\n    .{ .codepoint = \"U+1F4EA\", .width = 2 },\n    .{ .codepoint = \"U+1F4EB\", .width = 2 },\n    .{ .codepoint = \"U+1F4EC\", .width = 2 },\n    .{ .codepoint = \"U+1F4ED\", .width = 2 },\n    .{ .codepoint = \"U+1F4EE\", .width = 2 },\n    .{ .codepoint = \"U+1F4EF\", .width = 2 },\n    .{ .codepoint = \"U+1F4F0\", .width = 2 },\n    .{ .codepoint = \"U+1F4F1\", .width = 2 },\n    .{ .codepoint = \"U+1F4F2\", .width = 2 },\n    .{ .codepoint = \"U+1F4F3\", .width = 2 },\n    .{ .codepoint = \"U+1F4F4\", .width = 2 },\n    .{ .codepoint = \"U+1F4F5\", .width = 2 },\n    .{ .codepoint = \"U+1F4F6\", .width = 2 },\n    .{ .codepoint = \"U+1F4F7\", .width = 2 },\n    .{ .codepoint = \"U+1F4F8\", .width = 2 },\n    .{ .codepoint = \"U+1F4F9\", .width = 2 },\n    .{ .codepoint = \"U+1F4FA\", .width = 2 },\n    .{ .codepoint = \"U+1F4FB\", .width = 2 },\n    .{ .codepoint = \"U+1F4FC\", .width = 2 },\n    .{ .codepoint = \"U+1F4FD\", .width = 1 },\n    .{ .codepoint = \"U+1F4FE\", .width = 1 },\n    .{ .codepoint = \"U+1F4FF\", .width = 2 },\n    .{ .codepoint = \"U+1F600\", .width = 2 },\n    .{ .codepoint = \"U+1F601\", .width = 2 },\n    .{ .codepoint = \"U+1F602\", .width = 2 },\n    .{ .codepoint = \"U+1F603\", .width = 2 },\n    .{ .codepoint = \"U+1F604\", .width = 2 },\n    .{ .codepoint = \"U+1F605\", .width = 2 },\n    .{ .codepoint = \"U+1F606\", .width = 2 },\n    .{ .codepoint = \"U+1F607\", .width = 2 },\n    .{ .codepoint = \"U+1F608\", .width = 2 },\n    .{ .codepoint = \"U+1F609\", .width = 2 },\n    .{ .codepoint = \"U+1F60A\", .width = 2 },\n    .{ .codepoint = \"U+1F60B\", .width = 2 },\n    .{ .codepoint = \"U+1F60C\", .width = 2 },\n    .{ .codepoint = \"U+1F60D\", .width = 2 },\n    .{ .codepoint = \"U+1F60E\", .width = 2 },\n    .{ .codepoint = \"U+1F60F\", .width = 2 },\n    .{ .codepoint = \"U+1F610\", .width = 2 },\n    .{ .codepoint = \"U+1F611\", .width = 2 },\n    .{ .codepoint = \"U+1F612\", .width = 2 },\n    .{ .codepoint = \"U+1F613\", .width = 2 },\n    .{ .codepoint = \"U+1F614\", .width = 2 },\n    .{ .codepoint = \"U+1F615\", .width = 2 },\n    .{ .codepoint = \"U+1F616\", .width = 2 },\n    .{ .codepoint = \"U+1F617\", .width = 2 },\n    .{ .codepoint = \"U+1F618\", .width = 2 },\n    .{ .codepoint = \"U+1F619\", .width = 2 },\n    .{ .codepoint = \"U+1F61A\", .width = 2 },\n    .{ .codepoint = \"U+1F61B\", .width = 2 },\n    .{ .codepoint = \"U+1F61C\", .width = 2 },\n    .{ .codepoint = \"U+1F61D\", .width = 2 },\n    .{ .codepoint = \"U+1F61E\", .width = 2 },\n    .{ .codepoint = \"U+1F61F\", .width = 2 },\n    .{ .codepoint = \"U+1F620\", .width = 2 },\n    .{ .codepoint = \"U+1F621\", .width = 2 },\n    .{ .codepoint = \"U+1F622\", .width = 2 },\n    .{ .codepoint = \"U+1F623\", .width = 2 },\n    .{ .codepoint = \"U+1F624\", .width = 2 },\n    .{ .codepoint = \"U+1F625\", .width = 2 },\n    .{ .codepoint = \"U+1F626\", .width = 2 },\n    .{ .codepoint = \"U+1F627\", .width = 2 },\n    .{ .codepoint = \"U+1F628\", .width = 2 },\n    .{ .codepoint = \"U+1F629\", .width = 2 },\n    .{ .codepoint = \"U+1F62A\", .width = 2 },\n    .{ .codepoint = \"U+1F62B\", .width = 2 },\n    .{ .codepoint = \"U+1F62C\", .width = 2 },\n    .{ .codepoint = \"U+1F62D\", .width = 2 },\n    .{ .codepoint = \"U+1F62E\", .width = 2 },\n    .{ .codepoint = \"U+1F62F\", .width = 2 },\n    .{ .codepoint = \"U+1F630\", .width = 2 },\n    .{ .codepoint = \"U+1F631\", .width = 2 },\n    .{ .codepoint = \"U+1F632\", .width = 2 },\n    .{ .codepoint = \"U+1F633\", .width = 2 },\n    .{ .codepoint = \"U+1F634\", .width = 2 },\n    .{ .codepoint = \"U+1F635\", .width = 2 },\n    .{ .codepoint = \"U+1F636\", .width = 2 },\n    .{ .codepoint = \"U+1F637\", .width = 2 },\n    .{ .codepoint = \"U+1F638\", .width = 2 },\n    .{ .codepoint = \"U+1F639\", .width = 2 },\n    .{ .codepoint = \"U+1F63A\", .width = 2 },\n    .{ .codepoint = \"U+1F63B\", .width = 2 },\n    .{ .codepoint = \"U+1F63C\", .width = 2 },\n    .{ .codepoint = \"U+1F63D\", .width = 2 },\n    .{ .codepoint = \"U+1F63E\", .width = 2 },\n    .{ .codepoint = \"U+1F63F\", .width = 2 },\n    .{ .codepoint = \"U+1F640\", .width = 2 },\n    .{ .codepoint = \"U+1F641\", .width = 2 },\n    .{ .codepoint = \"U+1F642\", .width = 2 },\n    .{ .codepoint = \"U+1F643\", .width = 2 },\n    .{ .codepoint = \"U+1F644\", .width = 2 },\n    .{ .codepoint = \"U+1F645\", .width = 2 },\n    .{ .codepoint = \"U+1F646\", .width = 2 },\n    .{ .codepoint = \"U+1F647\", .width = 2 },\n    .{ .codepoint = \"U+1F648\", .width = 2 },\n    .{ .codepoint = \"U+1F649\", .width = 2 },\n    .{ .codepoint = \"U+1F64A\", .width = 2 },\n    .{ .codepoint = \"U+1F64B\", .width = 2 },\n    .{ .codepoint = \"U+1F64C\", .width = 2 },\n    .{ .codepoint = \"U+1F64D\", .width = 2 },\n    .{ .codepoint = \"U+1F64E\", .width = 2 },\n    .{ .codepoint = \"U+1F64F\", .width = 2 },\n    .{ .codepoint = \"U+1F680\", .width = 2 },\n    .{ .codepoint = \"U+1F681\", .width = 2 },\n    .{ .codepoint = \"U+1F682\", .width = 2 },\n    .{ .codepoint = \"U+1F683\", .width = 2 },\n    .{ .codepoint = \"U+1F684\", .width = 2 },\n    .{ .codepoint = \"U+1F685\", .width = 2 },\n    .{ .codepoint = \"U+1F686\", .width = 2 },\n    .{ .codepoint = \"U+1F687\", .width = 2 },\n    .{ .codepoint = \"U+1F688\", .width = 2 },\n    .{ .codepoint = \"U+1F689\", .width = 2 },\n    .{ .codepoint = \"U+1F68A\", .width = 2 },\n    .{ .codepoint = \"U+1F68B\", .width = 2 },\n    .{ .codepoint = \"U+1F68C\", .width = 2 },\n    .{ .codepoint = \"U+1F68D\", .width = 2 },\n    .{ .codepoint = \"U+1F68E\", .width = 2 },\n    .{ .codepoint = \"U+1F68F\", .width = 2 },\n    .{ .codepoint = \"U+1F690\", .width = 2 },\n    .{ .codepoint = \"U+1F691\", .width = 2 },\n    .{ .codepoint = \"U+1F692\", .width = 2 },\n    .{ .codepoint = \"U+1F693\", .width = 2 },\n    .{ .codepoint = \"U+1F694\", .width = 2 },\n    .{ .codepoint = \"U+1F695\", .width = 2 },\n    .{ .codepoint = \"U+1F696\", .width = 2 },\n    .{ .codepoint = \"U+1F697\", .width = 2 },\n    .{ .codepoint = \"U+1F698\", .width = 2 },\n    .{ .codepoint = \"U+1F699\", .width = 2 },\n    .{ .codepoint = \"U+1F69A\", .width = 2 },\n    .{ .codepoint = \"U+1F69B\", .width = 2 },\n    .{ .codepoint = \"U+1F69C\", .width = 2 },\n    .{ .codepoint = \"U+1F69D\", .width = 2 },\n    .{ .codepoint = \"U+1F69E\", .width = 2 },\n    .{ .codepoint = \"U+1F69F\", .width = 2 },\n    .{ .codepoint = \"U+1F6A0\", .width = 2 },\n    .{ .codepoint = \"U+1F6A1\", .width = 2 },\n    .{ .codepoint = \"U+1F6A2\", .width = 2 },\n    .{ .codepoint = \"U+1F6A3\", .width = 2 },\n    .{ .codepoint = \"U+1F6A4\", .width = 2 },\n    .{ .codepoint = \"U+1F6A5\", .width = 2 },\n    .{ .codepoint = \"U+1F6A6\", .width = 2 },\n    .{ .codepoint = \"U+1F6A7\", .width = 2 },\n    .{ .codepoint = \"U+1F6A8\", .width = 2 },\n    .{ .codepoint = \"U+1F6A9\", .width = 2 },\n    .{ .codepoint = \"U+1F6AA\", .width = 2 },\n    .{ .codepoint = \"U+1F6AB\", .width = 2 },\n    .{ .codepoint = \"U+1F6AC\", .width = 2 },\n    .{ .codepoint = \"U+1F6AD\", .width = 2 },\n    .{ .codepoint = \"U+1F6AE\", .width = 2 },\n    .{ .codepoint = \"U+1F6AF\", .width = 2 },\n    .{ .codepoint = \"U+1F6B0\", .width = 2 },\n    .{ .codepoint = \"U+1F6B1\", .width = 2 },\n    .{ .codepoint = \"U+1F6B2\", .width = 2 },\n    .{ .codepoint = \"U+1F6B3\", .width = 2 },\n    .{ .codepoint = \"U+1F6B4\", .width = 2 },\n    .{ .codepoint = \"U+1F6B5\", .width = 2 },\n    .{ .codepoint = \"U+1F6B6\", .width = 2 },\n    .{ .codepoint = \"U+1F6B7\", .width = 2 },\n    .{ .codepoint = \"U+1F6B8\", .width = 2 },\n    .{ .codepoint = \"U+1F6B9\", .width = 2 },\n    .{ .codepoint = \"U+1F6BA\", .width = 2 },\n    .{ .codepoint = \"U+1F6BB\", .width = 2 },\n    .{ .codepoint = \"U+1F6BC\", .width = 2 },\n    .{ .codepoint = \"U+1F6BD\", .width = 2 },\n    .{ .codepoint = \"U+1F6BE\", .width = 2 },\n    .{ .codepoint = \"U+1F6BF\", .width = 2 },\n    .{ .codepoint = \"U+1F6C0\", .width = 2 },\n    .{ .codepoint = \"U+1F6C1\", .width = 2 },\n    .{ .codepoint = \"U+1F6C2\", .width = 2 },\n    .{ .codepoint = \"U+1F6C3\", .width = 2 },\n    .{ .codepoint = \"U+1F6C4\", .width = 2 },\n    .{ .codepoint = \"U+1F6C5\", .width = 2 },\n    .{ .codepoint = \"U+1F6C6\", .width = 1 },\n    .{ .codepoint = \"U+1F6C7\", .width = 1 },\n    .{ .codepoint = \"U+1F6C8\", .width = 1 },\n    .{ .codepoint = \"U+1F6C9\", .width = 1 },\n    .{ .codepoint = \"U+1F6CA\", .width = 1 },\n    .{ .codepoint = \"U+1F6CB\", .width = 1 },\n    .{ .codepoint = \"U+1F6CC\", .width = 2 },\n    .{ .codepoint = \"U+1F6CD\", .width = 1 },\n    .{ .codepoint = \"U+1F6CE\", .width = 1 },\n    .{ .codepoint = \"U+1F6CF\", .width = 1 },\n    .{ .codepoint = \"U+1F6D0\", .width = 2 },\n    .{ .codepoint = \"U+1F6D1\", .width = 2 },\n    .{ .codepoint = \"U+1F6D2\", .width = 2 },\n    .{ .codepoint = \"U+1F6D3\", .width = 1 },\n    .{ .codepoint = \"U+1F6D4\", .width = 1 },\n    .{ .codepoint = \"U+1F6D5\", .width = 2 },\n    .{ .codepoint = \"U+1F6D6\", .width = 2 },\n    .{ .codepoint = \"U+1F6D7\", .width = 2 },\n    .{ .codepoint = \"U+1F6D8\", .width = 1 },\n    .{ .codepoint = \"U+1F6D9\", .width = 1 },\n    .{ .codepoint = \"U+1F6DA\", .width = 1 },\n    .{ .codepoint = \"U+1F6DB\", .width = 1 },\n    .{ .codepoint = \"U+1F6DC\", .width = 2 },\n    .{ .codepoint = \"U+1F6DD\", .width = 2 },\n    .{ .codepoint = \"U+1F6DE\", .width = 2 },\n    .{ .codepoint = \"U+1F6DF\", .width = 2 },\n    .{ .codepoint = \"U+1F6E0\", .width = 1 },\n    .{ .codepoint = \"U+1F6E1\", .width = 1 },\n    .{ .codepoint = \"U+1F6E2\", .width = 1 },\n    .{ .codepoint = \"U+1F6E3\", .width = 1 },\n    .{ .codepoint = \"U+1F6E4\", .width = 1 },\n    .{ .codepoint = \"U+1F6E5\", .width = 1 },\n    .{ .codepoint = \"U+1F6E6\", .width = 1 },\n    .{ .codepoint = \"U+1F6E7\", .width = 1 },\n    .{ .codepoint = \"U+1F6E8\", .width = 1 },\n    .{ .codepoint = \"U+1F6E9\", .width = 1 },\n    .{ .codepoint = \"U+1F6EA\", .width = 1 },\n    .{ .codepoint = \"U+1F6EB\", .width = 2 },\n    .{ .codepoint = \"U+1F6EC\", .width = 2 },\n    .{ .codepoint = \"U+1F6ED\", .width = 1 },\n    .{ .codepoint = \"U+1F6EE\", .width = 1 },\n    .{ .codepoint = \"U+1F6EF\", .width = 1 },\n    .{ .codepoint = \"U+1F6F0\", .width = 1 },\n    .{ .codepoint = \"U+1F6F1\", .width = 1 },\n    .{ .codepoint = \"U+1F6F2\", .width = 1 },\n    .{ .codepoint = \"U+1F6F3\", .width = 1 },\n    .{ .codepoint = \"U+1F6F4\", .width = 2 },\n    .{ .codepoint = \"U+1F6F5\", .width = 2 },\n    .{ .codepoint = \"U+1F6F6\", .width = 2 },\n    .{ .codepoint = \"U+1F6F7\", .width = 2 },\n    .{ .codepoint = \"U+1F6F8\", .width = 2 },\n    .{ .codepoint = \"U+1F6F9\", .width = 2 },\n    .{ .codepoint = \"U+1F6FA\", .width = 2 },\n    .{ .codepoint = \"U+1F6FB\", .width = 2 },\n    .{ .codepoint = \"U+1F6FC\", .width = 2 },\n    .{ .codepoint = \"U+1F6FD\", .width = 1 },\n    .{ .codepoint = \"U+1F6FE\", .width = 1 },\n    .{ .codepoint = \"U+1F6FF\", .width = 1 },\n    .{ .codepoint = \"U+1F900\", .width = 1 },\n    .{ .codepoint = \"U+1F901\", .width = 1 },\n    .{ .codepoint = \"U+1F902\", .width = 1 },\n    .{ .codepoint = \"U+1F903\", .width = 1 },\n    .{ .codepoint = \"U+1F904\", .width = 1 },\n    .{ .codepoint = \"U+1F905\", .width = 1 },\n    .{ .codepoint = \"U+1F906\", .width = 1 },\n    .{ .codepoint = \"U+1F907\", .width = 1 },\n    .{ .codepoint = \"U+1F908\", .width = 1 },\n    .{ .codepoint = \"U+1F909\", .width = 1 },\n    .{ .codepoint = \"U+1F90A\", .width = 1 },\n    .{ .codepoint = \"U+1F90B\", .width = 1 },\n    .{ .codepoint = \"U+1F90C\", .width = 2 },\n    .{ .codepoint = \"U+1F90D\", .width = 2 },\n    .{ .codepoint = \"U+1F90E\", .width = 2 },\n    .{ .codepoint = \"U+1F90F\", .width = 2 },\n    .{ .codepoint = \"U+1F910\", .width = 2 },\n    .{ .codepoint = \"U+1F911\", .width = 2 },\n    .{ .codepoint = \"U+1F912\", .width = 2 },\n    .{ .codepoint = \"U+1F913\", .width = 2 },\n    .{ .codepoint = \"U+1F914\", .width = 2 },\n    .{ .codepoint = \"U+1F915\", .width = 2 },\n    .{ .codepoint = \"U+1F916\", .width = 2 },\n    .{ .codepoint = \"U+1F917\", .width = 2 },\n    .{ .codepoint = \"U+1F918\", .width = 2 },\n    .{ .codepoint = \"U+1F919\", .width = 2 },\n    .{ .codepoint = \"U+1F91A\", .width = 2 },\n    .{ .codepoint = \"U+1F91B\", .width = 2 },\n    .{ .codepoint = \"U+1F91C\", .width = 2 },\n    .{ .codepoint = \"U+1F91D\", .width = 2 },\n    .{ .codepoint = \"U+1F91E\", .width = 2 },\n    .{ .codepoint = \"U+1F91F\", .width = 2 },\n    .{ .codepoint = \"U+1F920\", .width = 2 },\n    .{ .codepoint = \"U+1F921\", .width = 2 },\n    .{ .codepoint = \"U+1F922\", .width = 2 },\n    .{ .codepoint = \"U+1F923\", .width = 2 },\n    .{ .codepoint = \"U+1F924\", .width = 2 },\n    .{ .codepoint = \"U+1F925\", .width = 2 },\n    .{ .codepoint = \"U+1F926\", .width = 2 },\n    .{ .codepoint = \"U+1F927\", .width = 2 },\n    .{ .codepoint = \"U+1F928\", .width = 2 },\n    .{ .codepoint = \"U+1F929\", .width = 2 },\n    .{ .codepoint = \"U+1F92A\", .width = 2 },\n    .{ .codepoint = \"U+1F92B\", .width = 2 },\n    .{ .codepoint = \"U+1F92C\", .width = 2 },\n    .{ .codepoint = \"U+1F92D\", .width = 2 },\n    .{ .codepoint = \"U+1F92E\", .width = 2 },\n    .{ .codepoint = \"U+1F92F\", .width = 2 },\n    .{ .codepoint = \"U+1F930\", .width = 2 },\n    .{ .codepoint = \"U+1F931\", .width = 2 },\n    .{ .codepoint = \"U+1F932\", .width = 2 },\n    .{ .codepoint = \"U+1F933\", .width = 2 },\n    .{ .codepoint = \"U+1F934\", .width = 2 },\n    .{ .codepoint = \"U+1F935\", .width = 2 },\n    .{ .codepoint = \"U+1F936\", .width = 2 },\n    .{ .codepoint = \"U+1F937\", .width = 2 },\n    .{ .codepoint = \"U+1F938\", .width = 2 },\n    .{ .codepoint = \"U+1F939\", .width = 2 },\n    .{ .codepoint = \"U+1F93A\", .width = 2 },\n    .{ .codepoint = \"U+1F93B\", .width = 1 },\n    .{ .codepoint = \"U+1F93C\", .width = 2 },\n    .{ .codepoint = \"U+1F93D\", .width = 2 },\n    .{ .codepoint = \"U+1F93E\", .width = 2 },\n    .{ .codepoint = \"U+1F93F\", .width = 2 },\n    .{ .codepoint = \"U+1F940\", .width = 2 },\n    .{ .codepoint = \"U+1F941\", .width = 2 },\n    .{ .codepoint = \"U+1F942\", .width = 2 },\n    .{ .codepoint = \"U+1F943\", .width = 2 },\n    .{ .codepoint = \"U+1F944\", .width = 2 },\n    .{ .codepoint = \"U+1F945\", .width = 2 },\n    .{ .codepoint = \"U+1F946\", .width = 1 },\n    .{ .codepoint = \"U+1F947\", .width = 2 },\n    .{ .codepoint = \"U+1F948\", .width = 2 },\n    .{ .codepoint = \"U+1F949\", .width = 2 },\n    .{ .codepoint = \"U+1F94A\", .width = 2 },\n    .{ .codepoint = \"U+1F94B\", .width = 2 },\n    .{ .codepoint = \"U+1F94C\", .width = 2 },\n    .{ .codepoint = \"U+1F94D\", .width = 2 },\n    .{ .codepoint = \"U+1F94E\", .width = 2 },\n    .{ .codepoint = \"U+1F94F\", .width = 2 },\n    .{ .codepoint = \"U+1F950\", .width = 2 },\n    .{ .codepoint = \"U+1F951\", .width = 2 },\n    .{ .codepoint = \"U+1F952\", .width = 2 },\n    .{ .codepoint = \"U+1F953\", .width = 2 },\n    .{ .codepoint = \"U+1F954\", .width = 2 },\n    .{ .codepoint = \"U+1F955\", .width = 2 },\n    .{ .codepoint = \"U+1F956\", .width = 2 },\n    .{ .codepoint = \"U+1F957\", .width = 2 },\n    .{ .codepoint = \"U+1F958\", .width = 2 },\n    .{ .codepoint = \"U+1F959\", .width = 2 },\n    .{ .codepoint = \"U+1F95A\", .width = 2 },\n    .{ .codepoint = \"U+1F95B\", .width = 2 },\n    .{ .codepoint = \"U+1F95C\", .width = 2 },\n    .{ .codepoint = \"U+1F95D\", .width = 2 },\n    .{ .codepoint = \"U+1F95E\", .width = 2 },\n    .{ .codepoint = \"U+1F95F\", .width = 2 },\n    .{ .codepoint = \"U+1F960\", .width = 2 },\n    .{ .codepoint = \"U+1F961\", .width = 2 },\n    .{ .codepoint = \"U+1F962\", .width = 2 },\n    .{ .codepoint = \"U+1F963\", .width = 2 },\n    .{ .codepoint = \"U+1F964\", .width = 2 },\n    .{ .codepoint = \"U+1F965\", .width = 2 },\n    .{ .codepoint = \"U+1F966\", .width = 2 },\n    .{ .codepoint = \"U+1F967\", .width = 2 },\n    .{ .codepoint = \"U+1F968\", .width = 2 },\n    .{ .codepoint = \"U+1F969\", .width = 2 },\n    .{ .codepoint = \"U+1F96A\", .width = 2 },\n    .{ .codepoint = \"U+1F96B\", .width = 2 },\n    .{ .codepoint = \"U+1F96C\", .width = 2 },\n    .{ .codepoint = \"U+1F96D\", .width = 2 },\n    .{ .codepoint = \"U+1F96E\", .width = 2 },\n    .{ .codepoint = \"U+1F96F\", .width = 2 },\n    .{ .codepoint = \"U+1F970\", .width = 2 },\n    .{ .codepoint = \"U+1F971\", .width = 2 },\n    .{ .codepoint = \"U+1F972\", .width = 2 },\n    .{ .codepoint = \"U+1F973\", .width = 2 },\n    .{ .codepoint = \"U+1F974\", .width = 2 },\n    .{ .codepoint = \"U+1F975\", .width = 2 },\n    .{ .codepoint = \"U+1F976\", .width = 2 },\n    .{ .codepoint = \"U+1F977\", .width = 2 },\n    .{ .codepoint = \"U+1F978\", .width = 2 },\n    .{ .codepoint = \"U+1F979\", .width = 2 },\n    .{ .codepoint = \"U+1F97A\", .width = 2 },\n    .{ .codepoint = \"U+1F97B\", .width = 2 },\n    .{ .codepoint = \"U+1F97C\", .width = 2 },\n    .{ .codepoint = \"U+1F97D\", .width = 2 },\n    .{ .codepoint = \"U+1F97E\", .width = 2 },\n    .{ .codepoint = \"U+1F97F\", .width = 2 },\n    .{ .codepoint = \"U+1F980\", .width = 2 },\n    .{ .codepoint = \"U+1F981\", .width = 2 },\n    .{ .codepoint = \"U+1F982\", .width = 2 },\n    .{ .codepoint = \"U+1F983\", .width = 2 },\n    .{ .codepoint = \"U+1F984\", .width = 2 },\n    .{ .codepoint = \"U+1F985\", .width = 2 },\n    .{ .codepoint = \"U+1F986\", .width = 2 },\n    .{ .codepoint = \"U+1F987\", .width = 2 },\n    .{ .codepoint = \"U+1F988\", .width = 2 },\n    .{ .codepoint = \"U+1F989\", .width = 2 },\n    .{ .codepoint = \"U+1F98A\", .width = 2 },\n    .{ .codepoint = \"U+1F98B\", .width = 2 },\n    .{ .codepoint = \"U+1F98C\", .width = 2 },\n    .{ .codepoint = \"U+1F98D\", .width = 2 },\n    .{ .codepoint = \"U+1F98E\", .width = 2 },\n    .{ .codepoint = \"U+1F98F\", .width = 2 },\n    .{ .codepoint = \"U+1F990\", .width = 2 },\n    .{ .codepoint = \"U+1F991\", .width = 2 },\n    .{ .codepoint = \"U+1F992\", .width = 2 },\n    .{ .codepoint = \"U+1F993\", .width = 2 },\n    .{ .codepoint = \"U+1F994\", .width = 2 },\n    .{ .codepoint = \"U+1F995\", .width = 2 },\n    .{ .codepoint = \"U+1F996\", .width = 2 },\n    .{ .codepoint = \"U+1F997\", .width = 2 },\n    .{ .codepoint = \"U+1F998\", .width = 2 },\n    .{ .codepoint = \"U+1F999\", .width = 2 },\n    .{ .codepoint = \"U+1F99A\", .width = 2 },\n    .{ .codepoint = \"U+1F99B\", .width = 2 },\n    .{ .codepoint = \"U+1F99C\", .width = 2 },\n    .{ .codepoint = \"U+1F99D\", .width = 2 },\n    .{ .codepoint = \"U+1F99E\", .width = 2 },\n    .{ .codepoint = \"U+1F99F\", .width = 2 },\n    .{ .codepoint = \"U+1F9A0\", .width = 2 },\n    .{ .codepoint = \"U+1F9A1\", .width = 2 },\n    .{ .codepoint = \"U+1F9A2\", .width = 2 },\n    .{ .codepoint = \"U+1F9A3\", .width = 2 },\n    .{ .codepoint = \"U+1F9A4\", .width = 2 },\n    .{ .codepoint = \"U+1F9A5\", .width = 2 },\n    .{ .codepoint = \"U+1F9A6\", .width = 2 },\n    .{ .codepoint = \"U+1F9A7\", .width = 2 },\n    .{ .codepoint = \"U+1F9A8\", .width = 2 },\n    .{ .codepoint = \"U+1F9A9\", .width = 2 },\n    .{ .codepoint = \"U+1F9AA\", .width = 2 },\n    .{ .codepoint = \"U+1F9AB\", .width = 2 },\n    .{ .codepoint = \"U+1F9AC\", .width = 2 },\n    .{ .codepoint = \"U+1F9AD\", .width = 2 },\n    .{ .codepoint = \"U+1F9AE\", .width = 2 },\n    .{ .codepoint = \"U+1F9AF\", .width = 2 },\n    .{ .codepoint = \"U+1F9B0\", .width = 2 },\n    .{ .codepoint = \"U+1F9B1\", .width = 2 },\n    .{ .codepoint = \"U+1F9B2\", .width = 2 },\n    .{ .codepoint = \"U+1F9B3\", .width = 2 },\n    .{ .codepoint = \"U+1F9B4\", .width = 2 },\n    .{ .codepoint = \"U+1F9B5\", .width = 2 },\n    .{ .codepoint = \"U+1F9B6\", .width = 2 },\n    .{ .codepoint = \"U+1F9B7\", .width = 2 },\n    .{ .codepoint = \"U+1F9B8\", .width = 2 },\n    .{ .codepoint = \"U+1F9B9\", .width = 2 },\n    .{ .codepoint = \"U+1F9BA\", .width = 2 },\n    .{ .codepoint = \"U+1F9BB\", .width = 2 },\n    .{ .codepoint = \"U+1F9BC\", .width = 2 },\n    .{ .codepoint = \"U+1F9BD\", .width = 2 },\n    .{ .codepoint = \"U+1F9BE\", .width = 2 },\n    .{ .codepoint = \"U+1F9BF\", .width = 2 },\n    .{ .codepoint = \"U+1F9C0\", .width = 2 },\n    .{ .codepoint = \"U+1F9C1\", .width = 2 },\n    .{ .codepoint = \"U+1F9C2\", .width = 2 },\n    .{ .codepoint = \"U+1F9C3\", .width = 2 },\n    .{ .codepoint = \"U+1F9C4\", .width = 2 },\n    .{ .codepoint = \"U+1F9C5\", .width = 2 },\n    .{ .codepoint = \"U+1F9C6\", .width = 2 },\n    .{ .codepoint = \"U+1F9C7\", .width = 2 },\n    .{ .codepoint = \"U+1F9C8\", .width = 2 },\n    .{ .codepoint = \"U+1F9C9\", .width = 2 },\n    .{ .codepoint = \"U+1F9CA\", .width = 2 },\n    .{ .codepoint = \"U+1F9CB\", .width = 2 },\n    .{ .codepoint = \"U+1F9CC\", .width = 2 },\n    .{ .codepoint = \"U+1F9CD\", .width = 2 },\n    .{ .codepoint = \"U+1F9CE\", .width = 2 },\n    .{ .codepoint = \"U+1F9CF\", .width = 2 },\n    .{ .codepoint = \"U+1F9D0\", .width = 2 },\n    .{ .codepoint = \"U+1F9D1\", .width = 2 },\n    .{ .codepoint = \"U+1F9D2\", .width = 2 },\n    .{ .codepoint = \"U+1F9D3\", .width = 2 },\n    .{ .codepoint = \"U+1F9D4\", .width = 2 },\n    .{ .codepoint = \"U+1F9D5\", .width = 2 },\n    .{ .codepoint = \"U+1F9D6\", .width = 2 },\n    .{ .codepoint = \"U+1F9D7\", .width = 2 },\n    .{ .codepoint = \"U+1F9D8\", .width = 2 },\n    .{ .codepoint = \"U+1F9D9\", .width = 2 },\n    .{ .codepoint = \"U+1F9DA\", .width = 2 },\n    .{ .codepoint = \"U+1F9DB\", .width = 2 },\n    .{ .codepoint = \"U+1F9DC\", .width = 2 },\n    .{ .codepoint = \"U+1F9DD\", .width = 2 },\n    .{ .codepoint = \"U+1F9DE\", .width = 2 },\n    .{ .codepoint = \"U+1F9DF\", .width = 2 },\n    .{ .codepoint = \"U+1F9E0\", .width = 2 },\n    .{ .codepoint = \"U+1F9E1\", .width = 2 },\n    .{ .codepoint = \"U+1F9E2\", .width = 2 },\n    .{ .codepoint = \"U+1F9E3\", .width = 2 },\n    .{ .codepoint = \"U+1F9E4\", .width = 2 },\n    .{ .codepoint = \"U+1F9E5\", .width = 2 },\n    .{ .codepoint = \"U+1F9E6\", .width = 2 },\n    .{ .codepoint = \"U+1F9E7\", .width = 2 },\n    .{ .codepoint = \"U+1F9E8\", .width = 2 },\n    .{ .codepoint = \"U+1F9E9\", .width = 2 },\n    .{ .codepoint = \"U+1F9EA\", .width = 2 },\n    .{ .codepoint = \"U+1F9EB\", .width = 2 },\n    .{ .codepoint = \"U+1F9EC\", .width = 2 },\n    .{ .codepoint = \"U+1F9ED\", .width = 2 },\n    .{ .codepoint = \"U+1F9EE\", .width = 2 },\n    .{ .codepoint = \"U+1F9EF\", .width = 2 },\n    .{ .codepoint = \"U+1F9F0\", .width = 2 },\n    .{ .codepoint = \"U+1F9F1\", .width = 2 },\n    .{ .codepoint = \"U+1F9F2\", .width = 2 },\n    .{ .codepoint = \"U+1F9F3\", .width = 2 },\n    .{ .codepoint = \"U+1F9F4\", .width = 2 },\n    .{ .codepoint = \"U+1F9F5\", .width = 2 },\n    .{ .codepoint = \"U+1F9F6\", .width = 2 },\n    .{ .codepoint = \"U+1F9F7\", .width = 2 },\n    .{ .codepoint = \"U+1F9F8\", .width = 2 },\n    .{ .codepoint = \"U+1F9F9\", .width = 2 },\n    .{ .codepoint = \"U+1F9FA\", .width = 2 },\n    .{ .codepoint = \"U+1F9FB\", .width = 2 },\n    .{ .codepoint = \"U+1F9FC\", .width = 2 },\n    .{ .codepoint = \"U+1F9FD\", .width = 2 },\n    .{ .codepoint = \"U+1F9FE\", .width = 2 },\n    .{ .codepoint = \"U+1F9FF\", .width = 2 },\n    .{ .codepoint = \"U+000A\", .width = 0 },\n    .{ .codepoint = \"U+200B\", .width = 0 },\n    .{ .codepoint = \"U+200D\", .width = 0 },\n    .{ .codepoint = \"U+FEFF\", .width = 0 },\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/utf8_no_zwj_test.zig",
    "content": "const std = @import(\"std\");\nconst testing = std.testing;\nconst utf8 = @import(\"../utf8.zig\");\n\ntest \"no_zwj: basic emoji ZWJ sequence split\" {\n    const text = \"👩‍🚀\"; // Woman + ZWJ + Rocket\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n    const width_wcwidth = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n\n    // unicode: single grapheme cluster (width 2)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n\n    // no_zwj: woman (2) + ZWJ breaks + rocket (2) = 4\n    try testing.expectEqual(@as(u32, 4), width_no_zwj);\n\n    // wcwidth: woman (2) + ZWJ (0) + rocket (2) = 4\n    try testing.expectEqual(@as(u32, 4), width_wcwidth);\n}\n\ntest \"no_zwj: family emoji split\" {\n    const text = \"👨‍👩‍👧\"; // Man + ZWJ + Woman + ZWJ + Girl\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n\n    // unicode: single grapheme (width 2)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n\n    // no_zwj: man (2) + woman (2) + girl (2) = 6 (ZWJ is ignored/width 0)\n    try testing.expectEqual(@as(u32, 6), width_no_zwj);\n}\n\ntest \"no_zwj: combining marks still combined\" {\n    const text = \"é\"; // e + combining acute (U+0301)\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n\n    // Both should treat this as a single grapheme with width 1\n    try testing.expectEqual(@as(u32, 1), width_unicode);\n    try testing.expectEqual(@as(u32, 1), width_no_zwj);\n}\n\ntest \"no_zwj: skin tone modifiers still combined\" {\n    const text = \"👋🏿\"; // Wave + dark skin tone\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n    const width_wcwidth = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n\n    // unicode: single grapheme (width 2)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n\n    // no_zwj: should also be single grapheme (width 2) - skin tone is not ZWJ\n    try testing.expectEqual(@as(u32, 2), width_no_zwj);\n\n    // wcwidth: two separate codepoints (2 + 2 = 4)\n    try testing.expectEqual(@as(u32, 4), width_wcwidth);\n}\n\ntest \"no_zwj: flag emoji stays combined\" {\n    const text = \"🇺🇸\"; // US flag (two regional indicators)\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n    const width_wcwidth = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n\n    // unicode: single flag grapheme (width 2)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n\n    // no_zwj: should also be single grapheme (width 2) - no ZWJ involved\n    try testing.expectEqual(@as(u32, 2), width_no_zwj);\n\n    // wcwidth: two separate RIs (1 + 1 = 2)\n    try testing.expectEqual(@as(u32, 2), width_wcwidth);\n}\n\ntest \"no_zwj: mixed text with ZWJ emoji\" {\n    const text = \"Hello👩‍🚀World\"; // Hello + woman astronaut + World\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n\n    // unicode: Hello(5) + astronaut(2) + World(5) = 12\n    try testing.expectEqual(@as(u32, 12), width_unicode);\n\n    // no_zwj: Hello(5) + woman(2) + rocket(2) + World(5) = 14\n    try testing.expectEqual(@as(u32, 14), width_no_zwj);\n}\n\ntest \"no_zwj: findGraphemeInfo splits ZWJ sequences\" {\n    var result_unicode: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result_unicode.deinit(testing.allocator);\n    var result_no_zwj: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result_no_zwj.deinit(testing.allocator);\n\n    const text = \"Hi👩‍🚀Bye\";\n\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result_unicode);\n    try utf8.findGraphemeInfo(text, 4, false, .no_zwj, testing.allocator, &result_no_zwj);\n\n    // unicode: 1 grapheme (the whole ZWJ sequence)\n    try testing.expectEqual(@as(usize, 1), result_unicode.items.len);\n    try testing.expectEqual(@as(u8, 2), result_unicode.items[0].width);\n\n    // no_zwj: 2 graphemes (woman and rocket separately)\n    try testing.expectEqual(@as(usize, 2), result_no_zwj.items.len);\n    try testing.expectEqual(@as(u8, 2), result_no_zwj.items[0].width);\n    try testing.expectEqual(@as(u8, 2), result_no_zwj.items[1].width);\n}\n\ntest \"no_zwj: findWrapPosByWidth with ZWJ sequences\" {\n    const text = \"AB👩‍🚀CD\"; // A(1) B(1) woman(2) rocket(2) C(1) D(1)\n\n    const result_unicode = utf8.findWrapPosByWidth(text, 4, 4, false, .unicode);\n    const result_no_zwj = utf8.findWrapPosByWidth(text, 4, 4, false, .no_zwj);\n\n    // unicode: stops after \"AB👩‍🚀\" = 4 columns (whole sequence)\n    try testing.expectEqual(@as(u32, 4), result_unicode.columns_used);\n\n    // no_zwj: stops after \"AB👩\" = 4 columns (woman only)\n    try testing.expectEqual(@as(u32, 4), result_no_zwj.columns_used);\n}\n\ntest \"no_zwj: findPosByWidth with ZWJ sequences\" {\n    const text = \"AB👩‍🚀CD\"; // A(1) B(1) woman(2) rocket(2) C(1) D(1)\n\n    // With include_start_before=false\n    const start4_unicode = utf8.findPosByWidth(text, 4, 4, false, false, .unicode);\n    const start4_no_zwj = utf8.findPosByWidth(text, 4, 4, false, false, .no_zwj);\n\n    // unicode: Woman+ZWJ+Rocket is one grapheme that ends at col 4, so it's included\n    // Stops at 'C' (byte 13)\n    try testing.expectEqual(@as(u32, 13), start4_unicode.byte_offset);\n    try testing.expectEqual(@as(u32, 4), start4_unicode.columns_used);\n\n    // no_zwj: Woman (col 2-4) ends at 4, included. Rocket (col 4-6) ends at 6 > 4, excluded\n    // Stops after woman+ZWJ (byte 9, before rocket)\n    try testing.expectEqual(@as(u32, 9), start4_no_zwj.byte_offset);\n    try testing.expectEqual(@as(u32, 4), start4_no_zwj.columns_used);\n\n    // With include_start_before=true\n    const end4_unicode = utf8.findPosByWidth(text, 4, 4, false, true, .unicode);\n    const end4_no_zwj = utf8.findPosByWidth(text, 4, 4, false, true, .no_zwj);\n\n    // unicode: includes whole sequence\n    try testing.expectEqual(@as(u32, 4), end4_unicode.columns_used);\n\n    // no_zwj: includes woman only\n    try testing.expectEqual(@as(u32, 4), end4_no_zwj.columns_used);\n}\n\ntest \"no_zwj: getWidthAt with ZWJ sequence\" {\n    const text = \"👩‍🚀\"; // Woman + ZWJ + Rocket\n\n    // At woman emoji (byte 0)\n    const width_woman_unicode = utf8.getWidthAt(text, 0, 4, .unicode);\n    const width_woman_no_zwj = utf8.getWidthAt(text, 0, 4, .no_zwj);\n\n    // unicode: whole sequence width\n    try testing.expectEqual(@as(u32, 2), width_woman_unicode);\n\n    // no_zwj: just woman\n    try testing.expectEqual(@as(u32, 2), width_woman_no_zwj);\n\n    // At ZWJ (byte 4)\n    const width_zwj_no_zwj = utf8.getWidthAt(text, 4, 4, .no_zwj);\n    try testing.expectEqual(@as(u32, 0), width_zwj_no_zwj); // ZWJ itself has width 0\n}\n\ntest \"no_zwj: getPrevGraphemeStart with ZWJ sequence\" {\n    const text = \"AB👩‍🚀CD\";\n\n    // From end of text (after 'D')\n    const r1_unicode = utf8.getPrevGraphemeStart(text, text.len, 4, .unicode);\n    const r1_no_zwj = utf8.getPrevGraphemeStart(text, text.len, 4, .no_zwj);\n\n    try testing.expect(r1_unicode != null);\n    try testing.expect(r1_no_zwj != null);\n\n    // unicode: should point to 'D' (last ASCII char)\n    try testing.expectEqual(@as(u32, 1), r1_unicode.?.width);\n\n    // no_zwj: should also point to 'D'\n    try testing.expectEqual(@as(u32, 1), r1_no_zwj.?.width);\n}\n\ntest \"no_zwj: multiple ZWJ sequences\" {\n    const text = \"👨‍👩‍👧👨‍👩‍👦\"; // Family with girl + Family with boy\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n\n    // unicode: 2 families = 4 columns\n    try testing.expectEqual(@as(u32, 4), width_unicode);\n\n    // no_zwj: 6 people = 12 columns (each person is width 2)\n    try testing.expectEqual(@as(u32, 12), width_no_zwj);\n}\n\ntest \"no_zwj: ZWJ with skin tones\" {\n    const text = \"👨🏿‍❤️‍👨🏻\"; // Man with dark skin + ZWJ + heart + ZWJ + man with light skin\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n\n    // unicode: single couple grapheme (width 2)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n\n    // no_zwj: man+skin (2) + heart+VS16 (2) + man+skin (2) = 6\n    try testing.expectEqual(@as(u32, 6), width_no_zwj);\n}\n\ntest \"no_zwj: keycap sequences without ZWJ\" {\n    const text = \"1️⃣\"; // 1 + VS16 + combining enclosing keycap\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n    const width_wcwidth = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n\n    // unicode and no_zwj should both treat this as a single grapheme\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n    try testing.expectEqual(@as(u32, 2), width_no_zwj);\n\n    // wcwidth counts each codepoint: 1(1) + VS16(0) + keycap combining(0) = 1\n    // Keycap is a combining character that doesn't add width\n    try testing.expectEqual(@as(u32, 1), width_wcwidth);\n}\n\ntest \"no_zwj: rainbow flag without ZWJ\" {\n    const text = \"🏳️‍🌈\"; // White flag + VS16 + ZWJ + rainbow\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n\n    // unicode: single rainbow flag grapheme (width 2)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n\n    // no_zwj: flag+VS16 (2) + rainbow (2) = 4 (ZWJ causes break)\n    try testing.expectEqual(@as(u32, 4), width_no_zwj);\n}\n\ntest \"no_zwj: Devanagari conjuncts still work\" {\n    const text = \"क्ष\"; // Ka + virama + Sha (conjunct)\n\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n    const width_no_zwj = utf8.calculateTextWidth(text, 4, false, .no_zwj);\n    const width_wcwidth = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n\n    // unicode: Devanagari renders as width 2 in terminals (Ka=1 + Sha=1)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n\n    // no_zwj: should behave same as unicode for non-ZWJ sequences\n    try testing.expectEqual(@as(u32, 2), width_no_zwj);\n\n    // wcwidth: counts each codepoint separately (same result)\n    try testing.expectEqual(@as(u32, 2), width_wcwidth); // Ka(1) + virama(0) + Sha(1)\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/utf8_test.zig",
    "content": "const std = @import(\"std\");\nconst testing = std.testing;\nconst utf8 = @import(\"../utf8.zig\");\n\n// ============================================================================\n// ASCII-ONLY DETECTION TESTS\n// ============================================================================\n\ntest \"isAsciiOnly: empty string\" {\n    // Empty string is not ASCII-only by convention\n    try testing.expect(!utf8.isAsciiOnly(\"\"));\n}\n\ntest \"isAsciiOnly: simple ASCII\" {\n    try testing.expect(utf8.isAsciiOnly(\"Hello, World!\"));\n    try testing.expect(utf8.isAsciiOnly(\"The quick brown fox\"));\n    try testing.expect(utf8.isAsciiOnly(\"0123456789\"));\n    try testing.expect(utf8.isAsciiOnly(\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"));\n}\n\ntest \"isAsciiOnly: control chars rejected\" {\n    try testing.expect(!utf8.isAsciiOnly(\"Hello\\tWorld\"));\n    try testing.expect(!utf8.isAsciiOnly(\"Hello\\nWorld\"));\n    try testing.expect(!utf8.isAsciiOnly(\"Hello\\rWorld\"));\n    try testing.expect(!utf8.isAsciiOnly(\"\\x00\"));\n    try testing.expect(!utf8.isAsciiOnly(\"\\x1F\"));\n}\n\ntest \"isAsciiOnly: extended ASCII rejected\" {\n    try testing.expect(!utf8.isAsciiOnly(\"Hello\\x7FWorld\"));\n    try testing.expect(!utf8.isAsciiOnly(\"Hello\\x80World\"));\n    try testing.expect(!utf8.isAsciiOnly(\"Hello\\xFFWorld\"));\n}\n\ntest \"isAsciiOnly: Unicode rejected\" {\n    try testing.expect(!utf8.isAsciiOnly(\"Hello 👋\"));\n    try testing.expect(!utf8.isAsciiOnly(\"Hello 世界\"));\n    try testing.expect(!utf8.isAsciiOnly(\"café\"));\n    try testing.expect(!utf8.isAsciiOnly(\"Привет\"));\n}\n\ntest \"isAsciiOnly: space character accepted\" {\n    try testing.expect(utf8.isAsciiOnly(\" \"));\n    try testing.expect(utf8.isAsciiOnly(\"   \"));\n    try testing.expect(utf8.isAsciiOnly(\"Hello World\"));\n}\n\ntest \"isAsciiOnly: all printable ASCII chars\" {\n    const all_printable = \" !\\\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\";\n    try testing.expect(utf8.isAsciiOnly(all_printable));\n}\n\ntest \"isAsciiOnly: SIMD boundary tests\" {\n    try testing.expect(utf8.isAsciiOnly(\"0123456789abcdef\"));\n    try testing.expect(utf8.isAsciiOnly(\"0123456789abcde\"));\n    try testing.expect(utf8.isAsciiOnly(\"0123456789abcdefg\"));\n    try testing.expect(utf8.isAsciiOnly(\"0123456789abcdef0123456789abcdef\"));\n    try testing.expect(utf8.isAsciiOnly(\"0123456789abcdef0123456789abcdefX\"));\n}\n\ntest \"isAsciiOnly: non-ASCII at different positions\" {\n    try testing.expect(!utf8.isAsciiOnly(\"Hello\\x00World\"));\n    try testing.expect(!utf8.isAsciiOnly(\"\\x00bcdefghijklmnop\"));\n    try testing.expect(!utf8.isAsciiOnly(\"0123456789abcde\\x00\"));\n    try testing.expect(!utf8.isAsciiOnly(\"0123456789abcdef\\x00\"));\n    try testing.expect(!utf8.isAsciiOnly(\"0123456789abcdef0123456789\\x00bcdef\"));\n    try testing.expect(!utf8.isAsciiOnly(\"0123456789abcdef01234\\x00\"));\n}\n\ntest \"isAsciiOnly: large ASCII text\" {\n    const size = 10000;\n    const buf = try testing.allocator.alloc(u8, size);\n    defer testing.allocator.free(buf);\n\n    for (buf, 0..) |*b, i| {\n        b.* = 32 + @as(u8, @intCast(i % 95));\n    }\n\n    try testing.expect(utf8.isAsciiOnly(buf));\n\n    buf[5000] = 0x80;\n    try testing.expect(!utf8.isAsciiOnly(buf));\n}\n\n// ============================================================================\n// LINE BREAK TESTS\n// ============================================================================\n\nconst LineBreakTestCase = struct {\n    name: []const u8,\n    input: []const u8,\n    expected: []const usize,\n};\n\nconst line_break_golden_tests = [_]LineBreakTestCase{\n    .{\n        .name = \"empty string\",\n        .input = \"\",\n        .expected = &[_]usize{},\n    },\n    .{\n        .name = \"only LF\",\n        .input = \"a\\nb\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"only CR\",\n        .input = \"a\\rb\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"CRLF\",\n        .input = \"a\\r\\nb\",\n        .expected = &[_]usize{2}, // CRLF recorded at \\n index\n    },\n    .{\n        .name = \"ending with CR\",\n        .input = \"a\\r\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"ending with LF\",\n        .input = \"a\\n\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"ending with CRLF\",\n        .input = \"a\\r\\n\",\n        .expected = &[_]usize{2},\n    },\n    .{\n        .name = \"consecutive LF\",\n        .input = \"\\n\\n\",\n        .expected = &[_]usize{ 0, 1 },\n    },\n    .{\n        .name = \"consecutive CRLF\",\n        .input = \"\\r\\n\\r\\n\",\n        .expected = &[_]usize{ 1, 3 },\n    },\n    .{\n        .name = \"mixed breaks\",\n        .input = \"\\n\\r\\n\\r\",\n        .expected = &[_]usize{ 0, 2, 3 },\n    },\n    .{\n        .name = \"CR LF separate\",\n        .input = \"\\r\\r\\n\",\n        .expected = &[_]usize{ 0, 2 },\n    },\n    .{\n        .name = \"very long line no breaks\",\n        .input = \"a\" ** 1000,\n        .expected = &[_]usize{},\n    },\n    .{\n        .name = \"multiple LF\",\n        .input = \"line1\\nline2\\nline3\\n\",\n        .expected = &[_]usize{ 5, 11, 17 },\n    },\n    .{\n        .name = \"multiple CRLF\",\n        .input = \"line1\\r\\nline2\\r\\nline3\\r\\n\",\n        .expected = &[_]usize{ 6, 13, 20 },\n    },\n    .{\n        .name = \"mixed line endings\",\n        .input = \"unix\\nmac\\rwin\\r\\n\",\n        .expected = &[_]usize{ 4, 8, 13 },\n    },\n};\n\nfn testLineBreaks(test_case: LineBreakTestCase, allocator: std.mem.Allocator) !void {\n    var result = utf8.LineBreakResult.init(allocator);\n    defer result.deinit();\n\n    try utf8.findLineBreaks(test_case.input, &result);\n\n    try testing.expectEqual(test_case.expected.len, result.breaks.items.len);\n\n    for (test_case.expected, 0..) |exp, i| {\n        try testing.expectEqual(exp, result.breaks.items[i].pos);\n    }\n}\n\ntest \"line breaks: golden tests\" {\n    for (line_break_golden_tests) |tc| {\n        try testLineBreaks(tc, testing.allocator);\n    }\n}\n\ntest \"line breaks: CRLF at SIMD16 edge (15-16)\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    buf[15] = '\\r';\n    buf[16] = '\\n';\n\n    const expected = [_]usize{16}; // CRLF recorded at \\n index\n\n    try testLineBreaks(.{\n        .name = \"CRLF@15-16\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"line breaks: multiple breaks around SIMD16 boundary\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    buf[14] = '\\n';\n    buf[15] = '\\r';\n    buf[16] = '\\n';\n    buf[17] = '\\n';\n\n    const expected = [_]usize{ 14, 16, 17 }; // 15-16 is CRLF\n\n    try testLineBreaks(.{\n        .name = \"multi@boundary\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"line breaks: multibyte adjacent to LF\" {\n    const input = \"é\\n\";\n    const expected = [_]usize{2};\n\n    try testLineBreaks(.{\n        .name = \"é\\\\n\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"line breaks: multibyte adjacent to CRLF\" {\n    const input = \"漢\\r\\n\";\n    const expected = [_]usize{4};\n\n    try testLineBreaks(.{\n        .name = \"漢\\\\r\\\\n\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"line breaks: multibyte at SIMD boundary without breaks\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 0);\n\n    const text = \"Test世界Test\";\n    @memcpy(buf[0..text.len], text);\n\n    const expected = [_]usize{};\n\n    try testLineBreaks(.{\n        .name = \"unicode@boundary\",\n        .input = buf[0..text.len],\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"line breaks: realistic text\" {\n    const sample_text =\n        \"The quick brown fox jumps over the lazy dog.\\n\" ++\n        \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\\n\" ++\n        \"Windows uses CRLF line endings.\\r\\n\" ++\n        \"Unix uses LF line endings.\\n\" ++\n        \"Classic Mac used CR line endings.\\r\" ++\n        \"UTF-8 text: 世界 こんにちは\\n\" ++\n        \"Multiple\\n\\nEmpty\\n\\n\\nLines\\n\" ++\n        \"Mixed\\r\\nendings\\nhere\\r\";\n\n    var result = utf8.LineBreakResult.init(testing.allocator);\n    defer result.deinit();\n\n    try utf8.findLineBreaks(sample_text, &result);\n\n    // Verify we found some breaks\n    try testing.expect(result.breaks.items.len > 0);\n}\n\ntest \"line breaks: random small buffers\" {\n    var prng = std.Random.DefaultPrng.init(42);\n    const random = prng.random();\n\n    var i: usize = 0;\n    while (i < 50) : (i += 1) {\n        const size = 16 + random.uintLessThan(usize, 1024);\n        const buf = try testing.allocator.alloc(u8, size);\n        defer testing.allocator.free(buf);\n\n        for (buf) |*b| {\n            const r = random.uintLessThan(u8, 100);\n            if (r < 5) {\n                b.* = '\\n';\n            } else if (r < 10) {\n                b.* = '\\r';\n            } else {\n                b.* = 'a' + random.uintLessThan(u8, 26);\n            }\n        }\n\n        var result = utf8.LineBreakResult.init(testing.allocator);\n        defer result.deinit();\n        try utf8.findLineBreaks(buf, &result);\n    }\n}\n\n// ============================================================================\n// TAB STOP TESTS\n// ============================================================================\n\nconst TabStopTestCase = struct {\n    name: []const u8,\n    input: []const u8,\n    expected: []const usize,\n};\n\nconst tab_stop_golden_tests = [_]TabStopTestCase{\n    .{\n        .name = \"empty string\",\n        .input = \"\",\n        .expected = &[_]usize{},\n    },\n    .{\n        .name = \"no tabs\",\n        .input = \"hello world\",\n        .expected = &[_]usize{},\n    },\n    .{\n        .name = \"single tab\",\n        .input = \"a\\tb\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"multiple tabs\",\n        .input = \"a\\tb\\tc\",\n        .expected = &[_]usize{ 1, 3 },\n    },\n    .{\n        .name = \"tab at start\",\n        .input = \"\\tabc\",\n        .expected = &[_]usize{0},\n    },\n    .{\n        .name = \"tab at end\",\n        .input = \"abc\\t\",\n        .expected = &[_]usize{3},\n    },\n    .{\n        .name = \"consecutive tabs\",\n        .input = \"a\\t\\tb\",\n        .expected = &[_]usize{ 1, 2 },\n    },\n    .{\n        .name = \"only tabs\",\n        .input = \"\\t\\t\\t\",\n        .expected = &[_]usize{ 0, 1, 2 },\n    },\n    .{\n        .name = \"tabs mixed with spaces\",\n        .input = \"a \\tb \\tc\",\n        .expected = &[_]usize{ 2, 5 },\n    },\n    .{\n        .name = \"tab with newline\",\n        .input = \"a\\tb\\nc\\td\",\n        .expected = &[_]usize{ 1, 5 },\n    },\n    .{\n        .name = \"many tabs\",\n        .input = \"\\ta\\tb\\tc\\td\\te\\tf\\t\",\n        .expected = &[_]usize{ 0, 2, 4, 6, 8, 10, 12 },\n    },\n};\n\nfn testTabStops(test_case: TabStopTestCase, allocator: std.mem.Allocator) !void {\n    var result = utf8.TabStopResult.init(allocator);\n    defer result.deinit();\n\n    try utf8.findTabStops(test_case.input, &result);\n\n    try testing.expectEqual(test_case.expected.len, result.positions.items.len);\n\n    for (test_case.expected, 0..) |exp, i| {\n        try testing.expectEqual(exp, result.positions.items[i]);\n    }\n}\n\ntest \"tab stops: golden tests\" {\n    for (tab_stop_golden_tests) |tc| {\n        try testTabStops(tc, testing.allocator);\n    }\n}\n\ntest \"tab stops: tab at SIMD16 edge (15)\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    buf[15] = '\\t';\n    buf[16] = 'y';\n\n    const expected = [_]usize{15};\n\n    try testTabStops(.{\n        .name = \"tab@15\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: tab at SIMD16 edge (16)\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    buf[16] = '\\t';\n    buf[17] = 'y';\n\n    const expected = [_]usize{16};\n\n    try testTabStops(.{\n        .name = \"tab@16\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: multiple tabs around SIMD16 boundary\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    buf[14] = '\\t';\n    buf[15] = '\\t';\n    buf[16] = '\\t';\n    buf[17] = '\\t';\n\n    const expected = [_]usize{ 14, 15, 16, 17 };\n\n    try testTabStops(.{\n        .name = \"tabs@boundary\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: tabs in all SIMD lanes\" {\n    var buf: [16]u8 = undefined;\n    for (&buf) |*b| {\n        b.* = '\\t';\n    }\n\n    const expected = [_]usize{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };\n\n    try testTabStops(.{\n        .name = \"all_tabs\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: multibyte adjacent to tab\" {\n    const input = \"é\\ttest\"; // é is 2 bytes: 0xC3 0xA9\n    const expected = [_]usize{2}; // Tab at index 2\n\n    try testTabStops(.{\n        .name = \"é\\\\t\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: CJK adjacent to tab\" {\n    const input = \"漢\\ttest\"; // 漢 is 3 bytes: 0xE6 0xBC 0xA2\n    const expected = [_]usize{3}; // Tab at index 3\n\n    try testTabStops(.{\n        .name = \"漢\\\\t\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: emoji adjacent to tab\" {\n    const input = \"👋\\twave\"; // 👋 is 4 bytes\n    const expected = [_]usize{4}; // Tab at index 4\n\n    try testTabStops(.{\n        .name = \"emoji\\\\t\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: multibyte at SIMD boundary without tabs\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 0);\n\n    const text = \"Test世界Test\";\n    @memcpy(buf[0..text.len], text);\n\n    const expected = [_]usize{}; // No tabs\n\n    try testTabStops(.{\n        .name = \"unicode@boundary\",\n        .input = buf[0..text.len],\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: realistic code text\" {\n    const sample_text =\n        \"function test() {\\n\" ++\n        \"\\tconst x = 10;\\n\" ++\n        \"\\tif (x > 5) {\\n\" ++\n        \"\\t\\treturn true;\\n\" ++\n        \"\\t}\\n\" ++\n        \"\\treturn false;\\n\" ++\n        \"}\\n\";\n\n    var result = utf8.TabStopResult.init(testing.allocator);\n    defer result.deinit();\n\n    try utf8.findTabStops(sample_text, &result);\n\n    // Should find 6 tabs (including double-tab for nested return)\n    try testing.expectEqual(@as(usize, 6), result.positions.items.len);\n}\n\ntest \"tab stops: TSV data\" {\n    const tsv_line = \"name\\tage\\tcity\\tcountry\";\n    const expected = [_]usize{ 4, 8, 13 };\n\n    try testTabStops(.{\n        .name = \"tsv\",\n        .input = tsv_line,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: random small buffers\" {\n    var prng = std.Random.DefaultPrng.init(42);\n    const random = prng.random();\n\n    var i: usize = 0;\n    while (i < 50) : (i += 1) {\n        const size = 16 + random.uintLessThan(usize, 1024);\n        const buf = try testing.allocator.alloc(u8, size);\n        defer testing.allocator.free(buf);\n\n        for (buf) |*b| {\n            const r = random.uintLessThan(u8, 100);\n            if (r < 10) {\n                b.* = '\\t';\n            } else {\n                b.* = 'a' + random.uintLessThan(u8, 26);\n            }\n        }\n\n        var result = utf8.TabStopResult.init(testing.allocator);\n        defer result.deinit();\n        try utf8.findTabStops(buf, &result);\n    }\n}\n\ntest \"tab stops: large buffer with periodic tabs\" {\n    const size = 10000;\n    const buf = try testing.allocator.alloc(u8, size);\n    defer testing.allocator.free(buf);\n\n    var expected_count: usize = 0;\n    for (buf, 0..) |*b, idx| {\n        if (idx % 50 == 0) {\n            b.* = '\\t';\n            expected_count += 1;\n        } else {\n            b.* = 'a' + @as(u8, @intCast(idx % 26));\n        }\n    }\n\n    var result = utf8.TabStopResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findTabStops(buf, &result);\n\n    try testing.expectEqual(expected_count, result.positions.items.len);\n}\n\ntest \"tab stops: exactly 16 bytes with tab\" {\n    const input = \"0123456789abcd\\tx\"; // exactly 16 bytes with tab at pos 14\n    const expected = [_]usize{14};\n\n    try testTabStops(.{\n        .name = \"16bytes_with_tab\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: exactly 16 bytes no tab\" {\n    const input = \"0123456789abcdef\"; // exactly 16 bytes, no tab\n    const expected = [_]usize{};\n\n    try testTabStops(.{\n        .name = \"16bytes_no_tab\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: 17 bytes with tab at 16\" {\n    const input = \"0123456789abcdef\\t\"; // tab at position 16\n    const expected = [_]usize{16};\n\n    try testTabStops(.{\n        .name = \"tab@16\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: result reuse\" {\n    var result = utf8.TabStopResult.init(testing.allocator);\n    defer result.deinit();\n\n    // First use\n    try utf8.findTabStops(\"a\\tb\\tc\", &result);\n    try testing.expectEqual(@as(usize, 2), result.positions.items.len);\n\n    // Second use - should reset automatically\n    try utf8.findTabStops(\"x\\ty\", &result);\n    try testing.expectEqual(@as(usize, 1), result.positions.items.len);\n    try testing.expectEqual(@as(usize, 1), result.positions.items[0]);\n}\n\ntest \"tab stops: mixed with other whitespace\" {\n    const input = \"  \\t  \\t  \";\n    const expected = [_]usize{ 2, 5 };\n\n    try testTabStops(.{\n        .name = \"mixed_whitespace\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"tab stops: makefile style\" {\n    const makefile = \"target:\\n\\t@echo Building\\n\\t@gcc -o out main.c\\n\";\n\n    var result = utf8.TabStopResult.init(testing.allocator);\n    defer result.deinit();\n\n    try utf8.findTabStops(makefile, &result);\n\n    // Should find 2 tabs (one per command line)\n    try testing.expectEqual(@as(usize, 2), result.positions.items.len);\n}\n\ntest \"tab stops: tabs across multiple SIMD chunks\" {\n    const size = 64; // 4 SIMD chunks\n    const buf = try testing.allocator.alloc(u8, size);\n    defer testing.allocator.free(buf);\n\n    @memset(buf, 'x');\n    buf[0] = '\\t';\n    buf[16] = '\\t';\n    buf[32] = '\\t';\n    buf[48] = '\\t';\n    buf[63] = '\\t';\n\n    const expected = [_]usize{ 0, 16, 32, 48, 63 };\n\n    try testTabStops(.{\n        .name = \"multi_chunk\",\n        .input = buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\n// ============================================================================\n// WORD WRAP BREAK TESTS\n// ============================================================================\n\nconst WrapBreakTestCase = struct {\n    name: []const u8,\n    input: []const u8,\n    expected: []const usize,\n};\n\nconst wrap_break_golden_tests = [_]WrapBreakTestCase{\n    .{\n        .name = \"empty string\",\n        .input = \"\",\n        .expected = &[_]usize{},\n    },\n    .{\n        .name = \"no breaks\",\n        .input = \"abcdef\",\n        .expected = &[_]usize{},\n    },\n    .{\n        .name = \"single space\",\n        .input = \"a b\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"multiple spaces\",\n        .input = \"a b c\",\n        .expected = &[_]usize{ 1, 3 },\n    },\n    .{\n        .name = \"tab character\",\n        .input = \"a\\tb\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"newline\",\n        .input = \"a\\nb\",\n        .expected = &[_]usize{},\n    },\n    .{\n        .name = \"carriage return\",\n        .input = \"a\\rb\",\n        .expected = &[_]usize{},\n    },\n    .{\n        .name = \"dash\",\n        .input = \"pre-post\",\n        .expected = &[_]usize{3},\n    },\n    .{\n        .name = \"forward slash\",\n        .input = \"path/to/file\",\n        .expected = &[_]usize{ 4, 7 },\n    },\n    .{\n        .name = \"backslash\",\n        .input = \"path\\\\to\\\\file\",\n        .expected = &[_]usize{ 4, 7 },\n    },\n    .{\n        .name = \"punctuation\",\n        .input = \"Hello, world! How are you? Fine.\",\n        .expected = &[_]usize{ 5, 6, 12, 13, 17, 21, 25, 26, 31 },\n    },\n    .{\n        .name = \"brackets\",\n        .input = \"(a)[b]{c}\",\n        .expected = &[_]usize{ 0, 2, 3, 5, 6, 8 },\n    },\n    .{\n        .name = \"mixed breaks\",\n        .input = \"Hello, world! -path/file.\",\n        .expected = &[_]usize{ 5, 6, 12, 13, 14, 19, 24 },\n    },\n    .{\n        .name = \"consecutive spaces\",\n        .input = \"a  b\",\n        .expected = &[_]usize{ 1, 2 },\n    },\n    .{\n        .name = \"only spaces\",\n        .input = \"   \",\n        .expected = &[_]usize{ 0, 1, 2 },\n    },\n    .{\n        .name = \"all break types\",\n        .input = \" \\t-/\\\\.,:;!?()[]{}\",\n        .expected = &[_]usize{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 },\n    },\n    .{\n        .name = \"nbsp\",\n        .input = \"a\\u{00A0}b\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"em space\",\n        .input = \"a\\u{2003}b\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"ideo space\",\n        .input = \"a\\u{3000}b\",\n        .expected = &[_]usize{1},\n    },\n    .{\n        .name = \"soft hyphen\",\n        .input = \"pre\\u{00AD}post\",\n        .expected = &[_]usize{3},\n    },\n    .{\n        .name = \"unicode hyphen\",\n        .input = \"pre\\u{2010}post\",\n        .expected = &[_]usize{3},\n    },\n    .{\n        .name = \"zero width space\",\n        .input = \"a\\u{200B}b\",\n        .expected = &[_]usize{1},\n    },\n};\n\nfn testWrapBreaks(test_case: WrapBreakTestCase, allocator: std.mem.Allocator) !void {\n    var result = utf8.WrapBreakResult.init(allocator);\n    defer result.deinit();\n\n    try utf8.findWrapBreaks(test_case.input, &result, .unicode);\n\n    try testing.expectEqual(test_case.expected.len, result.breaks.items.len);\n\n    for (test_case.expected, 0..) |exp, i| {\n        try testing.expectEqual(exp, result.breaks.items[i].byte_offset);\n    }\n}\n\ntest \"wrap breaks: golden tests\" {\n    for (wrap_break_golden_tests) |tc| {\n        try testWrapBreaks(tc, testing.allocator);\n    }\n}\n\ntest \"wrap breaks: space at SIMD16 edge (15)\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    buf[15] = ' ';\n    buf[16] = 'y';\n\n    const expected = [_]usize{15};\n\n    try testWrapBreaks(.{\n        .name = \"space@15\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"wrap breaks: unicode NBSP at SIMD16 edge (15)\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    // NBSP U+00A0 = 0xC2 0xA0\n    buf[15] = 0xC2;\n    buf[16] = 0xA0;\n\n    const expected = [_]usize{15};\n\n    try testWrapBreaks(.{\n        .name = \"nbsp@15\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"wrap breaks: multiple breaks around SIMD16 boundary\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    buf[14] = ' ';\n    buf[15] = '-';\n    buf[16] = '/';\n    buf[17] = '.';\n\n    const expected = [_]usize{ 14, 15, 16, 17 };\n\n    try testWrapBreaks(.{\n        .name = \"multi@boundary\",\n        .input = &buf,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"wrap breaks: multibyte adjacent to space\" {\n    const input = \"é test\"; // é is 2 bytes: 0xC3 0xA9\n    const expected = [_]usize{2}; // Space at index 2\n\n    try testWrapBreaks(.{\n        .name = \"é space\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"wrap breaks: multibyte adjacent to dash\" {\n    const input = \"漢-test\"; // 漢 is 3 bytes: 0xE6 0xBC 0xA2\n    const expected = [_]usize{3}; // Dash at index 3\n\n    try testWrapBreaks(.{\n        .name = \"漢-\",\n        .input = input,\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"wrap breaks: multibyte at SIMD boundary with script transitions\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 0);\n\n    // Place UTF-8 sequences around boundary\n    const text = \"Test世界Test\";\n    @memcpy(buf[0..text.len], text);\n\n    //// Breaks at ASCII<->CJK transitions:\n    // - after 't' in \"Test\" (byte 3)\n    // - after '界' before \"Test\" (byte 7)\n    const expected = [_]usize{ 3, 7 };\n\n    try testWrapBreaks(.{\n        .name = \"unicode@boundary\",\n        .input = buf[0..text.len],\n        .expected = &expected,\n    }, testing.allocator);\n}\n\ntest \"wrap breaks: realistic text\" {\n    const sample_text =\n        \"The quick brown fox jumps over the lazy dog.\\n\" ++\n        \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\\n\" ++\n        \"File paths: /usr/local/bin and C:\\\\Windows\\\\System32\\n\" ++\n        \"Punctuation test: Hello, world! How are you? I'm fine.\\n\" ++\n        \"Brackets test: (parentheses) [square] {curly}\\n\" ++\n        \"Dashes test: pre-dash post-dash multi-word-expression\\n\" ++\n        \"Mixed: Hello, /path/to-file.txt [done]!\\n\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n\n    try utf8.findWrapBreaks(sample_text, &result, .unicode);\n\n    // Verify we found many breaks\n    try testing.expect(result.breaks.items.len > 0);\n}\n\ntest \"wrap breaks: random small buffers\" {\n    var prng = std.Random.DefaultPrng.init(42);\n    const random = prng.random();\n\n    const break_chars = \" \\t-/\\\\.,:;!?()[]{}\";\n\n    var i: usize = 0;\n    while (i < 50) : (i += 1) {\n        const size = 16 + random.uintLessThan(usize, 1024);\n        const buf = try testing.allocator.alloc(u8, size);\n        defer testing.allocator.free(buf);\n\n        // Fill with ASCII letters and randomly insert breaks\n        for (buf) |*b| {\n            const r = random.uintLessThan(u8, 100);\n            if (r < 20) {\n                const break_idx = random.uintLessThan(usize, break_chars.len);\n                b.* = break_chars[break_idx];\n            } else {\n                b.* = 'a' + random.uintLessThan(u8, 26);\n            }\n        }\n\n        var result = utf8.WrapBreakResult.init(testing.allocator);\n        defer result.deinit();\n        try utf8.findWrapBreaks(buf, &result, .unicode);\n    }\n}\n\ntest \"wrap breaks: large buffer\" {\n    const size = 10000;\n    const buf = try testing.allocator.alloc(u8, size);\n    defer testing.allocator.free(buf);\n\n    // Create realistic text with periodic breaks\n    for (buf, 0..) |*b, idx| {\n        if (idx % 50 == 0) {\n            b.* = ' ';\n        } else if (idx % 75 == 0) {\n            b.* = '-';\n        } else {\n            b.* = 'a' + @as(u8, @intCast(idx % 26));\n        }\n    }\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(buf, &result, .unicode);\n\n    try testing.expect(result.breaks.items.len > 0);\n}\n\ntest \"wrap breaks: buffer exceeding 64KB\" {\n    const size = 100_000;\n    const buf = try testing.allocator.alloc(u8, size);\n    defer testing.allocator.free(buf);\n\n    @memset(buf, 'a');\n\n    // Place a space at 70000, with u16, this will truncate to 4464 (70000 % 65536)\n    const break_pos: usize = 70_000;\n    buf[break_pos] = ' ';\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(buf, &result, .unicode);\n\n    // Should find exactly one wrap break\n    try testing.expectEqual(@as(usize, 1), result.breaks.items.len);\n\n    // The byte_offset must be the actual position, not truncated\n    try testing.expectEqual(@as(u32, break_pos), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u32, break_pos), result.breaks.items[0].char_offset);\n}\n\n// ============================================================================\n// EDGE CASES AND INTEGRATION TESTS\n// ============================================================================\n\ntest \"edge case: result reuse\" {\n    var line_result = utf8.LineBreakResult.init(testing.allocator);\n    defer line_result.deinit();\n\n    // First use - line breaks\n    try utf8.findLineBreaks(\"a\\nb\\nc\", &line_result);\n    try testing.expectEqual(@as(usize, 2), line_result.breaks.items.len);\n\n    // Second use - should reset automatically\n    try utf8.findLineBreaks(\"x\\ny\", &line_result);\n    try testing.expectEqual(@as(usize, 1), line_result.breaks.items.len);\n    try testing.expectEqual(@as(usize, 1), line_result.breaks.items[0].pos);\n\n    // Third use - wrap breaks (different result type)\n    var wrap_result = utf8.WrapBreakResult.init(testing.allocator);\n    defer wrap_result.deinit();\n    try utf8.findWrapBreaks(\"a b c\", &wrap_result, .unicode);\n    try testing.expectEqual(@as(usize, 2), wrap_result.breaks.items.len);\n}\n\ntest \"edge case: empty input\" {\n    var line_result = utf8.LineBreakResult.init(testing.allocator);\n    defer line_result.deinit();\n\n    try utf8.findLineBreaks(\"\", &line_result);\n    try testing.expectEqual(@as(usize, 0), line_result.breaks.items.len);\n\n    var wrap_result = utf8.WrapBreakResult.init(testing.allocator);\n    defer wrap_result.deinit();\n    try utf8.findWrapBreaks(\"\", &wrap_result, .unicode);\n    try testing.expectEqual(@as(usize, 0), wrap_result.breaks.items.len);\n}\n\ntest \"edge case: exactly 16 bytes\" {\n    var line_result = utf8.LineBreakResult.init(testing.allocator);\n    defer line_result.deinit();\n\n    const input = \"0123456789abcdef\"; // exactly 16 bytes\n    try utf8.findLineBreaks(input, &line_result);\n    try testing.expectEqual(@as(usize, 0), line_result.breaks.items.len);\n\n    var wrap_result = utf8.WrapBreakResult.init(testing.allocator);\n    defer wrap_result.deinit();\n    try utf8.findWrapBreaks(input, &wrap_result, .unicode);\n    try testing.expectEqual(@as(usize, 0), wrap_result.breaks.items.len);\n}\n\ntest \"edge case: 17 bytes with break at 16\" {\n    var line_result = utf8.LineBreakResult.init(testing.allocator);\n    defer line_result.deinit();\n\n    const input = \"0123456789abcde\\nx\"; // break at position 15\n    try utf8.findLineBreaks(input, &line_result);\n    try testing.expectEqual(@as(usize, 1), line_result.breaks.items.len);\n    try testing.expectEqual(@as(usize, 15), line_result.breaks.items[0].pos);\n\n    var wrap_result = utf8.WrapBreakResult.init(testing.allocator);\n    defer wrap_result.deinit();\n    const input2 = \"0123456789abcde x\"; // space at position 15\n    try utf8.findWrapBreaks(input2, &wrap_result, .unicode);\n    try testing.expectEqual(@as(usize, 1), wrap_result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 15), wrap_result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 15), wrap_result.breaks.items[0].char_offset);\n}\n\n// ============================================================================\n// GRAPHEME CLUSTER TESTS\n// ============================================================================\n\ntest \"wrap breaks: emoji with ZWJ - char offset should count grapheme not codepoints\" {\n    const input = \"ab 👩‍🚀 cd\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 2), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 2), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 2), result.breaks.items[0].char_offset);\n    try testing.expectEqual(@as(u16, 14), result.breaks.items[1].byte_offset);\n    try testing.expectEqual(@as(u16, 4), result.breaks.items[1].char_offset); // Should be 4, not 6\n}\n\ntest \"wrap breaks: emoji with skin tone - char offset should count grapheme\" {\n    const input = \"hi 👋🏿 bye\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 2), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 2), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 2), result.breaks.items[0].char_offset);\n    try testing.expectEqual(@as(u16, 11), result.breaks.items[1].byte_offset);\n    try testing.expectEqual(@as(u16, 4), result.breaks.items[1].char_offset); // Should be 4, not 5\n}\n\ntest \"wrap breaks: emoji with VS16 selector - char offset should count grapheme\" {\n    const input = \"I ❤️ U\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 2), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 1), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 1), result.breaks.items[0].char_offset);\n    try testing.expectEqual(@as(u16, 8), result.breaks.items[1].byte_offset);\n    try testing.expectEqual(@as(u16, 3), result.breaks.items[1].char_offset); // Should be 3, not 4\n}\n\ntest \"wrap breaks: combining diacritic - char offset should count grapheme\" {\n    const input = \"cafe\\u{0301} time\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 1), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 6), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 4), result.breaks.items[0].char_offset); // Should be 4, not 5\n}\n\ntest \"wrap breaks: flag emoji - char offset should count grapheme\" {\n    const input = \"USA🇺🇸 flag\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 1), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 11), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 4), result.breaks.items[0].char_offset); // 3(USA) + 1(flag) = 4\n}\n\ntest \"wrap breaks: mixed graphemes and ASCII\" {\n    const input = \"Hello 👋🏿 world 🇺🇸 test\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 4), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 5), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 5), result.breaks.items[0].char_offset);\n    try testing.expectEqual(@as(u16, 14), result.breaks.items[1].byte_offset);\n    try testing.expectEqual(@as(u16, 7), result.breaks.items[1].char_offset); // 5 + 1 + 1(grapheme) = 7\n    try testing.expectEqual(@as(u16, 20), result.breaks.items[2].byte_offset);\n    try testing.expectEqual(@as(u16, 13), result.breaks.items[2].char_offset); // 7 + 1 + 5 = 13\n    try testing.expectEqual(@as(u16, 29), result.breaks.items[3].byte_offset);\n    try testing.expectEqual(@as(u16, 15), result.breaks.items[3].char_offset); // 13 + 1(space) + 1(RI) + 1(RI) = 15 (per uucode)\n}\n\ntest \"wrap breaks: CJK characters keep break offsets\" {\n    // Ensure multibyte graphemes don't shift wrap break offsets.\n    const input = \"Hello 世界 test\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    // Should find 2 wrap breaks (2 spaces)\n    try testing.expectEqual(@as(usize, 2), result.breaks.items.len);\n\n    // First break: space after \"Hello\"\n    try testing.expectEqual(@as(u16, 5), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 5), result.breaks.items[0].char_offset);\n\n    // Second break: space after \"世界\"\n    // Byte: \"Hello \" = 6 bytes, \"世\" = 3 bytes, \"界\" = 3 bytes, total = 12\n    try testing.expectEqual(@as(u16, 12), result.breaks.items[1].byte_offset);\n    try testing.expectEqual(@as(u16, 8), result.breaks.items[1].char_offset); // 6 graphemes(Hello space) + 2 graphemes(世界) = 8\n}\n\ntest \"wrap breaks: CJK to ASCII script transition\" {\n    const input = \"日本語abc\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 1), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 6), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 2), result.breaks.items[0].char_offset);\n}\n\ntest \"wrap breaks: ASCII to CJK script transition\" {\n    const input = \"abc日本語\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 1), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 2), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 2), result.breaks.items[0].char_offset);\n}\n\ntest \"wrap breaks: CJK punctuation before ASCII\" {\n    const input = \"日本語。abc\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 1), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 9), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 3), result.breaks.items[0].char_offset);\n}\n\ntest \"wrap breaks: compat ideograph to ASCII script transition\" {\n    const input = \"丽abc\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 1), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 0), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 0), result.breaks.items[0].char_offset);\n}\n\ntest \"wrap breaks: extension I ideograph to ASCII script transition\" {\n    const input = \"𮯰abc\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    try testing.expectEqual(@as(usize, 1), result.breaks.items.len);\n    try testing.expectEqual(@as(u16, 0), result.breaks.items[0].byte_offset);\n    try testing.expectEqual(@as(u16, 0), result.breaks.items[0].char_offset);\n}\n\ntest \"wrap breaks: emoji and CJK mixed offsets\" {\n    const input = \"🌟 Unicode test: こんにちは世界 Hello World\";\n\n    var result = utf8.WrapBreakResult.init(testing.allocator);\n    defer result.deinit();\n    try utf8.findWrapBreaks(input, &result, .unicode);\n\n    // Find the space before \"Hello\"\n    var space_before_hello: ?utf8.WrapBreak = null;\n    for (result.breaks.items) |brk| {\n        if (brk.byte_offset == 40) {\n            space_before_hello = brk;\n            break;\n        }\n    }\n\n    try testing.expect(space_before_hello != null);\n    try testing.expectEqual(@as(u16, 40), space_before_hello.?.byte_offset);\n    try testing.expectEqual(@as(u16, 23), space_before_hello.?.char_offset); // Graphemes before this space\n\n    // Find the space after \"Hello\"\n    var space_after_hello: ?utf8.WrapBreak = null;\n    for (result.breaks.items) |brk| {\n        if (brk.byte_offset == 46) {\n            space_after_hello = brk;\n            break;\n        }\n    }\n\n    try testing.expect(space_after_hello != null);\n    try testing.expectEqual(@as(u16, 46), space_after_hello.?.byte_offset);\n    try testing.expectEqual(@as(u16, 29), space_after_hello.?.char_offset);\n}\n\n// ============================================================================\n// WRAP BY WIDTH TESTS\n// ============================================================================\n\ntest \"wrap by width: empty string\" {\n    const result = utf8.findWrapPosByWidth(\"\", 10, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 0), result.byte_offset);\n    try testing.expectEqual(@as(u32, 0), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 0), result.columns_used);\n}\n\ntest \"wrap by width: simple ASCII no wrap\" {\n    const result = utf8.findWrapPosByWidth(\"hello\", 10, 4, true, .unicode);\n    try testing.expectEqual(@as(u32, 5), result.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 5), result.columns_used);\n}\n\ntest \"wrap by width: ASCII wrap exactly at limit\" {\n    const result = utf8.findWrapPosByWidth(\"hello\", 5, 4, true, .unicode);\n    try testing.expectEqual(@as(u32, 5), result.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 5), result.columns_used);\n}\n\ntest \"wrap by width: ASCII wrap before limit\" {\n    const result = utf8.findWrapPosByWidth(\"hello world\", 7, 4, true, .unicode);\n    try testing.expectEqual(@as(u32, 7), result.byte_offset);\n    try testing.expectEqual(@as(u32, 7), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 7), result.columns_used);\n}\n\ntest \"wrap by width: East Asian wide char\" {\n    const result = utf8.findWrapPosByWidth(\"世界\", 3, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 3), result.byte_offset); // After first char\n    try testing.expectEqual(@as(u32, 1), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 2), result.columns_used);\n}\n\ntest \"wrap by width: combining mark\" {\n    const result = utf8.findWrapPosByWidth(\"e\\u{0301}test\", 3, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), result.byte_offset); // After \"é\" (3 bytes) + \"te\" (2 bytes)\n    try testing.expectEqual(@as(u32, 3), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 3), result.columns_used);\n}\n\ntest \"wrap by width: tab handling\" {\n    const result = utf8.findWrapPosByWidth(\"a\\tb\", 5, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result.byte_offset); // After \"a\\t\"\n    try testing.expectEqual(@as(u32, 2), result.grapheme_count); // 'a' + tab\n    try testing.expectEqual(@as(u32, 5), result.columns_used); // 'a' (1) + tab (4) = 5\n}\n\nfn testWrapByWidthMethodsMatch(input: []const u8, max_columns: u32, tab_width: u8, isASCIIOnly: bool) !void {\n    const result = utf8.findWrapPosByWidth(input, max_columns, tab_width, isASCIIOnly, .unicode);\n    // Since we only have SIMD16 in utf8.zig, just verify it doesn't crash\n    _ = result;\n}\n\ntest \"wrap by width: consistency - realistic text\" {\n    const sample_text =\n        \"The quick brown fox jumps over the lazy dog. \" ++\n        \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. \" ++\n        \"File paths: /usr/local/bin and C:\\\\Windows\\\\System32. \" ++\n        \"Punctuation test: Hello, world! How are you? I'm fine.\";\n\n    const widths = [_]u32{ 10, 20, 40, 80, 120 };\n    for (widths) |w| {\n        try testWrapByWidthMethodsMatch(sample_text, w, 4, true);\n    }\n}\n\ntest \"wrap by width: consistency - Unicode text\" {\n    const unicode_text = \"世界 こんにちは test 你好 CJK-mixed\";\n\n    const widths = [_]u32{ 5, 10, 15, 20, 30 };\n    for (widths) |w| {\n        try testWrapByWidthMethodsMatch(unicode_text, w, 4, false);\n    }\n}\n\ntest \"wrap by width: consistency - edge cases\" {\n    const edge_cases = [_]struct { text: []const u8, ascii: bool }{\n        .{ .text = \"\", .ascii = false },\n        .{ .text = \" \", .ascii = true },\n        .{ .text = \"a\", .ascii = true },\n        .{ .text = \"abc\", .ascii = true },\n        .{ .text = \"   \", .ascii = true },\n        .{ .text = \"a b c d e\", .ascii = true },\n        .{ .text = \"no-spaces-here\", .ascii = true },\n        .{ .text = \"/usr/local/bin\", .ascii = true },\n        .{ .text = \"世界\", .ascii = false },\n        .{ .text = \"\\t\\t\\t\", .ascii = false },\n    };\n\n    for (edge_cases) |input| {\n        const widths = [_]u32{ 1, 5, 10, 20 };\n        for (widths) |w| {\n            try testWrapByWidthMethodsMatch(input.text, w, 4, input.ascii);\n        }\n    }\n}\n\ntest \"wrap by width: property - random ASCII buffers\" {\n    var prng = std.Random.DefaultPrng.init(42);\n    const random = prng.random();\n\n    var i: usize = 0;\n    while (i < 50) : (i += 1) {\n        const size = 16 + random.uintLessThan(usize, 256);\n        const buf = try testing.allocator.alloc(u8, size);\n        defer testing.allocator.free(buf);\n\n        for (buf) |*b| {\n            b.* = 'a' + random.uintLessThan(u8, 26);\n        }\n\n        const width = 10 + random.uintLessThan(u32, 70);\n        try testWrapByWidthMethodsMatch(buf, width, 4, true);\n    }\n}\n\ntest \"wrap by width: boundary - SIMD16 chunk boundary\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    try testWrapByWidthMethodsMatch(&buf, 20, 4, true);\n    try testWrapByWidthMethodsMatch(&buf, 10, 4, true);\n}\n\ntest \"wrap by width: boundary - Unicode at SIMD boundary\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'a');\n    const cjk = \"世\";\n    @memcpy(buf[14..17], cjk);\n    try testWrapByWidthMethodsMatch(buf[0..20], 20, 4, false);\n}\n\ntest \"wrap by width: wide emoji exactly at column boundary\" {\n    const input = \"Hello 🌍 World\";\n\n    const result7 = utf8.findWrapPosByWidth(input, 7, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), result7.byte_offset);\n    try testing.expectEqual(@as(u32, 6), result7.columns_used);\n\n    const result8 = utf8.findWrapPosByWidth(input, 8, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 10), result8.byte_offset);\n    try testing.expectEqual(@as(u32, 8), result8.columns_used);\n\n    const result6 = utf8.findWrapPosByWidth(input, 6, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), result6.byte_offset);\n    try testing.expectEqual(@as(u32, 6), result6.columns_used);\n}\n\ntest \"wrap by width: wide emoji at start\" {\n    const input = \"🌍 World\";\n\n    const result1 = utf8.findWrapPosByWidth(input, 1, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 0), result1.byte_offset);\n    try testing.expectEqual(@as(u32, 0), result1.columns_used);\n\n    const result2 = utf8.findWrapPosByWidth(input, 2, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), result2.byte_offset);\n    try testing.expectEqual(@as(u32, 2), result2.columns_used);\n\n    const result3 = utf8.findWrapPosByWidth(input, 3, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), result3.byte_offset);\n    try testing.expectEqual(@as(u32, 3), result3.columns_used);\n}\n\ntest \"wrap by width: multiple wide characters\" {\n    const input = \"AB🌍CD🌎EF\";\n\n    const result5 = utf8.findWrapPosByWidth(input, 5, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 7), result5.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result5.columns_used);\n\n    const result6 = utf8.findWrapPosByWidth(input, 6, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), result6.byte_offset);\n    try testing.expectEqual(@as(u32, 6), result6.columns_used);\n}\n\ntest \"wrap by width: CJK wide characters at boundary\" {\n    const input = \"hello世界test\";\n\n    const result6 = utf8.findWrapPosByWidth(input, 6, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), result6.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result6.columns_used);\n\n    const result7 = utf8.findWrapPosByWidth(input, 7, 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), result7.byte_offset);\n    try testing.expectEqual(@as(u32, 7), result7.columns_used);\n}\n\n// ============================================================================\n// FIND POS BY WIDTH TESTS (for selection - includes graphemes that start before limit)\n// ============================================================================\n\ntest \"find pos by width: wide emoji at boundary - INCLUDES grapheme\" {\n    const input = \"Hello 🌍 World\";\n    // Layout: H(0) e(1) l(2) l(3) o(4) space(5) 🌍(6-7) space(8) W(9)...\n\n    // include_start_before=true (selection end): include graphemes that START before max_columns\n    const result7 = utf8.findPosByWidth(input, 7, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 10), result7.byte_offset); // After emoji (snapped forward)\n    try testing.expectEqual(@as(u32, 8), result7.columns_used);\n\n    const result8 = utf8.findPosByWidth(input, 8, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 10), result8.byte_offset);\n    try testing.expectEqual(@as(u32, 8), result8.columns_used);\n\n    const result6 = utf8.findPosByWidth(input, 6, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 6), result6.byte_offset);\n    try testing.expectEqual(@as(u32, 6), result6.columns_used);\n\n    // include_start_before=false (selection start): exclude graphemes that cross max_columns\n    const start7 = utf8.findPosByWidth(input, 7, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), start7.byte_offset); // Before emoji (snapped backward)\n    try testing.expectEqual(@as(u32, 6), start7.columns_used);\n}\n\ntest \"find pos by width: start at second cell of width=2 grapheme snaps backward\" {\n    const input = \"AB🌍CD\";\n    const result = utf8.findPosByWidth(input, 3, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result.byte_offset); // After \"AB\", before emoji\n    try testing.expectEqual(@as(u32, 2), result.columns_used);\n}\n\ntest \"find pos by width: end at first cell of width=2 grapheme snaps forward\" {\n    const input = \"AB🌍CD\";\n    const result = utf8.findPosByWidth(input, 2, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 2), result.byte_offset); // After \"AB\" (emoji starts at 2, which is NOT > 2, but hasn't been consumed yet)\n    try testing.expectEqual(@as(u32, 2), result.columns_used);\n\n    const result3 = utf8.findPosByWidth(input, 3, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 6), result3.byte_offset); // After \"AB🌍\"\n    try testing.expectEqual(@as(u32, 4), result3.columns_used);\n}\n\ntest \"find pos by width: selection boundaries with multiple wide chars\" {\n    const input = \"A🌍B🌎C\";\n    const start2 = utf8.findPosByWidth(input, 2, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), start2.byte_offset); // After \"A\", before first emoji\n    try testing.expectEqual(@as(u32, 1), start2.columns_used);\n\n    const end5 = utf8.findPosByWidth(input, 5, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 10), end5.byte_offset); // After \"A🌍B🌎\"\n    try testing.expectEqual(@as(u32, 6), end5.columns_used);\n}\n\ntest \"find pos by width: empty string\" {\n    const result = utf8.findPosByWidth(\"\", 10, 4, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 0), result.byte_offset);\n    try testing.expectEqual(@as(u32, 0), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 0), result.columns_used);\n}\n\ntest \"find pos by width: simple ASCII no limit\" {\n    const result = utf8.findPosByWidth(\"hello\", 10, 4, true, true, .unicode);\n    try testing.expectEqual(@as(u32, 5), result.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 5), result.columns_used);\n}\n\ntest \"find pos by width: ASCII exactly at limit\" {\n    const result = utf8.findPosByWidth(\"hello\", 5, 4, true, true, .unicode);\n    try testing.expectEqual(@as(u32, 5), result.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 5), result.columns_used);\n}\n\ntest \"find pos by width: wide emoji at start\" {\n    const input = \"🌍 World\";\n\n    const result1 = utf8.findPosByWidth(input, 1, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 4), result1.byte_offset);\n    try testing.expectEqual(@as(u32, 2), result1.columns_used);\n\n    const result2 = utf8.findPosByWidth(input, 2, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 4), result2.byte_offset);\n    try testing.expectEqual(@as(u32, 2), result2.columns_used);\n\n    const result3 = utf8.findPosByWidth(input, 3, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 5), result3.byte_offset);\n    try testing.expectEqual(@as(u32, 3), result3.columns_used);\n}\n\ntest \"find pos by width: multiple wide characters\" {\n    const input = \"AB🌍CD🌎EF\";\n\n    const result5 = utf8.findPosByWidth(input, 5, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 7), result5.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result5.columns_used);\n\n    const result7 = utf8.findPosByWidth(input, 7, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 12), result7.byte_offset);\n    try testing.expectEqual(@as(u32, 8), result7.columns_used);\n}\n\ntest \"find pos by width: CJK wide characters\" {\n    const input = \"hello世界test\";\n\n    const result6 = utf8.findPosByWidth(input, 6, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 8), result6.byte_offset);\n    try testing.expectEqual(@as(u32, 7), result6.columns_used);\n\n    const result8 = utf8.findPosByWidth(input, 8, 8, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 11), result8.byte_offset);\n    try testing.expectEqual(@as(u32, 9), result8.columns_used);\n}\n\ntest \"eastAsianWidth: verify all characters in test string have correct width\" {\n    // Test each CJK character individually to ensure width calculation is correct\n\n    // Test hiragana characters from \"こんにちは\"\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x3053)); // こ\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x3093)); // ん\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x306B)); // に\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x3061)); // ち\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x306F)); // は\n\n    // Test kanji characters from \"世界\"\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x4E16)); // 世\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x754C)); // 界\n\n    // Test emoji\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x1F31F)); // 🌟\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x1F680)); // 🚀\n\n    // Test Chinese characters from \"你好\"\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x4F60)); // 你\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0x597D)); // 好\n\n    // Test Korean characters from \"안녕하세요\"\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0xC548)); // 안\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0xB155)); // 녕\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0xD558)); // 하\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0xC138)); // 세\n    try testing.expectEqual(@as(u32, 2), utf8.eastAsianWidth(0xC694)); // 요\n\n    // Test ASCII characters\n    try testing.expectEqual(@as(u32, 1), utf8.eastAsianWidth('H'));\n    try testing.expectEqual(@as(u32, 1), utf8.eastAsianWidth('e'));\n    try testing.expectEqual(@as(u32, 1), utf8.eastAsianWidth(' '));\n    try testing.expectEqual(@as(u32, 1), utf8.eastAsianWidth(':'));\n}\n\ntest \"calculateTextWidth: verify CJK string widths character by character\" {\n    // Verify width of individual CJK characters\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"こ\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"ん\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"に\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"ち\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"は\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"世\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"界\", 8, false, .unicode));\n\n    // Verify cumulative widths\n    try testing.expectEqual(@as(u32, 4), utf8.calculateTextWidth(\"こん\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 6), utf8.calculateTextWidth(\"こんに\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 14), utf8.calculateTextWidth(\"こんにちは世界\", 8, false, .unicode));\n\n    // Verify mixed ASCII and CJK\n    try testing.expectEqual(@as(u32, 5), utf8.calculateTextWidth(\"Hello\", 8, true, .unicode));\n    try testing.expectEqual(@as(u32, 6), utf8.calculateTextWidth(\"Hello \", 8, true, .unicode));\n    try testing.expectEqual(@as(u32, 8), utf8.calculateTextWidth(\"Hello 世\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 10), utf8.calculateTextWidth(\"Hello 世界\", 8, false, .unicode));\n}\n\ntest \"calculateTextWidth: step by step for emoji CJK test string\" {\n    // Manually verify each section\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"🌟\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(\"🌟 \", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 10), utf8.calculateTextWidth(\"🌟 Unicode\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 11), utf8.calculateTextWidth(\"🌟 Unicode \", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 15), utf8.calculateTextWidth(\"🌟 Unicode test\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 16), utf8.calculateTextWidth(\"🌟 Unicode test:\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 17), utf8.calculateTextWidth(\"🌟 Unicode test: \", 8, false, .unicode));\n\n    // CJK section - verify each character adds 2 columns\n    try testing.expectEqual(@as(u32, 19), utf8.calculateTextWidth(\"🌟 Unicode test: こ\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 21), utf8.calculateTextWidth(\"🌟 Unicode test: こん\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 23), utf8.calculateTextWidth(\"🌟 Unicode test: こんに\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 25), utf8.calculateTextWidth(\"🌟 Unicode test: こんにち\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 27), utf8.calculateTextWidth(\"🌟 Unicode test: こんにちは\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 29), utf8.calculateTextWidth(\"🌟 Unicode test: こんにちは世\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 31), utf8.calculateTextWidth(\"🌟 Unicode test: こんにちは世界\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 32), utf8.calculateTextWidth(\"🌟 Unicode test: こんにちは世界 \", 8, false, .unicode));\n\n    // English section\n    try testing.expectEqual(@as(u32, 33), utf8.calculateTextWidth(\"🌟 Unicode test: こんにちは世界 H\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 37), utf8.calculateTextWidth(\"🌟 Unicode test: こんにちは世界 Hello\", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 38), utf8.calculateTextWidth(\"🌟 Unicode test: こんにちは世界 Hello \", 8, false, .unicode));\n    try testing.expectEqual(@as(u32, 43), utf8.calculateTextWidth(\"🌟 Unicode test: こんにちは世界 Hello World\", 8, false, .unicode));\n}\n\ntest \"find pos by width: CJK characters with English - verify column calculation\" {\n    // This test verifies that findPosByWidth correctly handles mixed CJK and ASCII\n    const input = \"🌟 Unicode test: こんにちは世界 Hello World 你好世界\";\n\n    // Verify width calculations at key positions\n    const width_before_hello = utf8.calculateTextWidth(input[0..40], 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 31), width_before_hello);\n\n    const width_including_space_before_hello = utf8.calculateTextWidth(input[0..41], 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 32), width_including_space_before_hello);\n\n    const width_up_to_hello = utf8.calculateTextWidth(input[0..46], 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 37), width_up_to_hello);\n\n    const width_including_hello_space = utf8.calculateTextWidth(input[0..47], 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 38), width_including_hello_space);\n\n    const width_up_to_world = utf8.calculateTextWidth(input[0..52], 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 43), width_up_to_world);\n\n    const width_including_world_space = utf8.calculateTextWidth(input[0..53], 8, false, .unicode);\n    try testing.expectEqual(@as(u32, 44), width_including_world_space);\n\n    // Verify findPosByWidth returns correct positions\n    const result35 = utf8.findPosByWidth(input, 35, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 44), result35.byte_offset);\n    try testing.expectEqual(@as(u32, 35), result35.columns_used);\n\n    const result36 = utf8.findPosByWidth(input, 36, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 45), result36.byte_offset);\n    try testing.expectEqual(@as(u32, 36), result36.columns_used);\n\n    const result37 = utf8.findPosByWidth(input, 37, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 46), result37.byte_offset);\n    try testing.expectEqual(@as(u32, 37), result37.columns_used);\n\n    const result42 = utf8.findPosByWidth(input, 42, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 51), result42.byte_offset);\n    try testing.expectEqual(@as(u32, 42), result42.columns_used);\n}\n\ntest \"find pos by width: combining mark\" {\n    const result = utf8.findPosByWidth(\"e\\u{0301}test\", 3, 4, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 5), result.byte_offset); // After \"é\" (3 bytes) + \"te\" (2 bytes)\n    try testing.expectEqual(@as(u32, 3), result.grapheme_count);\n    try testing.expectEqual(@as(u32, 3), result.columns_used);\n}\n\ntest \"find pos by width: tab handling\" {\n    const result = utf8.findPosByWidth(\"a\\tb\", 5, 4, false, true, .unicode);\n    try testing.expectEqual(@as(u32, 2), result.byte_offset); // After \"a\\t\"\n    try testing.expectEqual(@as(u32, 2), result.grapheme_count); // 'a' + tab\n    try testing.expectEqual(@as(u32, 5), result.columns_used); // 'a' (1) + tab (4) = 5\n}\n\n// ============================================================================\n// SPLIT CHUNK AT WEIGHT TESTS (include_start_before=false)\n// Tests for the exact behavior needed by splitChunkAtWeight in edit-buffer.zig\n// ============================================================================\n\ntest \"split at weight: ASCII simple split\" {\n    const input = \"hello world\";\n\n    // Split at column 5 - should stop at 'h' of \"hello\"\n    const result = utf8.findPosByWidth(input, 5, 8, true, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), result.byte_offset); // After \"hello\"\n    try testing.expectEqual(@as(u32, 5), result.columns_used);\n}\n\ntest \"split at weight: ASCII split in middle\" {\n    const input = \"abcdefghij\";\n\n    // Split at column 3\n    const result = utf8.findPosByWidth(input, 3, 8, true, false, .unicode);\n    try testing.expectEqual(@as(u32, 3), result.byte_offset); // After \"abc\"\n    try testing.expectEqual(@as(u32, 3), result.columns_used);\n}\n\ntest \"split at weight: wide char at boundary - exclude when starting after\" {\n    const input = \"AB🌍CD\"; // A(1) B(1) 🌍(2) C(1) D(1)\n\n    // Split at column 2 - should include up to B, exclude emoji\n    const result2 = utf8.findPosByWidth(input, 2, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result2.byte_offset); // After \"AB\"\n    try testing.expectEqual(@as(u32, 2), result2.columns_used);\n\n    const result3 = utf8.findPosByWidth(input, 3, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result3.byte_offset); // After \"AB\", before emoji\n    try testing.expectEqual(@as(u32, 2), result3.columns_used);\n}\n\ntest \"split at weight: CJK characters\" {\n    const input = \"hello世界test\"; // h(1) e(1) l(1) l(1) o(1) 世(2) 界(2) t(1) e(1) s(1) t(1)\n\n    // Split at column 5 - after \"hello\"\n    const result5 = utf8.findPosByWidth(input, 5, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), result5.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result5.columns_used);\n\n    const result6 = utf8.findPosByWidth(input, 6, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), result6.byte_offset); // After \"hello\", before 世\n    try testing.expectEqual(@as(u32, 5), result6.columns_used);\n\n    // Split at column 9 - should include both CJK chars\n    const result9 = utf8.findPosByWidth(input, 9, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 11), result9.byte_offset); // After \"hello世界\"\n    try testing.expectEqual(@as(u32, 9), result9.columns_used);\n}\n\ntest \"split at weight: combining marks\" {\n    const input = \"cafe\\u{0301}test\"; // c(1) a(1) f(1) é(1) t(1) e(1) s(1) t(1)\n\n    // Split at column 4 - should include the combining mark with 'e'\n    const result4 = utf8.findPosByWidth(input, 4, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), result4.byte_offset); // After \"café\" (5 bytes: cafe + combining accent)\n    try testing.expectEqual(@as(u32, 4), result4.columns_used);\n}\n\ntest \"split at weight: emoji with skin tone\" {\n    const input = \"Hi👋🏿Bye\"; // H(1) i(1) 👋🏿(wide) B(1) y(1) e(1)\n\n    // Split at column 2 - should stop before or after emoji depending on where it starts\n    const result2 = utf8.findPosByWidth(input, 2, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result2.byte_offset); // After \"Hi\"\n    try testing.expectEqual(@as(u32, 2), result2.columns_used);\n\n    // Split at column 5 - should include emoji\n    const result5 = utf8.findPosByWidth(input, 5, 8, false, false, .unicode);\n    // Result will stop at first grapheme that starts >= max_columns\n    // Just verify it returns a reasonable offset\n    try testing.expect(result5.byte_offset >= 2); // At least past \"Hi\"\n    try testing.expect(result5.columns_used >= 2); // At least 2 columns\n}\n\ntest \"split at weight: zero width at start\" {\n    const input = \"hello\";\n\n    // Split at column 0 - should return offset 0\n    const result = utf8.findPosByWidth(input, 0, 8, true, false, .unicode);\n    try testing.expectEqual(@as(u32, 0), result.byte_offset);\n    try testing.expectEqual(@as(u32, 0), result.columns_used);\n}\n\ntest \"split at weight: beyond end\" {\n    const input = \"hello\"; // 5 columns\n\n    // Split at column 10 - should return entire string\n    const result = utf8.findPosByWidth(input, 10, 8, true, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), result.byte_offset);\n    try testing.expectEqual(@as(u32, 5), result.columns_used);\n}\n\ntest \"split at weight: tab character\" {\n    const input = \"a\\tbc\"; // a(1) tab(4 fixed) b(1) c(1) = 7 columns total\n\n    // Split at column 4 - should stop before tab since it would exceed limit\n    const result4 = utf8.findPosByWidth(input, 4, 4, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), result4.byte_offset); // After \"a\"\n    try testing.expectEqual(@as(u32, 1), result4.columns_used); // a(1)\n}\n\ntest \"split at weight: complex mixed content\" {\n    const input = \"A🌍B世C\"; // A(1) 🌍(2) B(1) 世(2) C(1) = 7 columns total\n    const r1 = utf8.findPosByWidth(input, 1, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), r1.byte_offset); // After \"A\"\n\n    const r2 = utf8.findPosByWidth(input, 2, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), r2.byte_offset); // After \"A\"\n\n    const r3 = utf8.findPosByWidth(input, 3, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), r3.byte_offset); // After \"A🌍\"\n\n    const r4 = utf8.findPosByWidth(input, 4, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), r4.byte_offset); // After \"A🌍B\"\n\n    const r5 = utf8.findPosByWidth(input, 5, 8, false, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), r5.byte_offset); // After \"A🌍B\"\n}\n\n// ============================================================================\n// GET WIDTH AT TESTS\n// ============================================================================\n\ntest \"getWidthAt: empty string\" {\n    const result = utf8.getWidthAt(\"\", 0, 8, .unicode);\n    try testing.expectEqual(@as(u32, 0), result);\n}\n\ntest \"getWidthAt: out of bounds\" {\n    const result = utf8.getWidthAt(\"hello\", 10, 8, .unicode);\n    try testing.expectEqual(@as(u32, 0), result);\n}\n\ntest \"getWidthAt: simple ASCII\" {\n    const text = \"hello\";\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 0, 8, .unicode)); // 'h'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 1, 8, .unicode)); // 'e'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 4, 8, .unicode)); // 'o'\n}\n\ntest \"getWidthAt: tab character\" {\n    const text = \"a\\tb\";\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 0, 4, .unicode)); // 'a'\n    try testing.expectEqual(@as(u32, 4), utf8.getWidthAt(text, 1, 4, .unicode)); // tab fixed width 4\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 2, 4, .unicode)); // 'b'\n}\n\ntest \"getWidthAt: tab at different columns\" {\n    const text = \"\\t\";\n    // Tab now has fixed width regardless of current_column\n    try testing.expectEqual(@as(u32, 4), utf8.getWidthAt(text, 0, 4, .unicode)); // Tab fixed width 4\n    try testing.expectEqual(@as(u32, 4), utf8.getWidthAt(text, 0, 4, .unicode)); // Tab fixed width 4\n    try testing.expectEqual(@as(u32, 4), utf8.getWidthAt(text, 0, 4, .unicode)); // Tab fixed width 4\n    try testing.expectEqual(@as(u32, 4), utf8.getWidthAt(text, 0, 4, .unicode)); // Tab fixed width 4\n    try testing.expectEqual(@as(u32, 4), utf8.getWidthAt(text, 0, 4, .unicode)); // Tab fixed width 4\n}\n\ntest \"getWidthAt: CJK wide character\" {\n    const text = \"世界\";\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 0, 8, .unicode)); // '世' (3 bytes)\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 3, 8, .unicode)); // '界' (3 bytes)\n}\n\ntest \"getWidthAt: emoji single width\" {\n    const text = \"🌍\";\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 0, 8, .unicode)); // emoji\n}\n\ntest \"getWidthAt: combining mark grapheme\" {\n    const text = \"cafe\\u{0301}\"; // é with combining acute accent\n    const width = utf8.getWidthAt(text, 3, 8, .unicode); // At 'e' (which has combining mark after)\n    try testing.expectEqual(@as(u32, 1), width); // 'e' width 1 + combining mark width 0 = 1\n}\n\ntest \"getWidthAt: emoji with skin tone\" {\n    const text = \"👋🏿\"; // Wave + dark skin tone modifier\n    const width = utf8.getWidthAt(text, 0, 8, .unicode);\n    try testing.expectEqual(@as(u32, 2), width); // Single grapheme cluster, width 2\n}\n\ntest \"getWidthAt: emoji with ZWJ\" {\n    const text = \"👩‍🚀\"; // Woman astronaut (woman + ZWJ + rocket)\n    const width = utf8.getWidthAt(text, 0, 8, .unicode);\n    try testing.expectEqual(@as(u32, 2), width); // Single grapheme cluster, width 2\n}\n\ntest \"getWidthAt: flag emoji\" {\n    const text = \"🇺🇸\"; // US flag (two regional indicators)\n    const width = utf8.getWidthAt(text, 0, 8, .unicode);\n    try testing.expectEqual(@as(u32, 2), width); // Entire grapheme cluster\n}\n\ntest \"getWidthAt: mixed ASCII and CJK\" {\n    const text = \"Hello世界\";\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 0, 8, .unicode)); // 'H'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 1, 8, .unicode)); // 'e'\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 5, 8, .unicode)); // '世'\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 8, 8, .unicode)); // '界'\n}\n\ntest \"getWidthAt: emoji with VS16 selector\" {\n    const text = \"❤️\"; // Heart + VS16 selector\n    const width = utf8.getWidthAt(text, 0, 8, .unicode);\n    try testing.expectEqual(@as(u32, 2), width); // Single grapheme cluster, width 2\n}\n\ntest \"getWidthAt: hiragana\" {\n    const text = \"こんにちは\";\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 0, 8, .unicode)); // 'こ'\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 3, 8, .unicode)); // 'ん'\n}\n\ntest \"getWidthAt: katakana\" {\n    const text = \"カタカナ\";\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 0, 8, .unicode)); // 'カ'\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 3, 8, .unicode)); // 'タ'\n}\n\ntest \"getWidthAt: fullwidth forms\" {\n    const text = \"ＡＢＣ\"; // Fullwidth A, B, C\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 0, 8, .unicode)); // Fullwidth 'A'\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 3, 8, .unicode)); // Fullwidth 'B'\n}\n\ntest \"getWidthAt: zero width at start of string\" {\n    const text = \"a\\u{0301}bc\"; // a + combining accent + bc\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 0, 8, .unicode)); // 'a' + combining = 1\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 3, 8, .unicode)); // 'b'\n}\n\ntest \"getWidthAt: control characters\" {\n    const text = \"a\\x00b\";\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 0, 8, .unicode)); // 'a'\n    try testing.expectEqual(@as(u32, 0), utf8.getWidthAt(text, 1, 8, .unicode)); // null\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 2, 8, .unicode)); // 'b'\n}\n\ntest \"getWidthAt: multiple combining marks\" {\n    const text = \"e\\u{0301}\\u{0302}\"; // e + acute + circumflex\n    const width = utf8.getWidthAt(text, 0, 8, .unicode);\n    try testing.expectEqual(@as(u32, 1), width); // All combining marks part of one grapheme\n}\n\ntest \"getWidthAt: at exact end boundary\" {\n    const text = \"hello\";\n    const width = utf8.getWidthAt(text, 5, 8, .unicode); // At index 5 (past end)\n    try testing.expectEqual(@as(u32, 0), width);\n}\n\ntest \"getWidthAt: realistic mixed content\" {\n    const text = \"Hello 世界! 👋\";\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 0, 8, .unicode)); // 'H'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 5, 8, .unicode)); // ' '\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 6, 8, .unicode)); // '世'\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 9, 8, .unicode)); // '界'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 12, 8, .unicode)); // '!'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 13, 8, .unicode)); // ' '\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 14, 8, .unicode)); // emoji\n}\n\ntest \"getWidthAt: grapheme at SIMD boundary\" {\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    const cjk = \"世\";\n    @memcpy(buf[14..17], cjk); // Place CJK char near boundary\n\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(&buf, 13, 8, .unicode)); // 'x'\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(&buf, 14, 8, .unicode)); // '世'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(&buf, 17, 8, .unicode)); // 'x'\n}\n\ntest \"getWidthAt: incomplete UTF-8 at end\" {\n    const text = \"abc\\xC3\"; // Incomplete 2-byte sequence\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 0, 8, .unicode)); // 'a'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 3, 8, .unicode)); // Incomplete, returns 1 for error\n}\n\ntest \"getWidthAt: random positions in realistic text\" {\n    const text = \"The quick brown 🦊 jumps over the lazy 犬\";\n\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 0, 8, .unicode)); // 'T'\n    try testing.expectEqual(@as(u32, 1), utf8.getWidthAt(text, 10, 8, .unicode)); // 'b'\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 16, 8, .unicode)); // fox emoji\n    try testing.expectEqual(@as(u32, 2), utf8.getWidthAt(text, 41, 8, .unicode)); // '犬' (dog)\n}\n\n// ============================================================================\n// GET PREV GRAPHEME START TESTS\n// ============================================================================\n\ntest \"getPrevGraphemeStart: at start\" {\n    const text = \"hello\";\n    const result = utf8.getPrevGraphemeStart(text, 0, 8, .unicode);\n    try testing.expect(result == null);\n}\n\ntest \"getPrevGraphemeStart: empty string\" {\n    const result = utf8.getPrevGraphemeStart(\"\", 0, 8, .unicode);\n    try testing.expect(result == null);\n}\n\ntest \"getPrevGraphemeStart: out of bounds\" {\n    const text = \"hello\";\n    const result = utf8.getPrevGraphemeStart(text, 100, 8, .unicode);\n    try testing.expect(result == null);\n}\n\ntest \"getPrevGraphemeStart: simple ASCII\" {\n    const text = \"hello\";\n\n    const r1 = utf8.getPrevGraphemeStart(text, 1, 8, .unicode);\n    try testing.expect(r1 != null);\n    try testing.expectEqual(@as(usize, 0), r1.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r1.?.width);\n\n    const r2 = utf8.getPrevGraphemeStart(text, 2, 8, .unicode);\n    try testing.expect(r2 != null);\n    try testing.expectEqual(@as(usize, 1), r2.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r2.?.width);\n\n    const r5 = utf8.getPrevGraphemeStart(text, 5, 8, .unicode);\n    try testing.expect(r5 != null);\n    try testing.expectEqual(@as(usize, 4), r5.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r5.?.width);\n}\n\ntest \"getPrevGraphemeStart: CJK wide character\" {\n    const text = \"a世界\";\n\n    const r1 = utf8.getPrevGraphemeStart(text, 1, 8, .unicode);\n    try testing.expect(r1 != null);\n    try testing.expectEqual(@as(usize, 0), r1.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r1.?.width);\n\n    const r4 = utf8.getPrevGraphemeStart(text, 4, 8, .unicode);\n    try testing.expect(r4 != null);\n    try testing.expectEqual(@as(usize, 1), r4.?.start_offset);\n    try testing.expectEqual(@as(u32, 2), r4.?.width);\n\n    const r7 = utf8.getPrevGraphemeStart(text, 7, 8, .unicode);\n    try testing.expect(r7 != null);\n    try testing.expectEqual(@as(usize, 4), r7.?.start_offset);\n    try testing.expectEqual(@as(u32, 2), r7.?.width);\n}\n\ntest \"getPrevGraphemeStart: combining mark\" {\n    const text = \"cafe\\u{0301}\"; // café with combining acute\n\n    const r6 = utf8.getPrevGraphemeStart(text, 6, 8, .unicode);\n    try testing.expect(r6 != null);\n    try testing.expectEqual(@as(usize, 3), r6.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r6.?.width);\n}\n\ntest \"getPrevGraphemeStart: emoji with skin tone\" {\n    const text = \"Hi👋🏿\";\n\n    const r2 = utf8.getPrevGraphemeStart(text, 2, 8, .unicode);\n    try testing.expect(r2 != null);\n    try testing.expectEqual(@as(usize, 1), r2.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r2.?.width);\n\n    const r_end = utf8.getPrevGraphemeStart(text, text.len, 8, .unicode);\n    try testing.expect(r_end != null);\n    try testing.expectEqual(@as(usize, 2), r_end.?.start_offset);\n}\n\ntest \"getPrevGraphemeStart: emoji with ZWJ\" {\n    const text = \"a👩‍🚀\"; // a + woman astronaut\n\n    const r1 = utf8.getPrevGraphemeStart(text, 1, 8, .unicode);\n    try testing.expect(r1 != null);\n    try testing.expectEqual(@as(usize, 0), r1.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r1.?.width);\n\n    const r_end = utf8.getPrevGraphemeStart(text, text.len, 8, .unicode);\n    try testing.expect(r_end != null);\n    try testing.expectEqual(@as(usize, 1), r_end.?.start_offset);\n}\n\ntest \"getPrevGraphemeStart: flag emoji\" {\n    const text = \"US🇺🇸\";\n\n    const r_end = utf8.getPrevGraphemeStart(text, text.len, 8, .unicode);\n    try testing.expect(r_end != null);\n    try testing.expectEqual(@as(usize, 2), r_end.?.start_offset);\n}\n\ntest \"getPrevGraphemeStart: tab handling\" {\n    const text = \"a\\tb\";\n\n    const r2 = utf8.getPrevGraphemeStart(text, 2, 4, .unicode);\n    try testing.expect(r2 != null);\n    try testing.expectEqual(@as(usize, 1), r2.?.start_offset);\n\n    const r1 = utf8.getPrevGraphemeStart(text, 1, 4, .unicode);\n    try testing.expect(r1 != null);\n    try testing.expectEqual(@as(usize, 0), r1.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r1.?.width);\n}\n\ntest \"getPrevGraphemeStart: mixed content\" {\n    const text = \"Hi世界!\";\n\n    const r2 = utf8.getPrevGraphemeStart(text, 2, 8, .unicode);\n    try testing.expect(r2 != null);\n    try testing.expectEqual(@as(usize, 1), r2.?.start_offset);\n\n    const r5 = utf8.getPrevGraphemeStart(text, 5, 8, .unicode);\n    try testing.expect(r5 != null);\n    try testing.expectEqual(@as(usize, 2), r5.?.start_offset);\n    try testing.expectEqual(@as(u32, 2), r5.?.width);\n\n    const r8 = utf8.getPrevGraphemeStart(text, 8, 8, .unicode);\n    try testing.expect(r8 != null);\n    try testing.expectEqual(@as(usize, 5), r8.?.start_offset);\n    try testing.expectEqual(@as(u32, 2), r8.?.width);\n}\n\ntest \"getPrevGraphemeStart: multiple combining marks\" {\n    const text = \"e\\u{0301}\\u{0302}x\"; // e + acute + circumflex + x\n\n    const r_x = utf8.getPrevGraphemeStart(text, text.len, 8, .unicode);\n    try testing.expect(r_x != null);\n    try testing.expectEqual(@as(usize, text.len - 1), r_x.?.start_offset);\n\n    const r_e = utf8.getPrevGraphemeStart(text, text.len - 1, 8, .unicode);\n    try testing.expect(r_e != null);\n    try testing.expectEqual(@as(usize, 0), r_e.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r_e.?.width);\n}\n\ntest \"getPrevGraphemeStart: hiragana\" {\n    const text = \"こんにちは\";\n\n    const r_last = utf8.getPrevGraphemeStart(text, text.len, 8, .unicode);\n    try testing.expect(r_last != null);\n    try testing.expectEqual(@as(usize, 12), r_last.?.start_offset);\n    try testing.expectEqual(@as(u32, 2), r_last.?.width);\n}\n\ntest \"getPrevGraphemeStart: realistic scenario\" {\n    const text = \"Hello 世界! 👋\";\n\n    const r_end = utf8.getPrevGraphemeStart(text, text.len, 8, .unicode);\n    try testing.expect(r_end != null);\n    try testing.expectEqual(@as(usize, 14), r_end.?.start_offset);\n\n    const r_space = utf8.getPrevGraphemeStart(text, 14, 8, .unicode);\n    try testing.expect(r_space != null);\n    try testing.expectEqual(@as(usize, 13), r_space.?.start_offset);\n    try testing.expectEqual(@as(u32, 1), r_space.?.width);\n}\n\ntest \"getPrevGraphemeStart: consecutive wide chars\" {\n    const text = \"世界中\";\n\n    const r9 = utf8.getPrevGraphemeStart(text, 9, 8, .unicode);\n    try testing.expect(r9 != null);\n    try testing.expectEqual(@as(usize, 6), r9.?.start_offset);\n    try testing.expectEqual(@as(u32, 2), r9.?.width);\n\n    const r6 = utf8.getPrevGraphemeStart(text, 6, 8, .unicode);\n    try testing.expect(r6 != null);\n    try testing.expectEqual(@as(usize, 3), r6.?.start_offset);\n    try testing.expectEqual(@as(u32, 2), r6.?.width);\n\n    const r3 = utf8.getPrevGraphemeStart(text, 3, 8, .unicode);\n    try testing.expect(r3 != null);\n    try testing.expectEqual(@as(usize, 0), r3.?.start_offset);\n    try testing.expectEqual(@as(u32, 2), r3.?.width);\n}\n\n// ============================================================================\n// CALCULATE TEXT WIDTH TESTS (static tab width)\n// ============================================================================\n\ntest \"calculateTextWidth: empty string\" {\n    const result = utf8.calculateTextWidth(\"\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 0), result);\n}\n\ntest \"calculateTextWidth: simple ASCII\" {\n    const result = utf8.calculateTextWidth(\"hello\", 4, true, .unicode);\n    try testing.expectEqual(@as(u32, 5), result);\n}\n\ntest \"calculateTextWidth: single tab\" {\n    const result = utf8.calculateTextWidth(\"\\t\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), result);\n}\n\ntest \"calculateTextWidth: tab with different widths\" {\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"\\t\", 2, false, .unicode));\n    try testing.expectEqual(@as(u32, 4), utf8.calculateTextWidth(\"\\t\", 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 8), utf8.calculateTextWidth(\"\\t\", 8, false, .unicode));\n}\n\ntest \"calculateTextWidth: multiple tabs\" {\n    const result = utf8.calculateTextWidth(\"\\t\\t\\t\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 12), result); // 3 tabs * 4 = 12\n}\n\ntest \"calculateTextWidth: text with tabs\" {\n    const result = utf8.calculateTextWidth(\"a\\tb\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), result); // a(1) + tab(4) + b(1) = 6\n}\n\ntest \"calculateTextWidth: multiple tabs between text\" {\n    const result = utf8.calculateTextWidth(\"a\\t\\tb\", 2, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), result); // a(1) + tab(2) + tab(2) + b(1) = 6\n}\n\ntest \"calculateTextWidth: tab at start\" {\n    const result = utf8.calculateTextWidth(\"\\tabc\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 7), result); // tab(4) + a(1) + b(1) + c(1) = 7\n}\n\ntest \"calculateTextWidth: tab at end\" {\n    const result = utf8.calculateTextWidth(\"abc\\t\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 7), result); // a(1) + b(1) + c(1) + tab(4) = 7\n}\n\ntest \"calculateTextWidth: CJK with tabs\" {\n    const result = utf8.calculateTextWidth(\"世\\t界\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), result); // 世(2) + tab(4) + 界(2) = 8\n}\n\ntest \"calculateTextWidth: emoji with tab\" {\n    const result = utf8.calculateTextWidth(\"🌍\\t\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), result); // emoji(2) + tab(4) = 6\n}\n\ntest \"calculateTextWidth: mixed ASCII and Unicode with tabs\" {\n    const result = utf8.calculateTextWidth(\"hello\\t世界\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 13), result); // hello(5) + tab(4) + 世(2) + 界(2) = 13\n}\n\ntest \"calculateTextWidth: realistic code with tabs\" {\n    const text = \"\\tif (x > 5) {\\n\\t\\treturn true;\\n\\t}\";\n    const result = utf8.calculateTextWidth(text, 2, false, .unicode);\n    // tab(2) + \"if (x > 5) {\" (12) + newline(0) + tab(2) + tab(2) + \"return true;\" (12) + newline(0) + tab(2) + \"}\" (1)\n    // = 2 + 12 + 2 + 2 + 12 + 2 + 1 = 33\n    try testing.expectEqual(@as(u32, 33), result);\n}\n\ntest \"calculateTextWidth: only spaces\" {\n    const result = utf8.calculateTextWidth(\"     \", 4, true, .unicode);\n    try testing.expectEqual(@as(u32, 5), result);\n}\n\ntest \"calculateTextWidth: tabs and spaces mixed\" {\n    const result = utf8.calculateTextWidth(\"  \\t  \\t  \", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 14), result); // 2 + 4 + 2 + 4 + 2 = 14\n}\n\ntest \"calculateTextWidth: control characters\" {\n    const result = utf8.calculateTextWidth(\"a\\x00b\\x1Fc\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 3), result); // Only printable chars: a, b, c\n}\n\ntest \"calculateTextWidth: combining marks\" {\n    const result = utf8.calculateTextWidth(\"cafe\\u{0301}\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), result); // c(1) + a(1) + f(1) + e(1) + combining(0) = 4\n}\n\ntest \"calculateTextWidth: scroll book and writing emojis width 2\" {\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"📜\", 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: Devanagari नमस्ते width 4\" {\n    const result = utf8.calculateTextWidth(\"नमस्ते\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), result);\n}\n\n// ============================================================================\n// UNICODE WARNING SIGNS WIDTH TESTS\n// ============================================================================\n\ntest \"calculateTextWidth: U+26A0 warning sign should be width 2\" {\n    const result = utf8.calculateTextWidth(\"⚠\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result);\n}\n\ntest \"calculateTextWidth: U+2049 exclamation question mark should be width 2\" {\n    const result = utf8.calculateTextWidth(\"⁉\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result);\n}\n\ntest \"calculateTextWidth: U+203C double exclamation mark should be width 2\" {\n    const result = utf8.calculateTextWidth(\"‼\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result);\n}\n\ntest \"calculateTextWidth: U+26D1 rescue worker helmet should be width 2\" {\n    const result = utf8.calculateTextWidth(\"⛑\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result);\n}\n\ntest \"calculateTextWidth: U+2622 radioactive sign should be width 2\" {\n    const result = utf8.calculateTextWidth(\"☢\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result);\n}\n\ntest \"calculateTextWidth: U+2623 biohazard sign should be width 2\" {\n    const result = utf8.calculateTextWidth(\"☣\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result);\n}\n\ntest \"calculateTextWidth: U+269B atom symbol should be width 2\" {\n    const result = utf8.calculateTextWidth(\"⚛\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result);\n}\n\n// ============================================================================\n// GRAPHEME INFO TESTS (for caching multi-byte graphemes and tabs)\n// ============================================================================\n\ntest \"findGraphemeInfo: empty string\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(\"\", 4, false, .unicode, testing.allocator, &result);\n    try testing.expectEqual(@as(usize, 0), result.items.len);\n}\n\ntest \"findGraphemeInfo: ASCII-only returns empty\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(\"hello world\", 4, true, .unicode, testing.allocator, &result);\n    try testing.expectEqual(@as(usize, 0), result.items.len);\n}\n\ntest \"findGraphemeInfo: ASCII with tab\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(\"hello\\tworld\", 4, false, .unicode, testing.allocator, &result);\n\n    // Should have one entry for the tab\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n    try testing.expectEqual(@as(u32, 5), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 1), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[0].width);\n    try testing.expectEqual(@as(u32, 5), result.items[0].col_offset);\n}\n\ntest \"findGraphemeInfo: multiple tabs\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(\"a\\tb\\tc\", 4, false, .unicode, testing.allocator, &result);\n\n    // Should have two entries for the tabs\n    try testing.expectEqual(@as(usize, 2), result.items.len);\n\n    // First tab at byte 1, col 1\n    try testing.expectEqual(@as(u32, 1), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 1), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[0].width);\n    try testing.expectEqual(@as(u32, 1), result.items[0].col_offset);\n\n    // Second tab at byte 3, col 6 (1 + 4 + 1)\n    try testing.expectEqual(@as(u32, 3), result.items[1].byte_offset);\n    try testing.expectEqual(@as(u8, 1), result.items[1].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[1].width);\n    try testing.expectEqual(@as(u32, 6), result.items[1].col_offset);\n}\n\ntest \"findGraphemeInfo: CJK characters\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"hello世界\";\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have two entries for the CJK characters\n    try testing.expectEqual(@as(usize, 2), result.items.len);\n\n    // 世 at byte 5\n    try testing.expectEqual(@as(u32, 5), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 2), result.items[0].width);\n    try testing.expectEqual(@as(u32, 5), result.items[0].col_offset);\n\n    // 界 at byte 8\n    try testing.expectEqual(@as(u32, 8), result.items[1].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[1].byte_len);\n    try testing.expectEqual(@as(u8, 2), result.items[1].width);\n    try testing.expectEqual(@as(u32, 7), result.items[1].col_offset);\n}\n\ntest \"findGraphemeInfo: emoji with skin tone\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"Hi👋🏿Bye\"; // Hi + wave + dark skin tone + Bye\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have one entry for the emoji cluster\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u32, 2), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 8), result.items[0].byte_len); // 4 + 4 bytes\n    try testing.expectEqual(@as(u8, 2), result.items[0].width);\n    try testing.expectEqual(@as(u32, 2), result.items[0].col_offset);\n}\n\ntest \"findGraphemeInfo: emoji with ZWJ\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"a👩‍🚀b\"; // a + woman astronaut + b\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have one entry for the emoji cluster\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u32, 1), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 2), result.items[0].width);\n    try testing.expectEqual(@as(u32, 1), result.items[0].col_offset);\n}\n\ntest \"findGraphemeInfo: combining mark\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"cafe\\u{0301}\"; // café with combining acute\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have one entry for e + combining mark\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u32, 3), result.items[0].byte_offset); // 'e' position\n    try testing.expectEqual(@as(u8, 3), result.items[0].byte_len); // e (1 byte) + combining (2 bytes)\n    try testing.expectEqual(@as(u8, 1), result.items[0].width);\n    try testing.expectEqual(@as(u32, 3), result.items[0].col_offset);\n}\n\ntest \"findGraphemeInfo: flag emoji\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"US🇺🇸\"; // US + flag\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have one entry for the flag (two regional indicators)\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u32, 2), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 8), result.items[0].byte_len); // Two 4-byte chars\n    try testing.expectEqual(@as(u8, 2), result.items[0].width);\n    try testing.expectEqual(@as(u32, 2), result.items[0].col_offset);\n}\n\ntest \"findGraphemeInfo: mixed content\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"Hi\\t世界!\"; // Hi + tab + CJK + !\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have three entries: tab, 世, 界\n    try testing.expectEqual(@as(usize, 3), result.items.len);\n\n    // Tab at byte 2, col 2\n    try testing.expectEqual(@as(u32, 2), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 1), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[0].width);\n    try testing.expectEqual(@as(u32, 2), result.items[0].col_offset);\n\n    // 世 at byte 3, col 6\n    try testing.expectEqual(@as(u32, 3), result.items[1].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[1].byte_len);\n    try testing.expectEqual(@as(u8, 2), result.items[1].width);\n    try testing.expectEqual(@as(u32, 6), result.items[1].col_offset);\n\n    // 界 at byte 6, col 8\n    try testing.expectEqual(@as(u32, 6), result.items[2].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[2].byte_len);\n    try testing.expectEqual(@as(u8, 2), result.items[2].width);\n    try testing.expectEqual(@as(u32, 8), result.items[2].col_offset);\n}\n\ntest \"findGraphemeInfo: only ASCII letters no cache\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(\"abcdefghij\", 4, false, .unicode, testing.allocator, &result);\n\n    // No special characters, should be empty\n    try testing.expectEqual(@as(usize, 0), result.items.len);\n}\n\ntest \"findGraphemeInfo: emoji with VS16\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"I ❤️ U\"; // I + space + heart + VS16 + space + U\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have one entry for the emoji cluster\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u32, 2), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 2), result.items[0].width);\n    try testing.expectEqual(@as(u32, 2), result.items[0].col_offset);\n}\n\ntest \"findGraphemeInfo: realistic text\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"function test() {\\n\\tconst 世界 = 10;\\n}\";\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have entries for: tab, 世, 界\n    try testing.expectEqual(@as(usize, 3), result.items.len);\n}\n\ntest \"findGraphemeInfo: hiragana\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"こんにちは\";\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    // Should have 5 entries (each hiragana is 3 bytes, width 2)\n    try testing.expectEqual(@as(usize, 5), result.items.len);\n\n    // Check first character\n    try testing.expectEqual(@as(u32, 0), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 2), result.items[0].width);\n}\n\ntest \"findGraphemeInfo: at SIMD boundary\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    // Create text with multibyte char near SIMD boundary (16 bytes)\n    var buf: [32]u8 = undefined;\n    @memset(&buf, 'x');\n    const cjk = \"世\";\n    @memcpy(buf[14..17], cjk); // Place CJK char at boundary\n\n    try utf8.findGraphemeInfo(&buf, 4, false, .unicode, testing.allocator, &result);\n\n    // Should find the CJK character\n    var found = false;\n    for (result.items) |g| {\n        if (g.byte_offset == 14) {\n            found = true;\n            try testing.expectEqual(@as(u8, 3), g.byte_len);\n            try testing.expectEqual(@as(u8, 2), g.width);\n            break;\n        }\n    }\n    try testing.expect(found);\n}\n\ntest \"calculateTextWidth: book and writing hand emojis width 2\" {\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"📖\", 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(\"✍️\", 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: Devanagari script\" {\n    const result = utf8.calculateTextWidth(\"देवनागरी\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 5), result);\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(\"प्रथम\", 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: checkmark symbol\" {\n    const result = utf8.calculateTextWidth(\"✓\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), result);\n}\n\ntest \"calculateTextWidth: emoji with skin tone\" {\n    const result = utf8.calculateTextWidth(\"👋🏿\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result); // 👋🏿 is a single grapheme with width 2\n}\n\ntest \"calculateTextWidth: emoji with ZWJ\" {\n    const result = utf8.calculateTextWidth(\"👩‍🚀\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result); // 👩‍🚀 is a single grapheme with width 2\n}\n\ntest \"calculateTextWidth: emoji with VS16 selector\" {\n    const result = utf8.calculateTextWidth(\"❤️\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result); // ❤️ (heart + VS16) is a single grapheme with width 2\n}\n\ntest \"calculateTextWidth: flag emoji\" {\n    const result = utf8.calculateTextWidth(\"🇺🇸\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result); // 🇺🇸 is a single grapheme with width 2\n}\n\ntest \"calculateTextWidth: hiragana with tab\" {\n    const result = utf8.calculateTextWidth(\"こん\\tにちは\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 14), result); // こ(2) + ん(2) + tab(4) + に(2) + ち(2) + は(2) = 14\n}\n\ntest \"calculateTextWidth: fullwidth forms with tab\" {\n    const result = utf8.calculateTextWidth(\"ＡＢ\\tＣ\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 10), result); // Ａ(2) + Ｂ(2) + tab(4) + Ｃ(2) = 10\n}\n\ntest \"calculateTextWidth: ASCII fast path consistency\" {\n    const text_ascii = \"hello world\";\n    const result_fast = utf8.calculateTextWidth(text_ascii, 4, true, .unicode);\n    const result_slow = utf8.calculateTextWidth(text_ascii, 4, false, .unicode);\n    try testing.expectEqual(result_fast, result_slow);\n}\n\ntest \"calculateTextWidth: large text with many tabs\" {\n    const size = 1000;\n    const buf = try testing.allocator.alloc(u8, size);\n    defer testing.allocator.free(buf);\n\n    var expected: u32 = 0;\n    for (buf, 0..) |*b, i| {\n        if (i % 10 == 0) {\n            b.* = '\\t';\n            expected += 4;\n        } else {\n            b.* = 'a';\n            expected += 1;\n        }\n    }\n\n    const result = utf8.calculateTextWidth(buf, 4, false, .unicode);\n    try testing.expectEqual(expected, result);\n}\n\ntest \"calculateTextWidth: comparison with manual calculation\" {\n    const test_cases = [_]struct {\n        text: []const u8,\n        tab_width: u8,\n        expected: u32,\n    }{\n        .{ .text = \"\\t\", .tab_width = 2, .expected = 2 },\n        .{ .text = \"\\t\\t\", .tab_width = 2, .expected = 4 },\n        .{ .text = \"a\\t\", .tab_width = 2, .expected = 3 },\n        .{ .text = \"\\ta\", .tab_width = 2, .expected = 3 },\n        .{ .text = \"a\\tb\", .tab_width = 2, .expected = 4 },\n        .{ .text = \"ab\\tcd\", .tab_width = 4, .expected = 8 },\n        .{ .text = \"\\t\\tx\", .tab_width = 2, .expected = 5 },\n        .{ .text = \"世\\t界\", .tab_width = 2, .expected = 6 },\n    };\n\n    for (test_cases) |tc| {\n        const result = utf8.calculateTextWidth(tc.text, tc.tab_width, false, .unicode);\n        try testing.expectEqual(tc.expected, result);\n    }\n}\n\n// ============================================================================\n// LINE WIDTH WITH GRAPHEMES TESTS\n// Testing that calculateTextWidth returns correct Unicode display widths\n// ============================================================================\n\ntest \"calculateTextWidth: checkmark grapheme ✅\" {\n    // Test simple checkmark emoji\n    const checkmark = \"✅\";\n\n    // Calculate width using utf8.zig's calculateTextWidth\n    const width = utf8.calculateTextWidth(checkmark, 4, false, .unicode);\n\n    // The checkmark ✅ (U+2705) should be width 2\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\ntest \"calculateTextWidth: Sanskrit text with combining marks\" {\n    const result = utf8.calculateTextWidth(\"संस्कृति\", 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), result);\n}\n\ntest \"calculateTextWidth: checkmark in text\" {\n    // Test checkmark in context\n    const text = \"Done ✅\";\n\n    // Calculate width using utf8.zig\n    const width = utf8.calculateTextWidth(text, 4, false, .unicode);\n\n    // Should return: D(1) + o(1) + n(1) + e(1) + space(1) + ✅(2) = 7\n    try testing.expectEqual(@as(u32, 7), width);\n}\n\ntest \"calculateTextWidth: various emoji graphemes\" {\n    const test_cases = [_]struct {\n        text: []const u8,\n        name: []const u8,\n        expected_width: u32,\n    }{\n        .{ .text = \"✅\", .name = \"checkmark U+2705\", .expected_width = 2 },\n        .{ .text = \"❤️\", .name = \"red heart U+2764+FE0F\", .expected_width = 2 },\n        .{ .text = \"🎉\", .name = \"party popper U+1F389\", .expected_width = 2 },\n        .{ .text = \"🔥\", .name = \"fire U+1F525\", .expected_width = 2 },\n        .{ .text = \"💯\", .name = \"hundred points U+1F4AF\", .expected_width = 2 },\n        .{ .text = \"🚀\", .name = \"rocket U+1F680\", .expected_width = 2 },\n        .{ .text = \"⭐\", .name = \"star U+2B50\", .expected_width = 2 },\n        .{ .text = \"👍\", .name = \"thumbs up U+1F44D\", .expected_width = 2 },\n    };\n\n    for (test_cases) |tc| {\n        const width = utf8.calculateTextWidth(tc.text, 4, false, .unicode);\n        try testing.expectEqual(tc.expected_width, width);\n    }\n}\n\ntest \"calculateTextWidth: complex graphemes with ZWJ\" {\n    // Woman astronaut: 👩‍🚀 (woman + ZWJ + rocket)\n    const woman_astronaut = \"👩‍🚀\";\n\n    const width = utf8.calculateTextWidth(woman_astronaut, 4, false, .unicode);\n\n    // Should return 2 for the combined grapheme (not 5 for individual codepoints)\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\ntest \"calculateTextWidth: flag emoji grapheme\" {\n    // US flag: 🇺🇸 (two regional indicator symbols)\n    const us_flag = \"🇺🇸\";\n\n    const width = utf8.calculateTextWidth(us_flag, 4, false, .unicode);\n\n    // Should return 2 for the flag grapheme\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\ntest \"calculateTextWidth: skin tone modifier grapheme\" {\n    // Waving hand with dark skin tone: 👋🏿\n    const wave_dark = \"👋🏿\";\n\n    const width = utf8.calculateTextWidth(wave_dark, 4, false, .unicode);\n\n    // Should return 2 for the combined grapheme (not 4 for individual codepoints)\n    try testing.expectEqual(@as(u32, 2), width);\n}\n// ============================================================================\n// COMPREHENSIVE UNICODE GRAPHEME TESTS FOR calculateTextWidth\n// Testing various emoji, ZWJ sequences, Indic scripts, and Unicode edge cases\n// ============================================================================\n\n// ----------------------------------------------------------------------------\n// Emoji Presentation Tests\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: emoji presentation with VS15 (text)\" {\n    // U+2764 (heart) + U+FE0E (VS15 - text presentation)\n    const heart_text = \"❤\\u{FE0E}\";\n    const width = utf8.calculateTextWidth(heart_text, 4, false, .unicode);\n    // With text presentation selector, should still be counted as grapheme width 2\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\ntest \"calculateTextWidth: emoji presentation with VS16 (emoji)\" {\n    // U+2764 (heart) + U+FE0F (VS16 - emoji presentation) - already tested as ❤️\n    const heart_emoji = \"❤️\";\n    const width = utf8.calculateTextWidth(heart_emoji, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\ntest \"calculateTextWidth: keycap sequences\" {\n    // Digit + U+FE0F + U+20E3 (combining enclosing keycap)\n    const keycap_1 = \"1️⃣\"; // U+0031 U+FE0F U+20E3\n    const keycap_hash = \"#️⃣\"; // U+0023 U+FE0F U+20E3\n\n    // Keycap: base char (1) + VS16 (changes to emoji presentation, width 2) + combining keycap (0) = 2 total width\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(keycap_1, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(keycap_hash, 4, false, .unicode));\n}\n\n// ----------------------------------------------------------------------------\n// Complex ZWJ Sequences\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: family ZWJ sequences\" {\n    // Family: man, woman, girl, boy (4 people)\n    const family = \"👨‍👩‍👧‍👦\"; // man + ZWJ + woman + ZWJ + girl + ZWJ + boy\n    const width = utf8.calculateTextWidth(family, 4, false, .unicode);\n    // Should be counted as single grapheme with width 2\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\ntest \"calculateTextWidth: profession ZWJ sequences\" {\n    // Woman health worker: woman + ZWJ + health worker\n    const health_worker = \"👩‍⚕️\";\n    const firefighter = \"👨‍🚒\";\n    const teacher = \"👩‍🏫\";\n\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(health_worker, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(firefighter, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(teacher, 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: couple ZWJ sequences\" {\n    // Kiss: person + ZWJ + heart + ZWJ + person\n    const kiss = \"💏\"; // Single codepoint\n    const couple_with_heart = \"👩‍❤️‍👨\"; // woman + ZWJ + heart + VS16 + ZWJ + man\n\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(kiss, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(couple_with_heart, 4, false, .unicode));\n}\n\n// ----------------------------------------------------------------------------\n// Skin Tone Modifiers (Fitzpatrick scale)\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: all skin tone modifiers\" {\n    // Fitzpatrick Type-1-2 (light skin tone) U+1F3FB\n    const wave_light = \"👋🏻\";\n    // Fitzpatrick Type-3 (medium-light skin tone) U+1F3FC\n    const wave_medium_light = \"👋🏼\";\n    // Fitzpatrick Type-4 (medium skin tone) U+1F3FD\n    const wave_medium = \"👋🏽\";\n    // Fitzpatrick Type-5 (medium-dark skin tone) U+1F3FE\n    const wave_medium_dark = \"👋🏾\";\n    // Fitzpatrick Type-6 (dark skin tone) U+1F3FF\n    const wave_dark = \"👋🏿\";\n\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(wave_light, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(wave_medium_light, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(wave_medium, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(wave_medium_dark, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(wave_dark, 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: skin tone with ZWJ\" {\n    // Family with skin tones: man(dark) + ZWJ + woman(light) + ZWJ + child\n    const family_skin_tones = \"👨🏿‍👩🏻‍👶\";\n    const width = utf8.calculateTextWidth(family_skin_tones, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\n// ----------------------------------------------------------------------------\n// Regional Indicator Symbols (Flags)\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: various flag emojis\" {\n    const flag_us = \"🇺🇸\"; // U+1F1FA U+1F1F8\n    const flag_uk = \"🇬🇧\"; // U+1F1EC U+1F1E7\n    const flag_jp = \"🇯🇵\"; // U+1F1EF U+1F1F5\n    const flag_de = \"🇩🇪\"; // U+1F1E9 U+1F1EA\n    const flag_fr = \"🇫🇷\"; // U+1F1EB U+1F1F7\n\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(flag_us, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(flag_uk, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(flag_jp, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(flag_de, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(flag_fr, 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: multiple flags in text\" {\n    const text = \"Flags: 🇺🇸 🇬🇧 🇯🇵\";\n    const width = utf8.calculateTextWidth(text, 4, false, .unicode);\n    // \"Flags: \" (7) + 🇺🇸 (2) + \" \" (1) + 🇬🇧 (2) + \" \" (1) + 🇯🇵 (2) = 15\n    try testing.expectEqual(@as(u32, 15), width);\n}\n\n// ----------------------------------------------------------------------------\n// Devanagari and Indic Scripts\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: Devanagari basic characters\" {\n    // Devanagari script (Hindi, Sanskrit, etc.)\n    const namaste = \"नमस्ते\"; // na-ma-s-te with virama\n    const width = utf8.calculateTextWidth(namaste, 4, false, .unicode);\n    // Devanagari characters are typically width 1 each\n    // This is 5 graphemes: न म स् ते (the virama combines with स)\n    try testing.expect(width > 0); // Exact width depends on grapheme clustering\n}\n\ntest \"calculateTextWidth: Devanagari with combining marks\" {\n    // Devanagari vowel signs and nukta\n    const ka = \"क\"; // Base character\n    const ki = \"कि\"; // क + vowel sign i (U+093F)\n    const kii = \"की\"; // क + vowel sign ii (U+0940)\n\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(ka, 4, false, .unicode));\n    // With combining vowel signs, should still be 1 grapheme\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(ki, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(kii, 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: Devanagari conjuncts\" {\n    // Conjunct consonants with virama\n    const kta = \"क्त\"; // क + virama + त (kta)\n    const jna = \"ज्ञ\"; // ज + virama + ञ (jna)\n    const ksha = \"क्‍ष\"; // क + virama + ZWJ + ष (kṣa with explicit ZWJ)\n\n    // These form single grapheme clusters but width = number of base consonants\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(kta, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(jna, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(ksha, 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: Bengali script\" {\n    // Bengali/Bangla script\n    const bangla = \"বাংলা\"; // Bangla\n    const width = utf8.calculateTextWidth(bangla, 4, false, .unicode);\n    try testing.expect(width > 0);\n}\n\ntest \"calculateTextWidth: Tamil script\" {\n    // Tamil script (no conjuncts, simpler than Devanagari)\n    const tamil = \"தமிழ்\"; // Tamil\n    const width = utf8.calculateTextWidth(tamil, 4, false, .unicode);\n    try testing.expect(width > 0);\n}\n\ntest \"calculateTextWidth: Telugu script\" {\n    // Telugu script\n    const telugu = \"తెలుగు\"; // Telugu\n    const width = utf8.calculateTextWidth(telugu, 4, false, .unicode);\n    try testing.expect(width > 0);\n}\n\n// ----------------------------------------------------------------------------\n// Arabic and RTL Scripts\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: Arabic basic text\" {\n    // Arabic text (RTL, but width calculation is the same)\n    const arabic = \"مرحبا\"; // Marhaba (hello)\n    const width = utf8.calculateTextWidth(arabic, 4, false, .unicode);\n    // Arabic characters are width 1 each\n    try testing.expect(width >= 5);\n}\n\ntest \"calculateTextWidth: Arabic with diacritics\" {\n    // Arabic with harakat (diacritical marks)\n    const with_diacritics = \"مَرْحَبًا\"; // Marhaba with vowel marks\n    const width = utf8.calculateTextWidth(with_diacritics, 4, false, .unicode);\n    // Combining marks should not add to width\n    try testing.expect(width >= 5);\n}\n\ntest \"calculateTextWidth: Hebrew text\" {\n    // Hebrew text (RTL)\n    const hebrew = \"שלום\"; // Shalom\n    const width = utf8.calculateTextWidth(hebrew, 4, false, .unicode);\n    try testing.expect(width >= 4);\n}\n\n// ----------------------------------------------------------------------------\n// East Asian Scripts (CJK)\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: Chinese traditional characters\" {\n    const traditional = \"繁體中文\"; // Traditional Chinese\n    const width = utf8.calculateTextWidth(traditional, 4, false, .unicode);\n    // Each CJK character is width 2\n    try testing.expectEqual(@as(u32, 8), width); // 4 chars * 2 = 8\n}\n\ntest \"calculateTextWidth: Chinese simplified characters\" {\n    const simplified = \"简体中文\"; // Simplified Chinese\n    const width = utf8.calculateTextWidth(simplified, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), width); // 4 chars * 2 = 8\n}\n\ntest \"calculateTextWidth: Japanese mixed scripts\" {\n    // Hiragana + Kanji + Katakana\n    const mixed = \"ひらがな漢字カタカナ\"; // hiragana, kanji, katakana\n    const width = utf8.calculateTextWidth(mixed, 4, false, .unicode);\n    // All are width 2: 4 hiragana + 2 kanji + 4 katakana = 10 chars * 2 = 20\n    try testing.expectEqual(@as(u32, 20), width);\n}\n\ntest \"calculateTextWidth: Korean Hangul syllables\" {\n    const korean = \"한글\"; // Hangul (Korean)\n    const width = utf8.calculateTextWidth(korean, 4, false, .unicode);\n    // Hangul syllables are width 2\n    try testing.expectEqual(@as(u32, 4), width); // 2 chars * 2 = 4\n}\n\ntest \"calculateTextWidth: CJK with ASCII\" {\n    const mixed = \"Hello世界World\"; // ASCII + CJK + ASCII\n    const width = utf8.calculateTextWidth(mixed, 4, false, .unicode);\n    // \"Hello\" (5) + \"世界\" (4) + \"World\" (5) = 14\n    try testing.expectEqual(@as(u32, 14), width);\n}\n\n// ----------------------------------------------------------------------------\n// Combining Marks and Diacritics\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: multiple combining marks on one base\" {\n    // Base + multiple combining marks\n    const multiple = \"e\\u{0301}\\u{0302}\\u{0304}\"; // e + acute + circumflex + macron\n    const width = utf8.calculateTextWidth(multiple, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), width);\n}\n\ntest \"calculateTextWidth: combining enclosing marks\" {\n    // Combining enclosing circle backslash U+20E0\n    const enclosed = \"a\\u{20E0}\";\n    const width = utf8.calculateTextWidth(enclosed, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), width);\n}\n\ntest \"calculateTextWidth: Vietnamese with multiple diacritics\" {\n    // Vietnamese uses Latin with complex diacritics\n    const vietnamese = \"Tiếng Việt\"; // Vietnamese language\n    const width = utf8.calculateTextWidth(vietnamese, 4, false, .unicode);\n    // Each base character with combining marks = 1 width\n    // \"Tiếng\" (5) + \" \" (1) + \"Việt\" (4) = 10\n    try testing.expectEqual(@as(u32, 10), width);\n}\n\n// ----------------------------------------------------------------------------\n// Zero-Width Characters\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: zero width joiner (ZWJ)\" {\n    // ZWJ by itself (shouldn't happen, but test it) - it's a format char with width 0\n    const zwj = \"\\u{200D}\";\n    const width = utf8.calculateTextWidth(zwj, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 0), width); // Width of ZWJ is 0 (Cf category)\n}\n\ntest \"calculateTextWidth: zero width non-joiner (ZWNJ)\" {\n    // ZWNJ U+200C\n    const zwnj = \"ab\\u{200C}cd\";\n    const width = utf8.calculateTextWidth(zwnj, 4, false, .unicode);\n    // ZWNJ has width 0, so should be 4 (a, b, c, d)\n    try testing.expectEqual(@as(u32, 4), width);\n}\n\ntest \"calculateTextWidth: zero width space\" {\n    // ZWSP U+200B is Cf (format) category with width 0\n    const zwsp = \"a\\u{200B}b\\u{200B}c\";\n    const width = utf8.calculateTextWidth(zwsp, 4, false, .unicode);\n    // a(1) + ZWSP(0) + b(1) + ZWSP(0) + c(1) = 3\n    try testing.expectEqual(@as(u32, 3), width);\n}\n\ntest \"calculateTextWidth: word joiner\" {\n    // Word joiner U+2060 is Cf (format) category with width 0\n    const word_joiner = \"word\\u{2060}joiner\";\n    const width = utf8.calculateTextWidth(word_joiner, 4, false, .unicode);\n    // word(4) + word_joiner(0) + joiner(6) = 10\n    try testing.expectEqual(@as(u32, 10), width);\n}\n\n// ----------------------------------------------------------------------------\n// Special Unicode Spaces\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: various Unicode spaces\" {\n    // En space U+2002\n    const en_space = \"a\\u{2002}b\";\n    // Em space U+2003\n    const em_space = \"a\\u{2003}b\";\n    // Thin space U+2009\n    const thin_space = \"a\\u{2009}b\";\n    // Hair space U+200A\n    const hair_space = \"a\\u{200A}b\";\n    // Ideographic space U+3000 (CJK)\n    const ideo_space = \"a\\u{3000}b\";\n\n    // These are all real spaces with width 1\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(en_space, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(em_space, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(thin_space, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(hair_space, 4, false, .unicode));\n    // Ideographic space is width 2 (fullwidth)\n    try testing.expectEqual(@as(u32, 4), utf8.calculateTextWidth(ideo_space, 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: non-breaking spaces\" {\n    // NBSP U+00A0\n    const nbsp = \"a\\u{00A0}b\";\n    // Narrow NBSP U+202F\n    const narrow_nbsp = \"a\\u{202F}b\";\n\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(nbsp, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(narrow_nbsp, 4, false, .unicode));\n}\n\n// ----------------------------------------------------------------------------\n// Emoji Modifiers and Tags\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: emoji with multiple modifiers\" {\n    // Rainbow flag (black flag + rainbow)\n    const rainbow_flag = \"🏴‍🌈\"; // U+1F3F4 U+200D U+1F308\n    const width = utf8.calculateTextWidth(rainbow_flag, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\ntest \"calculateTextWidth: emoji tag sequences (subdivision flags)\" {\n    // England flag: 🏴󠁧󠁢󠁥󠁮󠁧󠁿 (black flag + tag chars + cancel tag)\n    // This is complex to type, so we'll test a simpler version\n    const black_flag = \"🏴\"; // Just the base flag\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(black_flag, 4, false, .unicode));\n}\n\ntest \"calculateTextWidth: hair style variations\" {\n    // Person: red hair, curly hair, white hair, bald\n    const red_hair = \"👩‍🦰\";\n    const curly_hair = \"👨‍🦱\";\n    const white_hair = \"👩‍🦳\";\n    const bald = \"👨‍🦲\";\n\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(red_hair, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(curly_hair, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(white_hair, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(bald, 4, false, .unicode));\n}\n\n// ----------------------------------------------------------------------------\n// Mixed Content and Real-world Scenarios\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: multilingual sentence\" {\n    // Mix of Latin, CJK, Arabic, Emoji\n    const text = \"Hello 世界! مرحبا 👋\";\n    const width = utf8.calculateTextWidth(text, 4, false, .unicode);\n    // \"Hello \" (6) + \"世界\" (4) + \"! \" (2) + \"مرحبا\" (5) + \" \" (1) + \"👋\" (2) = 20\n    try testing.expect(width >= 18); // Allow some flexibility for combining marks\n}\n\ntest \"calculateTextWidth: code with emoji comments\" {\n    const code = \"const x = 42; // ✅ works\";\n    const width = utf8.calculateTextWidth(code, 4, false, .unicode);\n    // Most chars are width 1, checkmark is width 2\n    // \"const x = 42; // \" (17) + \"✅\" (2) + \" works\" (6) = 25\n    try testing.expectEqual(@as(u32, 25), width);\n}\n\ntest \"calculateTextWidth: emoji sentence\" {\n    const text = \"I ❤️ 🍕 and 🍣!\";\n    const width = utf8.calculateTextWidth(text, 4, false, .unicode);\n    // \"I \" (2) + \"❤️\" (2) + \" \" (1) + \"🍕\" (2) + \" and \" (5) + \"🍣\" (2) + \"!\" (1) = 15\n    try testing.expectEqual(@as(u32, 15), width);\n}\n\ntest \"calculateTextWidth: social media style text\" {\n    const text = \"#OpenTUI 🚀 is #awesome 💯!\";\n    const width = utf8.calculateTextWidth(text, 4, false, .unicode);\n    // \"#OpenTUI \" (9) + \"🚀\" (2) + \" is #awesome \" (13) + \"💯\" (2) + \"!\" (1) = 27\n    try testing.expectEqual(@as(u32, 27), width);\n}\n\n// ----------------------------------------------------------------------------\n// Edge Cases and Boundaries\n// ----------------------------------------------------------------------------\n\ntest \"calculateTextWidth: surrogate pair edge cases\" {\n    // Valid surrogate pairs (emoji are in supplementary planes)\n    const emoji = \"𝕳𝖊𝖑𝖑𝖔\"; // Mathematical bold letters (U+1D577 etc)\n    const width = utf8.calculateTextWidth(emoji, 4, false, .unicode);\n    // These are typically width 1 each\n    try testing.expectEqual(@as(u32, 5), width);\n}\n\ntest \"calculateTextWidth: long grapheme cluster chain\" {\n    // Create a base + many combining marks\n    var text: std.ArrayListUnmanaged(u8) = .{};\n    defer text.deinit(testing.allocator);\n\n    try text.appendSlice(testing.allocator, \"e\");\n    // Add 10 combining marks\n    var i: usize = 0;\n    while (i < 10) : (i += 1) {\n        try text.appendSlice(testing.allocator, \"\\u{0301}\"); // Combining acute accent\n    }\n\n    const width = utf8.calculateTextWidth(text.items, 4, false, .unicode);\n    // Should be treated as single grapheme\n    try testing.expectEqual(@as(u32, 1), width);\n}\n\ntest \"calculateTextWidth: all emoji skin tones in sequence\" {\n    const text = \"👋🏻👋🏼👋🏽👋🏾👋🏿\";\n    const width = utf8.calculateTextWidth(text, 4, false, .unicode);\n    // 5 emoji with skin tones, each is 1 grapheme with width 2\n    try testing.expectEqual(@as(u32, 10), width); // 5 * 2 = 10\n}\n\ntest \"calculateTextWidth: emoji zodiac signs\" {\n    const zodiac = \"♈♉♊♋♌♍♎♏♐♑♒♓\"; // All 12 zodiac signs\n    const width = utf8.calculateTextWidth(zodiac, 4, false, .unicode);\n    // Each zodiac symbol is width 2\n    try testing.expectEqual(@as(u32, 24), width); // 12 * 2 = 24\n}\n\ntest \"calculateTextWidth: mathematical symbols\" {\n    // Mathematical operators and symbols\n    const math = \"∀∃∈∉∋∑∏∫∂∇≠≤≥\"; // Various math symbols\n    const width = utf8.calculateTextWidth(math, 4, false, .unicode);\n    // Most math symbols are width 1\n    try testing.expect(width >= 13);\n}\n\ntest \"calculateTextWidth: box drawing characters\" {\n    // Box drawing characters (width 1)\n    const box = \"┌─┐│└─┘\"; // Simple box\n    const width = utf8.calculateTextWidth(box, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 7), width);\n}\n\ntest \"calculateTextWidth: braille patterns\" {\n    // Braille patterns U+2800-U+28FF\n    const braille = \"⠀⠁⠂⠃⠄⠅⠆⠇\"; // Some braille patterns\n    const width = utf8.calculateTextWidth(braille, 4, false, .unicode);\n    // Braille patterns are width 1\n    try testing.expectEqual(@as(u32, 8), width);\n}\n\ntest \"calculateTextWidth: musical symbols\" {\n    // Musical notation symbols\n    const music = \"𝄞𝄢𝅘𝅥𝅮\"; // Treble clef, bass clef, notes (U+1D11E etc)\n    const width = utf8.calculateTextWidth(music, 4, false, .unicode);\n    // Musical symbols are typically width 1, but encoding might be issue - just verify no crash\n    try testing.expect(width >= 0); // Accept any non-negative width\n}\n\ntest \"calculateTextWidth: weather and nature emoji\" {\n    const weather = \"☀️🌤️⛅🌦️🌧️⛈️\"; // Sun, clouds, rain\n    const width = utf8.calculateTextWidth(weather, 4, false, .unicode);\n    // Each emoji is width 2\n    try testing.expectEqual(@as(u32, 12), width); // 6 * 2 = 12\n}\n\ntest \"calculateTextWidth: food emoji collection\" {\n    const food = \"🍎🍌🍇🍓🥕🥦🍞🧀\"; // Various food items\n    const width = utf8.calculateTextWidth(food, 4, false, .unicode);\n    // 8 emoji * 2 = 16\n    try testing.expectEqual(@as(u32, 16), width);\n}\n\ntest \"calculateTextWidth: animal emoji\" {\n    const animals = \"🐶🐱🐭🐹🐰🦊🐻🐼\"; // Various animals\n    const width = utf8.calculateTextWidth(animals, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 16), width); // 8 * 2 = 16\n}\n\ntest \"calculateTextWidth: realistic chat message\" {\n    const message = \"Hey! 👋 Can you review my PR? 🙏 It fixes the bug 🐛 we discussed earlier. Thanks! 😊\";\n    const width = utf8.calculateTextWidth(message, 4, false, .unicode);\n    // Long string with multiple emoji - just verify it doesn't crash\n    try testing.expect(width > 70);\n}\n\ntest \"calculateTextWidth: empty string with tabs\" {\n    const text = \"\";\n    try testing.expectEqual(@as(u32, 0), utf8.calculateTextWidth(text, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 0), utf8.calculateTextWidth(text, 8, false, .unicode));\n}\n\ntest \"calculateTextWidth: only combining marks (invalid but should not crash)\" {\n    const text = \"\\u{0301}\\u{0302}\\u{0303}\"; // Just combining marks, no base\n    const width = utf8.calculateTextWidth(text, 4, false, .unicode);\n    // Should handle gracefully - each combining mark might be width 0\n    try testing.expect(width >= 0);\n}\n\ntest \"calculateTextWidth: emoji collection - celestial and symbols\" {\n    const celestial = \"🌟🔮✨\";\n    const width = utf8.calculateTextWidth(celestial, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - religious and gestures\" {\n    const religious = \"🙏\";\n    const width = utf8.calculateTextWidth(religious, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), width); // 1 emoji * 2 = 2\n}\n\ntest \"calculateTextWidth: emoji collection - ZWJ sequences astronauts\" {\n    const astronauts = \"🧑‍🚀👨‍🚀👩‍🚀\";\n    const width = utf8.calculateTextWidth(astronauts, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 graphemes * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - rainbow and magical creatures\" {\n    const magical = \"🌈🦄🧚‍♀️\";\n    const width = utf8.calculateTextWidth(magical, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 graphemes * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - books and writing\" {\n    const writing = \"📜📖✍️\";\n    const width = utf8.calculateTextWidth(writing, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - Japanese culture\" {\n    const japanese = \"🏯🎋🌸\";\n    const width = utf8.calculateTextWidth(japanese, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - traditional Japanese items\" {\n    const traditional = \"📯🎴🎎\";\n    const width = utf8.calculateTextWidth(traditional, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - hearts and peace\" {\n    const peace = \"💝🕊️☮️\";\n    const width = utf8.calculateTextWidth(peace, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - meditation and nature\" {\n    const meditation = \"🧘‍♂️🌳\";\n    const width = utf8.calculateTextWidth(meditation, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), width); // 2 graphemes * 2 = 4\n}\n\ntest \"calculateTextWidth: emoji collection - food and drink\" {\n    const food = \"🍵🥟\";\n    const width = utf8.calculateTextWidth(food, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), width); // 2 emoji * 2 = 4\n}\n\ntest \"calculateTextWidth: emoji collection - exotic animals\" {\n    const animals = \"🦥🦦🦧🦨🦩🦚🦜🦝🦞🦟\";\n    const width = utf8.calculateTextWidth(animals, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 20), width); // 10 emoji * 2 = 20\n}\n\ntest \"calculateTextWidth: emoji collection - communication\" {\n    const communication = \"🤫🗣️💬\";\n    const width = utf8.calculateTextWidth(communication, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - water and nature\" {\n    const nature = \"🌊📝🎭\";\n    const width = utf8.calculateTextWidth(nature, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - landscape\" {\n    const landscape = \"🏞️🌊💧\";\n    const width = utf8.calculateTextWidth(landscape, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - circus and art\" {\n    const circus = \"🤹‍♂️🎪🎨\";\n    const width = utf8.calculateTextWidth(circus, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 graphemes * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - shopping and food items\" {\n    const shopping = \"🏪🛒💰🌶️🧄🧅\";\n    const width = utf8.calculateTextWidth(shopping, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 12), width); // 6 emoji * 2 = 12\n}\n\ntest \"calculateTextWidth: emoji collection - textiles and art\" {\n    const textiles = \"🧵👘🎨🖼️\";\n    const width = utf8.calculateTextWidth(textiles, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), width); // 4 emoji * 2 = 8\n}\n\ntest \"calculateTextWidth: emoji collection - prehistoric creatures\" {\n    const prehistoric = \"🦖🦕🐉🐲\";\n    const width = utf8.calculateTextWidth(prehistoric, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), width); // 4 emoji * 2 = 8\n}\n\ntest \"calculateTextWidth: emoji collection - hand gestures\" {\n    const hands = \"🤝🤲👐\";\n    const width = utf8.calculateTextWidth(hands, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - lanterns and lights\" {\n    const lanterns = \"🏮🎆🎇🕯️💡\";\n    const width = utf8.calculateTextWidth(lanterns, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 10), width); // 5 emoji * 2 = 10\n}\n\ntest \"calculateTextWidth: emoji collection - dancers\" {\n    const dancers = \"💃🕺🩰\";\n    const width = utf8.calculateTextWidth(dancers, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - musical instruments\" {\n    const instruments = \"🎻🎺🎷🎸🪕🪘\";\n    const width = utf8.calculateTextWidth(instruments, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 12), width); // 6 emoji * 2 = 12\n}\n\ntest \"calculateTextWidth: emoji collection - bells and shrine\" {\n    const bells = \"🔔⛩️\";\n    const width = utf8.calculateTextWidth(bells, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), width); // 2 emoji * 2 = 4\n}\n\ntest \"calculateTextWidth: emoji collection - shocked and amazed\" {\n    const shocked = \"😵‍💫🤯✨\";\n    const width = utf8.calculateTextWidth(shocked, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 graphemes * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - sweets and bubble tea\" {\n    const sweets = \"🧋🍬🍭🧁\";\n    const width = utf8.calculateTextWidth(sweets, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), width); // 4 emoji * 2 = 8\n}\n\ntest \"calculateTextWidth: emoji collection - machinery and robots\" {\n    const machinery = \"⚙️🤖🦾🦿\";\n    const width = utf8.calculateTextWidth(machinery, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), width); // 4 emoji * 2 = 8\n}\n\ntest \"calculateTextWidth: emoji collection - vehicles\" {\n    const vehicles = \"🚗🚕🚙🚌🚎\";\n    const width = utf8.calculateTextWidth(vehicles, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 10), width); // 5 emoji * 2 = 10\n}\n\ntest \"calculateTextWidth: emoji collection - space travel\" {\n    const space = \"🚀🛸🛰️\";\n    const width = utf8.calculateTextWidth(space, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - technology\" {\n    const tech = \"🐍💻⌨️\";\n    const width = utf8.calculateTextWidth(tech, 4, false, .unicode);\n    // 🐍(2) + 💻(2) + ⌨️(2, VS16 makes it emoji presentation) = 6\n    try testing.expectEqual(@as(u32, 6), width);\n}\n\ntest \"calculateTextWidth: emoji collection - education and brain\" {\n    const education = \"🧠📚🎓\";\n    const width = utf8.calculateTextWidth(education, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - professional ZWJ sequences\" {\n    const professionals = \"👨‍💼👩‍💼👨‍🔬👩‍🔬\";\n    const width = utf8.calculateTextWidth(professionals, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), width); // 4 graphemes * 2 = 8\n}\n\ntest \"calculateTextWidth: emoji collection - earth globes\" {\n    const globes = \"🌍🌎🌏\";\n    const width = utf8.calculateTextWidth(globes, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - family ZWJ sequence\" {\n    const family = \"👨‍👩‍👧‍👦\";\n    const width = utf8.calculateTextWidth(family, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), width); // 1 grapheme * 2 = 2\n}\n\ntest \"calculateTextWidth: emoji collection - elderly people\" {\n    const elderly = \"👴👵\";\n    const width = utf8.calculateTextWidth(elderly, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), width); // 2 emoji * 2 = 4\n}\n\ntest \"calculateTextWidth: emoji collection - sunrise and sunset\" {\n    const sunrise = \"🌅🌄🌠\";\n    const width = utf8.calculateTextWidth(sunrise, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - mountains\" {\n    const mountains = \"🏔️⛰️🗻\";\n    const width = utf8.calculateTextWidth(mountains, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - thoughts and dreams\" {\n    const dreams = \"💭💤🌌\";\n    const width = utf8.calculateTextWidth(dreams, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - campfire\" {\n    const campfire = \"🔥🏕️\";\n    const width = utf8.calculateTextWidth(campfire, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), width); // 2 emoji * 2 = 4\n}\n\ntest \"calculateTextWidth: emoji collection - cooking\" {\n    const cooking = \"🍛🍲🥘\";\n    const width = utf8.calculateTextWidth(cooking, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - love hearts\" {\n    const hearts = \"❤️💕💖\";\n    const width = utf8.calculateTextWidth(hearts, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - media\" {\n    const media = \"📸🎞️📹\";\n    const width = utf8.calculateTextWidth(media, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - global and handshake\" {\n    const global = \"🌐🤝🌈\";\n    const width = utf8.calculateTextWidth(global, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - special symbols\" {\n    const special = \"🦩🧿🪬🫀🫁🧠\";\n    const width = utf8.calculateTextWidth(special, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 12), width); // 6 emoji * 2 = 12\n}\n\ntest \"calculateTextWidth: emoji collection - strength\" {\n    const strength = \"💪✊🙌\";\n    const width = utf8.calculateTextWidth(strength, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width); // 3 emoji * 2 = 6\n}\n\ntest \"calculateTextWidth: emoji collection - entertainment\" {\n    const entertainment = \"🎬🎭🎪✨🌟⭐\";\n    const width = utf8.calculateTextWidth(entertainment, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 12), width); // 6 emoji * 2 = 12\n}\n\n// ============================================================================\n// DEVANAGARI SCRIPT WIDTH TESTS\n// ============================================================================\n\ntest \"calculateTextWidth: Devanagari - Sanskrit word\" {\n    // संस्कृति (culture/civilization)\n    const sanskrit = \"संस्कृति\";\n    const width = utf8.calculateTextWidth(sanskrit, 4, false, .unicode);\n    // 4 base consonants (SA, SA, KA, TA) with combining marks = width 4\n    try testing.expectEqual(@as(u32, 4), width);\n}\n\ntest \"calculateTextWidth: Devanagari - namaste\" {\n    const namaste = \"नमस्ते\";\n    const width = utf8.calculateTextWidth(namaste, 4, false, .unicode);\n    // 4 base consonants: NA, MA, SA, TA = width 4\n    try testing.expectEqual(@as(u32, 4), width);\n}\n\ntest \"calculateTextWidth: Devanagari - Om symbol\" {\n    const om = \"ॐ\";\n    const width = utf8.calculateTextWidth(om, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), width);\n}\n\ntest \"calculateTextWidth: Devanagari - mixed with ASCII\" {\n    const mixed = \"Hello नमस्ते World\";\n    const width = utf8.calculateTextWidth(mixed, 4, false, .unicode);\n    // \"Hello \"(6) + नमस्ते(4 base consonants) + \" World\"(6) = 16\n    try testing.expectEqual(@as(u32, 16), width);\n}\n\n// ============================================================================\n// CJK SCRIPT WIDTH TESTS\n// ============================================================================\n\ntest \"calculateTextWidth: Chinese characters - kanji\" {\n    const kanji = \"漢字\";\n    const width = utf8.calculateTextWidth(kanji, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), width); // 2 chars * 2 = 4\n}\n\ntest \"calculateTextWidth: Hiragana\" {\n    const hiragana = \"ひらがな\";\n    const width = utf8.calculateTextWidth(hiragana, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), width); // 4 chars * 2 = 8\n}\n\ntest \"calculateTextWidth: Katakana\" {\n    const katakana = \"カタカナ\";\n    const width = utf8.calculateTextWidth(katakana, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 8), width); // 4 chars * 2 = 8\n}\n\ntest \"calculateTextWidth: Korean Hangul\" {\n    const hangul = \"한글\";\n    const width = utf8.calculateTextWidth(hangul, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), width); // 2 chars * 2 = 4\n}\n\ntest \"calculateTextWidth: Korean words - love and peace\" {\n    const korean = \"사랑 평화\";\n    const width = utf8.calculateTextWidth(korean, 4, false, .unicode);\n    // 사(2) + 랑(2) + space(1) + 평(2) + 화(2) = 9\n    try testing.expectEqual(@as(u32, 9), width);\n}\n\n// ============================================================================\n// TIBETAN SCRIPT WIDTH TESTS\n// ============================================================================\n\ntest \"calculateTextWidth: Tibetan script\" {\n    const tibetan = \"རྒྱ་མཚོ\";\n    const width = utf8.calculateTextWidth(tibetan, 4, false, .unicode);\n    // Tibetan has complex combining characters\n    // Base chars are width 1, subjoined letters width 0\n    try testing.expect(width >= 3 and width <= 6);\n}\n\n// ============================================================================\n// OTHER INDIC SCRIPTS WIDTH TESTS\n// ============================================================================\n\ntest \"calculateTextWidth: Gujarati script\" {\n    const gujarati = \"ગુજરાતી\";\n    const width = utf8.calculateTextWidth(gujarati, 4, false, .unicode);\n    // ગ(1) + ુ(0) + જ(1) + ર(1) + ા(0) + ત(1) + ી(0) = 4\n    try testing.expectEqual(@as(u32, 4), width);\n}\n\ntest \"calculateTextWidth: Tamil script word\" {\n    const tamil = \"தமிழ்\";\n    const width = utf8.calculateTextWidth(tamil, 4, false, .unicode);\n    // த(1) + ம(1) + ி(0) + ழ(1) + ்(0) = 3\n    try testing.expectEqual(@as(u32, 3), width);\n}\n\ntest \"calculateTextWidth: Punjabi script word\" {\n    const punjabi = \"ਪੰਜਾਬੀ\";\n    const width = utf8.calculateTextWidth(punjabi, 4, false, .unicode);\n    // ਪ(1) + ੰ(0) + ਜ(1) + ਾ(0) + ਬ(1) + ੀ(0) = 3 base chars\n    try testing.expectEqual(@as(u32, 3), width);\n}\n\ntest \"calculateTextWidth: Telugu script word\" {\n    const telugu = \"తెలుగు\";\n    const width = utf8.calculateTextWidth(telugu, 4, false, .unicode);\n    // త(1) + ె(0) + ల(1) + ు(0) + గ(1) + ు(0) = 3\n    try testing.expectEqual(@as(u32, 3), width);\n}\n\ntest \"calculateTextWidth: Bengali script word\" {\n    const bengali = \"বাংলা\";\n    const width = utf8.calculateTextWidth(bengali, 4, false, .unicode);\n    // ব(1) + া(0) + ং(0) + ল(1) + া(0) = 2\n    try testing.expectEqual(@as(u32, 2), width);\n}\n\ntest \"calculateTextWidth: Kannada script\" {\n    const kannada = \"ಕನ್ನಡ\";\n    const width = utf8.calculateTextWidth(kannada, 4, false, .unicode);\n    // ಕ(1) + ನ(1) + ್(0) + ನ(1) + ಡ(1) = 4\n    try testing.expectEqual(@as(u32, 4), width);\n}\n\ntest \"calculateTextWidth: Malayalam script\" {\n    const malayalam = \"മലയാളം\";\n    const width = utf8.calculateTextWidth(malayalam, 4, false, .unicode);\n    // Each base letter is width 1, vowel signs width 0\n    try testing.expect(width >= 4 and width <= 5);\n}\n\ntest \"calculateTextWidth: Oriya script\" {\n    const oriya = \"ଓଡ଼ିଆ\";\n    const width = utf8.calculateTextWidth(oriya, 4, false, .unicode);\n    // ଓ(1) + ଡ(1) + ଼(0) + ି(0) + ଆ(1) = 3\n    try testing.expectEqual(@as(u32, 3), width);\n}\n\n// ============================================================================\n// THAI AND LAO SCRIPT WIDTH TESTS\n// ============================================================================\n\ntest \"calculateTextWidth: Thai script\" {\n    const thai = \"ภาษา\";\n    const width = utf8.calculateTextWidth(thai, 4, false, .unicode);\n    // Thai base chars width 1, combining vowels/tones width 0\n    try testing.expect(width >= 3 and width <= 4);\n}\n\ntest \"calculateTextWidth: Thai numerals\" {\n    const thai_num = \"๑๐๐\";\n    const width = utf8.calculateTextWidth(thai_num, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 3), width); // 3 digits * 1 = 3\n}\n\ntest \"calculateTextWidth: Lao script\" {\n    const lao = \"ໂຫຍ່າກເຈົ້າ\";\n    const width = utf8.calculateTextWidth(lao, 4, false, .unicode);\n    // Lao has complex vowels and tone marks (width 0)\n    try testing.expect(width >= 5 and width <= 10);\n}\n\n// ============================================================================\n// ARABIC AND OTHER SCRIPTS WIDTH TESTS\n// ============================================================================\n\ntest \"calculateTextWidth: Arabic character\" {\n    const arabic = \"ا\";\n    const width = utf8.calculateTextWidth(arabic, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 1), width);\n}\n\ntest \"calculateTextWidth: Sinhala script\" {\n    const sinhala = \"ආහාර\";\n    const width = utf8.calculateTextWidth(sinhala, 4, false, .unicode);\n    // Sinhala chars width 1, vowel signs width 0\n    try testing.expect(width >= 3 and width <= 4);\n}\n\ntest \"calculateTextWidth: Chinese text\" {\n    const chinese = \"中文\";\n    const width = utf8.calculateTextWidth(chinese, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 4), width); // 2 chars * 2 = 4\n}\n\ntest \"calculateTextWidth: Hangul Jamo\" {\n    const jamo = \"ㄱ\";\n    const width = utf8.calculateTextWidth(jamo, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), width); // Hangul Jamo is width 2\n}\n\n// ============================================================================\n// MIXED SCRIPT COMPREHENSIVE TESTS\n// ============================================================================\n\ntest \"calculateTextWidth: realistic multilingual sentence\" {\n    const multilingual = \"Hello 世界! नमस्ते 🙏\";\n    const width = utf8.calculateTextWidth(multilingual, 4, false, .unicode);\n    // \"Hello \"(6) + 世界(4) + \"! \"(2) + नमस्ते(4) + \" \"(1) + 🙏(2) = 19\n    try testing.expectEqual(@as(u32, 19), width);\n}\n\ntest \"calculateTextWidth: all ending words from text\" {\n    const endings = \"समाप्त끝จบముగింపుಅಂತ್ಯઅંત\";\n    const width = utf8.calculateTextWidth(endings, 4, false, .unicode);\n    // TODO: Expect absolutely\n    try testing.expect(width > 10);\n}\n\ntest \"calculateTextWidth: complex text with emojis and multiple scripts\" {\n    const complex = \"The 🌟 journey: संस्कृति meets 漢字 🎋\";\n    const width = utf8.calculateTextWidth(complex, 4, false, .unicode);\n    // TODO: Expect absolutely\n    try testing.expect(width >= 30 and width <= 50);\n}\n\ntest \"calculateTextWidth: validate against unicode-width-map.zon\" {\n    const zon_content = @embedFile(\"unicode-width-map.zon\");\n\n    // Use arena allocator to avoid memory leaks from ZON parser string allocations\n    var arena = std.heap.ArenaAllocator.init(testing.allocator);\n    defer arena.deinit();\n    const allocator = arena.allocator();\n\n    const zon_with_null = try allocator.dupeZ(u8, zon_content);\n\n    const WidthEntry = struct {\n        codepoint: []const u8,\n        width: i32,\n    };\n\n    const width_entries = std.zon.parse.fromSlice(\n        []const WidthEntry,\n        allocator,\n        zon_with_null,\n        null,\n        .{},\n    ) catch |err| {\n        return err;\n    };\n\n    var successes: usize = 0;\n    var failures: usize = 0;\n\n    for (width_entries) |entry| {\n        const codepoint_str = entry.codepoint;\n        const expected_width = entry.width;\n\n        // Parse \"U+XXXX\" from codepoint string\n        if (codepoint_str.len < 3 or !std.mem.startsWith(u8, codepoint_str, \"U+\")) {\n            continue;\n        }\n        const hex_str = codepoint_str[2..];\n        const code_point = std.fmt.parseInt(u21, hex_str, 16) catch continue;\n\n        var buf: [4]u8 = undefined;\n        const len = std.unicode.utf8Encode(code_point, &buf) catch continue;\n        const str = buf[0..len];\n\n        const actual_width = utf8.calculateTextWidth(str, 4, false, .unicode);\n\n        if (actual_width == expected_width) {\n            successes += 1;\n        } else {\n            failures += 1;\n        }\n    }\n\n    try testing.expectEqual(@as(usize, 0), failures);\n}\n\ntest \"findGraphemeInfo: comprehensive multilingual text\" {\n    const text =\n        \\\\# The Celestial Journey of संस्कृति 🌟🔮✨\n        \\\\In the beginning, there was नमस्ते 🙏 and the ancient wisdom of the ॐ symbol echoing through dimensions. The travelers 🧑‍🚀👨‍🚀👩‍🚀 embarked on their quest through the cosmos, guided by the mysterious རྒྱ་མཚོ and the luminous 🌈🦄🧚‍♀️ beings of light. They encountered the great देवनागरी scribes who wrote in flowing अक्षर characters, documenting everything in their sacred texts 📜📖✍️.\n        \\\\## Chapter प्रथम: The Eastern Gardens 🏯🎋🌸\n        \\\\The journey led them to the mystical lands where 漢字 (kanji) danced with ひらがな and カタカナ across ancient scrolls 📯🎴🎎. In the gardens of Seoul, they found 한글 inscriptions speaking of 사랑 (love) and 평화 (peace) 💝🕊️☮️. The monks meditated under the bodhi tree 🧘‍♂️🌳, contemplating the nature of धर्म while drinking matcha 🍵 and eating 餃子 dumplings 🥟.\n        \\\\Strange creatures emerged from the mist: 🦥🦦🦧🦨🦩🦚🦜🦝🦞🦟. They spoke in riddles about the प्राचीन (ancient) ways and the नवीन (new) paths forward. \"भविष्य में क्या है?\" they asked, while the ໂຫຍ່າກເຈົ້າ whispered secrets in Lao script 🤫🗣️💬.\n        \\\\## The संगम (Confluence) of Scripts 🌊📝🎭\n        \\\\At the great confluence, they witnessed the merger of བོད་ཡིག (Tibetan), ગુજરાતી (Gujarati), and தமிழ் (Tamil) scripts flowing together like rivers 🏞️🌊💧. The scholars debated about ਪੰਜਾਬੀ philosophy while juggling 🤹‍♂️🎪🎨 colorful orbs that represented different తెలుగు concepts.\n        \\\\The marketplace buzzed with activity 🏪🛒💰: merchants sold বাংলা spices 🌶️🧄🧅, ಕನ್ನಡ silks 🧵👘, and മലയാളം handicrafts 🎨🖼️. Children played with toys shaped like 🦖🦕🐉🐲 while their parents bargained using ancient ଓଡ଼ିଆ numerals and gestures 🤝🤲👐.\n        \\\\## The Festival of ๑๐๐ Lanterns 🏮🎆🎇\n        \\\\During the grand festival, they lit exactly ๑๐๐ (100 in Thai numerals) lanterns 🏮🕯️💡 that floated into the night sky like ascending ความหวัง (hopes). The celebration featured dancers 💃🕺🩰 performing classical moves from भरतनाट्यम tradition, their मुद्रा hand gestures telling stories of प्रेम and वीरता.\n        \\\\Musicians played unusual instruments: the 🎻🎺🎷🎸🪕🪘 ensemble created harmonies that resonated with the वेद chants and མཆོད་རྟེན bells 🔔⛩️. The audience sat mesmerized 😵‍💫🤯✨, some sipping on bubble tea 🧋 while others enjoyed मिठाई sweets 🍬🍭🧁.\n        \\\\## The འཕྲུལ་དེབ (Machine) Age Arrives ⚙️🤖🦾\n        \\\\As modernity crept in, the ancient འཁོར་ལོ (wheel) gave way to 🚗🚕🚙🚌🚎 vehicles and eventually to 🚀🛸🛰️ spacecraft. The યુવાન (youth) learned to code in Python 🐍💻⌨️, but still honored their గురువు (teachers) who taught them the old ways of ज्ञान acquisition 🧠📚🎓.\n        \\\\The সমাজ (society) transformed: robots 🤖🦾🦿 worked alongside humans 👨‍💼👩‍💼👨‍🔬👩‍🔬, and AI learned to read སྐད (languages) from across the planet 🌍🌎🌏. Yet somehow, the essence of मानवता remained intact, preserved in the கவிதை (poetry) and the ກາບແກ້ວ stories passed down through generations 👴👵👨‍👩‍👧‍👦.\n        \\\\## The Final ಅಧ್ಯಾಯ (Chapter) 🌅🌄🌠\n        \\\\As the sun set over the പർവ്വതങ്ങൾ (mountains) 🏔️⛰️🗻, our travelers realized that every script, every symbol—from ا to ㄱ to অ to अ—represented not just sounds, but entire civilizations' worth of विचार (thoughts) and ಕನಸು (dreams) 💭💤🌌.\n        \\\\They gathered around the final campfire 🔥🏕️, sharing stories in ภาษา (languages) both ancient and new. Someone brought out a guitar 🎸 and started singing in ગીત form, while others prepared ආහාර (food) 🍛🍲🥘 seasoned with love ❤️💕💖 and memories 📸🎞️📹.\n        \\\\And so they learned that whether written in দেবনাগরী, 中文, 한글, or ไทย, the human experience transcends boundaries 🌐🤝🌈. The weird emojis 🦩🧿🪬🫀🫁🧠 and complex scripts were all part of the same beautiful བསྟན་པ (teaching): that diversity is our greatest strength 💪✊🙌.\n        \\\\The end. समाप्त. 끝. จบ. முடிவு. ముగింపు. সমাপ্তি. ഒടുക്കം. ಅಂತ್ಯ. અંત. 🎬🎭🎪✨🌟⭐\n        \\\\\n    ;\n\n    const expected_width = utf8.calculateTextWidth(text, 4, false, .unicode);\n\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n    try testing.expect(result.items.len > 0);\n\n    var prev_end_byte: usize = 0;\n\n    for (result.items) |g| {\n        try testing.expect(g.byte_offset >= prev_end_byte);\n\n        const text_before = text[0..g.byte_offset];\n        const expected_col = utf8.calculateTextWidth(text_before, 4, false, .unicode);\n\n        try testing.expectEqual(expected_col, g.col_offset);\n\n        prev_end_byte = g.byte_offset + g.byte_len;\n    }\n\n    const final_computed_width = utf8.calculateTextWidth(text, 4, false, .unicode);\n    try testing.expectEqual(expected_width, final_computed_width);\n}\n\n// ============================================================================\n// THAI DIACRITICS AND COMBINING MARKS TESTS\n// ============================================================================\n\ntest \"Thai: base consonants have width 1\" {\n    const consonants = \"กขคงจฉชซญฎฏฐดตถทธนบปผฝพฟภมยรลวศษสหอฮ\";\n    const width = utf8.calculateTextWidth(consonants, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 36), width);\n}\n\ntest \"Thai: spacing vowels have width 1\" {\n    const spacing_vowels = \"าะแโใไ\";\n    const width = utf8.calculateTextWidth(spacing_vowels, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), width);\n}\n\ntest \"Thai: combining vowels above have width 0\" {\n    const base = \"ก\";\n    const with_sara_i = \"กิ\";\n    const with_sara_ii = \"กี\";\n    const with_sara_ue = \"กึ\";\n    const with_sara_uee = \"กื\";\n    const with_mai_han_akat = \"กั\";\n\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(base, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_sara_i, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_sara_ii, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_sara_ue, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_sara_uee, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_mai_han_akat, 4, false, .unicode));\n}\n\ntest \"Thai: combining vowels below have width 0\" {\n    const with_sara_u = \"กุ\";\n    const with_sara_uu = \"กู\";\n\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_sara_u, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_sara_uu, 4, false, .unicode));\n}\n\ntest \"Thai: tone marks have width 0\" {\n    const with_mai_ek = \"ก่\";\n    const with_mai_tho = \"ก้\";\n    const with_mai_tri = \"ก๊\";\n    const with_mai_chattawa = \"ก๋\";\n\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_mai_ek, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_mai_tho, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_mai_tri, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_mai_chattawa, 4, false, .unicode));\n}\n\ntest \"Thai: other diacritics have width 0\" {\n    const with_maitaikhu = \"ก็\";\n    const with_thanthakhat = \"ก์\";\n    const with_nikhahit = \"กํ\";\n\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_maitaikhu, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_thanthakhat, 4, false, .unicode));\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(with_nikhahit, 4, false, .unicode));\n}\n\ntest \"Thai: combined vowel and tone mark\" {\n    const text = \"กี่\";\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(text, 4, false, .unicode));\n\n    const text2 = \"คือ\";\n    try testing.expectEqual(@as(u32, 2), utf8.calculateTextWidth(text2, 4, false, .unicode));\n}\n\ntest \"Thai: word 'ภาษาไทย' (Thai language)\" {\n    const text = \"ภาษาไทย\";\n    try testing.expectEqual(@as(u32, 7), utf8.calculateTextWidth(text, 4, false, .unicode));\n}\n\ntest \"Thai: word 'อย่าง' with tone mark\" {\n    const text = \"อย่าง\";\n    try testing.expectEqual(@as(u32, 4), utf8.calculateTextWidth(text, 4, false, .unicode));\n}\n\ntest \"Thai: word 'อธิบาย' with vowel above\" {\n    const text = \"อธิบาย\";\n    try testing.expectEqual(@as(u32, 5), utf8.calculateTextWidth(text, 4, false, .unicode));\n}\n\ntest \"Thai: full sentence with spaces\" {\n    const text = \"ภาษาไทย คืออะไร อธิบายมาอย่างละเอียด\";\n    try testing.expectEqual(@as(u32, 32), utf8.calculateTextWidth(text, 4, false, .unicode));\n}\n\ntest \"Thai: wrap by width respects combining marks\" {\n    const text = \"คือ\";\n\n    const result1 = utf8.findWrapPosByWidth(text, 1, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 6), result1.byte_offset);\n    try testing.expectEqual(@as(u32, 1), result1.columns_used);\n\n    const result2 = utf8.findWrapPosByWidth(text, 2, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 9), result2.byte_offset);\n    try testing.expectEqual(@as(u32, 2), result2.columns_used);\n}\n\ntest \"Thai: wrap by width with tone marks\" {\n    const text = \"ก่อน\";\n\n    const result2 = utf8.findWrapPosByWidth(text, 2, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 2), result2.columns_used);\n\n    const result3 = utf8.findWrapPosByWidth(text, 3, 4, false, .unicode);\n    try testing.expectEqual(@as(u32, 3), result3.columns_used);\n}\n\ntest \"Thai: grapheme info for combining marks\" {\n    const text = \"กี่\";\n\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n    try testing.expectEqual(@as(u8, 1), result.items[0].width);\n}\n\ntest \"Thai: grapheme info for word with combining marks\" {\n    const text = \"คือ\";\n\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 2), result.items.len);\n    try testing.expectEqual(@as(u8, 1), result.items[0].width);\n    try testing.expectEqual(@as(u8, 1), result.items[1].width);\n}\n\ntest \"Thai: mixed Thai and ASCII\" {\n    const text = \"Hello ภาษาไทย World\";\n    try testing.expectEqual(@as(u32, 19), utf8.calculateTextWidth(text, 4, false, .unicode));\n}\n\ntest \"Thai: mixed Thai and emoji\" {\n    const text = \"ภาษา 🇹🇭 ไทย\";\n    try testing.expectEqual(@as(u32, 11), utf8.calculateTextWidth(text, 4, false, .unicode));\n}\n\ntest \"Thai: คำว่า width should be 3\" {\n    const text = \"คำว่า\";\n    try testing.expectEqual(@as(u32, 3), utf8.calculateTextWidth(text, 4, false, .unicode));\n}\n\ntest \"Thai: ว่ width should be 1\" {\n    const text = \"ว่\";\n    try testing.expectEqual(@as(u32, 1), utf8.calculateTextWidth(text, 4, false, .unicode));\n}\n\ntest \"Thai: ว่ wcwidth vs unicode mode comparison\" {\n    const text = \"ว่\";\n    const wcwidth_result = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    const unicode_result = utf8.calculateTextWidth(text, 4, false, .unicode);\n\n    try testing.expectEqual(@as(u32, 1), wcwidth_result);\n    try testing.expectEqual(@as(u32, 1), unicode_result);\n}\n\ntest \"Thai: ว่ is a single grapheme cluster\" {\n    const text = \"ว่\";\n\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n    try testing.expectEqual(@as(u8, 1), result.items[0].width);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/utf8_wcwidth_cursor_test.zig",
    "content": "const std = @import(\"std\");\nconst testing = std.testing;\nconst utf8 = @import(\"../utf8.zig\");\n\ntest \"wcwidth: cursor movement through emoji with skin tone\" {\n    const text = \"👋🏿\"; // Wave + dark skin tone = 4 columns\n\n    const width_wave = utf8.getWidthAt(text, 0, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_wave);\n\n    const width_skin = utf8.getWidthAt(text, 4, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_skin);\n\n    const total = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 4), total);\n}\n\ntest \"wcwidth: cursor movement through ZWJ sequence\" {\n    const text = \"👩‍🚀\"; // Woman + ZWJ + Rocket = 4 columns (2+0+2)\n\n    const total = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 4), total);\n\n    const width_woman = utf8.getWidthAt(text, 0, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_woman);\n\n    const width_zwj = utf8.getWidthAt(text, 4, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 0), width_zwj);\n\n    const width_rocket = utf8.getWidthAt(text, 7, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_rocket);\n}\n\ntest \"wcwidth: cursor movement through family emoji\" {\n    const text = \"👨‍👩‍👧\"; // Man + ZWJ + Woman + ZWJ + Girl = 6 columns (2+0+2+0+2)\n\n    const total = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 6), total);\n\n    const width_man = utf8.getWidthAt(text, 0, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_man);\n\n    const width_zwj1 = utf8.getWidthAt(text, 4, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 0), width_zwj1);\n\n    const width_woman = utf8.getWidthAt(text, 7, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_woman);\n\n    const width_zwj2 = utf8.getWidthAt(text, 11, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 0), width_zwj2);\n\n    const width_girl = utf8.getWidthAt(text, 14, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_girl);\n}\n\ntest \"wcwidth: getPrevGraphemeStart through emoji with skin tone\" {\n    const text = \"A👋🏿B\"; // A(1) + 👋(2) + 🏿(2) + B(1) = 6 columns\n\n    const r_end = utf8.getPrevGraphemeStart(text, text.len, 4, .wcwidth);\n    try testing.expect(r_end != null);\n    try testing.expectEqual(@as(u32, 1), r_end.?.width);\n\n    const r_b = utf8.getPrevGraphemeStart(text, r_end.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_b != null);\n    try testing.expectEqual(@as(u32, 2), r_b.?.width);\n\n    const r_skin = utf8.getPrevGraphemeStart(text, r_b.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_skin != null);\n    try testing.expectEqual(@as(u32, 2), r_skin.?.width);\n\n    const r_wave = utf8.getPrevGraphemeStart(text, r_skin.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_wave != null);\n    try testing.expectEqual(@as(u32, 1), r_wave.?.width);\n}\n\ntest \"wcwidth: getPrevGraphemeStart through ZWJ sequence\" {\n    const text = \"X👩‍🚀Y\"; // X(1) + 👩(2) + ZWJ(0) + 🚀(2) + Y(1) = 6 columns\n\n    const r_end = utf8.getPrevGraphemeStart(text, text.len, 4, .wcwidth);\n    try testing.expect(r_end != null);\n    try testing.expectEqual(@as(u32, 1), r_end.?.width);\n\n    const r_y = utf8.getPrevGraphemeStart(text, r_end.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_y != null);\n    try testing.expectEqual(@as(u32, 2), r_y.?.width);\n\n    const r_rocket = utf8.getPrevGraphemeStart(text, r_y.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_rocket != null);\n    try testing.expectEqual(@as(u32, 2), r_rocket.?.width);\n\n    const r_woman = utf8.getPrevGraphemeStart(text, r_rocket.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_woman != null);\n    try testing.expectEqual(@as(u32, 1), r_woman.?.width);\n}\n\ntest \"wcwidth: findPosByWidth through emoji sequence\" {\n    const text = \"AB👋🏿CD\"; // A(1) B(1) 👋(2) 🏿(2) C(1) D(1) = 8 columns\n\n    const pos_start = utf8.findPosByWidth(text, 3, 4, false, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), pos_start.byte_offset);\n\n    const pos_end = utf8.findPosByWidth(text, 3, 4, false, true, .wcwidth);\n    try testing.expectEqual(@as(u32, 6), pos_end.byte_offset);\n}\n\ntest \"wcwidth: findWrapPosByWidth through emoji\" {\n    const text = \"Hi👋🏿Bye\"; // H(1) i(1) 👋(2) 🏿(2) B(1) y(1) e(1) = 10 columns\n\n    const wrap_4 = utf8.findWrapPosByWidth(text, 4, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 6), wrap_4.byte_offset);\n    try testing.expectEqual(@as(u32, 4), wrap_4.columns_used);\n\n    const wrap_5 = utf8.findWrapPosByWidth(text, 5, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 6), wrap_5.byte_offset);\n    try testing.expectEqual(@as(u32, 4), wrap_5.columns_used);\n\n    const wrap_6 = utf8.findWrapPosByWidth(text, 6, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 10), wrap_6.byte_offset);\n    try testing.expectEqual(@as(u32, 6), wrap_6.columns_used);\n}\n\ntest \"wcwidth: combining marks have zero width\" {\n    const text = \"e\\u{0301}\"; // e + combining acute\n\n    const width_e = utf8.getWidthAt(text, 0, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), width_e);\n\n    const width_combining = utf8.getWidthAt(text, 1, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 0), width_combining);\n\n    const total = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), total);\n}\n\ntest \"wcwidth: CJK characters have width 2\" {\n    const text = \"你好世界\"; // 4 CJK characters\n\n    const total = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 8), total);\n\n    const width_char1 = utf8.getWidthAt(text, 0, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_char1);\n\n    const width_char2 = utf8.getWidthAt(text, 3, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width_char2);\n}\n\ntest \"wcwidth: variation selectors have zero width\" {\n    const text = \"☺\\u{FE0F}\"; // Smiling face + VS16\n\n    const width_face = utf8.getWidthAt(text, 0, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), width_face);\n\n    const width_vs = utf8.getWidthAt(text, 3, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 0), width_vs);\n\n    const total = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), total);\n}\n\ntest \"wcwidth: flag emoji counts both regional indicators\" {\n    const text = \"🇺🇸\"; // US flag (two regional indicators)\n\n    const width_ri1 = utf8.getWidthAt(text, 0, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), width_ri1);\n\n    const width_ri2 = utf8.getWidthAt(text, 4, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), width_ri2);\n\n    const total = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), total);\n}\n\ntest \"wcwidth: mixed content with cursor movement\" {\n    const text = \"A👋🏿B世C\"; // A(1) 👋(2) 🏿(2) B(1) 世(2) C(1) = 9 columns\n\n    const r_end = utf8.getPrevGraphemeStart(text, text.len, 4, .wcwidth);\n    try testing.expect(r_end != null);\n    try testing.expectEqual(@as(u32, 1), r_end.?.width);\n\n    const r_cjk = utf8.getPrevGraphemeStart(text, r_end.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_cjk != null);\n    try testing.expectEqual(@as(u32, 2), r_cjk.?.width);\n\n    const r_b = utf8.getPrevGraphemeStart(text, r_cjk.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_b != null);\n    try testing.expectEqual(@as(u32, 1), r_b.?.width);\n\n    const r_skin = utf8.getPrevGraphemeStart(text, r_b.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_skin != null);\n    try testing.expectEqual(@as(u32, 2), r_skin.?.width);\n\n    const r_wave = utf8.getPrevGraphemeStart(text, r_skin.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_wave != null);\n    try testing.expectEqual(@as(u32, 2), r_wave.?.width);\n\n    const r_a = utf8.getPrevGraphemeStart(text, r_wave.?.start_offset, 4, .wcwidth);\n    try testing.expect(r_a != null);\n    try testing.expectEqual(@as(u32, 1), r_a.?.width);\n}\n\ntest \"wcwidth: findGraphemeInfo with emoji\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"👋🏿\"; // Wave + skin tone modifier\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u32, 0), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 8), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[0].width);\n}\n\ntest \"wcwidth: findGraphemeInfo with ZWJ sequence\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"👩‍🚀\"; // Woman + ZWJ + Rocket\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u32, 0), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 11), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[0].width);\n}\n\ntest \"wcwidth: findGraphemeInfo with combining marks\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"e\\u{0301}\"; // e + combining acute\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n    try testing.expectEqual(@as(u32, 0), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 1), result.items[0].width);\n}\n\ntest \"wcwidth: tab width handling\" {\n    const text = \"A\\tB\"; // A + tab + B\n\n    const total = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 6), total);\n\n    const tab_width = utf8.getWidthAt(text, 1, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 4), tab_width);\n}\n\ntest \"wcwidth: boundary at wide character\" {\n    const text = \"世X\"; // 世(2) X(1) = 3 columns\n\n    const pos_start = utf8.findPosByWidth(text, 2, 4, false, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 3), pos_start.byte_offset);\n    try testing.expectEqual(@as(u32, 2), pos_start.columns_used);\n\n    const pos_end = utf8.findPosByWidth(text, 2, 4, false, true, .wcwidth);\n    try testing.expectEqual(@as(u32, 3), pos_end.byte_offset);\n    try testing.expectEqual(@as(u32, 2), pos_end.columns_used);\n\n    const pos_3 = utf8.findPosByWidth(text, 3, 4, false, true, .wcwidth);\n    try testing.expectEqual(@as(u32, 4), pos_3.byte_offset);\n    try testing.expectEqual(@as(u32, 3), pos_3.columns_used);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/utf8_wcwidth_test.zig",
    "content": "const std = @import(\"std\");\nconst testing = std.testing;\nconst utf8 = @import(\"../utf8.zig\");\n\ntest \"findGraphemeInfo wcwidth: empty string\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(\"\", 4, false, .wcwidth, testing.allocator, &result);\n    try testing.expectEqual(@as(usize, 0), result.items.len);\n}\n\ntest \"findGraphemeInfo wcwidth: ASCII-only returns empty\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(\"hello world\", 4, true, .wcwidth, testing.allocator, &result);\n    try testing.expectEqual(@as(usize, 0), result.items.len);\n}\n\ntest \"findGraphemeInfo wcwidth: ASCII with tab\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    try utf8.findGraphemeInfo(\"hello\\tworld\", 4, false, .wcwidth, testing.allocator, &result);\n\n    // Should have one entry for the tab\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n    try testing.expectEqual(@as(u32, 5), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 1), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[0].width);\n    try testing.expectEqual(@as(u32, 5), result.items[0].col_offset);\n}\n\ntest \"findGraphemeInfo wcwidth: CJK characters\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"hello世界\";\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result);\n\n    // Should have two entries for the CJK characters (each codepoint separately)\n    try testing.expectEqual(@as(usize, 2), result.items.len);\n\n    // First CJK char '世' at byte 5\n    try testing.expectEqual(@as(u32, 5), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 2), result.items[0].width);\n    try testing.expectEqual(@as(u32, 5), result.items[0].col_offset);\n\n    // Second CJK char '界' at byte 8\n    try testing.expectEqual(@as(u32, 8), result.items[1].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[1].byte_len);\n    try testing.expectEqual(@as(u8, 2), result.items[1].width);\n    try testing.expectEqual(@as(u32, 7), result.items[1].col_offset);\n}\n\ntest \"findGraphemeInfo wcwidth: emoji with skin tone - single grapheme cluster\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"👋🏿\"; // Wave + skin tone modifier\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u32, 0), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 8), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[0].width);\n}\n\ntest \"findGraphemeInfo wcwidth: emoji with ZWJ - single grapheme cluster\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"👩‍🚀\"; // Woman + ZWJ + Rocket (11 bytes total)\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n\n    try testing.expectEqual(@as(u8, 11), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 4), result.items[0].width);\n}\n\ntest \"findGraphemeInfo wcwidth: combining mark - part of base grapheme\" {\n    var result: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result.deinit(testing.allocator);\n\n    const text = \"e\\u{0301}test\"; // e + combining acute accent + test\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result);\n\n    try testing.expectEqual(@as(usize, 1), result.items.len);\n    try testing.expectEqual(@as(u32, 0), result.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 3), result.items[0].byte_len);\n    try testing.expectEqual(@as(u8, 1), result.items[0].width);\n}\n\ntest \"findGraphemeInfo wcwidth vs unicode: emoji with skin tone\" {\n    var result_wcwidth: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result_wcwidth.deinit(testing.allocator);\n    var result_unicode: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result_unicode.deinit(testing.allocator);\n\n    const text = \"Hi👋🏿Bye\";\n\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result_wcwidth);\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result_unicode);\n\n    try testing.expectEqual(@as(usize, 1), result_wcwidth.items.len);\n    try testing.expectEqual(@as(usize, 1), result_unicode.items.len);\n\n    try testing.expectEqual(@as(u32, 2), result_wcwidth.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 8), result_wcwidth.items[0].byte_len);\n\n    try testing.expectEqual(@as(u32, 2), result_unicode.items[0].byte_offset);\n    try testing.expectEqual(@as(u8, 8), result_unicode.items[0].byte_len);\n\n    try testing.expectEqual(@as(u8, 4), result_wcwidth.items[0].width);\n    try testing.expectEqual(@as(u8, 2), result_unicode.items[0].width);\n}\n\ntest \"findGraphemeInfo wcwidth vs unicode: flag emoji\" {\n    var result_wcwidth: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result_wcwidth.deinit(testing.allocator);\n    var result_unicode: std.ArrayListUnmanaged(utf8.GraphemeInfo) = .{};\n    defer result_unicode.deinit(testing.allocator);\n\n    const text = \"🇺🇸\"; // US flag (two regional indicators)\n\n    try utf8.findGraphemeInfo(text, 4, false, .wcwidth, testing.allocator, &result_wcwidth);\n    try utf8.findGraphemeInfo(text, 4, false, .unicode, testing.allocator, &result_unicode);\n\n    try testing.expectEqual(@as(usize, 1), result_wcwidth.items.len);\n    try testing.expectEqual(@as(usize, 1), result_unicode.items.len);\n\n    try testing.expectEqual(@as(u8, 2), result_wcwidth.items[0].width);\n    try testing.expectEqual(@as(u8, 2), result_unicode.items[0].width);\n}\n\n// ============================================================================\n// WIDTH CALCULATION TESTS - WCWIDTH MODE\n// ============================================================================\n\ntest \"getWidthAt wcwidth: combining mark has zero width\" {\n    const text = \"e\\u{0301}\"; // e + combining acute accent\n\n    // In wcwidth mode, combining mark is a separate codepoint\n    const width_e = utf8.getWidthAt(text, 0, 8, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), width_e); // Just 'e'\n\n    const width_combining = utf8.getWidthAt(text, 1, 8, .wcwidth);\n    try testing.expectEqual(@as(u32, 0), width_combining); // Combining mark has width 0\n}\n\ntest \"calculateTextWidth wcwidth: emoji with skin tone counts both codepoints\" {\n    const text = \"👋🏿\"; // Wave + dark skin tone\n\n    const width_wcwidth = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n\n    // wcwidth: counts both codepoints (2 + 2 = 4)\n    try testing.expectEqual(@as(u32, 4), width_wcwidth);\n\n    // unicode: single grapheme cluster (width 2)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n}\n\ntest \"calculateTextWidth wcwidth: flag emoji counts both RIs\" {\n    const text = \"🇺🇸\"; // US flag\n\n    const width_wcwidth = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    const width_unicode = utf8.calculateTextWidth(text, 4, false, .unicode);\n\n    // wcwidth: counts both regional indicators (1 + 1 = 2)\n    try testing.expectEqual(@as(u32, 2), width_wcwidth);\n\n    // unicode: single flag grapheme (width 2)\n    try testing.expectEqual(@as(u32, 2), width_unicode);\n}\n\n// ============================================================================\n// FIND WRAP POS BY WIDTH TESTS - WCWIDTH MODE\n// ============================================================================\n\ntest \"findWrapPosByWidth wcwidth: emoji with skin tone stops earlier\" {\n    const text = \"Hi👋🏿Bye\"; // H(1) i(1) wave(2) skin(2) B(1) y(1) e(1) = 10 cols wcwidth\n\n    const result_wcwidth = utf8.findWrapPosByWidth(text, 4, 4, false, .wcwidth);\n    const result_unicode = utf8.findWrapPosByWidth(text, 4, 4, false, .unicode);\n\n    // wcwidth: stops after \"Hi👋\" = 4 columns (1+1+2)\n    try testing.expectEqual(@as(u32, 6), result_wcwidth.byte_offset);\n    try testing.expectEqual(@as(u32, 4), result_wcwidth.columns_used);\n\n    // unicode: stops after \"Hi👋🏿\" = 4 columns (1+1+2 for whole grapheme)\n    try testing.expectEqual(@as(u32, 10), result_unicode.byte_offset);\n    try testing.expectEqual(@as(u32, 4), result_unicode.columns_used);\n}\n\ntest \"findPosByWidth wcwidth: emoji boundary behavior\" {\n    const text = \"AB👋🏿CD\"; // A(1) B(1) wave(2) skin(2) C(1) D(1)\n\n    // With include_start_before=false (selection start)\n    const start3 = utf8.findPosByWidth(text, 3, 4, false, false, .wcwidth);\n    // wcwidth: stops after \"AB\" at 2 columns (wave would exceed)\n    try testing.expectEqual(@as(u32, 2), start3.byte_offset);\n\n    // With include_start_before=true (selection end)\n    const end3 = utf8.findPosByWidth(text, 3, 4, false, true, .wcwidth);\n    // wcwidth: includes wave since it starts at column 2 which is < 3\n    try testing.expectEqual(@as(u32, 6), end3.byte_offset);\n    try testing.expectEqual(@as(u32, 4), end3.columns_used);\n}\n\ntest \"getPrevGraphemeStart wcwidth: each codepoint separate\" {\n    const text = \"Hi👋🏿\";\n\n    // From end of text (after skin tone)\n    const r_end = utf8.getPrevGraphemeStart(text, text.len, 4, .wcwidth);\n    try testing.expect(r_end != null);\n    try testing.expectEqual(@as(usize, 6), r_end.?.start_offset); // Skin tone starts at byte 6\n    try testing.expectEqual(@as(u32, 2), r_end.?.width);\n\n    // From start of skin tone (byte 6)\n    const r_wave = utf8.getPrevGraphemeStart(text, 6, 4, .wcwidth);\n    try testing.expect(r_wave != null);\n    try testing.expectEqual(@as(usize, 2), r_wave.?.start_offset); // Wave starts at byte 2\n    try testing.expectEqual(@as(u32, 2), r_wave.?.width);\n}\n\n// ============================================================================\n// ADDITIONAL COMPREHENSIVE WCWIDTH TESTS\n// ============================================================================\n\ntest \"wcwidth: zero-width characters are handled correctly\" {\n    // ZWJ (Zero Width Joiner) should have width 0\n    const text_zwj = \"\\u{200D}\";\n    const width_zwj = utf8.calculateTextWidth(text_zwj, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 0), width_zwj);\n\n    // Combining marks should have width 0\n    const text_combining = \"e\\u{0301}\"; // e + combining acute\n    const width = utf8.calculateTextWidth(text_combining, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), width); // Only 'e' contributes\n}\n\ntest \"wcwidth: variation selectors\" {\n    // VS15 (text presentation) and VS16 (emoji presentation)\n    const text_vs16 = \"☺\\u{FE0F}\"; // Smiling face + VS16\n    const width_vs16 = utf8.calculateTextWidth(text_vs16, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), width_vs16); // Smiling face (1) + VS16 (0) = 1\n}\n\ntest \"wcwidth: regional indicators counted separately\" {\n    // Each regional indicator should contribute width 1\n    const text = \"🇺🇸\"; // US flag = two regional indicators\n    const width = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 2), width); // Each RI has width 1\n}\n\ntest \"wcwidth: emoji ZWJ sequences split\" {\n    // Woman astronaut = woman + ZWJ + rocket\n    const text = \"👩‍🚀\";\n    const width = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    // Woman (2) + ZWJ (0) + Rocket (2) = 4\n    try testing.expectEqual(@as(u32, 4), width);\n}\n\ntest \"wcwidth: family emoji split into components\" {\n    // Family emoji with ZWJ\n    const text = \"👨‍👩‍👧\"; // Man + ZWJ + Woman + ZWJ + Girl\n    const width = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    // Man (2) + ZWJ (0) + Woman (2) + ZWJ (0) + Girl (2) = 6\n    try testing.expectEqual(@as(u32, 6), width);\n}\n\ntest \"wcwidth: skin tone modifiers counted separately\" {\n    // Emoji with skin tone modifier\n    const text = \"👋🏻\"; // Wave + light skin tone\n    const width = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    // Wave (2) + Skin tone modifier (2) = 4\n    try testing.expectEqual(@as(u32, 4), width);\n}\n\ntest \"wcwidth: CJK characters have width 2\" {\n    const text = \"你好世界\"; // 4 CJK characters\n    const width = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 8), width); // 4 * 2 = 8\n}\n\ntest \"wcwidth: mixed ASCII and emoji\" {\n    const text = \"Hello👋World\";\n    // H(1) e(1) l(1) l(1) o(1) 👋(2) W(1) o(1) r(1) l(1) d(1) = 12\n    const width = utf8.calculateTextWidth(text, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 12), width);\n}\n\ntest \"wcwidth: findWrapPosByWidth with ZWJ sequences\" {\n    const text = \"AB👩‍🚀CD\"; // A(1) B(1) woman(2) ZWJ(0) rocket(2) C(1) D(1) = 8\n\n    // Should wrap after woman emoji (before ZWJ)\n    const result = utf8.findWrapPosByWidth(text, 4, 4, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 6), result.byte_offset); // After woman emoji\n    try testing.expectEqual(@as(u32, 4), result.columns_used);\n}\n\ntest \"wcwidth: findPosByWidth with skin tone modifier\" {\n    const text = \"AB👋🏻CD\"; // A(1) B(1) wave(2) skin(2) C(1) D(1) = 8\n\n    // With include_start_before=false, include codepoints that end at or before max_columns\n    // Wave ends at column 4, which is at max_columns=4, so it's included\n    const start4 = utf8.findPosByWidth(text, 4, 4, false, false, .wcwidth);\n    try testing.expectEqual(@as(u32, 6), start4.byte_offset); // After wave\n    try testing.expectEqual(@as(u32, 4), start4.columns_used);\n\n    // With include_start_before=true, include codepoints that start before max_columns\n    // Wave starts at column 2 which is < 4, so it's included\n    const end4 = utf8.findPosByWidth(text, 4, 4, false, true, .wcwidth);\n    try testing.expectEqual(@as(u32, 6), end4.byte_offset); // After wave\n    try testing.expectEqual(@as(u32, 4), end4.columns_used);\n}\n\ntest \"wcwidth: getWidthAt with combining marks\" {\n    const text = \"e\\u{0301}test\"; // e + combining acute\n\n    // Width at 'e' should be 1\n    const width_e = utf8.getWidthAt(text, 0, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 1), width_e);\n\n    // Width at combining mark should be 0 (but next non-zero is 't')\n    const width_combining = utf8.getWidthAt(text, 1, 4, .wcwidth);\n    try testing.expectEqual(@as(u32, 0), width_combining);\n}\n\ntest \"wcwidth: getPrevGraphemeStart with ZWJ sequence\" {\n    const text = \"AB👩‍🚀\"; // A B woman ZWJ rocket\n\n    // From end (after rocket)\n    const r1 = utf8.getPrevGraphemeStart(text, text.len, 4, .wcwidth);\n    try testing.expect(r1 != null);\n    // Should point to rocket emoji (after ZWJ)\n    try testing.expectEqual(@as(u32, 2), r1.?.width);\n\n    // From rocket start, should go to ZWJ\n    const r2 = utf8.getPrevGraphemeStart(text, r1.?.start_offset, 4, .wcwidth);\n    try testing.expect(r2 != null);\n\n    // Eventually should reach woman emoji\n    var pos = text.len;\n    var count: usize = 0;\n    while (utf8.getPrevGraphemeStart(text, pos, 4, .wcwidth)) |prev| {\n        pos = prev.start_offset;\n        count += 1;\n        if (count > 10) break; // Safety limit\n    }\n    try testing.expect(count >= 3); // At least rocket, ZWJ, woman\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/word-wrap-editing_test.zig",
    "content": "const std = @import(\"std\");\nconst edit_buffer = @import(\"../edit-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst EditBuffer = edit_buffer.EditBuffer;\nconst TextBufferView = text_buffer_view.TextBufferView;\n\ntest \"Word wrap - editing around wrap boundary creates correct wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello my good\");\n\n    const vlines1 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines1.len);\n\n    try eb.setCursor(0, 13);\n    try eb.insertText(\" friend\");\n\n    const vlines2 = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines2.len);\n\n    try std.testing.expectEqual(@as(u32, 14), vlines2[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 6), vlines2[1].width_cols);\n}\n\ntest \"Word wrap - backspace and retype near boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello my good friend\");\n\n    var vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    try eb.setCursor(0, 20);\n    var i: usize = 0;\n    while (i < 7) : (i += 1) {\n        try eb.backspace();\n    }\n\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\" friend\");\n\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    try std.testing.expectEqual(@as(u32, 14), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 6), vlines[1].width_cols);\n}\n\ntest \"Word wrap - type character by character near boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello my good \");\n\n    var vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.setCursor(0, 14);\n    try eb.insertText(\"f\");\n\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\"r\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\"i\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\"e\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\"n\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    try eb.insertText(\"d\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    try eb.insertText(\" \");\n    vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n    try std.testing.expectEqual(@as(u32, 14), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 7), vlines[1].width_cols);\n}\n\ntest \"Word wrap - insert word in middle causes rewrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(20);\n\n    try eb.setText(\"hello friend\");\n\n    var vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.setCursor(0, 6);\n    try eb.insertText(\"my good \");\n\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n}\n\ntest \"Word wrap - delete word causes rewrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello my good friend buddy\");\n\n    var vlines = view.getVirtualLines();\n    try std.testing.expect(vlines.len >= 2);\n\n    try eb.setCursor(0, 6);\n    var i: usize = 0;\n    while (i < 8) : (i += 1) {\n        try eb.deleteForward();\n    }\n\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n}\n\ntest \"Word wrap - rapid edits maintain correct wrapping\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello my \");\n    try eb.setCursor(0, 9);\n    try eb.insertText(\"g\");\n    try eb.insertText(\"o\");\n    try eb.insertText(\"o\");\n    try eb.insertText(\"d\");\n    try eb.insertText(\" \");\n    try eb.insertText(\"f\");\n    try eb.insertText(\"r\");\n    try eb.insertText(\"i\");\n    try eb.insertText(\"e\");\n    try eb.insertText(\"n\");\n    try eb.insertText(\"d\");\n\n    const vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    try std.testing.expectEqual(@as(u32, 14), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 6), vlines[1].width_cols);\n}\n\ntest \"Word wrap - fragmented at exact word boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello \");\n    try eb.setCursor(0, 6);\n    try eb.insertText(\"my \");\n    try eb.insertText(\"good \");\n    try eb.insertText(\"friend\");\n\n    const vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n    try std.testing.expectEqual(@as(u32, 14), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 6), vlines[1].width_cols);\n}\n\ntest \"Word wrap - stale rollback state after newline with EditBuffer inserts\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(3);\n\n    try eb.setText(\"a\\n好\");\n\n    try eb.setCursor(0, 1);\n    try eb.insertText(\" b\");\n\n    try eb.setCursor(1, 2);\n    try eb.insertText(\"界\");\n\n    var plain_text: [32]u8 = undefined;\n    const plain_text_len = view.getPlainTextIntoBuffer(&plain_text);\n    try std.testing.expectEqualStrings(\"a b\\n好界\", plain_text[0..plain_text_len]);\n\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 3), vlines.len);\n    try std.testing.expectEqual(@as(u32, 3), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 2), vlines[1].width_cols);\n    try std.testing.expectEqual(@as(u32, 2), vlines[2].width_cols);\n    try std.testing.expectEqual(@as(u32, 0), vlines[0].source_line);\n    try std.testing.expectEqual(@as(u32, 1), vlines[1].source_line);\n    try std.testing.expectEqual(@as(u32, 1), vlines[2].source_line);\n}\n\ntest \"Word wrap - chunk boundary at start of word\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello my good \");\n    try eb.setCursor(0, 14);\n\n    try eb.insertText(\"f\");\n\n    try eb.backspace();\n    try eb.insertText(\"friend\");\n\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n    try std.testing.expectEqual(@as(u32, 14), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 6), vlines[1].width_cols);\n}\n\ntest \"Word wrap - multiple edits create complex fragmentation\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(20);\n\n    try eb.setText(\"hello \");\n    try eb.setCursor(0, 6);\n    try eb.insertText(\"w\");\n    try eb.backspace();\n    try eb.insertText(\"m\");\n    try eb.insertText(\"y\");\n    try eb.insertText(\" \");\n    try eb.insertText(\"g\");\n    try eb.insertText(\"o\");\n    try eb.backspace();\n    try eb.insertText(\"o\");\n    try eb.insertText(\"o\");\n    try eb.insertText(\"d\");\n    try eb.insertText(\" \");\n    try eb.insertText(\"x\");\n    try eb.backspace();\n    try eb.insertText(\"f\");\n    try eb.insertText(\"r\");\n    try eb.insertText(\"iend\");\n\n    const vlines = view.getVirtualLines();\n\n    var buffer: [100]u8 = undefined;\n    const len = view.getPlainTextIntoBuffer(&buffer);\n    try std.testing.expectEqualStrings(\"hello my good friend\", buffer[0..len]);\n\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n}\n\ntest \"Word wrap - insert at wrap boundary with existing wrap\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(15);\n\n    try eb.setText(\"hello world test\");\n\n    var vlines = view.getVirtualLines();\n    try std.testing.expect(vlines.len >= 2);\n\n    try eb.setCursor(0, 11);\n    try eb.insertText(\"s\");\n\n    vlines = view.getVirtualLines();\n\n    try std.testing.expect(vlines.len >= 2);\n\n    for (vlines) |vline| {\n        try std.testing.expect(vline.width_cols <= 15);\n    }\n}\n\ntest \"Word wrap - word at exact wrap width\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(20);\n\n    try eb.setText(\"12345678901234567890\");\n\n    var vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.setCursor(0, 20);\n    try eb.insertText(\" word\");\n\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n    try std.testing.expectEqual(@as(u32, 20), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 5), vlines[1].width_cols);\n}\n\ntest \"Word wrap - debug virtual line contents\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello my good \");\n    try eb.setCursor(0, 14);\n    try eb.insertText(\"f\");\n    try eb.backspace();\n    try eb.insertText(\"friend\");\n\n    const vlines = view.getVirtualLines();\n\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n}\n\ntest \"Word wrap - incremental character edits near boundary\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var eb = try EditBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer eb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, eb.getTextBuffer());\n    defer view.deinit();\n\n    view.setWrapMode(.word);\n    view.setWrapWidth(18);\n\n    try eb.setText(\"hello my good \");\n\n    var vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.setCursor(0, 14);\n    try eb.insertText(\"f\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\"r\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\"i\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\"e\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 1), vlines.len);\n\n    try eb.insertText(\"n\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    try eb.insertText(\"d\");\n    vlines = view.getVirtualLines();\n    try std.testing.expectEqual(@as(usize, 2), vlines.len);\n\n    try std.testing.expectEqual(@as(u32, 14), vlines[0].width_cols);\n    try std.testing.expectEqual(@as(u32, 6), vlines[1].width_cols);\n}\n"
  },
  {
    "path": "packages/core/src/zig/tests/wrap-cache-perf_test.zig",
    "content": "const std = @import(\"std\");\nconst text_buffer = @import(\"../text-buffer.zig\");\nconst text_buffer_view = @import(\"../text-buffer-view.zig\");\nconst gp = @import(\"../grapheme.zig\");\nconst link = @import(\"../link.zig\");\n\nconst TextBuffer = text_buffer.UnifiedTextBuffer;\nconst TextBufferView = text_buffer_view.UnifiedTextBufferView;\n\ntest \"word wrap complexity - width changes are O(n)\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    const size: usize = 100_000;\n\n    const text = try std.testing.allocator.alloc(u8, size);\n    defer std.testing.allocator.free(text);\n    @memset(text, 'x');\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n    try tb.setText(text);\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n    view.setWrapMode(.word);\n\n    const widths = [_]u32{ 60, 70, 80, 90, 100 };\n\n    // Run multiple iterations and use median to reduce noise from CI variability\n    const iterations = 5;\n    var median_times: [widths.len]u64 = undefined;\n\n    for (widths, 0..) |width, width_idx| {\n        var iter_times: [iterations]u64 = undefined;\n\n        for (0..iterations) |iter| {\n            // Reset cache by setting a different width first\n            view.setWrapWidth(50);\n            _ = view.getVirtualLineCount();\n\n            view.setWrapWidth(width);\n            var timer = std.time.Timer.start() catch unreachable;\n            _ = view.getVirtualLineCount();\n            iter_times[iter] = timer.read();\n        }\n\n        // Sort and take median\n        std.mem.sort(u64, &iter_times, {}, std.sort.asc(u64));\n        median_times[width_idx] = iter_times[iterations / 2];\n    }\n\n    var min_time: u64 = std.math.maxInt(u64);\n    var max_time: u64 = 0;\n    for (median_times) |t| {\n        min_time = @min(min_time, t);\n        max_time = @max(max_time, t);\n    }\n\n    const ratio = @as(f64, @floatFromInt(max_time)) / @as(f64, @floatFromInt(min_time));\n\n    // All times should be roughly similar since text size is constant.\n    // Use a generous threshold (5x) to account for CI runner variability.\n    try std.testing.expect(ratio < 5.0);\n}\n\ntest \"word wrap - virtual line count correctness\" {\n    const pool = gp.initGlobalPool(std.testing.allocator);\n    defer gp.deinitGlobalPool();\n    const link_pool = link.initGlobalLinkPool(std.testing.allocator);\n    defer link.deinitGlobalLinkPool();\n\n    var tb = try TextBuffer.init(std.testing.allocator, pool, link_pool, .wcwidth);\n    defer tb.deinit();\n\n    var view = try TextBufferView.init(std.testing.allocator, tb);\n    defer view.deinit();\n\n    const pattern = \"var abc=123;function foo(){return bar+baz;}if(x>0){y=z*2;}else{y=0;}\";\n    const size = 10_000;\n    var text = try std.testing.allocator.alloc(u8, size);\n    defer std.testing.allocator.free(text);\n\n    var i: usize = 0;\n    while (i < size) {\n        const remaining = size - i;\n        const copy_len = @min(pattern.len, remaining);\n        @memcpy(text[i .. i + copy_len], pattern[0..copy_len]);\n        i += copy_len;\n    }\n\n    try tb.setText(text);\n    view.setWrapMode(.word);\n\n    view.setWrapWidth(80);\n    const count_80 = view.getVirtualLineCount();\n\n    view.setWrapWidth(100);\n    const count_100 = view.getVirtualLineCount();\n\n    view.setWrapWidth(60);\n    const count_60 = view.getVirtualLineCount();\n\n    view.setWrapWidth(80);\n    const count_80_again = view.getVirtualLineCount();\n\n    try std.testing.expect(count_80 > 100);\n    try std.testing.expectEqual(count_80, count_80_again);\n    try std.testing.expect(count_100 < count_80);\n    try std.testing.expect(count_60 > count_80);\n}\n"
  },
  {
    "path": "packages/core/src/zig/text-buffer-iterators.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst seg_mod = @import(\"text-buffer-segment.zig\");\nconst mem_registry_mod = @import(\"mem-registry.zig\");\nconst utf8 = @import(\"utf8.zig\");\n\nconst Segment = seg_mod.Segment;\nconst UnifiedRope = seg_mod.UnifiedRope;\nconst TextChunk = seg_mod.TextChunk;\nconst GraphemeInfo = seg_mod.GraphemeInfo;\nconst MemRegistry = mem_registry_mod.MemRegistry;\n\npub const LineInfo = struct {\n    line_idx: u32,\n    col_offset: u32,\n    width_cols: u32,\n    seg_start: u32,\n    seg_end: u32,\n};\n\npub const Coords = struct {\n    row: u32,\n    col: u32,\n};\n\n/// Note: Takes mutable rope for lazy marker cache rebuilding\npub fn walkLines(\n    rope: *UnifiedRope,\n    ctx: *anyopaque,\n    callback: *const fn (ctx: *anyopaque, line_info: LineInfo) void,\n    include_newlines_in_offset: bool,\n) void {\n    const linestart_count = rope.markerCount(.linestart);\n    if (linestart_count == 0) return;\n\n    var i: u32 = 0;\n    while (i < linestart_count) : (i += 1) {\n        const marker = rope.getMarker(.linestart, i) orelse continue;\n        const line_start_weight = marker.global_weight;\n        const width_cols = lineWidthAt(rope, i);\n        const seg_end = if (i + 1 < linestart_count) blk: {\n            const next_marker = rope.getMarker(.linestart, i + 1) orelse break :blk marker.leaf_index + 1;\n            break :blk next_marker.leaf_index;\n        } else blk: {\n            break :blk rope.count();\n        };\n\n        // Line i has i newlines before it (one after each previous line)\n        const col_offset = if (include_newlines_in_offset)\n            line_start_weight\n        else\n            line_start_weight - i;\n\n        callback(ctx, LineInfo{\n            .line_idx = i,\n            .col_offset = col_offset,\n            .width_cols = width_cols,\n            .seg_start = marker.leaf_index,\n            .seg_end = seg_end,\n        });\n    }\n}\n\n/// This is the most efficient way to iterate lines and their content\npub fn walkLinesAndSegments(\n    rope: *const UnifiedRope,\n    ctx: *anyopaque,\n    segment_callback: *const fn (ctx: *anyopaque, line_idx: u32, chunk: *const TextChunk, chunk_idx_in_line: u32) void,\n    line_end_callback: *const fn (ctx: *anyopaque, line_info: LineInfo) void,\n) void {\n    if (rope.count() == 0) {\n        return;\n    }\n\n    const WalkContext = struct {\n        user_ctx: *anyopaque,\n        seg_callback: *const fn (ctx: *anyopaque, line_idx: u32, chunk: *const TextChunk, chunk_idx_in_line: u32) void,\n        line_callback: *const fn (ctx: *anyopaque, line_info: LineInfo) void,\n        current_line_idx: u32 = 0,\n        current_col_offset: u32 = 0,\n        line_start_seg: u32 = 0,\n        current_seg_idx: u32 = 0,\n        line_width_cols: u32 = 0,\n        chunk_idx_in_line: u32 = 0,\n\n        fn walker(walk_ctx_ptr: *anyopaque, seg: *const Segment, idx: u32) UnifiedRope.Node.WalkerResult {\n            const walk_ctx = @as(*@This(), @ptrCast(@alignCast(walk_ctx_ptr)));\n\n            if (seg.asText()) |chunk| {\n                walk_ctx.seg_callback(walk_ctx.user_ctx, walk_ctx.current_line_idx, chunk, walk_ctx.chunk_idx_in_line);\n                walk_ctx.chunk_idx_in_line += 1;\n                walk_ctx.line_width_cols += chunk.width;\n            } else if (seg.isBreak()) {\n                walk_ctx.line_callback(walk_ctx.user_ctx, LineInfo{\n                    .line_idx = walk_ctx.current_line_idx,\n                    .col_offset = walk_ctx.current_col_offset,\n                    .width_cols = walk_ctx.line_width_cols,\n                    .seg_start = walk_ctx.line_start_seg,\n                    .seg_end = idx, // Don't include the break\n                });\n\n                walk_ctx.current_line_idx += 1;\n                walk_ctx.current_col_offset += walk_ctx.line_width_cols + 1;\n                walk_ctx.line_start_seg = idx + 1;\n                walk_ctx.line_width_cols = 0;\n                walk_ctx.chunk_idx_in_line = 0;\n            }\n\n            walk_ctx.current_seg_idx = idx + 1;\n            return .{};\n        }\n    };\n\n    var walk_ctx = WalkContext{\n        .user_ctx = ctx,\n        .seg_callback = segment_callback,\n        .line_callback = line_end_callback,\n    };\n    rope.walk(&walk_ctx, WalkContext.walker) catch {};\n\n    // Emit final line if we have content after last break OR if we had at least one break\n    // (A trailing break creates an empty final line)\n    const had_breaks = walk_ctx.current_line_idx > 0;\n    const has_content_after_break = walk_ctx.line_start_seg < walk_ctx.current_seg_idx;\n\n    if (has_content_after_break or had_breaks) {\n        line_end_callback(ctx, LineInfo{\n            .line_idx = walk_ctx.current_line_idx,\n            .col_offset = walk_ctx.current_col_offset,\n            .width_cols = walk_ctx.line_width_cols,\n            .seg_start = walk_ctx.line_start_seg,\n            .seg_end = walk_ctx.current_seg_idx,\n        });\n    }\n}\n\npub fn getLineCount(rope: *const UnifiedRope) u32 {\n    const metrics = rope.root.metrics();\n    return metrics.custom.linestart_count;\n}\n\npub fn getMaxLineWidth(rope: *const UnifiedRope) u32 {\n    const metrics = rope.root.metrics();\n    return metrics.custom.max_line_width;\n}\n\npub fn getTotalWidth(rope: *const UnifiedRope) u32 {\n    const metrics = rope.root.metrics();\n    return metrics.custom.total_width;\n}\n\n/// Optimized O(1) implementation using linestart marker lookups\n/// Note: Rope weight includes newlines (each .brk adds +1), but col is still display width\n/// Takes mutable rope for lazy marker cache rebuilding\npub fn coordsToOffset(rope: *UnifiedRope, row: u32, col: u32) ?u32 {\n    const linestart_count = rope.markerCount(.linestart);\n    if (row >= linestart_count) return null;\n\n    const marker = rope.getMarker(.linestart, row) orelse return null;\n    const line_start_weight = marker.global_weight;\n    const line_width = lineWidthAt(rope, row);\n\n    if (col > line_width) return null;\n\n    return line_start_weight + col;\n}\n\n/// Optimized O(log n) implementation using binary search on linestart markers\n/// Note: Rope weight includes newlines, so valid offsets are 0..totalWeight() inclusive\n/// Takes mutable rope for lazy marker cache rebuilding\n/// TODO: Should clamp to min/max offset and always return valid coords\npub fn offsetToCoords(rope: *UnifiedRope, offset: u32) ?Coords {\n    const linestart_count = rope.markerCount(.linestart);\n    if (linestart_count == 0) return null;\n\n    const total_weight = rope.totalWeight();\n    if (offset > total_weight) return null;\n\n    var left: u32 = 0;\n    var right: u32 = linestart_count;\n\n    while (left < right) {\n        const mid = left + (right - left) / 2;\n        const marker = rope.getMarker(.linestart, mid) orelse return null;\n        const line_start_weight = marker.global_weight;\n\n        if (offset < line_start_weight) {\n            right = mid;\n        } else {\n            const next_line_start_weight = if (mid + 1 < linestart_count) blk: {\n                const next_marker = rope.getMarker(.linestart, mid + 1) orelse return null;\n                break :blk next_marker.global_weight;\n            } else blk: {\n                // Last line: ends at total weight\n                break :blk total_weight;\n            };\n\n            // Offset belongs to this line if it's before the next line starts\n            // (newline offset at end of non-final line maps to col==line_width)\n            if (offset < next_line_start_weight or (offset == total_weight and mid + 1 == linestart_count)) {\n                return Coords{\n                    .row = mid,\n                    .col = offset - line_start_weight,\n                };\n            }\n            left = mid + 1;\n        }\n    }\n\n    return null;\n}\n\n/// Note: Returns display width only (excludes newline weight)\n/// Takes mutable rope for lazy marker cache rebuilding\npub fn lineWidthAt(rope: *UnifiedRope, row: u32) u32 {\n    const linestart_count = rope.markerCount(.linestart);\n    if (row >= linestart_count) return 0;\n\n    const line_marker = rope.getMarker(.linestart, row) orelse return 0;\n    const line_start_weight = line_marker.global_weight;\n    if (row + 1 < linestart_count) {\n        // Non-final line: width = (next_line_start - current_start - 1_for_newline)\n        const next_marker = rope.getMarker(.linestart, row + 1) orelse return 0;\n        const next_line_start_weight = next_marker.global_weight;\n        // Guard against underflow (adjacent linestart markers or empty line)\n        if (next_line_start_weight <= line_start_weight) return 0;\n        return next_line_start_weight - line_start_weight - 1;\n    } else {\n        // Final line: width = total_weight - line_start (total weight includes all previous newlines)\n        const total_weight = rope.totalWeight();\n        return total_weight - line_start_weight;\n    }\n}\n\n/// Takes mutable rope for lazy marker cache rebuilding\npub fn getGraphemeWidthAt(rope: *UnifiedRope, mem_registry: *const MemRegistry, row: u32, col: u32, tab_width: u8, width_method: utf8.WidthMethod) u32 {\n    const line_width = lineWidthAt(rope, row);\n    if (col >= line_width) return 0;\n\n    const linestart = rope.getMarker(.linestart, row) orelse return 0;\n    var seg_idx = linestart.leaf_index + 1;\n    var cols_before: u32 = 0;\n\n    while (seg_idx < rope.count()) : (seg_idx += 1) {\n        const seg = rope.get(seg_idx) orelse break;\n        if (seg.isBreak() or seg.isLineStart()) break;\n        if (seg.asText()) |chunk| {\n            const next_cols = cols_before + chunk.width;\n            if (col < next_cols) {\n                const local_col: u32 = col - cols_before;\n                const bytes = chunk.getBytes(mem_registry);\n                const is_ascii = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n                const pos = utf8.findPosByWidth(bytes, local_col, tab_width, is_ascii, false, width_method);\n                if (pos.byte_offset >= bytes.len) return 0; // at end of chunk\n                const grapheme_start_col = pos.columns_used;\n                const width = utf8.getWidthAt(bytes, pos.byte_offset, tab_width, width_method);\n\n                // Calculate remaining width: if cursor is in the middle of a wide grapheme,\n                // return only the remaining columns to reach the end of the grapheme\n                const grapheme_end_col = grapheme_start_col + width;\n                const remaining_width = grapheme_end_col - local_col;\n                return remaining_width;\n            }\n            cols_before = next_cols;\n        }\n    }\n    return 0;\n}\n\n/// Takes mutable rope for lazy marker cache rebuilding\npub fn getPrevGraphemeWidth(rope: *UnifiedRope, mem_registry: *const MemRegistry, row: u32, col: u32, tab_width: u8, width_method: utf8.WidthMethod) u32 {\n    if (col == 0) return 0;\n\n    const line_width = lineWidthAt(rope, row);\n    const clamped_col: u32 = @min(col, line_width);\n\n    const linestart = rope.getMarker(.linestart, row) orelse return 0;\n    var seg_idx = linestart.leaf_index + 1;\n    var cols_before: u32 = 0;\n    var prev_chunk: ?struct { chunk: TextChunk, cols_before: u32 } = null;\n\n    while (seg_idx < rope.count()) : (seg_idx += 1) {\n        const seg = rope.get(seg_idx) orelse break;\n        if (seg.isBreak() or seg.isLineStart()) break;\n        if (seg.asText()) |chunk| {\n            const next_cols = cols_before + chunk.width;\n\n            if (clamped_col <= next_cols) {\n                if (clamped_col == cols_before and prev_chunk != null) {\n                    // Exactly at chunk boundary - get last grapheme from previous chunk\n                    const pc = prev_chunk.?;\n                    const bytes = pc.chunk.getBytes(mem_registry);\n                    const prev = utf8.getPrevGraphemeStart(bytes, bytes.len, tab_width, width_method);\n                    if (prev) |res| {\n                        return res.width;\n                    }\n                    return 0;\n                }\n\n                const bytes = chunk.getBytes(mem_registry);\n                const is_ascii = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n                const local_col: u32 = clamped_col - cols_before;\n\n                const here = utf8.findPosByWidth(bytes, local_col, tab_width, is_ascii, false, width_method);\n\n                const grapheme_start_col = here.columns_used;\n\n                // Check for integer underflow: if grapheme_start_col > local_col, we're in the middle of a grapheme\n                // that spans beyond local_col. This can happen with multi-codepoint graphemes.\n                if (grapheme_start_col > local_col) {\n                    // We're in the middle of a wide grapheme cluster - need to look at previous chunk or grapheme\n                    if (prev_chunk) |pc| {\n                        const prev_bytes = pc.chunk.getBytes(mem_registry);\n                        const prev = utf8.getPrevGraphemeStart(prev_bytes, prev_bytes.len, tab_width, width_method);\n                        if (prev) |res| return res.width;\n                    }\n                    return 0;\n                }\n\n                const offset_into_grapheme = local_col - grapheme_start_col;\n\n                if (offset_into_grapheme > 0) {\n                    // We need to jump back: offset_into_grapheme + width of previous grapheme\n                    const prev = utf8.getPrevGraphemeStart(bytes, @intCast(here.byte_offset), tab_width, width_method);\n                    if (prev) |res| {\n                        const total_distance = offset_into_grapheme + res.width;\n                        return total_distance;\n                    }\n                    return offset_into_grapheme;\n                }\n\n                const prev = utf8.getPrevGraphemeStart(bytes, @intCast(here.byte_offset), tab_width, width_method);\n                if (prev) |res| {\n                    return res.width;\n                }\n                return 0;\n            }\n\n            prev_chunk = .{ .chunk = chunk.*, .cols_before = cols_before };\n            cols_before = next_cols;\n        }\n    }\n    return 0;\n}\n\npub const CharOffsetColumnInfo = struct {\n    col: u32,\n    width: u32,\n};\n\n/// char_offset is grapheme-count based (not raw codepoint)\n/// grapheme_idx and col_delta carry incremental state across calls\npub fn charOffsetToColumn(\n    char_offset: u32,\n    graphemes: []const GraphemeInfo,\n    grapheme_idx: *usize,\n    col_delta: *i64,\n) CharOffsetColumnInfo {\n    while (grapheme_idx.* < graphemes.len) {\n        const info = graphemes[grapheme_idx.*];\n        const info_char_offset = @as(i64, info.col_offset) - col_delta.*;\n        if (info_char_offset >= @as(i64, char_offset)) break;\n        col_delta.* += @as(i64, info.width) - 1;\n        grapheme_idx.* += 1;\n    }\n\n    var break_col_i64 = @as(i64, char_offset) + col_delta.*;\n    if (break_col_i64 < 0) break_col_i64 = 0;\n    const break_col = @as(u32, @intCast(break_col_i64));\n\n    var width: u32 = 1;\n    if (grapheme_idx.* < graphemes.len) {\n        const info = graphemes[grapheme_idx.*];\n        const info_char_offset = @as(i64, info.col_offset) - col_delta.*;\n        if (info_char_offset == @as(i64, char_offset)) {\n            width = @as(u32, info.width);\n        }\n    }\n\n    return .{ .col = break_col, .width = width };\n}\n\n/// Extract text between display-width offsets into a buffer\n/// Automatically snaps to grapheme boundaries:\n/// - start_offset excludes graphemes that start before it\n/// - end_offset includes graphemes that start before it\n/// Returns number of bytes written to out_buffer\npub fn extractTextBetweenOffsets(\n    rope: *const UnifiedRope,\n    mem_registry: *const MemRegistry,\n    tab_width: u8,\n    start_offset: u32,\n    end_offset: u32,\n    out_buffer: []u8,\n    width_method: utf8.WidthMethod,\n) usize {\n    if (start_offset >= end_offset) return 0;\n    if (out_buffer.len == 0) return 0;\n\n    const line_count = rope.root.metrics().custom.linestart_count;\n\n    var out_index: usize = 0;\n    var col_offset: u32 = 0;\n\n    _ = width_method; // Just ignore for now, will use .unicode as default\n\n    const Context = struct {\n        rope: *const UnifiedRope,\n        mem_registry: *const MemRegistry,\n        tab_width: u8,\n        out_buffer: []u8,\n        out_index: *usize,\n        col_offset: *u32,\n        start: u32,\n        end: u32,\n        line_count: u32,\n        line_had_content: bool = false,\n\n        fn segment_callback(ctx_ptr: *anyopaque, line_idx: u32, chunk: *const TextChunk, chunk_idx_in_line: u32) void {\n            _ = line_idx;\n            _ = chunk_idx_in_line;\n            const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n\n            const chunk_start_offset = ctx.col_offset.*;\n            const chunk_end_offset = chunk_start_offset + chunk.width;\n\n            // Skip chunk if it's entirely outside range\n            if (chunk_end_offset <= ctx.start or chunk_start_offset >= ctx.end) {\n                ctx.col_offset.* = chunk_end_offset;\n                return;\n            }\n\n            ctx.line_had_content = true;\n\n            const chunk_bytes = chunk.getBytes(ctx.mem_registry);\n            const is_ascii_only = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n\n            const local_start_col: u32 = if (ctx.start > chunk_start_offset) ctx.start - chunk_start_offset else 0;\n            const local_end_col: u32 = @min(ctx.end - chunk_start_offset, chunk.width);\n\n            var byte_start: u32 = 0;\n            var byte_end: u32 = @intCast(chunk_bytes.len);\n\n            if (local_start_col > 0) {\n                const start_result = utf8.findPosByWidth(chunk_bytes, local_start_col, ctx.tab_width, is_ascii_only, false, .unicode);\n                byte_start = start_result.byte_offset;\n            }\n\n            if (local_end_col < chunk.width) {\n                const end_result = utf8.findPosByWidth(chunk_bytes, local_end_col, ctx.tab_width, is_ascii_only, true, .unicode);\n                byte_end = end_result.byte_offset;\n            }\n\n            if (byte_start < byte_end and byte_start < chunk_bytes.len) {\n                const actual_end = @min(byte_end, @as(u32, @intCast(chunk_bytes.len)));\n                const selected_bytes = chunk_bytes[byte_start..actual_end];\n                const copy_len = @min(selected_bytes.len, ctx.out_buffer.len - ctx.out_index.*);\n\n                if (copy_len > 0) {\n                    @memcpy(ctx.out_buffer[ctx.out_index.* .. ctx.out_index.* + copy_len], selected_bytes[0..copy_len]);\n                    ctx.out_index.* += copy_len;\n                }\n            }\n\n            ctx.col_offset.* = chunk_end_offset;\n        }\n\n        fn line_end_callback(ctx_ptr: *anyopaque, line_info: LineInfo) void {\n            const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n\n            // Add newline if we had content and range extends beyond this line's newline\n            if (ctx.line_had_content and line_info.line_idx < ctx.line_count - 1 and ctx.col_offset.* + 1 < ctx.end and ctx.out_index.* < ctx.out_buffer.len) {\n                ctx.out_buffer[ctx.out_index.*] = '\\n';\n                ctx.out_index.* += 1;\n            }\n\n            // Account for newline in display offset\n            ctx.col_offset.* += 1;\n\n            ctx.line_had_content = false;\n        }\n    };\n\n    var ctx = Context{\n        .rope = rope,\n        .mem_registry = mem_registry,\n        .tab_width = tab_width,\n        .out_buffer = out_buffer,\n        .out_index = &out_index,\n        .col_offset = &col_offset,\n        .start = start_offset,\n        .end = end_offset,\n        .line_count = line_count,\n    };\n\n    walkLinesAndSegments(rope, &ctx, Context.segment_callback, Context.line_end_callback);\n\n    return out_index;\n}\n"
  },
  {
    "path": "packages/core/src/zig/text-buffer-segment.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst rope_mod = @import(\"rope.zig\");\nconst buffer = @import(\"buffer.zig\");\nconst mem_registry_mod = @import(\"mem-registry.zig\");\n\nconst gp = @import(\"grapheme.zig\");\n\nconst utf8 = @import(\"utf8.zig\");\n\npub const RGBA = buffer.RGBA;\npub const TextSelection = buffer.TextSelection;\n\npub const TextBufferError = error{\n    OutOfMemory,\n    InvalidDimensions,\n    InvalidIndex,\n    InvalidId,\n    InvalidMemId,\n};\n\nconst MemRegistry = mem_registry_mod.MemRegistry;\n\npub const WrapMode = enum {\n    none,\n    char,\n    word,\n};\n\npub const ChunkFitResult = struct {\n    char_count: u32,\n    width: u32,\n};\n\npub const GraphemeInfo = utf8.GraphemeInfo;\n\n/// A chunk represents a contiguous sequence of UTF-8 bytes from a specific memory buffer\npub const TextChunk = struct {\n    mem_id: u8,\n    byte_start: u32,\n    byte_end: u32,\n    width: u16,\n    flags: u8 = 0,\n    graphemes: ?[]GraphemeInfo = null,\n    wrap_offsets: ?[]utf8.WrapBreak = null,\n\n    pub const Flags = struct {\n        pub const ASCII_ONLY: u8 = 0b00000001; // Printable ASCII only (32..126).\n    };\n\n    pub fn isAsciiOnly(self: *const TextChunk) bool {\n        return (self.flags & Flags.ASCII_ONLY) != 0;\n    }\n\n    pub fn empty() TextChunk {\n        return .{\n            .mem_id = 0,\n            .byte_start = 0,\n            .byte_end = 0,\n            .width = 0,\n        };\n    }\n\n    pub fn is_empty(self: *const TextChunk) bool {\n        return self.width == 0;\n    }\n\n    pub fn getBytes(self: *const TextChunk, mem_registry: *const MemRegistry) []const u8 {\n        const mem_buf = mem_registry.get(self.mem_id) orelse return &[_]u8{};\n        return mem_buf[self.byte_start..self.byte_end];\n    }\n\n    /// Lazily compute and cache grapheme info for this chunk\n    /// Returns a slice that is valid until the buffer is reset\n    /// For ASCII-only chunks, returns an empty slice (sentinel)\n    /// For mixed chunks, returns only multibyte (non-ASCII) graphemes and tabs with their column offsets\n    pub fn getGraphemes(\n        self: *const TextChunk,\n        mem_registry: *const MemRegistry,\n        allocator: Allocator,\n        tabwidth: u8,\n        width_method: utf8.WidthMethod,\n    ) TextBufferError![]const GraphemeInfo {\n        const mut_self = @constCast(self);\n        if (self.graphemes) |cached| {\n            return cached;\n        }\n\n        if (self.isAsciiOnly()) {\n            const empty_slice = try allocator.alloc(GraphemeInfo, 0);\n            mut_self.graphemes = empty_slice;\n            return empty_slice;\n        }\n\n        const chunk_bytes = self.getBytes(mem_registry);\n\n        var grapheme_list: std.ArrayListUnmanaged(GraphemeInfo) = .{};\n        errdefer grapheme_list.deinit(allocator);\n\n        try utf8.findGraphemeInfo(chunk_bytes, tabwidth, self.isAsciiOnly(), width_method, allocator, &grapheme_list);\n\n        // TODO: Calling this with an arena allocator will just double the memory usage?\n        const graphemes = try grapheme_list.toOwnedSlice(allocator);\n\n        mut_self.graphemes = graphemes;\n        return graphemes;\n    }\n\n    /// Lazily compute and cache wrap offsets for this chunk\n    /// Returns a slice that is valid until the buffer is reset\n    pub fn getWrapOffsets(\n        self: *const TextChunk,\n        mem_registry: *const MemRegistry,\n        allocator: Allocator,\n        width_method: utf8.WidthMethod,\n    ) TextBufferError![]const utf8.WrapBreak {\n        const mut_self = @constCast(self);\n        if (self.wrap_offsets) |cached| {\n            return cached;\n        }\n\n        const chunk_bytes = self.getBytes(mem_registry);\n        var wrap_result = utf8.WrapBreakResult.init(allocator);\n        errdefer wrap_result.deinit();\n\n        try utf8.findWrapBreaks(chunk_bytes, &wrap_result, width_method);\n\n        // TODO: Do not cache for chunks < 64 bytes, as it does not profit from the cache\n        // Use toOwnedSlice to transfer ownership without copying\n        const wrap_offsets = try wrap_result.breaks.toOwnedSlice(allocator);\n        mut_self.wrap_offsets = wrap_offsets;\n\n        return wrap_offsets;\n    }\n};\n\n/// A highlight represents a styled region on a line\npub const Highlight = struct {\n    col_start: u32,\n    col_end: u32,\n    style_id: u32,\n    priority: u8,\n    hl_ref: u16 = 0,\n};\n\n/// Pre-computed style span for efficient rendering\n/// Represents a contiguous region with a single style\npub const StyleSpan = struct {\n    col: u32,\n    style_id: u32,\n    next_col: u32,\n};\n\n/// A segment in the unified rope - either text content or a line break marker\npub const Segment = union(enum) {\n    text: TextChunk,\n    brk: void,\n    linestart: void,\n\n    /// Define which union tags are markers (for O(1) line lookup)\n    pub const MarkerTypes = &[_]std.meta.Tag(Segment){ .brk, .linestart };\n\n    /// Metrics for aggregation in the rope tree\n    /// These enable O(log n) row/col coordinate mapping and efficient line queries\n    pub const Metrics = struct {\n        total_width: u32 = 0,\n        total_bytes: u32 = 0,\n        linestart_count: u32 = 0,\n        newline_count: u32 = 0,\n        max_line_width: u32 = 0,\n        /// Whether all text segments in subtree are ASCII-only (for fast wrapping paths)\n        ascii_only: bool = true,\n\n        pub fn add(self: *Metrics, other: Metrics) void {\n            self.total_width += other.total_width;\n            self.total_bytes += other.total_bytes;\n            self.linestart_count += other.linestart_count;\n            self.newline_count += other.newline_count;\n\n            self.max_line_width = @max(self.max_line_width, other.max_line_width);\n\n            self.ascii_only = self.ascii_only and other.ascii_only;\n        }\n\n        /// Get the balancing weight for the rope\n        /// We use total_width + newline_count to give each break a weight of 1\n        /// This eliminates boundary ambiguity in coordinate/offset conversions\n        pub fn weight(self: *const Metrics) u32 {\n            return self.total_width + self.newline_count;\n        }\n    };\n\n    /// Measure this segment to produce its metrics\n    pub fn measure(self: *const Segment) Metrics {\n        return switch (self.*) {\n            .text => |chunk| blk: {\n                const is_ascii = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n                const byte_len = chunk.byte_end - chunk.byte_start;\n                break :blk Metrics{\n                    .total_width = chunk.width,\n                    .total_bytes = byte_len,\n                    .linestart_count = 0,\n                    .newline_count = 0,\n                    .max_line_width = chunk.width,\n                    .ascii_only = is_ascii,\n                };\n            },\n            .brk => Metrics{\n                .total_width = 0,\n                .total_bytes = 0,\n                .linestart_count = 0,\n                .newline_count = 1,\n                .max_line_width = 0,\n                .ascii_only = true,\n            },\n            .linestart => Metrics{\n                .total_width = 0,\n                .total_bytes = 0,\n                .linestart_count = 1,\n                .newline_count = 0,\n                .max_line_width = 0,\n                .ascii_only = true,\n            },\n        };\n    }\n\n    pub fn empty() Segment {\n        return .{ .text = TextChunk.empty() };\n    }\n\n    pub fn is_empty(self: *const Segment) bool {\n        return switch (self.*) {\n            .text => |chunk| chunk.is_empty(),\n            .brk => false,\n            .linestart => false,\n        };\n    }\n\n    pub fn getBytes(self: *const Segment, mem_registry: *const MemRegistry) []const u8 {\n        return switch (self.*) {\n            .text => |chunk| chunk.getBytes(mem_registry),\n            .brk => &[_]u8{},\n            .linestart => &[_]u8{},\n        };\n    }\n\n    pub fn isBreak(self: *const Segment) bool {\n        return switch (self.*) {\n            .brk => true,\n            else => false,\n        };\n    }\n\n    pub fn isLineStart(self: *const Segment) bool {\n        return switch (self.*) {\n            .linestart => true,\n            else => false,\n        };\n    }\n\n    pub fn isText(self: *const Segment) bool {\n        return switch (self.*) {\n            .text => true,\n            else => false,\n        };\n    }\n\n    pub fn asText(self: *const Segment) ?*const TextChunk {\n        return switch (self.*) {\n            .text => |*chunk| chunk,\n            else => null,\n        };\n    }\n\n    /// Two text chunks can be merged if they reference contiguous memory in the same buffer\n    pub fn canMerge(left: *const Segment, right: *const Segment) bool {\n        if (!left.isText() or !right.isText()) return false;\n\n        const left_chunk = left.asText() orelse return false;\n        const right_chunk = right.asText() orelse return false;\n\n        if (left_chunk.mem_id != right_chunk.mem_id) return false;\n        if (left_chunk.byte_end != right_chunk.byte_start) return false;\n        if (left_chunk.flags != right_chunk.flags) return false;\n\n        return true;\n    }\n\n    pub fn merge(allocator: Allocator, left: *const Segment, right: *const Segment) Segment {\n        _ = allocator;\n\n        const left_chunk = left.asText().?;\n        const right_chunk = right.asText().?;\n\n        // TODO: could clear the caches on the original chunks,\n        // as the original chunks are only kept for history purposes.\n\n        return Segment{\n            .text = TextChunk{\n                .mem_id = left_chunk.mem_id,\n                .byte_start = left_chunk.byte_start,\n                .byte_end = right_chunk.byte_end,\n                .width = left_chunk.width + right_chunk.width,\n                .flags = left_chunk.flags,\n                .graphemes = null,\n                .wrap_offsets = null,\n            },\n        };\n    }\n\n    /// Boundary normalization action\n    pub const BoundaryAction = struct {\n        delete_left: bool = false,\n        delete_right: bool = false,\n        insert_between: []const Segment = &[_]Segment{},\n    };\n\n    /// Rewrite boundary between two adjacent segments to enforce invariants\n    ///\n    /// Document invariants enforced at join boundaries:\n    /// - Every line starts with a linestart marker\n    /// - Line breaks must be followed by linestart markers\n    /// - No duplicate linestart markers (deduplicated automatically)\n    /// - When joining lines, orphaned linestart markers are removed\n    /// - Empty lines are represented as [linestart, brk] with no text, or [linestart] if final\n    /// - Consecutive breaks [brk, brk] get a linestart inserted between (empty line)\n    ///\n    /// Rules applied locally at O(log n) join points:\n    /// - [linestart, linestart] → delete right (dedup)\n    /// - [brk, text] → insert linestart between (ensure line starts with marker)\n    /// - [brk, brk] → insert linestart between (represents empty line)\n    /// - [text, linestart] → delete right (remove orphaned linestart when joining lines)\n    ///\n    /// Valid patterns (no action needed):\n    /// - [text, brk] (line content followed by break)\n    /// - [linestart, text] (line marker followed by content)\n    /// - [linestart, brk] (empty line before another line)\n    /// - [linestart] alone (empty final line or empty buffer)\n    /// - [brk, linestart, brk] (empty line between two lines, normalized from [brk, brk])\n    ///\n    /// These rules preserve linestart markers when deleting at col=0 within a line,\n    /// since the deletion splits around the marker, and [text, linestart] only triggers\n    /// when actually joining lines (deleting the break between them).\n    pub fn rewriteBoundary(allocator: Allocator, left: ?*const Segment, right: ?*const Segment) !BoundaryAction {\n        _ = allocator;\n\n        if (left == null or right == null) return .{};\n\n        const left_seg = left.?;\n        const right_seg = right.?;\n\n        // [linestart, linestart] -> delete right (dedup)\n        if (left_seg.isLineStart() and right_seg.isLineStart()) {\n            return .{ .delete_right = true };\n        }\n\n        // [brk, brk] -> insert linestart between (represents empty line)\n        if (left_seg.isBreak() and right_seg.isBreak()) {\n            const linestart_segment = Segment{ .linestart = {} };\n            const insert_slice = &[_]Segment{linestart_segment};\n            return .{ .insert_between = insert_slice };\n        }\n\n        // [brk, text] -> insert linestart between\n        if (left_seg.isBreak() and right_seg.isText()) {\n            const linestart_segment = Segment{ .linestart = {} };\n            const insert_slice = &[_]Segment{linestart_segment};\n            return .{ .insert_between = insert_slice };\n        }\n\n        // [text, linestart] -> delete right (remove orphaned linestart when joining lines)\n        if (left_seg.isText() and right_seg.isLineStart()) {\n            return .{ .delete_right = true };\n        }\n\n        return .{};\n    }\n\n    /// Rewrite rope ends to enforce invariants\n    /// Rules:\n    /// - Rope must start with linestart (even when empty - ensures at least one line)\n    pub fn rewriteEnds(allocator: Allocator, first: ?*const Segment, last: ?*const Segment) !BoundaryAction {\n        _ = allocator;\n        _ = last;\n\n        // Ensure rope starts with linestart (insert even if empty)\n        if (first) |first_seg| {\n            if (!first_seg.isLineStart()) {\n                const linestart_segment = Segment{ .linestart = {} };\n                const insert_slice = &[_]Segment{linestart_segment};\n                return .{ .insert_between = insert_slice };\n            }\n        } else {\n            // Empty rope - insert linestart to ensure at least one line\n            const linestart_segment = Segment{ .linestart = {} };\n            const insert_slice = &[_]Segment{linestart_segment};\n            return .{ .insert_between = insert_slice };\n        }\n\n        return .{};\n    }\n};\n\npub const UnifiedRope = rope_mod.Rope(Segment);\n"
  },
  {
    "path": "packages/core/src/zig/text-buffer-view.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst tb = @import(\"text-buffer.zig\");\nconst seg_mod = @import(\"text-buffer-segment.zig\");\nconst iter_mod = @import(\"text-buffer-iterators.zig\");\nconst gp = @import(\"grapheme.zig\");\nconst utf8 = @import(\"utf8.zig\");\n\nconst logger = @import(\"logger.zig\");\n\nconst UnifiedTextBuffer = tb.UnifiedTextBuffer;\nconst RGBA = tb.RGBA;\nconst TextSelection = tb.TextSelection;\npub const WrapMode = tb.WrapMode;\nconst TextChunk = seg_mod.TextChunk;\nconst StyleSpan = tb.StyleSpan;\nconst GraphemeInfo = seg_mod.GraphemeInfo;\n\npub const TextBufferViewError = error{\n    OutOfMemory,\n};\n\n/// Viewport defines a rectangular window into the virtual line space\npub const Viewport = struct {\n    x: u32,\n    y: u32,\n    width: u32,\n    height: u32,\n};\n\npub const LineInfo = struct {\n    line_start_cols: []const u32,\n    line_width_cols: []const u32,\n    line_sources: []const u32,\n    line_wraps: []const u32,\n    line_width_cols_max: u32,\n};\n\npub const WrapInfo = struct {\n    line_first_vline: []const u32,\n    line_vline_counts: []const u32,\n};\n\n/// Output structure for virtual line calculation\npub const VirtualLineOutput = struct {\n    virtual_lines: *std.ArrayListUnmanaged(VirtualLine),\n    cached_line_starts: *std.ArrayListUnmanaged(u32),\n    cached_line_widths: *std.ArrayListUnmanaged(u32),\n    cached_line_sources: *std.ArrayListUnmanaged(u32),\n    cached_line_wrap_indices: *std.ArrayListUnmanaged(u32),\n    cached_line_first_vline: *std.ArrayListUnmanaged(u32),\n    cached_line_vline_counts: *std.ArrayListUnmanaged(u32),\n};\n\n/// Result from measuring dimensions without modifying cache\npub const MeasureResult = struct {\n    line_count: u32,\n    width_cols_max: u32,\n};\n\npub const VirtualLineSpanInfo = struct {\n    spans: []const StyleSpan,\n    source_line: usize,\n    col_offset: u32,\n};\n\npub const VirtualChunk = struct {\n    grapheme_start: u32,\n    width: u32,\n    // Direct reference to source chunk for rendering\n    chunk: *const TextChunk,\n};\n\npub const VirtualLine = struct {\n    chunks: std.ArrayListUnmanaged(VirtualChunk),\n    width_cols: u32,\n    col_offset: u32,\n    source_line: usize,\n    source_col_offset: u32,\n    is_truncated: bool,\n    ellipsis_pos: u32,\n    truncation_suffix_start: u32,\n\n    pub fn init() VirtualLine {\n        return .{\n            .chunks = .{},\n            .width_cols = 0,\n            .col_offset = 0,\n            .source_line = 0,\n            .source_col_offset = 0,\n            .is_truncated = false,\n            .ellipsis_pos = 0,\n            .truncation_suffix_start = 0,\n        };\n    }\n\n    pub fn deinit(self: *VirtualLine, allocator: Allocator) void {\n        self.chunks.deinit(allocator);\n    }\n};\n\npub const LocalSelection = struct {\n    anchorX: i32,\n    anchorY: i32,\n    focusX: i32,\n    focusY: i32,\n    isActive: bool,\n};\n\npub const TextBufferView = UnifiedTextBufferView;\n\npub const UnifiedTextBufferView = struct {\n    const Self = @This();\n\n    text_buffer: *UnifiedTextBuffer,\n    original_text_buffer: *UnifiedTextBuffer,\n    view_id: u32,\n    selection: ?TextSelection,\n    selection_anchor_offset: ?u32,\n    viewport: ?Viewport,\n    wrap_width: ?u32,\n    wrap_mode: WrapMode,\n    virtual_lines: std.ArrayListUnmanaged(VirtualLine),\n    virtual_lines_dirty: bool,\n    cached_line_starts: std.ArrayListUnmanaged(u32),\n    cached_line_widths: std.ArrayListUnmanaged(u32),\n    cached_line_sources: std.ArrayListUnmanaged(u32),\n    cached_line_wrap_indices: std.ArrayListUnmanaged(u32),\n    cached_line_first_vline: std.ArrayListUnmanaged(u32),\n    cached_line_vline_counts: std.ArrayListUnmanaged(u32),\n    global_allocator: Allocator,\n    virtual_lines_arena: *std.heap.ArenaAllocator,\n\n    /// Persistent arena for measureForDimensions. Each call resets it with\n    /// retain_capacity to avoid mmap/munmap churn during streaming.\n    measure_arena: std.heap.ArenaAllocator,\n    tab_indicator: ?u32,\n    tab_indicator_color: ?RGBA,\n    truncate: bool,\n    ellipsis_chunk: TextChunk,\n    ellipsis_mem_id: u8,\n\n    // Measurement cache for Yoga layout. Keyed by (buffer, epoch, width, wrap_mode).\n    // Using epoch instead of dirty flag prevents stale returns when unrelated\n    // code paths clear dirty (e.g., updateVirtualLines).\n    cached_measure_width: ?u32,\n    cached_measure_wrap_mode: WrapMode,\n    cached_measure_result: ?MeasureResult,\n    cached_measure_epoch: u64,\n    cached_measure_buffer: ?*UnifiedTextBuffer,\n\n    truncation_applied: bool,\n    truncation_epoch: u64,\n    truncation_viewport: ?Viewport,\n\n    pub fn init(global_allocator: Allocator, text_buffer: *UnifiedTextBuffer) TextBufferViewError!*Self {\n        const self = global_allocator.create(Self) catch return TextBufferViewError.OutOfMemory;\n        errdefer global_allocator.destroy(self);\n\n        const virtual_lines_internal_arena = global_allocator.create(std.heap.ArenaAllocator) catch return TextBufferViewError.OutOfMemory;\n        errdefer global_allocator.destroy(virtual_lines_internal_arena);\n        virtual_lines_internal_arena.* = std.heap.ArenaAllocator.init(global_allocator);\n\n        const view_id = text_buffer.registerView() catch return TextBufferViewError.OutOfMemory;\n\n        const ellipsis_text = \"...\";\n        const ellipsis_mem_id = text_buffer.registerMemBuffer(ellipsis_text, false) catch return TextBufferViewError.OutOfMemory;\n        const ellipsis_chunk = text_buffer.createChunk(ellipsis_mem_id, 0, 3);\n\n        self.* = .{\n            .text_buffer = text_buffer,\n            .original_text_buffer = text_buffer,\n            .view_id = view_id,\n            .selection = null,\n            .selection_anchor_offset = null,\n            .viewport = null,\n            .wrap_width = null,\n            .wrap_mode = .none,\n            .virtual_lines = .{},\n            .virtual_lines_dirty = true,\n            .cached_line_starts = .{},\n            .cached_line_widths = .{},\n            .cached_line_sources = .{},\n            .cached_line_wrap_indices = .{},\n            .cached_line_first_vline = .{},\n            .cached_line_vline_counts = .{},\n            .global_allocator = global_allocator,\n            .virtual_lines_arena = virtual_lines_internal_arena,\n            .measure_arena = std.heap.ArenaAllocator.init(global_allocator),\n            .tab_indicator = null,\n            .tab_indicator_color = null,\n            .truncate = false,\n            .ellipsis_chunk = ellipsis_chunk,\n            .ellipsis_mem_id = ellipsis_mem_id,\n            .cached_measure_width = null,\n            .cached_measure_wrap_mode = .none,\n            .cached_measure_result = null,\n            .cached_measure_epoch = 0,\n            .cached_measure_buffer = null,\n            .truncation_applied = false,\n            .truncation_epoch = 0,\n            .truncation_viewport = null,\n        };\n\n        return self;\n    }\n\n    /// IMPORTANT: Views must be destroyed BEFORE their associated TextBuffer.\n    /// Destroying the TextBuffer first will cause use-after-free when calling deinit.\n    /// The TypeScript wrappers enforce this order via the destroy() methods.\n    pub fn deinit(self: *Self) void {\n        self.original_text_buffer.unregisterView(self.view_id);\n        self.virtual_lines_arena.deinit();\n        self.global_allocator.destroy(self.virtual_lines_arena);\n        self.measure_arena.deinit();\n        self.global_allocator.destroy(self);\n    }\n\n    pub fn setViewport(self: *Self, vp: ?Viewport) void {\n        self.viewport = vp;\n\n        // If viewport has width, set wrap width (wrapping behavior depends on wrap_mode)\n        if (vp) |viewport| {\n            if (self.wrap_width != viewport.width) {\n                self.wrap_width = viewport.width;\n                self.virtual_lines_dirty = true;\n                self.truncation_applied = false;\n            }\n        } else {\n            self.truncation_applied = false;\n        }\n    }\n\n    pub fn getViewport(self: *const Self) ?Viewport {\n        return self.viewport;\n    }\n\n    // This is a convenience method that preserves existing offset\n    pub fn setViewportSize(self: *Self, width: u32, height: u32) void {\n        if (self.viewport) |vp| {\n            self.setViewport(Viewport{\n                .x = vp.x,\n                .y = vp.y,\n                .width = width,\n                .height = height,\n            });\n        } else {\n            self.setViewport(Viewport{\n                .x = 0,\n                .y = 0,\n                .width = width,\n                .height = height,\n            });\n        }\n    }\n\n    pub fn setWrapWidth(self: *Self, width: ?u32) void {\n        if (self.wrap_width != width) {\n            self.wrap_width = width;\n            self.virtual_lines_dirty = true;\n            self.truncation_applied = false;\n        }\n    }\n\n    pub fn setWrapMode(self: *Self, mode: WrapMode) void {\n        if (self.wrap_mode != mode) {\n            self.wrap_mode = mode;\n            self.virtual_lines_dirty = true;\n            self.truncation_applied = false;\n        }\n    }\n\n    fn calculateChunkFitWord(self: *const Self, chunk: *const TextChunk, char_offset_in_chunk: u32, max_width: u32) tb.ChunkFitResult {\n        if (max_width == 0) return .{ .char_count = 0, .width = 0 };\n\n        const total_width = @as(u32, chunk.width) - char_offset_in_chunk;\n        if (total_width == 0) return .{ .char_count = 0, .width = 0 };\n        if (total_width <= max_width) return .{ .char_count = total_width, .width = total_width };\n\n        const wrap_offsets = self.text_buffer.getWrapOffsetsFor(chunk) catch {\n            const fit_width = @min(max_width, total_width);\n            return .{ .char_count = fit_width, .width = fit_width };\n        };\n\n        var last_boundary: ?u32 = null;\n        var first_boundary: ?u32 = null;\n\n        for (wrap_offsets) |wrap_break| {\n            const offset = @as(u32, wrap_break.char_offset);\n            if (offset < char_offset_in_chunk) continue;\n\n            const local_offset = offset - char_offset_in_chunk;\n            if (local_offset >= total_width) break;\n\n            const width_to_boundary = local_offset + 1;\n            if (first_boundary == null) first_boundary = width_to_boundary;\n\n            if (width_to_boundary <= max_width) {\n                last_boundary = width_to_boundary;\n            } else break;\n        }\n\n        if (last_boundary) |width| return .{ .char_count = width, .width = width };\n\n        const line_width = self.wrap_width orelse max_width;\n        const needs_force_break = (first_boundary orelse total_width) > line_width;\n\n        if (needs_force_break) {\n            const fit_width = @min(max_width, total_width);\n            return .{ .char_count = fit_width, .width = fit_width };\n        }\n\n        return .{ .char_count = 0, .width = 0 };\n    }\n\n    pub fn updateVirtualLines(self: *Self) void {\n        const buffer_dirty = self.text_buffer.isViewDirty(self.view_id);\n        if (!self.virtual_lines_dirty and !buffer_dirty) return;\n\n        _ = self.virtual_lines_arena.reset(.free_all);\n        self.virtual_lines = .{};\n        self.cached_line_starts = .{};\n        self.cached_line_widths = .{};\n        self.cached_line_sources = .{};\n        self.cached_line_wrap_indices = .{};\n        self.cached_line_first_vline = .{};\n        self.cached_line_vline_counts = .{};\n        self.truncation_applied = false;\n        const virtual_allocator = self.virtual_lines_arena.allocator();\n\n        // Create output structure for the generic function\n        const output = VirtualLineOutput{\n            .virtual_lines = &self.virtual_lines,\n            .cached_line_starts = &self.cached_line_starts,\n            .cached_line_widths = &self.cached_line_widths,\n            .cached_line_sources = &self.cached_line_sources,\n            .cached_line_wrap_indices = &self.cached_line_wrap_indices,\n            .cached_line_first_vline = &self.cached_line_first_vline,\n            .cached_line_vline_counts = &self.cached_line_vline_counts,\n        };\n\n        // Call the generic calculation function\n        calculateVirtualLinesGeneric(\n            self.text_buffer,\n            self.wrap_mode,\n            self.wrap_width,\n            virtual_allocator,\n            output,\n        );\n\n        self.virtual_lines_dirty = false;\n        self.text_buffer.clearViewDirty(self.view_id);\n    }\n\n    pub fn getVirtualLineCount(self: *Self) u32 {\n        self.updateVirtualLines();\n        return @intCast(self.virtual_lines.items.len);\n    }\n\n    pub fn getVirtualLines(self: *Self) []const VirtualLine {\n        self.updateVirtualLines();\n\n        const all_vlines = self.virtual_lines.items;\n\n        if (self.truncate and self.viewport != null) {\n            self.ensureTruncation();\n        }\n\n        if (self.viewport) |vp| {\n            const start_idx = @min(vp.y, @as(u32, @intCast(all_vlines.len)));\n            const end_idx = @min(start_idx + vp.height, @as(u32, @intCast(all_vlines.len)));\n            return all_vlines[start_idx..end_idx];\n        }\n\n        return all_vlines;\n    }\n\n    pub fn getCachedLineInfo(self: *Self) LineInfo {\n        self.updateVirtualLines();\n\n        // If viewport is set, return only the visible lines' info\n        if (self.viewport) |vp| {\n            const start_idx = @min(vp.y, @as(u32, @intCast(self.cached_line_starts.items.len)));\n            const end_idx = @min(start_idx + vp.height, @as(u32, @intCast(self.cached_line_starts.items.len)));\n\n            const viewport_line_start_cols = self.cached_line_starts.items[start_idx..end_idx];\n            const viewport_line_width_cols = self.cached_line_widths.items[start_idx..end_idx];\n            const viewport_line_sources = self.cached_line_sources.items[start_idx..end_idx];\n            const viewport_line_wraps = self.cached_line_wrap_indices.items[start_idx..end_idx];\n\n            var width_cols_max: u32 = 0;\n            for (viewport_line_width_cols) |w| {\n                width_cols_max = @max(width_cols_max, w);\n            }\n\n            return LineInfo{\n                .line_start_cols = viewport_line_start_cols,\n                .line_width_cols = viewport_line_width_cols,\n                .line_sources = viewport_line_sources,\n                .line_wraps = viewport_line_wraps,\n                .line_width_cols_max = width_cols_max,\n            };\n        }\n\n        return LineInfo{\n            .line_start_cols = self.cached_line_starts.items,\n            .line_width_cols = self.cached_line_widths.items,\n            .line_sources = self.cached_line_sources.items,\n            .line_wraps = self.cached_line_wrap_indices.items,\n            .line_width_cols_max = self.text_buffer.lineWidthColsMax(),\n        };\n    }\n\n    pub fn getLogicalLineInfo(self: *Self) LineInfo {\n        self.updateVirtualLines();\n\n        return LineInfo{\n            .line_start_cols = self.cached_line_starts.items,\n            .line_width_cols = self.cached_line_widths.items,\n            .line_sources = self.cached_line_sources.items,\n            .line_wraps = self.cached_line_wrap_indices.items,\n            .line_width_cols_max = self.text_buffer.lineWidthColsMax(),\n        };\n    }\n\n    pub fn getWrapInfo(self: *Self) WrapInfo {\n        self.updateVirtualLines();\n        return WrapInfo{\n            .line_first_vline = self.cached_line_first_vline.items,\n            .line_vline_counts = self.cached_line_vline_counts.items,\n        };\n    }\n\n    pub fn findVisualLineIndex(self: *Self, logical_row: u32, logical_col: u32) u32 {\n        self.updateVirtualLines();\n\n        const vlines = self.virtual_lines.items;\n        if (vlines.len == 0) return 0;\n\n        const wrap_info = self.getWrapInfo();\n\n        // Clamp logical_row to valid range\n        const clamped_row = if (logical_row >= wrap_info.line_first_vline.len)\n            if (wrap_info.line_first_vline.len > 0) wrap_info.line_first_vline.len - 1 else 0\n        else\n            logical_row;\n\n        if (clamped_row >= wrap_info.line_first_vline.len) return 0;\n\n        const first_vline_idx = wrap_info.line_first_vline[clamped_row];\n        const vline_count = wrap_info.line_vline_counts[clamped_row];\n\n        if (vline_count == 0) return first_vline_idx;\n\n        var i: u32 = 0;\n        while (i < vline_count) : (i += 1) {\n            const vline_idx = first_vline_idx + i;\n            if (vline_idx >= vlines.len) break;\n\n            const vline = &vlines[vline_idx];\n            const vline_start_col = vline.source_col_offset;\n            const vline_end_col = vline_start_col + vline.width_cols;\n\n            const is_last_vline = (i == vline_count - 1);\n\n            // For the end check: use < for all lines except the last line where we use <=\n            // This ensures that a position exactly at vline_end_col goes to the NEXT line\n            // unless this is the last line (where there is no next line)\n            const end_check = if (is_last_vline) logical_col <= vline_end_col else logical_col < vline_end_col;\n\n            if (logical_col >= vline_start_col and end_check) {\n                return vline_idx;\n            }\n        }\n\n        // If not found, return last virtual line for this logical line\n        const last_vline_idx = first_vline_idx + vline_count - 1;\n        if (last_vline_idx < vlines.len) {\n            return last_vline_idx;\n        }\n\n        return first_vline_idx;\n    }\n\n    pub fn getPlainTextIntoBuffer(self: *const Self, out_buffer: []u8) usize {\n        return self.text_buffer.getPlainTextIntoBuffer(out_buffer);\n    }\n\n    pub fn getArenaAllocatedBytes(self: *const Self) usize {\n        return self.virtual_lines_arena.queryCapacity();\n    }\n\n    pub fn setSelection(self: *Self, start: u32, end: u32, bgColor: ?RGBA, fgColor: ?RGBA) void {\n        self.selection = TextSelection{\n            .start = start,\n            .end = end,\n            .bgColor = bgColor,\n            .fgColor = fgColor,\n        };\n    }\n\n    pub fn updateSelection(self: *Self, end: u32, bgColor: ?RGBA, fgColor: ?RGBA) void {\n        if (self.selection) |sel| {\n            self.selection = TextSelection{\n                .start = sel.start,\n                .end = end,\n                .bgColor = bgColor,\n                .fgColor = fgColor,\n            };\n        }\n    }\n\n    pub fn resetSelection(self: *Self) void {\n        self.selection = null;\n    }\n\n    pub fn getSelection(self: *const Self) ?TextSelection {\n        return self.selection;\n    }\n\n    pub fn getTextBuffer(self: *const Self) *UnifiedTextBuffer {\n        return self.text_buffer;\n    }\n\n    pub fn switchToBuffer(self: *Self, buffer: *UnifiedTextBuffer) void {\n        self.text_buffer = buffer;\n        self.virtual_lines_dirty = true;\n    }\n\n    pub fn switchToOriginalBuffer(self: *Self) void {\n        if (self.text_buffer != self.original_text_buffer) {\n            self.text_buffer = self.original_text_buffer;\n            self.virtual_lines_dirty = true;\n        }\n    }\n\n    pub fn setLocalSelection(self: *Self, anchorX: i32, anchorY: i32, focusX: i32, focusY: i32, bgColor: ?RGBA, fgColor: ?RGBA) bool {\n        self.updateVirtualLines();\n        if (self.truncate and self.viewport != null) {\n            self.ensureTruncation();\n        }\n\n        const anchor_above = anchorY < 0;\n        const focus_above = focusY < 0;\n        const max_y = @as(i32, @intCast(self.virtual_lines.items.len)) - 1;\n        const anchor_below = anchorY > max_y;\n        const focus_below = focusY > max_y;\n\n        if ((anchor_above and focus_above) or (anchor_below and focus_below)) {\n            const had_selection = self.selection != null;\n            self.selection = null;\n            self.selection_anchor_offset = null;\n            return had_selection;\n        }\n\n        const text_end_offset = self.getTextEndOffset();\n\n        const anchor_offset = if (anchor_above or anchorX < 0)\n            0\n        else if (anchor_below)\n            text_end_offset\n        else\n            self.coordsToCharOffset(anchorX, anchorY) orelse {\n                const had_selection = self.selection != null;\n                self.selection = null;\n                self.selection_anchor_offset = null;\n                return had_selection;\n            };\n\n        const focus_offset = if (focus_above or focusX < 0)\n            0\n        else if (focus_below)\n            text_end_offset\n        else\n            self.coordsToCharOffset(focusX, focusY) orelse {\n                const had_selection = self.selection != null;\n                self.selection = null;\n                self.selection_anchor_offset = null;\n                return had_selection;\n            };\n\n        self.selection_anchor_offset = anchor_offset;\n\n        const new_start = @min(anchor_offset, focus_offset);\n        const new_end = @max(anchor_offset, focus_offset);\n\n        // Always store selection, even if zero-width, to preserve anchor for updateLocalSelection\n        const new_selection = TextSelection{\n            .start = new_start,\n            .end = new_end,\n            .bgColor = bgColor,\n            .fgColor = fgColor,\n        };\n\n        const selection_changed = if (self.selection) |old_sel|\n            old_sel.start != new_selection.start or old_sel.end != new_selection.end\n        else\n            true;\n\n        self.selection = new_selection;\n        return selection_changed;\n    }\n\n    pub fn updateLocalSelection(self: *Self, anchorX: i32, anchorY: i32, focusX: i32, focusY: i32, bgColor: ?RGBA, fgColor: ?RGBA) bool {\n        if (self.selection_anchor_offset) |_| {\n            return self.updateLocalSelectionFocusOnly(focusX, focusY, bgColor, fgColor);\n        } else {\n            return self.setLocalSelection(anchorX, anchorY, focusX, focusY, bgColor, fgColor);\n        }\n    }\n\n    fn updateLocalSelectionFocusOnly(self: *Self, focusX: i32, focusY: i32, bgColor: ?RGBA, fgColor: ?RGBA) bool {\n        const anchor_offset = self.selection_anchor_offset orelse return false;\n\n        self.updateVirtualLines();\n        if (self.truncate and self.viewport != null) {\n            self.applyTruncation();\n        }\n\n        const focus_above = focusY < 0;\n        const max_y = @as(i32, @intCast(self.virtual_lines.items.len)) - 1;\n        const focus_below = focusY > max_y;\n\n        const text_end_offset = self.getTextEndOffset();\n\n        const focus_col_offset = if (focus_above or focusX < 0)\n            0\n        else if (focus_below)\n            text_end_offset\n        else\n            self.coordsToCharOffset(focusX, focusY) orelse return false;\n\n        const new_start = @min(anchor_offset, focus_col_offset);\n        var new_end = @max(anchor_offset, focus_col_offset);\n\n        if (focus_col_offset < anchor_offset) {\n            new_end = @min(new_end + 1, text_end_offset);\n        }\n\n        self.selection = TextSelection{\n            .start = new_start,\n            .end = new_end,\n            .bgColor = bgColor,\n            .fgColor = fgColor,\n        };\n\n        return true;\n    }\n\n    pub fn resetLocalSelection(self: *Self) void {\n        self.selection = null;\n        self.selection_anchor_offset = null;\n    }\n\n    fn getTextEndOffset(self: *Self) u32 {\n        if (self.truncate and self.viewport != null) {\n            self.ensureTruncation();\n        }\n\n        if (self.virtual_lines.items.len == 0) return 0;\n        const last_line_idx = self.virtual_lines.items.len - 1;\n        const last_vline = &self.virtual_lines.items[last_line_idx];\n\n        if (last_vline.is_truncated) {\n            return last_vline.col_offset + last_vline.truncation_suffix_start + (last_vline.width_cols - last_vline.ellipsis_pos - 3);\n        }\n\n        return last_vline.col_offset + last_vline.width_cols;\n    }\n\n    fn coordsToCharOffset(self: *Self, x: i32, y: i32) ?u32 {\n        self.updateVirtualLines();\n        if (self.truncate and self.viewport != null) {\n            self.ensureTruncation();\n        }\n\n        const y_offset: i32 = if (self.viewport) |vp| @intCast(vp.y) else 0;\n        const x_offset: i32 = if (self.viewport) |vp|\n            (if (self.wrap_mode == .none) @intCast(vp.x) else 0)\n        else\n            0;\n\n        if (self.virtual_lines.items.len == 0) {\n            return 0;\n        }\n\n        const abs_y = y + y_offset;\n        const abs_x = x + x_offset;\n\n        const clamped_y = @max(0, @min(abs_y, @as(i32, @intCast(self.virtual_lines.items.len)) - 1));\n\n        const vline_idx: usize = @intCast(clamped_y);\n        const vline = &self.virtual_lines.items[vline_idx];\n        const lineStart = vline.col_offset;\n        const lineWidth = vline.width_cols;\n\n        var localX = @max(0, @min(abs_x, @as(i32, @intCast(lineWidth))));\n\n        if (vline.is_truncated) {\n            const ellipsis_width: u32 = 3;\n            const localX_u32: u32 = @intCast(localX);\n\n            if (localX_u32 >= vline.ellipsis_pos and localX_u32 < vline.ellipsis_pos + ellipsis_width) {\n                localX = @intCast(vline.ellipsis_pos);\n            } else if (localX_u32 >= vline.ellipsis_pos + ellipsis_width) {\n                const suffix_offset = localX_u32 - vline.ellipsis_pos - ellipsis_width;\n                localX = @intCast(vline.truncation_suffix_start + suffix_offset);\n            }\n        }\n\n        const result = lineStart + @as(u32, @intCast(localX));\n\n        return result;\n    }\n\n    /// Pack selection info into u64 for efficient passing\n    /// Returns 0xFFFF_FFFF_FFFF_FFFF for no selection or zero-width selection\n    pub fn packSelectionInfo(self: *const Self) u64 {\n        if (self.selection) |sel| {\n            if (sel.start == sel.end) {\n                return 0xFFFF_FFFF_FFFF_FFFF;\n            }\n            return (@as(u64, sel.start) << 32) | @as(u64, sel.end);\n        } else {\n            return 0xFFFF_FFFF_FFFF_FFFF;\n        }\n    }\n\n    /// Get selected text into buffer - using efficient single-pass API\n    pub fn getSelectedTextIntoBuffer(self: *Self, out_buffer: []u8) usize {\n        const selection = self.selection orelse return 0;\n        if (selection.start == selection.end) return 0;\n        return self.text_buffer.getTextRange(selection.start, selection.end, out_buffer);\n    }\n\n    pub fn getVirtualLineSpans(self: *const Self, vline_idx: usize) VirtualLineSpanInfo {\n        if (vline_idx >= self.virtual_lines.items.len) {\n            return VirtualLineSpanInfo{ .spans = &[_]StyleSpan{}, .source_line = 0, .col_offset = 0 };\n        }\n\n        const vline = &self.virtual_lines.items[vline_idx];\n        const spans = self.text_buffer.getLineSpans(vline.source_line);\n\n        return VirtualLineSpanInfo{\n            .spans = spans,\n            .source_line = vline.source_line,\n            .col_offset = vline.source_col_offset,\n        };\n    }\n\n    pub fn setTabIndicator(self: *Self, indicator: ?u32) void {\n        self.tab_indicator = indicator;\n    }\n\n    pub fn getTabIndicator(self: *const Self) ?u32 {\n        return self.tab_indicator;\n    }\n\n    pub fn setTabIndicatorColor(self: *Self, color: ?RGBA) void {\n        self.tab_indicator_color = color;\n    }\n\n    pub fn getTabIndicatorColor(self: *const Self) ?RGBA {\n        return self.tab_indicator_color;\n    }\n\n    pub fn setTruncate(self: *Self, truncate: bool) void {\n        if (self.truncate != truncate) {\n            self.truncate = truncate;\n            self.virtual_lines_dirty = true;\n            self.truncation_applied = false;\n        }\n    }\n\n    pub fn getTruncate(self: *const Self) bool {\n        return self.truncate;\n    }\n\n    fn ensureTruncation(self: *Self) void {\n        if (!self.truncate or self.viewport == null) return;\n\n        const epoch = self.text_buffer.getContentEpoch();\n        if (self.truncation_applied and self.truncation_epoch == epoch and\n            self.truncation_viewport != null and self.viewport != null and\n            self.truncation_viewport.?.x == self.viewport.?.x and\n            self.truncation_viewport.?.y == self.viewport.?.y and\n            self.truncation_viewport.?.width == self.viewport.?.width and\n            self.truncation_viewport.?.height == self.viewport.?.height)\n        {\n            return;\n        }\n\n        self.applyTruncation();\n        self.truncation_applied = true;\n        self.truncation_epoch = epoch;\n        self.truncation_viewport = self.viewport;\n    }\n\n    fn applyTruncation(self: *Self) void {\n        const vp = self.viewport orelse return;\n        if (vp.width == 0) return;\n\n        const ellipsis_width: u32 = 3;\n\n        for (self.virtual_lines.items) |*vline| {\n            if (vline.width_cols <= vp.width) continue;\n\n            if (vp.width <= ellipsis_width) {\n                vline.chunks.clearRetainingCapacity();\n                vline.width_cols = 0;\n                vline.is_truncated = true;\n                vline.ellipsis_pos = 0;\n                vline.truncation_suffix_start = vline.width_cols;\n                continue;\n            }\n\n            const available_width = vp.width - ellipsis_width;\n            const prefix_width = available_width / 2;\n            const suffix_width = available_width - prefix_width;\n\n            var new_chunks: std.ArrayListUnmanaged(VirtualChunk) = .{};\n\n            var prefix_accumulated: u32 = 0;\n            for (vline.chunks.items) |chunk| {\n                if (prefix_accumulated >= prefix_width) break;\n\n                const space_left = prefix_width - prefix_accumulated;\n                if (chunk.width <= space_left) {\n                    new_chunks.append(self.virtual_lines_arena.allocator(), chunk) catch return;\n                    prefix_accumulated += chunk.width;\n                } else {\n                    var partial = chunk;\n                    partial.width = space_left;\n                    new_chunks.append(self.virtual_lines_arena.allocator(), partial) catch return;\n                    prefix_accumulated += space_left;\n                    break;\n                }\n            }\n\n            new_chunks.append(self.virtual_lines_arena.allocator(), VirtualChunk{\n                .grapheme_start = 0,\n                .width = ellipsis_width,\n                .chunk = &self.ellipsis_chunk,\n            }) catch return;\n\n            const suffix_start_pos = vline.width_cols - suffix_width;\n\n            var pos_accumulated: u32 = 0;\n            for (vline.chunks.items) |chunk| {\n                const chunk_end = pos_accumulated + chunk.width;\n\n                if (chunk_end <= suffix_start_pos) {\n                    pos_accumulated += chunk.width;\n                    continue;\n                }\n\n                if (pos_accumulated >= suffix_start_pos) {\n                    new_chunks.append(self.virtual_lines_arena.allocator(), chunk) catch return;\n                } else {\n                    const offset_in_chunk = suffix_start_pos - pos_accumulated;\n                    var partial = chunk;\n                    partial.grapheme_start += offset_in_chunk;\n                    partial.width = chunk.width - offset_in_chunk;\n                    new_chunks.append(self.virtual_lines_arena.allocator(), partial) catch return;\n                }\n\n                pos_accumulated += chunk.width;\n            }\n\n            vline.chunks.clearRetainingCapacity();\n            vline.chunks.appendSlice(self.virtual_lines_arena.allocator(), new_chunks.items) catch return;\n            vline.width_cols = vp.width;\n            vline.is_truncated = true;\n            vline.ellipsis_pos = prefix_width;\n            vline.truncation_suffix_start = suffix_start_pos;\n        }\n    }\n\n    /// Measure dimensions for given width/height WITHOUT modifying virtual lines cache\n    /// This is useful for Yoga measure functions that need to know dimensions without committing changes\n    /// Special case: width=0 or wrap_mode=.none means \"measure intrinsic/max-content width\" (no wrapping)\n    pub fn measureForDimensions(self: *Self, width: u32, height: u32) TextBufferViewError!MeasureResult {\n        _ = height; // Height is for future use, currently only width affects layout\n        const epoch = self.text_buffer.getContentEpoch();\n        if (self.cached_measure_result) |result| {\n            if (self.cached_measure_epoch == epoch and self.cached_measure_buffer == self.text_buffer) {\n                if (self.cached_measure_width) |cached_width| {\n                    if (cached_width == width and self.cached_measure_wrap_mode == self.wrap_mode) {\n                        return result;\n                    }\n                }\n            }\n        }\n\n        // No-wrap path avoids allocations by using marker-based line widths.\n        if (width == 0 or self.wrap_mode == .none) {\n            const line_count = self.text_buffer.lineCount();\n            var width_cols_max: u32 = 0;\n            var row: u32 = 0;\n            while (row < line_count) : (row += 1) {\n                width_cols_max = @max(width_cols_max, self.text_buffer.lineWidthAt(row));\n            }\n\n            const result = MeasureResult{\n                .line_count = line_count,\n                .width_cols_max = width_cols_max,\n            };\n\n            self.cached_measure_width = width;\n            self.cached_measure_wrap_mode = self.wrap_mode;\n            self.cached_measure_result = result;\n            self.cached_measure_epoch = epoch;\n            self.cached_measure_buffer = self.text_buffer;\n\n            return result;\n        }\n\n        // Reuse arena capacity to avoid allocation overhead during streaming.\n        _ = self.measure_arena.reset(.retain_capacity);\n        const measure_allocator = self.measure_arena.allocator();\n\n        // Create temporary output structures\n        var temp_virtual_lines = std.ArrayListUnmanaged(VirtualLine){};\n        var temp_line_starts = std.ArrayListUnmanaged(u32){};\n        var temp_line_widths = std.ArrayListUnmanaged(u32){};\n        var temp_line_sources = std.ArrayListUnmanaged(u32){};\n        var temp_line_wrap_indices = std.ArrayListUnmanaged(u32){};\n        var temp_line_first_vline = std.ArrayListUnmanaged(u32){};\n        var temp_line_vline_counts = std.ArrayListUnmanaged(u32){};\n\n        const output = VirtualLineOutput{\n            .virtual_lines = &temp_virtual_lines,\n            .cached_line_starts = &temp_line_starts,\n            .cached_line_widths = &temp_line_widths,\n            .cached_line_sources = &temp_line_sources,\n            .cached_line_wrap_indices = &temp_line_wrap_indices,\n            .cached_line_first_vline = &temp_line_first_vline,\n            .cached_line_vline_counts = &temp_line_vline_counts,\n        };\n\n        // Use width for wrap calculation\n        const wrap_width_for_measure = if (self.wrap_mode != .none and width > 0) width else null;\n\n        // Call generic calculation with temporary structures\n        calculateVirtualLinesGeneric(\n            self.text_buffer,\n            self.wrap_mode,\n            wrap_width_for_measure,\n            measure_allocator,\n            output,\n        );\n\n        // Calculate max width from temp structures\n        var width_cols_max: u32 = 0;\n        for (temp_line_widths.items) |w| {\n            width_cols_max = @max(width_cols_max, w);\n        }\n\n        const result = MeasureResult{\n            .line_count = @intCast(temp_virtual_lines.items.len),\n            .width_cols_max = width_cols_max,\n        };\n\n        self.cached_measure_width = width;\n        self.cached_measure_wrap_mode = self.wrap_mode;\n        self.cached_measure_result = result;\n        self.cached_measure_epoch = epoch;\n        self.cached_measure_buffer = self.text_buffer;\n\n        return result;\n    }\n\n    /// Generic virtual line calculation that writes to provided output structures\n    fn calculateVirtualLinesGeneric(\n        text_buffer: *UnifiedTextBuffer,\n        wrap_mode: WrapMode,\n        wrap_width: ?u32,\n        allocator: Allocator,\n        output: VirtualLineOutput,\n    ) void {\n        if (wrap_mode == .none or wrap_width == null) {\n            // No wrapping - create 1:1 mapping to real lines\n            const Context = struct {\n                text_buffer: *UnifiedTextBuffer,\n                allocator: Allocator,\n                output: VirtualLineOutput,\n                current_vline: ?VirtualLine = null,\n\n                fn segment_callback(ctx_ptr: *anyopaque, line_idx: u32, chunk: *const TextChunk, _: u32) void {\n                    _ = line_idx;\n                    const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n\n                    if (ctx.current_vline) |*vline| {\n                        vline.chunks.append(ctx.allocator, VirtualChunk{\n                            .grapheme_start = 0,\n                            .width = chunk.width,\n                            .chunk = chunk,\n                        }) catch {};\n                    }\n                }\n\n                fn line_end_callback(ctx_ptr: *anyopaque, line_info: iter_mod.LineInfo) void {\n                    const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n\n                    const first_vline_idx: u32 = @intCast(ctx.output.virtual_lines.items.len);\n                    ctx.output.cached_line_first_vline.append(ctx.allocator, first_vline_idx) catch {};\n                    ctx.output.cached_line_vline_counts.append(ctx.allocator, 1) catch {};\n\n                    var vline = if (ctx.current_vline) |v| v else VirtualLine.init();\n                    vline.width_cols = line_info.width_cols;\n                    vline.col_offset = line_info.col_offset;\n                    vline.source_line = line_info.line_idx;\n                    vline.source_col_offset = 0;\n\n                    ctx.output.virtual_lines.append(ctx.allocator, vline) catch {};\n                    ctx.output.cached_line_starts.append(ctx.allocator, vline.col_offset) catch {};\n                    ctx.output.cached_line_widths.append(ctx.allocator, vline.width_cols) catch {};\n                    ctx.output.cached_line_sources.append(ctx.allocator, @intCast(line_info.line_idx)) catch {};\n                    ctx.output.cached_line_wrap_indices.append(ctx.allocator, 0) catch {};\n\n                    ctx.current_vline = VirtualLine.init();\n                }\n            };\n\n            var ctx = Context{\n                .text_buffer = text_buffer,\n                .allocator = allocator,\n                .output = output,\n                .current_vline = VirtualLine.init(),\n            };\n\n            text_buffer.walkLinesAndSegments(&ctx, Context.segment_callback, Context.line_end_callback);\n        } else {\n            const wrap_w = wrap_width.?;\n\n            const WrapContext = struct {\n                text_buffer: *UnifiedTextBuffer,\n                allocator: Allocator,\n                output: VirtualLineOutput,\n                wrap_mode: WrapMode,\n                wrap_w: u32,\n                global_char_offset: u32 = 0,\n                line_idx: u32 = 0,\n                line_col_offset: u32 = 0,\n                line_position: u32 = 0,\n                current_vline: VirtualLine = VirtualLine.init(),\n                chunk_idx_in_line: u32 = 0,\n                current_line_first_vline_idx: u32 = 0,\n                current_line_vline_count: u32 = 0,\n\n                last_wrap_chunk_count: u32 = 0,\n                last_wrap_line_position: u32 = 0,\n                last_wrap_global_offset: u32 = 0,\n\n                fn commitVirtualLine(wctx: *@This()) void {\n                    wctx.current_vline.width_cols = wctx.line_position;\n                    wctx.current_vline.source_line = wctx.line_idx;\n                    wctx.current_vline.source_col_offset = wctx.line_col_offset;\n                    wctx.output.virtual_lines.append(wctx.allocator, wctx.current_vline) catch {};\n                    wctx.output.cached_line_starts.append(wctx.allocator, wctx.current_vline.col_offset) catch {};\n                    wctx.output.cached_line_widths.append(wctx.allocator, wctx.current_vline.width_cols) catch {};\n                    wctx.output.cached_line_sources.append(wctx.allocator, wctx.line_idx) catch {};\n                    wctx.output.cached_line_wrap_indices.append(wctx.allocator, wctx.current_line_vline_count) catch {};\n\n                    wctx.current_line_vline_count += 1;\n\n                    wctx.line_col_offset += wctx.line_position;\n                    wctx.current_vline = VirtualLine.init();\n                    wctx.current_vline.col_offset = wctx.global_char_offset;\n                    wctx.line_position = 0;\n\n                    wctx.last_wrap_chunk_count = 0;\n                    wctx.last_wrap_line_position = 0;\n                    wctx.last_wrap_global_offset = 0;\n                }\n\n                fn addVirtualChunk(wctx: *@This(), chunk: *const TextChunk, _: u32, start: u32, width_param: u32) void {\n                    wctx.current_vline.chunks.append(wctx.allocator, VirtualChunk{\n                        .grapheme_start = start,\n                        .width = width_param,\n                        .chunk = chunk,\n                    }) catch {};\n                    wctx.global_char_offset += width_param;\n                    wctx.line_position += width_param;\n                }\n\n                fn segment_callback(ctx_ptr: *anyopaque, _: u32, chunk: *const TextChunk, chunk_idx_in_line: u32) void {\n                    const wctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n                    wctx.chunk_idx_in_line = chunk_idx_in_line;\n\n                    if (wctx.wrap_mode == .word) {\n                        const chunk_bytes = chunk.getBytes(wctx.text_buffer.memRegistry());\n                        const wrap_offsets = wctx.text_buffer.getWrapOffsetsFor(chunk) catch &[_]utf8.WrapBreak{};\n                        const is_ascii_only = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n                        const graphemes: []const GraphemeInfo = if (is_ascii_only)\n                            &[_]GraphemeInfo{}\n                        else\n                            chunk.getGraphemes(wctx.text_buffer.memRegistry(), wctx.text_buffer.getAllocator(), wctx.text_buffer.tabWidth(), wctx.text_buffer.widthMethod()) catch &[_]GraphemeInfo{};\n                        var grapheme_idx: usize = 0;\n                        var col_delta: i64 = 0;\n\n                        // char_offset tracks COLUMN position within the chunk (not grapheme count)\n                        // chunk.width is also in columns. The loop processes the chunk column by column.\n                        var char_offset: u32 = 0; // Column offset within chunk\n                        var byte_offset: u32 = 0;\n                        var wrap_idx: usize = 0;\n\n                        while (char_offset < chunk.width) {\n                            const remaining_in_chunk = chunk.width - char_offset;\n                            const remaining_on_line = if (wctx.line_position < wctx.wrap_w) wctx.wrap_w - wctx.line_position else 0;\n\n                            var last_wrap_that_fits: ?u32 = null;\n                            var saved_wrap_idx = wrap_idx;\n                            while (wrap_idx < wrap_offsets.len) : (wrap_idx += 1) {\n                                const wrap_break = wrap_offsets[wrap_idx];\n\n                                const break_info = iter_mod.charOffsetToColumn(wrap_break.char_offset, graphemes, &grapheme_idx, &col_delta);\n                                const break_col = break_info.col;\n\n                                // Skip breaks that are before our current column position in the chunk\n                                if (break_col < char_offset) continue;\n\n                                // width_to_boundary: columns needed to reach and include this break\n                                // break_col is the column where the break character starts (relative to chunk)\n                                // char_offset is our current column position (relative to chunk)\n                                // To include the break character, we need: break_col - char_offset + width\n                                const width_to_boundary = break_col - char_offset + break_info.width;\n                                if (width_to_boundary > remaining_on_line or width_to_boundary > remaining_in_chunk) {\n                                    break;\n                                }\n                                last_wrap_that_fits = width_to_boundary;\n                                saved_wrap_idx = wrap_idx + 1;\n                            }\n                            wrap_idx = saved_wrap_idx;\n\n                            var to_add: u32 = 0;\n                            var has_wrap_after: bool = false;\n\n                            if (remaining_in_chunk <= remaining_on_line) {\n                                if (last_wrap_that_fits) |boundary_w| {\n                                    const would_fill_line = wctx.line_position + remaining_in_chunk >= wctx.wrap_w;\n                                    if (would_fill_line and boundary_w < remaining_in_chunk) {\n                                        to_add = boundary_w;\n                                        has_wrap_after = true;\n                                    } else {\n                                        to_add = remaining_in_chunk;\n                                        has_wrap_after = true;\n                                    }\n                                } else {\n                                    to_add = remaining_in_chunk;\n                                }\n                            } else if (last_wrap_that_fits) |boundary_w| {\n                                to_add = boundary_w;\n                                has_wrap_after = true;\n                            } else if (wctx.line_position == 0) {\n                                // Use tracked byte_offset instead of recalculating from scratch (avoids O(n²))\n                                const remaining_bytes = chunk_bytes[byte_offset..];\n                                const wrap_result = utf8.findWrapPosByWidth(remaining_bytes, remaining_on_line, wctx.text_buffer.tabWidth(), is_ascii_only, wctx.text_buffer.widthMethod());\n                                to_add = wrap_result.columns_used;\n                                byte_offset += wrap_result.byte_offset;\n                                if (to_add == 0) {\n                                    to_add = 1;\n                                    const single_result = utf8.findWrapPosByWidth(remaining_bytes, 1, wctx.text_buffer.tabWidth(), is_ascii_only, wctx.text_buffer.widthMethod());\n                                    byte_offset += single_result.byte_offset;\n                                }\n                            } else if (wctx.last_wrap_chunk_count > 0 and\n                                wctx.last_wrap_chunk_count <= wctx.current_vline.chunks.items.len)\n                            {\n                                var accumulated_width: u32 = 0;\n                                for (wctx.current_vline.chunks.items[0..wctx.last_wrap_chunk_count]) |vchunk| {\n                                    accumulated_width += vchunk.width;\n                                }\n\n                                const chunks_after_wrap = wctx.current_vline.chunks.items[wctx.last_wrap_chunk_count..];\n                                var chunks_to_move_count = chunks_after_wrap.len;\n                                var split_chunk: ?VirtualChunk = null;\n\n                                if (accumulated_width > wctx.last_wrap_line_position) {\n                                    const last_chunk_idx = wctx.last_wrap_chunk_count - 1;\n                                    const last_chunk = wctx.current_vline.chunks.items[last_chunk_idx];\n                                    const overhang = accumulated_width - wctx.last_wrap_line_position;\n\n                                    split_chunk = VirtualChunk{\n                                        .grapheme_start = last_chunk.grapheme_start + last_chunk.width - overhang,\n                                        .width = overhang,\n                                        .chunk = last_chunk.chunk,\n                                    };\n\n                                    wctx.current_vline.chunks.items[last_chunk_idx].width -= overhang;\n\n                                    chunks_to_move_count += 1;\n                                }\n\n                                const saved_chunks_result = wctx.allocator.alloc(VirtualChunk, chunks_to_move_count);\n                                if (saved_chunks_result) |saved_chunks| {\n                                    var saved_idx: usize = 0;\n\n                                    if (split_chunk) |sc| {\n                                        saved_chunks[saved_idx] = sc;\n                                        saved_idx += 1;\n                                    }\n\n                                    @memcpy(saved_chunks[saved_idx..], chunks_after_wrap);\n\n                                    wctx.line_position = wctx.last_wrap_line_position;\n                                    wctx.global_char_offset = wctx.last_wrap_global_offset;\n                                    wctx.current_vline.chunks.items.len = wctx.last_wrap_chunk_count;\n\n                                    commitVirtualLine(wctx);\n\n                                    for (saved_chunks) |vchunk| {\n                                        wctx.current_vline.chunks.append(wctx.allocator, vchunk) catch {};\n                                        wctx.global_char_offset += vchunk.width;\n                                        wctx.line_position += vchunk.width;\n                                    }\n                                } else |_| {\n                                    commitVirtualLine(wctx);\n                                }\n\n                                continue;\n                            } else {\n                                commitVirtualLine(wctx);\n                                if (char_offset > 0) {\n                                    const pos_result = utf8.findPosByWidth(chunk_bytes, char_offset, wctx.text_buffer.tabWidth(), is_ascii_only, false, wctx.text_buffer.widthMethod());\n                                    byte_offset = pos_result.byte_offset;\n                                }\n                                const remaining_bytes = chunk_bytes[byte_offset..];\n                                const wrap_result = utf8.findWrapPosByWidth(remaining_bytes, wctx.wrap_w, wctx.text_buffer.tabWidth(), is_ascii_only, wctx.text_buffer.widthMethod());\n                                to_add = wrap_result.columns_used;\n                                byte_offset += wrap_result.byte_offset;\n                                if (to_add == 0) {\n                                    to_add = 1;\n                                    const single_result = utf8.findWrapPosByWidth(remaining_bytes, 1, wctx.text_buffer.tabWidth(), is_ascii_only, wctx.text_buffer.widthMethod());\n                                    byte_offset += single_result.byte_offset;\n                                }\n                            }\n\n                            if (to_add > 0) {\n                                const position_before_add = wctx.line_position;\n                                const offset_before_add = wctx.global_char_offset;\n\n                                addVirtualChunk(wctx, chunk, chunk_idx_in_line, char_offset, to_add);\n                                char_offset += to_add;\n\n                                if (has_wrap_after) {\n                                    const wrap_pos_in_added = if (last_wrap_that_fits) |boundary_w|\n                                        @min(boundary_w, to_add)\n                                    else\n                                        to_add;\n\n                                    wctx.last_wrap_chunk_count = @intCast(wctx.current_vline.chunks.items.len);\n                                    wctx.last_wrap_line_position = position_before_add + wrap_pos_in_added;\n                                    wctx.last_wrap_global_offset = offset_before_add + wrap_pos_in_added;\n                                }\n\n                                if (wctx.line_position >= wctx.wrap_w and char_offset < chunk.width) {\n                                    if (has_wrap_after or wctx.last_wrap_chunk_count > 0) {\n                                        commitVirtualLine(wctx);\n                                    }\n                                }\n                            }\n                        }\n                    } else {\n                        const chunk_bytes = chunk.getBytes(wctx.text_buffer.memRegistry());\n                        const is_ascii_only = (chunk.flags & TextChunk.Flags.ASCII_ONLY) != 0;\n                        var byte_offset: usize = 0;\n                        var char_offset: u32 = 0;\n\n                        while (char_offset < chunk.width) {\n                            const remaining_width = if (wctx.line_position < wctx.wrap_w) wctx.wrap_w - wctx.line_position else 0;\n\n                            if (remaining_width == 0) {\n                                if (wctx.line_position > 0) {\n                                    commitVirtualLine(wctx);\n                                    continue;\n                                }\n                                const remaining_bytes = chunk_bytes[byte_offset..];\n                                const force_result = utf8.findWrapPosByWidth(remaining_bytes, 1, wctx.text_buffer.tabWidth(), is_ascii_only, wctx.text_buffer.widthMethod());\n                                if (force_result.grapheme_count > 0) {\n                                    addVirtualChunk(wctx, chunk, chunk_idx_in_line, char_offset, force_result.columns_used);\n                                    char_offset += force_result.columns_used;\n                                    byte_offset += force_result.byte_offset;\n                                } else {\n                                    break;\n                                }\n                                continue;\n                            }\n\n                            const remaining_bytes = chunk_bytes[byte_offset..];\n                            const wrap_result = utf8.findWrapPosByWidth(\n                                remaining_bytes,\n                                remaining_width,\n                                wctx.text_buffer.tabWidth(),\n                                is_ascii_only,\n                                wctx.text_buffer.widthMethod(),\n                            );\n\n                            if (wrap_result.grapheme_count == 0) {\n                                if (wctx.line_position > 0) {\n                                    commitVirtualLine(wctx);\n                                    continue;\n                                }\n                                const force_result = utf8.findWrapPosByWidth(remaining_bytes, 1000, wctx.text_buffer.tabWidth(), is_ascii_only, wctx.text_buffer.widthMethod());\n                                if (force_result.grapheme_count > 0) {\n                                    addVirtualChunk(wctx, chunk, chunk_idx_in_line, char_offset, force_result.columns_used);\n                                    char_offset += force_result.columns_used;\n                                    byte_offset += force_result.byte_offset;\n                                    if (char_offset < chunk.width) {\n                                        commitVirtualLine(wctx);\n                                    }\n                                }\n                                break;\n                            }\n\n                            addVirtualChunk(wctx, chunk, chunk_idx_in_line, char_offset, wrap_result.columns_used);\n                            char_offset += wrap_result.columns_used;\n                            byte_offset += wrap_result.byte_offset;\n\n                            if (wctx.line_position >= wctx.wrap_w and char_offset < chunk.width) {\n                                commitVirtualLine(wctx);\n                            }\n                        }\n                    }\n                }\n\n                fn line_end_callback(ctx_ptr: *anyopaque, line_info: iter_mod.LineInfo) void {\n                    const wctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n\n                    if (wctx.current_vline.chunks.items.len > 0 or line_info.width_cols == 0) {\n                        wctx.current_vline.width_cols = wctx.line_position;\n                        wctx.current_vline.source_line = wctx.line_idx;\n                        wctx.current_vline.source_col_offset = wctx.line_col_offset;\n                        wctx.output.virtual_lines.append(wctx.allocator, wctx.current_vline) catch {};\n                        wctx.output.cached_line_starts.append(wctx.allocator, wctx.current_vline.col_offset) catch {};\n                        wctx.output.cached_line_widths.append(wctx.allocator, wctx.current_vline.width_cols) catch {};\n                        wctx.output.cached_line_sources.append(wctx.allocator, wctx.line_idx) catch {};\n                        wctx.output.cached_line_wrap_indices.append(wctx.allocator, wctx.current_line_vline_count) catch {};\n                        wctx.current_line_vline_count += 1;\n                    }\n\n                    wctx.output.cached_line_first_vline.append(wctx.allocator, wctx.current_line_first_vline_idx) catch {};\n                    wctx.output.cached_line_vline_counts.append(wctx.allocator, wctx.current_line_vline_count) catch {};\n\n                    wctx.global_char_offset += 1;\n\n                    wctx.line_idx += 1;\n                    wctx.line_col_offset = 0;\n                    wctx.line_position = 0;\n                    wctx.current_vline = VirtualLine.init();\n                    wctx.current_vline.col_offset = wctx.global_char_offset;\n                    wctx.last_wrap_chunk_count = 0;\n                    wctx.last_wrap_line_position = 0;\n                    wctx.last_wrap_global_offset = 0;\n                    wctx.chunk_idx_in_line = 0;\n                    wctx.current_line_first_vline_idx = @intCast(wctx.output.virtual_lines.items.len);\n                    wctx.current_line_vline_count = 0;\n                }\n            };\n\n            var wrap_ctx = WrapContext{\n                .text_buffer = text_buffer,\n                .allocator = allocator,\n                .output = output,\n                .wrap_mode = wrap_mode,\n                .wrap_w = wrap_w,\n            };\n\n            text_buffer.walkLinesAndSegments(&wrap_ctx, WrapContext.segment_callback, WrapContext.line_end_callback);\n        }\n    }\n};\n"
  },
  {
    "path": "packages/core/src/zig/text-buffer.zig",
    "content": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst seg_mod = @import(\"text-buffer-segment.zig\");\nconst iter_mod = @import(\"text-buffer-iterators.zig\");\nconst mem_registry_mod = @import(\"mem-registry.zig\");\nconst ss = @import(\"syntax-style.zig\");\nconst gp = @import(\"grapheme.zig\");\nconst ansi = @import(\"ansi.zig\");\nconst link = @import(\"link.zig\");\n\nconst utf8 = @import(\"utf8.zig\");\nconst utils = @import(\"utils.zig\");\n\nconst logger = @import(\"logger.zig\");\n\nconst Segment = seg_mod.Segment;\nconst UnifiedRope = seg_mod.UnifiedRope;\nconst LineInfo = iter_mod.LineInfo;\n\n// Re-export types from segment module\npub const TextChunk = seg_mod.TextChunk;\npub const MemRegistry = mem_registry_mod.MemRegistry;\npub const RGBA = seg_mod.RGBA;\npub const TextSelection = seg_mod.TextSelection;\npub const TextBufferError = seg_mod.TextBufferError;\npub const Highlight = seg_mod.Highlight;\npub const StyleSpan = seg_mod.StyleSpan;\npub const WrapMode = seg_mod.WrapMode;\npub const ChunkFitResult = seg_mod.ChunkFitResult;\npub const GraphemeInfo = seg_mod.GraphemeInfo;\n\npub const SyntaxStyle = ss.SyntaxStyle;\n\npub const TextBuffer = UnifiedTextBuffer;\n\npub const StyledChunk = extern struct {\n    text_ptr: [*]const u8,\n    text_len: usize,\n    fg_ptr: ?[*]const f32,\n    bg_ptr: ?[*]const f32,\n    attributes: u32,\n    link_ptr: ?[*]const u8 = null,\n    link_len: usize = 0,\n};\n\npub const UnifiedTextBuffer = struct {\n    const Self = @This();\n\n    mem_registry: MemRegistry,\n    default_fg: ?RGBA,\n    default_bg: ?RGBA,\n    default_attributes: ?u32,\n\n    allocator: Allocator,\n    global_allocator: Allocator,\n    arena: *std.heap.ArenaAllocator,\n\n    _rope: UnifiedRope,\n    syntax_style: ?*const SyntaxStyle,\n\n    pool: *gp.GraphemePool,\n    link_pool: *link.LinkPool,\n    link_tracker: ?link.LinkTracker,\n\n    width_method: utf8.WidthMethod,\n\n    view_dirty_flags: std.ArrayListUnmanaged(bool),\n    next_view_id: u32,\n    free_view_ids: std.ArrayListUnmanaged(u32),\n\n    /// Monotonic counter that increments on every content change. Views use this\n    /// to detect stale caches even after clearViewDirty() runs.\n    content_epoch: u64,\n\n    // Per-line highlight cache (invalidated on edits)\n    // Maps line_idx to highlights for that line\n    line_highlights: std.ArrayListUnmanaged(std.ArrayListUnmanaged(Highlight)),\n    line_spans: std.ArrayListUnmanaged(std.ArrayListUnmanaged(StyleSpan)),\n    highlight_batch_depth: u32,\n    dirty_span_lines: std.AutoHashMap(usize, void),\n\n    styled_text_mem_id: ?u8,\n    styled_buffer: ?[]u8,\n    styled_capacity: usize,\n\n    tab_width: u8,\n\n    pub const Defaults = struct {\n        fg: ?RGBA,\n        bg: ?RGBA,\n        attributes: ?u32,\n    };\n\n    /// Accessor: return default fg/bg/attributes as a struct.\n    pub fn defaults(self: *const Self) Defaults {\n        return .{\n            .fg = self.default_fg,\n            .bg = self.default_bg,\n            .attributes = self.default_attributes,\n        };\n    }\n\n    /// Accessor: return a const pointer to the mem registry.\n    pub fn memRegistry(self: *const Self) *const MemRegistry {\n        return &self.mem_registry;\n    }\n\n    /// Accessor: return the width method.\n    pub fn widthMethod(self: *const Self) utf8.WidthMethod {\n        return self.width_method;\n    }\n\n    /// Accessor: return tab width.\n    pub fn tabWidth(self: *const Self) u8 {\n        return self.tab_width;\n    }\n\n    /// Accessor: return the internal allocator.\n    pub fn getAllocator(self: *const Self) Allocator {\n        return self.allocator;\n    }\n\n    /// Accessor: return pointer to the rope (for edit-path callers).\n    /// Const-preserving: returns *UnifiedRope or *const UnifiedRope depending on receiver.\n    pub fn rope(self: anytype) blk: {\n        const T = @TypeOf(self);\n        break :blk if (T == *Self) *UnifiedRope else if (T == *const Self) *const UnifiedRope else @compileError(\"expected *Self or *const Self\");\n    } {\n        return &self._rope;\n    }\n\n    /// Accessor: get line width at a given row.\n    pub fn lineWidthAt(self: *const Self, row: u32) u32 {\n        return iter_mod.lineWidthAt(@constCast(&self._rope), row);\n    }\n\n    /// Accessor: get maximum line width across all lines.\n    pub fn lineWidthColsMax(self: *const Self) u32 {\n        return iter_mod.getMaxLineWidth(&self._rope);\n    }\n\n    pub fn getGraphemeWidthAt(self: *const Self, row: u32, col: u32) u32 {\n        return iter_mod.getGraphemeWidthAt(@constCast(&self._rope), &self.mem_registry, row, col, self.tab_width, self.width_method);\n    }\n\n    pub fn getPrevGraphemeWidth(self: *const Self, row: u32, col: u32) u32 {\n        return iter_mod.getPrevGraphemeWidth(@constCast(&self._rope), &self.mem_registry, row, col, self.tab_width, self.width_method);\n    }\n\n    pub fn getWrapOffsetsFor(self: *const Self, chunk: *const TextChunk) TextBufferError![]const utf8.WrapBreak {\n        return chunk.getWrapOffsets(&self.mem_registry, self.allocator, self.width_method);\n    }\n\n    /// Accessor: walk all lines and segments via callbacks.\n    pub fn walkLinesAndSegments(\n        self: *const Self,\n        ctx: *anyopaque,\n        segment_callback: *const fn (ctx: *anyopaque, line_idx: u32, chunk: *const TextChunk, chunk_idx_in_line: u32) void,\n        line_end_callback: *const fn (ctx: *anyopaque, line_info: LineInfo) void,\n    ) void {\n        iter_mod.walkLinesAndSegments(&self._rope, ctx, segment_callback, line_end_callback);\n    }\n\n    pub fn init(\n        global_allocator: Allocator,\n        pool: *gp.GraphemePool,\n        link_pool: *link.LinkPool,\n        width_method: utf8.WidthMethod,\n    ) TextBufferError!*Self {\n        const self = global_allocator.create(Self) catch return TextBufferError.OutOfMemory;\n        errdefer global_allocator.destroy(self);\n\n        const internal_arena = global_allocator.create(std.heap.ArenaAllocator) catch return TextBufferError.OutOfMemory;\n        errdefer global_allocator.destroy(internal_arena);\n        internal_arena.* = std.heap.ArenaAllocator.init(global_allocator);\n\n        const internal_allocator = internal_arena.allocator();\n\n        const init_rope = UnifiedRope.init(internal_allocator) catch return TextBufferError.OutOfMemory;\n\n        var view_dirty_flags: std.ArrayListUnmanaged(bool) = .{};\n        errdefer view_dirty_flags.deinit(global_allocator);\n\n        var free_view_ids: std.ArrayListUnmanaged(u32) = .{};\n        errdefer free_view_ids.deinit(global_allocator);\n\n        var mem_registry = MemRegistry.init(global_allocator);\n        errdefer mem_registry.deinit();\n\n        var dirty_span_lines = std.AutoHashMap(usize, void).init(global_allocator);\n        errdefer dirty_span_lines.deinit();\n\n        self.* = .{\n            .mem_registry = mem_registry,\n            .default_fg = null,\n            .default_bg = null,\n            .default_attributes = null,\n            .allocator = internal_allocator,\n            .global_allocator = global_allocator,\n            .arena = internal_arena,\n            ._rope = init_rope,\n            .syntax_style = null,\n            .pool = pool,\n            .link_pool = link_pool,\n            .link_tracker = null,\n            .width_method = width_method,\n            .view_dirty_flags = view_dirty_flags,\n            .next_view_id = 0,\n            .free_view_ids = free_view_ids,\n            .content_epoch = 0,\n            .line_highlights = .{},\n            .line_spans = .{},\n            .highlight_batch_depth = 0,\n            .dirty_span_lines = dirty_span_lines,\n            .styled_text_mem_id = null,\n            .styled_buffer = null,\n            .styled_capacity = 0,\n            .tab_width = 2,\n        };\n\n        return self;\n    }\n\n    pub fn deinit(self: *Self) void {\n        if (self.syntax_style) |style| {\n            (@constCast(style)).offDestroy(@ptrCast(self), onSyntaxStyleDestroyed);\n        }\n\n        self.view_dirty_flags.deinit(self.global_allocator);\n        self.free_view_ids.deinit(self.global_allocator);\n\n        // Free highlight/span caches\n        for (self.line_highlights.items) |*hl_list| {\n            hl_list.deinit(self.global_allocator);\n        }\n        self.line_highlights.deinit(self.global_allocator);\n\n        for (self.line_spans.items) |*span_list| {\n            span_list.deinit(self.global_allocator);\n        }\n        self.line_spans.deinit(self.global_allocator);\n\n        // Free dirty span lines hashmap\n        self.dirty_span_lines.deinit();\n\n        // Free persistent styled text buffer\n        if (self.styled_buffer) |buf| {\n            self.global_allocator.free(buf);\n        }\n\n        if (self.link_tracker) |*tracker| {\n            tracker.deinit();\n        }\n\n        self.mem_registry.deinit();\n        self.arena.deinit();\n        self.global_allocator.destroy(self.arena);\n        self.global_allocator.destroy(self);\n    }\n\n    // View registration (same as original)\n    pub fn registerView(self: *Self) TextBufferError!u32 {\n        if (self.free_view_ids.items.len > 0) {\n            const id = self.free_view_ids.items[self.free_view_ids.items.len - 1];\n            _ = self.free_view_ids.pop();\n            self.view_dirty_flags.items[id] = true;\n            return id;\n        }\n\n        const id = self.next_view_id;\n        self.next_view_id += 1;\n        try self.view_dirty_flags.append(self.global_allocator, true);\n        return id;\n    }\n\n    pub fn unregisterView(self: *Self, view_id: u32) void {\n        if (view_id < self.view_dirty_flags.items.len) {\n            self.free_view_ids.append(self.global_allocator, view_id) catch {};\n        }\n    }\n\n    pub fn isViewDirty(self: *const Self, view_id: u32) bool {\n        if (view_id < self.view_dirty_flags.items.len) {\n            return self.view_dirty_flags.items[view_id];\n        }\n        return false;\n    }\n\n    pub fn clearViewDirty(self: *Self, view_id: u32) void {\n        if (view_id < self.view_dirty_flags.items.len) {\n            self.view_dirty_flags.items[view_id] = false;\n        }\n    }\n\n    /// Returns the current content epoch. Use this to detect buffer changes\n    /// independent of the dirty flag (other code paths may clear dirty).\n    pub fn getContentEpoch(self: *const Self) u64 {\n        return self.content_epoch;\n    }\n\n    fn markAllViewsDirty(self: *Self) void {\n        // Increment epoch first so views see the new value when checking caches.\n        // Use wrapping add for safety, though u64 won't overflow in practice.\n        self.content_epoch +%= 1;\n        for (self.view_dirty_flags.items) |*flag| {\n            flag.* = true;\n        }\n    }\n\n    pub fn markViewsDirty(self: *Self) void {\n        self.markAllViewsDirty();\n    }\n\n    // Basic queries using unified rope\n    pub fn getLength(self: *const Self) u32 {\n        const metrics = self._rope.root.metrics();\n        return metrics.custom.total_width;\n    }\n\n    pub fn getByteSize(self: *const Self) u32 {\n        const metrics = self._rope.root.metrics();\n        const total_bytes = metrics.custom.total_bytes;\n\n        // Add newlines between lines (line_count - 1)\n        const line_count = iter_mod.getLineCount(&self._rope);\n        if (line_count > 0) {\n            return total_bytes + (line_count - 1); // newlines\n        }\n        return total_bytes;\n    }\n\n    pub fn measureText(self: *const Self, text: []const u8) u32 {\n        // For grapheme-accurate width calculation (used by highlighting system),\n        // use utf8.calculateTextWidth which properly handles grapheme clusters\n        const is_ascii = utf8.isAsciiOnly(text);\n        return utf8.calculateTextWidth(text, self.tab_width, is_ascii, self.width_method);\n    }\n\n    /// Clear the text content without resetting arena or memory registry.\n    /// Preserves highlights, memory buffers, and arena allocations.\n    /// Use this for frequent text updates where undo/redo history should be preserved.\n    pub fn clear(self: *Self) void {\n        self.clearLinkRefs();\n        self._rope.clear();\n        self.markAllViewsDirty();\n    }\n\n    pub fn reset(self: *Self) void {\n        self.clearLinkRefs();\n\n        // Free highlight/span arrays (they use global_allocator, not arena)\n        for (self.line_highlights.items) |*hl_list| {\n            hl_list.deinit(self.global_allocator);\n        }\n        self.line_highlights.clearRetainingCapacity();\n\n        for (self.line_spans.items) |*span_list| {\n            span_list.deinit(self.global_allocator);\n        }\n        self.line_spans.clearRetainingCapacity();\n\n        // Free persistent styled text buffer\n        if (self.styled_buffer) |buf| {\n            self.global_allocator.free(buf);\n        }\n        self.styled_buffer = null;\n        self.styled_text_mem_id = null;\n        self.styled_capacity = 0;\n\n        // Now reset the arena (frees all the internal memory)\n        _ = self.arena.reset(if (self.arena.queryCapacity() > 0) .retain_capacity else .free_all);\n\n        self.mem_registry.clear();\n\n        self._rope = UnifiedRope.init(self.allocator) catch return;\n\n        self.markAllViewsDirty();\n    }\n\n    // Default colors/attributes\n    pub fn setDefaultFg(self: *Self, fg: ?RGBA) void {\n        self.default_fg = fg;\n    }\n\n    pub fn setDefaultBg(self: *Self, bg: ?RGBA) void {\n        self.default_bg = bg;\n    }\n\n    pub fn setDefaultAttributes(self: *Self, attributes: ?u32) void {\n        self.default_attributes = attributes;\n    }\n\n    pub fn resetDefaults(self: *Self) void {\n        self.default_fg = null;\n        self.default_bg = null;\n        self.default_attributes = null;\n    }\n\n    fn onSyntaxStyleDestroyed(ctx_ptr: *anyopaque) void {\n        const self = @as(*Self, @ptrCast(@alignCast(ctx_ptr)));\n        self.syntax_style = null;\n    }\n\n    pub fn setSyntaxStyle(self: *Self, syntax_style: ?*const SyntaxStyle) void {\n        if (self.syntax_style) |prev| {\n            (@constCast(prev)).offDestroy(@ptrCast(self), onSyntaxStyleDestroyed);\n        }\n        self.syntax_style = syntax_style;\n        if (syntax_style) |style| {\n            _ = (@constCast(style)).onDestroy(@ptrCast(self), onSyntaxStyleDestroyed) catch {};\n        }\n    }\n\n    pub fn getSyntaxStyle(self: *const Self) ?*const SyntaxStyle {\n        return self.syntax_style;\n    }\n\n    fn getLinkTracker(self: *Self) *link.LinkTracker {\n        if (self.link_tracker == null) {\n            self.link_tracker = link.LinkTracker.init(self.global_allocator, self.link_pool);\n        }\n\n        return &self.link_tracker.?;\n    }\n\n    fn clearLinkRefs(self: *Self) void {\n        if (self.link_tracker) |*tracker| {\n            tracker.clear();\n        }\n    }\n\n    /// Set the text content using SIMD-optimized line break detection\n    pub fn setText(self: *Self, text: []const u8) TextBufferError!void {\n        self.clear();\n        const mem_id = try self.mem_registry.register(text, false);\n        try self.setTextInternal(mem_id, text);\n    }\n\n    /// Set text from a pre-registered memory ID\n    pub fn setTextFromMemId(self: *Self, mem_id: u8) TextBufferError!void {\n        const text = self.mem_registry.get(mem_id) orelse return TextBufferError.InvalidMemId;\n        self.clear();\n        try self.setTextInternal(mem_id, text);\n    }\n\n    /// Append text to the end of the buffer without clearing\n    pub fn append(self: *Self, text: []const u8) TextBufferError!void {\n        if (text.len == 0) {\n            return;\n        }\n\n        const mem_id = try self.mem_registry.register(text, false);\n        try self.appendInternal(mem_id, text);\n    }\n\n    /// Append text from a pre-registered memory ID\n    pub fn appendFromMemId(self: *Self, mem_id: u8) TextBufferError!void {\n        const text = self.mem_registry.get(mem_id) orelse return TextBufferError.InvalidMemId;\n        try self.appendInternal(mem_id, text);\n    }\n\n    /// Internal append that doesn't register memory\n    fn appendInternal(self: *Self, mem_id: u8, text: []const u8) TextBufferError!void {\n        if (text.len == 0) {\n            return;\n        }\n\n        // The rope's boundary rewrite will handle normalization at join points\n        var result = try self.textToSegments(self.global_allocator, text, mem_id, 0, false);\n        defer result.segments.deinit(result.allocator);\n\n        const insert_pos = self._rope.count();\n        try self._rope.insert_slice(insert_pos, result.segments.items);\n\n        self.markAllViewsDirty();\n    }\n\n    /// Internal setText that doesn't call clear (for use by setStyledText)\n    fn setTextInternal(self: *Self, mem_id: u8, text: []const u8) TextBufferError!void {\n        if (text.len == 0) {\n            self.markAllViewsDirty();\n            return;\n        }\n\n        var result = try self.textToSegments(self.global_allocator, text, mem_id, 0, true);\n        defer result.segments.deinit(result.allocator);\n\n        try self._rope.setSegments(result.segments.items);\n\n        self.markAllViewsDirty();\n    }\n\n    /// Create a TextChunk from a memory buffer range\n    pub fn createChunk(\n        self: *const Self,\n        mem_id: u8,\n        byte_start: u32,\n        byte_end: u32,\n    ) TextChunk {\n        const mem_buf = self.mem_registry.get(mem_id).?;\n        const chunk_bytes = mem_buf[byte_start..byte_end];\n        const is_ascii = utf8.isAsciiOnly(chunk_bytes);\n\n        var flags: u8 = 0;\n        if (chunk_bytes.len > 0 and is_ascii) {\n            flags |= TextChunk.Flags.ASCII_ONLY;\n        }\n\n        const chunk_width: u16 = @intCast(@min(65535, utf8.calculateTextWidth(chunk_bytes, self.tab_width, is_ascii, self.width_method)));\n\n        return TextChunk{\n            .mem_id = mem_id,\n            .byte_start = byte_start,\n            .byte_end = byte_end,\n            .width = chunk_width,\n            .flags = flags,\n        };\n    }\n\n    /// Convert text to segments with line breaks\n    /// Returns segments array and total width\n    pub fn textToSegments(\n        self: *const Self,\n        allocator: Allocator,\n        text: []const u8,\n        mem_id: u8,\n        byte_offset: u32,\n        prepend_linestart: bool,\n    ) TextBufferError!struct { segments: std.ArrayListUnmanaged(Segment), total_width: u32, allocator: Allocator } {\n        var break_result = utf8.LineBreakResult.init(allocator);\n        defer break_result.deinit();\n        try utf8.findLineBreaks(text, &break_result);\n\n        var segments: std.ArrayListUnmanaged(Segment) = .{};\n        errdefer segments.deinit(allocator);\n\n        if (prepend_linestart) {\n            try segments.append(allocator, Segment{ .linestart = {} });\n        }\n\n        var local_start: u32 = 0;\n        var total_width: u32 = 0;\n\n        for (break_result.breaks.items) |line_break| {\n            const break_pos: u32 = @intCast(line_break.pos);\n            const local_end: u32 = switch (line_break.kind) {\n                .CRLF => break_pos - 1,\n                .CR, .LF => break_pos,\n            };\n\n            if (local_end > local_start) {\n                const chunk = self.createChunk(mem_id, byte_offset + local_start, byte_offset + local_end);\n                try segments.append(allocator, Segment{ .text = chunk });\n                total_width += chunk.width;\n            }\n\n            try segments.append(allocator, Segment{ .brk = {} });\n            try segments.append(allocator, Segment{ .linestart = {} });\n\n            local_start = break_pos + 1;\n        }\n\n        if (local_start < text.len) {\n            const chunk = self.createChunk(mem_id, byte_offset + local_start, byte_offset + @as(u32, @intCast(text.len)));\n            try segments.append(allocator, Segment{ .text = chunk });\n            total_width += chunk.width;\n        }\n\n        return .{ .segments = segments, .total_width = total_width, .allocator = allocator };\n    }\n\n    pub fn getLineCount(self: *const Self) u32 {\n        const count = self._rope.count();\n        if (count == 0) return 0; // Truly empty (after reset)\n        return iter_mod.getLineCount(&self._rope);\n    }\n\n    pub fn lineCount(self: *const Self) u32 {\n        return self.getLineCount();\n    }\n\n    /// Register a memory buffer\n    pub fn registerMemBuffer(self: *Self, data: []const u8, owned: bool) TextBufferError!u8 {\n        return try self.mem_registry.register(data, owned);\n    }\n\n    pub fn replaceMemBuffer(self: *Self, mem_id: u8, data: []const u8, owned: bool) TextBufferError!void {\n        try self.mem_registry.replace(mem_id, data, owned);\n    }\n\n    pub fn clearMemRegistry(self: *Self) void {\n        self.mem_registry.clear();\n    }\n\n    pub fn getMemBuffer(self: *const Self, mem_id: u8) ?[]const u8 {\n        return self.mem_registry.get(mem_id);\n    }\n\n    /// Add a line from a memory buffer (for compatibility with old API)\n    /// Note: This is not as efficient as setText for bulk operations\n    /// Adds text segment with a break separator before it (if not the first line)\n    pub fn addLine(\n        self: *Self,\n        mem_id: u8,\n        byte_start: u32,\n        byte_end: u32,\n    ) TextBufferError!void {\n        _ = self.mem_registry.get(mem_id) orelse return TextBufferError.InvalidMemId;\n\n        const chunk = self.createChunk(mem_id, byte_start, byte_end);\n\n        const had_content = self._rope.count() > 1;\n\n        if (had_content) {\n            try self._rope.append(Segment{ .brk = {} });\n            try self._rope.append(Segment{ .linestart = {} });\n        }\n\n        try self._rope.append(Segment{ .text = chunk });\n\n        self.markAllViewsDirty();\n    }\n\n    pub fn getArenaAllocatedBytes(self: *const Self) usize {\n        return self.arena.queryCapacity();\n    }\n\n    /// Extract all text as UTF-8 bytes into provided output buffer\n    pub fn getPlainTextIntoBuffer(self: *const Self, out_buffer: []u8) usize {\n        var out_index: usize = 0;\n\n        const line_count = self.getLineCount();\n\n        const Context = struct {\n            buffer: *const UnifiedTextBuffer,\n            out_buffer: []u8,\n            out_index: *usize,\n            line_count: u32,\n\n            fn segmentCallback(ctx_ptr: *anyopaque, line_idx: u32, chunk: *const TextChunk, chunk_idx_in_line: u32) void {\n                _ = line_idx;\n                _ = chunk_idx_in_line;\n                const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n                const chunk_bytes = chunk.getBytes(&ctx.buffer.mem_registry);\n                const copy_len = @min(chunk_bytes.len, ctx.out_buffer.len - ctx.out_index.*);\n                if (copy_len > 0) {\n                    @memcpy(ctx.out_buffer[ctx.out_index.* .. ctx.out_index.* + copy_len], chunk_bytes[0..copy_len]);\n                    ctx.out_index.* += copy_len;\n                }\n            }\n\n            fn lineEndCallback(ctx_ptr: *anyopaque, line_info: LineInfo) void {\n                const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n                // Add newline between lines (not after last line)\n                if (ctx.line_count > 0 and line_info.line_idx < ctx.line_count - 1 and ctx.out_index.* < ctx.out_buffer.len) {\n                    ctx.out_buffer[ctx.out_index.*] = '\\n';\n                    ctx.out_index.* += 1;\n                }\n            }\n        };\n\n        var ctx = Context{\n            .buffer = self,\n            .out_buffer = out_buffer,\n            .out_index = &out_index,\n            .line_count = line_count,\n        };\n        self.walkLinesAndSegments(&ctx, Context.segmentCallback, Context.lineEndCallback);\n\n        return out_index;\n    }\n\n    pub fn startHighlightsTransaction(self: *Self) void {\n        self.highlight_batch_depth += 1;\n    }\n\n    pub fn endHighlightsTransaction(self: *Self) void {\n        if (self.highlight_batch_depth == 0) return;\n\n        self.highlight_batch_depth -= 1;\n\n        if (self.highlight_batch_depth == 0) {\n            var it = self.dirty_span_lines.keyIterator();\n            while (it.next()) |line_idx| {\n                self.rebuildLineSpans(line_idx.*) catch {};\n            }\n            self.dirty_span_lines.clearRetainingCapacity();\n        }\n    }\n\n    fn markLineSpansDirty(self: *Self, line_idx: usize) void {\n        self.dirty_span_lines.put(line_idx, {}) catch {};\n    }\n\n    // Highlight system\n    fn ensureLineHighlightStorage(self: *Self, line_idx: usize) TextBufferError!void {\n        while (self.line_highlights.items.len <= line_idx) {\n            try self.line_highlights.append(self.global_allocator, .{});\n        }\n        while (self.line_spans.items.len <= line_idx) {\n            try self.line_spans.append(self.global_allocator, .{});\n        }\n    }\n\n    pub fn addHighlight(\n        self: *Self,\n        line_idx: usize,\n        col_start: u32,\n        col_end: u32,\n        style_id: u32,\n        priority: u8,\n        hl_ref: u16,\n    ) TextBufferError!void {\n        const line_count = self.getLineCount();\n        if (line_idx >= line_count) {\n            return TextBufferError.InvalidIndex;\n        }\n\n        if (col_start >= col_end) {\n            return; // Empty range\n        }\n\n        try self.ensureLineHighlightStorage(line_idx);\n\n        const hl = Highlight{\n            .col_start = col_start,\n            .col_end = col_end,\n            .style_id = style_id,\n            .priority = priority,\n            .hl_ref = hl_ref,\n        };\n\n        try self.line_highlights.items[line_idx].append(self.global_allocator, hl);\n\n        if (self.highlight_batch_depth == 0) {\n            try self.rebuildLineSpans(line_idx);\n        } else {\n            self.markLineSpansDirty(line_idx);\n        }\n    }\n\n    pub fn getLineHighlights(self: *const Self, line_idx: usize) []const Highlight {\n        if (line_idx < self.line_highlights.items.len) {\n            return self.line_highlights.items[line_idx].items;\n        }\n        return &[_]Highlight{};\n    }\n\n    pub fn getLineSpans(self: *const Self, line_idx: usize) []const StyleSpan {\n        if (line_idx < self.line_spans.items.len) {\n            return self.line_spans.items[line_idx].items;\n        }\n        return &[_]StyleSpan{};\n    }\n\n    fn rebuildLineSpans(self: *Self, line_idx: usize) TextBufferError!void {\n        if (line_idx >= self.line_spans.items.len) {\n            return TextBufferError.InvalidIndex;\n        }\n\n        self.line_spans.items[line_idx].clearRetainingCapacity();\n\n        if (line_idx >= self.line_highlights.items.len or self.line_highlights.items[line_idx].items.len == 0) {\n            return; // No highlights\n        }\n\n        const highlights = self.line_highlights.items[line_idx].items;\n\n        // Collect all boundary columns\n        const Event = struct {\n            col: u32,\n            is_start: bool,\n            hl_idx: usize,\n        };\n\n        var events: std.ArrayListUnmanaged(Event) = .{};\n        defer events.deinit(self.global_allocator);\n\n        for (highlights, 0..) |hl, idx| {\n            try events.append(self.global_allocator, .{ .col = hl.col_start, .is_start = true, .hl_idx = idx });\n            try events.append(self.global_allocator, .{ .col = hl.col_end, .is_start = false, .hl_idx = idx });\n        }\n\n        // Sort by column, ends before starts at same position\n        const sortFn = struct {\n            fn lessThan(_: void, a: Event, b: Event) bool {\n                if (a.col != b.col) return a.col < b.col;\n                if (a.is_start != b.is_start) return !a.is_start; // ends before starts\n                // If both are same type at same column, use hl_idx for stable sort\n                return a.hl_idx < b.hl_idx;\n            }\n        }.lessThan;\n        std.mem.sort(Event, events.items, {}, sortFn);\n\n        // Build spans by tracking active highlights\n        var active = std.AutoHashMap(usize, void).init(self.global_allocator);\n        defer active.deinit();\n\n        var current_col: u32 = 0;\n\n        for (events.items) |event| {\n            // Find current highest priority style before processing event\n            var current_priority: i16 = -1;\n            var current_style: u32 = 0;\n            var it = active.keyIterator();\n            while (it.next()) |hl_idx| {\n                const hl = highlights[hl_idx.*];\n                if (hl.priority > current_priority) {\n                    current_priority = @intCast(hl.priority);\n                    current_style = hl.style_id;\n                }\n            }\n\n            // Emit span for the segment leading up to this event\n            if (event.col > current_col) {\n                try self.line_spans.items[line_idx].append(self.global_allocator, StyleSpan{\n                    .col = current_col,\n                    .style_id = current_style,\n                    .next_col = event.col,\n                });\n                current_col = event.col;\n            }\n\n            // Process event\n            if (event.is_start) {\n                try active.put(event.hl_idx, {});\n            } else {\n                _ = active.remove(event.hl_idx);\n            }\n        }\n\n        // Emit final span after last event if there were any highlights\n        // This ensures the line returns to default styling after the last highlight ends\n        if (events.items.len > 0 and active.count() == 0) {\n            const line_width = self.lineWidthAt(@intCast(line_idx));\n            if (current_col < line_width) {\n                try self.line_spans.items[line_idx].append(self.global_allocator, StyleSpan{\n                    .col = current_col,\n                    .style_id = 0, // No style (default)\n                    .next_col = line_width,\n                });\n            }\n        }\n    }\n\n    /// Add highlight by row/col coordinates\n    pub fn addHighlightByCoords(\n        self: *Self,\n        start_row: u32,\n        start_col: u32,\n        end_row: u32,\n        end_col: u32,\n        style_id: u32,\n        priority: u8,\n        hl_ref: u16,\n    ) TextBufferError!void {\n        const char_start = iter_mod.coordsToOffset(&self._rope, start_row, start_col) orelse return TextBufferError.InvalidIndex;\n        const char_end = iter_mod.coordsToOffset(&self._rope, end_row, end_col) orelse return TextBufferError.InvalidIndex;\n        return self.addHighlightByCharRange(char_start, char_end, style_id, priority, hl_ref);\n    }\n\n    /// Add highlight by character range\n    pub fn addHighlightByCharRange(\n        self: *Self,\n        char_start: u32,\n        char_end: u32,\n        style_id: u32,\n        priority: u8,\n        hl_ref: u16,\n    ) TextBufferError!void {\n        const line_count = self.getLineCount();\n        if (char_start >= char_end or line_count == 0) {\n            return;\n        }\n\n        // Walk lines to find which lines this highlight affects\n        const Context = struct {\n            buffer: *Self,\n            char_start: u32,\n            char_end: u32,\n            style_id: u32,\n            priority: u8,\n            hl_ref: u16,\n            start_line_idx: ?usize = null,\n\n            fn callback(ctx_ptr: *anyopaque, line_info: LineInfo) void {\n                const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_ptr)));\n                const line_start_col_offset = line_info.col_offset;\n                const line_end_col_offset = line_info.col_offset + line_info.width_cols;\n\n                // Skip lines before the highlight\n                if (line_end_col_offset <= ctx.char_start) return;\n                // Stop after the highlight ends\n                if (line_start_col_offset >= ctx.char_end) return;\n\n                // This line overlaps with the highlight\n                const col_start = if (ctx.char_start > line_start_col_offset)\n                    ctx.char_start - line_start_col_offset\n                else\n                    0;\n\n                const col_end = if (ctx.char_end < line_end_col_offset)\n                    ctx.char_end - line_start_col_offset\n                else\n                    line_info.width_cols;\n\n                ctx.buffer.addHighlight(\n                    line_info.line_idx,\n                    col_start,\n                    col_end,\n                    ctx.style_id,\n                    ctx.priority,\n                    ctx.hl_ref,\n                ) catch {};\n            }\n        };\n\n        var ctx = Context{\n            .buffer = self,\n            .char_start = char_start,\n            .char_end = char_end,\n            .style_id = style_id,\n            .priority = priority,\n            .hl_ref = hl_ref,\n        };\n        iter_mod.walkLines(&self._rope, &ctx, Context.callback, false);\n    }\n\n    /// Remove all highlights with a specific reference ID\n    pub fn removeHighlightsByRef(self: *Self, hl_ref: u16) void {\n        for (self.line_highlights.items, 0..) |*hl_list, line_idx| {\n            var i: usize = 0;\n            var changed = false;\n            while (i < hl_list.items.len) {\n                if (hl_list.items[i].hl_ref == hl_ref) {\n                    _ = hl_list.orderedRemove(i);\n                    changed = true;\n                    continue;\n                }\n                i += 1;\n            }\n            if (changed) {\n                if (self.highlight_batch_depth == 0) {\n                    self.rebuildLineSpans(line_idx) catch {};\n                } else {\n                    self.markLineSpansDirty(line_idx);\n                }\n            }\n        }\n    }\n\n    /// Clear all highlights from a specific line\n    pub fn clearLineHighlights(self: *Self, line_idx: usize) void {\n        if (line_idx < self.line_highlights.items.len) {\n            self.line_highlights.items[line_idx].clearRetainingCapacity();\n        }\n        if (line_idx < self.line_spans.items.len) {\n            self.line_spans.items[line_idx].clearRetainingCapacity();\n        }\n    }\n\n    /// Clear all highlights\n    pub fn clearAllHighlights(self: *Self) void {\n        for (self.line_highlights.items) |*hl_list| {\n            hl_list.clearRetainingCapacity();\n        }\n        for (self.line_spans.items) |*span_list| {\n            span_list.clearRetainingCapacity();\n        }\n    }\n\n    /// Get highlights for a specific line\n    pub fn getLineHighlightsSlice(self: *const Self, line_idx: usize) []const Highlight {\n        if (line_idx < self.line_highlights.items.len) {\n            return self.line_highlights.items[line_idx].items;\n        }\n        return &[_]Highlight{};\n    }\n\n    /// Get total number of highlights across all lines\n    pub fn getHighlightCount(self: *const Self) u32 {\n        var count: u32 = 0;\n        for (self.line_highlights.items) |hl_list| {\n            count += @intCast(hl_list.items.len);\n        }\n        return count;\n    }\n\n    /// Set styled text from chunks with individual styling\n    /// Accepts StyledChunk array for FFI compatibility\n    /// TODO: This is for backward compatibility, there should be a better way to do this.\n    pub fn setStyledText(\n        self: *Self,\n        chunks: []const StyledChunk,\n    ) TextBufferError!void {\n        if (chunks.len == 0) {\n            self.clear();\n            self.clearAllHighlights();\n            return;\n        }\n\n        // Calculate total text length\n        var total_len: usize = 0;\n        for (chunks) |chunk| {\n            total_len += chunk.text_len;\n        }\n\n        if (total_len == 0) {\n            self.clear();\n            self.clearAllHighlights();\n            return;\n        }\n\n        self.clear();\n        self.clearAllHighlights();\n\n        _ = self.arena.reset(.retain_capacity);\n\n        self._rope = UnifiedRope.init(self.allocator) catch return TextBufferError.OutOfMemory;\n\n        if (total_len > self.styled_capacity) {\n            if (self.styled_buffer) |old_buf| {\n                self.global_allocator.free(old_buf);\n            }\n            const new_buf = self.global_allocator.alloc(u8, total_len) catch return TextBufferError.OutOfMemory;\n            self.styled_buffer = new_buf;\n            self.styled_capacity = total_len;\n        }\n\n        const full_text = self.styled_buffer.?[0..total_len];\n\n        var offset: usize = 0;\n        for (chunks) |chunk| {\n            if (chunk.text_len > 0) {\n                const chunk_text = chunk.text_ptr[0..chunk.text_len];\n                @memcpy(full_text[offset .. offset + chunk.text_len], chunk_text);\n                offset += chunk.text_len;\n            }\n        }\n\n        if (self.styled_text_mem_id) |mem_id| {\n            try self.mem_registry.replace(mem_id, full_text, false);\n        } else {\n            const mem_id = try self.mem_registry.register(full_text, false);\n            self.styled_text_mem_id = mem_id;\n        }\n\n        try self.setTextInternal(self.styled_text_mem_id.?, full_text);\n\n        if (self.syntax_style) |style| {\n            var seen_link_ids: std.AutoHashMapUnmanaged(u32, void) = .{};\n            defer seen_link_ids.deinit(self.global_allocator);\n\n            self.startHighlightsTransaction();\n            defer self.endHighlightsTransaction();\n\n            var char_pos: u32 = 0;\n            for (chunks, 0..) |chunk, i| {\n                const chunk_text = chunk.text_ptr[0..chunk.text_len];\n                const chunk_len = self.measureText(chunk_text);\n\n                if (chunk_len > 0) {\n                    const fg = if (chunk.fg_ptr) |fgPtr| utils.f32PtrToRGBA(fgPtr) else null;\n                    const bg = if (chunk.bg_ptr) |bgPtr| utils.f32PtrToRGBA(bgPtr) else null;\n\n                    var attributes = chunk.attributes;\n                    if (chunk.link_ptr) |link_ptr| {\n                        if (chunk.link_len > 0) {\n                            const tracker = self.getLinkTracker();\n                            const url = link_ptr[0..chunk.link_len];\n                            const link_id = tracker.pool.alloc(url) catch 0;\n                            if (link_id != 0) {\n                                const maybe_seen = seen_link_ids.getOrPut(self.global_allocator, link_id) catch null;\n                                const should_track = if (maybe_seen) |seen| !seen.found_existing else true;\n                                if (should_track) {\n                                    tracker.addCellRef(link_id);\n                                }\n                                attributes = ansi.TextAttributes.setLinkId(attributes, link_id);\n                            }\n                        }\n                    }\n\n                    var style_name_buf: [64]u8 = undefined;\n                    const style_name = std.fmt.bufPrint(&style_name_buf, \"chunk{d}\", .{i}) catch continue;\n                    const style_id = (@constCast(style)).registerStyle(style_name, fg, bg, attributes) catch continue;\n\n                    self.addHighlightByCharRange(char_pos, char_pos + chunk_len, style_id, 1, 0) catch {};\n                }\n\n                char_pos += chunk_len;\n            }\n        }\n    }\n\n    /// Load text from a file path (relative to cwd)\n    /// The file content is allocated in the arena and will be freed when the buffer is destroyed\n    pub fn loadFile(self: *Self, path: []const u8) TextBufferError!void {\n        const file = std.fs.cwd().openFile(path, .{}) catch |err| {\n            return switch (err) {\n                error.FileNotFound => TextBufferError.InvalidIndex,\n                error.AccessDenied => TextBufferError.InvalidIndex,\n                else => TextBufferError.OutOfMemory,\n            };\n        };\n        defer file.close();\n\n        const file_size = file.getEndPos() catch return TextBufferError.OutOfMemory;\n\n        self.clear();\n\n        const content = self.allocator.alloc(u8, file_size) catch return TextBufferError.OutOfMemory;\n        const bytes_read = file.readAll(content) catch return TextBufferError.OutOfMemory;\n        const text = content[0..bytes_read];\n        const mem_id = try self.mem_registry.register(text, false);\n\n        try self.setTextInternal(mem_id, text);\n    }\n\n    pub fn getTabWidth(self: *const Self) u8 {\n        return self.tabWidth();\n    }\n\n    /// Set tab width, rounding up to nearest multiple of 2 (minimum 2).\n    /// Marks all views dirty if the width actually changes, since tab width\n    /// affects measured line widths and virtual line calculations.\n    pub fn setTabWidth(self: *Self, width: u8) void {\n        const clamped_width = @max(2, width);\n        const new_width = if (clamped_width % 2 == 0) clamped_width else clamped_width + 1;\n        if (self.tab_width == new_width) return;\n        self.tab_width = new_width;\n        self.markAllViewsDirty();\n    }\n\n    /// Debug log the rope structure using rope.toText\n    pub fn debugLogRope(self: *const Self) void {\n        logger.debug(\"=== TextBuffer Rope Debug ===\", .{});\n        logger.debug(\"Line count: {}\", .{self.getLineCount()});\n        logger.debug(\"Char count: {}\", .{self.getLength()});\n        logger.debug(\"Byte size: {}\", .{self.getByteSize()});\n\n        const rope_text = self._rope.toText(self.allocator) catch {\n            logger.debug(\"Failed to generate rope text representation\", .{});\n            return;\n        };\n        logger.debug(\"Rope structure: {s}\", .{rope_text});\n        logger.debug(\"=== End Rope Debug ===\", .{});\n    }\n\n    /// Get text within a range of display-width offsets\n    /// Automatically snaps to grapheme boundaries:\n    /// Returns number of bytes written to out_buffer\n    pub fn getTextRange(self: *const Self, start_offset: u32, end_offset: u32, out_buffer: []u8) usize {\n        if (start_offset >= end_offset) return 0;\n        if (out_buffer.len == 0) return 0;\n\n        const total_weight = self._rope.totalWeight();\n        if (start_offset >= total_weight) return 0;\n\n        const clamped_end = @min(end_offset, total_weight);\n\n        return iter_mod.extractTextBetweenOffsets(\n            &self._rope,\n            &self.mem_registry,\n            self.tab_width,\n            start_offset,\n            clamped_end,\n            out_buffer,\n            self.width_method,\n        );\n    }\n\n    /// Get text within a range specified by row/col coordinates\n    /// Automatically snaps to grapheme boundaries:\n    /// Returns number of bytes written to out_buffer\n    pub fn getTextRangeByCoords(self: *Self, start_row: u32, start_col: u32, end_row: u32, end_col: u32, out_buffer: []u8) usize {\n        const start_offset = iter_mod.coordsToOffset(&self._rope, start_row, start_col) orelse return 0;\n        const end_offset = iter_mod.coordsToOffset(&self._rope, end_row, end_col) orelse return 0;\n        return self.getTextRange(start_offset, end_offset, out_buffer);\n    }\n};\n"
  },
  {
    "path": "packages/core/src/zig/utf8.zig",
    "content": "const std = @import(\"std\");\nconst uucode = @import(\"uucode\");\n\n/// The method to use when calculating the width of a grapheme\npub const WidthMethod = enum {\n    wcwidth,\n    unicode,\n    no_zwj,\n};\n\n/// Check if a byte slice contains only printable ASCII (32..126)\n/// Uses SIMD16 for fast checking\npub fn isAsciiOnly(text: []const u8) bool {\n    if (text.len == 0) return false;\n\n    const vector_len = 16;\n    const Vec = @Vector(vector_len, u8);\n\n    const min_printable: Vec = @splat(32);\n    const max_printable: Vec = @splat(126);\n\n    var pos: usize = 0;\n\n    // Process full 16-byte vectors\n    while (pos + vector_len <= text.len) {\n        const chunk: Vec = text[pos..][0..vector_len].*;\n\n        // Check if all bytes are in [32, 126]\n        const too_low = chunk < min_printable;\n        const too_high = chunk > max_printable;\n\n        // Check if any byte is out of range\n        if (@reduce(.Or, too_low) or @reduce(.Or, too_high)) {\n            return false;\n        }\n\n        pos += vector_len;\n    }\n\n    // Handle remaining bytes with scalar code\n    while (pos < text.len) : (pos += 1) {\n        const b = text[pos];\n        if (b < 32 or b > 126) {\n            return false;\n        }\n    }\n\n    return true;\n}\n\npub const LineBreakKind = enum {\n    LF, // \\n (Unix/Linux)\n    CR, // \\r (Old Mac)\n    CRLF, // \\r\\n (Windows)\n};\n\npub const LineBreak = struct {\n    pos: usize,\n    kind: LineBreakKind,\n};\n\npub const LineBreakResult = struct {\n    breaks: std.ArrayListUnmanaged(LineBreak),\n    allocator: std.mem.Allocator,\n\n    pub fn init(allocator: std.mem.Allocator) LineBreakResult {\n        return .{\n            .breaks = .{},\n            .allocator = allocator,\n        };\n    }\n\n    pub fn deinit(self: *LineBreakResult) void {\n        self.breaks.deinit(self.allocator);\n    }\n\n    pub fn reset(self: *LineBreakResult) void {\n        self.breaks.clearRetainingCapacity();\n    }\n};\n\npub const TabStopResult = struct {\n    positions: std.ArrayListUnmanaged(usize),\n    allocator: std.mem.Allocator,\n\n    pub fn init(allocator: std.mem.Allocator) TabStopResult {\n        return .{\n            .positions = .{},\n            .allocator = allocator,\n        };\n    }\n\n    pub fn deinit(self: *TabStopResult) void {\n        self.positions.deinit(self.allocator);\n    }\n\n    pub fn reset(self: *TabStopResult) void {\n        self.positions.clearRetainingCapacity();\n    }\n};\n\npub const WrapBreak = struct {\n    // byte_offset points at the grapheme that creates this break opportunity.\n    // For whitespace and punctuation, this is the delimiter grapheme.\n    // For CJK<->ASCII transitions, this is the last grapheme in the previous run.\n    byte_offset: u32,\n\n    // char_offset is grapheme-count based, not a display column.\n    // Callers convert it to columns with charOffsetToColumn().\n    char_offset: u32,\n};\n\npub const WrapBreakResult = struct {\n    breaks: std.ArrayListUnmanaged(WrapBreak),\n    allocator: std.mem.Allocator,\n\n    pub fn init(allocator: std.mem.Allocator) WrapBreakResult {\n        return .{\n            .breaks = .{},\n            .allocator = allocator,\n        };\n    }\n\n    pub fn deinit(self: *WrapBreakResult) void {\n        self.breaks.deinit(self.allocator);\n    }\n\n    pub fn reset(self: *WrapBreakResult) void {\n        self.breaks.clearRetainingCapacity();\n    }\n};\n\n// Helper function to check if an ASCII byte is a wrap break point (CR/LF excluded)\ninline fn isAsciiWrapBreak(b: u8) bool {\n    return switch (b) {\n        ' ', '\\t' => true, // Whitespace (no CR/LF in inputs)\n        '-' => true, // Dash\n        '/', '\\\\' => true, // Slashes\n        '.', ',', ';', ':', '!', '?' => true, // Punctuation\n        '(', ')', '[', ']', '{', '}' => true, // Brackets\n        else => false,\n    };\n}\n\n// Decode a UTF-8 codepoint starting at pos. Assumes valid UTF-8 input.\n// Returns (codepoint, length). If the remaining bytes are insufficient, returns length 1.\npub inline fn decodeUtf8Unchecked(text: []const u8, pos: usize) struct { cp: u21, len: u3 } {\n    const b0 = text[pos];\n    if (b0 < 0x80) return .{ .cp = @intCast(b0), .len = 1 };\n\n    if (pos + 1 >= text.len) return .{ .cp = 0xFFFD, .len = 1 };\n    const b1 = text[pos + 1];\n\n    if ((b0 & 0xE0) == 0xC0) {\n        const cp2: u21 = @intCast((@as(u32, b0 & 0x1F) << 6) | @as(u32, b1 & 0x3F));\n        return .{ .cp = cp2, .len = 2 };\n    }\n\n    if (pos + 2 >= text.len) return .{ .cp = 0xFFFD, .len = 1 };\n    const b2 = text[pos + 2];\n\n    if ((b0 & 0xF0) == 0xE0) {\n        const cp3: u21 = @intCast((@as(u32, b0 & 0x0F) << 12) | (@as(u32, b1 & 0x3F) << 6) | @as(u32, b2 & 0x3F));\n        return .{ .cp = cp3, .len = 3 };\n    }\n\n    if (pos + 3 >= text.len) return .{ .cp = 0xFFFD, .len = 1 };\n    const b3 = text[pos + 3];\n    const cp4: u21 = @intCast((@as(u32, b0 & 0x07) << 18) | (@as(u32, b1 & 0x3F) << 12) | (@as(u32, b2 & 0x3F) << 6) | @as(u32, b3 & 0x3F));\n    return .{ .cp = cp4, .len = 4 };\n}\n\n// Unicode wrap-break codepoints\ninline fn isUnicodeWrapBreak(cp: u21) bool {\n    return switch (cp) {\n        0x00A0, // NBSP\n        0x1680, // OGHAM SPACE MARK\n        0x2000...0x200A, // En quad..Hair space\n        0x202F, // NARROW NO-BREAK SPACE\n        0x205F, // MEDIUM MATHEMATICAL SPACE\n        0x3000, // IDEOGRAPHIC SPACE\n        0x200B, // ZERO WIDTH SPACE\n        0x00AD, // SOFT HYPHEN\n        0x2010, // HYPHEN\n        0x3001, // IDEOGRAPHIC COMMA\n        0x3002, // IDEOGRAPHIC FULL STOP\n        0xFF01, // FULLWIDTH EXCLAMATION MARK\n        0xFF1F, // FULLWIDTH QUESTION MARK\n        => true,\n        else => false,\n    };\n}\n\n// WordClass keeps word-boundary behavior predictable in mixed-script text.\n// We split between ASCII word runs and CJK word runs, and we keep each\n// CJK run grouped as one unit.\nconst WordClass = enum {\n    ascii_word,\n    cjk_word,\n    other,\n};\n\ninline fn isAsciiWordByte(b: u8) bool {\n    return (b >= 'a' and b <= 'z') or\n        (b >= 'A' and b <= 'Z') or\n        (b >= '0' and b <= '9') or\n        b == '_';\n}\n\ninline fn isCjkWordCodepoint(cp: u21) bool {\n    return\n    // Han ideographs\n    (cp >= 0x3400 and cp <= 0x4DBF) or\n        (cp >= 0x4E00 and cp <= 0x9FFF) or\n        (cp >= 0xF900 and cp <= 0xFAFF) or\n        (cp >= 0x20000 and cp <= 0x2A6DF) or\n        (cp >= 0x2A700 and cp <= 0x2B73F) or\n        (cp >= 0x2B740 and cp <= 0x2B81F) or\n        (cp >= 0x2B820 and cp <= 0x2CEAF) or\n        (cp >= 0x2CEB0 and cp <= 0x2EBEF) or\n        (cp >= 0x2EBF0 and cp <= 0x2EE5D) or\n        (cp >= 0x2F800 and cp <= 0x2FA1F) or\n        // Hiragana + Katakana\n        (cp >= 0x3040 and cp <= 0x309F) or\n        (cp >= 0x30A0 and cp <= 0x30FF) or\n        (cp >= 0x31F0 and cp <= 0x31FF) or\n        (cp >= 0xFF66 and cp <= 0xFF9D) or\n        // Hangul\n        (cp >= 0x1100 and cp <= 0x11FF) or\n        (cp >= 0x3130 and cp <= 0x318F) or\n        (cp >= 0xA960 and cp <= 0xA97F) or\n        (cp >= 0xAC00 and cp <= 0xD7AF) or\n        (cp >= 0xD7B0 and cp <= 0xD7FF);\n}\n\ninline fn classifyWordClass(cp: u21) WordClass {\n    if (cp <= 0x7F) {\n        return if (isAsciiWordByte(@intCast(cp))) .ascii_word else .other;\n    }\n    if (isCjkWordCodepoint(cp)) return .cjk_word;\n    return .other;\n}\n\npub inline fn isWordCodepoint(cp: u21) bool {\n    return classifyWordClass(cp) != .other;\n}\n\ninline fn isCjkAsciiTransition(prev_class: WordClass, curr_class: WordClass) bool {\n    return (prev_class == .cjk_word and curr_class == .ascii_word) or\n        (prev_class == .ascii_word and curr_class == .cjk_word);\n}\n\n// Nothing needed here - using uucode.grapheme.isBreak directly\n\npub fn findWrapBreaks(text: []const u8, result: *WrapBreakResult, width_method: WidthMethod) !void {\n    // This function clears previous results and writes fresh break points.\n    // Callers should treat `result.breaks` as replaced after the call.\n    _ = width_method; // Currently unused, but kept for API consistency\n    result.reset();\n    const vector_len = 16;\n\n    var pos: usize = 0;\n    var char_offset: u32 = 0;\n    var prev_cp: ?u21 = null; // Track previous codepoint for grapheme detection\n    var break_state: uucode.grapheme.BreakState = .default;\n    // We keep track of the current grapheme so we can add a break at\n    // CJK<->ASCII transitions. The break is emitted at the previous grapheme,\n    // so callers that add grapheme width land exactly at the run boundary.\n    var have_current_grapheme = false;\n    var current_grapheme_byte_offset: u32 = 0;\n    var current_grapheme_char_offset: u32 = 0;\n    var current_grapheme_class: WordClass = .other;\n\n    while (pos + vector_len <= text.len) {\n        const chunk: @Vector(vector_len, u8) = text[pos..][0..vector_len].*;\n        const ascii_threshold: @Vector(vector_len, u8) = @splat(0x80);\n        const is_non_ascii = chunk >= ascii_threshold;\n\n        // Fast path: all ASCII\n        if (!@reduce(.Or, is_non_ascii)) {\n            const first_class = classifyWordClass(text[pos]);\n            if (have_current_grapheme and isCjkAsciiTransition(current_grapheme_class, first_class)) {\n                try result.breaks.append(result.allocator, .{\n                    .byte_offset = current_grapheme_byte_offset,\n                    .char_offset = current_grapheme_char_offset,\n                });\n            }\n\n            // Use SIMD to find break characters\n            var match_mask: @Vector(vector_len, bool) = @splat(false);\n\n            // Check whitespace\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat(' ')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('\\t')));\n\n            // Check dashes and slashes\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('-')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('/')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('\\\\')));\n\n            // Check punctuation\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('.')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat(',')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat(';')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat(':')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('!')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('?')));\n\n            // Check brackets\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('(')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat(')')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('[')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat(']')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('{')));\n            match_mask = match_mask | (chunk == @as(@Vector(vector_len, u8), @splat('}')));\n\n            // Convert boolean mask to integer bitmask for faster iteration\n            var bitmask: u16 = 0;\n            inline for (0..vector_len) |i| {\n                if (match_mask[i]) {\n                    bitmask |= @as(u16, 1) << @intCast(i);\n                }\n            }\n\n            // Use bit manipulation to extract positions\n            while (bitmask != 0) {\n                const bit_pos = @ctz(bitmask);\n                try result.breaks.append(result.allocator, .{\n                    .byte_offset = @intCast(pos + bit_pos),\n                    .char_offset = char_offset + @as(u32, @intCast(bit_pos)),\n                });\n                bitmask &= bitmask - 1;\n            }\n\n            pos += vector_len;\n            const block_start_char_offset = char_offset;\n            char_offset += vector_len;\n            prev_cp = text[pos - 1]; // Last ASCII char\n            break_state = .default;\n            have_current_grapheme = true;\n            current_grapheme_byte_offset = @intCast(pos - 1);\n            current_grapheme_char_offset = block_start_char_offset + (vector_len - 1);\n            current_grapheme_class = classifyWordClass(text[pos - 1]);\n            continue;\n        }\n\n        // Slow path: mixed ASCII/non-ASCII - need grapheme-aware counting\n        var i: usize = 0;\n        while (i < vector_len) {\n            const b0 = text[pos + i];\n            if (b0 < 0x80) {\n                const curr_cp: u21 = b0;\n\n                // Check if this starts a new grapheme cluster\n                // Skip invalid/replacement codepoints or codepoints that might be outside the grapheme table range\n                const is_break = if (curr_cp == 0xFFFD or curr_cp > 0x10FFFF) true else if (prev_cp) |p| blk: {\n                    if (p == 0xFFFD or p > 0x10FFFF) break :blk true;\n                    break :blk uucode.grapheme.isBreak(p, curr_cp, &break_state);\n                } else true;\n\n                if (is_break) {\n                    const curr_class = classifyWordClass(curr_cp);\n                    if (have_current_grapheme and isCjkAsciiTransition(current_grapheme_class, curr_class)) {\n                        try result.breaks.append(result.allocator, .{\n                            .byte_offset = current_grapheme_byte_offset,\n                            .char_offset = current_grapheme_char_offset,\n                        });\n                    }\n                    have_current_grapheme = true;\n                    current_grapheme_byte_offset = @intCast(pos + i);\n                    current_grapheme_char_offset = char_offset;\n                    current_grapheme_class = curr_class;\n                }\n\n                if (isAsciiWrapBreak(b0)) {\n                    try result.breaks.append(result.allocator, .{\n                        .byte_offset = @intCast(pos + i),\n                        .char_offset = char_offset,\n                    });\n                }\n                i += 1;\n                if (is_break) {\n                    char_offset += 1;\n                }\n                prev_cp = curr_cp;\n            } else {\n                const dec = decodeUtf8Unchecked(text, pos + i);\n                if (pos + i + dec.len > text.len) break;\n                if (pos + i + dec.len > pos + vector_len) break;\n\n                // Check if this starts a new grapheme cluster\n                // Skip invalid/replacement codepoints or codepoints that might be outside the grapheme table range\n                const is_break = if (dec.cp == 0xFFFD or dec.cp > 0x10FFFF) true else if (prev_cp) |p| blk: {\n                    if (p == 0xFFFD or p > 0x10FFFF) break :blk true;\n                    break :blk uucode.grapheme.isBreak(p, dec.cp, &break_state);\n                } else true;\n\n                if (is_break) {\n                    const curr_class = classifyWordClass(dec.cp);\n                    if (have_current_grapheme and isCjkAsciiTransition(current_grapheme_class, curr_class)) {\n                        try result.breaks.append(result.allocator, .{\n                            .byte_offset = current_grapheme_byte_offset,\n                            .char_offset = current_grapheme_char_offset,\n                        });\n                    }\n                    have_current_grapheme = true;\n                    current_grapheme_byte_offset = @intCast(pos + i);\n                    current_grapheme_char_offset = char_offset;\n                    current_grapheme_class = curr_class;\n                }\n\n                if (isUnicodeWrapBreak(dec.cp)) {\n                    try result.breaks.append(result.allocator, .{\n                        .byte_offset = @intCast(pos + i),\n                        .char_offset = char_offset,\n                    });\n                }\n                i += dec.len;\n                if (is_break) {\n                    char_offset += 1;\n                }\n                prev_cp = dec.cp;\n            }\n        }\n        pos += i;\n    }\n\n    // Tail\n    var i: usize = pos;\n    while (i < text.len) {\n        const b0 = text[i];\n        if (b0 < 0x80) {\n            const curr_cp: u21 = b0;\n            const is_break = if (prev_cp) |p| blk: {\n                if (p == 0xFFFD or p > 0x10FFFF) break :blk true;\n                break :blk uucode.grapheme.isBreak(p, curr_cp, &break_state);\n            } else true;\n\n            if (is_break) {\n                const curr_class = classifyWordClass(curr_cp);\n                if (have_current_grapheme and isCjkAsciiTransition(current_grapheme_class, curr_class)) {\n                    try result.breaks.append(result.allocator, .{\n                        .byte_offset = current_grapheme_byte_offset,\n                        .char_offset = current_grapheme_char_offset,\n                    });\n                }\n                have_current_grapheme = true;\n                current_grapheme_byte_offset = @intCast(i);\n                current_grapheme_char_offset = char_offset;\n                current_grapheme_class = curr_class;\n            }\n\n            if (isAsciiWrapBreak(b0)) {\n                try result.breaks.append(result.allocator, .{\n                    .byte_offset = @intCast(i),\n                    .char_offset = char_offset,\n                });\n            }\n            i += 1;\n            if (is_break) {\n                char_offset += 1;\n            }\n            prev_cp = curr_cp;\n        } else {\n            const dec = decodeUtf8Unchecked(text, i);\n            if (i + dec.len > text.len) break;\n\n            const is_break = if (dec.cp == 0xFFFD or dec.cp > 0x10FFFF) true else if (prev_cp) |p| blk: {\n                if (p == 0xFFFD or p > 0x10FFFF) break :blk true;\n                break :blk uucode.grapheme.isBreak(p, dec.cp, &break_state);\n            } else true;\n\n            if (is_break) {\n                const curr_class = classifyWordClass(dec.cp);\n                if (have_current_grapheme and isCjkAsciiTransition(current_grapheme_class, curr_class)) {\n                    try result.breaks.append(result.allocator, .{\n                        .byte_offset = current_grapheme_byte_offset,\n                        .char_offset = current_grapheme_char_offset,\n                    });\n                }\n                have_current_grapheme = true;\n                current_grapheme_byte_offset = @intCast(i);\n                current_grapheme_char_offset = char_offset;\n                current_grapheme_class = curr_class;\n            }\n\n            if (isUnicodeWrapBreak(dec.cp)) {\n                try result.breaks.append(result.allocator, .{\n                    .byte_offset = @intCast(i),\n                    .char_offset = char_offset,\n                });\n            }\n            i += dec.len;\n            if (is_break) {\n                char_offset += 1;\n            }\n            prev_cp = dec.cp;\n        }\n    }\n}\n\npub fn findTabStops(text: []const u8, result: *TabStopResult) !void {\n    result.reset();\n    const vector_len = 16;\n    const Vec = @Vector(vector_len, u8);\n\n    const vTab: Vec = @splat('\\t');\n\n    var pos: usize = 0;\n\n    while (pos + vector_len <= text.len) {\n        const chunk: Vec = text[pos..][0..vector_len].*;\n        const cmp_tab = chunk == vTab;\n\n        if (@reduce(.Or, cmp_tab)) {\n            var i: usize = 0;\n            while (i < vector_len) : (i += 1) {\n                if (text[pos + i] == '\\t') {\n                    try result.positions.append(result.allocator, pos + i);\n                }\n            }\n        }\n        pos += vector_len;\n    }\n\n    while (pos < text.len) : (pos += 1) {\n        if (text[pos] == '\\t') {\n            try result.positions.append(result.allocator, pos);\n        }\n    }\n}\n\npub fn findLineBreaks(text: []const u8, result: *LineBreakResult) !void {\n    result.reset();\n    const vector_len = 16; // Use 16-byte vectors (SSE2/NEON compatible)\n    const Vec = @Vector(vector_len, u8);\n\n    // Prepare vector constants for '\\n' and '\\r'\n    const vNL: Vec = @splat('\\n');\n    const vCR: Vec = @splat('\\r');\n\n    var pos: usize = 0;\n    var prev_was_cr = false; // Track if previous chunk ended with \\r\n\n    // Process full vector chunks\n    while (pos + vector_len <= text.len) {\n        const chunk: Vec = text[pos..][0..vector_len].*;\n        const cmp_nl = chunk == vNL;\n        const cmp_cr = chunk == vCR;\n\n        // Check if any newline or CR found\n        if (@reduce(.Or, cmp_nl) or @reduce(.Or, cmp_cr)) {\n            // Found a match, process this chunk\n            var i: usize = 0;\n            while (i < vector_len) : (i += 1) {\n                const absolute_index = pos + i;\n                const b = text[absolute_index];\n                if (b == '\\n') {\n                    // Skip if this is the \\n part of a CRLF split across chunks\n                    if (i == 0 and prev_was_cr) {\n                        prev_was_cr = false;\n                        continue;\n                    }\n                    // Check if this is part of CRLF\n                    const kind: LineBreakKind = if (absolute_index > 0 and text[absolute_index - 1] == '\\r') .CRLF else .LF;\n                    try result.breaks.append(result.allocator, .{ .pos = absolute_index, .kind = kind });\n                } else if (b == '\\r') {\n                    // Check for CRLF\n                    if (absolute_index + 1 < text.len and text[absolute_index + 1] == '\\n') {\n                        try result.breaks.append(result.allocator, .{ .pos = absolute_index + 1, .kind = .CRLF });\n                        i += 1; // Skip the \\n in next iteration\n                    } else {\n                        try result.breaks.append(result.allocator, .{ .pos = absolute_index, .kind = .CR });\n                    }\n                }\n            }\n            // Update prev_was_cr for next chunk\n            prev_was_cr = (text[pos + vector_len - 1] == '\\r');\n        } else {\n            prev_was_cr = false;\n        }\n        pos += vector_len;\n    }\n\n    // Handle remaining bytes with scalar code\n    while (pos < text.len) : (pos += 1) {\n        const b = text[pos];\n        if (b == '\\n') {\n            // Handle CRLF split at chunk boundary\n            if (pos > 0 and text[pos - 1] == '\\r') {\n                // Already recorded at pos - 1 or will be skipped\n                if (prev_was_cr) {\n                    prev_was_cr = false;\n                    continue;\n                }\n            }\n            const kind: LineBreakKind = if (pos > 0 and text[pos - 1] == '\\r') .CRLF else .LF;\n            try result.breaks.append(result.allocator, .{ .pos = pos, .kind = kind });\n        } else if (b == '\\r') {\n            if (pos + 1 < text.len and text[pos + 1] == '\\n') {\n                try result.breaks.append(result.allocator, .{ .pos = pos + 1, .kind = .CRLF });\n                pos += 1;\n            } else {\n                try result.breaks.append(result.allocator, .{ .pos = pos, .kind = .CR });\n            }\n        }\n        prev_was_cr = false;\n    }\n}\n\npub const WrapByWidthResult = struct {\n    byte_offset: u32,\n    grapheme_count: u32,\n    columns_used: u32,\n};\n\npub const PosByWidthResult = struct {\n    byte_offset: u32,\n    grapheme_count: u32,\n    columns_used: u32,\n};\n\npub inline fn eastAsianWidth(cp: u21) u32 {\n    if (cp > 0x10FFFF) return 0;\n    const eaw = uucode.get(.east_asian_width, cp);\n    const width = eawToWidth(cp, eaw);\n    return if (width > 0) @intCast(width) else 0;\n}\n\n/// Calculate width from east asian width property and Unicode properties\n/// Returns -1 for control characters (they don't contribute to width)\ninline fn eawToWidth(cp: u21, eaw: uucode.types.EastAsianWidth) i16 {\n    if (cp == 0) return 0;\n    if (cp < 32 or (cp >= 0x7F and cp < 0xA0)) return -1;\n\n    const gc = uucode.get(.general_category, cp);\n    switch (gc) {\n        .mark_nonspacing, .mark_spacing_combining, .mark_enclosing => return 0,\n        else => {},\n    }\n\n    if (cp == 0x200B) return 0;\n    if (cp == 0x200C) return 0;\n    if (cp == 0x200D) return 0;\n    if (cp == 0x2060) return 0;\n    if (cp == 0x034F) return 0;\n    if (cp == 0xFEFF) return 0;\n    if (cp >= 0x180B and cp <= 0x180D) return 0;\n    if (cp >= 0xFE00 and cp <= 0xFE0F) return 0;\n    if (cp >= 0xE0100 and cp <= 0xE01EF) return 0;\n\n    if (eaw == .fullwidth or eaw == .wide) return 2;\n\n    if (cp >= 0x1F000 and cp <= 0x1F02B) return 2;\n    if (cp >= 0x1F030 and cp <= 0x1F093) return 2;\n    if (cp >= 0x1F0A0 and cp <= 0x1F0AE) return 2;\n    if (cp >= 0x1F0B1 and cp <= 0x1F0BF) return 2;\n    if (cp >= 0x1F0C1 and cp <= 0x1F0CF) return 2;\n    if (cp >= 0x1F0D1 and cp <= 0x1F0F5) return 2;\n\n    if (cp == 0x231A or cp == 0x231B) return 2;\n    if (cp == 0x2329 or cp == 0x232A) return 2;\n    if (cp >= 0x23E9 and cp <= 0x23EC) return 2;\n    if (cp == 0x23F0 or cp == 0x23F3) return 2;\n    if (cp >= 0x25FD and cp <= 0x25FE) return 2;\n\n    if (cp >= 0x2614 and cp <= 0x2615) return 2;\n    if (cp == 0x2622 or cp == 0x2623) return 2;\n    if (cp >= 0x2630 and cp <= 0x2637) return 2;\n    if (cp >= 0x2648 and cp <= 0x2653) return 2;\n    if (cp == 0x267F or cp == 0x2693 or cp == 0x269B) return 2;\n    if (cp == 0x26A0 or cp == 0x26A1) return 2;\n    if (cp >= 0x26AA and cp <= 0x26AB) return 2;\n    if (cp >= 0x26BD and cp <= 0x26BE) return 2;\n    if (cp >= 0x26C4 and cp <= 0x26C5) return 2;\n    if (cp == 0x26CE or cp == 0x26D1 or cp == 0x26D4) return 2;\n    if (cp == 0x26EA or cp == 0x26F2 or cp == 0x26F3) return 2;\n    if (cp == 0x26F5 or cp == 0x26FA or cp == 0x26FD) return 2;\n\n    if (cp == 0x203C or cp == 0x2049) return 2;\n    if (cp == 0x2705 or cp >= 0x270A and cp <= 0x270B) return 2;\n    if (cp == 0x2728 or cp == 0x274C or cp == 0x274E) return 2;\n    if (cp >= 0x2753 and cp <= 0x2755) return 2;\n    if (cp == 0x2757) return 2;\n    if (cp >= 0x2760 and cp <= 0x2767) return 2;\n    if (cp >= 0x2795 and cp <= 0x2797) return 2;\n    if (cp == 0x27B0 or cp == 0x27BF) return 2;\n    if (cp >= 0x2B1B and cp <= 0x2B1C) return 2;\n    if (cp >= 0x2B50 and cp <= 0x2B50) return 2;\n    if (cp >= 0x2B55 and cp <= 0x2B55) return 2;\n\n    if (cp >= 0x1F300 and cp <= 0x1F320) return 2;\n    if (cp >= 0x1F32D and cp <= 0x1F335) return 2;\n    if (cp >= 0x1F337 and cp <= 0x1F37C) return 2;\n    if (cp >= 0x1F37E and cp <= 0x1F393) return 2;\n    if (cp >= 0x1F3A0 and cp <= 0x1F3CA) return 2;\n    if (cp >= 0x1F3CF and cp <= 0x1F3D3) return 2;\n    if (cp >= 0x1F3E0 and cp <= 0x1F3F0) return 2;\n    if (cp == 0x1F3F4) return 2;\n    if (cp >= 0x1F3F8 and cp <= 0x1F3FF) return 2;\n    if (cp >= 0x1F400 and cp <= 0x1F43E) return 2;\n    if (cp == 0x1F440) return 2;\n    if (cp >= 0x1F442 and cp <= 0x1F4FC) return 2;\n    if (cp >= 0x1F4FF and cp <= 0x1F6C5) return 2;\n    if (cp == 0x1F6CC) return 2;\n    if (cp >= 0x1F6D0 and cp <= 0x1F6D2) return 2;\n    if (cp >= 0x1F6D5 and cp <= 0x1F6D7) return 2;\n    if (cp >= 0x1F6DC and cp <= 0x1F6DF) return 2;\n    if (cp >= 0x1F6EB and cp <= 0x1F6EC) return 2;\n    if (cp >= 0x1F6F4 and cp <= 0x1F6FC) return 2;\n    if (cp >= 0x1F700 and cp <= 0x1F773) return 2;\n    if (cp >= 0x1F780 and cp <= 0x1F7D8) return 2;\n    if (cp >= 0x1F7E0 and cp <= 0x1F7EB) return 2;\n    if (cp >= 0x1F800 and cp <= 0x1F80B) return 2;\n    if (cp >= 0x1F810 and cp <= 0x1F847) return 2;\n    if (cp >= 0x1F850 and cp <= 0x1F859) return 2;\n    if (cp >= 0x1F860 and cp <= 0x1F887) return 2;\n    if (cp >= 0x1F890 and cp <= 0x1F8AD) return 2;\n    if (cp >= 0x1F8B0 and cp <= 0x1F8B1) return 2;\n    if (cp >= 0x1F90C and cp <= 0x1F93A) return 2;\n    if (cp >= 0x1F93C and cp <= 0x1F945) return 2;\n    if (cp >= 0x1F947 and cp <= 0x1FA53) return 2;\n    if (cp >= 0x1FA60 and cp <= 0x1FA6D) return 2;\n    if (cp >= 0x1FA70 and cp <= 0x1FA74) return 2;\n    if (cp >= 0x1FA78 and cp <= 0x1FA7C) return 2;\n    if (cp >= 0x1FA80 and cp <= 0x1FA86) return 2;\n    if (cp >= 0x1FA90 and cp <= 0x1FAAC) return 2;\n    if (cp >= 0x1FAB0 and cp <= 0x1FABA) return 2;\n    if (cp >= 0x1FAC0 and cp <= 0x1FAC5) return 2;\n    if (cp >= 0x1FAD0 and cp <= 0x1FAD9) return 2;\n    if (cp >= 0x1FAE0 and cp <= 0x1FAE7) return 2;\n    if (cp >= 0x1FAF0 and cp <= 0x1FAF8) return 2;\n\n    return 1;\n}\n\n/// Calculate the display width of a byte in columns\n/// Used for ASCII-only fast paths\ninline fn asciiCharWidth(byte: u8, tab_width: u8) u32 {\n    if (byte == '\\t') {\n        return tab_width;\n    } else if (byte >= 32 and byte <= 126) {\n        return 1;\n    }\n    return 0;\n}\n\n/// Calculate the display width of a character (byte or codepoint) in columns\ninline fn charWidth(byte: u8, codepoint: u21, tab_width: u8) u32 {\n    if (byte == '\\t') {\n        return tab_width;\n    } else if (byte < 0x80 and byte >= 32 and byte <= 126) {\n        return 1;\n    } else if (byte >= 0x80) {\n        const eaw = uucode.get(.east_asian_width, codepoint);\n        const w = eawToWidth(codepoint, eaw);\n        return if (w > 0) @intCast(w) else 0;\n    }\n    return 0;\n}\n\n/// Check if a codepoint is valid for grapheme break detection\ninline fn isValidCodepoint(cp: u21) bool {\n    return cp != 0xFFFD and cp <= 0x10FFFF;\n}\n\n/// Check if there's a grapheme break between two codepoints\n/// - wcwidth mode: use Unicode grapheme clustering for proper rendering,\n///   but calculate width using wcwidth (sum of codepoint widths)\n/// - no_zwj mode: use grapheme breaks but treat ZWJ as a break (ignore joining)\n/// - unicode mode: use standard grapheme cluster segmentation\ninline fn isGraphemeBreak(prev_cp: ?u21, curr_cp: u21, break_state: *uucode.grapheme.BreakState, width_method: WidthMethod) bool {\n    // wcwidth mode uses Unicode grapheme clustering for proper rendering\n    // (ZWJ sequences, skin tone modifiers stay together), but width is\n    // calculated using wcwidth semantics (sum of codepoint widths)\n    if (width_method == .wcwidth) {\n        if (prev_cp == null) return true;\n\n        if (!isValidCodepoint(curr_cp)) return true;\n        if (!isValidCodepoint(prev_cp.?)) return true;\n        return uucode.grapheme.isBreak(prev_cp.?, curr_cp, break_state);\n    }\n\n    if (!isValidCodepoint(curr_cp)) return true;\n\n    // In no_zwj mode, treat ZWJ (U+200D) as NOT joining characters\n    // When we see ZWJ after a character, it's part of that character's grapheme\n    // But when we see a character after ZWJ, it starts a new grapheme\n    if (width_method == .no_zwj) {\n        const ZWJ: u21 = 0x200D;\n        if (prev_cp) |p| {\n            // If previous was ZWJ, current starts a new grapheme\n            // Don't call uucode.grapheme.isBreak because it will say no break\n            if (p == ZWJ) {\n                // Reset break state since we're forcing a break\n                break_state.* = .default;\n                return true;\n            }\n        }\n        // If current is ZWJ, don't break yet - it's part of previous grapheme\n        // (will have width 0 anyway)\n    }\n\n    if (prev_cp) |p| {\n        if (!isValidCodepoint(p)) return true;\n        return uucode.grapheme.isBreak(p, curr_cp, break_state);\n    }\n    return true;\n}\n\n/// State for accumulating grapheme cluster width\nconst GraphemeWidthState = struct {\n    width: u32 = 0,\n    has_width: bool = false,\n    is_regional_indicator_pair: bool = false,\n    has_vs16: bool = false,\n    has_indic_virama: bool = false,\n    width_method: WidthMethod,\n\n    /// Initialize state with the first codepoint of a grapheme cluster\n    inline fn init(first_cp: u21, first_width: u32, width_method: WidthMethod) GraphemeWidthState {\n        return .{\n            .width = first_width,\n            .has_width = (first_width > 0),\n            .is_regional_indicator_pair = (first_cp >= 0x1F1E6 and first_cp <= 0x1F1FF),\n            .has_vs16 = false,\n            .has_indic_virama = false,\n            .width_method = width_method,\n        };\n    }\n\n    /// Add a codepoint to the current grapheme cluster\n    inline fn addCodepoint(self: *GraphemeWidthState, cp: u21, cp_width: u32) void {\n        // wcwidth mode: sum all codepoint widths (tmux-style)\n        if (self.width_method == .wcwidth) {\n            const eaw = uucode.get(.east_asian_width, cp);\n            const w = eawToWidth(cp, eaw);\n            if (w > 0) {\n                self.width += @intCast(w);\n                self.has_width = true;\n            }\n            return;\n        }\n\n        // unicode and no_zwj modes: use grapheme-aware width\n        const is_ri = (cp >= 0x1F1E6 and cp <= 0x1F1FF);\n        const is_vs16 = (cp == 0xFE0F); // Variation Selector-16 (emoji presentation)\n\n        const gc = uucode.get(.general_category, cp);\n        const is_virama = gc == .mark_nonspacing;\n\n        const is_devanagari_ra = (cp == 0x0930);\n\n        const is_devanagari_base = (cp >= 0x0915 and cp <= 0x0939) or (cp >= 0x0958 and cp <= 0x095F);\n\n        if (is_vs16) {\n            self.has_vs16 = true;\n            if (self.has_width and self.width == 1) {\n                self.width = 2;\n            }\n            return;\n        }\n\n        if (is_virama) {\n            self.has_indic_virama = true;\n            return;\n        }\n\n        if (self.is_regional_indicator_pair and is_ri) {\n            self.width += cp_width;\n            self.has_width = true;\n        } else if (!self.has_width and cp_width > 0) {\n            self.width = cp_width;\n            self.has_width = true;\n        } else if (self.has_width and self.has_indic_virama and is_devanagari_base and cp_width > 0) {\n            if (!is_devanagari_ra) {\n                self.width += cp_width;\n            }\n            self.has_indic_virama = false;\n        }\n    }\n};\n\nconst ClusterState = struct {\n    columns_used: u32,\n    grapheme_count: u32,\n    cluster_width: u32,\n    cluster_start: usize,\n    prev_cp: ?u21,\n    break_state: uucode.grapheme.BreakState,\n    width_state: GraphemeWidthState,\n    width_method: WidthMethod,\n    cluster_started: bool,\n\n    fn init(width_method: WidthMethod) ClusterState {\n        const dummy_width_state = GraphemeWidthState.init(0, 0, width_method);\n        return .{\n            .columns_used = 0,\n            .grapheme_count = 0,\n            .cluster_width = 0,\n            .cluster_start = 0,\n            .prev_cp = null,\n            .break_state = .default,\n            .width_state = dummy_width_state,\n            .width_method = width_method,\n            .cluster_started = false,\n        };\n    }\n};\n\n/// Handle grapheme cluster boundary when wrapping by width (stops BEFORE exceeding limit)\n/// Returns true if we should stop (limit exceeded)\ninline fn handleClusterForWrap(\n    state: *ClusterState,\n    is_break: bool,\n    new_cluster_start: usize,\n    max_columns: u32,\n) bool {\n    if (is_break) {\n        if (state.prev_cp != null) {\n            if (state.columns_used + state.cluster_width > max_columns) {\n                return true; // Signal to stop\n            }\n            state.columns_used += state.cluster_width;\n            state.grapheme_count += 1;\n        }\n        state.cluster_width = 0;\n        state.cluster_start = new_cluster_start;\n        state.cluster_started = false;\n    }\n    return false;\n}\n\n/// Handle grapheme cluster boundary when finding position (snaps to grapheme boundaries)\n/// Returns true if we should stop\n///\n/// Snapping behavior:\n/// - include_start_before=true (for selection end): Include graphemes that START at or before max_columns\n///   If max_columns=3 and grapheme occupies columns [2-3], include it (starts at 2 <= 3)\n///   This snaps forward to include the whole grapheme even if max_columns points to its middle\n/// - include_start_before=false (for selection start): Only include graphemes that END before max_columns\n///   If max_columns=3 and grapheme occupies columns [2-3], exclude it (ends at 4 > 3)\n///   This snaps backward to exclude wide graphemes that would cross max_columns\ninline fn handleClusterForPos(\n    state: *ClusterState,\n    is_break: bool,\n    new_cluster_start: usize,\n    max_columns: u32,\n    include_start_before: bool,\n) bool {\n    if (is_break) {\n        if (state.prev_cp != null) {\n            const cluster_start_col = state.columns_used;\n            const cluster_end_col = state.columns_used + state.cluster_width;\n\n            if (include_start_before) {\n                if (cluster_start_col >= max_columns) {\n                    return true;\n                }\n                state.columns_used = cluster_end_col;\n                state.grapheme_count += 1;\n            } else {\n                if (cluster_end_col > max_columns) {\n                    return true; // Signal to stop (don't include this grapheme)\n                }\n                state.columns_used = cluster_end_col;\n            }\n        }\n        state.cluster_width = 0;\n        state.cluster_start = new_cluster_start;\n        state.cluster_started = false;\n    }\n    return false;\n}\n\n/// Find wrap position by width - proxy function that dispatches based on width_method\npub fn findWrapPosByWidth(\n    text: []const u8,\n    max_columns: u32,\n    tab_width: u8,\n    isASCIIOnly: bool,\n    width_method: WidthMethod,\n) WrapByWidthResult {\n    switch (width_method) {\n        .unicode, .no_zwj => return findWrapPosByWidthUnicode(text, max_columns, tab_width, isASCIIOnly, width_method),\n        .wcwidth => return findWrapPosByWidthWCWidth(text, max_columns, tab_width, isASCIIOnly),\n    }\n}\n\n/// Find wrap position by width using Unicode grapheme cluster segmentation\nfn findWrapPosByWidthUnicode(\n    text: []const u8,\n    max_columns: u32,\n    tab_width: u8,\n    isASCIIOnly: bool,\n    width_method: WidthMethod,\n) WrapByWidthResult {\n    if (text.len == 0 or max_columns == 0) {\n        return .{ .byte_offset = 0, .grapheme_count = 0, .columns_used = 0 };\n    }\n\n    // ASCII-only fast path\n    if (isASCIIOnly) {\n        if (max_columns >= text.len) {\n            return .{ .byte_offset = @intCast(text.len), .grapheme_count = @intCast(text.len), .columns_used = @intCast(text.len) };\n        } else {\n            return .{ .byte_offset = max_columns, .grapheme_count = max_columns, .columns_used = max_columns };\n        }\n    }\n\n    const vector_len = 16;\n    var pos: usize = 0;\n    var state = ClusterState.init(width_method);\n\n    while (pos + vector_len <= text.len) {\n        const chunk: @Vector(vector_len, u8) = text[pos..][0..vector_len].*;\n        const ascii_threshold: @Vector(vector_len, u8) = @splat(0x80);\n        const is_non_ascii = chunk >= ascii_threshold;\n\n        if (!@reduce(.Or, is_non_ascii)) {\n            // All ASCII\n            var i: usize = 0;\n            while (i < vector_len) : (i += 1) {\n                const b = text[pos + i];\n                const curr_cp: u21 = b;\n                const is_break = isGraphemeBreak(state.prev_cp, curr_cp, &state.break_state, state.width_method);\n\n                if (handleClusterForWrap(&state, is_break, pos + i, max_columns)) {\n                    return .{ .byte_offset = @intCast(state.cluster_start), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n                }\n\n                const cp_width = asciiCharWidth(b, tab_width);\n                if (!state.cluster_started) {\n                    state.width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n                    state.cluster_width = cp_width;\n                    state.cluster_started = true;\n                } else {\n                    state.width_state.addCodepoint(curr_cp, cp_width);\n                    state.cluster_width = state.width_state.width;\n                }\n                state.prev_cp = curr_cp;\n            }\n            pos += vector_len;\n            continue;\n        }\n\n        // Mixed ASCII/non-ASCII - process rest of chunk\n        var i: usize = 0;\n        while (i < vector_len and pos + i < text.len) {\n            const b0 = text[pos + i];\n            const curr_cp: u21 = if (b0 < 0x80) b0 else decodeUtf8Unchecked(text, pos + i).cp;\n            const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos + i).len;\n\n            if (pos + i + cp_len > text.len) break;\n\n            const is_break = isGraphemeBreak(state.prev_cp, curr_cp, &state.break_state, state.width_method);\n\n            if (handleClusterForWrap(&state, is_break, pos + i, max_columns)) {\n                return .{ .byte_offset = @intCast(state.cluster_start), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n            }\n\n            const cp_width = charWidth(b0, curr_cp, tab_width);\n            if (!state.cluster_started) {\n                state.width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n                state.cluster_width = cp_width;\n                state.cluster_started = true;\n            } else {\n                state.width_state.addCodepoint(curr_cp, cp_width);\n                state.cluster_width = state.width_state.width;\n            }\n            state.prev_cp = curr_cp;\n            i += cp_len;\n        }\n        pos += i; // Advance by how much we actually processed\n    }\n\n    // Tail\n    while (pos < text.len) {\n        const b0 = text[pos];\n        const curr_cp: u21 = if (b0 < 0x80) b0 else decodeUtf8Unchecked(text, pos).cp;\n        const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        const is_break = isGraphemeBreak(state.prev_cp, curr_cp, &state.break_state, state.width_method);\n\n        if (handleClusterForWrap(&state, is_break, pos, max_columns)) {\n            return .{ .byte_offset = @intCast(state.cluster_start), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n        }\n\n        const cp_width = charWidth(b0, curr_cp, tab_width);\n        if (!state.cluster_started) {\n            state.width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n            state.cluster_width = cp_width;\n            state.cluster_started = true;\n        } else {\n            state.width_state.addCodepoint(curr_cp, cp_width);\n            state.cluster_width = state.width_state.width;\n        }\n        state.prev_cp = curr_cp;\n        pos += cp_len;\n    }\n\n    // Final cluster\n    if (state.prev_cp != null and state.cluster_width > 0) {\n        if (state.columns_used + state.cluster_width > max_columns) {\n            return .{ .byte_offset = @intCast(state.cluster_start), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n        }\n        state.columns_used += state.cluster_width;\n        state.grapheme_count += 1;\n    }\n\n    return .{ .byte_offset = @intCast(text.len), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n}\n\n/// Find wrap position by width using wcwidth-style codepoint-by-codepoint processing\nfn findWrapPosByWidthWCWidth(\n    text: []const u8,\n    max_columns: u32,\n    tab_width: u8,\n    isASCIIOnly: bool,\n) WrapByWidthResult {\n    if (text.len == 0 or max_columns == 0) {\n        return .{ .byte_offset = 0, .grapheme_count = 0, .columns_used = 0 };\n    }\n\n    // ASCII-only fast path\n    if (isASCIIOnly) {\n        if (max_columns >= text.len) {\n            return .{ .byte_offset = @intCast(text.len), .grapheme_count = @intCast(text.len), .columns_used = @intCast(text.len) };\n        } else {\n            return .{ .byte_offset = max_columns, .grapheme_count = max_columns, .columns_used = max_columns };\n        }\n    }\n\n    // Unicode path - process each codepoint independently\n    var pos: usize = 0;\n    var columns_used: u32 = 0;\n    var codepoint_count: u32 = 0;\n\n    while (pos < text.len) {\n        const b0 = text[pos];\n        const curr_cp: u21 = if (b0 < 0x80) b0 else blk: {\n            const dec = decodeUtf8Unchecked(text, pos);\n            if (pos + dec.len > text.len) break :blk 0xFFFD;\n            break :blk dec.cp;\n        };\n        const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        if (pos + cp_len > text.len) break;\n\n        const cp_width = charWidth(b0, curr_cp, tab_width);\n\n        // In wcwidth mode, stop if we've already used max_columns\n        // (don't continue adding zero-width chars after reaching limit)\n        if (columns_used >= max_columns) {\n            return .{ .byte_offset = @intCast(pos), .grapheme_count = codepoint_count, .columns_used = columns_used };\n        }\n\n        // Stop if adding this codepoint would exceed max_columns\n        if (columns_used + cp_width > max_columns) {\n            return .{ .byte_offset = @intCast(pos), .grapheme_count = codepoint_count, .columns_used = columns_used };\n        }\n\n        columns_used += cp_width;\n        codepoint_count += 1;\n        pos += cp_len;\n    }\n\n    return .{ .byte_offset = @intCast(text.len), .grapheme_count = codepoint_count, .columns_used = columns_used };\n}\n\n/// Find position by column width - proxy function that dispatches based on width_method\n/// - If include_start_before: include graphemes that START before max_columns (snap forward for selection end)\n///   This ensures that if max_columns points to the middle of a width=2 grapheme, we include the whole grapheme\n/// - If !include_start_before: exclude graphemes that START at or after max_columns (snap backward for selection start)\n///   This ensures that if max_columns points to the middle of a width=2 grapheme, we snap back to exclude it\npub fn findPosByWidth(\n    text: []const u8,\n    max_columns: u32,\n    tab_width: u8,\n    isASCIIOnly: bool,\n    include_start_before: bool,\n    width_method: WidthMethod,\n) PosByWidthResult {\n    switch (width_method) {\n        .unicode, .no_zwj => return findPosByWidthUnicode(text, max_columns, tab_width, isASCIIOnly, include_start_before, width_method),\n        .wcwidth => return findPosByWidthWCWidth(text, max_columns, tab_width, isASCIIOnly, include_start_before),\n    }\n}\n\n/// Find position by column width using Unicode grapheme cluster segmentation\nfn findPosByWidthUnicode(\n    text: []const u8,\n    max_columns: u32,\n    tab_width: u8,\n    isASCIIOnly: bool,\n    include_start_before: bool,\n    width_method: WidthMethod,\n) PosByWidthResult {\n    if (text.len == 0 or max_columns == 0) {\n        return .{ .byte_offset = 0, .grapheme_count = 0, .columns_used = 0 };\n    }\n\n    // ASCII-only fast path\n    if (isASCIIOnly) {\n        if (max_columns >= text.len) {\n            return .{ .byte_offset = @intCast(text.len), .grapheme_count = @intCast(text.len), .columns_used = @intCast(text.len) };\n        } else {\n            return .{ .byte_offset = max_columns, .grapheme_count = max_columns, .columns_used = max_columns };\n        }\n    }\n\n    const vector_len = 16;\n    var pos: usize = 0;\n    var state = ClusterState.init(width_method);\n\n    while (pos + vector_len <= text.len) {\n        const chunk: @Vector(vector_len, u8) = text[pos..][0..vector_len].*;\n        const ascii_threshold: @Vector(vector_len, u8) = @splat(0x80);\n        const is_non_ascii = chunk >= ascii_threshold;\n\n        if (!@reduce(.Or, is_non_ascii)) {\n            // All ASCII\n            var i: usize = 0;\n            while (i < vector_len) : (i += 1) {\n                const b = text[pos + i];\n                const curr_cp: u21 = b;\n                const is_break = isGraphemeBreak(state.prev_cp, curr_cp, &state.break_state, state.width_method);\n\n                if (handleClusterForPos(&state, is_break, pos + i, max_columns, include_start_before)) {\n                    return .{ .byte_offset = @intCast(state.cluster_start), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n                }\n\n                const cp_width = asciiCharWidth(b, tab_width);\n                if (!state.cluster_started) {\n                    state.width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n                    state.cluster_width = cp_width;\n                    state.cluster_started = true;\n                } else {\n                    state.width_state.addCodepoint(curr_cp, cp_width);\n                    state.cluster_width = state.width_state.width;\n                }\n                state.prev_cp = curr_cp;\n            }\n            pos += vector_len;\n            continue;\n        }\n\n        // Mixed ASCII/non-ASCII - process rest of chunk\n        var i: usize = 0;\n        while (i < vector_len and pos + i < text.len) {\n            const b0 = text[pos + i];\n            const curr_cp: u21 = if (b0 < 0x80) b0 else decodeUtf8Unchecked(text, pos + i).cp;\n            const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos + i).len;\n\n            if (pos + i + cp_len > text.len) break;\n\n            const is_break = isGraphemeBreak(state.prev_cp, curr_cp, &state.break_state, state.width_method);\n\n            if (handleClusterForPos(&state, is_break, pos + i, max_columns, include_start_before)) {\n                return .{ .byte_offset = @intCast(state.cluster_start), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n            }\n\n            const cp_width = charWidth(b0, curr_cp, tab_width);\n            if (!state.cluster_started) {\n                state.width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n                state.cluster_width = cp_width;\n                state.cluster_started = true;\n            } else {\n                state.width_state.addCodepoint(curr_cp, cp_width);\n                state.cluster_width = state.width_state.width;\n            }\n            state.prev_cp = curr_cp;\n            i += cp_len;\n        }\n        pos += i; // Advance by how much we actually processed\n    }\n\n    // Tail\n    while (pos < text.len) {\n        const b0 = text[pos];\n        const curr_cp: u21 = if (b0 < 0x80) b0 else decodeUtf8Unchecked(text, pos).cp;\n        const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        const is_break = isGraphemeBreak(state.prev_cp, curr_cp, &state.break_state, state.width_method);\n\n        if (handleClusterForPos(&state, is_break, pos, max_columns, include_start_before)) {\n            return .{ .byte_offset = @intCast(state.cluster_start), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n        }\n\n        const cp_width = charWidth(b0, curr_cp, tab_width);\n        if (!state.cluster_started) {\n            state.width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n            state.cluster_width = cp_width;\n            state.cluster_started = true;\n        } else {\n            state.width_state.addCodepoint(curr_cp, cp_width);\n            state.cluster_width = state.width_state.width;\n        }\n        state.prev_cp = curr_cp;\n        pos += cp_len;\n    }\n\n    // Final cluster\n    if (state.prev_cp != null and state.cluster_width > 0) {\n        if (state.columns_used >= max_columns) {\n            return .{ .byte_offset = @intCast(state.cluster_start), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n        }\n        state.columns_used += state.cluster_width;\n        if (include_start_before) {\n            state.grapheme_count += 1;\n        }\n    }\n\n    return .{ .byte_offset = @intCast(text.len), .grapheme_count = state.grapheme_count, .columns_used = state.columns_used };\n}\n\n/// Find position by column width using wcwidth-style codepoint-by-codepoint processing\nfn findPosByWidthWCWidth(\n    text: []const u8,\n    max_columns: u32,\n    tab_width: u8,\n    isASCIIOnly: bool,\n    include_start_before: bool,\n) PosByWidthResult {\n    if (text.len == 0 or max_columns == 0) {\n        return .{ .byte_offset = 0, .grapheme_count = 0, .columns_used = 0 };\n    }\n\n    // ASCII-only fast path\n    if (isASCIIOnly) {\n        if (max_columns >= text.len) {\n            return .{ .byte_offset = @intCast(text.len), .grapheme_count = @intCast(text.len), .columns_used = @intCast(text.len) };\n        } else {\n            return .{ .byte_offset = max_columns, .grapheme_count = max_columns, .columns_used = max_columns };\n        }\n    }\n\n    // Unicode path - process each codepoint independently\n    var pos: usize = 0;\n    var columns_used: u32 = 0;\n    var codepoint_count: u32 = 0;\n\n    while (pos < text.len) {\n        const b0 = text[pos];\n        const curr_cp: u21 = if (b0 < 0x80) b0 else blk: {\n            const dec = decodeUtf8Unchecked(text, pos);\n            if (pos + dec.len > text.len) break :blk 0xFFFD;\n            break :blk dec.cp;\n        };\n        const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        if (pos + cp_len > text.len) break;\n\n        const cp_width = charWidth(b0, curr_cp, tab_width);\n        const cp_start_col = columns_used;\n        const cp_end_col = columns_used + cp_width;\n\n        // Apply boundary behavior\n        if (include_start_before) {\n            // Selection end: include codepoints that START before max_columns\n            if (cp_start_col >= max_columns) {\n                return .{ .byte_offset = @intCast(pos), .grapheme_count = codepoint_count, .columns_used = columns_used };\n            }\n        } else {\n            // Selection start: only include codepoints that END before or at max_columns\n            // So exclude (stop) if end > max_columns\n            if (cp_end_col > max_columns) {\n                return .{ .byte_offset = @intCast(pos), .grapheme_count = codepoint_count, .columns_used = columns_used };\n            }\n        }\n\n        columns_used = cp_end_col;\n        codepoint_count += 1;\n        pos += cp_len;\n    }\n\n    return .{ .byte_offset = @intCast(text.len), .grapheme_count = codepoint_count, .columns_used = columns_used };\n}\n\n/// Get width at byte offset - proxy function that dispatches based on width_method\npub fn getWidthAt(text: []const u8, byte_offset: usize, tab_width: u8, width_method: WidthMethod) u32 {\n    switch (width_method) {\n        .unicode, .no_zwj => return getWidthAtUnicode(text, byte_offset, tab_width, width_method),\n        .wcwidth => return getWidthAtWCWidth(text, byte_offset, tab_width),\n    }\n}\n\n/// Get width at byte offset using Unicode grapheme cluster segmentation\nfn getWidthAtUnicode(text: []const u8, byte_offset: usize, tab_width: u8, width_method: WidthMethod) u32 {\n    if (byte_offset >= text.len) return 0;\n\n    const b0 = text[byte_offset];\n\n    const first_cp: u21 = if (b0 < 0x80) b0 else blk: {\n        const dec = decodeUtf8Unchecked(text, byte_offset);\n        if (byte_offset + dec.len > text.len) return 1;\n        break :blk dec.cp;\n    };\n\n    const first_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, byte_offset).len;\n\n    var break_state: uucode.grapheme.BreakState = .default;\n    var prev_cp: ?u21 = first_cp;\n    const first_width = charWidth(b0, first_cp, tab_width);\n    var state = GraphemeWidthState.init(first_cp, first_width, width_method);\n\n    var pos = byte_offset + first_len;\n\n    while (pos < text.len) {\n        const b = text[pos];\n        const curr_cp: u21 = if (b < 0x80) b else decodeUtf8Unchecked(text, pos).cp;\n        const cp_len: usize = if (b < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        if (pos + cp_len > text.len) break;\n\n        const is_break = isGraphemeBreak(prev_cp, curr_cp, &break_state, width_method);\n        if (is_break) break;\n\n        const cp_width = charWidth(b, curr_cp, tab_width);\n        state.addCodepoint(curr_cp, cp_width);\n\n        prev_cp = curr_cp;\n        pos += cp_len;\n    }\n\n    return state.width;\n}\n\n/// Get width at byte offset using wcwidth-style codepoint-by-codepoint processing\n/// In wcwidth mode, each codepoint is treated independently - return its width directly\nfn getWidthAtWCWidth(text: []const u8, byte_offset: usize, tab_width: u8) u32 {\n    if (byte_offset >= text.len) return 0;\n\n    const b0 = text[byte_offset];\n\n    const first_cp: u21 = if (b0 < 0x80) b0 else blk: {\n        const dec = decodeUtf8Unchecked(text, byte_offset);\n        if (byte_offset + dec.len > text.len) return 1;\n        break :blk dec.cp;\n    };\n\n    const first_width = charWidth(b0, first_cp, tab_width);\n    return first_width;\n}\n\npub const PrevGraphemeResult = struct {\n    start_offset: usize,\n    width: u32,\n};\n\n/// Get previous grapheme start - proxy function that dispatches based on width_method\npub fn getPrevGraphemeStart(text: []const u8, byte_offset: usize, tab_width: u8, width_method: WidthMethod) ?PrevGraphemeResult {\n    switch (width_method) {\n        .unicode, .no_zwj => return getPrevGraphemeStartUnicode(text, byte_offset, tab_width, width_method),\n        .wcwidth => return getPrevGraphemeStartWCWidth(text, byte_offset, tab_width),\n    }\n}\n\n/// Get previous grapheme start using wcwidth-style codepoint-by-codepoint processing\nfn getPrevGraphemeStartWCWidth(text: []const u8, byte_offset: usize, tab_width: u8) ?PrevGraphemeResult {\n    if (byte_offset == 0 or text.len == 0) return null;\n    if (byte_offset > text.len) return null;\n\n    var pos: usize = 0;\n    var last_result: ?PrevGraphemeResult = null;\n\n    while (pos < byte_offset) {\n        const b = text[pos];\n        const curr_cp: u21 = if (b < 0x80) b else blk: {\n            const dec = decodeUtf8Unchecked(text, pos);\n            if (pos + dec.len > text.len) break :blk 0xFFFD;\n            break :blk dec.cp;\n        };\n        const cp_len: usize = if (b < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n        const cp_width = charWidth(b, curr_cp, tab_width);\n\n        if (cp_width > 0) {\n            last_result = .{\n                .start_offset = pos,\n                .width = cp_width,\n            };\n        }\n        pos += cp_len;\n    }\n\n    return last_result;\n}\n\n/// Get previous grapheme start using Unicode grapheme cluster segmentation\nfn getPrevGraphemeStartUnicode(text: []const u8, byte_offset: usize, tab_width: u8, width_method: WidthMethod) ?PrevGraphemeResult {\n    if (byte_offset == 0 or text.len == 0) return null;\n    if (byte_offset > text.len) return null;\n\n    // For unicode/no_zwj modes, use grapheme cluster detection\n    var break_state: uucode.grapheme.BreakState = .default;\n    var pos: usize = 0;\n    var prev_cp: ?u21 = null;\n    var prev_grapheme_start: usize = 0;\n    var second_to_last_grapheme_start: usize = 0;\n\n    while (pos < byte_offset) {\n        const b = text[pos];\n        const curr_cp: u21 = if (b < 0x80) b else blk: {\n            const dec = decodeUtf8Unchecked(text, pos);\n            if (pos + dec.len > text.len) break :blk 0xFFFD;\n            break :blk dec.cp;\n        };\n\n        const cp_len: usize = if (b < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        if (isValidCodepoint(curr_cp)) {\n            const is_break = if (prev_cp) |p| blk: {\n                if (!isValidCodepoint(p)) break :blk true;\n                break :blk uucode.grapheme.isBreak(p, curr_cp, &break_state);\n            } else true;\n\n            if (is_break) {\n                second_to_last_grapheme_start = prev_grapheme_start;\n                prev_grapheme_start = pos;\n            }\n\n            prev_cp = curr_cp;\n        }\n\n        pos += cp_len;\n    }\n\n    if (prev_grapheme_start == 0 and byte_offset == 0) {\n        return null;\n    }\n\n    const start_offset = if (prev_grapheme_start < byte_offset) prev_grapheme_start else second_to_last_grapheme_start;\n    const width = getWidthAt(text, start_offset, tab_width, width_method);\n\n    return .{\n        .start_offset = start_offset,\n        .width = width,\n    };\n}\n\n/// Calculate the display width of text - proxy function that dispatches based on width_method\npub fn calculateTextWidth(text: []const u8, tab_width: u8, isASCIIOnly: bool, width_method: WidthMethod) u32 {\n    switch (width_method) {\n        .unicode, .no_zwj => return calculateTextWidthUnicode(text, tab_width, isASCIIOnly, width_method),\n        .wcwidth => return calculateTextWidthWCWidth(text, tab_width, isASCIIOnly),\n    }\n}\n\n/// Calculate text width using Unicode grapheme cluster segmentation\nfn calculateTextWidthUnicode(text: []const u8, tab_width: u8, isASCIIOnly: bool, width_method: WidthMethod) u32 {\n    if (text.len == 0) return 0;\n\n    // ASCII-only fast path\n    if (isASCIIOnly) {\n        return @intCast(text.len);\n    }\n\n    // General case with Unicode support and grapheme cluster handling\n    var total_width: u32 = 0;\n    var pos: usize = 0;\n    var prev_cp: ?u21 = null;\n    var break_state: uucode.grapheme.BreakState = .default;\n    var state: GraphemeWidthState = undefined;\n\n    while (pos < text.len) {\n        const b0 = text[pos];\n        const curr_cp: u21 = if (b0 < 0x80) b0 else blk: {\n            const dec = decodeUtf8Unchecked(text, pos);\n            if (pos + dec.len > text.len) break :blk 0xFFFD;\n            break :blk dec.cp;\n        };\n        const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n        const is_break = isGraphemeBreak(prev_cp, curr_cp, &break_state, width_method);\n\n        if (is_break) {\n            if (prev_cp != null) {\n                total_width += state.width;\n            }\n\n            const cp_width = charWidth(b0, curr_cp, tab_width);\n            state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n        } else {\n            const cp_width = charWidth(b0, curr_cp, tab_width);\n            state.addCodepoint(curr_cp, cp_width);\n        }\n\n        prev_cp = curr_cp;\n        pos += cp_len;\n    }\n\n    if (prev_cp != null) {\n        total_width += state.width;\n    }\n\n    return total_width;\n}\n\n/// Calculate text width using wcwidth-style codepoint-by-codepoint processing\nfn calculateTextWidthWCWidth(text: []const u8, tab_width: u8, isASCIIOnly: bool) u32 {\n    if (text.len == 0) return 0;\n\n    // ASCII-only fast path\n    if (isASCIIOnly) {\n        return @intCast(text.len);\n    }\n\n    // Unicode path - sum width of all codepoints\n    var total_width: u32 = 0;\n    var pos: usize = 0;\n\n    while (pos < text.len) {\n        const b0 = text[pos];\n        const curr_cp: u21 = if (b0 < 0x80) b0 else blk: {\n            const dec = decodeUtf8Unchecked(text, pos);\n            if (pos + dec.len > text.len) break :blk 0xFFFD;\n            break :blk dec.cp;\n        };\n        const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        const cp_width = charWidth(b0, curr_cp, tab_width);\n        total_width += cp_width;\n\n        pos += cp_len;\n    }\n\n    return total_width;\n}\n\n/// Grapheme cluster information for caching\npub const GraphemeInfo = struct {\n    byte_offset: u32,\n    byte_len: u8,\n    width: u8,\n    col_offset: u32,\n};\n\npub const GraphemeInfoResult = struct {\n    graphemes: std.ArrayList(GraphemeInfo),\n\n    pub fn init(allocator: std.mem.Allocator) GraphemeInfoResult {\n        return .{\n            .graphemes = std.ArrayList(GraphemeInfo).init(allocator),\n        };\n    }\n\n    pub fn deinit(self: *GraphemeInfoResult) void {\n        self.graphemes.deinit();\n    }\n\n    pub fn reset(self: *GraphemeInfoResult) void {\n        self.graphemes.clearRetainingCapacity();\n    }\n};\n\n/// Find all grapheme clusters in text and return info for multi-byte graphemes and tabs\n/// This is a proxy function that dispatches to the appropriate implementation based on width_method\npub fn findGraphemeInfo(\n    text: []const u8,\n    tab_width: u8,\n    isASCIIOnly: bool,\n    width_method: WidthMethod,\n    allocator: std.mem.Allocator,\n    result: *std.ArrayListUnmanaged(GraphemeInfo),\n) !void {\n    switch (width_method) {\n        .unicode, .no_zwj => try findGraphemeInfoUnicode(text, tab_width, isASCIIOnly, width_method, allocator, result),\n        .wcwidth => try findGraphemeInfoWCWidth(text, tab_width, isASCIIOnly, allocator, result),\n    }\n}\n\n/// Find all grapheme clusters using Unicode grapheme cluster segmentation\n/// This version treats grapheme clusters as single units for width calculation\nfn findGraphemeInfoUnicode(\n    text: []const u8,\n    tab_width: u8,\n    isASCIIOnly: bool,\n    width_method: WidthMethod,\n    allocator: std.mem.Allocator,\n    result: *std.ArrayListUnmanaged(GraphemeInfo),\n) !void {\n    // In wcwidth mode, always process to capture combining marks on ASCII\n    if (isASCIIOnly and width_method != .wcwidth) {\n        return;\n    }\n\n    if (text.len == 0) {\n        return;\n    }\n\n    const vector_len = 16;\n    var pos: usize = 0;\n    var col: u32 = 0;\n    var prev_cp: ?u21 = null;\n    var break_state: uucode.grapheme.BreakState = .default;\n\n    // Track current grapheme cluster\n    var cluster_start: usize = 0;\n    var cluster_start_col: u32 = 0;\n    var cluster_width_state: GraphemeWidthState = undefined;\n    var cluster_is_multibyte: bool = false;\n    var cluster_is_tab: bool = false;\n\n    while (pos + vector_len <= text.len) {\n        const chunk: @Vector(vector_len, u8) = text[pos..][0..vector_len].*;\n        const ascii_threshold: @Vector(vector_len, u8) = @splat(0x80);\n        const is_non_ascii = chunk >= ascii_threshold;\n\n        // Fast path: all ASCII\n        if (!@reduce(.Or, is_non_ascii)) {\n            var i: usize = 0;\n            while (i < vector_len) : (i += 1) {\n                const b = text[pos + i];\n                const curr_cp: u21 = b;\n                const is_break = isGraphemeBreak(prev_cp, curr_cp, &break_state, width_method);\n\n                if (is_break) {\n                    if (prev_cp != null and (cluster_is_multibyte or cluster_is_tab)) {\n                        if (cluster_width_state.width > 0 or width_method == .wcwidth) {\n                            const cluster_byte_len = (pos + i) - cluster_start;\n                            try result.append(allocator, GraphemeInfo{\n                                .byte_offset = @intCast(cluster_start),\n                                .byte_len = @intCast(cluster_byte_len),\n                                .width = @intCast(cluster_width_state.width),\n                                .col_offset = cluster_start_col,\n                            });\n                        }\n                        col += cluster_width_state.width;\n                    } else if (prev_cp != null) {\n                        col += cluster_width_state.width;\n                    }\n\n                    cluster_start = pos + i;\n                    cluster_start_col = col;\n                    cluster_is_tab = (b == '\\t');\n                    cluster_is_multibyte = false;\n\n                    const cp_width = asciiCharWidth(b, tab_width);\n                    cluster_width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n                } else {\n                    // Continuing cluster (shouldn't happen for ASCII, but handle it)\n                    const cp_width = asciiCharWidth(b, tab_width);\n                    cluster_width_state.addCodepoint(curr_cp, cp_width);\n                }\n\n                prev_cp = curr_cp;\n            }\n            pos += vector_len;\n            continue;\n        }\n\n        // Slow path: mixed ASCII/non-ASCII\n        var i: usize = 0;\n        while (i < vector_len and pos + i < text.len) {\n            const b0 = text[pos + i];\n            const curr_cp: u21 = if (b0 < 0x80) b0 else decodeUtf8Unchecked(text, pos + i).cp;\n            const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos + i).len;\n\n            if (pos + i + cp_len > text.len) break;\n\n            const is_break = isGraphemeBreak(prev_cp, curr_cp, &break_state, width_method);\n\n            if (is_break) {\n                if (prev_cp != null and (cluster_is_multibyte or cluster_is_tab)) {\n                    if (cluster_width_state.width > 0 or width_method == .wcwidth) {\n                        const cluster_byte_len = (pos + i) - cluster_start;\n                        try result.append(allocator, GraphemeInfo{\n                            .byte_offset = @intCast(cluster_start),\n                            .byte_len = @intCast(cluster_byte_len),\n                            .width = @intCast(cluster_width_state.width),\n                            .col_offset = cluster_start_col,\n                        });\n                    }\n                    col += cluster_width_state.width;\n                } else if (prev_cp != null) {\n                    col += cluster_width_state.width;\n                }\n\n                cluster_start = pos + i;\n                cluster_start_col = col;\n                cluster_is_tab = (b0 == '\\t');\n                cluster_is_multibyte = (cp_len != 1);\n\n                const cp_width = charWidth(b0, curr_cp, tab_width);\n                cluster_width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n            } else {\n                cluster_is_multibyte = cluster_is_multibyte or (cp_len != 1);\n                const cp_width = charWidth(b0, curr_cp, tab_width);\n                cluster_width_state.addCodepoint(curr_cp, cp_width);\n            }\n\n            prev_cp = curr_cp;\n            i += cp_len;\n        }\n        pos += i;\n    }\n\n    // Tail processing\n    while (pos < text.len) {\n        const b0 = text[pos];\n        const curr_cp: u21 = if (b0 < 0x80) b0 else decodeUtf8Unchecked(text, pos).cp;\n        const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        if (pos + cp_len > text.len) break;\n\n        const is_break = isGraphemeBreak(prev_cp, curr_cp, &break_state, width_method);\n\n        if (is_break) {\n            if (prev_cp != null and (cluster_is_multibyte or cluster_is_tab)) {\n                if (cluster_width_state.width > 0 or width_method == .wcwidth) {\n                    const cluster_byte_len = pos - cluster_start;\n                    try result.append(allocator, GraphemeInfo{\n                        .byte_offset = @intCast(cluster_start),\n                        .byte_len = @intCast(cluster_byte_len),\n                        .width = @intCast(cluster_width_state.width),\n                        .col_offset = cluster_start_col,\n                    });\n                }\n                col += cluster_width_state.width;\n            } else if (prev_cp != null) {\n                col += cluster_width_state.width;\n            }\n\n            cluster_start = pos;\n            cluster_start_col = col;\n            cluster_is_tab = (b0 == '\\t');\n            cluster_is_multibyte = (cp_len != 1);\n\n            const cp_width = charWidth(b0, curr_cp, tab_width);\n            cluster_width_state = GraphemeWidthState.init(curr_cp, cp_width, width_method);\n        } else {\n            cluster_is_multibyte = cluster_is_multibyte or (cp_len != 1);\n            const cp_width = charWidth(b0, curr_cp, tab_width);\n            cluster_width_state.addCodepoint(curr_cp, cp_width);\n        }\n\n        prev_cp = curr_cp;\n        pos += cp_len;\n    }\n\n    if (prev_cp != null and (cluster_is_multibyte or cluster_is_tab)) {\n        if (cluster_width_state.width > 0 or width_method == .wcwidth) {\n            const cluster_byte_len = text.len - cluster_start;\n            try result.append(allocator, GraphemeInfo{\n                .byte_offset = @intCast(cluster_start),\n                .byte_len = @intCast(cluster_byte_len),\n                .width = @intCast(cluster_width_state.width),\n                .col_offset = cluster_start_col,\n            });\n        }\n    }\n}\n\n/// Find all grapheme clusters using wcwidth-style codepoint-by-codepoint processing\n/// This version treats each codepoint as a separate character (tmux/wcwidth behavior)\nfn findGraphemeInfoWCWidth(\n    text: []const u8,\n    tab_width: u8,\n    isASCIIOnly: bool,\n    allocator: std.mem.Allocator,\n    result: *std.ArrayListUnmanaged(GraphemeInfo),\n) !void {\n    // wcwidth mode should still produce the same grapheme cluster boundaries as Unicode\n    // (so ZWJ sequences and combining marks stay together), but the width of each cluster\n    // is calculated using wcwidth (sum of codepoint widths). This keeps rendering coherent\n    // while preserving tmux-style widths.\n    if (isASCIIOnly) {\n        return;\n    }\n\n    if (text.len == 0) {\n        return;\n    }\n\n    var pos: usize = 0;\n    var col: u32 = 0;\n    var prev_cp: ?u21 = null;\n    var break_state: uucode.grapheme.BreakState = .default;\n\n    // Track current cluster\n    var cluster_start: usize = 0;\n    var cluster_start_col: u32 = 0;\n    var cluster_width_state: GraphemeWidthState = undefined;\n    var cluster_is_multibyte: bool = false;\n    var cluster_is_tab: bool = false;\n    var cluster_started = false;\n\n    while (pos < text.len) {\n        const b0 = text[pos];\n        const curr_cp: u21 = if (b0 < 0x80) b0 else blk: {\n            const dec = decodeUtf8Unchecked(text, pos);\n            if (pos + dec.len > text.len) break :blk 0xFFFD;\n            break :blk dec.cp;\n        };\n        const cp_len: usize = if (b0 < 0x80) 1 else decodeUtf8Unchecked(text, pos).len;\n\n        if (pos + cp_len > text.len) break;\n\n        // Use wcwidth break detection (each codepoint is separate, tmux-style)\n        const is_break = isGraphemeBreak(prev_cp, curr_cp, &break_state, .wcwidth);\n\n        if (is_break) {\n            if (cluster_started and (cluster_is_multibyte or cluster_is_tab)) {\n                try result.append(allocator, GraphemeInfo{\n                    .byte_offset = @intCast(cluster_start),\n                    .byte_len = @intCast(pos - cluster_start),\n                    .width = @intCast(cluster_width_state.width),\n                    .col_offset = cluster_start_col,\n                });\n                col += cluster_width_state.width;\n            } else if (cluster_started) {\n                // Still need to advance col by cluster width even if not emitted\n                col += cluster_width_state.width;\n            }\n\n            // Start a new cluster\n            cluster_start = pos;\n            cluster_start_col = col;\n            cluster_is_tab = (b0 == '\\t');\n            cluster_is_multibyte = (cp_len != 1);\n            const cp_width = charWidth(b0, curr_cp, tab_width);\n            cluster_width_state = GraphemeWidthState.init(curr_cp, cp_width, .wcwidth);\n            cluster_started = true;\n        } else {\n            // Continuing cluster\n            cluster_is_multibyte = cluster_is_multibyte or (cp_len != 1);\n            const cp_width = charWidth(b0, curr_cp, tab_width);\n            cluster_width_state.addCodepoint(curr_cp, cp_width);\n        }\n\n        prev_cp = curr_cp;\n        pos += cp_len;\n    }\n\n    // Commit final cluster\n    if (cluster_started) {\n        if (cluster_is_multibyte or cluster_is_tab) {\n            try result.append(allocator, GraphemeInfo{\n                .byte_offset = @intCast(cluster_start),\n                .byte_len = @intCast(text.len - cluster_start),\n                .width = @intCast(cluster_width_state.width),\n                .col_offset = cluster_start_col,\n            });\n            col += cluster_width_state.width;\n        } else {\n            col += cluster_width_state.width;\n        }\n    }\n}\n"
  },
  {
    "path": "packages/core/src/zig/utils.zig",
    "content": "const std = @import(\"std\");\n\n/// RGBA color type (4 f32 values)\npub const RGBA = @Vector(4, f32);\n\n/// Convert a pointer to 4 f32 values into an RGBA color\npub fn f32PtrToRGBA(ptr: [*]const f32) RGBA {\n    return .{ ptr[0], ptr[1], ptr[2], ptr[3] };\n}\n"
  },
  {
    "path": "packages/core/src/zig-structs.ts",
    "content": "import { defineStruct, defineEnum } from \"bun-ffi-structs\"\nimport { ptr, toArrayBuffer, type Pointer } from \"bun:ffi\"\nimport { RGBA } from \"./lib/RGBA.js\"\n\nconst rgbaPackTransform = (rgba?: RGBA) => (rgba ? ptr(rgba.buffer) : null)\nconst rgbaUnpackTransform = (ptr?: Pointer) => (ptr ? RGBA.fromArray(new Float32Array(toArrayBuffer(ptr))) : undefined)\n\ntype StyledChunkInput = {\n  text: string\n  fg?: RGBA | null\n  bg?: RGBA | null\n  attributes?: number | null\n  link?: { url: string } | string | null\n}\n\nexport const StyledChunkStruct = defineStruct(\n  [\n    [\"text\", \"char*\"],\n    [\"text_len\", \"u64\", { lengthOf: \"text\" }],\n    [\n      \"fg\",\n      \"pointer\",\n      {\n        optional: true,\n        packTransform: rgbaPackTransform,\n        unpackTransform: rgbaUnpackTransform,\n      },\n    ],\n    [\n      \"bg\",\n      \"pointer\",\n      {\n        optional: true,\n        packTransform: rgbaPackTransform,\n        unpackTransform: rgbaUnpackTransform,\n      },\n    ],\n    [\"attributes\", \"u32\", { default: 0 }],\n    [\"link\", \"char*\", { default: \"\" }],\n    [\"link_len\", \"u64\", { lengthOf: \"link\" }],\n  ],\n  {\n    mapValue: (chunk: StyledChunkInput): StyledChunkInput => {\n      if (!chunk.link || typeof chunk.link === \"string\") {\n        return chunk\n      }\n\n      return {\n        ...chunk,\n        link: chunk.link.url,\n      }\n    },\n  },\n)\n\nexport const HighlightStruct = defineStruct([\n  [\"start\", \"u32\"],\n  [\"end\", \"u32\"],\n  [\"styleId\", \"u32\"],\n  [\"priority\", \"u8\", { default: 0 }],\n  [\"hlRef\", \"u16\", { default: 0 }],\n])\n\nexport const LogicalCursorStruct = defineStruct([\n  [\"row\", \"u32\"],\n  [\"col\", \"u32\"],\n  [\"offset\", \"u32\"],\n])\n\nexport const VisualCursorStruct = defineStruct([\n  [\"visualRow\", \"u32\"],\n  [\"visualCol\", \"u32\"],\n  [\"logicalRow\", \"u32\"],\n  [\"logicalCol\", \"u32\"],\n  [\"offset\", \"u32\"],\n])\n\nconst UnicodeMethodEnum = defineEnum({ wcwidth: 0, unicode: 1 }, \"u8\")\n\nexport const TerminalCapabilitiesStruct = defineStruct([\n  [\"kitty_keyboard\", \"bool_u8\"],\n  [\"kitty_graphics\", \"bool_u8\"],\n  [\"rgb\", \"bool_u8\"],\n  [\"unicode\", UnicodeMethodEnum],\n  [\"sgr_pixels\", \"bool_u8\"],\n  [\"color_scheme_updates\", \"bool_u8\"],\n  [\"explicit_width\", \"bool_u8\"],\n  [\"scaled_text\", \"bool_u8\"],\n  [\"sixel\", \"bool_u8\"],\n  [\"focus_tracking\", \"bool_u8\"],\n  [\"sync\", \"bool_u8\"],\n  [\"bracketed_paste\", \"bool_u8\"],\n  [\"hyperlinks\", \"bool_u8\"],\n  [\"osc52\", \"bool_u8\"],\n  [\"explicit_cursor_positioning\", \"bool_u8\"],\n  [\"term_name\", \"char*\"],\n  [\"term_name_len\", \"u64\", { lengthOf: \"term_name\" }],\n  [\"term_version\", \"char*\"],\n  [\"term_version_len\", \"u64\", { lengthOf: \"term_version\" }],\n  [\"term_from_xtversion\", \"bool_u8\"],\n])\n\nexport const EncodedCharStruct = defineStruct([\n  [\"width\", \"u8\"],\n  [\"char\", \"u32\"],\n])\n\nexport const LineInfoStruct = defineStruct([\n  [\"startCols\", [\"u32\"]],\n  [\"startColsLen\", \"u32\", { lengthOf: \"startCols\" }],\n  [\"widthCols\", [\"u32\"]],\n  [\"widthColsLen\", \"u32\", { lengthOf: \"widthCols\" }],\n  [\"sources\", [\"u32\"]],\n  [\"sourcesLen\", \"u32\", { lengthOf: \"sources\" }],\n  [\"wraps\", [\"u32\"]],\n  [\"wrapsLen\", \"u32\", { lengthOf: \"wraps\" }],\n  [\"widthColsMax\", \"u32\"],\n])\n\nexport const MeasureResultStruct = defineStruct([\n  [\"lineCount\", \"u32\"],\n  [\"widthColsMax\", \"u32\"],\n])\n\nexport const CursorStateStruct = defineStruct([\n  [\"x\", \"u32\"],\n  [\"y\", \"u32\"],\n  [\"visible\", \"bool_u8\"],\n  [\"style\", \"u8\"],\n  [\"blinking\", \"bool_u8\"],\n  [\"r\", \"f32\"],\n  [\"g\", \"f32\"],\n  [\"b\", \"f32\"],\n  [\"a\", \"f32\"],\n])\n\nexport const CursorStyleOptionsStruct = defineStruct([\n  [\"style\", \"u8\", { default: 255 }],\n  [\"blinking\", \"u8\", { default: 255 }],\n  [\n    \"color\",\n    \"pointer\",\n    {\n      optional: true,\n      packTransform: rgbaPackTransform,\n      unpackTransform: rgbaUnpackTransform,\n    },\n  ],\n  [\"cursor\", \"u8\", { default: 255 }],\n])\n\nexport const GridDrawOptionsStruct = defineStruct([\n  [\"drawInner\", \"bool_u8\", { default: true }],\n  [\"drawOuter\", \"bool_u8\", { default: true }],\n])\n\nexport type BuildOptions = {\n  gpaSafeStats: boolean\n  gpaMemoryLimitTracking: boolean\n}\n\nexport const BuildOptionsStruct = defineStruct([\n  [\"gpaSafeStats\", \"bool_u8\"],\n  [\"gpaMemoryLimitTracking\", \"bool_u8\"],\n])\n\nexport type AllocatorStats = {\n  totalRequestedBytes: number\n  activeAllocations: number\n  smallAllocations: number\n  largeAllocations: number\n  requestedBytesValid: boolean\n}\n\nexport const AllocatorStatsStruct = defineStruct([\n  [\"totalRequestedBytes\", \"u64\"],\n  [\"activeAllocations\", \"u64\"],\n  [\"smallAllocations\", \"u64\"],\n  [\"largeAllocations\", \"u64\"],\n  [\"requestedBytesValid\", \"bool_u8\"],\n])\n\nexport type GrowthPolicy = \"grow\" | \"block\"\n\nexport type NativeSpanFeedOptions = {\n  chunkSize?: number\n  initialChunks?: number\n  maxBytes?: bigint\n  growthPolicy?: GrowthPolicy\n  autoCommitOnFull?: boolean\n  spanQueueCapacity?: number\n}\n\nexport type NativeSpanFeedStats = {\n  bytesWritten: bigint\n  spansCommitted: bigint\n  chunks: number\n  pendingSpans: number\n}\n\nexport type SpanInfo = {\n  chunkPtr: Pointer\n  offset: number\n  len: number\n  chunkIndex: number\n}\n\nexport type ReserveInfo = {\n  ptr: Pointer\n  len: number\n}\n\nconst GrowthPolicyEnum = defineEnum({ grow: 0, block: 1 }, \"u8\")\n\nexport const NativeSpanFeedOptionsStruct = defineStruct([\n  [\"chunkSize\", \"u32\", { default: 64 * 1024 }],\n  [\"initialChunks\", \"u32\", { default: 2 }],\n  [\"maxBytes\", \"u64\", { default: 0n }],\n  [\"growthPolicy\", GrowthPolicyEnum, { default: \"grow\" }],\n  [\"autoCommitOnFull\", \"bool_u8\", { default: true }],\n  [\"spanQueueCapacity\", \"u32\", { default: 0 }],\n])\n\nexport const NativeSpanFeedStatsStruct = defineStruct([\n  [\"bytesWritten\", \"u64\"],\n  [\"spansCommitted\", \"u64\"],\n  [\"chunks\", \"u32\"],\n  [\"pendingSpans\", \"u32\"],\n])\n\nexport const SpanInfoStruct = defineStruct(\n  [\n    [\"chunkPtr\", \"pointer\"],\n    [\"offset\", \"u32\"],\n    [\"len\", \"u32\"],\n    [\"chunkIndex\", \"u32\"],\n    [\"reserved\", \"u32\", { default: 0 }],\n  ],\n  {\n    reduceValue: (value: { chunkPtr: Pointer; offset: number; len: number; chunkIndex: number }) => ({\n      chunkPtr: value.chunkPtr as Pointer,\n      offset: value.offset,\n      len: value.len,\n      chunkIndex: value.chunkIndex,\n    }),\n  },\n)\n\nexport const ReserveInfoStruct = defineStruct(\n  [\n    [\"ptr\", \"pointer\"],\n    [\"len\", \"u32\"],\n    [\"reserved\", \"u32\", { default: 0 }],\n  ],\n  {\n    reduceValue: (value: { ptr: Pointer; len: number }) => ({\n      ptr: value.ptr as Pointer,\n      len: value.len,\n    }),\n  },\n)\n"
  },
  {
    "path": "packages/core/src/zig.ts",
    "content": "import { dlopen, toArrayBuffer, JSCallback, ptr, type Pointer } from \"bun:ffi\"\nimport { existsSync, writeFileSync } from \"fs\"\nimport { EventEmitter } from \"events\"\nimport {\n  type CursorStyle,\n  type CursorStyleOptions,\n  type TargetChannel,\n  type DebugOverlayCorner,\n  type WidthMethod,\n  type Highlight,\n  type LineInfo,\n  type MousePointerStyle,\n} from \"./types.js\"\nexport type { LineInfo, AllocatorStats, BuildOptions }\n\nimport { RGBA } from \"./lib/RGBA.js\"\nimport { OptimizedBuffer } from \"./buffer.js\"\nimport { TextBuffer } from \"./text-buffer.js\"\nimport { env, registerEnvVar } from \"./lib/env.js\"\nimport {\n  StyledChunkStruct,\n  HighlightStruct,\n  LogicalCursorStruct,\n  VisualCursorStruct,\n  TerminalCapabilitiesStruct,\n  EncodedCharStruct,\n  LineInfoStruct,\n  MeasureResultStruct,\n  CursorStateStruct,\n  CursorStyleOptionsStruct,\n  GridDrawOptionsStruct,\n  NativeSpanFeedOptionsStruct,\n  NativeSpanFeedStatsStruct,\n  ReserveInfoStruct,\n  BuildOptionsStruct,\n  AllocatorStatsStruct,\n} from \"./zig-structs.js\"\nimport type {\n  NativeSpanFeedOptions,\n  NativeSpanFeedStats,\n  ReserveInfo,\n  BuildOptions,\n  AllocatorStats,\n} from \"./zig-structs.js\"\nimport { isBunfsPath } from \"./lib/bunfs.js\"\n\nconst module = await import(`@opentui/core-${process.platform}-${process.arch}/index.ts`)\nlet targetLibPath = module.default\n\nif (isBunfsPath(targetLibPath)) {\n  targetLibPath = targetLibPath.replace(\"../\", \"\")\n}\n\nif (!existsSync(targetLibPath)) {\n  throw new Error(`opentui is not supported on the current platform: ${process.platform}-${process.arch}`)\n}\n\nregisterEnvVar({\n  name: \"OTUI_DEBUG_FFI\",\n  description: \"Enable debug logging for the FFI bindings.\",\n  type: \"boolean\",\n  default: false,\n})\n\nregisterEnvVar({\n  name: \"OTUI_TRACE_FFI\",\n  description: \"Enable tracing for the FFI bindings.\",\n  type: \"boolean\",\n  default: false,\n})\n\n// Env vars used in terminal.zig\nregisterEnvVar({\n  name: \"OPENTUI_FORCE_WCWIDTH\",\n  description: \"Use wcwidth for character width calculations\",\n  type: \"boolean\",\n  default: false,\n})\nregisterEnvVar({\n  name: \"OPENTUI_FORCE_UNICODE\",\n  description: \"Force Mode 2026 Unicode support in terminal capabilities\",\n  type: \"boolean\",\n  default: false,\n})\nregisterEnvVar({\n  name: \"OPENTUI_GRAPHICS\",\n  description: \"Enable Kitty graphics protocol detection\",\n  type: \"boolean\",\n  default: true,\n})\nregisterEnvVar({\n  name: \"OPENTUI_FORCE_NOZWJ\",\n  description: \"Use no_zwj width method (Unicode without ZWJ joining)\",\n  type: \"boolean\",\n  default: false,\n})\n\n// Cursor & mouse pointer style mappings (avoid recreation on each call)\nconst CURSOR_STYLE_TO_ID = { block: 0, line: 1, underline: 2, default: 3 } as const\nconst CURSOR_ID_TO_STYLE = [\"block\", \"line\", \"underline\", \"default\"] as const\nconst MOUSE_STYLE_TO_ID = { default: 0, pointer: 1, text: 2, crosshair: 3, move: 4, \"not-allowed\": 5 } as const\n\n// Global singleton state for FFI tracing to prevent duplicate exit handlers\nlet globalTraceSymbols: Record<string, number[]> | null = null\nlet globalFFILogPath: string | null = null\nlet exitHandlerRegistered = false\n\nfunction toPointer(value: number | bigint): Pointer {\n  if (typeof value === \"bigint\") {\n    if (value > BigInt(Number.MAX_SAFE_INTEGER)) {\n      throw new Error(\"Pointer exceeds safe integer range\")\n    }\n    return Number(value) as Pointer\n  }\n  return value as Pointer\n}\n\nfunction toNumber(value: number | bigint): number {\n  return typeof value === \"bigint\" ? Number(value) : value\n}\n\nfunction getOpenTUILib(libPath?: string) {\n  const resolvedLibPath = libPath || targetLibPath\n\n  const rawSymbols = dlopen(resolvedLibPath, {\n    // Logging\n    setLogCallback: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    // Event bus\n    setEventCallback: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    // Renderer management\n    createRenderer: {\n      args: [\"u32\", \"u32\", \"bool\", \"bool\"],\n      returns: \"ptr\",\n    },\n    setTerminalEnvVar: {\n      args: [\"ptr\", \"ptr\", \"usize\", \"ptr\", \"usize\"],\n      returns: \"bool\",\n    },\n    destroyRenderer: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    setUseThread: {\n      args: [\"ptr\", \"bool\"],\n      returns: \"void\",\n    },\n    setBackgroundColor: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    setRenderOffset: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    updateStats: {\n      args: [\"ptr\", \"f64\", \"u32\", \"f64\"],\n      returns: \"void\",\n    },\n    updateMemoryStats: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    render: {\n      args: [\"ptr\", \"bool\"],\n      returns: \"void\",\n    },\n    getNextBuffer: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    getCurrentBuffer: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n\n    queryPixelResolution: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n\n    createOptimizedBuffer: {\n      args: [\"u32\", \"u32\", \"bool\", \"u8\", \"ptr\", \"usize\"],\n      returns: \"ptr\",\n    },\n    destroyOptimizedBuffer: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n\n    drawFrameBuffer: {\n      args: [\"ptr\", \"i32\", \"i32\", \"ptr\", \"u32\", \"u32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    getBufferWidth: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    getBufferHeight: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    bufferClear: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    bufferGetCharPtr: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    bufferGetFgPtr: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    bufferGetBgPtr: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    bufferGetAttributesPtr: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    bufferGetRespectAlpha: {\n      args: [\"ptr\"],\n      returns: \"bool\",\n    },\n    bufferSetRespectAlpha: {\n      args: [\"ptr\", \"bool\"],\n      returns: \"void\",\n    },\n    bufferGetId: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    bufferGetRealCharSize: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    bufferWriteResolvedChars: {\n      args: [\"ptr\", \"ptr\", \"usize\", \"bool\"],\n      returns: \"u32\",\n    },\n\n    bufferDrawText: {\n      args: [\"ptr\", \"ptr\", \"u32\", \"u32\", \"u32\", \"ptr\", \"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    bufferSetCellWithAlphaBlending: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"ptr\", \"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    bufferSetCell: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"ptr\", \"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    bufferFillRect: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\", \"ptr\"],\n      returns: \"void\",\n    },\n    bufferColorMatrix: {\n      args: [\"ptr\", \"ptr\", \"ptr\", \"usize\", \"f32\", \"u8\"],\n      returns: \"void\",\n    },\n    bufferColorMatrixUniform: {\n      args: [\"ptr\", \"ptr\", \"f32\", \"u8\"],\n      returns: \"void\",\n    },\n    bufferResize: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n\n    // Link API\n    linkAlloc: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"u32\",\n    },\n    linkGetUrl: {\n      args: [\"u32\", \"ptr\", \"u32\"],\n      returns: \"u32\",\n    },\n    attributesWithLink: {\n      args: [\"u32\", \"u32\"],\n      returns: \"u32\",\n    },\n    attributesGetLinkId: {\n      args: [\"u32\"],\n      returns: \"u32\",\n    },\n\n    resizeRenderer: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n\n    // Cursor functions (now renderer-scoped)\n    setCursorPosition: {\n      args: [\"ptr\", \"i32\", \"i32\", \"bool\"],\n      returns: \"void\",\n    },\n    setCursorColor: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    getCursorState: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n\n    // Cursor and mouse pointer style (combined)\n    setCursorStyleOptions: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n\n    // Debug overlay\n    setDebugOverlay: {\n      args: [\"ptr\", \"bool\", \"u8\"],\n      returns: \"void\",\n    },\n\n    // Terminal control\n    clearTerminal: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    setTerminalTitle: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    copyToClipboardOSC52: {\n      args: [\"ptr\", \"u8\", \"ptr\", \"usize\"],\n      returns: \"bool\",\n    },\n    clearClipboardOSC52: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"bool\",\n    },\n\n    bufferDrawSuperSampleBuffer: {\n      args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"usize\", \"u8\", \"u32\"],\n      returns: \"void\",\n    },\n    bufferDrawPackedBuffer: {\n      args: [\"ptr\", \"ptr\", \"usize\", \"u32\", \"u32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    bufferDrawGrayscaleBuffer: {\n      args: [\"ptr\", \"i32\", \"i32\", \"ptr\", \"u32\", \"u32\", \"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    bufferDrawGrayscaleBufferSupersampled: {\n      args: [\"ptr\", \"i32\", \"i32\", \"ptr\", \"u32\", \"u32\", \"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    bufferDrawGrid: {\n      args: [\"ptr\", \"ptr\", \"ptr\", \"ptr\", \"ptr\", \"u32\", \"ptr\", \"u32\", \"ptr\"],\n      returns: \"void\",\n    },\n    bufferDrawBox: {\n      args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\", \"ptr\", \"u32\", \"ptr\", \"ptr\", \"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    bufferPushScissorRect: {\n      args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    bufferPopScissorRect: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    bufferClearScissorRects: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    bufferPushOpacity: {\n      args: [\"ptr\", \"f32\"],\n      returns: \"void\",\n    },\n    bufferPopOpacity: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    bufferGetCurrentOpacity: {\n      args: [\"ptr\"],\n      returns: \"f32\",\n    },\n    bufferClearOpacity: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n\n    addToHitGrid: {\n      args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    clearCurrentHitGrid: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    hitGridPushScissorRect: {\n      args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    hitGridPopScissorRect: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    hitGridClearScissorRects: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    addToCurrentHitGridClipped: {\n      args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    checkHit: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"u32\",\n    },\n    getHitGridDirty: {\n      args: [\"ptr\"],\n      returns: \"bool\",\n    },\n    dumpHitGrid: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    dumpBuffers: {\n      args: [\"ptr\", \"i64\"],\n      returns: \"void\",\n    },\n    dumpStdoutBuffer: {\n      args: [\"ptr\", \"i64\"],\n      returns: \"void\",\n    },\n    restoreTerminalModes: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    enableMouse: {\n      args: [\"ptr\", \"bool\"],\n      returns: \"void\",\n    },\n    disableMouse: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    enableKittyKeyboard: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    disableKittyKeyboard: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    setKittyKeyboardFlags: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    getKittyKeyboardFlags: {\n      args: [\"ptr\"],\n      returns: \"u8\",\n    },\n    setupTerminal: {\n      args: [\"ptr\", \"bool\"],\n      returns: \"void\",\n    },\n    suspendRenderer: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    resumeRenderer: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    writeOut: {\n      args: [\"ptr\", \"ptr\", \"u64\"],\n      returns: \"void\",\n    },\n\n    // TextBuffer functions\n    createTextBuffer: {\n      args: [\"u8\"],\n      returns: \"ptr\",\n    },\n    destroyTextBuffer: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferGetLength: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    textBufferGetByteSize: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n\n    textBufferReset: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferClear: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferSetDefaultFg: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferSetDefaultBg: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferSetDefaultAttributes: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferResetDefaults: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferGetTabWidth: {\n      args: [\"ptr\"],\n      returns: \"u8\",\n    },\n    textBufferSetTabWidth: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    textBufferRegisterMemBuffer: {\n      args: [\"ptr\", \"ptr\", \"usize\", \"bool\"],\n      returns: \"u16\",\n    },\n    textBufferReplaceMemBuffer: {\n      args: [\"ptr\", \"u8\", \"ptr\", \"usize\", \"bool\"],\n      returns: \"bool\",\n    },\n    textBufferClearMemRegistry: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferSetTextFromMem: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    textBufferAppend: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    textBufferAppendFromMemId: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    textBufferLoadFile: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"bool\",\n    },\n    textBufferSetStyledText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    textBufferGetLineCount: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    textBufferGetPlainText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    textBufferAddHighlightByCharRange: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferAddHighlight: {\n      args: [\"ptr\", \"u32\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferRemoveHighlightsByRef: {\n      args: [\"ptr\", \"u16\"],\n      returns: \"void\",\n    },\n    textBufferClearLineHighlights: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    textBufferClearAllHighlights: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferSetSyntaxStyle: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferGetLineHighlightsPtr: {\n      args: [\"ptr\", \"u32\", \"ptr\"],\n      returns: \"ptr\",\n    },\n    textBufferFreeLineHighlights: {\n      args: [\"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    textBufferGetHighlightCount: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    textBufferGetTextRange: {\n      args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    textBufferGetTextRangeByCoords: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n\n    // TextBufferView functions\n    createTextBufferView: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    destroyTextBufferView: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferViewSetSelection: {\n      args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferViewResetSelection: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferViewGetSelectionInfo: {\n      args: [\"ptr\"],\n      returns: \"u64\",\n    },\n    textBufferViewSetLocalSelection: {\n      args: [\"ptr\", \"i32\", \"i32\", \"i32\", \"i32\", \"ptr\", \"ptr\"],\n      returns: \"bool\",\n    },\n    textBufferViewUpdateSelection: {\n      args: [\"ptr\", \"u32\", \"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferViewUpdateLocalSelection: {\n      args: [\"ptr\", \"i32\", \"i32\", \"i32\", \"i32\", \"ptr\", \"ptr\"],\n      returns: \"bool\",\n    },\n    textBufferViewResetLocalSelection: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    textBufferViewSetWrapWidth: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    textBufferViewSetWrapMode: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    textBufferViewSetViewportSize: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    textBufferViewSetViewport: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    textBufferViewGetVirtualLineCount: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    textBufferViewGetLineInfoDirect: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferViewGetLogicalLineInfoDirect: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferViewGetSelectedText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    textBufferViewGetPlainText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    textBufferViewSetTabIndicator: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    textBufferViewSetTabIndicatorColor: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    textBufferViewSetTruncate: {\n      args: [\"ptr\", \"bool\"],\n      returns: \"void\",\n    },\n    textBufferViewMeasureForDimensions: {\n      args: [\"ptr\", \"u32\", \"u32\", \"ptr\"],\n      returns: \"bool\",\n    },\n    bufferDrawTextBufferView: {\n      args: [\"ptr\", \"ptr\", \"i32\", \"i32\"],\n      returns: \"void\",\n    },\n    bufferDrawEditorView: {\n      args: [\"ptr\", \"ptr\", \"i32\", \"i32\"],\n      returns: \"void\",\n    },\n\n    // EditorView functions\n    createEditorView: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"ptr\",\n    },\n    destroyEditorView: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editorViewSetViewportSize: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    editorViewSetViewport: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\", \"bool\"],\n      returns: \"void\",\n    },\n    editorViewGetViewport: {\n      args: [\"ptr\", \"ptr\", \"ptr\", \"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewSetScrollMargin: {\n      args: [\"ptr\", \"f32\"],\n      returns: \"void\",\n    },\n    editorViewSetWrapMode: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    editorViewGetVirtualLineCount: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    editorViewGetTotalVirtualLineCount: {\n      args: [\"ptr\"],\n      returns: \"u32\",\n    },\n    editorViewGetTextBufferView: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    editorViewGetLineInfoDirect: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewGetLogicalLineInfoDirect: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n\n    // EditBuffer functions\n    createEditBuffer: {\n      args: [\"u8\"],\n      returns: \"ptr\",\n    },\n    destroyEditBuffer: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferSetText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    editBufferSetTextFromMem: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    editBufferReplaceText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    editBufferReplaceTextFromMem: {\n      args: [\"ptr\", \"u8\"],\n      returns: \"void\",\n    },\n    editBufferGetText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    editBufferInsertChar: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    editBufferInsertText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    editBufferDeleteChar: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferDeleteCharBackward: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferDeleteRange: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    editBufferNewLine: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferDeleteLine: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferMoveCursorLeft: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferMoveCursorRight: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferMoveCursorUp: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferMoveCursorDown: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferGotoLine: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    editBufferSetCursor: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    editBufferSetCursorToLineCol: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"void\",\n    },\n    editBufferSetCursorByOffset: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    editBufferGetCursorPosition: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editBufferGetId: {\n      args: [\"ptr\"],\n      returns: \"u16\",\n    },\n    editBufferGetTextBuffer: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    editBufferDebugLogRope: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferUndo: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    editBufferRedo: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    editBufferCanUndo: {\n      args: [\"ptr\"],\n      returns: \"bool\",\n    },\n    editBufferCanRedo: {\n      args: [\"ptr\"],\n      returns: \"bool\",\n    },\n    editBufferClearHistory: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferClear: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editBufferGetNextWordBoundary: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editBufferGetPrevWordBoundary: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editBufferGetEOL: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editBufferOffsetToPosition: {\n      args: [\"ptr\", \"u32\", \"ptr\"],\n      returns: \"bool\",\n    },\n    editBufferPositionToOffset: {\n      args: [\"ptr\", \"u32\", \"u32\"],\n      returns: \"u32\",\n    },\n    editBufferGetLineStartOffset: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"u32\",\n    },\n    editBufferGetTextRange: {\n      args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    editBufferGetTextRangeByCoords: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n\n    // EditorView selection and editing methods\n    editorViewSetSelection: {\n      args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewResetSelection: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editorViewGetSelection: {\n      args: [\"ptr\"],\n      returns: \"u64\",\n    },\n    editorViewSetLocalSelection: {\n      args: [\"ptr\", \"i32\", \"i32\", \"i32\", \"i32\", \"ptr\", \"ptr\", \"bool\", \"bool\"],\n      returns: \"bool\",\n    },\n    editorViewUpdateSelection: {\n      args: [\"ptr\", \"u32\", \"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewUpdateLocalSelection: {\n      args: [\"ptr\", \"i32\", \"i32\", \"i32\", \"i32\", \"ptr\", \"ptr\", \"bool\", \"bool\"],\n      returns: \"bool\",\n    },\n    editorViewResetLocalSelection: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editorViewGetSelectedTextBytes: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n    editorViewGetCursor: {\n      args: [\"ptr\", \"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewGetText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"usize\",\n    },\n\n    // EditorView VisualCursor methods\n    editorViewGetVisualCursor: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n\n    editorViewMoveUpVisual: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editorViewMoveDownVisual: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editorViewDeleteSelectedText: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    editorViewSetCursorByOffset: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    editorViewGetNextWordBoundary: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewGetPrevWordBoundary: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewGetEOL: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewGetVisualSOL: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewGetVisualEOL: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    editorViewSetPlaceholderStyledText: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    editorViewSetTabIndicator: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n    editorViewSetTabIndicatorColor: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n\n    getArenaAllocatedBytes: {\n      args: [],\n      returns: \"usize\",\n    },\n    getBuildOptions: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    getAllocatorStats: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n\n    // SyntaxStyle functions\n    createSyntaxStyle: {\n      args: [],\n      returns: \"ptr\",\n    },\n    destroySyntaxStyle: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    syntaxStyleRegister: {\n      args: [\"ptr\", \"ptr\", \"usize\", \"ptr\", \"ptr\", \"u8\"],\n      returns: \"u32\",\n    },\n    syntaxStyleResolveByName: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"u32\",\n    },\n    syntaxStyleGetStyleCount: {\n      args: [\"ptr\"],\n      returns: \"usize\",\n    },\n\n    // Terminal capability functions\n    getTerminalCapabilities: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n    processCapabilityResponse: {\n      args: [\"ptr\", \"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n\n    // Unicode encoding API\n    encodeUnicode: {\n      args: [\"ptr\", \"usize\", \"ptr\", \"ptr\", \"u8\"],\n      returns: \"bool\",\n    },\n    freeUnicode: {\n      args: [\"ptr\", \"usize\"],\n      returns: \"void\",\n    },\n    bufferDrawChar: {\n      args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"ptr\", \"ptr\", \"u32\"],\n      returns: \"void\",\n    },\n\n    // NativeSpanFeed\n    createNativeSpanFeed: {\n      args: [\"ptr\"],\n      returns: \"ptr\",\n    },\n    attachNativeSpanFeed: {\n      args: [\"ptr\"],\n      returns: \"i32\",\n    },\n    destroyNativeSpanFeed: {\n      args: [\"ptr\"],\n      returns: \"void\",\n    },\n    streamWrite: {\n      args: [\"ptr\", \"ptr\", \"u64\"],\n      returns: \"i32\",\n    },\n    streamCommit: {\n      args: [\"ptr\"],\n      returns: \"i32\",\n    },\n    streamDrainSpans: {\n      args: [\"ptr\", \"ptr\", \"u32\"],\n      returns: \"u32\",\n    },\n    streamClose: {\n      args: [\"ptr\"],\n      returns: \"i32\",\n    },\n    streamReserve: {\n      args: [\"ptr\", \"u32\", \"ptr\"],\n      returns: \"i32\",\n    },\n    streamCommitReserved: {\n      args: [\"ptr\", \"u32\"],\n      returns: \"i32\",\n    },\n    streamSetOptions: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"i32\",\n    },\n    streamGetStats: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"i32\",\n    },\n    streamSetCallback: {\n      args: [\"ptr\", \"ptr\"],\n      returns: \"void\",\n    },\n  })\n\n  if (env.OTUI_DEBUG_FFI || env.OTUI_TRACE_FFI) {\n    return {\n      symbols: convertToDebugSymbols(rawSymbols.symbols),\n    }\n  }\n\n  return rawSymbols\n}\n\nfunction convertToDebugSymbols<T extends Record<string, any>>(symbols: T): T {\n  // Initialize global state on first call\n  if (!globalTraceSymbols) {\n    globalTraceSymbols = {}\n  }\n\n  // Initialize global debug log path on first call\n  if (env.OTUI_DEBUG_FFI && !globalFFILogPath) {\n    const now = new Date()\n    const timestamp = now.toISOString().replace(/[:.]/g, \"-\").replace(/T/, \"_\").split(\"Z\")[0]\n    globalFFILogPath = `ffi_otui_debug_${timestamp}.log`\n  }\n\n  const debugSymbols: Record<string, any> = {}\n  let hasTracing = false\n\n  Object.entries(symbols).forEach(([key, value]) => {\n    debugSymbols[key] = value\n  })\n\n  if (env.OTUI_DEBUG_FFI && globalFFILogPath) {\n    const logPath = globalFFILogPath\n    const writeSync = (msg: string) => {\n      writeFileSync(logPath, msg + \"\\n\", { flag: \"a\" })\n    }\n\n    Object.entries(symbols).forEach(([key, value]) => {\n      if (typeof value === \"function\") {\n        debugSymbols[key] = (...args: any[]) => {\n          writeSync(`${key}(${args.map((arg) => String(arg)).join(\", \")})`)\n          const result = value(...args)\n          writeSync(`${key} returned: ${String(result)}`)\n          return result\n        }\n      }\n    })\n  }\n\n  if (env.OTUI_TRACE_FFI) {\n    hasTracing = true\n    Object.entries(symbols).forEach(([key, value]) => {\n      if (typeof value === \"function\") {\n        // Initialize trace array for this symbol if not exists\n        if (!globalTraceSymbols![key]) {\n          globalTraceSymbols![key] = []\n        }\n\n        const originalFunc = debugSymbols[key]\n        debugSymbols[key] = (...args: any[]) => {\n          const start = performance.now()\n          const result = originalFunc(...args)\n          const end = performance.now()\n          globalTraceSymbols![key].push(end - start)\n          return result\n        }\n      }\n    })\n  }\n\n  // Register exit handler only once\n  if ((env.OTUI_DEBUG_FFI || env.OTUI_TRACE_FFI) && !exitHandlerRegistered) {\n    exitHandlerRegistered = true\n\n    process.on(\"exit\", () => {\n      if (globalTraceSymbols) {\n        const allStats: Array<{\n          name: string\n          count: number\n          total: number\n          average: number\n          min: number\n          max: number\n          median: number\n          p90: number\n          p99: number\n        }> = []\n\n        for (const [key, timings] of Object.entries(globalTraceSymbols)) {\n          if (!Array.isArray(timings) || timings.length === 0) {\n            continue\n          }\n\n          const sortedTimings = [...timings].sort((a, b) => a - b)\n          const count = sortedTimings.length\n\n          const total = sortedTimings.reduce((acc, t) => acc + t, 0)\n          const average = total / count\n          const min = sortedTimings[0]\n          const max = sortedTimings[count - 1]\n\n          const medianIndex = Math.floor(count / 2)\n          const p90Index = Math.floor(count * 0.9)\n          const p99Index = Math.floor(count * 0.99)\n\n          const median = sortedTimings[medianIndex]\n          const p90 = sortedTimings[Math.min(p90Index, count - 1)]\n          const p99 = sortedTimings[Math.min(p99Index, count - 1)]\n\n          allStats.push({\n            name: key,\n            count,\n            total,\n            average,\n            min,\n            max,\n            median,\n            p90,\n            p99,\n          })\n        }\n\n        allStats.sort((a, b) => b.total - a.total)\n\n        const lines: string[] = []\n        lines.push(\"\\n--- OpenTUI FFI Call Performance ---\")\n        lines.push(\"Sorted by total time spent (descending)\")\n        lines.push(\n          \"-------------------------------------------------------------------------------------------------------------------------\",\n        )\n\n        if (allStats.length === 0) {\n          lines.push(\"No trace data collected or all symbols had zero calls.\")\n        } else {\n          const nameHeader = \"Symbol\"\n          const callsHeader = \"Calls\"\n          const totalHeader = \"Total (ms)\"\n          const avgHeader = \"Avg (ms)\"\n          const minHeader = \"Min (ms)\"\n          const maxHeader = \"Max (ms)\"\n          const medHeader = \"Med (ms)\"\n          const p90Header = \"P90 (ms)\"\n          const p99Header = \"P99 (ms)\"\n\n          const nameWidth = Math.max(nameHeader.length, ...allStats.map((s) => s.name.length))\n          const countWidth = Math.max(callsHeader.length, ...allStats.map((s) => String(s.count).length))\n          const totalWidth = Math.max(totalHeader.length, ...allStats.map((s) => s.total.toFixed(2).length))\n          const avgWidth = Math.max(avgHeader.length, ...allStats.map((s) => s.average.toFixed(2).length))\n          const statWidthMin = Math.max(minHeader.length, ...allStats.map((s) => s.min.toFixed(2).length))\n          const statWidthMax = Math.max(maxHeader.length, ...allStats.map((s) => s.max.toFixed(2).length))\n          const medianWidth = Math.max(medHeader.length, ...allStats.map((s) => s.median.toFixed(2).length))\n          const p90Width = Math.max(p90Header.length, ...allStats.map((s) => s.p90.toFixed(2).length))\n          const p99Width = Math.max(p99Header.length, ...allStats.map((s) => s.p99.toFixed(2).length))\n\n          lines.push(\n            `${nameHeader.padEnd(nameWidth)} | ` +\n              `${callsHeader.padStart(countWidth)} | ` +\n              `${totalHeader.padStart(totalWidth)} | ` +\n              `${avgHeader.padStart(avgWidth)} | ` +\n              `${minHeader.padStart(statWidthMin)} | ` +\n              `${maxHeader.padStart(statWidthMax)} | ` +\n              `${medHeader.padStart(medianWidth)} | ` +\n              `${p90Header.padStart(p90Width)} | ` +\n              `${p99Header.padStart(p99Width)}`,\n          )\n          lines.push(\n            `${\"-\".repeat(nameWidth)}-+-${\"-\".repeat(countWidth)}-+-${\"-\".repeat(totalWidth)}-+-${\"-\".repeat(avgWidth)}-+-${\"-\".repeat(statWidthMin)}-+-${\"-\".repeat(statWidthMax)}-+-${\"-\".repeat(medianWidth)}-+-${\"-\".repeat(p90Width)}-+-${\"-\".repeat(p99Width)}`,\n          )\n\n          allStats.forEach((stat) => {\n            lines.push(\n              `${stat.name.padEnd(nameWidth)} | ` +\n                `${String(stat.count).padStart(countWidth)} | ` +\n                `${stat.total.toFixed(2).padStart(totalWidth)} | ` +\n                `${stat.average.toFixed(2).padStart(avgWidth)} | ` +\n                `${stat.min.toFixed(2).padStart(statWidthMin)} | ` +\n                `${stat.max.toFixed(2).padStart(statWidthMax)} | ` +\n                `${stat.median.toFixed(2).padStart(medianWidth)} | ` +\n                `${stat.p90.toFixed(2).padStart(p90Width)} | ` +\n                `${stat.p99.toFixed(2).padStart(p99Width)}`,\n            )\n          })\n        }\n        lines.push(\n          \"-------------------------------------------------------------------------------------------------------------------------\",\n        )\n\n        const output = lines.join(\"\\n\")\n        console.log(output)\n\n        try {\n          const now = new Date()\n          const timestamp = now.toISOString().replace(/[:.]/g, \"-\").replace(/T/, \"_\").split(\"Z\")[0]\n          const traceFilePath = `ffi_otui_trace_${timestamp}.log`\n          Bun.write(traceFilePath, output)\n        } catch (e) {\n          console.error(\"Failed to write FFI trace file:\", e)\n        }\n      }\n    })\n  }\n\n  return debugSymbols as T\n}\n\n// Log levels matching Zig's LogLevel enum\nexport enum LogLevel {\n  Error = 0,\n  Warn = 1,\n  Info = 2,\n  Debug = 3,\n}\n\n/**\n * VisualCursor represents a cursor position with both visual and logical coordinates.\n * Visual coordinates (visualRow, visualCol) are VIEWPORT-RELATIVE.\n * This means visualRow=0 is the first visible line in the viewport, not the first line in the document.\n * Logical coordinates (logicalRow, logicalCol) are document-absolute.\n */\nexport interface VisualCursor {\n  visualRow: number // Viewport-relative row (0 = top of viewport)\n  visualCol: number // Viewport-relative column (0 = left edge of viewport when not wrapping)\n  logicalRow: number // Document-absolute row\n  logicalCol: number // Document-absolute column\n  offset: number // Global display-width offset from buffer start\n}\n\nexport interface LogicalCursor {\n  row: number\n  col: number\n  offset: number\n}\n\nexport interface CursorState {\n  x: number\n  y: number\n  visible: boolean\n  style: CursorStyle\n  blinking: boolean\n  color: RGBA\n}\n\nexport type NativeSpanFeedEventHandler = (eventId: number, arg0: Pointer, arg1: number | bigint) => void\n\nexport interface RenderLib {\n  createRenderer: (width: number, height: number, options?: { testing?: boolean; remote?: boolean }) => Pointer | null\n  setTerminalEnvVar: (renderer: Pointer, key: string, value: string) => boolean\n  destroyRenderer: (renderer: Pointer) => void\n  setUseThread: (renderer: Pointer, useThread: boolean) => void\n  setBackgroundColor: (renderer: Pointer, color: RGBA) => void\n  setRenderOffset: (renderer: Pointer, offset: number) => void\n  updateStats: (renderer: Pointer, time: number, fps: number, frameCallbackTime: number) => void\n  updateMemoryStats: (renderer: Pointer, heapUsed: number, heapTotal: number, arrayBuffers: number) => void\n  render: (renderer: Pointer, force: boolean) => void\n  getNextBuffer: (renderer: Pointer) => OptimizedBuffer\n  getCurrentBuffer: (renderer: Pointer) => OptimizedBuffer\n  createOptimizedBuffer: (\n    width: number,\n    height: number,\n    widthMethod: WidthMethod,\n    respectAlpha?: boolean,\n    id?: string,\n  ) => OptimizedBuffer\n  destroyOptimizedBuffer: (bufferPtr: Pointer) => void\n  drawFrameBuffer: (\n    targetBufferPtr: Pointer,\n    destX: number,\n    destY: number,\n    bufferPtr: Pointer,\n    sourceX?: number,\n    sourceY?: number,\n    sourceWidth?: number,\n    sourceHeight?: number,\n  ) => void\n  getBufferWidth: (buffer: Pointer) => number\n  getBufferHeight: (buffer: Pointer) => number\n  bufferClear: (buffer: Pointer, color: RGBA) => void\n  bufferGetCharPtr: (buffer: Pointer) => Pointer\n  bufferGetFgPtr: (buffer: Pointer) => Pointer\n  bufferGetBgPtr: (buffer: Pointer) => Pointer\n  bufferGetAttributesPtr: (buffer: Pointer) => Pointer\n  bufferGetRespectAlpha: (buffer: Pointer) => boolean\n  bufferSetRespectAlpha: (buffer: Pointer, respectAlpha: boolean) => void\n  bufferGetId: (buffer: Pointer) => string\n  bufferGetRealCharSize: (buffer: Pointer) => number\n  bufferWriteResolvedChars: (buffer: Pointer, outputBuffer: Uint8Array, addLineBreaks: boolean) => number\n  bufferDrawText: (\n    buffer: Pointer,\n    text: string,\n    x: number,\n    y: number,\n    color: RGBA,\n    bgColor?: RGBA,\n    attributes?: number,\n  ) => void\n  bufferSetCellWithAlphaBlending: (\n    buffer: Pointer,\n    x: number,\n    y: number,\n    char: string,\n    color: RGBA,\n    bgColor: RGBA,\n    attributes?: number,\n  ) => void\n  bufferSetCell: (\n    buffer: Pointer,\n    x: number,\n    y: number,\n    char: string,\n    color: RGBA,\n    bgColor: RGBA,\n    attributes?: number,\n  ) => void\n  bufferFillRect: (buffer: Pointer, x: number, y: number, width: number, height: number, color: RGBA) => void\n  bufferColorMatrix: (\n    buffer: Pointer,\n    matrixPtr: Pointer,\n    cellMaskPtr: Pointer,\n    cellMaskCount: number,\n    strength: number,\n    target: TargetChannel,\n  ) => void\n  bufferColorMatrixUniform: (buffer: Pointer, matrixPtr: Pointer, strength: number, target: TargetChannel) => void\n  bufferDrawSuperSampleBuffer: (\n    buffer: Pointer,\n    x: number,\n    y: number,\n    pixelDataPtr: Pointer,\n    pixelDataLength: number,\n    format: \"bgra8unorm\" | \"rgba8unorm\",\n    alignedBytesPerRow: number,\n  ) => void\n  bufferDrawPackedBuffer: (\n    buffer: Pointer,\n    dataPtr: Pointer,\n    dataLen: number,\n    posX: number,\n    posY: number,\n    terminalWidthCells: number,\n    terminalHeightCells: number,\n  ) => void\n  bufferDrawGrayscaleBuffer: (\n    buffer: Pointer,\n    posX: number,\n    posY: number,\n    intensitiesPtr: Pointer,\n    srcWidth: number,\n    srcHeight: number,\n    fg: RGBA | null,\n    bg: RGBA | null,\n  ) => void\n  bufferDrawGrayscaleBufferSupersampled: (\n    buffer: Pointer,\n    posX: number,\n    posY: number,\n    intensitiesPtr: Pointer,\n    srcWidth: number,\n    srcHeight: number,\n    fg: RGBA | null,\n    bg: RGBA | null,\n  ) => void\n  bufferDrawGrid: (\n    buffer: Pointer,\n    borderChars: Uint32Array,\n    borderFg: RGBA,\n    borderBg: RGBA,\n    columnOffsets: Int32Array,\n    columnCount: number,\n    rowOffsets: Int32Array,\n    rowCount: number,\n    options: { drawInner: boolean; drawOuter: boolean },\n  ) => void\n  bufferDrawBox: (\n    buffer: Pointer,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    borderChars: Uint32Array,\n    packedOptions: number,\n    borderColor: RGBA,\n    backgroundColor: RGBA,\n    title: string | null,\n  ) => void\n  bufferResize: (buffer: Pointer, width: number, height: number) => void\n  resizeRenderer: (renderer: Pointer, width: number, height: number) => void\n  setCursorPosition: (renderer: Pointer, x: number, y: number, visible: boolean) => void\n  setCursorColor: (renderer: Pointer, color: RGBA) => void\n  getCursorState: (renderer: Pointer) => CursorState\n  setCursorStyleOptions: (renderer: Pointer, options: CursorStyleOptions) => void\n  setDebugOverlay: (renderer: Pointer, enabled: boolean, corner: DebugOverlayCorner) => void\n  clearTerminal: (renderer: Pointer) => void\n  setTerminalTitle: (renderer: Pointer, title: string) => void\n  copyToClipboardOSC52: (renderer: Pointer, target: number, payload: Uint8Array) => boolean\n  clearClipboardOSC52: (renderer: Pointer, target: number) => boolean\n  addToHitGrid: (renderer: Pointer, x: number, y: number, width: number, height: number, id: number) => void\n  clearCurrentHitGrid: (renderer: Pointer) => void\n  hitGridPushScissorRect: (renderer: Pointer, x: number, y: number, width: number, height: number) => void\n  hitGridPopScissorRect: (renderer: Pointer) => void\n  hitGridClearScissorRects: (renderer: Pointer) => void\n  addToCurrentHitGridClipped: (\n    renderer: Pointer,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    id: number,\n  ) => void\n  checkHit: (renderer: Pointer, x: number, y: number) => number\n  getHitGridDirty: (renderer: Pointer) => boolean\n  dumpHitGrid: (renderer: Pointer) => void\n  dumpBuffers: (renderer: Pointer, timestamp?: number) => void\n  dumpStdoutBuffer: (renderer: Pointer, timestamp?: number) => void\n  restoreTerminalModes: (renderer: Pointer) => void\n  enableMouse: (renderer: Pointer, enableMovement: boolean) => void\n  disableMouse: (renderer: Pointer) => void\n  enableKittyKeyboard: (renderer: Pointer, flags: number) => void\n  disableKittyKeyboard: (renderer: Pointer) => void\n  setKittyKeyboardFlags: (renderer: Pointer, flags: number) => void\n  getKittyKeyboardFlags: (renderer: Pointer) => number\n  setupTerminal: (renderer: Pointer, useAlternateScreen: boolean) => void\n  suspendRenderer: (renderer: Pointer) => void\n  resumeRenderer: (renderer: Pointer) => void\n  queryPixelResolution: (renderer: Pointer) => void\n  writeOut: (renderer: Pointer, data: string | Uint8Array) => void\n\n  // TextBuffer methods\n  createTextBuffer: (widthMethod: WidthMethod) => TextBuffer\n  destroyTextBuffer: (buffer: Pointer) => void\n  textBufferGetLength: (buffer: Pointer) => number\n  textBufferGetByteSize: (buffer: Pointer) => number\n\n  textBufferReset: (buffer: Pointer) => void\n  textBufferClear: (buffer: Pointer) => void\n  textBufferRegisterMemBuffer: (buffer: Pointer, bytes: Uint8Array, owned?: boolean) => number\n  textBufferReplaceMemBuffer: (buffer: Pointer, memId: number, bytes: Uint8Array, owned?: boolean) => boolean\n  textBufferClearMemRegistry: (buffer: Pointer) => void\n  textBufferSetTextFromMem: (buffer: Pointer, memId: number) => void\n  textBufferAppend: (buffer: Pointer, bytes: Uint8Array) => void\n  textBufferAppendFromMemId: (buffer: Pointer, memId: number) => void\n  textBufferLoadFile: (buffer: Pointer, path: string) => boolean\n  textBufferSetStyledText: (\n    buffer: Pointer,\n    chunks: Array<{ text: string; fg?: RGBA | null; bg?: RGBA | null; attributes?: number; link?: { url: string } }>,\n  ) => void\n  textBufferSetDefaultFg: (buffer: Pointer, fg: RGBA | null) => void\n  textBufferSetDefaultBg: (buffer: Pointer, bg: RGBA | null) => void\n  textBufferSetDefaultAttributes: (buffer: Pointer, attributes: number | null) => void\n  textBufferResetDefaults: (buffer: Pointer) => void\n  textBufferGetTabWidth: (buffer: Pointer) => number\n  textBufferSetTabWidth: (buffer: Pointer, width: number) => void\n  textBufferGetLineCount: (buffer: Pointer) => number\n  getPlainTextBytes: (buffer: Pointer, maxLength: number) => Uint8Array | null\n  textBufferGetTextRange: (\n    buffer: Pointer,\n    startOffset: number,\n    endOffset: number,\n    maxLength: number,\n  ) => Uint8Array | null\n  textBufferGetTextRangeByCoords: (\n    buffer: Pointer,\n    startRow: number,\n    startCol: number,\n    endRow: number,\n    endCol: number,\n    maxLength: number,\n  ) => Uint8Array | null\n\n  // TextBufferView methods\n  createTextBufferView: (textBuffer: Pointer) => Pointer\n  destroyTextBufferView: (view: Pointer) => void\n  textBufferViewSetSelection: (\n    view: Pointer,\n    start: number,\n    end: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n  ) => void\n  textBufferViewResetSelection: (view: Pointer) => void\n  textBufferViewGetSelection: (view: Pointer) => { start: number; end: number } | null\n  textBufferViewSetLocalSelection: (\n    view: Pointer,\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n  ) => boolean\n  textBufferViewUpdateSelection: (view: Pointer, end: number, bgColor: RGBA | null, fgColor: RGBA | null) => void\n  textBufferViewUpdateLocalSelection: (\n    view: Pointer,\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n  ) => boolean\n  textBufferViewResetLocalSelection: (view: Pointer) => void\n  textBufferViewSetWrapWidth: (view: Pointer, width: number) => void\n  textBufferViewSetWrapMode: (view: Pointer, mode: \"none\" | \"char\" | \"word\") => void\n  textBufferViewSetViewportSize: (view: Pointer, width: number, height: number) => void\n  textBufferViewSetViewport: (view: Pointer, x: number, y: number, width: number, height: number) => void\n  textBufferViewGetLineInfo: (view: Pointer) => LineInfo\n  textBufferViewGetLogicalLineInfo: (view: Pointer) => LineInfo\n  textBufferViewGetSelectedTextBytes: (view: Pointer, maxLength: number) => Uint8Array | null\n  textBufferViewGetPlainTextBytes: (view: Pointer, maxLength: number) => Uint8Array | null\n  textBufferViewSetTabIndicator: (view: Pointer, indicator: number) => void\n  textBufferViewSetTabIndicatorColor: (view: Pointer, color: RGBA) => void\n  textBufferViewSetTruncate: (view: Pointer, truncate: boolean) => void\n  textBufferViewMeasureForDimensions: (\n    view: Pointer,\n    width: number,\n    height: number,\n  ) => { lineCount: number; widthColsMax: number } | null\n  textBufferViewGetVirtualLineCount: (view: Pointer) => number\n\n  readonly encoder: TextEncoder\n  readonly decoder: TextDecoder\n  bufferDrawTextBufferView: (buffer: Pointer, view: Pointer, x: number, y: number) => void\n  bufferDrawEditorView: (buffer: Pointer, view: Pointer, x: number, y: number) => void\n\n  // EditBuffer methods\n  createEditBuffer: (widthMethod: WidthMethod) => Pointer\n  destroyEditBuffer: (buffer: Pointer) => void\n  editBufferSetText: (buffer: Pointer, textBytes: Uint8Array) => void\n  editBufferSetTextFromMem: (buffer: Pointer, memId: number) => void\n  editBufferReplaceText: (buffer: Pointer, textBytes: Uint8Array) => void\n  editBufferReplaceTextFromMem: (buffer: Pointer, memId: number) => void\n  editBufferGetText: (buffer: Pointer, maxLength: number) => Uint8Array | null\n  editBufferInsertChar: (buffer: Pointer, char: string) => void\n  editBufferInsertText: (buffer: Pointer, text: string) => void\n  editBufferDeleteChar: (buffer: Pointer) => void\n  editBufferDeleteCharBackward: (buffer: Pointer) => void\n  editBufferDeleteRange: (buffer: Pointer, startLine: number, startCol: number, endLine: number, endCol: number) => void\n  editBufferNewLine: (buffer: Pointer) => void\n  editBufferDeleteLine: (buffer: Pointer) => void\n  editBufferMoveCursorLeft: (buffer: Pointer) => void\n  editBufferMoveCursorRight: (buffer: Pointer) => void\n  editBufferMoveCursorUp: (buffer: Pointer) => void\n  editBufferMoveCursorDown: (buffer: Pointer) => void\n  editBufferGotoLine: (buffer: Pointer, line: number) => void\n  editBufferSetCursor: (buffer: Pointer, line: number, col: number) => void\n  editBufferSetCursorToLineCol: (buffer: Pointer, line: number, col: number) => void\n  editBufferSetCursorByOffset: (buffer: Pointer, offset: number) => void\n  editBufferGetCursorPosition: (buffer: Pointer) => LogicalCursor\n  editBufferGetId: (buffer: Pointer) => number\n  editBufferGetTextBuffer: (buffer: Pointer) => Pointer\n  editBufferDebugLogRope: (buffer: Pointer) => void\n  editBufferUndo: (buffer: Pointer, maxLength: number) => Uint8Array | null\n  editBufferRedo: (buffer: Pointer, maxLength: number) => Uint8Array | null\n  editBufferCanUndo: (buffer: Pointer) => boolean\n  editBufferCanRedo: (buffer: Pointer) => boolean\n  editBufferClearHistory: (buffer: Pointer) => void\n  editBufferClear: (buffer: Pointer) => void\n  editBufferGetNextWordBoundary: (buffer: Pointer) => { row: number; col: number; offset: number }\n  editBufferGetPrevWordBoundary: (buffer: Pointer) => { row: number; col: number; offset: number }\n  editBufferGetEOL: (buffer: Pointer) => { row: number; col: number; offset: number }\n  editBufferOffsetToPosition: (buffer: Pointer, offset: number) => { row: number; col: number; offset: number } | null\n  editBufferPositionToOffset: (buffer: Pointer, row: number, col: number) => number\n  editBufferGetLineStartOffset: (buffer: Pointer, row: number) => number\n  editBufferGetTextRange: (\n    buffer: Pointer,\n    startOffset: number,\n    endOffset: number,\n    maxLength: number,\n  ) => Uint8Array | null\n  editBufferGetTextRangeByCoords: (\n    buffer: Pointer,\n    startRow: number,\n    startCol: number,\n    endRow: number,\n    endCol: number,\n    maxLength: number,\n  ) => Uint8Array | null\n\n  // EditorView methods\n  createEditorView: (editBufferPtr: Pointer, viewportWidth: number, viewportHeight: number) => Pointer\n  destroyEditorView: (view: Pointer) => void\n  editorViewSetViewportSize: (view: Pointer, width: number, height: number) => void\n  editorViewSetViewport: (\n    view: Pointer,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    moveCursor: boolean,\n  ) => void\n  editorViewGetViewport: (view: Pointer) => { offsetY: number; offsetX: number; height: number; width: number }\n  editorViewSetScrollMargin: (view: Pointer, margin: number) => void\n  editorViewSetWrapMode: (view: Pointer, mode: \"none\" | \"char\" | \"word\") => void\n  editorViewGetVirtualLineCount: (view: Pointer) => number\n  editorViewGetTotalVirtualLineCount: (view: Pointer) => number\n  editorViewGetTextBufferView: (view: Pointer) => Pointer\n  editorViewSetSelection: (\n    view: Pointer,\n    start: number,\n    end: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n  ) => void\n  editorViewResetSelection: (view: Pointer) => void\n  editorViewGetSelection: (view: Pointer) => { start: number; end: number } | null\n  editorViewSetLocalSelection: (\n    view: Pointer,\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n    updateCursor: boolean,\n    followCursor: boolean,\n  ) => boolean\n\n  editorViewUpdateSelection: (view: Pointer, end: number, bgColor: RGBA | null, fgColor: RGBA | null) => void\n  editorViewUpdateLocalSelection: (\n    view: Pointer,\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n    updateCursor: boolean,\n    followCursor: boolean,\n  ) => boolean\n\n  editorViewResetLocalSelection: (view: Pointer) => void\n  editorViewGetSelectedTextBytes: (view: Pointer, maxLength: number) => Uint8Array | null\n  editorViewGetCursor: (view: Pointer) => { row: number; col: number }\n  editorViewGetText: (view: Pointer, maxLength: number) => Uint8Array | null\n  editorViewGetVisualCursor: (view: Pointer) => VisualCursor\n  editorViewMoveUpVisual: (view: Pointer) => void\n  editorViewMoveDownVisual: (view: Pointer) => void\n  editorViewDeleteSelectedText: (view: Pointer) => void\n  editorViewSetCursorByOffset: (view: Pointer, offset: number) => void\n  editorViewGetNextWordBoundary: (view: Pointer) => VisualCursor\n  editorViewGetPrevWordBoundary: (view: Pointer) => VisualCursor\n  editorViewGetEOL: (view: Pointer) => VisualCursor\n  editorViewGetVisualSOL: (view: Pointer) => VisualCursor\n  editorViewGetVisualEOL: (view: Pointer) => VisualCursor\n  editorViewGetLineInfo: (view: Pointer) => LineInfo\n  editorViewGetLogicalLineInfo: (view: Pointer) => LineInfo\n  editorViewSetPlaceholderStyledText: (\n    view: Pointer,\n    chunks: Array<{ text: string; fg?: RGBA | null; bg?: RGBA | null; attributes?: number }>,\n  ) => void\n  editorViewSetTabIndicator: (view: Pointer, indicator: number) => void\n  editorViewSetTabIndicatorColor: (view: Pointer, color: RGBA) => void\n\n  bufferPushScissorRect: (buffer: Pointer, x: number, y: number, width: number, height: number) => void\n  bufferPopScissorRect: (buffer: Pointer) => void\n  bufferClearScissorRects: (buffer: Pointer) => void\n  bufferPushOpacity: (buffer: Pointer, opacity: number) => void\n  bufferPopOpacity: (buffer: Pointer) => void\n  bufferGetCurrentOpacity: (buffer: Pointer) => number\n  bufferClearOpacity: (buffer: Pointer) => void\n  textBufferAddHighlightByCharRange: (buffer: Pointer, highlight: Highlight) => void\n  textBufferAddHighlight: (buffer: Pointer, lineIdx: number, highlight: Highlight) => void\n  textBufferRemoveHighlightsByRef: (buffer: Pointer, hlRef: number) => void\n  textBufferClearLineHighlights: (buffer: Pointer, lineIdx: number) => void\n  textBufferClearAllHighlights: (buffer: Pointer) => void\n  textBufferSetSyntaxStyle: (buffer: Pointer, style: Pointer | null) => void\n  textBufferGetLineHighlights: (buffer: Pointer, lineIdx: number) => Array<Highlight>\n  textBufferGetHighlightCount: (buffer: Pointer) => number\n\n  getArenaAllocatedBytes: () => number\n  getBuildOptions: () => BuildOptions\n  getAllocatorStats: () => AllocatorStats\n\n  createSyntaxStyle: () => Pointer\n  destroySyntaxStyle: (style: Pointer) => void\n  syntaxStyleRegister: (style: Pointer, name: string, fg: RGBA | null, bg: RGBA | null, attributes: number) => number\n  syntaxStyleResolveByName: (style: Pointer, name: string) => number | null\n  syntaxStyleGetStyleCount: (style: Pointer) => number\n\n  getTerminalCapabilities: (renderer: Pointer) => any\n  processCapabilityResponse: (renderer: Pointer, response: string) => void\n\n  encodeUnicode: (\n    text: string,\n    widthMethod: WidthMethod,\n  ) => { ptr: Pointer; data: Array<{ width: number; char: number }> } | null\n  freeUnicode: (encoded: { ptr: Pointer; data: Array<{ width: number; char: number }> }) => void\n  bufferDrawChar: (buffer: Pointer, char: number, x: number, y: number, fg: RGBA, bg: RGBA, attributes?: number) => void\n\n  registerNativeSpanFeedStream: (stream: Pointer, handler: NativeSpanFeedEventHandler) => void\n  unregisterNativeSpanFeedStream: (stream: Pointer) => void\n  createNativeSpanFeed: (options?: NativeSpanFeedOptions | null) => Pointer\n  attachNativeSpanFeed: (stream: Pointer) => number\n  destroyNativeSpanFeed: (stream: Pointer) => void\n  streamWrite: (stream: Pointer, data: Uint8Array | string) => number\n  streamCommit: (stream: Pointer) => number\n  streamDrainSpans: (stream: Pointer, outBuffer: Uint8Array, maxSpans: number) => number\n  streamClose: (stream: Pointer) => number\n  streamSetOptions: (stream: Pointer, options: NativeSpanFeedOptions) => number\n  streamGetStats: (stream: Pointer) => NativeSpanFeedStats | null\n  streamReserve: (stream: Pointer, minLen: number) => { status: number; info: ReserveInfo | null }\n  streamCommitReserved: (stream: Pointer, length: number) => number\n\n  onNativeEvent: (name: string, handler: (data: ArrayBuffer) => void) => void\n  onceNativeEvent: (name: string, handler: (data: ArrayBuffer) => void) => void\n  offNativeEvent: (name: string, handler: (data: ArrayBuffer) => void) => void\n  onAnyNativeEvent: (handler: (name: string, data: ArrayBuffer) => void) => void\n}\n\nclass FFIRenderLib implements RenderLib {\n  private opentui: ReturnType<typeof getOpenTUILib>\n  public readonly encoder: TextEncoder = new TextEncoder()\n  public readonly decoder: TextDecoder = new TextDecoder()\n  private logCallbackWrapper: any // Store the FFI callback wrapper\n  private eventCallbackWrapper: any // Store the FFI event callback wrapper\n  private _nativeEvents: EventEmitter = new EventEmitter()\n  private _anyEventHandlers: Array<(name: string, data: ArrayBuffer) => void> = []\n  private nativeSpanFeedCallbackWrapper: JSCallback | null = null\n  private nativeSpanFeedHandlers = new Map<Pointer, NativeSpanFeedEventHandler>()\n\n  constructor(libPath?: string) {\n    this.opentui = getOpenTUILib(libPath)\n    this.setupLogging()\n    this.setupEventBus()\n  }\n\n  private setupLogging() {\n    if (this.logCallbackWrapper) {\n      return\n    }\n\n    const logCallback = new JSCallback(\n      (level: number, msgPtr: Pointer, msgLenBigInt: bigint | number) => {\n        try {\n          const msgLen = typeof msgLenBigInt === \"bigint\" ? Number(msgLenBigInt) : msgLenBigInt\n\n          if (msgLen === 0 || !msgPtr) {\n            return\n          }\n\n          const msgBuffer = toArrayBuffer(msgPtr, 0, msgLen)\n          const msgBytes = new Uint8Array(msgBuffer)\n          const message = this.decoder.decode(msgBytes)\n\n          switch (level) {\n            case LogLevel.Error:\n              console.error(message)\n              break\n            case LogLevel.Warn:\n              console.warn(message)\n              break\n            case LogLevel.Info:\n              console.info(message)\n              break\n            case LogLevel.Debug:\n              console.debug(message)\n              break\n            default:\n              console.log(message)\n          }\n        } catch (error) {\n          console.error(\"Error in Zig log callback:\", error)\n        }\n      },\n      {\n        args: [\"u8\", \"ptr\", \"usize\"],\n        returns: \"void\",\n      },\n    )\n\n    this.logCallbackWrapper = logCallback\n\n    if (!logCallback.ptr) {\n      throw new Error(\"Failed to create log callback\")\n    }\n\n    this.setLogCallback(logCallback.ptr)\n  }\n\n  private setLogCallback(callbackPtr: Pointer) {\n    this.opentui.symbols.setLogCallback(callbackPtr)\n  }\n\n  private setupEventBus() {\n    if (this.eventCallbackWrapper) {\n      return\n    }\n\n    const eventCallback = new JSCallback(\n      (namePtr: Pointer, nameLenBigInt: bigint | number, dataPtr: Pointer, dataLenBigInt: bigint | number) => {\n        try {\n          const nameLen = typeof nameLenBigInt === \"bigint\" ? Number(nameLenBigInt) : nameLenBigInt\n          const dataLen = typeof dataLenBigInt === \"bigint\" ? Number(dataLenBigInt) : dataLenBigInt\n\n          if (nameLen === 0 || !namePtr) {\n            return\n          }\n\n          const nameBuffer = toArrayBuffer(namePtr, 0, nameLen)\n          const nameBytes = new Uint8Array(nameBuffer)\n          const eventName = this.decoder.decode(nameBytes)\n\n          let eventData: ArrayBuffer\n          if (dataLen > 0 && dataPtr) {\n            eventData = toArrayBuffer(dataPtr, 0, dataLen).slice()\n          } else {\n            eventData = new ArrayBuffer(0)\n          }\n\n          queueMicrotask(() => {\n            this._nativeEvents.emit(eventName, eventData)\n\n            for (const handler of this._anyEventHandlers) {\n              handler(eventName, eventData)\n            }\n          })\n        } catch (error) {\n          console.error(\"Error in native event callback:\", error)\n        }\n      },\n      {\n        args: [\"ptr\", \"usize\", \"ptr\", \"usize\"],\n        returns: \"void\",\n      },\n    )\n\n    this.eventCallbackWrapper = eventCallback\n\n    if (!eventCallback.ptr) {\n      throw new Error(\"Failed to create event callback\")\n    }\n\n    this.setEventCallback(eventCallback.ptr)\n  }\n\n  private ensureNativeSpanFeedCallback(): JSCallback {\n    if (this.nativeSpanFeedCallbackWrapper) {\n      return this.nativeSpanFeedCallbackWrapper\n    }\n\n    const callback = new JSCallback(\n      (streamPtr: Pointer, eventId: number, arg0: Pointer, arg1: number | bigint) => {\n        const handler = this.nativeSpanFeedHandlers.get(toPointer(streamPtr))\n        if (handler) {\n          handler(eventId, arg0, arg1)\n        }\n      },\n      {\n        args: [\"ptr\", \"u32\", \"ptr\", \"u64\"],\n        returns: \"void\",\n      },\n    )\n\n    this.nativeSpanFeedCallbackWrapper = callback\n\n    if (!callback.ptr) {\n      throw new Error(\"Failed to create native span feed callback\")\n    }\n\n    return callback\n  }\n\n  private setEventCallback(callbackPtr: Pointer) {\n    this.opentui.symbols.setEventCallback(callbackPtr)\n  }\n\n  public createRenderer(width: number, height: number, options: { testing?: boolean; remote?: boolean } = {}) {\n    const testing = options.testing ?? false\n    const remote = options.remote ?? false\n    return this.opentui.symbols.createRenderer(width, height, testing, remote)\n  }\n\n  public setTerminalEnvVar(renderer: Pointer, key: string, value: string): boolean {\n    const keyBytes = this.encoder.encode(key)\n    const valueBytes = this.encoder.encode(value)\n    return this.opentui.symbols.setTerminalEnvVar(renderer, keyBytes, keyBytes.length, valueBytes, valueBytes.length)\n  }\n\n  public destroyRenderer(renderer: Pointer): void {\n    this.opentui.symbols.destroyRenderer(renderer)\n  }\n\n  public setUseThread(renderer: Pointer, useThread: boolean) {\n    this.opentui.symbols.setUseThread(renderer, useThread)\n  }\n\n  public setBackgroundColor(renderer: Pointer, color: RGBA) {\n    this.opentui.symbols.setBackgroundColor(renderer, color.buffer)\n  }\n\n  public setRenderOffset(renderer: Pointer, offset: number) {\n    this.opentui.symbols.setRenderOffset(renderer, offset)\n  }\n\n  public updateStats(renderer: Pointer, time: number, fps: number, frameCallbackTime: number) {\n    this.opentui.symbols.updateStats(renderer, time, fps, frameCallbackTime)\n  }\n\n  public updateMemoryStats(renderer: Pointer, heapUsed: number, heapTotal: number, arrayBuffers: number) {\n    this.opentui.symbols.updateMemoryStats(renderer, heapUsed, heapTotal, arrayBuffers)\n  }\n\n  public getNextBuffer(renderer: Pointer): OptimizedBuffer {\n    const bufferPtr = this.opentui.symbols.getNextBuffer(renderer)\n    if (!bufferPtr) {\n      throw new Error(\"Failed to get next buffer\")\n    }\n\n    const width = this.opentui.symbols.getBufferWidth(bufferPtr)\n    const height = this.opentui.symbols.getBufferHeight(bufferPtr)\n\n    return new OptimizedBuffer(this, bufferPtr, width, height, { id: \"next buffer\", widthMethod: \"unicode\" })\n  }\n\n  public getCurrentBuffer(renderer: Pointer): OptimizedBuffer {\n    const bufferPtr = this.opentui.symbols.getCurrentBuffer(renderer)\n    if (!bufferPtr) {\n      throw new Error(\"Failed to get current buffer\")\n    }\n\n    const width = this.opentui.symbols.getBufferWidth(bufferPtr)\n    const height = this.opentui.symbols.getBufferHeight(bufferPtr)\n\n    return new OptimizedBuffer(this, bufferPtr, width, height, { id: \"current buffer\", widthMethod: \"unicode\" })\n  }\n\n  public bufferGetCharPtr(buffer: Pointer): Pointer {\n    const ptr = this.opentui.symbols.bufferGetCharPtr(buffer)\n    if (!ptr) {\n      throw new Error(\"Failed to get char pointer\")\n    }\n    return ptr\n  }\n\n  public bufferGetFgPtr(buffer: Pointer): Pointer {\n    const ptr = this.opentui.symbols.bufferGetFgPtr(buffer)\n    if (!ptr) {\n      throw new Error(\"Failed to get fg pointer\")\n    }\n    return ptr\n  }\n\n  public bufferGetBgPtr(buffer: Pointer): Pointer {\n    const ptr = this.opentui.symbols.bufferGetBgPtr(buffer)\n    if (!ptr) {\n      throw new Error(\"Failed to get bg pointer\")\n    }\n    return ptr\n  }\n\n  public bufferGetAttributesPtr(buffer: Pointer): Pointer {\n    const ptr = this.opentui.symbols.bufferGetAttributesPtr(buffer)\n    if (!ptr) {\n      throw new Error(\"Failed to get attributes pointer\")\n    }\n    return ptr\n  }\n\n  public bufferGetRespectAlpha(buffer: Pointer): boolean {\n    return this.opentui.symbols.bufferGetRespectAlpha(buffer)\n  }\n\n  public bufferSetRespectAlpha(buffer: Pointer, respectAlpha: boolean): void {\n    this.opentui.symbols.bufferSetRespectAlpha(buffer, respectAlpha)\n  }\n\n  public bufferGetId(buffer: Pointer): string {\n    const maxLen = 256\n    const outBuffer = new Uint8Array(maxLen)\n    const actualLen = this.opentui.symbols.bufferGetId(buffer, outBuffer, maxLen)\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n    return this.decoder.decode(outBuffer.slice(0, len))\n  }\n\n  public bufferGetRealCharSize(buffer: Pointer): number {\n    return this.opentui.symbols.bufferGetRealCharSize(buffer)\n  }\n\n  public bufferWriteResolvedChars(buffer: Pointer, outputBuffer: Uint8Array, addLineBreaks: boolean): number {\n    const bytesWritten = this.opentui.symbols.bufferWriteResolvedChars(\n      buffer,\n      outputBuffer,\n      outputBuffer.length,\n      addLineBreaks,\n    )\n    return typeof bytesWritten === \"bigint\" ? Number(bytesWritten) : bytesWritten\n  }\n\n  public getBufferWidth(buffer: Pointer): number {\n    return this.opentui.symbols.getBufferWidth(buffer)\n  }\n\n  public getBufferHeight(buffer: Pointer): number {\n    return this.opentui.symbols.getBufferHeight(buffer)\n  }\n\n  public bufferClear(buffer: Pointer, color: RGBA) {\n    this.opentui.symbols.bufferClear(buffer, color.buffer)\n  }\n\n  public bufferDrawText(\n    buffer: Pointer,\n    text: string,\n    x: number,\n    y: number,\n    color: RGBA,\n    bgColor?: RGBA,\n    attributes?: number,\n  ) {\n    const textBytes = this.encoder.encode(text)\n    const textLength = textBytes.byteLength\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = color.buffer\n\n    this.opentui.symbols.bufferDrawText(buffer, textBytes, textLength, x, y, fg, bg, attributes ?? 0)\n  }\n\n  public bufferSetCellWithAlphaBlending(\n    buffer: Pointer,\n    x: number,\n    y: number,\n    char: string,\n    color: RGBA,\n    bgColor: RGBA,\n    attributes?: number,\n  ) {\n    const charPtr = char.codePointAt(0) ?? \" \".codePointAt(0)!\n    const bg = bgColor.buffer\n    const fg = color.buffer\n\n    this.opentui.symbols.bufferSetCellWithAlphaBlending(buffer, x, y, charPtr, fg, bg, attributes ?? 0)\n  }\n\n  public bufferSetCell(\n    buffer: Pointer,\n    x: number,\n    y: number,\n    char: string,\n    color: RGBA,\n    bgColor: RGBA,\n    attributes?: number,\n  ) {\n    const charPtr = char.codePointAt(0) ?? \" \".codePointAt(0)!\n    const bg = bgColor.buffer\n    const fg = color.buffer\n\n    this.opentui.symbols.bufferSetCell(buffer, x, y, charPtr, fg, bg, attributes ?? 0)\n  }\n\n  public bufferFillRect(buffer: Pointer, x: number, y: number, width: number, height: number, color: RGBA) {\n    const bg = color.buffer\n    this.opentui.symbols.bufferFillRect(buffer, x, y, width, height, bg)\n  }\n\n  public bufferColorMatrix(\n    buffer: Pointer,\n    matrixPtr: Pointer,\n    cellMaskPtr: Pointer,\n    cellMaskCount: number,\n    strength: number,\n    target: TargetChannel,\n  ): void {\n    this.opentui.symbols.bufferColorMatrix(buffer, matrixPtr, cellMaskPtr, cellMaskCount, strength, target)\n  }\n\n  public bufferColorMatrixUniform(buffer: Pointer, matrixPtr: Pointer, strength: number, target: TargetChannel): void {\n    this.opentui.symbols.bufferColorMatrixUniform(buffer, matrixPtr, strength, target)\n  }\n\n  public bufferDrawSuperSampleBuffer(\n    buffer: Pointer,\n    x: number,\n    y: number,\n    pixelDataPtr: Pointer,\n    pixelDataLength: number,\n    format: \"bgra8unorm\" | \"rgba8unorm\",\n    alignedBytesPerRow: number,\n  ): void {\n    const formatId = format === \"bgra8unorm\" ? 0 : 1\n    this.opentui.symbols.bufferDrawSuperSampleBuffer(\n      buffer,\n      x,\n      y,\n      pixelDataPtr,\n      pixelDataLength,\n      formatId,\n      alignedBytesPerRow,\n    )\n  }\n\n  public bufferDrawPackedBuffer(\n    buffer: Pointer,\n    dataPtr: Pointer,\n    dataLen: number,\n    posX: number,\n    posY: number,\n    terminalWidthCells: number,\n    terminalHeightCells: number,\n  ): void {\n    this.opentui.symbols.bufferDrawPackedBuffer(\n      buffer,\n      dataPtr,\n      dataLen,\n      posX,\n      posY,\n      terminalWidthCells,\n      terminalHeightCells,\n    )\n  }\n\n  public bufferDrawGrayscaleBuffer(\n    buffer: Pointer,\n    posX: number,\n    posY: number,\n    intensitiesPtr: Pointer,\n    srcWidth: number,\n    srcHeight: number,\n    fg: RGBA | null,\n    bg: RGBA | null,\n  ): void {\n    this.opentui.symbols.bufferDrawGrayscaleBuffer(\n      buffer,\n      posX,\n      posY,\n      intensitiesPtr,\n      srcWidth,\n      srcHeight,\n      fg?.buffer ?? null,\n      bg?.buffer ?? null,\n    )\n  }\n\n  public bufferDrawGrayscaleBufferSupersampled(\n    buffer: Pointer,\n    posX: number,\n    posY: number,\n    intensitiesPtr: Pointer,\n    srcWidth: number,\n    srcHeight: number,\n    fg: RGBA | null,\n    bg: RGBA | null,\n  ): void {\n    this.opentui.symbols.bufferDrawGrayscaleBufferSupersampled(\n      buffer,\n      posX,\n      posY,\n      intensitiesPtr,\n      srcWidth,\n      srcHeight,\n      fg?.buffer ?? null,\n      bg?.buffer ?? null,\n    )\n  }\n\n  public bufferDrawGrid(\n    buffer: Pointer,\n    borderChars: Uint32Array,\n    borderFg: RGBA,\n    borderBg: RGBA,\n    columnOffsets: Int32Array,\n    columnCount: number,\n    rowOffsets: Int32Array,\n    rowCount: number,\n    options: { drawInner: boolean; drawOuter: boolean },\n  ): void {\n    const optionsBuffer = GridDrawOptionsStruct.pack({\n      drawInner: options.drawInner,\n      drawOuter: options.drawOuter,\n    })\n\n    this.opentui.symbols.bufferDrawGrid(\n      buffer,\n      borderChars,\n      borderFg.buffer,\n      borderBg.buffer,\n      columnOffsets,\n      columnCount,\n      rowOffsets,\n      rowCount,\n      ptr(optionsBuffer),\n    )\n  }\n\n  public bufferDrawBox(\n    buffer: Pointer,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    borderChars: Uint32Array,\n    packedOptions: number,\n    borderColor: RGBA,\n    backgroundColor: RGBA,\n    title: string | null,\n  ): void {\n    const titleBytes = title ? this.encoder.encode(title) : null\n    const titleLen = title ? titleBytes!.length : 0\n    const titlePtr = title ? titleBytes : null\n\n    this.opentui.symbols.bufferDrawBox(\n      buffer,\n      x,\n      y,\n      width,\n      height,\n      borderChars,\n      packedOptions,\n      borderColor.buffer,\n      backgroundColor.buffer,\n      titlePtr,\n      titleLen,\n    )\n  }\n\n  public bufferResize(buffer: Pointer, width: number, height: number): void {\n    this.opentui.symbols.bufferResize(buffer, width, height)\n  }\n\n  // Link API\n  public linkAlloc(url: string): number {\n    const urlBytes = this.encoder.encode(url)\n    return this.opentui.symbols.linkAlloc(urlBytes, urlBytes.length)\n  }\n\n  public linkGetUrl(linkId: number, maxLen: number = 512): string {\n    const outBuffer = new Uint8Array(maxLen)\n    const actualLen = this.opentui.symbols.linkGetUrl(linkId, outBuffer, maxLen)\n    return this.decoder.decode(outBuffer.slice(0, actualLen))\n  }\n\n  public attributesWithLink(baseAttributes: number, linkId: number): number {\n    return this.opentui.symbols.attributesWithLink(baseAttributes, linkId)\n  }\n\n  public attributesGetLinkId(attributes: number): number {\n    return this.opentui.symbols.attributesGetLinkId(attributes)\n  }\n\n  public resizeRenderer(renderer: Pointer, width: number, height: number) {\n    this.opentui.symbols.resizeRenderer(renderer, width, height)\n  }\n\n  public setCursorPosition(renderer: Pointer, x: number, y: number, visible: boolean) {\n    this.opentui.symbols.setCursorPosition(renderer, x, y, visible)\n  }\n\n  public setCursorColor(renderer: Pointer, color: RGBA) {\n    this.opentui.symbols.setCursorColor(renderer, color.buffer)\n  }\n\n  public getCursorState(renderer: Pointer): CursorState {\n    const cursorBuffer = new ArrayBuffer(CursorStateStruct.size)\n    this.opentui.symbols.getCursorState(renderer, ptr(cursorBuffer))\n    const struct = CursorStateStruct.unpack(cursorBuffer)\n\n    return {\n      x: struct.x,\n      y: struct.y,\n      visible: struct.visible,\n      style: CURSOR_ID_TO_STYLE[struct.style] ?? \"block\",\n      blinking: struct.blinking,\n      color: RGBA.fromValues(struct.r, struct.g, struct.b, struct.a),\n    }\n  }\n\n  public setCursorStyleOptions(renderer: Pointer, options: CursorStyleOptions): void {\n    const style = options.style != null ? CURSOR_STYLE_TO_ID[options.style] : 255\n    const blinking = options.blinking != null ? (options.blinking ? 1 : 0) : 255\n    const cursor = options.cursor != null ? MOUSE_STYLE_TO_ID[options.cursor] : 255\n\n    const buffer = CursorStyleOptionsStruct.pack({ style, blinking, color: options.color, cursor })\n    this.opentui.symbols.setCursorStyleOptions(renderer, ptr(buffer))\n  }\n\n  public render(renderer: Pointer, force: boolean) {\n    this.opentui.symbols.render(renderer, force)\n  }\n\n  public createOptimizedBuffer(\n    width: number,\n    height: number,\n    widthMethod: WidthMethod,\n    respectAlpha: boolean = false,\n    id?: string,\n  ): OptimizedBuffer {\n    if (Number.isNaN(width) || Number.isNaN(height)) {\n      console.error(new Error(`Invalid dimensions for OptimizedBuffer: ${width}x${height}`).stack)\n    }\n\n    const widthMethodCode = widthMethod === \"wcwidth\" ? 0 : 1\n    const idToUse = id || \"unnamed buffer\"\n    const idBytes = this.encoder.encode(idToUse)\n    const bufferPtr = this.opentui.symbols.createOptimizedBuffer(\n      width,\n      height,\n      respectAlpha,\n      widthMethodCode,\n      idBytes,\n      idBytes.length,\n    )\n    if (!bufferPtr) {\n      throw new Error(`Failed to create optimized buffer: ${width}x${height}`)\n    }\n\n    return new OptimizedBuffer(this, bufferPtr, width, height, { respectAlpha, id, widthMethod })\n  }\n\n  public destroyOptimizedBuffer(bufferPtr: Pointer) {\n    this.opentui.symbols.destroyOptimizedBuffer(bufferPtr)\n  }\n\n  public drawFrameBuffer(\n    targetBufferPtr: Pointer,\n    destX: number,\n    destY: number,\n    bufferPtr: Pointer,\n    sourceX?: number,\n    sourceY?: number,\n    sourceWidth?: number,\n    sourceHeight?: number,\n  ) {\n    const srcX = sourceX ?? 0\n    const srcY = sourceY ?? 0\n    const srcWidth = sourceWidth ?? 0\n    const srcHeight = sourceHeight ?? 0\n    this.opentui.symbols.drawFrameBuffer(targetBufferPtr, destX, destY, bufferPtr, srcX, srcY, srcWidth, srcHeight)\n  }\n\n  public setDebugOverlay(renderer: Pointer, enabled: boolean, corner: DebugOverlayCorner) {\n    this.opentui.symbols.setDebugOverlay(renderer, enabled, corner)\n  }\n\n  public clearTerminal(renderer: Pointer) {\n    this.opentui.symbols.clearTerminal(renderer)\n  }\n\n  public setTerminalTitle(renderer: Pointer, title: string) {\n    const titleBytes = this.encoder.encode(title)\n    this.opentui.symbols.setTerminalTitle(renderer, titleBytes, titleBytes.length)\n  }\n\n  public copyToClipboardOSC52(renderer: Pointer, target: number, payload: Uint8Array): boolean {\n    return this.opentui.symbols.copyToClipboardOSC52(renderer, target, payload, payload.length)\n  }\n\n  public clearClipboardOSC52(renderer: Pointer, target: number): boolean {\n    return this.opentui.symbols.clearClipboardOSC52(renderer, target)\n  }\n\n  public addToHitGrid(renderer: Pointer, x: number, y: number, width: number, height: number, id: number) {\n    this.opentui.symbols.addToHitGrid(renderer, x, y, width, height, id)\n  }\n\n  public clearCurrentHitGrid(renderer: Pointer) {\n    this.opentui.symbols.clearCurrentHitGrid(renderer)\n  }\n\n  public hitGridPushScissorRect(renderer: Pointer, x: number, y: number, width: number, height: number) {\n    this.opentui.symbols.hitGridPushScissorRect(renderer, x, y, width, height)\n  }\n\n  public hitGridPopScissorRect(renderer: Pointer) {\n    this.opentui.symbols.hitGridPopScissorRect(renderer)\n  }\n\n  public hitGridClearScissorRects(renderer: Pointer) {\n    this.opentui.symbols.hitGridClearScissorRects(renderer)\n  }\n\n  public addToCurrentHitGridClipped(\n    renderer: Pointer,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    id: number,\n  ) {\n    this.opentui.symbols.addToCurrentHitGridClipped(renderer, x, y, width, height, id)\n  }\n\n  public checkHit(renderer: Pointer, x: number, y: number): number {\n    return this.opentui.symbols.checkHit(renderer, x, y)\n  }\n\n  public getHitGridDirty(renderer: Pointer): boolean {\n    return this.opentui.symbols.getHitGridDirty(renderer)\n  }\n\n  public dumpHitGrid(renderer: Pointer): void {\n    this.opentui.symbols.dumpHitGrid(renderer)\n  }\n\n  public dumpBuffers(renderer: Pointer, timestamp?: number): void {\n    const ts = timestamp ?? Date.now()\n    this.opentui.symbols.dumpBuffers(renderer, ts)\n  }\n\n  public dumpStdoutBuffer(renderer: Pointer, timestamp?: number): void {\n    const ts = timestamp ?? Date.now()\n    this.opentui.symbols.dumpStdoutBuffer(renderer, ts)\n  }\n\n  public restoreTerminalModes(renderer: Pointer): void {\n    this.opentui.symbols.restoreTerminalModes(renderer)\n  }\n\n  public enableMouse(renderer: Pointer, enableMovement: boolean): void {\n    this.opentui.symbols.enableMouse(renderer, enableMovement)\n  }\n\n  public disableMouse(renderer: Pointer): void {\n    this.opentui.symbols.disableMouse(renderer)\n  }\n\n  public enableKittyKeyboard(renderer: Pointer, flags: number): void {\n    this.opentui.symbols.enableKittyKeyboard(renderer, flags)\n  }\n\n  public disableKittyKeyboard(renderer: Pointer): void {\n    this.opentui.symbols.disableKittyKeyboard(renderer)\n  }\n\n  public setKittyKeyboardFlags(renderer: Pointer, flags: number): void {\n    this.opentui.symbols.setKittyKeyboardFlags(renderer, flags)\n  }\n\n  public getKittyKeyboardFlags(renderer: Pointer): number {\n    return this.opentui.symbols.getKittyKeyboardFlags(renderer)\n  }\n\n  public setupTerminal(renderer: Pointer, useAlternateScreen: boolean): void {\n    this.opentui.symbols.setupTerminal(renderer, useAlternateScreen)\n  }\n\n  public suspendRenderer(renderer: Pointer): void {\n    this.opentui.symbols.suspendRenderer(renderer)\n  }\n\n  public resumeRenderer(renderer: Pointer): void {\n    this.opentui.symbols.resumeRenderer(renderer)\n  }\n\n  public queryPixelResolution(renderer: Pointer): void {\n    this.opentui.symbols.queryPixelResolution(renderer)\n  }\n\n  /**\n   * Write data to stdout, synchronizing with the render thread if necessary.\n   * This should be used for ALL stdout writes to avoid race conditions when\n   * the render thread is active.\n   */\n  public writeOut(renderer: Pointer, data: string | Uint8Array): void {\n    const bytes = typeof data === \"string\" ? new TextEncoder().encode(data) : data\n    if (bytes.length === 0) return\n    this.opentui.symbols.writeOut(renderer, ptr(bytes), bytes.length)\n  }\n\n  // TextBuffer methods\n  public createTextBuffer(widthMethod: WidthMethod): TextBuffer {\n    const widthMethodCode = widthMethod === \"wcwidth\" ? 0 : 1\n    const bufferPtr = this.opentui.symbols.createTextBuffer(widthMethodCode)\n    if (!bufferPtr) {\n      throw new Error(`Failed to create TextBuffer`)\n    }\n\n    return new TextBuffer(this, bufferPtr)\n  }\n\n  public destroyTextBuffer(buffer: Pointer): void {\n    this.opentui.symbols.destroyTextBuffer(buffer)\n  }\n\n  public textBufferGetLength(buffer: Pointer): number {\n    return this.opentui.symbols.textBufferGetLength(buffer)\n  }\n\n  public textBufferGetByteSize(buffer: Pointer): number {\n    return this.opentui.symbols.textBufferGetByteSize(buffer)\n  }\n\n  public textBufferReset(buffer: Pointer): void {\n    this.opentui.symbols.textBufferReset(buffer)\n  }\n\n  public textBufferClear(buffer: Pointer): void {\n    this.opentui.symbols.textBufferClear(buffer)\n  }\n\n  public textBufferSetDefaultFg(buffer: Pointer, fg: RGBA | null): void {\n    const fgPtr = fg ? fg.buffer : null\n    this.opentui.symbols.textBufferSetDefaultFg(buffer, fgPtr)\n  }\n\n  public textBufferSetDefaultBg(buffer: Pointer, bg: RGBA | null): void {\n    const bgPtr = bg ? bg.buffer : null\n    this.opentui.symbols.textBufferSetDefaultBg(buffer, bgPtr)\n  }\n\n  public textBufferSetDefaultAttributes(buffer: Pointer, attributes: number | null): void {\n    const attrValue = attributes === null ? null : new Uint8Array([attributes])\n    this.opentui.symbols.textBufferSetDefaultAttributes(buffer, attrValue)\n  }\n\n  public textBufferResetDefaults(buffer: Pointer): void {\n    this.opentui.symbols.textBufferResetDefaults(buffer)\n  }\n\n  public textBufferGetTabWidth(buffer: Pointer): number {\n    return this.opentui.symbols.textBufferGetTabWidth(buffer)\n  }\n\n  public textBufferSetTabWidth(buffer: Pointer, width: number): void {\n    this.opentui.symbols.textBufferSetTabWidth(buffer, width)\n  }\n\n  public textBufferRegisterMemBuffer(buffer: Pointer, bytes: Uint8Array, owned: boolean = false): number {\n    const result = this.opentui.symbols.textBufferRegisterMemBuffer(buffer, bytes, bytes.length, owned)\n    if (result === 0xffff) {\n      throw new Error(\"Failed to register memory buffer\")\n    }\n    return result\n  }\n\n  public textBufferReplaceMemBuffer(\n    buffer: Pointer,\n    memId: number,\n    bytes: Uint8Array,\n    owned: boolean = false,\n  ): boolean {\n    return this.opentui.symbols.textBufferReplaceMemBuffer(buffer, memId, bytes, bytes.length, owned)\n  }\n\n  public textBufferClearMemRegistry(buffer: Pointer): void {\n    this.opentui.symbols.textBufferClearMemRegistry(buffer)\n  }\n\n  public textBufferSetTextFromMem(buffer: Pointer, memId: number): void {\n    this.opentui.symbols.textBufferSetTextFromMem(buffer, memId)\n  }\n\n  public textBufferAppend(buffer: Pointer, bytes: Uint8Array): void {\n    this.opentui.symbols.textBufferAppend(buffer, bytes, bytes.length)\n  }\n\n  public textBufferAppendFromMemId(buffer: Pointer, memId: number): void {\n    this.opentui.symbols.textBufferAppendFromMemId(buffer, memId)\n  }\n\n  public textBufferLoadFile(buffer: Pointer, path: string): boolean {\n    const pathBytes = this.encoder.encode(path)\n    return this.opentui.symbols.textBufferLoadFile(buffer, pathBytes, pathBytes.length)\n  }\n\n  public textBufferSetStyledText(\n    buffer: Pointer,\n    chunks: Array<{ text: string; fg?: RGBA | null; bg?: RGBA | null; attributes?: number; link?: { url: string } }>,\n  ): void {\n    if (chunks.length === 0) {\n      this.textBufferClear(buffer)\n      return\n    }\n\n    const chunksBuffer = StyledChunkStruct.packList(chunks)\n    this.opentui.symbols.textBufferSetStyledText(buffer, ptr(chunksBuffer), chunks.length)\n  }\n\n  public textBufferGetLineCount(buffer: Pointer): number {\n    return this.opentui.symbols.textBufferGetLineCount(buffer)\n  }\n\n  private textBufferGetPlainText(buffer: Pointer, outPtr: Pointer, maxLen: number): number {\n    const result = this.opentui.symbols.textBufferGetPlainText(buffer, outPtr, maxLen)\n    return typeof result === \"bigint\" ? Number(result) : result\n  }\n\n  public getPlainTextBytes(buffer: Pointer, maxLength: number): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n\n    const actualLen = this.textBufferGetPlainText(buffer, ptr(outBuffer), maxLength)\n\n    if (actualLen === 0) {\n      return null\n    }\n\n    return outBuffer.slice(0, actualLen)\n  }\n\n  public textBufferGetTextRange(\n    buffer: Pointer,\n    startOffset: number,\n    endOffset: number,\n    maxLength: number,\n  ): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n\n    const actualLen = this.opentui.symbols.textBufferGetTextRange(\n      buffer,\n      startOffset,\n      endOffset,\n      ptr(outBuffer),\n      maxLength,\n    )\n\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n\n    if (len === 0) {\n      return null\n    }\n\n    return outBuffer.slice(0, len)\n  }\n\n  public textBufferGetTextRangeByCoords(\n    buffer: Pointer,\n    startRow: number,\n    startCol: number,\n    endRow: number,\n    endCol: number,\n    maxLength: number,\n  ): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n\n    const actualLen = this.opentui.symbols.textBufferGetTextRangeByCoords(\n      buffer,\n      startRow,\n      startCol,\n      endRow,\n      endCol,\n      ptr(outBuffer),\n      maxLength,\n    )\n\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n\n    if (len === 0) {\n      return null\n    }\n\n    return outBuffer.slice(0, len)\n  }\n\n  // TextBufferView methods\n  public createTextBufferView(textBuffer: Pointer): Pointer {\n    const viewPtr = this.opentui.symbols.createTextBufferView(textBuffer)\n    if (!viewPtr) {\n      throw new Error(\"Failed to create TextBufferView\")\n    }\n    return viewPtr\n  }\n\n  public destroyTextBufferView(view: Pointer): void {\n    this.opentui.symbols.destroyTextBufferView(view)\n  }\n\n  public textBufferViewSetSelection(\n    view: Pointer,\n    start: number,\n    end: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n  ): void {\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = fgColor ? fgColor.buffer : null\n    this.opentui.symbols.textBufferViewSetSelection(view, start, end, bg, fg)\n  }\n\n  public textBufferViewResetSelection(view: Pointer): void {\n    this.opentui.symbols.textBufferViewResetSelection(view)\n  }\n\n  public textBufferViewGetSelection(view: Pointer): { start: number; end: number } | null {\n    const packedInfo = this.textBufferViewGetSelectionInfo(view)\n\n    // Check for no selection marker (0xFFFFFFFF_FFFFFFFF)\n    if (packedInfo === 0xffff_ffff_ffff_ffffn) {\n      return null\n    }\n\n    const start = Number(packedInfo >> 32n)\n    const end = Number(packedInfo & 0xffff_ffffn)\n\n    return { start, end }\n  }\n\n  private textBufferViewGetSelectionInfo(view: Pointer): bigint {\n    return this.opentui.symbols.textBufferViewGetSelectionInfo(view)\n  }\n\n  public textBufferViewSetLocalSelection(\n    view: Pointer,\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n  ): boolean {\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = fgColor ? fgColor.buffer : null\n    return this.opentui.symbols.textBufferViewSetLocalSelection(view, anchorX, anchorY, focusX, focusY, bg, fg)\n  }\n\n  public textBufferViewUpdateSelection(view: Pointer, end: number, bgColor: RGBA | null, fgColor: RGBA | null): void {\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = fgColor ? fgColor.buffer : null\n    this.opentui.symbols.textBufferViewUpdateSelection(view, end, bg, fg)\n  }\n\n  public textBufferViewUpdateLocalSelection(\n    view: Pointer,\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n  ): boolean {\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = fgColor ? fgColor.buffer : null\n    return this.opentui.symbols.textBufferViewUpdateLocalSelection(view, anchorX, anchorY, focusX, focusY, bg, fg)\n  }\n\n  public textBufferViewResetLocalSelection(view: Pointer): void {\n    this.opentui.symbols.textBufferViewResetLocalSelection(view)\n  }\n\n  public textBufferViewSetWrapWidth(view: Pointer, width: number): void {\n    this.opentui.symbols.textBufferViewSetWrapWidth(view, width)\n  }\n\n  public textBufferViewSetWrapMode(view: Pointer, mode: \"none\" | \"char\" | \"word\"): void {\n    const modeValue = mode === \"none\" ? 0 : mode === \"char\" ? 1 : 2\n    this.opentui.symbols.textBufferViewSetWrapMode(view, modeValue)\n  }\n\n  public textBufferViewSetViewportSize(view: Pointer, width: number, height: number): void {\n    this.opentui.symbols.textBufferViewSetViewportSize(view, width, height)\n  }\n\n  public textBufferViewSetViewport(view: Pointer, x: number, y: number, width: number, height: number): void {\n    this.opentui.symbols.textBufferViewSetViewport(view, x, y, width, height)\n  }\n\n  public textBufferViewGetLineInfo(view: Pointer): LineInfo {\n    const outBuffer = new ArrayBuffer(LineInfoStruct.size)\n    this.textBufferViewGetLineInfoDirect(view, ptr(outBuffer))\n    const struct = LineInfoStruct.unpack(outBuffer)\n\n    const lineStartCols = struct.startCols as number[]\n    const lineWidthCols = struct.widthCols as number[]\n    const lineWidthColsMax = struct.widthColsMax\n\n    return {\n      lineStartCols,\n      lineWidthCols,\n      lineWidthColsMax,\n      lineSources: struct.sources as number[],\n      lineWraps: struct.wraps as number[],\n    }\n  }\n\n  public textBufferViewGetLogicalLineInfo(view: Pointer): LineInfo {\n    const outBuffer = new ArrayBuffer(LineInfoStruct.size)\n    this.textBufferViewGetLogicalLineInfoDirect(view, ptr(outBuffer))\n    const struct = LineInfoStruct.unpack(outBuffer)\n\n    const lineStartCols = struct.startCols as number[]\n    const lineWidthCols = struct.widthCols as number[]\n    const lineWidthColsMax = struct.widthColsMax\n\n    return {\n      lineStartCols,\n      lineWidthCols,\n      lineWidthColsMax,\n      lineSources: struct.sources as number[],\n      lineWraps: struct.wraps as number[],\n    }\n  }\n\n  public textBufferViewGetVirtualLineCount(view: Pointer): number {\n    return this.opentui.symbols.textBufferViewGetVirtualLineCount(view)\n  }\n\n  private textBufferViewGetLineInfoDirect(view: Pointer, outPtr: Pointer): void {\n    this.opentui.symbols.textBufferViewGetLineInfoDirect(view, outPtr)\n  }\n\n  private textBufferViewGetLogicalLineInfoDirect(view: Pointer, outPtr: Pointer): void {\n    this.opentui.symbols.textBufferViewGetLogicalLineInfoDirect(view, outPtr)\n  }\n\n  private textBufferViewGetSelectedText(view: Pointer, outPtr: Pointer, maxLen: number): number {\n    const result = this.opentui.symbols.textBufferViewGetSelectedText(view, outPtr, maxLen)\n    return typeof result === \"bigint\" ? Number(result) : result\n  }\n\n  private textBufferViewGetPlainText(view: Pointer, outPtr: Pointer, maxLen: number): number {\n    const result = this.opentui.symbols.textBufferViewGetPlainText(view, outPtr, maxLen)\n    return typeof result === \"bigint\" ? Number(result) : result\n  }\n\n  public textBufferViewGetSelectedTextBytes(view: Pointer, maxLength: number): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n\n    const actualLen = this.textBufferViewGetSelectedText(view, ptr(outBuffer), maxLength)\n\n    if (actualLen === 0) {\n      return null\n    }\n\n    return outBuffer.slice(0, actualLen)\n  }\n\n  public textBufferViewGetPlainTextBytes(view: Pointer, maxLength: number): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n\n    const actualLen = this.textBufferViewGetPlainText(view, ptr(outBuffer), maxLength)\n\n    if (actualLen === 0) {\n      return null\n    }\n\n    return outBuffer.slice(0, actualLen)\n  }\n\n  public textBufferViewSetTabIndicator(view: Pointer, indicator: number): void {\n    this.opentui.symbols.textBufferViewSetTabIndicator(view, indicator)\n  }\n\n  public textBufferViewSetTabIndicatorColor(view: Pointer, color: RGBA): void {\n    this.opentui.symbols.textBufferViewSetTabIndicatorColor(view, color.buffer)\n  }\n\n  public textBufferViewSetTruncate(view: Pointer, truncate: boolean): void {\n    this.opentui.symbols.textBufferViewSetTruncate(view, truncate)\n  }\n\n  public textBufferViewMeasureForDimensions(\n    view: Pointer,\n    width: number,\n    height: number,\n  ): { lineCount: number; widthColsMax: number } | null {\n    const resultBuffer = new ArrayBuffer(MeasureResultStruct.size)\n    const resultPtr = ptr(new Uint8Array(resultBuffer))\n    const success = this.opentui.symbols.textBufferViewMeasureForDimensions(view, width, height, resultPtr)\n    if (!success) {\n      return null\n    }\n    const result = MeasureResultStruct.unpack(resultBuffer)\n    return result\n  }\n\n  public textBufferAddHighlightByCharRange(buffer: Pointer, highlight: Highlight): void {\n    const packedHighlight = HighlightStruct.pack(highlight)\n    this.opentui.symbols.textBufferAddHighlightByCharRange(buffer, ptr(packedHighlight))\n  }\n\n  public textBufferAddHighlight(buffer: Pointer, lineIdx: number, highlight: Highlight): void {\n    const packedHighlight = HighlightStruct.pack(highlight)\n    this.opentui.symbols.textBufferAddHighlight(buffer, lineIdx, ptr(packedHighlight))\n  }\n\n  public textBufferRemoveHighlightsByRef(buffer: Pointer, hlRef: number): void {\n    this.opentui.symbols.textBufferRemoveHighlightsByRef(buffer, hlRef)\n  }\n\n  public textBufferClearLineHighlights(buffer: Pointer, lineIdx: number): void {\n    this.opentui.symbols.textBufferClearLineHighlights(buffer, lineIdx)\n  }\n\n  public textBufferClearAllHighlights(buffer: Pointer): void {\n    this.opentui.symbols.textBufferClearAllHighlights(buffer)\n  }\n\n  public textBufferSetSyntaxStyle(buffer: Pointer, style: Pointer | null): void {\n    this.opentui.symbols.textBufferSetSyntaxStyle(buffer, style)\n  }\n\n  public textBufferGetLineHighlights(buffer: Pointer, lineIdx: number): Array<Highlight> {\n    const outCountBuf = new BigUint64Array(1)\n\n    const nativePtr = this.opentui.symbols.textBufferGetLineHighlightsPtr(buffer, lineIdx, ptr(outCountBuf))\n    if (!nativePtr) return []\n\n    const count = Number(outCountBuf[0])\n    const byteLen = count * HighlightStruct.size\n    const raw = toArrayBuffer(nativePtr, 0, byteLen)\n    const results = HighlightStruct.unpackList(raw, count)\n\n    this.opentui.symbols.textBufferFreeLineHighlights(nativePtr, count)\n\n    return results\n  }\n\n  public textBufferGetHighlightCount(buffer: Pointer): number {\n    return this.opentui.symbols.textBufferGetHighlightCount(buffer)\n  }\n\n  public getArenaAllocatedBytes(): number {\n    const result = this.opentui.symbols.getArenaAllocatedBytes()\n    return typeof result === \"bigint\" ? Number(result) : result\n  }\n\n  public getBuildOptions(): BuildOptions {\n    const optionsBuffer = new ArrayBuffer(BuildOptionsStruct.size)\n    this.opentui.symbols.getBuildOptions(ptr(optionsBuffer))\n    const options = BuildOptionsStruct.unpack(optionsBuffer)\n\n    return {\n      gpaSafeStats: !!options.gpaSafeStats,\n      gpaMemoryLimitTracking: !!options.gpaMemoryLimitTracking,\n    }\n  }\n\n  public getAllocatorStats(): AllocatorStats {\n    const statsBuffer = new ArrayBuffer(AllocatorStatsStruct.size)\n    this.opentui.symbols.getAllocatorStats(ptr(statsBuffer))\n    const stats = AllocatorStatsStruct.unpack(statsBuffer)\n\n    return {\n      totalRequestedBytes: toNumber(stats.totalRequestedBytes),\n      activeAllocations: toNumber(stats.activeAllocations),\n      smallAllocations: toNumber(stats.smallAllocations),\n      largeAllocations: toNumber(stats.largeAllocations),\n      requestedBytesValid: !!stats.requestedBytesValid,\n    }\n  }\n\n  public bufferDrawTextBufferView(buffer: Pointer, view: Pointer, x: number, y: number): void {\n    this.opentui.symbols.bufferDrawTextBufferView(buffer, view, x, y)\n  }\n\n  public bufferDrawEditorView(buffer: Pointer, view: Pointer, x: number, y: number): void {\n    this.opentui.symbols.bufferDrawEditorView(buffer, view, x, y)\n  }\n\n  // EditorView methods\n  public createEditorView(editBufferPtr: Pointer, viewportWidth: number, viewportHeight: number): Pointer {\n    const viewPtr = this.opentui.symbols.createEditorView(editBufferPtr, viewportWidth, viewportHeight)\n    if (!viewPtr) {\n      throw new Error(\"Failed to create EditorView\")\n    }\n    return viewPtr\n  }\n\n  public destroyEditorView(view: Pointer): void {\n    this.opentui.symbols.destroyEditorView(view)\n  }\n\n  public editorViewSetViewportSize(view: Pointer, width: number, height: number): void {\n    this.opentui.symbols.editorViewSetViewportSize(view, width, height)\n  }\n\n  public editorViewSetViewport(\n    view: Pointer,\n    x: number,\n    y: number,\n    width: number,\n    height: number,\n    moveCursor: boolean,\n  ): void {\n    this.opentui.symbols.editorViewSetViewport(view, x, y, width, height, moveCursor)\n  }\n\n  public editorViewGetViewport(view: Pointer): { offsetY: number; offsetX: number; height: number; width: number } {\n    const x = new Uint32Array(1)\n    const y = new Uint32Array(1)\n    const width = new Uint32Array(1)\n    const height = new Uint32Array(1)\n\n    this.opentui.symbols.editorViewGetViewport(view, ptr(x), ptr(y), ptr(width), ptr(height))\n\n    return {\n      offsetX: x[0],\n      offsetY: y[0],\n      width: width[0],\n      height: height[0],\n    }\n  }\n\n  public editorViewSetScrollMargin(view: Pointer, margin: number): void {\n    this.opentui.symbols.editorViewSetScrollMargin(view, margin)\n  }\n\n  public editorViewSetWrapMode(view: Pointer, mode: \"none\" | \"char\" | \"word\"): void {\n    const modeValue = mode === \"none\" ? 0 : mode === \"char\" ? 1 : 2\n    this.opentui.symbols.editorViewSetWrapMode(view, modeValue)\n  }\n\n  public editorViewGetVirtualLineCount(view: Pointer): number {\n    return this.opentui.symbols.editorViewGetVirtualLineCount(view)\n  }\n\n  public editorViewGetTotalVirtualLineCount(view: Pointer): number {\n    return this.opentui.symbols.editorViewGetTotalVirtualLineCount(view)\n  }\n\n  public editorViewGetTextBufferView(view: Pointer): Pointer {\n    const result = this.opentui.symbols.editorViewGetTextBufferView(view)\n    if (!result) {\n      throw new Error(\"Failed to get TextBufferView from EditorView\")\n    }\n    return result\n  }\n\n  public editorViewGetLineInfo(view: Pointer): LineInfo {\n    const outBuffer = new ArrayBuffer(LineInfoStruct.size)\n    this.opentui.symbols.editorViewGetLineInfoDirect(view, ptr(outBuffer))\n    const struct = LineInfoStruct.unpack(outBuffer)\n\n    const lineStartCols = struct.startCols as number[]\n    const lineWidthCols = struct.widthCols as number[]\n    const lineWidthColsMax = struct.widthColsMax\n\n    return {\n      lineStartCols,\n      lineWidthCols,\n      lineWidthColsMax,\n      lineSources: struct.sources as number[],\n      lineWraps: struct.wraps as number[],\n    }\n  }\n\n  public editorViewGetLogicalLineInfo(view: Pointer): LineInfo {\n    const outBuffer = new ArrayBuffer(LineInfoStruct.size)\n    this.opentui.symbols.editorViewGetLogicalLineInfoDirect(view, ptr(outBuffer))\n    const struct = LineInfoStruct.unpack(outBuffer)\n\n    const lineStartCols = struct.startCols as number[]\n    const lineWidthCols = struct.widthCols as number[]\n    const lineWidthColsMax = struct.widthColsMax\n\n    return {\n      lineStartCols,\n      lineWidthCols,\n      lineWidthColsMax,\n      lineSources: struct.sources as number[],\n      lineWraps: struct.wraps as number[],\n    }\n  }\n\n  // EditBuffer implementations\n  public createEditBuffer(widthMethod: WidthMethod): Pointer {\n    const widthMethodCode = widthMethod === \"wcwidth\" ? 0 : 1\n    const bufferPtr = this.opentui.symbols.createEditBuffer(widthMethodCode)\n    if (!bufferPtr) {\n      throw new Error(\"Failed to create EditBuffer\")\n    }\n    return bufferPtr\n  }\n\n  public destroyEditBuffer(buffer: Pointer): void {\n    this.opentui.symbols.destroyEditBuffer(buffer)\n  }\n\n  public editBufferSetText(buffer: Pointer, textBytes: Uint8Array): void {\n    this.opentui.symbols.editBufferSetText(buffer, textBytes, textBytes.length)\n  }\n\n  public editBufferSetTextFromMem(buffer: Pointer, memId: number): void {\n    this.opentui.symbols.editBufferSetTextFromMem(buffer, memId)\n  }\n\n  public editBufferReplaceText(buffer: Pointer, textBytes: Uint8Array): void {\n    this.opentui.symbols.editBufferReplaceText(buffer, textBytes, textBytes.length)\n  }\n\n  public editBufferReplaceTextFromMem(buffer: Pointer, memId: number): void {\n    this.opentui.symbols.editBufferReplaceTextFromMem(buffer, memId)\n  }\n\n  public editBufferGetText(buffer: Pointer, maxLength: number): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n    const actualLen = this.opentui.symbols.editBufferGetText(buffer, ptr(outBuffer), maxLength)\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n    if (len === 0) return null\n    return outBuffer.slice(0, len)\n  }\n\n  public editBufferInsertChar(buffer: Pointer, char: string): void {\n    const charBytes = this.encoder.encode(char)\n    this.opentui.symbols.editBufferInsertChar(buffer, charBytes, charBytes.length)\n  }\n\n  public editBufferInsertText(buffer: Pointer, text: string): void {\n    const textBytes = this.encoder.encode(text)\n    this.opentui.symbols.editBufferInsertText(buffer, textBytes, textBytes.length)\n  }\n\n  public editBufferDeleteChar(buffer: Pointer): void {\n    this.opentui.symbols.editBufferDeleteChar(buffer)\n  }\n\n  public editBufferDeleteCharBackward(buffer: Pointer): void {\n    this.opentui.symbols.editBufferDeleteCharBackward(buffer)\n  }\n\n  public editBufferDeleteRange(\n    buffer: Pointer,\n    startLine: number,\n    startCol: number,\n    endLine: number,\n    endCol: number,\n  ): void {\n    this.opentui.symbols.editBufferDeleteRange(buffer, startLine, startCol, endLine, endCol)\n  }\n\n  public editBufferNewLine(buffer: Pointer): void {\n    this.opentui.symbols.editBufferNewLine(buffer)\n  }\n\n  public editBufferDeleteLine(buffer: Pointer): void {\n    this.opentui.symbols.editBufferDeleteLine(buffer)\n  }\n\n  public editBufferMoveCursorLeft(buffer: Pointer): void {\n    this.opentui.symbols.editBufferMoveCursorLeft(buffer)\n  }\n\n  public editBufferMoveCursorRight(buffer: Pointer): void {\n    this.opentui.symbols.editBufferMoveCursorRight(buffer)\n  }\n\n  public editBufferMoveCursorUp(buffer: Pointer): void {\n    this.opentui.symbols.editBufferMoveCursorUp(buffer)\n  }\n\n  public editBufferMoveCursorDown(buffer: Pointer): void {\n    this.opentui.symbols.editBufferMoveCursorDown(buffer)\n  }\n\n  public editBufferGotoLine(buffer: Pointer, line: number): void {\n    this.opentui.symbols.editBufferGotoLine(buffer, line)\n  }\n\n  public editBufferSetCursor(buffer: Pointer, line: number, byteOffset: number): void {\n    this.opentui.symbols.editBufferSetCursor(buffer, line, byteOffset)\n  }\n\n  public editBufferSetCursorToLineCol(buffer: Pointer, line: number, col: number): void {\n    this.opentui.symbols.editBufferSetCursorToLineCol(buffer, line, col)\n  }\n\n  public editBufferSetCursorByOffset(buffer: Pointer, offset: number): void {\n    this.opentui.symbols.editBufferSetCursorByOffset(buffer, offset)\n  }\n\n  public editBufferGetCursorPosition(buffer: Pointer): LogicalCursor {\n    const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n    this.opentui.symbols.editBufferGetCursorPosition(buffer, ptr(cursorBuffer))\n    return LogicalCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editBufferGetId(buffer: Pointer): number {\n    return this.opentui.symbols.editBufferGetId(buffer)\n  }\n\n  public editBufferGetTextBuffer(buffer: Pointer): Pointer {\n    const result = this.opentui.symbols.editBufferGetTextBuffer(buffer)\n    if (!result) {\n      throw new Error(\"Failed to get TextBuffer from EditBuffer\")\n    }\n    return result\n  }\n\n  public editBufferDebugLogRope(buffer: Pointer): void {\n    this.opentui.symbols.editBufferDebugLogRope(buffer)\n  }\n\n  public editBufferUndo(buffer: Pointer, maxLength: number): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n    const actualLen = this.opentui.symbols.editBufferUndo(buffer, ptr(outBuffer), maxLength)\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n    if (len === 0) return null\n    return outBuffer.slice(0, len)\n  }\n\n  public editBufferRedo(buffer: Pointer, maxLength: number): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n    const actualLen = this.opentui.symbols.editBufferRedo(buffer, ptr(outBuffer), maxLength)\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n    if (len === 0) return null\n    return outBuffer.slice(0, len)\n  }\n\n  public editBufferCanUndo(buffer: Pointer): boolean {\n    return this.opentui.symbols.editBufferCanUndo(buffer)\n  }\n\n  public editBufferCanRedo(buffer: Pointer): boolean {\n    return this.opentui.symbols.editBufferCanRedo(buffer)\n  }\n\n  public editBufferClearHistory(buffer: Pointer): void {\n    this.opentui.symbols.editBufferClearHistory(buffer)\n  }\n\n  public editBufferClear(buffer: Pointer): void {\n    this.opentui.symbols.editBufferClear(buffer)\n  }\n\n  public editBufferGetNextWordBoundary(buffer: Pointer): LogicalCursor {\n    const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n    this.opentui.symbols.editBufferGetNextWordBoundary(buffer, ptr(cursorBuffer))\n    return LogicalCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editBufferGetPrevWordBoundary(buffer: Pointer): LogicalCursor {\n    const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n    this.opentui.symbols.editBufferGetPrevWordBoundary(buffer, ptr(cursorBuffer))\n    return LogicalCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editBufferGetEOL(buffer: Pointer): LogicalCursor {\n    const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n    this.opentui.symbols.editBufferGetEOL(buffer, ptr(cursorBuffer))\n    return LogicalCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editBufferOffsetToPosition(buffer: Pointer, offset: number): LogicalCursor | null {\n    const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n    const success = this.opentui.symbols.editBufferOffsetToPosition(buffer, offset, ptr(cursorBuffer))\n    if (!success) return null\n    return LogicalCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editBufferPositionToOffset(buffer: Pointer, row: number, col: number): number {\n    return this.opentui.symbols.editBufferPositionToOffset(buffer, row, col)\n  }\n\n  public editBufferGetLineStartOffset(buffer: Pointer, row: number): number {\n    return this.opentui.symbols.editBufferGetLineStartOffset(buffer, row)\n  }\n\n  public editBufferGetTextRange(\n    buffer: Pointer,\n    startOffset: number,\n    endOffset: number,\n    maxLength: number,\n  ): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n    const actualLen = this.opentui.symbols.editBufferGetTextRange(\n      buffer,\n      startOffset,\n      endOffset,\n      ptr(outBuffer),\n      maxLength,\n    )\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n    if (len === 0) return null\n    return outBuffer.slice(0, len)\n  }\n\n  public editBufferGetTextRangeByCoords(\n    buffer: Pointer,\n    startRow: number,\n    startCol: number,\n    endRow: number,\n    endCol: number,\n    maxLength: number,\n  ): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n    const actualLen = this.opentui.symbols.editBufferGetTextRangeByCoords(\n      buffer,\n      startRow,\n      startCol,\n      endRow,\n      endCol,\n      ptr(outBuffer),\n      maxLength,\n    )\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n    if (len === 0) return null\n    return outBuffer.slice(0, len)\n  }\n\n  // EditorView selection and editing implementations\n  public editorViewSetSelection(\n    view: Pointer,\n    start: number,\n    end: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n  ): void {\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = fgColor ? fgColor.buffer : null\n    this.opentui.symbols.editorViewSetSelection(view, start, end, bg, fg)\n  }\n\n  public editorViewResetSelection(view: Pointer): void {\n    this.opentui.symbols.editorViewResetSelection(view)\n  }\n\n  public editorViewGetSelection(view: Pointer): { start: number; end: number } | null {\n    const packedInfo = this.opentui.symbols.editorViewGetSelection(view)\n    if (packedInfo === 0xffff_ffff_ffff_ffffn) {\n      return null\n    }\n    const start = Number(packedInfo >> 32n)\n    const end = Number(packedInfo & 0xffff_ffffn)\n    return { start, end }\n  }\n\n  public editorViewSetLocalSelection(\n    view: Pointer,\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n    updateCursor: boolean,\n    followCursor: boolean,\n  ): boolean {\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = fgColor ? fgColor.buffer : null\n    return this.opentui.symbols.editorViewSetLocalSelection(\n      view,\n      anchorX,\n      anchorY,\n      focusX,\n      focusY,\n      bg,\n      fg,\n      updateCursor,\n      followCursor,\n    )\n  }\n\n  public editorViewUpdateSelection(view: Pointer, end: number, bgColor: RGBA | null, fgColor: RGBA | null): void {\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = fgColor ? fgColor.buffer : null\n    this.opentui.symbols.editorViewUpdateSelection(view, end, bg, fg)\n  }\n\n  public editorViewUpdateLocalSelection(\n    view: Pointer,\n    anchorX: number,\n    anchorY: number,\n    focusX: number,\n    focusY: number,\n    bgColor: RGBA | null,\n    fgColor: RGBA | null,\n    updateCursor: boolean,\n    followCursor: boolean,\n  ): boolean {\n    const bg = bgColor ? bgColor.buffer : null\n    const fg = fgColor ? fgColor.buffer : null\n    return this.opentui.symbols.editorViewUpdateLocalSelection(\n      view,\n      anchorX,\n      anchorY,\n      focusX,\n      focusY,\n      bg,\n      fg,\n      updateCursor,\n      followCursor,\n    )\n  }\n\n  public editorViewResetLocalSelection(view: Pointer): void {\n    this.opentui.symbols.editorViewResetLocalSelection(view)\n  }\n\n  public editorViewGetSelectedTextBytes(view: Pointer, maxLength: number): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n    const actualLen = this.opentui.symbols.editorViewGetSelectedTextBytes(view, ptr(outBuffer), maxLength)\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n    if (len === 0) return null\n    return outBuffer.slice(0, len)\n  }\n\n  public editorViewGetCursor(view: Pointer): { row: number; col: number } {\n    const row = new Uint32Array(1)\n    const col = new Uint32Array(1)\n    this.opentui.symbols.editorViewGetCursor(view, ptr(row), ptr(col))\n    return { row: row[0], col: col[0] }\n  }\n\n  public editorViewGetText(view: Pointer, maxLength: number): Uint8Array | null {\n    const outBuffer = new Uint8Array(maxLength)\n    const actualLen = this.opentui.symbols.editorViewGetText(view, ptr(outBuffer), maxLength)\n    const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n    if (len === 0) return null\n    return outBuffer.slice(0, len)\n  }\n\n  public editorViewGetVisualCursor(view: Pointer): VisualCursor {\n    const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n    this.opentui.symbols.editorViewGetVisualCursor(view, ptr(cursorBuffer))\n    return VisualCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editorViewMoveUpVisual(view: Pointer): void {\n    this.opentui.symbols.editorViewMoveUpVisual(view)\n  }\n\n  public editorViewMoveDownVisual(view: Pointer): void {\n    this.opentui.symbols.editorViewMoveDownVisual(view)\n  }\n\n  public editorViewDeleteSelectedText(view: Pointer): void {\n    this.opentui.symbols.editorViewDeleteSelectedText(view)\n  }\n\n  public editorViewSetCursorByOffset(view: Pointer, offset: number): void {\n    this.opentui.symbols.editorViewSetCursorByOffset(view, offset)\n  }\n\n  public editorViewGetNextWordBoundary(view: Pointer): VisualCursor {\n    const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n    this.opentui.symbols.editorViewGetNextWordBoundary(view, ptr(cursorBuffer))\n    return VisualCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editorViewGetPrevWordBoundary(view: Pointer): VisualCursor {\n    const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n    this.opentui.symbols.editorViewGetPrevWordBoundary(view, ptr(cursorBuffer))\n    return VisualCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editorViewGetEOL(view: Pointer): VisualCursor {\n    const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n    this.opentui.symbols.editorViewGetEOL(view, ptr(cursorBuffer))\n    return VisualCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editorViewGetVisualSOL(view: Pointer): VisualCursor {\n    const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n    this.opentui.symbols.editorViewGetVisualSOL(view, ptr(cursorBuffer))\n    return VisualCursorStruct.unpack(cursorBuffer)\n  }\n\n  public editorViewGetVisualEOL(view: Pointer): VisualCursor {\n    const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n    this.opentui.symbols.editorViewGetVisualEOL(view, ptr(cursorBuffer))\n    return VisualCursorStruct.unpack(cursorBuffer)\n  }\n\n  public bufferPushScissorRect(buffer: Pointer, x: number, y: number, width: number, height: number): void {\n    this.opentui.symbols.bufferPushScissorRect(buffer, x, y, width, height)\n  }\n\n  public bufferPopScissorRect(buffer: Pointer): void {\n    this.opentui.symbols.bufferPopScissorRect(buffer)\n  }\n\n  public bufferClearScissorRects(buffer: Pointer): void {\n    this.opentui.symbols.bufferClearScissorRects(buffer)\n  }\n\n  public bufferPushOpacity(buffer: Pointer, opacity: number): void {\n    this.opentui.symbols.bufferPushOpacity(buffer, opacity)\n  }\n\n  public bufferPopOpacity(buffer: Pointer): void {\n    this.opentui.symbols.bufferPopOpacity(buffer)\n  }\n\n  public bufferGetCurrentOpacity(buffer: Pointer): number {\n    return this.opentui.symbols.bufferGetCurrentOpacity(buffer)\n  }\n\n  public bufferClearOpacity(buffer: Pointer): void {\n    this.opentui.symbols.bufferClearOpacity(buffer)\n  }\n\n  public getTerminalCapabilities(renderer: Pointer) {\n    const capsBuffer = new ArrayBuffer(TerminalCapabilitiesStruct.size)\n    this.opentui.symbols.getTerminalCapabilities(renderer, ptr(capsBuffer))\n\n    const caps = TerminalCapabilitiesStruct.unpack(capsBuffer)\n\n    return {\n      kitty_keyboard: caps.kitty_keyboard,\n      kitty_graphics: caps.kitty_graphics,\n      rgb: caps.rgb,\n      unicode: caps.unicode,\n      sgr_pixels: caps.sgr_pixels,\n      color_scheme_updates: caps.color_scheme_updates,\n      explicit_width: caps.explicit_width,\n      scaled_text: caps.scaled_text,\n      sixel: caps.sixel,\n      focus_tracking: caps.focus_tracking,\n      sync: caps.sync,\n      bracketed_paste: caps.bracketed_paste,\n      hyperlinks: caps.hyperlinks,\n      osc52: caps.osc52,\n      explicit_cursor_positioning: caps.explicit_cursor_positioning,\n      terminal: {\n        name: caps.term_name ?? \"\",\n        version: caps.term_version ?? \"\",\n        from_xtversion: caps.term_from_xtversion,\n      },\n    }\n  }\n\n  public processCapabilityResponse(renderer: Pointer, response: string): void {\n    const responseBytes = this.encoder.encode(response)\n    this.opentui.symbols.processCapabilityResponse(renderer, responseBytes, responseBytes.length)\n  }\n\n  public encodeUnicode(\n    text: string,\n    widthMethod: WidthMethod,\n  ): { ptr: Pointer; data: Array<{ width: number; char: number }> } | null {\n    const textBytes = this.encoder.encode(text)\n    const widthMethodCode = widthMethod === \"wcwidth\" ? 0 : 1\n\n    const outPtrBuffer = new ArrayBuffer(8) // Pointer size\n    const outLenBuffer = new ArrayBuffer(8) // usize\n\n    const success = this.opentui.symbols.encodeUnicode(\n      textBytes,\n      textBytes.length,\n      ptr(outPtrBuffer),\n      ptr(outLenBuffer),\n      widthMethodCode,\n    )\n\n    if (!success) {\n      return null\n    }\n\n    const outPtrView = new BigUint64Array(outPtrBuffer)\n    const outLenView = new BigUint64Array(outLenBuffer)\n\n    const resultPtr = Number(outPtrView[0]) as Pointer\n    const resultLen = Number(outLenView[0])\n\n    if (resultLen === 0) {\n      return { ptr: resultPtr, data: [] }\n    }\n\n    // Convert pointer to ArrayBuffer and use EncodedCharStruct to unpack the list\n    const byteLen = resultLen * EncodedCharStruct.size\n    const raw = toArrayBuffer(resultPtr, 0, byteLen)\n    const data = EncodedCharStruct.unpackList(raw, resultLen)\n\n    return { ptr: resultPtr, data }\n  }\n\n  public freeUnicode(encoded: { ptr: Pointer; data: Array<{ width: number; char: number }> }): void {\n    this.opentui.symbols.freeUnicode(encoded.ptr, encoded.data.length)\n  }\n\n  public bufferDrawChar(\n    buffer: Pointer,\n    char: number,\n    x: number,\n    y: number,\n    fg: RGBA,\n    bg: RGBA,\n    attributes: number = 0,\n  ): void {\n    this.opentui.symbols.bufferDrawChar(buffer, char, x, y, fg.buffer, bg.buffer, attributes)\n  }\n\n  public registerNativeSpanFeedStream(stream: Pointer, handler: NativeSpanFeedEventHandler): void {\n    const callback = this.ensureNativeSpanFeedCallback()\n    this.nativeSpanFeedHandlers.set(toPointer(stream), handler)\n    this.opentui.symbols.streamSetCallback(stream, callback.ptr)\n  }\n\n  public unregisterNativeSpanFeedStream(stream: Pointer): void {\n    this.opentui.symbols.streamSetCallback(stream, null)\n    this.nativeSpanFeedHandlers.delete(toPointer(stream))\n  }\n\n  public createNativeSpanFeed(options?: NativeSpanFeedOptions | null): Pointer {\n    const optionsBuffer = options == null ? null : NativeSpanFeedOptionsStruct.pack(options)\n    const streamPtr = this.opentui.symbols.createNativeSpanFeed(optionsBuffer ? ptr(optionsBuffer) : null)\n    if (!streamPtr) {\n      throw new Error(\"Failed to create stream\")\n    }\n    return toPointer(streamPtr)\n  }\n\n  public attachNativeSpanFeed(stream: Pointer): number {\n    return this.opentui.symbols.attachNativeSpanFeed(stream)\n  }\n\n  public destroyNativeSpanFeed(stream: Pointer): void {\n    this.opentui.symbols.destroyNativeSpanFeed(stream)\n    this.nativeSpanFeedHandlers.delete(toPointer(stream))\n  }\n\n  public streamWrite(stream: Pointer, data: Uint8Array | string): number {\n    const bytes = typeof data === \"string\" ? this.encoder.encode(data) : data\n    return this.opentui.symbols.streamWrite(stream, ptr(bytes), bytes.length)\n  }\n\n  public streamCommit(stream: Pointer): number {\n    return this.opentui.symbols.streamCommit(stream)\n  }\n\n  public streamDrainSpans(stream: Pointer, outBuffer: Uint8Array, maxSpans: number): number {\n    const count = this.opentui.symbols.streamDrainSpans(stream, ptr(outBuffer), maxSpans)\n    return toNumber(count)\n  }\n\n  public streamClose(stream: Pointer): number {\n    return this.opentui.symbols.streamClose(stream)\n  }\n\n  public streamSetOptions(stream: Pointer, options: NativeSpanFeedOptions): number {\n    const optionsBuffer = NativeSpanFeedOptionsStruct.pack(options)\n    return this.opentui.symbols.streamSetOptions(stream, ptr(optionsBuffer))\n  }\n\n  public streamGetStats(stream: Pointer): NativeSpanFeedStats | null {\n    const statsBuffer = new ArrayBuffer(NativeSpanFeedStatsStruct.size)\n    const status = this.opentui.symbols.streamGetStats(stream, ptr(statsBuffer))\n    if (status !== 0) {\n      return null\n    }\n    const stats = NativeSpanFeedStatsStruct.unpack(statsBuffer)\n    return {\n      bytesWritten: typeof stats.bytesWritten === \"bigint\" ? stats.bytesWritten : BigInt(stats.bytesWritten),\n      spansCommitted: typeof stats.spansCommitted === \"bigint\" ? stats.spansCommitted : BigInt(stats.spansCommitted),\n      chunks: stats.chunks,\n      pendingSpans: stats.pendingSpans,\n    }\n  }\n\n  public streamReserve(stream: Pointer, minLen: number): { status: number; info: ReserveInfo | null } {\n    const reserveBuffer = new ArrayBuffer(ReserveInfoStruct.size)\n    const status = this.opentui.symbols.streamReserve(stream, minLen, ptr(reserveBuffer))\n    if (status !== 0) {\n      return { status, info: null }\n    }\n    return { status, info: ReserveInfoStruct.unpack(reserveBuffer) }\n  }\n\n  public streamCommitReserved(stream: Pointer, length: number): number {\n    return this.opentui.symbols.streamCommitReserved(stream, length)\n  }\n\n  public createSyntaxStyle(): Pointer {\n    const stylePtr = this.opentui.symbols.createSyntaxStyle()\n    if (!stylePtr) {\n      throw new Error(\"Failed to create SyntaxStyle\")\n    }\n    return stylePtr\n  }\n\n  public destroySyntaxStyle(style: Pointer): void {\n    this.opentui.symbols.destroySyntaxStyle(style)\n  }\n\n  public syntaxStyleRegister(\n    style: Pointer,\n    name: string,\n    fg: RGBA | null,\n    bg: RGBA | null,\n    attributes: number,\n  ): number {\n    const nameBytes = this.encoder.encode(name)\n    const fgPtr = fg ? fg.buffer : null\n    const bgPtr = bg ? bg.buffer : null\n    return this.opentui.symbols.syntaxStyleRegister(style, nameBytes, nameBytes.length, fgPtr, bgPtr, attributes)\n  }\n\n  public syntaxStyleResolveByName(style: Pointer, name: string): number | null {\n    const nameBytes = this.encoder.encode(name)\n    const id = this.opentui.symbols.syntaxStyleResolveByName(style, nameBytes, nameBytes.length)\n    return id === 0 ? null : id\n  }\n\n  public syntaxStyleGetStyleCount(style: Pointer): number {\n    const result = this.opentui.symbols.syntaxStyleGetStyleCount(style)\n    return typeof result === \"bigint\" ? Number(result) : result\n  }\n\n  public editorViewSetPlaceholderStyledText(\n    view: Pointer,\n    chunks: Array<{ text: string; fg?: RGBA | null; bg?: RGBA | null; attributes?: number }>,\n  ): void {\n    const nonEmptyChunks = chunks.filter((c) => c.text.length > 0)\n    if (nonEmptyChunks.length === 0) {\n      this.opentui.symbols.editorViewSetPlaceholderStyledText(view, null, 0)\n      return\n    }\n\n    const chunksBuffer = StyledChunkStruct.packList(nonEmptyChunks)\n    this.opentui.symbols.editorViewSetPlaceholderStyledText(view, ptr(chunksBuffer), nonEmptyChunks.length)\n  }\n\n  public editorViewSetTabIndicator(view: Pointer, indicator: number): void {\n    this.opentui.symbols.editorViewSetTabIndicator(view, indicator)\n  }\n\n  public editorViewSetTabIndicatorColor(view: Pointer, color: RGBA): void {\n    this.opentui.symbols.editorViewSetTabIndicatorColor(view, color.buffer)\n  }\n\n  public onNativeEvent(name: string, handler: (data: ArrayBuffer) => void): void {\n    this._nativeEvents.on(name, handler)\n  }\n\n  public onceNativeEvent(name: string, handler: (data: ArrayBuffer) => void): void {\n    this._nativeEvents.once(name, handler)\n  }\n\n  public offNativeEvent(name: string, handler: (data: ArrayBuffer) => void): void {\n    this._nativeEvents.off(name, handler)\n  }\n\n  public onAnyNativeEvent(handler: (name: string, data: ArrayBuffer) => void): void {\n    this._anyEventHandlers.push(handler)\n  }\n}\n\nlet opentuiLibPath: string | undefined\nlet opentuiLib: RenderLib | undefined\n\nexport function setRenderLibPath(libPath: string) {\n  if (opentuiLibPath !== libPath) {\n    opentuiLibPath = libPath\n    opentuiLib = undefined\n  }\n}\n\nexport function resolveRenderLib(): RenderLib {\n  if (!opentuiLib) {\n    try {\n      opentuiLib = new FFIRenderLib(opentuiLibPath)\n    } catch (error) {\n      throw new Error(\n        `Failed to initialize OpenTUI render library: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n      )\n    }\n  }\n  return opentuiLib\n}\n\n// Try eager loading\ntry {\n  opentuiLib = new FFIRenderLib(opentuiLibPath)\n} catch (error) {}\n"
  },
  {
    "path": "packages/core/tsconfig.build.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./dist\",\n    \"noEmit\": false,\n    \"rootDir\": \"./src\",\n    \"types\": [\"bun\", \"node\", \"three\"],\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\n    \"**/*.test.ts\",\n    \"**/*.spec.ts\",\n    \"**/*.fixture.ts\",\n    \"src/examples/**/*\",\n    \"src/benchmark/**/*\",\n    \"src/zig/**/*\"\n  ]\n}\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // Enable latest features\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": true,\n\n    // Bundler mode\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n\n    // Best practices\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    // Some stricter flags (disabled by default)\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false\n  },\n  \"exclude\": [\"dist\"]\n}\n"
  },
  {
    "path": "packages/react/README.md",
    "content": "# @opentui/react\n\nA React renderer for building terminal user interfaces using [OpenTUI core](https://github.com/anomalyco/opentui). Create rich, interactive console applications with familiar React patterns and components.\n\n## Installation\n\nQuick start with [bun](https://bun.sh) and [create-tui](https://github.com/msmps/create-tui):\n\n```bash\nbun create tui --template react\n```\n\nManual installation:\n\n```bash\nbun install @opentui/react @opentui/core react\n```\n\n## Quick Start\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nfunction App() {\n  return <text>Hello, world!</text>\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n## TypeScript Configuration\n\nFor optimal TypeScript support, configure your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@opentui/react\",\n    \"strict\": true,\n    \"skipLibCheck\": true\n  }\n}\n```\n\n## Table of Contents\n\n- [Core Concepts](#core-concepts)\n  - [Components](#components)\n  - [Styling](#styling)\n- [API Reference](#api-reference)\n  - [createRoot(renderer)](#createrootrenderer)\n  - [render(element, config?)](#renderelement-config-deprecated)\n  - [Hooks](#hooks)\n    - [useRenderer()](#userenderer)\n    - [useKeyboard(handler, options?)](#usekeyboardhandler-options)\n    - [useOnResize(callback)](#useonresizecallback)\n    - [useTerminalDimensions()](#useterminaldimensions)\n    - [useTimeline(options?)](#usetimelineoptions)\n- [Components](#components-1)\n  - [Layout & Display Components](#layout--display-components)\n    - [Text Component](#text-component)\n    - [Box Component](#box-component)\n    - [Scrollbox Component](#scrollbox-component)\n    - [ASCII Font Component](#ascii-font-component)\n  - [Input Components](#input-components)\n    - [Input Component](#input-component)\n    - [Textarea Component](#textarea-component)\n    - [Select Component](#select-component)\n  - [Code & Diff Components](#code--diff-components)\n    - [Code Component](#code-component)\n    - [Line Number Component](#line-number-component)\n    - [Diff Component](#diff-component)\n- [Examples](#examples)\n  - [Login Form](#login-form)\n  - [Counter with Timer](#counter-with-timer)\n  - [System Monitor Animation](#system-monitor-animation)\n  - [Styled Text Showcase](#styled-text-showcase)\n- [Component Extension](#component-extension)\n- [Using React DevTools](#using-react-devtools)\n\n## Core Concepts\n\n### Components\n\nOpenTUI React provides several built-in components that map to OpenTUI core renderables:\n\n**Layout & Display:**\n\n- **`<text>`** - Display text with styling\n- **`<box>`** - Container with borders and layout\n- **`<scrollbox>`** - A scrollable box\n- **`<ascii-font>`** - Display ASCII art text with different font styles\n\n**Input Components:**\n\n- **`<input>`** - Text input field\n- **`<textarea>`** - Multi-line text input field\n- **`<select>`** - Selection dropdown\n- **`<tab-select>`** - Tab-based selection\n\n**Code & Diff Components:**\n\n- **`<code>`** - Code block with syntax highlighting\n- **`<line-number>`** - Code display with line numbers, diff highlights, and diagnostics\n- **`<diff>`** - Unified or split diff viewer with syntax highlighting\n\n**Helpers:**\n\n- **`<span>`, `<strong>`, `<em>`, `<u>`, `<b>`, `<i>`, `<br>`** - Text modifiers (_must be used inside of the text component_)\n\n### Styling\n\nComponents can be styled using props or the `style` prop:\n\n```tsx\n// Direct props\n<box backgroundColor=\"blue\" padding={2}>\n  <text>Hello, world!</text>\n</box>\n\n// Style prop\n<box style={{ backgroundColor: \"blue\", padding: 2 }}>\n  <text>Hello, world!</text>\n</box>\n```\n\n## API Reference\n\n### `createRoot(renderer)`\n\nCreates a root for rendering a React tree with the given CLI renderer.\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nconst renderer = await createCliRenderer({\n  // Optional renderer configuration\n  exitOnCtrlC: false,\n})\ncreateRoot(renderer).render(<App />)\n```\n\n**Parameters:**\n\n- `renderer`: A `CliRenderer` instance (typically created with `createCliRenderer()`)\n\n**Returns:** An object with a `render` method that accepts a React element.\n\n### `render(element, config?)` (Deprecated)\n\n> **Deprecated:** Use `createRoot(renderer).render(node)` instead.\n\nRenders a React element to the terminal. This function is deprecated in favor of `createRoot`.\n\n### Hooks\n\n#### `useRenderer()`\n\nAccess the OpenTUI renderer instance.\n\n```tsx\nimport { useRenderer } from \"@opentui/react\"\n\nfunction App() {\n  const renderer = useRenderer()\n\n  useEffect(() => {\n    renderer.console.show()\n    console.log(\"Hello, from the console!\")\n  }, [])\n\n  return <box />\n}\n```\n\n#### `useKeyboard(handler, options?)`\n\nHandle keyboard events.\n\n```tsx\nimport { useKeyboard } from \"@opentui/react\"\n\nfunction App() {\n  useKeyboard((key) => {\n    if (key.name === \"escape\") {\n      process.exit(0)\n    }\n  })\n\n  return <text>Press ESC to exit</text>\n}\n```\n\n**Parameters:**\n\n- `handler`: Callback function that receives a `KeyEvent` object\n- `options?`: Optional configuration object:\n  - `release?`: Boolean to include key release events (default: `false`)\n\nBy default, only receives press events (including key repeats with `repeated: true`). Set `options.release` to `true` to also receive release events.\n\n**Example with release events:**\n\n```tsx\nimport { useKeyboard } from \"@opentui/react\"\nimport { useState } from \"react\"\n\nfunction App() {\n  const [pressedKeys, setPressedKeys] = useState<Set<string>>(new Set())\n\n  useKeyboard(\n    (event) => {\n      setPressedKeys((keys) => {\n        const newKeys = new Set(keys)\n        if (event.eventType === \"release\") {\n          newKeys.delete(event.name)\n        } else {\n          newKeys.add(event.name)\n        }\n        return newKeys\n      })\n    },\n    { release: true },\n  )\n\n  return (\n    <box>\n      <text>Currently pressed: {Array.from(pressedKeys).join(\", \") || \"none\"}</text>\n    </box>\n  )\n}\n```\n\n#### `useOnResize(callback)`\n\nHandle terminal resize events.\n\n```tsx\nimport { useOnResize, useRenderer } from \"@opentui/react\"\nimport { useEffect } from \"react\"\n\nfunction App() {\n  const renderer = useRenderer()\n\n  useEffect(() => {\n    renderer.console.show()\n  }, [renderer])\n\n  useOnResize((width, height) => {\n    console.log(`Terminal resized to ${width}x${height}`)\n  })\n\n  return <text>Resize-aware component</text>\n}\n```\n\n#### `useTerminalDimensions()`\n\nGet current terminal dimensions and automatically update when the terminal is resized.\n\n```tsx\nimport { useTerminalDimensions } from \"@opentui/react\"\n\nfunction App() {\n  const { width, height } = useTerminalDimensions()\n\n  return (\n    <box>\n      <text>\n        Terminal dimensions: {width}x{height}\n      </text>\n      <box style={{ width: Math.floor(width / 2), height: Math.floor(height / 3) }}>\n        <text>Half-width, third-height box</text>\n      </box>\n    </box>\n  )\n}\n```\n\n**Returns:** An object with `width` and `height` properties representing the current terminal dimensions.\n\n#### `useTimeline(options?)`\n\nCreate and manage animations using OpenTUI's timeline system. This hook automatically registers and unregisters the timeline with the animation engine.\n\n```tsx\nimport { useTimeline } from \"@opentui/react\"\nimport { useEffect, useState } from \"react\"\n\nfunction App() {\n  const [width, setWidth] = useState(0)\n\n  const timeline = useTimeline({\n    duration: 2000,\n    loop: false,\n  })\n\n  useEffect(() => {\n    timeline.add(\n      {\n        width,\n      },\n      {\n        width: 50,\n        duration: 2000,\n        ease: \"linear\",\n        onUpdate: (animation) => {\n          setWidth(animation.targets[0].width)\n        },\n      },\n    )\n  }, [])\n\n  return <box style={{ width, backgroundColor: \"#6a5acd\" }} />\n}\n```\n\n**Parameters:**\n\n- `options?`: Optional `TimelineOptions` object with properties:\n  - `duration?`: Animation duration in milliseconds (default: 1000)\n  - `loop?`: Whether the timeline should loop (default: false)\n  - `autoplay?`: Whether to automatically start the timeline (default: true)\n  - `onComplete?`: Callback when timeline completes\n  - `onPause?`: Callback when timeline is paused\n\n**Returns:** A `Timeline` instance with methods:\n\n- `add(target, properties, startTime)`: Add animation to timeline\n- `play()`: Start the timeline\n- `pause()`: Pause the timeline\n- `restart()`: Restart the timeline from beginning\n\n## Components\n\n### Layout & Display Components\n\n#### Text Component\n\nDisplay text with rich formatting.\n\n```tsx\nfunction App() {\n  return (\n    <box>\n      {/* Simple text */}\n      <text>Hello World</text>\n\n      {/* Rich text with children */}\n      <text>\n        <span fg=\"red\">Red Text</span>\n      </text>\n\n      {/* Text modifiers */}\n      <text>\n        <strong>Bold</strong>, <em>Italic</em>, and <u>Underlined</u>\n      </text>\n    </box>\n  )\n}\n```\n\n#### Box Component\n\nContainer with borders and layout capabilities.\n\n```tsx\nfunction App() {\n  return (\n    <box flexDirection=\"column\">\n      {/* Basic box */}\n      <box border>\n        <text>Simple box</text>\n      </box>\n\n      {/* Box with title and styling */}\n      <box title=\"Settings\" border borderStyle=\"double\" padding={2} backgroundColor=\"blue\">\n        <text>Box content</text>\n      </box>\n\n      {/* Styled box */}\n      <box\n        style={{\n          border: true,\n          width: 40,\n          height: 10,\n          margin: 1,\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <text>Centered content</text>\n      </box>\n    </box>\n  )\n}\n```\n\n#### Scrollbox Component\n\nA scrollable box.\n\n```tsx\nfunction App() {\n  return (\n    <scrollbox\n      style={{\n        rootOptions: {\n          backgroundColor: \"#24283b\",\n        },\n        wrapperOptions: {\n          backgroundColor: \"#1f2335\",\n        },\n        viewportOptions: {\n          backgroundColor: \"#1a1b26\",\n        },\n        contentOptions: {\n          backgroundColor: \"#16161e\",\n        },\n        scrollbarOptions: {\n          showArrows: true,\n          trackOptions: {\n            foregroundColor: \"#7aa2f7\",\n            backgroundColor: \"#414868\",\n          },\n        },\n      }}\n      focused\n    >\n      {Array.from({ length: 1000 }).map((_, i) => (\n        <box\n          key={i}\n          style={{ width: \"100%\", padding: 1, marginBottom: 1, backgroundColor: i % 2 === 0 ? \"#292e42\" : \"#2f3449\" }}\n        >\n          <text content={`Box ${i}`} />\n        </box>\n      ))}\n    </scrollbox>\n  )\n}\n```\n\n#### ASCII Font Component\n\nDisplay ASCII art text with different font styles.\n\n```tsx\nimport { useState } from \"react\"\n\nfunction App() {\n  const text = \"ASCII\"\n  const [font, setFont] = useState<\"block\" | \"shade\" | \"slick\" | \"tiny\">(\"tiny\")\n\n  return (\n    <box style={{ border: true, paddingLeft: 1, paddingRight: 1 }}>\n      <box\n        style={{\n          height: 8,\n          border: true,\n          marginBottom: 1,\n        }}\n      >\n        <select\n          focused\n          onChange={(_, option) => setFont(option?.value)}\n          showScrollIndicator\n          options={[\n            {\n              name: \"Tiny\",\n              description: \"Tiny font\",\n              value: \"tiny\",\n            },\n            {\n              name: \"Block\",\n              description: \"Block font\",\n              value: \"block\",\n            },\n            {\n              name: \"Slick\",\n              description: \"Slick font\",\n              value: \"slick\",\n            },\n            {\n              name: \"Shade\",\n              description: \"Shade font\",\n              value: \"shade\",\n            },\n          ]}\n          style={{ flexGrow: 1 }}\n        />\n      </box>\n\n      <ascii-font text={text} font={font} />\n    </box>\n  )\n}\n```\n\n### Input Components\n\n#### Input Component\n\nText input field with event handling.\n\n```tsx\nimport { useState } from \"react\"\n\nfunction App() {\n  const [value, setValue] = useState(\"\")\n\n  return (\n    <box title=\"Enter your name\" style={{ border: true, height: 3 }}>\n      <input\n        placeholder=\"Type here...\"\n        focused\n        onInput={setValue}\n        onSubmit={(value) => console.log(\"Submitted:\", value)}\n      />\n    </box>\n  )\n}\n```\n\n#### Textarea Component\n\n```tsx\nimport type { TextareaRenderable } from \"@opentui/core\"\nimport { useKeyboard, useRenderer } from \"@opentui/react\"\nimport { useEffect, useRef } from \"react\"\n\nfunction App() {\n  const renderer = useRenderer()\n  const textareaRef = useRef<TextareaRenderable>(null)\n\n  useEffect(() => {\n    renderer.console.show()\n  }, [renderer])\n\n  useKeyboard((key) => {\n    if (key.name === \"return\") {\n      console.log(textareaRef.current?.plainText)\n    }\n  })\n\n  return (\n    <box title=\"Interactive Editor\" style={{ border: true, flexGrow: 1 }}>\n      <textarea ref={textareaRef} placeholder=\"Type here...\" focused />\n    </box>\n  )\n}\n```\n\n#### Select Component\n\nDropdown selection component.\n\n```tsx\nimport type { SelectOption } from \"@opentui/core\"\nimport { useState } from \"react\"\n\nfunction App() {\n  const [selectedIndex, setSelectedIndex] = useState(0)\n\n  const options: SelectOption[] = [\n    { name: \"Option 1\", description: \"Option 1 description\", value: \"opt1\" },\n    { name: \"Option 2\", description: \"Option 2 description\", value: \"opt2\" },\n    { name: \"Option 3\", description: \"Option 3 description\", value: \"opt3\" },\n  ]\n\n  return (\n    <box style={{ border: true, height: 24 }}>\n      <select\n        style={{ height: 22 }}\n        options={options}\n        focused={true}\n        onChange={(index, option) => {\n          setSelectedIndex(index)\n          console.log(\"Selected:\", option)\n        }}\n      />\n    </box>\n  )\n}\n```\n\n### Code & Diff Components\n\n#### Code Component\n\n```tsx\nimport { RGBA, SyntaxStyle } from \"@opentui/core\"\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  keyword: { fg: RGBA.fromHex(\"#ff6b6b\"), bold: true }, // red, bold\n  string: { fg: RGBA.fromHex(\"#51cf66\") }, // green\n  comment: { fg: RGBA.fromHex(\"#868e96\"), italic: true }, // gray, italic\n  number: { fg: RGBA.fromHex(\"#ffd43b\") }, // yellow\n  default: { fg: RGBA.fromHex(\"#ffffff\") }, // white\n})\n\nconst codeExample = `function hello() {\n  // This is a comment\n\n  const message = \"Hello, world!\"\n  const count = 42\n\n  return message + \" \" + count\n}`\n\nfunction App() {\n  return (\n    <box style={{ border: true, flexGrow: 1 }}>\n      <code content={codeExample} filetype=\"javascript\" syntaxStyle={syntaxStyle} />\n    </box>\n  )\n}\n```\n\n#### Line Number Component\n\nDisplay code with line numbers, and optionally add diff highlights or diagnostic indicators.\n\n```tsx\nimport type { LineNumberRenderable } from \"@opentui/core\"\nimport { RGBA, SyntaxStyle } from \"@opentui/core\"\nimport { useEffect, useRef } from \"react\"\n\nfunction App() {\n  const lineNumberRef = useRef<LineNumberRenderable>(null)\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    keyword: { fg: RGBA.fromHex(\"#C792EA\") },\n    string: { fg: RGBA.fromHex(\"#C3E88D\") },\n    number: { fg: RGBA.fromHex(\"#F78C6C\") },\n    default: { fg: RGBA.fromHex(\"#A6ACCD\") },\n  })\n\n  const codeContent = `function fibonacci(n: number): number {\n  if (n <= 1) return n\n  return fibonacci(n - 1) + fibonacci(n - 2)\n}\n\nconsole.log(fibonacci(10))`\n\n  useEffect(() => {\n    // Add diff highlight - line was added\n    lineNumberRef.current?.setLineColor(1, \"#1a4d1a\")\n    lineNumberRef.current?.setLineSign(1, { after: \" +\", afterColor: \"#22c55e\" })\n\n    // Add diagnostic indicator\n    lineNumberRef.current?.setLineSign(4, { before: \"⚠️\", beforeColor: \"#f59e0b\" })\n  }, [])\n\n  return (\n    <box style={{ border: true, flexGrow: 1 }}>\n      <line-number\n        ref={lineNumberRef}\n        fg=\"#6b7280\"\n        bg=\"#161b22\"\n        minWidth={3}\n        paddingRight={1}\n        showLineNumbers={true}\n        width=\"100%\"\n        height=\"100%\"\n      >\n        <code content={codeContent} filetype=\"typescript\" syntaxStyle={syntaxStyle} width=\"100%\" height=\"100%\" />\n      </line-number>\n    </box>\n  )\n}\n```\n\nFor a more complete example with interactive diff highlights and diagnostics, see [`examples/line-number.tsx`](examples/line-number.tsx).\n\n#### Diff Component\n\nDisplay unified or split-view diffs with syntax highlighting, customizable themes, and line number support. Supports multiple view modes (unified/split), word wrapping, and theme customization.\n\nFor a complete interactive example with theme switching and keybindings, see [`examples/diff.tsx`](examples/diff.tsx).\n\n## Examples\n\n### Login Form\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createRoot, useKeyboard } from \"@opentui/react\"\nimport { useCallback, useState } from \"react\"\n\nfunction App() {\n  const [username, setUsername] = useState(\"\")\n  const [password, setPassword] = useState(\"\")\n  const [focused, setFocused] = useState<\"username\" | \"password\">(\"username\")\n  const [status, setStatus] = useState(\"idle\")\n\n  useKeyboard((key) => {\n    if (key.name === \"tab\") {\n      setFocused((prev) => (prev === \"username\" ? \"password\" : \"username\"))\n    }\n  })\n\n  const handleSubmit = useCallback(() => {\n    if (username === \"admin\" && password === \"secret\") {\n      setStatus(\"success\")\n    } else {\n      setStatus(\"error\")\n    }\n  }, [username, password])\n\n  return (\n    <box style={{ border: true, padding: 2, flexDirection: \"column\", gap: 1 }}>\n      <text fg=\"#FFFF00\">Login Form</text>\n\n      <box title=\"Username\" style={{ border: true, width: 40, height: 3 }}>\n        <input\n          placeholder=\"Enter username...\"\n          onInput={setUsername}\n          onSubmit={handleSubmit}\n          focused={focused === \"username\"}\n        />\n      </box>\n\n      <box title=\"Password\" style={{ border: true, width: 40, height: 3 }}>\n        <input\n          placeholder=\"Enter password...\"\n          onInput={setPassword}\n          onSubmit={handleSubmit}\n          focused={focused === \"password\"}\n        />\n      </box>\n\n      <text\n        style={{\n          fg: status === \"success\" ? \"green\" : status === \"error\" ? \"red\" : \"#999\",\n        }}\n      >\n        {status.toUpperCase()}\n      </text>\n    </box>\n  )\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n### Counter with Timer\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\nimport { useEffect, useState } from \"react\"\n\nfunction App() {\n  const [count, setCount] = useState(0)\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setCount((prev) => prev + 1)\n    }, 1000)\n\n    return () => clearInterval(interval)\n  }, [])\n\n  return (\n    <box title=\"Counter\" style={{ padding: 2 }}>\n      <text fg=\"#00FF00\">{`Count: ${count}`}</text>\n    </box>\n  )\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n### System Monitor Animation\n\n```tsx\nimport { createCliRenderer, TextAttributes } from \"@opentui/core\"\nimport { createRoot, useTimeline } from \"@opentui/react\"\nimport { useEffect, useState } from \"react\"\n\ntype Stats = {\n  cpu: number\n  memory: number\n  network: number\n  disk: number\n}\n\nexport const App = () => {\n  const [stats, setAnimatedStats] = useState<Stats>({\n    cpu: 0,\n    memory: 0,\n    network: 0,\n    disk: 0,\n  })\n\n  const timeline = useTimeline({\n    duration: 3000,\n    loop: false,\n  })\n\n  useEffect(() => {\n    timeline.add(\n      stats,\n      {\n        cpu: 85,\n        memory: 70,\n        network: 95,\n        disk: 60,\n        duration: 3000,\n        ease: \"linear\",\n        onUpdate: (values) => {\n          setAnimatedStats({ ...values.targets[0] })\n        },\n      },\n      0,\n    )\n  }, [])\n\n  const statsMap = [\n    { name: \"CPU\", key: \"cpu\", color: \"#6a5acd\" },\n    { name: \"Memory\", key: \"memory\", color: \"#4682b4\" },\n    { name: \"Network\", key: \"network\", color: \"#20b2aa\" },\n    { name: \"Disk\", key: \"disk\", color: \"#daa520\" },\n  ]\n\n  return (\n    <box\n      title=\"System Monitor\"\n      style={{\n        margin: 1,\n        padding: 1,\n        border: true,\n        marginLeft: 2,\n        marginRight: 2,\n        borderStyle: \"single\",\n        borderColor: \"#4a4a4a\",\n      }}\n    >\n      {statsMap.map((stat) => (\n        <box key={stat.key}>\n          <box flexDirection=\"row\" justifyContent=\"space-between\">\n            <text>{stat.name}</text>\n            <text attributes={TextAttributes.DIM}>{Math.round(stats[stat.key as keyof Stats])}%</text>\n          </box>\n          <box style={{ backgroundColor: \"#333333\" }}>\n            <box style={{ width: `${stats[stat.key as keyof Stats]}%`, height: 1, backgroundColor: stat.color }} />\n          </box>\n        </box>\n      ))}\n    </box>\n  )\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n### Styled Text Showcase\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nfunction App() {\n  return (\n    <>\n      <text>Simple text</text>\n      <text>\n        <strong>Bold text</strong>\n      </text>\n      <text>\n        <u>Underlined text</u>\n      </text>\n      <text>\n        <span fg=\"red\">Red text</span>\n      </text>\n      <text>\n        <span fg=\"blue\">Blue text</span>\n      </text>\n      <text>\n        <strong fg=\"red\">Bold red text</strong>\n      </text>\n      <text>\n        <strong>Bold</strong> and <span fg=\"blue\">blue</span> combined\n      </text>\n    </>\n  )\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n## Component Extension\n\nYou can create custom components by extending OpenTUIs base renderables:\n\n```tsx\nimport {\n  BoxRenderable,\n  createCliRenderer,\n  OptimizedBuffer,\n  RGBA,\n  type BoxOptions,\n  type RenderContext,\n} from \"@opentui/core\"\nimport { createRoot, extend } from \"@opentui/react\"\n\n// Create custom component class\nclass ButtonRenderable extends BoxRenderable {\n  private _label: string = \"Button\"\n\n  constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {\n    super(ctx, {\n      border: true,\n      borderStyle: \"single\",\n      minHeight: 3,\n      ...options,\n    })\n\n    if (options.label) {\n      this._label = options.label\n    }\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    super.renderSelf(buffer)\n\n    const centerX = this.x + Math.floor(this.width / 2 - this._label.length / 2)\n    const centerY = this.y + Math.floor(this.height / 2)\n\n    buffer.drawText(this._label, centerX, centerY, RGBA.fromInts(255, 255, 255, 255))\n  }\n\n  set label(value: string) {\n    this._label = value\n    this.requestRender()\n  }\n}\n\n// Add TypeScript support\ndeclare module \"@opentui/react\" {\n  interface OpenTUIComponents {\n    consoleButton: typeof ButtonRenderable\n  }\n}\n\n// Register the component\nextend({ consoleButton: ButtonRenderable })\n\n// Use in JSX\nfunction App() {\n  return (\n    <box>\n      <consoleButton label=\"Click me!\" style={{ backgroundColor: \"blue\" }} />\n      <consoleButton label=\"Another button\" style={{ backgroundColor: \"green\" }} />\n    </box>\n  )\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n## Using React DevTools\n\nOpenTUI React supports [React DevTools](https://github.com/facebook/react/tree/master/packages/react-devtools) for debugging your terminal applications. To enable DevTools integration:\n\n1. Install the optional peer dependency:\n\n```bash\nbun add --dev react-devtools-core@7\n```\n\n2. Start the standalone React DevTools:\n\n```bash\nnpx react-devtools@7\n```\n\n3. Run your app with the `DEV` environment variable:\n\n```bash\nDEV=true bun run your-app.ts\n```\n\nAfter the app starts, you should see the component tree in React DevTools. You can inspect and modify props in real-time, and changes will be reflected immediately in your terminal UI.\n\n### Process Exit with DevTools\n\nWhen DevTools is connected, the WebSocket connection may prevent your process from exiting naturally.\n"
  },
  {
    "path": "packages/react/docs/EXTEND.md",
    "content": "# OpenTUI React Component Extension\n\nThe `extend` function allows you to add custom renderable components to the OpenTUI React reconciler, similar to how `@react-three/fiber` allows extending Three.js objects.\n\n## Basic Usage\n\n### Extending Components\n\n```tsx\nimport { BoxRenderable, OptimizedBuffer, RGBA, type BoxOptions, type RenderContext } from \"@opentui/core\"\nimport { extend, render } from \"@opentui/react\"\n\nclass ConsoleButton extends BoxRenderable {\n  public label: string = \"Button\"\n\n  constructor(ctx: RenderContext, options: BoxOptions & { label: string }) {\n    super(ctx, options)\n    // Custom initialization\n\n    this.height = 3\n    this.width = 24\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    super.renderSelf(buffer)\n\n    const centerX = this.x + Math.floor(this.width / 2 - this.label.length / 2)\n    const centerY = this.y + Math.floor(this.height / 2)\n\n    buffer.drawText(this.label, centerX, centerY, RGBA.fromInts(255, 255, 255, 255))\n  }\n}\n\ndeclare module \"@opentui/react\" {\n  interface OpenTUIComponents {\n    consoleButton: typeof ConsoleButton\n  }\n}\n\n// Extend components\nextend({\n  consoleButton: ConsoleButton,\n})\n\n// Now you can use them in JSX\nfunction App() {\n  return <consoleButton label=\"Click me!\" />\n}\n```\n\n## TypeScript Support\n\nFor full TypeScript support, declare your extended components using module augmentation:\n\n```tsx\n// In your component file or declaration file\ndeclare module \"@opentui/react\" {\n  interface OpenTUIComponents {\n    consoleButton: typeof ConsoleButton\n  }\n}\n\n// Then extend and use with full type safety\nextend({\n  consoleButton: ConsoleButton,\n})\n\n// TypeScript will now know about these components\n<consoleButton label=\"Typed!\" />\n```\n\n## API Reference\n\n### `extend(components)`\n\nExtends the component catalogue with new renderable components.\n\n**Parameters:**\n\n- `components`: Object mapping component names to renderable constructors\n\n**Returns:**\n\n- `void` when passing an object of components\n\n### `getComponentCatalogue()`\n\nReturns the current extended component catalogue (used internally by reconciler).\n\n## Best Practices\n\n1. **Declare types with module augmentation**: This provides full IntelliSense and type checking\n\n2. **Call `requestRender()`**: Don't forget to call `requestRender()` when properties change to trigger re-rendering\n\n3. **Extend from appropriate base classes**: Use `BoxRenderable` for containers, `TextRenderable` for text, etc.\n\n## Limitations\n\n- Extended components must extend from OpenTUI's core renderable classes\n- Component names should be unique to avoid conflicts\n- TypeScript support requires manual module augmentation declarations\n"
  },
  {
    "path": "packages/react/examples/.plugin/index.tsx",
    "content": "import { type ReactPlugin } from \"@opentui/react\"\nimport { ExternalSidebarPanel, ExternalStatusCard } from \"./slot-components.tsx\"\n\nexport type ExternalPluginSlots = {\n  statusbar: { label: string }\n  sidebar: { section: string }\n}\n\nexport type ExternalPluginContext = {\n  appName: string\n  version: string\n}\n\nconst CAPABILITIES = [\"statusbar extension\", \"sidebar extension\", \"external jsx components\"]\n\nexport function loadExternalPlugin(): ReactPlugin<ExternalPluginSlots, ExternalPluginContext> {\n  return {\n    id: \"external-jsx-plugin\",\n    order: 20,\n    slots: {\n      statusbar(ctx, props) {\n        return <ExternalStatusCard host={ctx.appName} label={props.label} version={ctx.version} />\n      },\n      sidebar(_ctx, props) {\n        return (\n          <box flexDirection=\"column\">\n            <ExternalSidebarPanel section={props.section} capabilities={CAPABILITIES} />\n            <box marginTop={1} border borderStyle=\"single\" borderColor=\"#334155\" flexDirection=\"column\" padding={1}>\n              <text fg=\"#cbd5e1\">External plugin UI loaded from disk</text>\n              <text fg=\"#93c5fd\">No in-bundle React plugin code required.</text>\n            </box>\n          </box>\n        )\n      },\n    },\n  }\n}\n"
  },
  {
    "path": "packages/react/examples/.plugin/slot-components.tsx",
    "content": "type ExternalStatusCardProps = {\n  host: string\n  label: string\n  version: string\n}\n\nexport function ExternalStatusCard(props: ExternalStatusCardProps) {\n  return (\n    <box border borderStyle=\"single\" borderColor=\"#16a34a\" marginLeft={1} paddingLeft={1} paddingRight={1} height={3}>\n      <text fg=\"#bbf7d0\">{`${props.host} -> ${props.label} (${props.version})`}</text>\n    </box>\n  )\n}\n\ntype ExternalSidebarPanelProps = {\n  section: string\n  capabilities: string[]\n}\n\nexport function ExternalSidebarPanel(props: ExternalSidebarPanelProps) {\n  return (\n    <box border borderStyle=\"single\" borderColor=\"#06b6d4\" flexDirection=\"column\" paddingLeft={1} paddingRight={1}>\n      <text fg=\"#67e8f9\">{`External plugin section: ${props.section}`}</text>\n      {props.capabilities.map((capability) => (\n        <text key={capability} fg=\"#bae6fd\">{`- ${capability}`}</text>\n      ))}\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/react/examples/animation.tsx",
    "content": "import { createCliRenderer, TextAttributes } from \"@opentui/core\"\nimport { createRoot, useTimeline } from \"@opentui/react\"\nimport { useEffect, useState } from \"react\"\n\ntype Stats = {\n  cpu: number\n  memory: number\n  network: number\n  disk: number\n}\n\nexport const App = () => {\n  const [stats, setAnimatedStats] = useState<Stats>({\n    cpu: 0,\n    memory: 0,\n    network: 0,\n    disk: 0,\n  })\n\n  const timeline = useTimeline({\n    duration: 3000,\n    loop: false,\n  })\n\n  useEffect(() => {\n    timeline.add(\n      stats,\n      {\n        cpu: 85,\n        memory: 70,\n        network: 95,\n        disk: 60,\n        duration: 3000,\n        ease: \"linear\",\n        onUpdate: (values) => {\n          setAnimatedStats({ ...values.targets[0] })\n        },\n      },\n      0,\n    )\n  }, [])\n\n  const statsMap = [\n    { name: \"CPU\", key: \"cpu\", color: \"#6a5acd\" },\n    { name: \"Memory\", key: \"memory\", color: \"#4682b4\" },\n    { name: \"Network\", key: \"network\", color: \"#20b2aa\" },\n    { name: \"Disk\", key: \"disk\", color: \"#daa520\" },\n  ]\n\n  return (\n    <box\n      title=\"System Monitor\"\n      style={{\n        margin: 1,\n        padding: 1,\n        border: true,\n        marginLeft: 2,\n        marginRight: 2,\n        borderStyle: \"single\",\n        borderColor: \"#4a4a4a\",\n      }}\n    >\n      {statsMap.map((stat) => (\n        <box key={stat.key}>\n          <box flexDirection=\"row\" justifyContent=\"space-between\">\n            <text>{stat.name}</text>\n            <text attributes={TextAttributes.DIM}>{Math.round(stats[stat.key as keyof Stats])}%</text>\n          </box>\n          <box style={{ backgroundColor: \"#333333\" }}>\n            <box style={{ width: `${stats[stat.key as keyof Stats]}%`, height: 1, backgroundColor: stat.color }} />\n          </box>\n        </box>\n      ))}\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/ascii.tsx",
    "content": "import { createCliRenderer, type ASCIIFontName } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\nimport { useState } from \"react\"\n\nexport const App = () => {\n  const text = \"ASCII\"\n  const [font, setFont] = useState<ASCIIFontName>(\"tiny\")\n\n  return (\n    <box style={{ paddingLeft: 1, paddingRight: 1 }}>\n      <box\n        style={{\n          height: 8,\n          marginBottom: 1,\n          border: true,\n        }}\n      >\n        <select\n          focused\n          onChange={(_, option) => setFont(option?.value)}\n          showScrollIndicator\n          options={[\n            {\n              name: \"Tiny\",\n              description: \"Tiny font\",\n              value: \"tiny\",\n            },\n            {\n              name: \"Block\",\n              description: \"Block font\",\n              value: \"block\",\n            },\n            {\n              name: \"Slick\",\n              description: \"Slick font\",\n              value: \"slick\",\n            },\n            {\n              name: \"Shade\",\n              description: \"Shade font\",\n              value: \"shade\",\n            },\n          ]}\n          style={{ flexGrow: 1 }}\n        />\n      </box>\n\n      <ascii-font text={text} font={font} />\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/basic.tsx",
    "content": "import { bold, createCliRenderer, fg, italic, t, TextAttributes } from \"@opentui/core\"\nimport { createRoot, useKeyboard, useRenderer } from \"@opentui/react\"\nimport { useCallback, useState } from \"react\"\n\nexport const App = () => {\n  const renderer = useRenderer()\n  const [username, setUsername] = useState(\"\")\n  const [password, setPassword] = useState(\"\")\n  const [focused, setFocused] = useState<\"username\" | \"password\">(\"username\")\n  const [status, setStatus] = useState<\"idle\" | \"invalid\" | \"success\">(\"idle\")\n\n  useKeyboard((key) => {\n    if (key.name === \"tab\") {\n      setFocused((prevFocused) => (prevFocused === \"username\" ? \"password\" : \"username\"))\n    }\n\n    if (key.ctrl && key.name === \"k\") {\n      renderer?.toggleDebugOverlay()\n      renderer?.console.toggle()\n    }\n  })\n\n  const handleUsernameChange = useCallback((value: string) => {\n    setUsername(value)\n  }, [])\n\n  const handlePasswordChange = useCallback((value: string) => {\n    setPassword(value)\n  }, [])\n\n  const handleSubmit = useCallback(() => {\n    if (username === \"admin\" && password === \"secret\") {\n      setStatus(\"success\")\n    } else {\n      setStatus(\"invalid\")\n    }\n  }, [username, password])\n\n  return (\n    <box style={{ padding: 2, flexDirection: \"column\" }}>\n      <text\n        content=\"OpenTUI with React!\"\n        style={{\n          fg: \"#FFFF00\",\n          attributes: TextAttributes.BOLD | TextAttributes.ITALIC,\n        }}\n      />\n      <text content={t`${bold(italic(fg(\"cyan\")(`Styled Text!`)))}`} />\n\n      <box title=\"Username\" style={{ border: true, width: 40, height: 3, marginTop: 1 }}>\n        <input\n          placeholder=\"Enter your username...\"\n          onInput={handleUsernameChange}\n          onSubmit={handleSubmit}\n          focused={focused === \"username\"}\n          style={{ focusedBackgroundColor: \"#000000\" }}\n        />\n      </box>\n\n      <box title=\"Password\" style={{ border: true, width: 40, height: 3, marginTop: 1, marginBottom: 1 }}>\n        <input\n          placeholder=\"Enter your password...\"\n          onInput={handlePasswordChange}\n          onSubmit={handleSubmit}\n          focused={focused === \"password\"}\n          style={{ focusedBackgroundColor: \"#000000\" }}\n        />\n      </box>\n\n      <text\n        content={status.toUpperCase()}\n        style={{\n          fg: status === \"idle\" ? \"#AAAAAA\" : status === \"success\" ? \"green\" : \"red\",\n        }}\n      />\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/borders.tsx",
    "content": "import { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nexport const App = () => {\n  return (\n    <>\n      <box flexDirection=\"row\">\n        <box border borderStyle=\"single\">\n          <text content=\"Single\" />\n        </box>\n        <box border borderStyle=\"double\">\n          <text content=\"Double\" />\n        </box>\n        <box border borderStyle=\"rounded\">\n          <text content=\"Rounded\" />\n        </box>\n        <box border borderStyle=\"heavy\">\n          <text content=\"Heavy\" />\n        </box>\n      </box>\n    </>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/box.tsx",
    "content": "import { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nexport const App = () => {\n  return (\n    <>\n      <box flexDirection=\"column\">\n        <text attributes={1} content=\"Box Examples\" />\n        <box border>\n          <text content=\"1. Standard Box\" />\n        </box>\n        <box border title=\"Title\">\n          <text content=\"2. Box with Title\" />\n        </box>\n        <box border backgroundColor=\"blue\">\n          <text content=\"3. Box with Background Color\" />\n        </box>\n        <box border padding={1}>\n          <text content=\"4. Box with Padding\" />\n        </box>\n        <box border margin={1}>\n          <text content=\"5. Box with Margin\" />\n        </box>\n        <box border alignItems=\"center\">\n          <text content=\"6. Centered Text\" />\n        </box>\n        <box border justifyContent=\"center\" height={5}>\n          <text content=\"7. Justified Center\" />\n        </box>\n        <box border title=\"Nested Boxes\" backgroundColor=\"red\">\n          <box border backgroundColor=\"blue\">\n            <text content=\"8. Nested Box\" />\n          </box>\n        </box>\n      </box>\n    </>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/build.ts",
    "content": "#!/usr/bin/env bun\n\nimport { chmodSync, cpSync, existsSync, mkdirSync, rmSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport process from \"node:process\"\nimport { type BunPlugin } from \"bun\"\n\ntype BuildTarget = {\n  platform: \"darwin\" | \"linux\" | \"windows\"\n  arch: \"x64\" | \"arm64\"\n}\n\nconst ALL_TARGETS: BuildTarget[] = [\n  { platform: \"darwin\", arch: \"x64\" },\n  { platform: \"darwin\", arch: \"arm64\" },\n  { platform: \"linux\", arch: \"x64\" },\n  { platform: \"windows\", arch: \"x64\" },\n]\n\nfunction normalizePlatform(platform: NodeJS.Platform): BuildTarget[\"platform\"] | null {\n  if (platform === \"win32\") {\n    return \"windows\"\n  }\n\n  if (platform === \"darwin\" || platform === \"linux\") {\n    return platform\n  }\n\n  return null\n}\n\nfunction getHostTarget(): BuildTarget {\n  const platform = normalizePlatform(process.platform)\n  if (!platform) {\n    throw new Error(`Unsupported host platform: ${process.platform}`)\n  }\n\n  if (process.arch !== \"x64\" && process.arch !== \"arm64\") {\n    throw new Error(`Unsupported host architecture: ${process.arch}`)\n  }\n\n  return {\n    platform,\n    arch: process.arch,\n  }\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst packageRoot = resolve(__dirname, \"..\")\nconst distDir = join(__dirname, \"dist\")\nconst externalPluginSourceDir = join(__dirname, \".plugin\")\n\nconst packageJson = JSON.parse(await Bun.file(join(packageRoot, \"package.json\")).text()) as { version?: string }\nconst version = packageJson.version ?? \"0.0.0\"\n\nconst args = process.argv.slice(2)\nconst buildAll = args.includes(\"--all\")\nconst targets = buildAll ? ALL_TARGETS : [getHostTarget()]\n\nconst workspaceAliasPlugin: BunPlugin = {\n  name: \"workspace-alias\",\n  setup(build) {\n    build.onResolve({ filter: /^@opentui\\/react$/ }, () => {\n      return {\n        path: join(packageRoot, \"src\", \"index.ts\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/react\\/runtime-plugin-support$/ }, () => {\n      return {\n        path: join(packageRoot, \"scripts\", \"runtime-plugin-support.ts\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/react\\/jsx-runtime$/ }, () => {\n      return {\n        path: join(packageRoot, \"jsx-runtime.js\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/react\\/jsx-dev-runtime$/ }, () => {\n      return {\n        path: join(packageRoot, \"jsx-dev-runtime.js\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/core$/ }, () => {\n      return {\n        path: join(packageRoot, \"..\", \"core\", \"src\", \"index.ts\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/core\\/3d$/ }, () => {\n      return {\n        path: join(packageRoot, \"..\", \"core\", \"src\", \"3d.ts\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/core\\/testing$/ }, () => {\n      return {\n        path: join(packageRoot, \"..\", \"core\", \"src\", \"testing.ts\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/core\\/runtime-plugin$/ }, () => {\n      return {\n        path: join(packageRoot, \"..\", \"core\", \"src\", \"runtime-plugin.ts\"),\n      }\n    })\n  },\n}\n\nmkdirSync(distDir, { recursive: true })\n\nfunction syncExternalPluginFiles(targetDir: string): void {\n  if (!existsSync(externalPluginSourceDir)) {\n    return\n  }\n\n  const pluginOutDir = join(targetDir, \".plugin\")\n  rmSync(pluginOutDir, { recursive: true, force: true })\n  cpSync(externalPluginSourceDir, pluginOutDir, { recursive: true })\n}\n\nconsole.log(`Building React examples executable${buildAll ? \"s\" : \"\"}...`)\nconsole.log(`Output directory: ${distDir}`)\nconsole.log()\n\nlet successCount = 0\nlet failCount = 0\n\nfor (const { platform, arch } of targets) {\n  const exeName = platform === \"windows\" ? \"opentui-react-examples.exe\" : \"opentui-react-examples\"\n  const nullConfigPath = platform === \"windows\" ? \"NUL\" : \"/dev/null\"\n  const outfile = join(distDir, `${platform}-${arch}`, exeName)\n\n  mkdirSync(dirname(outfile), { recursive: true })\n\n  console.log(`Building for ${platform}-${arch}...`)\n\n  try {\n    const buildResult = await Bun.build({\n      entrypoints: [join(__dirname, \"index.tsx\")],\n      tsconfig: join(__dirname, \"tsconfig.json\"),\n      sourcemap: \"external\",\n      plugins: [workspaceAliasPlugin],\n      compile: {\n        target: `bun-${platform}-${arch}` as any,\n        outfile,\n        execArgv: [\n          `--user-agent=opentui-react-examples/${version}`,\n          `--config=${nullConfigPath}`,\n          `--env-file=\"\"`,\n          \"--\",\n        ],\n        windows: {},\n      },\n    })\n\n    if (buildResult.logs.length > 0) {\n      console.log(`  Build logs for ${platform}-${arch}:`)\n      buildResult.logs.forEach((log) => {\n        if (log.level === \"error\") {\n          console.error(\"  ERROR:\", log.message)\n        } else if (log.level === \"warning\") {\n          console.warn(\"  WARNING:\", log.message)\n        } else {\n          console.log(\"  INFO:\", log.message)\n        }\n      })\n    }\n\n    if (!buildResult.success) {\n      console.error(`  ❌ Build failed for ${platform}-${arch}`)\n      failCount++\n      console.log()\n      continue\n    }\n\n    if (platform !== \"windows\") {\n      chmodSync(outfile, 0o755)\n    }\n\n    syncExternalPluginFiles(dirname(outfile))\n\n    console.log(`  ✅ Successfully built: ${outfile}`)\n    successCount++\n  } catch (error) {\n    console.error(`  ❌ Build error for ${platform}-${arch}:`, error)\n    failCount++\n  }\n\n  console.log()\n}\n\nconsole.log(\"=\".repeat(60))\nconsole.log(`Build complete: ${successCount} succeeded, ${failCount} failed`)\nconsole.log(`Output directory: ${distDir}`)\n\nif (failCount > 0) {\n  process.exit(1)\n}\n"
  },
  {
    "path": "packages/react/examples/counter.tsx",
    "content": "import { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\nimport { useEffect, useState } from \"react\"\n\nexport const App = () => {\n  const [counter, setCounter] = useState(0)\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setCounter((prevCount) => prevCount + 1)\n    }, 50)\n\n    return () => clearInterval(interval)\n  }, [])\n\n  return <text content={`${counter} tests passed...`} fg=\"#00FF00\" />\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/diff.tsx",
    "content": "import { createCliRenderer, parseColor, SyntaxStyle } from \"@opentui/core\"\nimport { createRoot, useKeyboard } from \"@opentui/react\"\nimport { useState, useMemo } from \"react\"\n\ninterface DiffTheme {\n  name: string\n  backgroundColor: string\n  borderColor: string\n  addedBg: string\n  removedBg: string\n  contextBg: string\n  addedSignColor: string\n  removedSignColor: string\n  lineNumberFg: string\n  lineNumberBg: string\n  addedLineNumberBg: string\n  removedLineNumberBg: string\n  selectionBg: string\n  selectionFg: string\n  syntaxStyle: Parameters<typeof SyntaxStyle.fromStyles>[0]\n}\n\nconst themes: DiffTheme[] = [\n  {\n    name: \"GitHub Dark\",\n    backgroundColor: \"#0D1117\",\n    borderColor: \"#4ECDC4\",\n    addedBg: \"#1a4d1a\",\n    removedBg: \"#4d1a1a\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#22c55e\",\n    removedSignColor: \"#ef4444\",\n    lineNumberFg: \"#6b7280\",\n    lineNumberBg: \"#161b22\",\n    addedLineNumberBg: \"#0d3a0d\",\n    removedLineNumberBg: \"#3a0d0d\",\n    selectionBg: \"#264F78\",\n    selectionFg: \"#FFFFFF\",\n    syntaxStyle: {\n      keyword: { fg: parseColor(\"#FF7B72\"), bold: true },\n      \"keyword.import\": { fg: parseColor(\"#FF7B72\"), bold: true },\n      string: { fg: parseColor(\"#A5D6FF\") },\n      comment: { fg: parseColor(\"#8B949E\"), italic: true },\n      number: { fg: parseColor(\"#79C0FF\") },\n      boolean: { fg: parseColor(\"#79C0FF\") },\n      constant: { fg: parseColor(\"#79C0FF\") },\n      function: { fg: parseColor(\"#D2A8FF\") },\n      \"function.call\": { fg: parseColor(\"#D2A8FF\") },\n      constructor: { fg: parseColor(\"#FFA657\") },\n      type: { fg: parseColor(\"#FFA657\") },\n      operator: { fg: parseColor(\"#FF7B72\") },\n      variable: { fg: parseColor(\"#E6EDF3\") },\n      property: { fg: parseColor(\"#79C0FF\") },\n      bracket: { fg: parseColor(\"#F0F6FC\") },\n      punctuation: { fg: parseColor(\"#F0F6FC\") },\n      default: { fg: parseColor(\"#E6EDF3\") },\n    },\n  },\n  {\n    name: \"Monokai\",\n    backgroundColor: \"#272822\",\n    borderColor: \"#FD971F\",\n    addedBg: \"#2d4a2b\",\n    removedBg: \"#4a2b2b\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#A6E22E\",\n    removedSignColor: \"#F92672\",\n    lineNumberFg: \"#75715E\",\n    lineNumberBg: \"#1e1f1c\",\n    addedLineNumberBg: \"#1e3a1e\",\n    removedLineNumberBg: \"#3a1e1e\",\n    selectionBg: \"#49483E\",\n    selectionFg: \"#F8F8F2\",\n    syntaxStyle: {\n      keyword: { fg: parseColor(\"#F92672\"), bold: true },\n      \"keyword.import\": { fg: parseColor(\"#F92672\"), bold: true },\n      string: { fg: parseColor(\"#E6DB74\") },\n      comment: { fg: parseColor(\"#75715E\"), italic: true },\n      number: { fg: parseColor(\"#AE81FF\") },\n      boolean: { fg: parseColor(\"#AE81FF\") },\n      constant: { fg: parseColor(\"#AE81FF\") },\n      function: { fg: parseColor(\"#A6E22E\") },\n      \"function.call\": { fg: parseColor(\"#A6E22E\") },\n      constructor: { fg: parseColor(\"#FD971F\") },\n      type: { fg: parseColor(\"#66D9EF\") },\n      operator: { fg: parseColor(\"#F92672\") },\n      variable: { fg: parseColor(\"#F8F8F2\") },\n      property: { fg: parseColor(\"#66D9EF\") },\n      bracket: { fg: parseColor(\"#F8F8F2\") },\n      punctuation: { fg: parseColor(\"#F8F8F2\") },\n      default: { fg: parseColor(\"#F8F8F2\") },\n    },\n  },\n  {\n    name: \"Dracula\",\n    backgroundColor: \"#282A36\",\n    borderColor: \"#BD93F9\",\n    addedBg: \"#2d4737\",\n    removedBg: \"#4d2d37\",\n    contextBg: \"transparent\",\n    addedSignColor: \"#50FA7B\",\n    removedSignColor: \"#FF5555\",\n    lineNumberFg: \"#6272A4\",\n    lineNumberBg: \"#21222C\",\n    addedLineNumberBg: \"#1f3626\",\n    removedLineNumberBg: \"#3a2328\",\n    selectionBg: \"#44475A\",\n    selectionFg: \"#F8F8F2\",\n    syntaxStyle: {\n      keyword: { fg: parseColor(\"#FF79C6\"), bold: true },\n      \"keyword.import\": { fg: parseColor(\"#FF79C6\"), bold: true },\n      string: { fg: parseColor(\"#F1FA8C\") },\n      comment: { fg: parseColor(\"#6272A4\"), italic: true },\n      number: { fg: parseColor(\"#BD93F9\") },\n      boolean: { fg: parseColor(\"#BD93F9\") },\n      constant: { fg: parseColor(\"#BD93F9\") },\n      function: { fg: parseColor(\"#50FA7B\") },\n      \"function.call\": { fg: parseColor(\"#50FA7B\") },\n      constructor: { fg: parseColor(\"#FFB86C\") },\n      type: { fg: parseColor(\"#8BE9FD\") },\n      operator: { fg: parseColor(\"#FF79C6\") },\n      variable: { fg: parseColor(\"#F8F8F2\") },\n      property: { fg: parseColor(\"#8BE9FD\") },\n      bracket: { fg: parseColor(\"#F8F8F2\") },\n      punctuation: { fg: parseColor(\"#F8F8F2\") },\n      default: { fg: parseColor(\"#F8F8F2\") },\n    },\n  },\n]\n\nconst exampleDiff = `--- a/calculator.ts\n+++ b/calculator.ts\n@@ -1,13 +1,20 @@\n class Calculator {\n   add(a: number, b: number): number {\n     return a + b;\n   }\n\n-  subtract(a: number, b: number): number {\n-    return a - b;\n+  subtract(a: number, b: number, c: number = 0): number {\n+    return a - b - c;\n   }\n\n   multiply(a: number, b: number): number {\n     return a * b;\n   }\n+\n+  divide(a: number, b: number): number {\n+    if (b === 0) {\n+      throw new Error(\"Division by zero\");\n+    }\n+    return a / b;\n+  }\n }`\n\nconst HelpModal = ({ theme, visible }: { theme: DiffTheme; visible: boolean }) => {\n  if (!visible) return null\n\n  return (\n    <box\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        top: \"50%\",\n        width: 60,\n        height: 14,\n        marginLeft: -30,\n        marginTop: -7,\n        border: true,\n        borderStyle: \"double\",\n        borderColor: theme.borderColor,\n        backgroundColor: theme.backgroundColor,\n        padding: 2,\n        zIndex: 100,\n      }}\n      title=\"Keybindings\"\n      titleAlignment=\"center\"\n    >\n      <text\n        content={`View Controls:\n  V : Toggle view mode (Unified/Split)\n  L : Toggle line numbers\n  W : Toggle wrap mode (None/Word)\n\nTheme & Display:\n  T : Cycle through themes\n\nOther:\n  ? : Toggle this help screen\n  Ctrl+C : Exit`}\n        style={{ fg: \"#E6EDF3\" }}\n      />\n    </box>\n  )\n}\n\nexport function App() {\n  const [themeIndex, setThemeIndex] = useState(0)\n  const [view, setView] = useState<\"unified\" | \"split\">(\"unified\")\n  const [showLineNumbers, setShowLineNumbers] = useState(true)\n  const [wrapMode, setWrapMode] = useState<\"none\" | \"word\">(\"none\")\n  const [showHelp, setShowHelp] = useState(false)\n\n  const theme = themes[themeIndex]\n  const syntaxStyle = useMemo(() => SyntaxStyle.fromStyles(theme.syntaxStyle), [theme])\n\n  useKeyboard((key) => {\n    if (key.raw === \"?\") {\n      setShowHelp((prev) => !prev)\n      return\n    }\n\n    if (showHelp) return\n\n    if (key.name === \"v\" && !key.ctrl && !key.meta) {\n      setView((prev) => (prev === \"unified\" ? \"split\" : \"unified\"))\n    } else if (key.name === \"l\" && !key.ctrl && !key.meta) {\n      setShowLineNumbers((prev) => !prev)\n    } else if (key.name === \"w\" && !key.ctrl && !key.meta) {\n      setWrapMode((prev) => (prev === \"none\" ? \"word\" : \"none\"))\n    } else if (key.name === \"t\" && !key.ctrl && !key.meta) {\n      setThemeIndex((prev) => (prev + 1) % themes.length)\n    }\n  })\n\n  return (\n    <box\n      style={{\n        padding: 1,\n        backgroundColor: theme.backgroundColor,\n        zIndex: 10,\n      }}\n    >\n      <box\n        title={`Diff Demo - ${theme.name}`}\n        titleAlignment=\"center\"\n        style={{\n          height: 3,\n          border: true,\n          borderStyle: \"double\",\n          borderColor: theme.borderColor,\n          backgroundColor: theme.backgroundColor,\n        }}\n      >\n        <text content=\"Ctrl+C to exit | Press ? for keybindings\" style={{ fg: \"#888888\" }} />\n      </box>\n\n      <diff\n        diff={exampleDiff}\n        view={view}\n        filetype=\"typescript\"\n        syntaxStyle={syntaxStyle}\n        showLineNumbers={showLineNumbers}\n        wrapMode={wrapMode}\n        addedBg={theme.addedBg}\n        removedBg={theme.removedBg}\n        contextBg={theme.contextBg}\n        addedSignColor={theme.addedSignColor}\n        removedSignColor={theme.removedSignColor}\n        lineNumberFg={theme.lineNumberFg}\n        lineNumberBg={theme.lineNumberBg}\n        addedLineNumberBg={theme.addedLineNumberBg}\n        removedLineNumberBg={theme.removedLineNumberBg}\n        selectionBg={theme.selectionBg}\n        selectionFg={theme.selectionFg}\n        style={{\n          flexGrow: 1,\n          flexShrink: 1,\n        }}\n      />\n\n      <HelpModal theme={theme} visible={showHelp} />\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer({ exitOnCtrlC: true, targetFps: 60 })\n  renderer.setBackgroundColor(\"#0D1117\")\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/extend-example.tsx",
    "content": "import {\n  BoxRenderable,\n  createCliRenderer,\n  OptimizedBuffer,\n  RGBA,\n  type BoxOptions,\n  type RenderContext,\n} from \"@opentui/core\"\nimport { createRoot, extend } from \"@opentui/react\"\n\n// Custom renderable that extends BoxRenderable\nclass ConsoleButtonRenderable extends BoxRenderable {\n  private _label: string = \"Button\"\n\n  constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {\n    super(ctx, options)\n\n    if (options.label) {\n      this._label = options.label\n    }\n\n    // Set some default styling for buttons\n    this.borderStyle = \"single\"\n    this.padding = 2\n  }\n\n  protected renderSelf(buffer: OptimizedBuffer): void {\n    super.renderSelf(buffer)\n\n    const centerX = this.x + Math.floor(this.width / 2 - this._label.length / 2)\n    const centerY = this.y + Math.floor(this.height / 2)\n\n    buffer.drawText(this._label, centerX, centerY, RGBA.fromInts(255, 255, 255, 255))\n  }\n\n  get label(): string {\n    return this._label\n  }\n\n  set label(value: string) {\n    this._label = value\n    this.requestRender()\n  }\n}\n\n// TypeScript module augmentation for proper typing\ndeclare module \"@opentui/react\" {\n  interface OpenTUIComponents {\n    consoleButton: typeof ConsoleButtonRenderable\n  }\n}\n\n// Extend the component catalogue\nextend({ consoleButton: ConsoleButtonRenderable })\n\n// Example usage component\nexport function ExtendExample() {\n  return (\n    <consoleButton\n      label=\"Another Button\"\n      style={{\n        border: true,\n        backgroundColor: \"green\",\n      }}\n    />\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<ExtendExample />)\n}\n"
  },
  {
    "path": "packages/react/examples/external-plugin-slots-demo.tsx",
    "content": "import { existsSync } from \"node:fs\"\nimport { dirname, isAbsolute, join, resolve } from \"node:path\"\nimport process from \"node:process\"\nimport { fileURLToPath, pathToFileURL } from \"node:url\"\nimport \"@opentui/react/runtime-plugin-support\"\nimport {\n  Slot,\n  createReactSlotRegistry,\n  type ReactPlugin,\n  type SlotMode,\n  useKeyboard,\n  useRenderer,\n} from \"@opentui/react\"\nimport { useEffect, useMemo, useState } from \"react\"\n\nconst STATUSBAR_LABEL = \"host-status\"\nconst SIDEBAR_SECTION = \"external-plugins\"\nconst DEFAULT_PLUGIN_ENTRY = \".plugin/index.tsx\"\nconst EXTERNAL_PLUGIN_PATH_ENV = \"OPENTUI_REACT_EXTERNAL_PLUGIN_PATH\"\n\nconst moduleDir = dirname(fileURLToPath(import.meta.url))\n\ntype ExternalPluginSlots = {\n  statusbar: { label: string }\n  sidebar: { section: string }\n}\n\ntype ExternalPluginContext = {\n  appName: string\n  version: string\n}\n\ntype ExternalPluginModule = {\n  loadExternalPlugin(): ReactPlugin<ExternalPluginSlots, ExternalPluginContext>\n}\n\nfunction normalizePath(input: string): string {\n  if (input.startsWith(\"file://\")) {\n    return fileURLToPath(input)\n  }\n\n  if (isAbsolute(input)) {\n    return input\n  }\n\n  return resolve(process.cwd(), input)\n}\n\nfunction resolveExternalPluginCandidates(): string[] {\n  const paths = new Set<string>()\n  const envPath = process.env[EXTERNAL_PLUGIN_PATH_ENV]\n\n  if (envPath && envPath.trim().length > 0) {\n    paths.add(normalizePath(envPath.trim()))\n  }\n\n  paths.add(resolve(process.cwd(), \"packages\", \"react\", \"examples\", DEFAULT_PLUGIN_ENTRY))\n  paths.add(resolve(dirname(process.execPath), \"..\", \"..\", DEFAULT_PLUGIN_ENTRY))\n  paths.add(resolve(moduleDir, DEFAULT_PLUGIN_ENTRY))\n  paths.add(join(dirname(process.execPath), DEFAULT_PLUGIN_ENTRY))\n  paths.add(resolve(dirname(process.execPath), \"..\", DEFAULT_PLUGIN_ENTRY))\n  paths.add(resolve(process.cwd(), DEFAULT_PLUGIN_ENTRY))\n\n  return [...paths]\n}\n\nfunction resolveExternalPluginPath(): string {\n  const candidates = resolveExternalPluginCandidates()\n\n  for (const candidate of candidates) {\n    if (existsSync(candidate)) {\n      return candidate\n    }\n  }\n\n  throw new Error(`Unable to locate external plugin. Checked: ${candidates.join(\", \")}`)\n}\n\nasync function loadExternalPluginFromDisk(\n  nonce: number,\n): Promise<{ path: string; plugin: ReactPlugin<ExternalPluginSlots, ExternalPluginContext> }> {\n  const path = resolveExternalPluginPath()\n  const url = pathToFileURL(path)\n  url.searchParams.set(\"reload\", `${nonce}`)\n  url.searchParams.set(\"ts\", `${Date.now()}`)\n\n  const externalModule = (await import(url.href)) as Partial<ExternalPluginModule>\n\n  if (typeof externalModule.loadExternalPlugin !== \"function\") {\n    throw new Error(\"External plugin module does not export loadExternalPlugin()\")\n  }\n\n  return {\n    path,\n    plugin: externalModule.loadExternalPlugin(),\n  }\n}\n\nconst hostContext: ExternalPluginContext = {\n  appName: \"react-external-plugin-demo\",\n  version: \"1.0.0\",\n}\n\nfunction nextStatusbarMode(mode: SlotMode): SlotMode {\n  if (mode === \"append\") {\n    return \"replace\"\n  }\n\n  if (mode === \"replace\") {\n    return \"single_winner\"\n  }\n\n  return \"append\"\n}\n\nexport default function ExternalPluginSlotsDemo() {\n  const renderer = useRenderer()\n  const registry = useMemo(\n    () => createReactSlotRegistry<ExternalPluginSlots, ExternalPluginContext>(renderer, hostContext),\n    [renderer],\n  )\n  const AppSlot = Slot<ExternalPluginSlots, ExternalPluginContext>\n\n  const [statusbarMode, setStatusbarMode] = useState<SlotMode>(\"append\")\n  const [pluginEnabled, setPluginEnabled] = useState(true)\n  const [reloadNonce, setReloadNonce] = useState(0)\n  const [loadedPluginPath, setLoadedPluginPath] = useState(\"(not loaded yet)\")\n  const [lastPluginId, setLastPluginId] = useState(\"(none)\")\n  const [lastLoadError, setLastLoadError] = useState<string | null>(null)\n\n  useEffect(() => {\n    renderer.setBackgroundColor(\"#000000\")\n  }, [renderer])\n\n  useEffect(() => {\n    return registry.onPluginError((event) => {\n      setLastLoadError(`${event.phase}: ${event.error.message}`)\n    })\n  }, [registry])\n\n  useEffect(() => {\n    let cleanedUp = false\n    let unregisterPlugin: (() => void) | null = null\n\n    if (!pluginEnabled) {\n      setLastPluginId(\"(disabled)\")\n      setLastLoadError(null)\n      return () => {\n        cleanedUp = true\n      }\n    }\n\n    setLastLoadError(null)\n\n    void (async () => {\n      try {\n        const { path, plugin } = await loadExternalPluginFromDisk(reloadNonce)\n        if (cleanedUp) {\n          return\n        }\n\n        const unregister = registry.register(plugin)\n        unregisterPlugin = () => {\n          unregister()\n        }\n\n        setLoadedPluginPath(path)\n        setLastPluginId(plugin.id)\n        setLastLoadError(null)\n      } catch (error) {\n        const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error)\n        setLastPluginId(\"(load failed)\")\n        setLastLoadError(message)\n      }\n    })()\n\n    return () => {\n      cleanedUp = true\n      if (unregisterPlugin) {\n        unregisterPlugin()\n        unregisterPlugin = null\n      }\n    }\n  }, [registry, pluginEnabled, reloadNonce])\n\n  useKeyboard((key) => {\n    switch (key.name) {\n      case \"m\":\n        setStatusbarMode((current) => nextStatusbarMode(current))\n        return\n      case \"p\":\n        setPluginEnabled((current) => !current)\n        return\n      case \"r\":\n        setReloadNonce((current) => current + 1)\n        return\n      case \"c\":\n        if (key.ctrl) {\n          key.preventDefault()\n          renderer.destroy()\n        }\n        return\n    }\n  })\n\n  const info = [\n    \"React External Plugin Slot Demo\",\n    \"\",\n    `External plugin env override: ${EXTERNAL_PLUGIN_PATH_ENV}`,\n    `External plugin resolved path: ${loadedPluginPath}`,\n    `Last loaded plugin id: ${lastPluginId}`,\n    `Last plugin load error: ${lastLoadError ?? \"(none)\"}`,\n    \"\",\n    `Plugin enabled: ${pluginEnabled ? \"ON\" : \"OFF\"} (press p)`,\n    `Statusbar mode: ${statusbarMode.toUpperCase()} (press m to cycle)`,\n    \"Press r to reload external plugin from disk and re-register.\",\n    \"\",\n    `Statusbar slot label: ${STATUSBAR_LABEL}`,\n    `Sidebar slot section: ${SIDEBAR_SECTION}`,\n    \"\",\n    \"The plugin renders external JSX components for both slots.\",\n  ].join(\"\\n\")\n\n  return (\n    <box width=\"100%\" height=\"100%\" flexDirection=\"column\" padding={1} backgroundColor=\"#020617\">\n      <box\n        height={5}\n        width=\"100%\"\n        border\n        borderStyle=\"single\"\n        borderColor=\"#334155\"\n        alignItems=\"center\"\n        flexDirection=\"row\"\n        paddingLeft={1}\n        marginBottom={1}\n      >\n        <AppSlot registry={registry} name=\"statusbar\" label={STATUSBAR_LABEL} mode={statusbarMode}>\n          <text fg=\"#94a3b8\">Fallback statusbar content</text>\n        </AppSlot>\n      </box>\n\n      <box width=\"100%\" flexGrow={1} flexDirection=\"row\">\n        <box\n          width={44}\n          border\n          borderStyle=\"single\"\n          borderColor=\"#334155\"\n          flexDirection=\"column\"\n          padding={1}\n          marginRight={1}\n        >\n          <AppSlot registry={registry} name=\"sidebar\" section={SIDEBAR_SECTION} mode=\"replace\">\n            <text fg=\"#94a3b8\">No external sidebar plugin loaded</text>\n          </AppSlot>\n        </box>\n\n        <box flexGrow={1} border borderStyle=\"single\" borderColor=\"#334155\" flexDirection=\"column\" padding={1}>\n          <text fg=\"#e2e8f0\" content={info} />\n        </box>\n      </box>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/react/examples/flush-sync.tsx",
    "content": "import { createCliRenderer } from \"@opentui/core\"\nimport { createRoot, flushSync, useKeyboard } from \"@opentui/react\"\nimport { useRef, useState } from \"react\"\n\n/**\n * flushSync forces React to flush updates synchronously, preventing batching.\n * Press 'a' to see batched updates (1 render for 3 setState calls).\n * Press 's' to see flushSync updates (3 separate renders).\n */\nexport const App = () => {\n  const [a, setA] = useState(0)\n  const [b, setB] = useState(0)\n  const [c, setC] = useState(0)\n  const renderCount = useRef(0)\n  const [log, setLog] = useState<string[]>([])\n\n  renderCount.current++\n\n  useKeyboard((key) => {\n    if (key.name === \"q\") process.exit(0)\n\n    if (key.name === \"a\") {\n      const before = renderCount.current\n      // Without flushSync: React batches all 3 into 1 render\n      setA((x) => x + 1)\n      setB((x) => x + 1)\n      setC((x) => x + 1)\n      const after = renderCount.current\n      setLog((l) => [...l.slice(-4), `batched: renders ${before} -> ${after} (no change yet)`])\n    }\n\n    if (key.name === \"s\") {\n      const before = renderCount.current\n      // With flushSync: each update triggers a separate render\n      flushSync(() => setA((x) => x + 1))\n      flushSync(() => setB((x) => x + 1))\n      flushSync(() => setC((x) => x + 1))\n      const after = renderCount.current\n      setLog((l) => [...l.slice(-4), `flushSync: renders ${before} -> ${after} (+3 renders)`])\n    }\n  })\n\n  return (\n    <box style={{ flexDirection: \"column\", padding: 1 }}>\n      <text content=\"flushSync Demo\" style={{ fg: \"#FFFF00\", attributes: 1 }} />\n      <text content=\"'a' = batched | 's' = flushSync | 'q' = quit\" style={{ fg: \"#666666\" }} />\n      <text\n        content={`a=${a} b=${b} c=${c}  (renders: ${renderCount.current})`}\n        style={{ fg: \"#00FF00\", marginTop: 1 }}\n      />\n      <box title=\"Log\" style={{ border: true, marginTop: 1, flexDirection: \"column\", width: 55 }}>\n        {log.map((l, i) => (\n          <text key={i} content={l} style={{ fg: l.startsWith(\"flush\") ? \"#00FFFF\" : \"#FF8800\" }} />\n        ))}\n      </box>\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/index.tsx",
    "content": "import { createCliRenderer } from \"@opentui/core\"\nimport { createRoot, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from \"@opentui/react\"\nimport { createElement, useEffect, useState, type ComponentType } from \"react\"\nimport { App as AnimationDemo } from \"./animation\"\nimport { App as AsciiDemo } from \"./ascii\"\nimport { App as BasicDemo } from \"./basic\"\nimport { App as BordersDemo } from \"./borders\"\nimport { App as BoxDemo } from \"./box\"\nimport { App as CounterDemo } from \"./counter\"\nimport { App as DiffDemo } from \"./diff\"\nimport { ExtendExample } from \"./extend-example\"\nimport ExternalPluginSlotsDemo from \"./external-plugin-slots-demo\"\nimport { App as FlushSyncDemo } from \"./flush-sync\"\nimport LineNumberDemo from \"./line-number\"\nimport OpacityDemo from \"./opacity\"\nimport { App as ScrollDemo } from \"./scroll\"\nimport { App as TextDemo } from \"./text\"\n\ninterface ExampleDefinition {\n  name: string\n  description: string\n  component: ComponentType\n}\n\nconst EXAMPLES: ExampleDefinition[] = [\n  {\n    name: \"Basic Demo\",\n    description: \"Input form, focus management, and styled text\",\n    component: BasicDemo,\n  },\n  {\n    name: \"Counter Demo\",\n    description: \"State updates and interval-driven re-renders\",\n    component: CounterDemo,\n  },\n  {\n    name: \"Animation Demo\",\n    description: \"Timeline-driven system monitor animation\",\n    component: AnimationDemo,\n  },\n  {\n    name: \"ASCII Font Demo\",\n    description: \"Switch among multiple ASCII font renderers\",\n    component: AsciiDemo,\n  },\n  {\n    name: \"Text Demo\",\n    description: \"Styled text, colors, links, and nested formatting\",\n    component: TextDemo,\n  },\n  {\n    name: \"Box Demo\",\n    description: \"Box layout, spacing, nesting, and alignment\",\n    component: BoxDemo,\n  },\n  {\n    name: \"Borders Demo\",\n    description: \"Single, double, rounded, and heavy borders\",\n    component: BordersDemo,\n  },\n  {\n    name: \"Scroll Demo\",\n    description: \"Scrollable content with custom scrollbar styling\",\n    component: ScrollDemo,\n  },\n  {\n    name: \"Line Number Demo\",\n    description: \"Code with line numbers, signs, and diagnostics\",\n    component: LineNumberDemo,\n  },\n  {\n    name: \"Diff Demo\",\n    description: \"Unified and split diff view with themes\",\n    component: DiffDemo,\n  },\n  {\n    name: \"Opacity Demo\",\n    description: \"Layered opacity blending and animation\",\n    component: OpacityDemo,\n  },\n  {\n    name: \"Flush Sync Demo\",\n    description: \"Compare batched updates vs synchronous flushes\",\n    component: FlushSyncDemo,\n  },\n  {\n    name: \"Extend Demo\",\n    description: \"Custom renderable registration through extend\",\n    component: ExtendExample,\n  },\n  {\n    name: \"External Plugin Slots Demo\",\n    description: \"Loads .plugin/index.tsx and renders external React slot components\",\n    component: ExternalPluginSlotsDemo,\n  },\n]\n\nexport const ExamplesIndex = () => {\n  const renderer = useRenderer()\n  const terminalDimensions = useTerminalDimensions()\n  const [selected, setSelected] = useState(-1)\n\n  useEffect(() => {\n    renderer.useConsole = true\n  }, [renderer])\n\n  useKeyboard((key) => {\n    switch (key.name) {\n      case \"escape\":\n        setSelected(-1)\n        break\n      case \"`\":\n        renderer.console.toggle()\n        break\n      case \"t\":\n        renderer.toggleDebugOverlay()\n        break\n      case \"g\":\n        if (key.ctrl) {\n          renderer.dumpHitGrid()\n        }\n        break\n    }\n\n    if (key.ctrl && key.name === \"c\") {\n      key.preventDefault()\n      renderer.destroy()\n    }\n  })\n\n  if (selected !== -1) {\n    const selectedExample = EXAMPLES[selected]\n    return selectedExample ? createElement(selectedExample.component) : null\n  }\n\n  return (\n    <box style={{ height: terminalDimensions.height, backgroundColor: \"#001122\", padding: 1 }}>\n      <box alignItems=\"center\">\n        <ascii-font style={{ font: \"tiny\" }} text=\"OPENTUI REACT EXAMPLES\" />\n      </box>\n      <box\n        title=\"Examples\"\n        style={{\n          border: true,\n          flexGrow: 1,\n          marginTop: 1,\n          borderStyle: \"single\",\n          titleAlignment: \"center\",\n          focusedBorderColor: \"#00AAFF\",\n        }}\n      >\n        <select\n          focused\n          onSelect={(index) => {\n            setSelected(index)\n          }}\n          options={EXAMPLES.map((example, index) => ({\n            name: example.name,\n            description: example.description,\n            value: index,\n          }))}\n          style={{\n            height: \"100%\",\n            backgroundColor: \"transparent\",\n            focusedBackgroundColor: \"transparent\",\n            selectedBackgroundColor: \"#334455\",\n            selectedTextColor: \"#FFFF00\",\n            descriptionColor: \"#888888\",\n          }}\n          showScrollIndicator\n          wrapSelection\n          fastScrollStep={5}\n        />\n      </box>\n      <TimeToFirstDraw />\n      <text style={{ fg: \"#AAAAAA\", marginTop: 1, marginLeft: 1, marginRight: 1 }}>\n        Use up/down or j/k to navigate, Shift+up/down or Shift+j/k for fast scroll, Enter to run, Escape to return, ` to\n        toggle console, ctrl+c to quit\n      </text>\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<ExamplesIndex />)\n}\n\nexport default ExamplesIndex\n"
  },
  {
    "path": "packages/react/examples/line-number.tsx",
    "content": "import { createCliRenderer, LineNumberRenderable, RGBA, SyntaxStyle } from \"@opentui/core\"\nimport { createRoot, useKeyboard } from \"@opentui/react\"\nimport { useEffect, useRef, useState } from \"react\"\n\nexport default function App() {\n  const [showLineNumbers, setShowLineNumbers] = useState(true)\n  const [showDiffHighlights, setShowDiffHighlights] = useState(false)\n  const [showDiagnostics, setShowDiagnostics] = useState(false)\n\n  const codeContent = `function fibonacci(n: number): number {\n  if (n <= 1) return n\n  return fibonacci(n - 1) + fibonacci(n - 2)\n}\n\nconst results = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n  .map(fibonacci)\n\nconsole.log('Fibonacci sequence:', results)\n\n// Calculate the sum\nconst sum = results.reduce((acc, val) => acc + val, 0)\nconsole.log('Sum:', sum)\n\n// Find even numbers\nconst evens = results.filter(n => n % 2 === 0)\nconsole.log('Even numbers:', evens)`\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    keyword: { fg: RGBA.fromHex(\"#C792EA\") },\n    function: { fg: RGBA.fromHex(\"#82AAFF\") },\n    string: { fg: RGBA.fromHex(\"#C3E88D\") },\n    number: { fg: RGBA.fromHex(\"#F78C6C\") },\n    comment: { fg: RGBA.fromHex(\"#546E7A\") },\n    type: { fg: RGBA.fromHex(\"#FFCB6B\") },\n    operator: { fg: RGBA.fromHex(\"#89DDFF\") },\n    variable: { fg: RGBA.fromHex(\"#EEFFFF\") },\n    default: { fg: RGBA.fromHex(\"#A6ACCD\") },\n  })\n\n  let lineNumberRef = useRef<LineNumberRenderable>(null)\n\n  useEffect(() => {\n    // Set up diff highlights\n    if (showDiffHighlights) {\n      lineNumberRef.current?.setLineColor(1, \"#1a4d1a\") // Line 2: added\n      lineNumberRef.current?.setLineSign(1, { after: \" +\", afterColor: \"#22c55e\" })\n\n      lineNumberRef.current?.setLineColor(5, \"#4d1a1a\") // Line 6: removed\n      lineNumberRef.current?.setLineSign(5, { after: \" -\", afterColor: \"#ef4444\" })\n\n      lineNumberRef.current?.setLineColor(10, \"#1a4d1a\") // Line 11: added\n      lineNumberRef.current?.setLineSign(10, { after: \" +\", afterColor: \"#22c55e\" })\n    }\n\n    // Set up diagnostics\n    if (showDiagnostics) {\n      lineNumberRef.current?.setLineSign(0, { before: \"⚠️\", beforeColor: \"#f59e0b\" })\n      lineNumberRef.current?.setLineSign(7, { before: \"💡\", beforeColor: \"#3b82f6\" })\n      lineNumberRef.current?.setLineSign(13, { before: \"❌\", beforeColor: \"#ef4444\" })\n    }\n  }, [showDiffHighlights, showDiagnostics])\n\n  useKeyboard((key) => {\n    if (key.name === \"l\" && !key.ctrl && !key.meta) {\n      toggleLineNumbers()\n    } else if (key.name === \"h\" && !key.ctrl && !key.meta) {\n      toggleDiffHighlights()\n    } else if (key.name === \"d\" && !key.ctrl && !key.meta) {\n      toggleDiagnostics()\n    }\n  })\n\n  const toggleLineNumbers = () => {\n    setShowLineNumbers(!showLineNumbers)\n  }\n\n  const toggleDiffHighlights = () => {\n    const newValue = !showDiffHighlights\n    setShowDiffHighlights(newValue)\n\n    if (newValue) {\n      lineNumberRef.current?.setLineColor(1, \"#1a4d1a\")\n      lineNumberRef.current?.setLineSign(1, { after: \" +\", afterColor: \"#22c55e\" })\n      lineNumberRef.current?.setLineColor(5, \"#4d1a1a\")\n      lineNumberRef.current?.setLineSign(5, { after: \" -\", afterColor: \"#ef4444\" })\n      lineNumberRef.current?.setLineColor(10, \"#1a4d1a\")\n      lineNumberRef.current?.setLineSign(10, { after: \" +\", afterColor: \"#22c55e\" })\n    } else {\n      lineNumberRef.current?.clearAllLineColors()\n      // Clear only after signs\n      if (!showDiagnostics) {\n        lineNumberRef.current?.clearAllLineSigns()\n      } else {\n        lineNumberRef.current?.setLineSign(1, {})\n        lineNumberRef.current?.setLineSign(5, {})\n        lineNumberRef.current?.setLineSign(10, {})\n      }\n    }\n  }\n\n  const toggleDiagnostics = () => {\n    const newValue = !showDiagnostics\n    setShowDiagnostics(newValue)\n\n    if (newValue) {\n      lineNumberRef.current?.setLineSign(0, { before: \"⚠️\", beforeColor: \"#f59e0b\" })\n      lineNumberRef.current?.setLineSign(7, { before: \"💡\", beforeColor: \"#3b82f6\" })\n      lineNumberRef.current?.setLineSign(13, { before: \"❌\", beforeColor: \"#ef4444\" })\n    } else {\n      // Clear only before signs\n      if (!showDiffHighlights) {\n        lineNumberRef.current?.clearAllLineSigns()\n      } else {\n        lineNumberRef.current?.setLineSign(0, {})\n        lineNumberRef.current?.setLineSign(7, {})\n        lineNumberRef.current?.setLineSign(13, {})\n      }\n    }\n  }\n\n  return (\n    <box flexDirection=\"column\" width=\"100%\" height=\"100%\" gap={1}>\n      <box flexDirection=\"column\" backgroundColor=\"#0D1117\" padding={1} flexShrink={0} border borderColor=\"#30363D\">\n        <text fg=\"#4ECDC4\">Line Numbers Demo</text>\n        <text fg=\"#888888\">Keybindings:</text>\n        <text fg=\"#AAAAAA\">L - Toggle line numbers ({showLineNumbers ? \"ON\" : \"OFF\"})</text>\n        <text fg=\"#AAAAAA\">H - Toggle diff highlights ({showDiffHighlights ? \"ON\" : \"OFF\"})</text>\n        <text fg=\"#AAAAAA\">D - Toggle diagnostics ({showDiagnostics ? \"ON\" : \"OFF\"})</text>\n      </box>\n\n      <box flexGrow={1} border borderStyle=\"single\" borderColor=\"#4ECDC4\" backgroundColor=\"#0D1117\">\n        <line-number\n          ref={lineNumberRef}\n          fg=\"#6b7280\"\n          bg=\"#161b22\"\n          minWidth={3}\n          paddingRight={1}\n          showLineNumbers={showLineNumbers}\n          width=\"100%\"\n          height=\"100%\"\n        >\n          <code\n            content={codeContent}\n            filetype=\"typescript\"\n            syntaxStyle={syntaxStyle}\n            selectable\n            selectionBg=\"#264F78\"\n            selectionFg=\"#FFFFFF\"\n            width=\"100%\"\n            height=\"100%\"\n          />\n        </line-number>\n      </box>\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/opacity.tsx",
    "content": "import { createCliRenderer } from \"@opentui/core\"\nimport { createRoot, useKeyboard } from \"@opentui/react\"\nimport { useState, useEffect } from \"react\"\n\nexport default function App() {\n  const [animating, setAnimating] = useState(false)\n  const [opacities, setOpacities] = useState([1.0, 0.8, 0.5, 0.3])\n  const [phase, setPhase] = useState(0)\n\n  useKeyboard((key) => {\n    if (key.name === \"a\" && !key.ctrl && !key.meta) {\n      setAnimating(!animating)\n    } else if (key.name === \"1\") {\n      setOpacities((prev) => [prev[0] === 1.0 ? 0.3 : 1.0, prev[1], prev[2], prev[3]])\n    } else if (key.name === \"2\") {\n      setOpacities((prev) => [prev[0], prev[1] === 1.0 ? 0.3 : 1.0, prev[2], prev[3]])\n    } else if (key.name === \"3\") {\n      setOpacities((prev) => [prev[0], prev[1], prev[2] === 1.0 ? 0.3 : 1.0, prev[3]])\n    } else if (key.name === \"4\") {\n      setOpacities((prev) => [prev[0], prev[1], prev[2], prev[3] === 1.0 ? 0.3 : 1.0])\n    }\n  })\n\n  useEffect(() => {\n    if (!animating) return\n\n    const interval = setInterval(() => {\n      setPhase((p) => p + 0.05)\n    }, 50)\n\n    return () => clearInterval(interval)\n  }, [animating])\n\n  useEffect(() => {\n    if (animating) {\n      setOpacities([\n        0.3 + 0.7 * Math.abs(Math.sin(phase)),\n        0.3 + 0.7 * Math.abs(Math.sin(phase + 0.5)),\n        0.3 + 0.7 * Math.abs(Math.sin(phase + 1.0)),\n        0.3 + 0.7 * Math.abs(Math.sin(phase + 1.5)),\n      ])\n    }\n  }, [phase, animating])\n\n  const colors = [\"#e94560\", \"#0f3460\", \"#533483\", \"#16a085\"]\n\n  return (\n    <box flexDirection=\"column\" width=\"100%\" height=\"100%\">\n      {/* Header */}\n      <box height={3} backgroundColor=\"#16213e\" border borderStyle=\"single\" alignItems=\"center\" justifyContent=\"center\">\n        <text fg=\"#e94560\">\n          OPACITY DEMO | 1-4: Toggle opacity | A: {animating ? \"Stop\" : \"Animate\"} | Ctrl+C: Exit\n        </text>\n      </box>\n\n      {/* Main content */}\n      <box flexGrow={1} flexDirection=\"row\" padding={2}>\n        {/* Overlapping boxes */}\n        <box flexGrow={1} position=\"relative\">\n          {[0, 1, 2, 3].map((i) => (\n            <box\n              key={i}\n              position=\"absolute\"\n              left={10 + i * 8}\n              top={2 + i * 2}\n              width={20}\n              height={8}\n              backgroundColor={colors[i]}\n              opacity={opacities[i]}\n              border\n              borderStyle=\"double\"\n              borderColor=\"#ffffff\"\n              alignItems=\"center\"\n              justifyContent=\"center\"\n              flexDirection=\"column\"\n            >\n              <text fg=\"#ffffff\">Box {i + 1}</text>\n              <text fg=\"#ffffff\">Opacity: {opacities[i].toFixed(1)}</text>\n            </box>\n          ))}\n        </box>\n\n        {/* Nested opacity demo */}\n        <box\n          position=\"absolute\"\n          right={5}\n          top={5}\n          width={35}\n          height={10}\n          backgroundColor=\"#e94560\"\n          opacity={0.7}\n          border\n          borderStyle=\"single\"\n          padding={1}\n          flexDirection=\"column\"\n        >\n          <text fg=\"#ffffff\">Parent: 0.7 opacity</text>\n          <box\n            backgroundColor=\"#0f3460\"\n            opacity={0.5}\n            border\n            flexGrow={1}\n            alignItems=\"center\"\n            justifyContent=\"center\"\n            flexDirection=\"column\"\n          >\n            <text fg=\"#ffffff\">Child: 0.5 opacity</text>\n            <text fg=\"#ffcc00\">Effective: 0.35</text>\n          </box>\n        </box>\n      </box>\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/plugin-slots-errors.tsx",
    "content": "import { createCliRenderer, type PluginErrorEvent } from \"@opentui/core\"\nimport {\n  Slot,\n  createReactSlotRegistry,\n  createRoot,\n  type ReactPlugin,\n  type SlotMode,\n  useKeyboard,\n  useRenderer,\n} from \"@opentui/react\"\nimport { useEffect, useMemo, useState } from \"react\"\n\ntype DemoSlots = {\n  statusbar: { label: string }\n  sidebar: { section: string }\n}\n\nconst DEMO_STATUS_LABEL = \"host-status\"\nconst DEMO_SIDEBAR_SECTION = \"plugins\"\n\nconst hostContext = {\n  appName: \"react-plugin-slots-demo\",\n  version: \"1.0.0\",\n}\n\nfunction nextStatusbarMode(mode: SlotMode): SlotMode {\n  if (mode === \"append\") {\n    return \"replace\"\n  }\n\n  if (mode === \"replace\") {\n    return \"single_winner\"\n  }\n\n  return \"append\"\n}\n\nfunction formatPluginError(event: PluginErrorEvent): string {\n  return `${event.pluginId} [${event.phase}/${event.source}] @ ${event.slot ?? \"<none>\"}: ${event.error.message}`\n}\n\nfunction CrashNode({ pluginId }: { pluginId: string }) {\n  throw new Error(`Forced subtree crash in ${pluginId}`)\n  return null\n}\n\nfunction ClockStatusText({ label }: { label: string }) {\n  const [time, setTime] = useState(() => new Date().toLocaleTimeString())\n\n  useEffect(() => {\n    const timer = setInterval(() => {\n      setTime(new Date().toLocaleTimeString())\n    }, 1000)\n\n    return () => {\n      clearInterval(timer)\n    }\n  }, [])\n\n  return <text fg=\"#93c5fd\">{`Clock plugin -> ${label} (${time})`}</text>\n}\n\nfunction createClockPlugin(crash: boolean): ReactPlugin<DemoSlots, typeof hostContext> {\n  return {\n    id: \"clock-plugin\",\n    order: 0,\n    slots: {\n      statusbar(_ctx, props) {\n        if (crash) {\n          return <CrashNode pluginId=\"clock-plugin\" />\n        }\n\n        return (\n          <box\n            border\n            borderStyle=\"single\"\n            borderColor=\"#2563eb\"\n            marginLeft={1}\n            paddingLeft={1}\n            paddingRight={1}\n            height={3}\n          >\n            <ClockStatusText label={props.label} />\n          </box>\n        )\n      },\n      sidebar(_ctx, props) {\n        return (\n          <box\n            border\n            borderStyle=\"single\"\n            borderColor=\"#0ea5e9\"\n            flexDirection=\"column\"\n            paddingLeft={1}\n            paddingRight={1}\n          >\n            <text fg=\"#38bdf8\">{`Clock Sidebar (${props.section})`}</text>\n            <text fg=\"#e2e8f0\">Healthy</text>\n          </box>\n        )\n      },\n    },\n  }\n}\n\nfunction createActivityPlugin(crash: boolean): ReactPlugin<DemoSlots, typeof hostContext> {\n  return {\n    id: \"activity-plugin\",\n    order: 10,\n    slots: {\n      statusbar() {\n        if (crash) {\n          throw new Error(\"Forced activity render failure\")\n        }\n\n        return (\n          <box\n            border\n            borderStyle=\"single\"\n            borderColor=\"#16a34a\"\n            marginLeft={1}\n            paddingLeft={1}\n            paddingRight={1}\n            height={3}\n          >\n            <text fg=\"#86efac\">Activity plugin healthy</text>\n          </box>\n        )\n      },\n    },\n  }\n}\n\nfunction App() {\n  const renderer = useRenderer()\n  const registry = useMemo(\n    () => createReactSlotRegistry<DemoSlots, typeof hostContext>(renderer, hostContext),\n    [renderer],\n  )\n\n  const [statusbarMode, setStatusbarMode] = useState<SlotMode>(\"append\")\n  const [clockEnabled, setClockEnabled] = useState(true)\n  const [activityEnabled, setActivityEnabled] = useState(true)\n  const [clockCrashEnabled, setClockCrashEnabled] = useState(false)\n  const [activityCrashEnabled, setActivityCrashEnabled] = useState(false)\n  const [showPlaceholder, setShowPlaceholder] = useState(true)\n  const [refreshNonce, setRefreshNonce] = useState(0)\n  const [errorLines, setErrorLines] = useState<string[]>([])\n\n  const AppSlot = Slot<DemoSlots, typeof hostContext>\n\n  const pluginFailurePlaceholder = (failure: PluginErrorEvent) => {\n    return (\n      <box border borderStyle=\"single\" borderColor=\"#fb7185\" marginLeft={1} paddingLeft={1} paddingRight={1}>\n        <text fg=\"#fecaca\">{`Plugin error: ${failure.pluginId}`}</text>\n        <text fg=\"#fca5a5\">{`${failure.phase}/${failure.source} @ ${failure.slot ?? \"unknown\"}`}</text>\n      </box>\n    )\n  }\n\n  useEffect(() => {\n    renderer.setBackgroundColor(\"#000000\")\n  }, [renderer])\n\n  useEffect(() => {\n    return registry.onPluginError((event) => {\n      setErrorLines((current) => [formatPluginError(event), ...current].slice(0, 6))\n    })\n  }, [registry])\n\n  useEffect(() => {\n    const unregisterCallbacks: Array<() => void> = []\n\n    if (clockEnabled) {\n      unregisterCallbacks.push(registry.register(createClockPlugin(clockCrashEnabled)))\n    }\n\n    if (activityEnabled) {\n      unregisterCallbacks.push(registry.register(createActivityPlugin(activityCrashEnabled)))\n    }\n\n    return () => {\n      for (const unregister of unregisterCallbacks.reverse()) {\n        unregister()\n      }\n    }\n  }, [registry, clockEnabled, activityEnabled, clockCrashEnabled, activityCrashEnabled, refreshNonce])\n\n  useKeyboard((key) => {\n    switch (key.name) {\n      case \"1\":\n        setClockEnabled((current) => !current)\n        return\n      case \"2\":\n        setActivityEnabled((current) => !current)\n        return\n      case \"m\":\n        setStatusbarMode((current) => nextStatusbarMode(current))\n        return\n      case \"e\":\n        setClockCrashEnabled((current) => !current)\n        return\n      case \"d\":\n        setActivityCrashEnabled((current) => !current)\n        return\n      case \"p\":\n        setShowPlaceholder((current) => !current)\n        return\n      case \"r\":\n        setRefreshNonce((current) => current + 1)\n        return\n      case \"x\":\n        setClockCrashEnabled(false)\n        setActivityCrashEnabled(false)\n        setErrorLines([])\n        registry.clearPluginErrors()\n        setRefreshNonce((current) => current + 1)\n        return\n      case \"c\":\n        if (key.ctrl) {\n          key.preventDefault()\n          renderer.destroy()\n        }\n        return\n    }\n  })\n\n  const info = [\n    \"React Plugin Slot Demo\",\n    \"\",\n    `Statusbar mode: ${statusbarMode.toUpperCase()} (press m to cycle)`,\n    `Clock plugin: ${clockEnabled ? \"ON\" : \"OFF\"} (press 1)`,\n    `Activity plugin: ${activityEnabled ? \"ON\" : \"OFF\"} (press 2)`,\n    `Clock subtree crash: ${clockCrashEnabled ? \"ON\" : \"OFF\"} (press e)`,\n    `Activity throw: ${activityCrashEnabled ? \"ON\" : \"OFF\"} (press d)`,\n    `Show placeholders: ${showPlaceholder ? \"YES\" : \"NO\"} (press p)`,\n    \"\",\n    `Statusbar slot label: ${DEMO_STATUS_LABEL}`,\n    `Sidebar slot section: ${DEMO_SIDEBAR_SECTION}`,\n    \"\",\n    \"Press r to re-register active plugins.\",\n    \"Press x to reset errors and clear history.\",\n    \"\",\n    \"Recent plugin errors:\",\n    ...(errorLines.length > 0 ? errorLines : [\"(none)\"]),\n  ].join(\"\\n\")\n\n  return (\n    <box width=\"100%\" height=\"100%\" flexDirection=\"column\" padding={1} backgroundColor=\"#020617\">\n      <box\n        height={5}\n        width=\"100%\"\n        border\n        borderStyle=\"single\"\n        borderColor=\"#334155\"\n        alignItems=\"center\"\n        flexDirection=\"row\"\n        paddingLeft={1}\n        marginBottom={1}\n      >\n        <AppSlot\n          registry={registry}\n          name=\"statusbar\"\n          label={DEMO_STATUS_LABEL}\n          mode={statusbarMode}\n          pluginFailurePlaceholder={showPlaceholder ? pluginFailurePlaceholder : undefined}\n        >\n          <text fg=\"#94a3b8\">Fallback statusbar content</text>\n        </AppSlot>\n      </box>\n\n      <box width=\"100%\" flexGrow={1} flexDirection=\"row\">\n        <box\n          width={36}\n          border\n          borderStyle=\"single\"\n          borderColor=\"#334155\"\n          flexDirection=\"column\"\n          padding={1}\n          marginRight={1}\n        >\n          <AppSlot\n            registry={registry}\n            name=\"sidebar\"\n            section={DEMO_SIDEBAR_SECTION}\n            mode=\"replace\"\n            pluginFailurePlaceholder={showPlaceholder ? pluginFailurePlaceholder : undefined}\n          >\n            <text fg=\"#94a3b8\">No sidebar plugin active</text>\n          </AppSlot>\n        </box>\n\n        <box flexGrow={1} border borderStyle=\"single\" borderColor=\"#334155\" flexDirection=\"column\" padding={1}>\n          <text fg=\"#e2e8f0\" content={info} />\n        </box>\n      </box>\n    </box>\n  )\n}\n\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: true,\n  targetFps: 30,\n})\n\ncreateRoot(renderer).render(<App />)\n"
  },
  {
    "path": "packages/react/examples/scroll.tsx",
    "content": "import { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nconst LOREM = [\n  \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\n  \"Proin dictum rutrum mi, ac egestas elit dictum ac.\",\n  \"Aliquam erat volutpat. Nullam in nisi vitae turpis consequat ultrices.\",\n  \"Sed posuere pretium metus, a posuere est consequat nec.\",\n  \"Curabitur nec quam sed augue congue vestibulum.\",\n  \"Suspendisse tincidunt, augue at rhoncus cursus, urna felis malesuada leo.\",\n  \"Nam molestie euismod faucibus. Quisque id odio in pede ornare luctus.\",\n  \"Integer consequat, quam at congue cursus, magna eros pretium enim.\",\n  \"Vivamus cursus, ex eu tincidunt cursus, libero massa dictum arcu.\",\n  \"Morbi auctor magna a ultricies consequat.\",\n]\n\n// Helper function that creates a random selection of `num` lines from LOREM\nconst getRandomLoremLines = (num: number) => {\n  const lines: string[] = []\n  for (let i = 0; i < num; i++) {\n    const idx = Math.floor(Math.random() * LOREM.length)\n    lines.push(LOREM[idx])\n  }\n  return lines\n}\n\nexport const App = () => {\n  // Let's use 16 boxes for variety and clarity\n  const boxColors = [\n    \"#2e3440\",\n    \"#bf616a\",\n    \"#a3be8c\",\n    \"#ebcb8b\",\n    \"#81a1c1\",\n    \"#b48ead\",\n    \"#88c0d0\",\n    \"#5e81ac\",\n    \"#d08770\",\n    \"#e5e9f0\",\n    \"#414868\",\n    \"#7aa2f7\",\n    \"#292e42\",\n    \"#373d52\",\n    \"#24283b\",\n    \"#cdd6f4\",\n  ]\n  return (\n    <scrollbox\n      style={{\n        rootOptions: {\n          backgroundColor: \"#24283b\",\n        },\n        wrapperOptions: {\n          backgroundColor: \"#1f2335\",\n        },\n        viewportOptions: {\n          backgroundColor: \"#1a1b26\",\n        },\n        contentOptions: {\n          backgroundColor: \"#16161e\",\n        },\n        scrollbarOptions: {\n          showArrows: true,\n          trackOptions: {\n            foregroundColor: \"#7aa2f7\",\n            backgroundColor: \"#414868\",\n          },\n        },\n      }}\n      focused\n    >\n      {Array.from({ length: 16 }).map((_, i) => {\n        const numLines = 2 + Math.floor(Math.random() * 4) // 2 to 5 lines per box\n        const lines = getRandomLoremLines(numLines)\n        const bg = boxColors[i % boxColors.length]\n        const borderColor = boxColors[(i + 1) % boxColors.length]\n        return (\n          <box\n            key={i}\n            style={{\n              width: \"100%\",\n              padding: 2,\n              marginBottom: 2,\n              backgroundColor: bg,\n            }}\n          >\n            <text\n              style={{\n                marginBottom: 1,\n              }}\n              content={`Box ${i + 1}`}\n            />\n            {lines.map((txt, j) => (\n              <text\n                key={j}\n                style={{\n                  marginBottom: 0,\n                }}\n                content={txt}\n              />\n            ))}\n          </box>\n        )\n      })}\n    </scrollbox>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/text.tsx",
    "content": "import { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nexport function App() {\n  return (\n    <text>\n      Color Showcase{\"\\n\"}\n      <span fg=\"red\">Red text</span> <span fg=\"green\">Green text</span> <span fg=\"blue\">Blue text</span>{\" \"}\n      <span fg=\"yellow\">Yellow text</span>\n      {\"\\n\"}\n      <span fg=\"magenta\">Magenta</span> <span fg=\"cyan\">Cyan</span> <span fg=\"white\">White</span>\n      {\"\\n\"}\n      Background colors:{\"\\n\"}\n      <span fg=\"red\" bg=\"yellow\">\n        Red on Yellow\n      </span>{\" \"}\n      <span fg=\"blue\" bg=\"green\">\n        Blue on Green\n      </span>{\" \"}\n      <span fg=\"white\" bg=\"magenta\">\n        White on Magenta\n      </span>\n      {\"\\n\"}\n      <span fg=\"yellow\" bg=\"blue\">\n        Yellow on Blue\n      </span>{\" \"}\n      <span fg=\"green\" bg=\"red\">\n        Green on Red\n      </span>{\" \"}\n      <span fg=\"cyan\" bg=\"black\">\n        Cyan on Black\n      </span>\n      {\"\\n\"}\n      Hyperlinks:{\"\\n\"}\n      <u>\n        <a href=\"https://opentui.com\" fg=\"blue\">\n          opentui.com\n        </a>\n      </u>{\" \"}\n      - Click if your terminal supports OSC 8{\"\\n\"}\n      Bright colors:{\"\\n\"}\n      <span fg=\"brightRed\">Bright Red</span> <span fg=\"brightGreen\">Bright Green</span>{\" \"}\n      <span fg=\"brightBlue\">Bright Blue</span>\n      {\"\\n\"}\n      <span fg=\"brightYellow\">Bright Yellow</span> <span fg=\"brightMagenta\">Bright Magenta</span>{\" \"}\n      <span fg=\"brightCyan\">Bright Cyan</span>\n      {\"\\n\"}\n      Text Formatting:{\"\\n\"}\n      <strong>Strong/Bold text</strong> - <em>Emphasized/Italic text</em> - <u>Underlined text</u>\n      {\"\\n\"}\n      <b fg=\"yellow\">Bold yellow</b> - <i fg=\"green\">Italic green</i> - <u fg=\"magenta\">Underlined magenta</u>\n      {\"\\n\"}\n      Complex nesting:{\"\\n\"}\n      <strong fg=\"red\">\n        Bold red with <em fg=\"blue\">italic blue nested</em> inside\n      </strong>\n      {\"\\n\"}\n      <em>\n        Italic with <u fg=\"cyan\">underlined cyan</u> and <strong fg=\"yellow\">bold yellow</strong>\n      </em>\n      {\"\\n\"}\n      <span bg=\"black\" fg=\"white\">\n        Background with <strong fg=\"brightRed\">bold bright red</strong> and{\" \"}\n        <u fg=\"brightGreen\">underlined bright green</u>\n      </span>\n      {\"\\n\"}\n    </text>\n  )\n}\n\nif (import.meta.main) {\n  const renderer = await createCliRenderer()\n  createRoot(renderer).render(<App />)\n}\n"
  },
  {
    "path": "packages/react/examples/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // Enable latest features\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@opentui/react\",\n    \"allowJs\": true,\n\n    // Bundler mode\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n\n    // Best practices\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    // Some stricter flags (disabled by default)\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false\n  }\n}\n"
  },
  {
    "path": "packages/react/jsx-dev-runtime.d.ts",
    "content": "export { Fragment, jsxDEV } from \"react/jsx-dev-runtime\"\nexport type * from \"./jsx-namespace.d.ts\"\n"
  },
  {
    "path": "packages/react/jsx-dev-runtime.js",
    "content": "export { Fragment, jsxDEV } from \"react/jsx-dev-runtime\"\n"
  },
  {
    "path": "packages/react/jsx-namespace.d.ts",
    "content": "import type * as React from \"react\"\nimport type {\n  AsciiFontProps,\n  BoxProps,\n  CodeProps,\n  DiffProps,\n  ExtendedIntrinsicElements,\n  InputProps,\n  LineBreakProps,\n  LineNumberProps,\n  LinkProps,\n  MarkdownProps,\n  OpenTUIComponents,\n  ScrollBoxProps,\n  SelectProps,\n  SpanProps,\n  TabSelectProps,\n  TextareaProps,\n  TextProps,\n} from \"./src/types/components.js\"\n\nexport namespace JSX {\n  type Element = React.ReactNode\n\n  interface ElementClass extends React.Component<any> {\n    render(): React.ReactNode\n  }\n\n  interface ElementAttributesProperty {\n    props: {}\n  }\n\n  interface ElementChildrenAttribute {\n    children: {}\n  }\n\n  interface IntrinsicAttributes extends React.Attributes {}\n\n  interface IntrinsicElements extends React.JSX.IntrinsicElements, ExtendedIntrinsicElements<OpenTUIComponents> {\n    box: BoxProps\n    text: TextProps\n    span: SpanProps\n    code: CodeProps\n    diff: DiffProps\n    markdown: MarkdownProps\n    input: InputProps\n    textarea: TextareaProps\n    select: SelectProps\n    scrollbox: ScrollBoxProps\n    \"ascii-font\": AsciiFontProps\n    \"tab-select\": TabSelectProps\n    \"line-number\": LineNumberProps\n    // Text modifiers\n    b: SpanProps\n    i: SpanProps\n    u: SpanProps\n    strong: SpanProps\n    em: SpanProps\n    br: LineBreakProps\n    a: LinkProps\n  }\n}\n"
  },
  {
    "path": "packages/react/jsx-runtime.d.ts",
    "content": "export { Fragment, jsx, jsxs } from \"react/jsx-runtime\"\nexport type * from \"./jsx-namespace.d.ts\"\n"
  },
  {
    "path": "packages/react/jsx-runtime.js",
    "content": "export { Fragment, jsx, jsxs } from \"react/jsx-runtime\"\n"
  },
  {
    "path": "packages/react/package.json",
    "content": "{\n  \"name\": \"@opentui/react\",\n  \"version\": \"0.1.90\",\n  \"description\": \"React renderer for building terminal user interfaces using OpenTUI core\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/anomalyco/opentui\",\n    \"directory\": \"packages/react\"\n  },\n  \"module\": \"src/index.ts\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"main\": \"src/index.ts\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./src/index.ts\",\n      \"types\": \"./src/index.ts\"\n    },\n    \"./test-utils\": {\n      \"import\": \"./src/test-utils.ts\",\n      \"types\": \"./src/test-utils.d.ts\"\n    },\n    \"./runtime-plugin-support\": {\n      \"import\": \"./scripts/runtime-plugin-support.ts\",\n      \"types\": \"./scripts/runtime-plugin-support.ts\"\n    },\n    \"./jsx-runtime\": {\n      \"import\": \"./jsx-runtime.js\",\n      \"types\": \"./jsx-runtime.d.ts\"\n    },\n    \"./jsx-dev-runtime\": {\n      \"import\": \"./jsx-dev-runtime.js\",\n      \"types\": \"./jsx-dev-runtime.d.ts\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"bun run scripts/build.ts\",\n    \"build:examples\": \"bun examples/build.ts\",\n    \"build:dev\": \"bun run scripts/build.ts --dev\",\n    \"publish\": \"bun run scripts/publish.ts\",\n    \"test\": \"bun test\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"latest\",\n    \"@types/node\": \"^24.0.0\",\n    \"@types/react\": \"^19.0.0\",\n    \"@types/react-reconciler\": \"^0.32.0\",\n    \"@types/ws\": \"^8.18.1\",\n    \"react\": \">=19.0.0\",\n    \"react-devtools-core\": \"^7.0.1\",\n    \"typescript\": \"^5\",\n    \"ws\": \"^8.18.0\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=19.0.0\",\n    \"react-devtools-core\": \"^7.0.1\",\n    \"ws\": \"^8.18.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"react-devtools-core\": {\n      \"optional\": true\n    },\n    \"ws\": {\n      \"optional\": true\n    }\n  },\n  \"dependencies\": {\n    \"@opentui/core\": \"workspace:*\",\n    \"react-reconciler\": \"^0.32.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react/scripts/build.ts",
    "content": "import { spawnSync, type SpawnSyncReturns } from \"node:child_process\"\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"fs\"\nimport { dirname, join, resolve } from \"path\"\nimport { fileURLToPath } from \"url\"\nimport process from \"process\"\n\ninterface PackageJson {\n  name: string\n  version: string\n  license?: string\n  repository?: any\n  description?: string\n  homepage?: string\n  author?: string\n  bugs?: any\n  keywords?: string[]\n  module?: string\n  main?: string\n  types?: string\n  type?: string\n  exports?: any\n  dependencies?: Record<string, string>\n  devDependencies?: Record<string, string>\n  peerDependencies?: Record<string, string>\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = resolve(__dirname, \"..\")\nconst projectRootDir = resolve(rootDir, \"../..\")\nconst licensePath = join(projectRootDir, \"LICENSE\")\nconst packageJson: PackageJson = JSON.parse(readFileSync(join(rootDir, \"package.json\"), \"utf8\"))\n\nconst args = process.argv.slice(2)\nconst isDev = args.includes(\"--dev\")\nconst isCi = args.includes(\"--ci\")\n\nconst replaceLinks = (text: string): string => {\n  return packageJson.homepage\n    ? text.replace(\n        /(\\[.*?\\]\\()(\\.\\/.*?\\))/g,\n        (_, p1: string, p2: string) => `${p1}${packageJson.homepage}/blob/HEAD/${p2.replace(\"./\", \"\")}`,\n      )\n    : text\n}\n\nconst requiredFields: (keyof PackageJson)[] = [\"name\", \"version\", \"description\"]\nconst missingRequired = requiredFields.filter((field) => !packageJson[field])\nif (missingRequired.length > 0) {\n  console.error(`Error: Missing required fields in package.json: ${missingRequired.join(\", \")}`)\n  process.exit(1)\n}\n\nconsole.log(`Building @opentui/react library${isDev ? \" (dev mode)\" : \"\"}...`)\n\nconst distDir = join(rootDir, \"dist\")\nrmSync(distDir, { recursive: true, force: true })\nmkdirSync(distDir, { recursive: true })\n\nconst externalDeps: string[] = [\n  ...Object.keys(packageJson.dependencies || {}),\n  ...Object.keys(packageJson.peerDependencies || {}),\n]\n\nif (!packageJson.module) {\n  console.error(\"Error: 'module' field not found in package.json\")\n  process.exit(1)\n}\n\nconsole.log(\"Building all entry points...\")\nconst buildResult = await Bun.build({\n  entrypoints: [\n    join(rootDir, packageJson.module), // src/index.ts\n    join(rootDir, \"src/test-utils.ts\"), // test-utils\n  ],\n  target: \"bun\",\n  format: \"esm\",\n  outdir: join(rootDir, \"dist\"),\n  external: externalDeps,\n  splitting: true,\n})\n\nif (!buildResult.success) {\n  console.error(\"Build failed:\", buildResult.logs)\n  process.exit(1)\n}\n\nconsole.log(\"Generating TypeScript declarations...\")\n\nconst tsconfigBuildPath = join(rootDir, \"tsconfig.build.json\")\n\nconst tscResult: SpawnSyncReturns<Buffer> = spawnSync(\"bunx\", [\"tsc\", \"-p\", tsconfigBuildPath], {\n  cwd: rootDir,\n  stdio: \"inherit\",\n})\n\nif (tscResult.status !== 0) {\n  if (isCi) {\n    console.error(\"Error: TypeScript declaration generation failed\")\n    process.exit(1)\n  }\n  console.warn(\"Warning: TypeScript declaration generation failed\")\n} else {\n  console.log(\"TypeScript declarations generated\")\n}\n\n// Copy jsx runtime files\nif (existsSync(join(rootDir, \"jsx-runtime.d.ts\"))) {\n  copyFileSync(join(rootDir, \"jsx-runtime.d.ts\"), join(distDir, \"jsx-runtime.d.ts\"))\n}\n\nif (existsSync(join(rootDir, \"jsx-dev-runtime.d.ts\"))) {\n  copyFileSync(join(rootDir, \"jsx-dev-runtime.d.ts\"), join(distDir, \"jsx-dev-runtime.d.ts\"))\n}\n\nif (existsSync(join(rootDir, \"jsx-namespace.d.ts\"))) {\n  copyFileSync(join(rootDir, \"jsx-namespace.d.ts\"), join(distDir, \"jsx-namespace.d.ts\"))\n}\n\nif (existsSync(join(rootDir, \"jsx-runtime.js\"))) {\n  copyFileSync(join(rootDir, \"jsx-runtime.js\"), join(distDir, \"jsx-runtime.js\"))\n}\n\nif (existsSync(join(rootDir, \"jsx-dev-runtime.js\"))) {\n  copyFileSync(join(rootDir, \"jsx-dev-runtime.js\"), join(distDir, \"jsx-dev-runtime.js\"))\n}\n\nmkdirSync(join(distDir, \"scripts\"), { recursive: true })\n\nif (existsSync(join(rootDir, \"scripts\", \"runtime-plugin-support.ts\"))) {\n  copyFileSync(\n    join(rootDir, \"scripts\", \"runtime-plugin-support.ts\"),\n    join(distDir, \"scripts\", \"runtime-plugin-support.ts\"),\n  )\n}\n\nconst exports = {\n  \".\": {\n    types: \"./src/index.d.ts\",\n    import: \"./index.js\",\n    require: \"./index.js\",\n  },\n  \"./renderer\": {\n    types: \"./src/reconciler/renderer.d.ts\",\n    import: \"./index.js\",\n    require: \"./index.js\",\n  },\n  \"./test-utils\": {\n    types: \"./src/test-utils.d.ts\",\n    import: \"./test-utils.js\",\n    require: \"./test-utils.js\",\n  },\n  \"./runtime-plugin-support\": {\n    types: \"./scripts/runtime-plugin-support.d.ts\",\n    import: \"./scripts/runtime-plugin-support.ts\",\n  },\n  \"./jsx-runtime\": {\n    types: \"./jsx-runtime.d.ts\",\n    import: \"./jsx-runtime.js\",\n    require: \"./jsx-runtime.js\",\n  },\n  \"./jsx-dev-runtime\": {\n    types: \"./jsx-dev-runtime.d.ts\",\n    import: \"./jsx-dev-runtime.js\",\n    require: \"./jsx-dev-runtime.js\",\n  },\n}\n\nconst processedDependencies = { ...packageJson.dependencies }\nif (processedDependencies[\"@opentui/core\"] === \"workspace:*\") {\n  processedDependencies[\"@opentui/core\"] = packageJson.version\n}\n\nwriteFileSync(\n  join(distDir, \"package.json\"),\n  JSON.stringify(\n    {\n      name: packageJson.name,\n      module: \"index.js\",\n      main: \"index.js\",\n      types: \"src/index.d.ts\",\n      type: packageJson.type,\n      version: packageJson.version,\n      description: packageJson.description,\n      keywords: packageJson.keywords,\n      license: packageJson.license,\n      author: packageJson.author,\n      homepage: packageJson.homepage,\n      repository: packageJson.repository,\n      bugs: packageJson.bugs,\n      exports,\n      dependencies: processedDependencies,\n      devDependencies: packageJson.devDependencies,\n      peerDependencies: packageJson.peerDependencies,\n    },\n    null,\n    2,\n  ),\n)\n\nconst readmePath = join(rootDir, \"README.md\")\nif (existsSync(readmePath)) {\n  writeFileSync(join(distDir, \"README.md\"), replaceLinks(readFileSync(readmePath, \"utf8\")))\n} else {\n  console.warn(\"Warning: README.md not found in react package\")\n}\n\nif (existsSync(licensePath)) {\n  copyFileSync(licensePath, join(distDir, \"LICENSE\"))\n} else {\n  console.warn(\"Warning: LICENSE file not found in project root\")\n}\n\nconsole.log(\"Library built at:\", distDir)\n"
  },
  {
    "path": "packages/react/scripts/publish.ts",
    "content": "import { spawnSync, type SpawnSyncReturns } from \"node:child_process\"\nimport { readFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport process from \"node:process\"\nimport { fileURLToPath } from \"node:url\"\n\ninterface PackageJson {\n  name: string\n  version: string\n  dependencies?: Record<string, string>\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = resolve(__dirname, \"..\")\n\nconst packageJson: PackageJson = JSON.parse(readFileSync(join(rootDir, \"package.json\"), \"utf8\"))\n\nconsole.log(`Publishing @opentui/react@${packageJson.version}...`)\nconsole.log(\"Make sure you've run the pre-publish validation script first!\")\n\nconst distDir = join(rootDir, \"dist\")\n\nconsole.log(`\\nPublishing ${packageJson.name}@${packageJson.version}...`)\n\nconst isSnapshot = packageJson.version.includes(\"-snapshot\") || /^0\\.0\\.0-\\d{8}-[a-f0-9]{8}$/.test(packageJson.version)\nconst publishArgs = [\"publish\", \"--access=public\"]\n\nif (isSnapshot) {\n  publishArgs.push(\"--tag\", \"snapshot\")\n  console.log(`  Publishing as snapshot (--tag snapshot)`)\n}\n\nconst publish: SpawnSyncReturns<Buffer> = spawnSync(\"npm\", publishArgs, {\n  cwd: distDir,\n  stdio: \"inherit\",\n})\n\nif (publish.status !== 0) {\n  console.error(`Failed to publish '${packageJson.name}@${packageJson.version}'.`)\n  process.exit(1)\n}\n\nconsole.log(`Successfully published '${packageJson.name}@${packageJson.version}'`)\n"
  },
  {
    "path": "packages/react/scripts/runtime-plugin-support.ts",
    "content": "import { plugin as registerBunPlugin } from \"bun\"\nimport * as coreRuntime from \"@opentui/core\"\nimport { createRuntimePlugin, type RuntimeModuleEntry } from \"@opentui/core/runtime-plugin\"\nimport * as reactRuntime from \"react\"\nimport * as reactJsxRuntime from \"react/jsx-runtime\"\nimport * as reactJsxDevRuntime from \"react/jsx-dev-runtime\"\nimport * as opentuiReactRuntime from \"../src/index\"\n\nconst runtimePluginSupportInstalledKey = \"__opentuiReactRuntimePluginSupportInstalled__\"\n\ntype RuntimePluginSupportState = typeof globalThis & {\n  [runtimePluginSupportInstalledKey]?: boolean\n}\n\nconst additionalRuntimeModules: Record<string, RuntimeModuleEntry> = {\n  \"@opentui/react\": opentuiReactRuntime as Record<string, unknown>,\n  \"@opentui/react/jsx-runtime\": reactJsxRuntime as Record<string, unknown>,\n  \"@opentui/react/jsx-dev-runtime\": reactJsxDevRuntime as Record<string, unknown>,\n  react: reactRuntime as Record<string, unknown>,\n  \"react/jsx-runtime\": reactJsxRuntime as Record<string, unknown>,\n  \"react/jsx-dev-runtime\": reactJsxDevRuntime as Record<string, unknown>,\n}\n\nexport function ensureRuntimePluginSupport(): boolean {\n  const state = globalThis as RuntimePluginSupportState\n\n  if (state[runtimePluginSupportInstalledKey]) {\n    return false\n  }\n\n  registerBunPlugin(\n    createRuntimePlugin({\n      core: coreRuntime as Record<string, unknown>,\n      additional: additionalRuntimeModules,\n    }),\n  )\n\n  state[runtimePluginSupportInstalledKey] = true\n  return true\n}\n\nensureRuntimePluginSupport()\n"
  },
  {
    "path": "packages/react/src/components/app.tsx",
    "content": "import type { CliRenderer, KeyHandler } from \"@opentui/core\"\nimport { createContext, useContext } from \"react\"\n\ninterface AppContext {\n  keyHandler: KeyHandler | null\n  renderer: CliRenderer | null\n}\n\nexport const AppContext = createContext<AppContext>({\n  keyHandler: null,\n  renderer: null,\n})\n\nexport const useAppContext = () => {\n  return useContext(AppContext)\n}\n"
  },
  {
    "path": "packages/react/src/components/error-boundary.tsx",
    "content": "import React from \"react\"\n\nexport class ErrorBoundary extends React.Component<\n  { children: React.ReactNode },\n  { hasError: boolean; error: Error | null }\n> {\n  constructor(props: { children: React.ReactNode }) {\n    super(props)\n    this.state = { hasError: false, error: null }\n  }\n\n  static getDerivedStateFromError(error: Error): {\n    hasError: boolean\n    error: Error\n  } {\n    return { hasError: true, error }\n  }\n\n  override render(): any {\n    if (this.state.hasError && this.state.error) {\n      return (\n        <box style={{ flexDirection: \"column\", padding: 2 }}>\n          <text fg=\"red\">{this.state.error.stack || this.state.error.message}</text>\n        </box>\n      )\n    }\n\n    return this.props.children\n  }\n}\n"
  },
  {
    "path": "packages/react/src/components/index.ts",
    "content": "import {\n  ASCIIFontRenderable,\n  BoxRenderable,\n  CodeRenderable,\n  DiffRenderable,\n  InputRenderable,\n  LineNumberRenderable,\n  MarkdownRenderable,\n  ScrollBoxRenderable,\n  SelectRenderable,\n  TabSelectRenderable,\n  TextareaRenderable,\n  TextRenderable,\n} from \"@opentui/core\"\nimport type { RenderableConstructor } from \"../types/components.js\"\nimport {\n  BoldSpanRenderable,\n  ItalicSpanRenderable,\n  LineBreakRenderable,\n  LinkRenderable,\n  SpanRenderable,\n  UnderlineSpanRenderable,\n} from \"./text.js\"\n\nexport const baseComponents = {\n  box: BoxRenderable,\n  text: TextRenderable,\n  code: CodeRenderable,\n  diff: DiffRenderable,\n  markdown: MarkdownRenderable,\n  input: InputRenderable,\n  select: SelectRenderable,\n  textarea: TextareaRenderable,\n  scrollbox: ScrollBoxRenderable,\n  \"ascii-font\": ASCIIFontRenderable,\n  \"tab-select\": TabSelectRenderable,\n  \"line-number\": LineNumberRenderable,\n\n  // Text modifiers\n  span: SpanRenderable,\n  br: LineBreakRenderable,\n  b: BoldSpanRenderable,\n  strong: BoldSpanRenderable,\n  i: ItalicSpanRenderable,\n  em: ItalicSpanRenderable,\n  u: UnderlineSpanRenderable,\n  a: LinkRenderable,\n}\n\ntype ComponentCatalogue = Record<string, RenderableConstructor>\n\nexport const componentCatalogue: ComponentCatalogue = { ...baseComponents }\n\n/**\n * Extend the component catalogue with new renderable components\n *\n * @example\n * ```tsx\n * // Extend with an object of components\n * extend({\n *   consoleButton: ConsoleButtonRenderable,\n *   customBox: CustomBoxRenderable\n * })\n * ```\n */\nexport function extend<T extends ComponentCatalogue>(objects: T): void {\n  Object.assign(componentCatalogue, objects)\n}\n\nexport function getComponentCatalogue(): ComponentCatalogue {\n  return componentCatalogue\n}\n\nexport type { ExtendedComponentProps, ExtendedIntrinsicElements, RenderableConstructor } from \"../types/components.js\"\n"
  },
  {
    "path": "packages/react/src/components/text.ts",
    "content": "import { TextAttributes, TextNodeRenderable, type RenderContext, type TextNodeOptions } from \"@opentui/core\"\n\nexport const textNodeKeys = [\"span\", \"b\", \"strong\", \"i\", \"em\", \"u\", \"br\", \"a\"] as const\nexport type TextNodeKey = (typeof textNodeKeys)[number]\n\nexport class SpanRenderable extends TextNodeRenderable {\n  constructor(\n    private readonly ctx: RenderContext | null,\n    options: TextNodeOptions,\n  ) {\n    super(options)\n  }\n}\n\n// Custom TextNode component for text modifiers\nclass TextModifierRenderable extends SpanRenderable {\n  constructor(options: TextNodeOptions, modifier?: TextNodeKey) {\n    super(null, options)\n\n    // Set appropriate attributes based on modifier type\n    if (modifier === \"b\" || modifier === \"strong\") {\n      this.attributes = (this.attributes || 0) | TextAttributes.BOLD\n    } else if (modifier === \"i\" || modifier === \"em\") {\n      this.attributes = (this.attributes || 0) | TextAttributes.ITALIC\n    } else if (modifier === \"u\") {\n      this.attributes = (this.attributes || 0) | TextAttributes.UNDERLINE\n    }\n  }\n}\n\nexport class BoldSpanRenderable extends TextModifierRenderable {\n  constructor(_ctx: RenderContext | null, options: TextNodeOptions) {\n    super(options, \"b\")\n  }\n}\n\nexport class ItalicSpanRenderable extends TextModifierRenderable {\n  constructor(_ctx: RenderContext | null, options: TextNodeOptions) {\n    super(options, \"i\")\n  }\n}\n\nexport class UnderlineSpanRenderable extends TextModifierRenderable {\n  constructor(_ctx: RenderContext | null, options: TextNodeOptions) {\n    super(options, \"u\")\n  }\n}\n\nexport class LineBreakRenderable extends SpanRenderable {\n  constructor(_ctx: RenderContext | null, options: TextNodeOptions) {\n    super(null, options)\n    this.add() // Add a newline\n  }\n\n  public override add(): number {\n    return super.add(\"\\n\")\n  }\n}\n\nexport interface LinkOptions extends TextNodeOptions {\n  href: string\n}\n\nexport class LinkRenderable extends SpanRenderable {\n  constructor(_ctx: RenderContext | null, options: LinkOptions) {\n    const linkOptions: TextNodeOptions = {\n      ...options,\n      link: { url: options.href },\n    }\n    super(null, linkOptions)\n  }\n}\n"
  },
  {
    "path": "packages/react/src/hooks/index.ts",
    "content": "export * from \"./use-keyboard.js\"\nexport * from \"./use-renderer.js\"\nexport * from \"./use-resize.js\"\nexport * from \"./use-terminal-dimensions.js\"\nexport * from \"./use-timeline.js\"\n"
  },
  {
    "path": "packages/react/src/hooks/use-event.ts",
    "content": "import { useCallback, useLayoutEffect, useRef } from \"react\"\n\n/**\n * Returns a stable callback that always calls the latest version of the provided handler.\n * This prevents unnecessary re-renders and effect re-runs while ensuring the callback\n * always has access to the latest props and state.\n *\n * Useful for event handlers that need to be passed to effects with empty dependency arrays\n * or memoized child components.\n */\nexport function useEffectEvent<T extends (...args: any[]) => any>(handler: T): T {\n  const handlerRef = useRef<T>(handler)\n\n  useLayoutEffect(() => {\n    handlerRef.current = handler\n  })\n\n  return useCallback((...args: Parameters<T>) => {\n    const fn = handlerRef.current\n    return fn(...args)\n  }, []) as T\n}\n"
  },
  {
    "path": "packages/react/src/hooks/use-keyboard.ts",
    "content": "import type { KeyEvent } from \"@opentui/core\"\nimport { useEffect } from \"react\"\nimport { useAppContext } from \"../components/app.js\"\nimport { useEffectEvent } from \"./use-event.js\"\n\nexport interface UseKeyboardOptions {\n  /** Include release events - callback receives events with eventType: \"release\" */\n  release?: boolean\n}\n\n/**\n * Subscribe to keyboard events.\n *\n * By default, only receives press events (including key repeats with `repeated: true`).\n * Use `options.release` to also receive release events.\n *\n * @example\n * // Basic press handling (includes repeats)\n * useKeyboard((e) => console.log(e.name, e.repeated ? \"(repeat)\" : \"\"))\n *\n * // With release events\n * useKeyboard((e) => {\n *   if (e.eventType === \"release\") keys.delete(e.name)\n *   else keys.add(e.name)\n * }, { release: true })\n */\nexport const useKeyboard = (handler: (key: KeyEvent) => void, options: UseKeyboardOptions = { release: false }) => {\n  const { keyHandler } = useAppContext()\n  const stableHandler = useEffectEvent(handler)\n\n  useEffect(() => {\n    keyHandler?.on(\"keypress\", stableHandler)\n    if (options?.release) {\n      keyHandler?.on(\"keyrelease\", stableHandler)\n    }\n    return () => {\n      keyHandler?.off(\"keypress\", stableHandler)\n      if (options?.release) {\n        keyHandler?.off(\"keyrelease\", stableHandler)\n      }\n    }\n  }, [keyHandler, options.release])\n}\n"
  },
  {
    "path": "packages/react/src/hooks/use-renderer.ts",
    "content": "import { useAppContext } from \"../components/app.js\"\n\nexport const useRenderer = () => {\n  const { renderer } = useAppContext()\n\n  if (!renderer) {\n    throw new Error(\"Renderer not found.\")\n  }\n\n  return renderer\n}\n"
  },
  {
    "path": "packages/react/src/hooks/use-resize.ts",
    "content": "import { useEffect } from \"react\"\nimport { useEffectEvent } from \"./use-event.js\"\nimport { useRenderer } from \"./use-renderer.js\"\n\nexport const useOnResize = (callback: (width: number, height: number) => void) => {\n  const renderer = useRenderer()\n  const stableCallback = useEffectEvent(callback)\n\n  useEffect(() => {\n    renderer.on(\"resize\", stableCallback)\n\n    return () => {\n      renderer.off(\"resize\", stableCallback)\n    }\n  }, [renderer])\n\n  return renderer\n}\n"
  },
  {
    "path": "packages/react/src/hooks/use-terminal-dimensions.ts",
    "content": "import { useState } from \"react\"\nimport { useRenderer } from \"./use-renderer.js\"\nimport { useOnResize } from \"./use-resize.js\"\n\nexport const useTerminalDimensions = () => {\n  const renderer = useRenderer()\n\n  const [dimensions, setDimensions] = useState<{\n    width: number\n    height: number\n  }>({\n    width: renderer.width,\n    height: renderer.height,\n  })\n\n  const cb = (width: number, height: number) => {\n    setDimensions({ width, height })\n  }\n\n  useOnResize(cb)\n\n  return dimensions\n}\n"
  },
  {
    "path": "packages/react/src/hooks/use-timeline.ts",
    "content": "import { engine, Timeline, type TimelineOptions } from \"@opentui/core\"\nimport { useEffect } from \"react\"\n\nexport const useTimeline = (options: TimelineOptions = {}) => {\n  const timeline = new Timeline(options)\n\n  useEffect(() => {\n    if (!options.autoplay) {\n      timeline.play()\n    }\n\n    engine.register(timeline)\n\n    return () => {\n      timeline.pause()\n      engine.unregister(timeline)\n    }\n  }, [])\n\n  return timeline\n}\n"
  },
  {
    "path": "packages/react/src/index.ts",
    "content": "export * from \"./components/index.js\"\nexport * from \"./components/app.js\"\nexport * from \"./hooks/index.js\"\nexport * from \"./plugins/slot.js\"\nexport * from \"./reconciler/renderer.js\"\nexport * from \"./time-to-first-draw.js\"\nexport * from \"./types/components.js\"\n\nexport { createElement } from \"react\"\n"
  },
  {
    "path": "packages/react/src/plugins/slot.tsx",
    "content": "import {\n  createSlotRegistry,\n  SlotRegistry,\n  type CliRenderer,\n  type Plugin,\n  type PluginContext,\n  type PluginErrorEvent,\n  type ResolvedSlotRenderer,\n  type SlotMode,\n  type SlotRegistryOptions,\n} from \"@opentui/core\"\nimport React, { Fragment, useEffect, useMemo, useRef, useState } from \"react\"\nimport type { ReactNode } from \"react\"\n\nexport type { SlotMode }\ntype SlotMap = Record<string, object>\n\nexport type ReactPlugin<TSlots extends SlotMap, TContext extends PluginContext = PluginContext> = Plugin<\n  ReactNode,\n  TSlots,\n  TContext\n>\n\nexport type ReactSlotProps<\n  TSlots extends SlotMap,\n  K extends keyof TSlots,\n  TContext extends PluginContext = PluginContext,\n> = {\n  registry: SlotRegistry<ReactNode, TSlots, TContext>\n  name: K\n  mode?: SlotMode\n  children?: ReactNode\n  pluginFailurePlaceholder?: (failure: PluginErrorEvent) => ReactNode\n} & TSlots[K]\n\nexport type ReactBoundSlotProps<TSlots extends SlotMap, K extends keyof TSlots> = {\n  name: K\n  mode?: SlotMode\n  children?: ReactNode\n} & TSlots[K]\n\nexport type ReactRegistrySlotComponent<TSlots extends SlotMap, TContext extends PluginContext = PluginContext> = <\n  K extends keyof TSlots,\n>(\n  props: ReactSlotProps<TSlots, K, TContext>,\n) => ReactNode\n\nexport type ReactSlotComponent<TSlots extends SlotMap> = <K extends keyof TSlots>(\n  props: ReactBoundSlotProps<TSlots, K>,\n) => ReactNode\n\nexport interface ReactSlotOptions {\n  pluginFailurePlaceholder?: (failure: PluginErrorEvent) => ReactNode\n}\n\nexport function createReactSlotRegistry<TSlots extends SlotMap, TContext extends PluginContext = PluginContext>(\n  renderer: CliRenderer,\n  context: TContext,\n  options: SlotRegistryOptions = {},\n): SlotRegistry<ReactNode, TSlots, TContext> {\n  // React slots intentionally use one registry key per renderer instance.\n  // Use createSlotRegistry from @opentui/core with a custom key for independent registries.\n  return createSlotRegistry<ReactNode, TSlots, TContext>(renderer, \"react:slot-registry\", context, options)\n}\n\ntype PluginErrorBoundaryProps = {\n  registry: SlotRegistry<ReactNode, any, any>\n  pluginFailurePlaceholder?: (failure: PluginErrorEvent) => ReactNode\n  pluginId: string\n  slotName: string\n  resetToken: number\n  fallbackOnFailure?: ReactNode\n  children: ReactNode\n}\n\ntype PluginErrorBoundaryState = {\n  failure: PluginErrorEvent | null\n}\n\nfunction renderPluginFailurePlaceholder(\n  registry: SlotRegistry<ReactNode, any, any>,\n  pluginFailurePlaceholder: ((failure: PluginErrorEvent) => ReactNode) | undefined,\n  failure: PluginErrorEvent,\n  pluginId: string,\n  slot: string,\n): ReactNode {\n  if (!pluginFailurePlaceholder) {\n    return null\n  }\n\n  try {\n    return pluginFailurePlaceholder(failure)\n  } catch (error) {\n    registry.reportPluginError({\n      pluginId,\n      slot,\n      phase: \"error_placeholder\",\n      source: \"react\",\n      error,\n    })\n\n    return null\n  }\n}\n\nclass PluginErrorBoundary extends React.Component<PluginErrorBoundaryProps, PluginErrorBoundaryState> {\n  constructor(props: PluginErrorBoundaryProps) {\n    super(props)\n    this.state = { failure: null }\n  }\n\n  override componentDidCatch(error: Error): void {\n    const failure = this.props.registry.reportPluginError({\n      pluginId: this.props.pluginId,\n      slot: this.props.slotName,\n      phase: \"render\",\n      source: \"react\",\n      error,\n    })\n\n    this.setState({ failure })\n  }\n\n  override componentDidUpdate(previousProps: PluginErrorBoundaryProps): void {\n    if (previousProps.resetToken !== this.props.resetToken && this.state.failure) {\n      this.setState({ failure: null })\n    }\n  }\n\n  override render(): ReactNode {\n    if (this.state.failure) {\n      const placeholder = renderPluginFailurePlaceholder(\n        this.props.registry,\n        this.props.pluginFailurePlaceholder,\n        this.state.failure,\n        this.props.pluginId,\n        this.props.slotName,\n      )\n\n      if (placeholder === null || placeholder === undefined || placeholder === false) {\n        return this.props.fallbackOnFailure ?? null\n      }\n\n      return placeholder\n    }\n\n    return this.props.children\n  }\n}\n\nfunction getSlotProps<TSlots extends SlotMap, K extends keyof TSlots, TContext extends PluginContext = PluginContext>(\n  props: ReactSlotProps<TSlots, K, TContext>,\n): TSlots[K] {\n  const {\n    children: _children,\n    mode: _mode,\n    name: _name,\n    registry: _registry,\n    pluginFailurePlaceholder: _pluginFailurePlaceholder,\n    ...slotProps\n  } = props\n  return slotProps as TSlots[K]\n}\n\nexport function createSlot<TSlots extends SlotMap, TContext extends PluginContext = PluginContext>(\n  registry: SlotRegistry<ReactNode, TSlots, TContext>,\n  options: ReactSlotOptions = {},\n): ReactSlotComponent<TSlots> {\n  return function BoundSlot<K extends keyof TSlots>(props: ReactBoundSlotProps<TSlots, K>): ReactNode {\n    return (\n      <Slot<TSlots, TContext, K>\n        {...(props as ReactBoundSlotProps<TSlots, K>)}\n        registry={registry}\n        pluginFailurePlaceholder={options.pluginFailurePlaceholder}\n      />\n    )\n  }\n}\n\nexport function Slot<\n  TSlots extends SlotMap,\n  TContext extends PluginContext = PluginContext,\n  K extends keyof TSlots = keyof TSlots,\n>(props: ReactSlotProps<TSlots, K, TContext>): ReactNode {\n  const [version, setVersion] = useState(0)\n  const registry = props.registry\n  const slotName = String(props.name)\n  const renderFailuresByPluginRef = useRef<Map<string, PluginErrorEvent>>(new Map())\n  const pendingRenderReportsRef = useRef<Map<string, { pluginId: string; slot: string; error: Error }>>(new Map())\n\n  useEffect(() => {\n    return registry.subscribe(() => {\n      setVersion((current) => current + 1)\n    })\n  }, [registry])\n\n  useEffect(() => {\n    if (pendingRenderReportsRef.current.size === 0) {\n      return\n    }\n\n    const pendingReports = [...pendingRenderReportsRef.current.values()]\n    pendingRenderReportsRef.current.clear()\n\n    for (const report of pendingReports) {\n      const failure = registry.reportPluginError({\n        pluginId: report.pluginId,\n        slot: report.slot,\n        phase: \"render\",\n        source: \"react\",\n        error: report.error,\n      })\n\n      renderFailuresByPluginRef.current.set(`${report.slot}:${report.pluginId}:render`, failure)\n    }\n  })\n\n  const entries = useMemo<Array<ResolvedSlotRenderer<ReactNode, TSlots[K], TContext>>>(\n    () => registry.resolveEntries(props.name),\n    [registry, props.name, version],\n  )\n  const slotProps = getSlotProps(props)\n\n  const renderEntry = (\n    entry: ResolvedSlotRenderer<ReactNode, TSlots[K], TContext>,\n    fallbackOnFailure?: ReactNode,\n  ): ReactNode => {\n    const key = `${slotName}:${entry.id}`\n    const failureKey = `${slotName}:${entry.id}:render`\n\n    try {\n      const rendered = entry.renderer(registry.context, slotProps)\n      renderFailuresByPluginRef.current.delete(failureKey)\n      pendingRenderReportsRef.current.delete(failureKey)\n      return (\n        <PluginErrorBoundary\n          key={key}\n          registry={registry}\n          pluginFailurePlaceholder={props.pluginFailurePlaceholder}\n          pluginId={entry.id}\n          slotName={slotName}\n          resetToken={version}\n          fallbackOnFailure={fallbackOnFailure}\n        >\n          {rendered}\n        </PluginErrorBoundary>\n      )\n    } catch (error) {\n      const normalizedError =\n        error instanceof Error ? error : typeof error === \"string\" ? new Error(error) : new Error(String(error))\n      const lastFailure = renderFailuresByPluginRef.current.get(failureKey)\n      const isSameFailure = lastFailure && lastFailure.error.message === normalizedError.message\n\n      if (!isSameFailure) {\n        const queued = pendingRenderReportsRef.current.get(failureKey)\n        if (!queued || queued.error.message !== normalizedError.message) {\n          pendingRenderReportsRef.current.set(failureKey, {\n            pluginId: entry.id,\n            slot: slotName,\n            error: normalizedError,\n          })\n        }\n      }\n\n      const failure: PluginErrorEvent =\n        isSameFailure && lastFailure\n          ? lastFailure\n          : {\n              pluginId: entry.id,\n              slot: slotName,\n              phase: \"render\",\n              source: \"react\",\n              error: normalizedError,\n              timestamp: Date.now(),\n            }\n\n      renderFailuresByPluginRef.current.set(failureKey, failure)\n\n      const placeholder = renderPluginFailurePlaceholder(\n        registry,\n        props.pluginFailurePlaceholder,\n        failure,\n        entry.id,\n        slotName,\n      )\n      if (placeholder === null || placeholder === undefined || placeholder === false) {\n        return fallbackOnFailure ?? null\n      }\n\n      return <Fragment key={key}>{placeholder}</Fragment>\n    }\n  }\n\n  if (entries.length === 0) {\n    return <>{props.children}</>\n  }\n\n  if (props.mode === \"single_winner\") {\n    const winner = entries[0]\n    if (!winner) {\n      return <>{props.children}</>\n    }\n\n    const rendered = renderEntry(winner, props.children)\n    if (rendered === null || rendered === undefined || rendered === false) {\n      return <>{props.children}</>\n    }\n\n    return <>{rendered}</>\n  }\n\n  if (props.mode === \"replace\") {\n    if (entries.length === 1) {\n      const rendered = renderEntry(entries[0], props.children)\n      if (rendered === null || rendered === undefined || rendered === false) {\n        return <>{props.children}</>\n      }\n\n      return <>{rendered}</>\n    }\n\n    const renderedEntries = entries.map((entry) => renderEntry(entry))\n    const hasPluginOutput = renderedEntries.some((node) => node !== null && node !== undefined && node !== false)\n\n    if (!hasPluginOutput) {\n      return <>{props.children}</>\n    }\n\n    return <>{renderedEntries}</>\n  }\n\n  return (\n    <>\n      {props.children}\n      {entries.map((entry) => renderEntry(entry))}\n    </>\n  )\n}\n"
  },
  {
    "path": "packages/react/src/reconciler/devtools-polyfill.ts",
    "content": "// Polyfills required for react-devtools-core in Node.js/Bun environments\n// This file MUST be imported before react-devtools-core\n\nconst g = globalThis as any\n\n// Only polyfill WebSocket if not natively available (Node.js < 21)\n// Bun and Node.js 21+ should have native WebSocket support\nif (typeof g.WebSocket === \"undefined\") {\n  try {\n    const ws = await import(\"ws\")\n    g.WebSocket = ws.default\n  } catch {\n    // ws not installed - will fail later if DevTools actually needs it\n  }\n}\n\n// react-devtools-core expects browser-like globals\ng.window ||= globalThis\ng.self ||= globalThis\n\n// Filter out internal components from devtools for a cleaner view.\n// Since `react-devtools-shared` package isn't published on npm, we can't\n// use its types, that's why there are hard-coded values in `type` fields below.\n// See https://github.com/facebook/react/blob/edf6eac8a181860fd8a2d076a43806f1237495a1/packages/react-devtools-shared/src/types.js#L24\ng.window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = [\n  {\n    // ComponentFilterDisplayName\n    type: 2,\n    value: \"ErrorBoundary\",\n    isEnabled: true,\n    isValid: true,\n  },\n]\n"
  },
  {
    "path": "packages/react/src/reconciler/devtools.ts",
    "content": "// DevTools initialization module\n// This file is dynamically imported only when DEV=true\n\nimport \"./devtools-polyfill\"\n\n// @ts-expect-error - no types available for react-devtools-core\nimport devtools from \"react-devtools-core\"\n\ndevtools.initialize()\ndevtools.connectToDevTools()\n"
  },
  {
    "path": "packages/react/src/reconciler/host-config.ts",
    "content": "import { TextNodeRenderable, TextRenderable, type Renderable } from \"@opentui/core\"\nimport pkgJson from \"../../package.json\"\nimport { createContext } from \"react\"\nimport type { HostConfig, ReactContext } from \"react-reconciler\"\nimport { DefaultEventPriority, NoEventPriority } from \"react-reconciler/constants\"\nimport { getComponentCatalogue } from \"../components/index.js\"\nimport { textNodeKeys, type TextNodeKey } from \"../components/text.js\"\nimport type { Container, HostContext, Instance, Props, PublicInstance, TextInstance, Type } from \"../types/host.js\"\nimport { getNextId } from \"../utils/id.js\"\nimport { setInitialProperties, updateProperties } from \"../utils/index.js\"\n\nlet currentUpdatePriority = NoEventPriority\n\n// https://github.com/facebook/react/tree/main/packages/react-reconciler#practical-examples\nexport const hostConfig: HostConfig<\n  Type,\n  Props,\n  Container,\n  Instance,\n  TextInstance,\n  unknown, // SuspenseInstance\n  unknown, // HydratableInstance\n  unknown, // FormInstance\n  PublicInstance,\n  HostContext,\n  unknown, // ChildSet\n  unknown, // TimeoutHandle\n  unknown, // NoTimeout\n  unknown // TransitionStatus\n> = {\n  supportsMutation: true,\n  supportsPersistence: false,\n  supportsHydration: false,\n\n  // Create instances of opentui components\n  createInstance(type: Type, props: Props, rootContainerInstance: Container, hostContext: HostContext) {\n    if (textNodeKeys.includes(type as TextNodeKey) && !hostContext.isInsideText) {\n      throw new Error(`Component of type \"${type}\" must be created inside of a text node`)\n    }\n\n    const id = getNextId(type)\n    const components = getComponentCatalogue()\n\n    if (!components[type]) {\n      throw new Error(`Unknown component type: ${type}`)\n    }\n\n    return new components[type](rootContainerInstance.ctx, {\n      id,\n      ...props,\n    })\n  },\n\n  // Append a child to a parent\n  appendChild(parent: Instance, child: Instance) {\n    parent.add(child)\n  },\n\n  // Remove a child from a parent\n  removeChild(parent: Instance, child: Instance) {\n    parent.remove(child.id)\n  },\n\n  // Insert a child before another child\n  insertBefore(parent: Instance, child: Instance, beforeChild: Instance) {\n    parent.insertBefore(child, beforeChild)\n  },\n\n  // Insert a child at a specific index\n  insertInContainerBefore(parent: Container, child: Instance, beforeChild: Instance) {\n    parent.insertBefore(child, beforeChild)\n  },\n\n  // Remove a child from container\n  removeChildFromContainer(parent: Container, child: Instance) {\n    parent.remove(child.id)\n  },\n\n  // Prepare for commit\n  prepareForCommit(containerInfo: Container) {\n    return null\n  },\n\n  // Reset after commit\n  resetAfterCommit(containerInfo: Container) {\n    // Trigger a render update if needed\n    containerInfo.requestRender()\n  },\n\n  // Get root container\n  getRootHostContext(rootContainerInstance: Container) {\n    return { isInsideText: false }\n  },\n\n  // Get child context\n  getChildHostContext(parentHostContext: HostContext, type: Type, rootContainerInstance: Container) {\n    const isInsideText = [\"text\", ...textNodeKeys].includes(type)\n    return { ...parentHostContext, isInsideText }\n  },\n\n  // Should set text content\n  shouldSetTextContent(type: Type, props: Props) {\n    return false\n  },\n\n  // Create text instance\n  createTextInstance(text: string, rootContainerInstance: Container, hostContext: HostContext) {\n    if (!hostContext.isInsideText) {\n      throw new Error(\"Text must be created inside of a text node\")\n    }\n\n    return TextNodeRenderable.fromString(text)\n  },\n\n  // Schedule timeout\n  scheduleTimeout: setTimeout,\n\n  // Cancel timeout\n  cancelTimeout: clearTimeout,\n\n  // No timeout\n  noTimeout: -1,\n\n  // Should attempt synchronous flush\n  shouldAttemptEagerTransition() {\n    return false\n  },\n\n  // Finalize initial children\n  finalizeInitialChildren(\n    instance: Instance,\n    type: Type,\n    props: Props,\n    rootContainerInstance: Container,\n    hostContext: HostContext,\n  ) {\n    setInitialProperties(instance, type, props)\n    return false\n  },\n\n  // Commit mount\n  commitMount(instance: Instance, type: Type, props: Props, internalInstanceHandle: any) {\n    // We could focus the instance here, but we're handling focus in setInitialProperties\n  },\n\n  // Commit update\n  commitUpdate(instance: Instance, type: Type, oldProps: Props, newProps: Props, internalInstanceHandle: any) {\n    updateProperties(instance, type, oldProps, newProps)\n    instance.requestRender()\n  },\n\n  // Commit text update\n  commitTextUpdate(textInstance: TextInstance, oldText: string, newText: string) {\n    textInstance.children = [newText]\n    textInstance.requestRender()\n  },\n\n  // Append child to container\n  appendChildToContainer(container: Container, child: Instance) {\n    container.add(child)\n  },\n\n  appendInitialChild(parent: Instance, child: Instance) {\n    parent.add(child)\n  },\n\n  // Hide instance\n  hideInstance(instance: Instance) {\n    instance.visible = false\n    instance.requestRender()\n  },\n\n  // Unhide instance\n  unhideInstance(instance: Instance, props: Props) {\n    instance.visible = true\n    instance.requestRender()\n  },\n\n  // Hide text instance\n  hideTextInstance(textInstance: TextInstance) {\n    textInstance.visible = false\n    textInstance.requestRender()\n  },\n\n  // Unhide text instance\n  unhideTextInstance(textInstance: TextInstance, text: string) {\n    textInstance.visible = true\n    textInstance.requestRender()\n  },\n\n  // Clear container\n  clearContainer(container: Container) {\n    // Remove all children\n    const children = container.getChildren()\n    children.forEach((child) => container.remove(child.id))\n  },\n\n  // Misc\n  setCurrentUpdatePriority(newPriority: number) {\n    currentUpdatePriority = newPriority\n  },\n\n  getCurrentUpdatePriority: () => currentUpdatePriority,\n\n  resolveUpdatePriority() {\n    if (currentUpdatePriority !== NoEventPriority) {\n      return currentUpdatePriority\n    }\n\n    return DefaultEventPriority\n  },\n\n  maySuspendCommit() {\n    return false\n  },\n\n  NotPendingTransition: null,\n\n  HostTransitionContext: createContext(null) as unknown as ReactContext<null>,\n\n  resetFormInstance() {},\n\n  requestPostPaintCallback() {},\n\n  trackSchedulerEvent() {},\n\n  resolveEventType() {\n    return null\n  },\n\n  resolveEventTimeStamp() {\n    return -1.1\n  },\n\n  preloadInstance() {\n    return true\n  },\n\n  startSuspendingCommit() {},\n\n  suspendInstance() {},\n\n  waitForCommitToBeReady() {\n    return null\n  },\n\n  detachDeletedInstance(instance: Instance) {\n    if (!instance.parent) {\n      instance.destroyRecursively()\n    }\n  },\n\n  getPublicInstance(instance: Renderable | TextRenderable) {\n    return instance\n  },\n\n  preparePortalMount(containerInfo: Container) {},\n\n  isPrimaryRenderer: true,\n\n  getInstanceFromNode() {\n    return null\n  },\n\n  beforeActiveInstanceBlur() {},\n\n  afterActiveInstanceBlur() {},\n\n  prepareScopeUpdate() {},\n\n  getInstanceFromScope() {\n    return null\n  },\n\n  // @ts-expect-error DefinitelyTyped is not up to date\n  rendererPackageName: \"@opentui/react\",\n  rendererVersion: pkgJson.version,\n}\n"
  },
  {
    "path": "packages/react/src/reconciler/reconciler.ts",
    "content": "import type { RootRenderable } from \"@opentui/core\"\nimport React from \"react\"\nimport ReactReconciler from \"react-reconciler\"\nimport { ConcurrentRoot } from \"react-reconciler/constants\"\nimport { hostConfig } from \"./host-config.js\"\n\nexport const reconciler = ReactReconciler(hostConfig)\n\nif (process.env[\"DEV\"] === \"true\") {\n  try {\n    await import(\"./devtools.js\")\n  } catch (error: any) {\n    if (error.code === \"ERR_MODULE_NOT_FOUND\") {\n      console.warn(\n        `\nThe environment variable DEV is set to true, so opentui tried to import \\`react-devtools-core\\`,\nbut this failed as it was not installed. Debugging with React DevTools requires it.\n\nTo install use this command:\n\n$ bun add react-devtools-core@7 -d\n        `.trim() + \"\\n\",\n      )\n    } else {\n      throw error\n    }\n  }\n}\n\n// Inject into DevTools - this is safe to call even if devtools isn't connected\n// @ts-expect-error the types for `react-reconciler` are not up to date with the library.\nreconciler.injectIntoDevTools()\n\nexport function _render(element: React.ReactNode, root: RootRenderable) {\n  const container = reconciler.createContainer(\n    root,\n    ConcurrentRoot,\n    null,\n    false,\n    null,\n    \"\",\n    console.error,\n    console.error,\n    console.error,\n    console.error,\n    null,\n  )\n\n  reconciler.updateContainer(element, container, null, () => {})\n\n  return container\n}\n"
  },
  {
    "path": "packages/react/src/reconciler/renderer.ts",
    "content": "import { CliRenderer, CliRenderEvents, engine } from \"@opentui/core\"\nimport React, { type ReactNode } from \"react\"\nimport type { OpaqueRoot } from \"react-reconciler\"\nimport { AppContext } from \"../components/app.js\"\nimport { ErrorBoundary } from \"../components/error-boundary.js\"\nimport { _render, reconciler } from \"./reconciler.js\"\n\n// flushSync was renamed to flushSyncFromReconciler in react-reconciler 0.32.0\n// the types for react-reconciler are not up to date with the library\nconst _r = reconciler as typeof reconciler & { flushSyncFromReconciler?: typeof reconciler.flushSync }\nconst flushSync = _r.flushSyncFromReconciler ?? _r.flushSync\nconst { createPortal } = reconciler\n\nexport type Root = {\n  render: (node: ReactNode) => void\n  unmount: () => void\n}\n\n/**\n * Creates a root for rendering a React tree with the given CLI renderer.\n * @param renderer The CLI renderer to use\n * @returns A root object with a `render` method\n * @example\n * ```tsx\n * const renderer = await createCliRenderer()\n * createRoot(renderer).render(<App />)\n * ```\n */\nexport function createRoot(renderer: CliRenderer): Root {\n  let container: OpaqueRoot | null = null\n\n  const cleanup = () => {\n    if (container) {\n      reconciler.updateContainer(null, container, null, () => {})\n      // @ts-expect-error the types for `react-reconciler` are not up to date with the library.\n      reconciler.flushSyncWork()\n      container = null\n    }\n  }\n\n  renderer.once(CliRenderEvents.DESTROY, cleanup)\n\n  return {\n    render: (node: ReactNode) => {\n      engine.attach(renderer)\n\n      container = _render(\n        React.createElement(\n          AppContext.Provider,\n          { value: { keyHandler: renderer.keyInput, renderer } },\n          React.createElement(ErrorBoundary, null, node),\n        ),\n        renderer.root,\n      )\n    },\n\n    unmount: cleanup,\n  }\n}\n\nexport { createPortal, flushSync }\n"
  },
  {
    "path": "packages/react/src/test-utils.ts",
    "content": "import { createTestRenderer, type TestRendererOptions } from \"@opentui/core/testing\"\nimport { act, type ReactNode } from \"react\"\nimport { createRoot, type Root } from \"./reconciler/renderer.js\"\n\nfunction setIsReactActEnvironment(isReactActEnvironment: boolean) {\n  // @ts-expect-error - this is a test environment\n  globalThis.IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment\n}\n\nexport async function testRender(node: ReactNode, testRendererOptions: TestRendererOptions) {\n  let root: Root | null = null\n  setIsReactActEnvironment(true)\n\n  const testSetup = await createTestRenderer({\n    ...testRendererOptions,\n    onDestroy() {\n      act(() => {\n        if (root) {\n          root.unmount()\n          root = null\n        }\n      })\n      testRendererOptions.onDestroy?.()\n      setIsReactActEnvironment(false)\n    },\n  })\n\n  root = createRoot(testSetup.renderer)\n  act(() => {\n    if (root) {\n      root.render(node)\n    }\n  })\n\n  return testSetup\n}\n"
  },
  {
    "path": "packages/react/src/time-to-first-draw.tsx",
    "content": "import { TimeToFirstDrawRenderable } from \"@opentui/core\"\nimport { createElement } from \"react\"\nimport { extend } from \"./components\"\nimport type { ExtendedComponentProps } from \"./types/components\"\n\ndeclare module \"@opentui/react\" {\n  interface OpenTUIComponents {\n    \"time-to-first-draw\": typeof TimeToFirstDrawRenderable\n  }\n}\n\nextend({ \"time-to-first-draw\": TimeToFirstDrawRenderable })\n\nexport type TimeToFirstDrawProps = ExtendedComponentProps<typeof TimeToFirstDrawRenderable>\n\nexport const TimeToFirstDraw = (props: TimeToFirstDrawProps) => {\n  return createElement(\"time-to-first-draw\", props)\n}\n"
  },
  {
    "path": "packages/react/src/types/components.ts",
    "content": "import type {\n  ASCIIFontOptions,\n  ASCIIFontRenderable,\n  BaseRenderable,\n  BoxOptions,\n  BoxRenderable,\n  CodeOptions,\n  CodeRenderable,\n  DiffRenderable,\n  DiffRenderableOptions,\n  InputRenderable,\n  InputRenderableOptions,\n  LineNumberOptions,\n  LineNumberRenderable,\n  MarkdownOptions,\n  MarkdownRenderable,\n  RenderableOptions,\n  RenderContext,\n  ScrollBoxOptions,\n  ScrollBoxRenderable,\n  SelectOption,\n  SelectRenderable,\n  SelectRenderableOptions,\n  TabSelectOption,\n  TabSelectRenderable,\n  TabSelectRenderableOptions,\n  TextareaOptions,\n  TextareaRenderable,\n  TextNodeOptions,\n  TextNodeRenderable,\n  TextOptions,\n  TextRenderable,\n} from \"@opentui/core\"\nimport type React from \"react\"\n\n// ============================================================================\n// Core Type System\n// ============================================================================\n\n/** Properties that should not be included in the style prop */\nexport type NonStyledProps =\n  | \"id\"\n  | \"buffered\"\n  | \"live\"\n  | \"enableLayout\"\n  | \"selectable\"\n  | \"renderAfter\"\n  | \"renderBefore\"\n  | `on${string}`\n\n/** React-specific props for all components */\nexport type ReactProps<TRenderable = unknown> = {\n  key?: React.Key\n  ref?: React.Ref<TRenderable>\n}\n\n/** Base type for any renderable constructor */\nexport type RenderableConstructor<TRenderable extends BaseRenderable = BaseRenderable> = new (\n  ctx: RenderContext,\n  options: any,\n) => TRenderable\n\n/** Extract the options type from a renderable constructor */\ntype ExtractRenderableOptions<TConstructor> = TConstructor extends new (\n  ctx: RenderContext,\n  options: infer TOptions,\n) => any\n  ? TOptions\n  : never\n\n/** Extract the renderable type from a constructor */\ntype ExtractRenderable<TConstructor> = TConstructor extends new (ctx: RenderContext, options: any) => infer TRenderable\n  ? TRenderable\n  : never\n\n/** Determine which properties should be excluded from styling for different renderable types */\nexport type GetNonStyledProperties<TConstructor> =\n  TConstructor extends RenderableConstructor<TextRenderable>\n    ? NonStyledProps | \"content\"\n    : TConstructor extends RenderableConstructor<BoxRenderable>\n      ? NonStyledProps | \"title\"\n      : TConstructor extends RenderableConstructor<ASCIIFontRenderable>\n        ? NonStyledProps | \"text\" | \"selectable\"\n        : TConstructor extends RenderableConstructor<InputRenderable>\n          ? NonStyledProps | \"placeholder\" | \"value\"\n          : TConstructor extends RenderableConstructor<TextareaRenderable>\n            ? NonStyledProps | \"placeholder\" | \"initialValue\"\n            : TConstructor extends RenderableConstructor<CodeRenderable>\n              ?\n                  | NonStyledProps\n                  | \"content\"\n                  | \"filetype\"\n                  | \"syntaxStyle\"\n                  | \"treeSitterClient\"\n                  | \"conceal\"\n                  | \"drawUnstyledText\"\n              : TConstructor extends RenderableConstructor<MarkdownRenderable>\n                ? NonStyledProps | \"content\" | \"syntaxStyle\" | \"treeSitterClient\" | \"conceal\" | \"renderNode\"\n                : NonStyledProps\n\n// ============================================================================\n// Component Props System\n// ============================================================================\n\n/** Base props for container components that accept children */\ntype ContainerProps<TOptions> = TOptions & { children?: React.ReactNode }\n\n/** Smart component props that automatically determine excluded properties */\ntype ComponentProps<TOptions extends RenderableOptions<TRenderable>, TRenderable extends BaseRenderable> = TOptions & {\n  style?: Partial<Omit<TOptions, GetNonStyledProperties<RenderableConstructor<TRenderable>>>>\n} & ReactProps<TRenderable>\n\n/** Valid text content types for Text component children */\ntype TextChildren = string | number | boolean | null | undefined | React.ReactNode\n\n// ============================================================================\n// Built-in Component Props\n// ============================================================================\n\nexport type TextProps = ComponentProps<TextOptions, TextRenderable> & {\n  children?: TextChildren\n}\n\nexport type SpanProps = ComponentProps<TextNodeOptions, TextNodeRenderable> & {\n  children?: TextChildren\n}\n\nexport type LinkProps = SpanProps & {\n  href: string\n}\n\nexport type LineBreakProps = Pick<SpanProps, \"id\">\n\nexport type BoxProps = ComponentProps<ContainerProps<BoxOptions>, BoxRenderable> & {\n  focused?: boolean\n}\n\nexport type InputProps = ComponentProps<InputRenderableOptions, InputRenderable> & {\n  focused?: boolean\n  onInput?: (value: string) => void\n  onChange?: (value: string) => void\n  onSubmit?: (value: string) => void\n}\n\nexport type TextareaProps = ComponentProps<TextareaOptions, TextareaRenderable> & {\n  focused?: boolean\n}\n\nexport type CodeProps = ComponentProps<CodeOptions, CodeRenderable>\n\nexport type MarkdownProps = ComponentProps<MarkdownOptions, MarkdownRenderable>\n\nexport type DiffProps = ComponentProps<DiffRenderableOptions, DiffRenderable>\n\nexport type SelectProps = ComponentProps<SelectRenderableOptions, SelectRenderable> & {\n  focused?: boolean\n  onChange?: (index: number, option: SelectOption | null) => void\n  onSelect?: (index: number, option: SelectOption | null) => void\n}\n\nexport type ScrollBoxProps = ComponentProps<ContainerProps<ScrollBoxOptions>, ScrollBoxRenderable> & {\n  focused?: boolean\n}\n\nexport type AsciiFontProps = ComponentProps<ASCIIFontOptions, ASCIIFontRenderable>\n\nexport type TabSelectProps = ComponentProps<TabSelectRenderableOptions, TabSelectRenderable> & {\n  focused?: boolean\n  onChange?: (index: number, option: TabSelectOption | null) => void\n  onSelect?: (index: number, option: TabSelectOption | null) => void\n}\n\nexport type LineNumberProps = ComponentProps<ContainerProps<LineNumberOptions>, LineNumberRenderable> & {\n  focused?: boolean\n}\n\n// ============================================================================\n// Extended/Dynamic Component System\n// ============================================================================\n\n/** Convert renderable constructor to component props with proper style exclusions */\nexport type ExtendedComponentProps<\n  TConstructor extends RenderableConstructor,\n  TOptions = ExtractRenderableOptions<TConstructor>,\n> = TOptions & {\n  children?: React.ReactNode\n  style?: Partial<Omit<TOptions, GetNonStyledProperties<TConstructor>>>\n} & ReactProps<ExtractRenderable<TConstructor>>\n\n/** Helper type to create JSX element properties from a component catalogue */\nexport type ExtendedIntrinsicElements<TComponentCatalogue extends Record<string, RenderableConstructor>> = {\n  [TComponentName in keyof TComponentCatalogue]: ExtendedComponentProps<TComponentCatalogue[TComponentName]>\n}\n\n/**\n * Global augmentation interface for extended components\n * This will be augmented by user code using module augmentation\n */\nexport interface OpenTUIComponents {\n  [componentName: string]: RenderableConstructor\n}\n\n// Note: JSX.IntrinsicElements extension is handled in jsx-namespace.d.ts\n"
  },
  {
    "path": "packages/react/src/types/host.ts",
    "content": "import type { BaseRenderable, RootRenderable, TextNodeRenderable } from \"@opentui/core\"\nimport { baseComponents } from \"../components/index.js\"\n\nexport type Type = keyof typeof baseComponents\nexport type Props = Record<string, any>\nexport type Container = RootRenderable\nexport type Instance = BaseRenderable\nexport type TextInstance = TextNodeRenderable\nexport type PublicInstance = Instance\nexport type HostContext = Record<string, any> & { isInsideText?: boolean }\n"
  },
  {
    "path": "packages/react/src/utils/id.ts",
    "content": "import type { Type } from \"../types/host.js\"\n\nconst idCounter = new Map<string, number>()\n\nexport function getNextId(type: Type): string {\n  if (!idCounter.has(type)) {\n    idCounter.set(type, 0)\n  }\n\n  const value = idCounter.get(type)! + 1\n  idCounter.set(type, value)\n\n  return `${type}-${value}`\n}\n"
  },
  {
    "path": "packages/react/src/utils/index.ts",
    "content": "import {\n  InputRenderable,\n  InputRenderableEvents,\n  isRenderable,\n  SelectRenderable,\n  SelectRenderableEvents,\n  TabSelectRenderable,\n  TabSelectRenderableEvents,\n} from \"@opentui/core\"\nimport type { Instance, Props, Type } from \"../types/host.js\"\n\nfunction initEventListeners(instance: Instance, eventName: string, listener: any, previousListener?: any) {\n  if (previousListener) {\n    instance.off(eventName, previousListener)\n  }\n\n  if (listener) {\n    instance.on(eventName, listener)\n  }\n}\n\nfunction setStyle(instance: Instance, styles: any, oldStyles: any) {\n  if (oldStyles != null && typeof oldStyles === \"object\") {\n    for (const styleName in oldStyles) {\n      if (oldStyles.hasOwnProperty(styleName)) {\n        if (styles == null || !styles.hasOwnProperty(styleName)) {\n          // @ts-expect-error props are not strongly typed in the reconciler\n          instance[styleName] = null\n        }\n      }\n    }\n  }\n\n  if (styles != null && typeof styles === \"object\") {\n    for (const styleName in styles) {\n      if (styles.hasOwnProperty(styleName)) {\n        const value = styles[styleName]\n        const oldValue = oldStyles?.[styleName]\n        if (value !== oldValue) {\n          // @ts-expect-error props are not strongly typed in the reconciler\n          instance[styleName] = value\n        }\n      }\n    }\n  }\n}\n\nfunction setProperty(instance: Instance, type: Type, propKey: string, propValue: any, oldPropValue?: any) {\n  switch (propKey) {\n    case \"onChange\":\n      if (instance instanceof InputRenderable) {\n        initEventListeners(instance, InputRenderableEvents.CHANGE, propValue, oldPropValue)\n      } else if (instance instanceof SelectRenderable) {\n        initEventListeners(instance, SelectRenderableEvents.SELECTION_CHANGED, propValue, oldPropValue)\n      } else if (instance instanceof TabSelectRenderable) {\n        initEventListeners(instance, TabSelectRenderableEvents.SELECTION_CHANGED, propValue, oldPropValue)\n      }\n      break\n    case \"onInput\":\n      if (instance instanceof InputRenderable) {\n        initEventListeners(instance, InputRenderableEvents.INPUT, propValue, oldPropValue)\n      }\n      break\n    case \"onSubmit\":\n      if (instance instanceof InputRenderable) {\n        initEventListeners(instance, InputRenderableEvents.ENTER, propValue, oldPropValue)\n      }\n      break\n    case \"onSelect\":\n      if (instance instanceof SelectRenderable) {\n        initEventListeners(instance, SelectRenderableEvents.ITEM_SELECTED, propValue, oldPropValue)\n      } else if (instance instanceof TabSelectRenderable) {\n        initEventListeners(instance, TabSelectRenderableEvents.ITEM_SELECTED, propValue, oldPropValue)\n      }\n      break\n    case \"focused\":\n      if (isRenderable(instance)) {\n        if (!!propValue) {\n          instance.focus()\n        } else {\n          instance.blur()\n        }\n      }\n      break\n    case \"style\":\n      setStyle(instance, propValue, oldPropValue)\n      break\n    case \"children\":\n      // Skip children handling - React reconciler handles this automatically\n      break\n    default:\n      // @ts-expect-error props are not strongly typed in the reconciler, so we need to allow dynamic property access\n      instance[propKey] = propValue\n  }\n}\n\nexport function setInitialProperties(instance: Instance, type: Type, props: Props) {\n  for (const propKey in props) {\n    if (!props.hasOwnProperty(propKey)) {\n      continue\n    }\n\n    const propValue = props[propKey]\n    if (propValue == null) {\n      continue\n    }\n\n    setProperty(instance, type, propKey, propValue)\n  }\n}\n\nexport function updateProperties(instance: Instance, type: Type, oldProps: Props, newProps: Props) {\n  for (const propKey in oldProps) {\n    const oldProp = oldProps[propKey]\n    if (oldProps.hasOwnProperty(propKey) && oldProp != null && !newProps.hasOwnProperty(propKey)) {\n      setProperty(instance, type, propKey, null, oldProp)\n    }\n  }\n\n  for (const propKey in newProps) {\n    const newProp = newProps[propKey]\n    const oldProp = oldProps[propKey]\n\n    if (newProps.hasOwnProperty(propKey) && newProp !== oldProp && (newProp != null || oldProp != null)) {\n      setProperty(instance, type, propKey, newProp, oldProp)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/react/tests/__snapshots__/layout.test.tsx.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`React Renderer | Layout Tests Basic Text Rendering should render simple text correctly 1`] = `\n\"Hello World         \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`React Renderer | Layout Tests Basic Text Rendering should render multiline text correctly 1`] = `\n\"Line 1         \nLine 2         \nLine 3         \n               \n               \n\"\n`;\n\nexports[`React Renderer | Layout Tests Basic Text Rendering should render text with dynamic content 1`] = `\n\"Counter: 42         \n                    \n                    \n\"\n`;\n\nexports[`React Renderer | Layout Tests Box Layout Rendering should render basic box layout correctly 1`] = `\n\"┌──────────────────┐     \n│Inside Box        │     \n│                  │     \n│                  │     \n└──────────────────┘     \n                         \n                         \n                         \n\"\n`;\n\nexports[`React Renderer | Layout Tests Box Layout Rendering should render nested boxes correctly 1`] = `\n\"┌─Parent Box─────────────────┐     \n│                            │     \n│                            │     \n│  ┌────────┐                │     \n│  │Nested  │                │     \n│  └────────┘                │     \n│               Sibling      │     \n│                            │     \n│                            │     \n└────────────────────────────┘     \n                                   \n                                   \n\"\n`;\n\nexports[`React Renderer | Layout Tests Box Layout Rendering should render absolute positioned boxes 1`] = `\n\"┌────────┐               \n│Box 1   │               \n└────────┘  ┌────────┐   \n            │Box 2   │   \n            └────────┘   \n                         \n                         \n                         \n\"\n`;\n\nexports[`React Renderer | Layout Tests Box Layout Rendering should auto-enable border when borderStyle is set 1`] = `\n\"┌──────────────────┐     \n│With Border       │     \n│                  │     \n│                  │     \n└──────────────────┘     \n                         \n                         \n                         \n\"\n`;\n\nexports[`React Renderer | Layout Tests Box Layout Rendering should auto-enable border when borderColor is set 1`] = `\n\"┌──────────────────┐     \n│Colored Border    │     \n│                  │     \n│                  │     \n└──────────────────┘     \n                         \n                         \n                         \n\"\n`;\n\nexports[`React Renderer | Layout Tests Box Layout Rendering should auto-enable border when focusedBorderColor is set 1`] = `\n\"┌──────────────────┐     \n│Focused Border    │     \n│                  │     \n│                  │     \n└──────────────────┘     \n                         \n                         \n                         \n\"\n`;\n\nexports[`React Renderer | Layout Tests Complex Layouts should render complex nested layout correctly 1`] = `\n\"┌─Complex Layout───────────────────────┐     \n│  ┌─────────────┐                     │     \n│  │Header Sectio│                     │     \n│  │Menu Item 1  │                     │     \n│  │Menu Item 2  │                     │     \n│  └─────────────┘                     │     \n│                  ┌────────────────┐  │     \n│                  │Content Area    │  │     \n│                  │Some content her│  │     \n│                  │More content    │  │     \n│                  │Footer text     │  │     \n│                  │                │  │     \n│                  │                │  │     \n│                  └────────────────┘  │     \n│  Status: Ready                       │     \n└──────────────────────────────────────┘     \n                                             \n                                             \n\"\n`;\n\nexports[`React Renderer | Layout Tests Complex Layouts should render text with mixed styling and layout 1`] = `\n\"┌─────────────────────────────────┐     \n│ERROR: Something went wrong      │     \n│WARNING: Check your settings     │     \n│SUCCESS: All systems operational │     \n│                                 │     \n│                                 │     \n│                                 │     \n└─────────────────────────────────┘     \n                                        \n                                        \n\"\n`;\n\nexports[`React Renderer | Layout Tests Complex Layouts should render scrollbox with sticky scroll and spacer 1`] = `\n\"┌─scroll area────────────────┐\n│                            │\n│┌─hi───────────────────────┐│\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n│└──────────────────────────┘│\n│                            │\n│                            │\n└────────────────────────────┘\n┌─spacer─────────────────────┐\n│spacer                      │\n│                            │\n│                            │\n│                            │\n│                            │\n│                            │\n│                            │\n│                            │\n└────────────────────────────┘\n\"\n`;\n\nexports[`React Renderer | Layout Tests Empty and Edge Cases should handle empty component 1`] = `\n\"          \n          \n          \n          \n          \n\"\n`;\n\nexports[`React Renderer | Layout Tests Empty and Edge Cases should handle component with no children 1`] = `\n\"               \n               \n               \n               \n               \n               \n               \n               \n\"\n`;\n\nexports[`React Renderer | Layout Tests Empty and Edge Cases should handle very small dimensions 1`] = `\n\"Hi   \n     \n     \n\"\n`;\n"
  },
  {
    "path": "packages/react/tests/destroy-crash.test.tsx",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport React, { useEffect, useState } from \"react\"\nimport { createTestRenderer } from \"@opentui/core/testing\"\nimport { createRoot } from \"../src/reconciler/renderer.js\"\n\n/**\n * Regression test for: Native Yoga crash when renderer.destroy() is called\n * with pending React state updates.\n *\n * Bug report: https://gist.github.com/rauchg/b2e1e964f88773a5d08f5f682dce2224\n *\n * Issue: When calling renderer.destroy() while async operations (intervals,\n * promises, etc.) are still updating React state, Yoga crashes with:\n * \"Cannot add child: Nodes with measure functions cannot have children.\"\n * followed by \"RuntimeError: Out of bounds memory access\"\n *\n * The crash happens because:\n * 1. renderer.destroy() is called (WITHOUT unmounting React first)\n * 2. This calls root.destroyRecursively() which calls yogaNode.free() on all nodes\n * 3. The interval keeps firing setLines() because React wasn't unmounted\n * 4. React tries to re-render, calling appendChild() which calls add()\n * 5. add() calls yogaNode.insertChild() on a freed (parent) yoga node\n * 6. Yoga WASM crashes with out-of-bounds memory access\n *\n * The \"Cannot add child: Nodes with measure functions cannot have children\"\n * error message is misleading - it's actually a use-after-free crash where\n * the freed yoga node's memory has been reused/corrupted.\n *\n * CRITICAL: The bug only occurs when React is NOT unmounted before/during destroy.\n * In the bug report, there is NO root.unmount() call - just renderer.destroy().\n */\n\ndescribe(\"Renderer Destroy Crash with Pending React Updates\", () => {\n  it(\"should not crash when renderer is destroyed without unmounting React while interval updates state\", async () => {\n    // This test reproduces the EXACT scenario from the bug report:\n    // - Component has an interval updating state\n    // - renderer.destroy() is called WITHOUT calling root.unmount()\n    // - The interval continues to fire setLines() AFTER destroy\n    // - React tries to re-render on destroyed Yoga nodes -> CRASH\n\n    const testSetup = await createTestRenderer({\n      width: 40,\n      height: 20,\n      // CRITICAL: NO onDestroy callback - React is NOT unmounted\n      // This is exactly what happens in the bug report\n    })\n\n    const root = createRoot(testSetup.renderer)\n\n    function App() {\n      const [lines, setLines] = useState<string[]>([])\n\n      useEffect(() => {\n        // Interval keeps firing after destroy() because React isn't unmounted\n        const interval = setInterval(() => {\n          setLines((prev) => [...prev, `Line ${prev.length + 1}`])\n        }, 5)\n\n        return () => clearInterval(interval)\n      }, [])\n\n      return (\n        <box flexDirection=\"column\" border borderStyle=\"single\">\n          <text bold>OpenTUI Crash Repro</text>\n          {lines.slice(-10).map((line, i) => (\n            <text key={`line-${i}-${line}`}>{line}</text>\n          ))}\n        </box>\n      )\n    }\n\n    root.render(<App />)\n\n    // Let the component mount and interval start\n    await testSetup.renderOnce()\n    await Bun.sleep(30)\n    await testSetup.renderOnce()\n\n    // Destroy WITHOUT unmounting React - this is the bug!\n    // The interval will keep firing setLines() after this\n    // React will try to add new <text> elements to destroyed Yoga nodes\n    testSetup.renderer.destroy()\n\n    // Wait for interval to fire more updates after destroy\n    // This is when the crash occurs if the bug is present\n    await Bun.sleep(100)\n\n    // If we reach here without crashing, the bug is fixed\n    expect(true).toBe(true)\n  })\n})\n"
  },
  {
    "path": "packages/react/tests/layout.test.tsx",
    "content": "import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from \"bun:test\"\nimport { useState } from \"react\"\nimport { act } from \"react\"\nimport { testRender } from \"../src/test-utils.js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"React Renderer | Layout Tests\", () => {\n  let originalConsoleError: (...args: any[]) => void\n\n  beforeAll(() => {\n    originalConsoleError = console.error\n    console.error = mock(() => {})\n  })\n\n  afterAll(() => {\n    console.error = originalConsoleError\n  })\n\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  describe(\"Basic Text Rendering\", () => {\n    it(\"should render simple text correctly\", async () => {\n      testSetup = await testRender(<text>Hello World</text>, {\n        width: 20,\n        height: 5,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render multiline text correctly\", async () => {\n      testSetup = await testRender(\n        <text>\n          Line 1\n          <br />\n          Line 2\n          <br />\n          Line 3\n        </text>,\n        {\n          width: 15,\n          height: 5,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should catch and display error when rendering text without parent <text> element\", async () => {\n      testSetup = await testRender(<box>This text is not wrapped in a text element</box>, {\n        width: 60,\n        height: 15,\n      })\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Error:\")\n      expect(frame).toContain(\"Text must be created inside of a text node\")\n      expect(frame).not.toContain(\"This text is not wrapped in a text element\")\n    })\n\n    it(\"should catch and display error when rendering span without parent <text> element\", async () => {\n      testSetup = await testRender(\n        <box>\n          <span>This text is not wrapped in a text element</span>\n        </box>,\n        { width: 100, height: 15 },\n      )\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Error:\")\n      expect(frame).toContain('Component of type \"span\" must be created inside of a text node')\n      expect(frame).not.toContain(\"This text is not wrapped in a text element\")\n    })\n\n    it(\"should render text with dynamic content\", async () => {\n      const counter = () => 42\n\n      testSetup = await testRender(<text>Counter: {counter()}</text>, {\n        width: 20,\n        height: 3,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Box Layout Rendering\", () => {\n    it(\"should render basic box layout correctly\", async () => {\n      testSetup = await testRender(\n        <box style={{ width: 20, height: 5, border: true }}>\n          <text>Inside Box</text>\n        </box>,\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render nested boxes correctly\", async () => {\n      testSetup = await testRender(\n        <box style={{ width: 30, height: 10, border: true }} title=\"Parent Box\">\n          <box style={{ left: 2, top: 2, width: 10, height: 3, border: true }}>\n            <text>Nested</text>\n          </box>\n          <text style={{ left: 15, top: 2 }}>Sibling</text>\n        </box>,\n        {\n          width: 35,\n          height: 12,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render absolute positioned boxes\", async () => {\n      testSetup = await testRender(\n        <>\n          <box\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              width: 10,\n              height: 3,\n              border: true,\n              backgroundColor: \"red\",\n            }}\n          >\n            <text>Box 1</text>\n          </box>\n          <box\n            style={{\n              position: \"absolute\",\n              left: 12,\n              top: 2,\n              width: 10,\n              height: 3,\n              border: true,\n              backgroundColor: \"blue\",\n            }}\n          >\n            <text>Box 2</text>\n          </box>\n        </>,\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should auto-enable border when borderStyle is set\", async () => {\n      testSetup = await testRender(\n        <box style={{ width: 20, height: 5 }} borderStyle=\"single\">\n          <text>With Border</text>\n        </box>,\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should auto-enable border when borderColor is set\", async () => {\n      testSetup = await testRender(\n        <box style={{ width: 20, height: 5 }} borderColor=\"cyan\">\n          <text>Colored Border</text>\n        </box>,\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should auto-enable border when focusedBorderColor is set\", async () => {\n      testSetup = await testRender(\n        <box style={{ width: 20, height: 5 }} focusedBorderColor=\"yellow\">\n          <text>Focused Border</text>\n        </box>,\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should support focusable prop and controlled focus state\", async () => {\n      let boxRef: any\n      let setFocused: (value: boolean) => void\n\n      function TestComponent() {\n        const [focused, _setFocused] = useState(false)\n        setFocused = _setFocused\n\n        return (\n          <box\n            ref={(r) => {\n              boxRef = r\n            }}\n            focusable\n            focused={focused}\n            style={{ width: 10, height: 5, border: true }}\n          />\n        )\n      }\n\n      testSetup = await testRender(<TestComponent />, {\n        width: 15,\n        height: 8,\n      })\n\n      await testSetup.renderOnce()\n\n      expect(boxRef.focusable).toBe(true)\n      expect(boxRef.focused).toBe(false)\n\n      act(() => {\n        setFocused(true)\n      })\n      await testSetup.renderOnce()\n\n      expect(boxRef.focused).toBe(true)\n\n      act(() => {\n        setFocused(false)\n      })\n      await testSetup.renderOnce()\n\n      expect(boxRef.focused).toBe(false)\n    })\n  })\n\n  // describe(\"Reactive Updates\", () => {\n  //   it(\"should handle reactive state changes\", async () => {\n  //     const [counter, setCounter] = createSignal(0)\n\n  //     testSetup = await testRender(<text>Counter: {counter()}</text>, {\n  //       width: 15,\n  //       height: 3,\n  //     })\n\n  //     await testSetup.renderOnce()\n  //     const initialFrame = testSetup.captureCharFrame()\n\n  //     setCounter(5)\n  //     await testSetup.renderOnce()\n  //     const updatedFrame = testSetup.captureCharFrame()\n\n  //     expect(initialFrame).toMatchSnapshot()\n  //     expect(updatedFrame).toMatchSnapshot()\n  //     expect(updatedFrame).not.toBe(initialFrame)\n  //   })\n\n  //   it(\"should handle conditional rendering\", async () => {\n  //     const [showText, setShowText] = createSignal(true)\n\n  //     testSetup = await testRender(\n  //       () => (\n  //         <text wrapMode=\"none\">\n  //           Always visible\n  //           <Show when={showText()} fallback=\"\">\n  //             {\" - Conditional text\"}\n  //           </Show>\n  //         </text>\n  //       ),\n  //       {\n  //         width: 30,\n  //         height: 3,\n  //       },\n  //     )\n\n  //     await testSetup.renderOnce()\n  //     const visibleFrame = testSetup.captureCharFrame()\n\n  //     setShowText(false)\n  //     await testSetup.renderOnce()\n  //     const hiddenFrame = testSetup.captureCharFrame()\n\n  //     expect(visibleFrame).toMatchSnapshot()\n  //     expect(hiddenFrame).toMatchSnapshot()\n  //     expect(hiddenFrame).not.toBe(visibleFrame)\n  //   })\n  // })\n\n  describe(\"Complex Layouts\", () => {\n    it(\"should render complex nested layout correctly\", async () => {\n      testSetup = await testRender(\n        <box style={{ width: 40, border: true }} title=\"Complex Layout\">\n          <box style={{ left: 2, width: 15, height: 5, border: true, backgroundColor: \"#333\" }}>\n            <text wrapMode=\"none\" style={{ fg: \"cyan\" }}>\n              Header Section\n            </text>\n            <text wrapMode=\"none\" style={{ fg: \"yellow\" }}>\n              Menu Item 1\n            </text>\n            <text wrapMode=\"none\" style={{ fg: \"yellow\" }}>\n              Menu Item 2\n            </text>\n          </box>\n          <box style={{ left: 18, width: 18, height: 8, border: true, backgroundColor: \"#222\" }}>\n            <text wrapMode=\"none\" style={{ fg: \"green\" }}>\n              Content Area\n            </text>\n            <text wrapMode=\"none\" style={{ fg: \"white\" }}>\n              Some content here\n            </text>\n            <text wrapMode=\"none\" style={{ fg: \"white\" }}>\n              More content\n            </text>\n            <text wrapMode=\"none\" style={{ fg: \"magenta\" }}>\n              Footer text\n            </text>\n          </box>\n          <text style={{ left: 2, fg: \"gray\" }}>Status: Ready</text>\n        </box>,\n        {\n          width: 45,\n          height: 18,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text with mixed styling and layout\", async () => {\n      testSetup = await testRender(\n        <box style={{ width: 35, height: 8, border: true }}>\n          <text>\n            <span style={{ fg: \"red\", bold: true }}>ERROR:</span> Something went wrong\n          </text>\n          <text>\n            <span style={{ fg: \"yellow\" }}>WARNING:</span> Check your settings\n          </text>\n          <text>\n            <span style={{ fg: \"green\" }}>SUCCESS:</span> All systems operational\n          </text>\n        </box>,\n        {\n          width: 40,\n          height: 10,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render scrollbox with sticky scroll and spacer\", async () => {\n      testSetup = await testRender(\n        <box maxHeight={\"100%\"} maxWidth={\"100%\"}>\n          <scrollbox\n            scrollbarOptions={{ visible: false }}\n            stickyScroll={true}\n            stickyStart=\"bottom\"\n            paddingTop={1}\n            paddingBottom={1}\n            title=\"scroll area\"\n            rootOptions={{\n              flexGrow: 0,\n            }}\n            border\n          >\n            <box border height={10} title=\"hi\" />\n          </scrollbox>\n          <box border height={10} title=\"spacer\" flexShrink={0}>\n            <text>spacer</text>\n          </box>\n        </box>,\n        {\n          width: 30,\n          height: 25,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should clip nested scrollbox content (React) [issue #388]\", async () => {\n      const innerLines = Array.from({ length: 12 }, (_, i) => `LEAK-${i}`)\n\n      testSetup = await testRender(\n        <box style={{ width: 50, height: 18, flexDirection: \"column\", border: true, gap: 0 }}>\n          <text>HEADER</text>\n          <scrollbox\n            id=\"outer-scroll\"\n            style={{\n              width: 48,\n              height: 12,\n              border: true,\n              overflow: \"hidden\",\n              paddingTop: 0,\n              paddingBottom: 0,\n              paddingLeft: 0,\n              paddingRight: 0,\n            }}\n            scrollY\n          >\n            <scrollbox\n              id=\"inner-scroll\"\n              style={{\n                width: 44,\n                height: 6,\n                border: true,\n                overflow: \"hidden\",\n                paddingTop: 0,\n                paddingBottom: 0,\n                paddingLeft: 0,\n                paddingRight: 0,\n              }}\n              scrollY\n            >\n              {innerLines.map((line) => (\n                <text key={line}>{line}</text>\n              ))}\n            </scrollbox>\n          </scrollbox>\n          <text>FOOTER</text>\n        </box>,\n        {\n          width: 52,\n          height: 20,\n        },\n      )\n\n      await testSetup.renderOnce()\n\n      const outer = testSetup.renderer.root.findDescendantById?.(\"outer-scroll\") as any\n      const inner = testSetup.renderer.root.findDescendantById?.(\"inner-scroll\") as any\n      // Force both scrollboxes to scroll to exercise translation + clipping\n      if (inner && typeof inner.scrollTo === \"function\") {\n        inner.scrollTo({ x: 0, y: 100 })\n      }\n      if (outer && typeof outer.scrollTo === \"function\") {\n        outer.scrollTo({ x: 0, y: 50 })\n      }\n      await testSetup.renderOnce()\n\n      const frame = testSetup.captureCharFrame()\n      const visibleLeakLines = frame.split(\"\\n\").filter((line) => line.includes(\"LEAK-\"))\n\n      // The inner viewport height is 4 (6 minus 2 for borders). Currently, the renderer leaks and shows more.\n      expect(visibleLeakLines.length).toBeLessThanOrEqual(4)\n\n      // Ensure header/footer are still present for context\n      expect(frame).toContain(\"HEADER\")\n      expect(frame).toContain(\"FOOTER\")\n    })\n  })\n\n  describe(\"Empty and Edge Cases\", () => {\n    it(\"should handle empty component\", async () => {\n      testSetup = await testRender(<></>, {\n        width: 10,\n        height: 5,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle component with no children\", async () => {\n      testSetup = await testRender(<box style={{ width: 10, height: 5 }} />, {\n        width: 15,\n        height: 8,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle very small dimensions\", async () => {\n      testSetup = await testRender(<text>Hi</text>, {\n        width: 5,\n        height: 3,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Layout Property Reset on Component Change (Issue #391)\", () => {\n    it(\"should reset alignItems when conditionally switching components\", async () => {\n      let setToggle: (value: boolean) => void\n\n      function TestComponent() {\n        const [toggle, _setToggle] = useState(false)\n        setToggle = _setToggle\n\n        if (!toggle) {\n          return (\n            <box alignItems=\"center\" width={40} height={3}>\n              <text>Centered</text>\n            </box>\n          )\n        }\n        return (\n          <box width={40} height={3}>\n            <text>Default</text>\n          </box>\n        )\n      }\n\n      testSetup = await testRender(<TestComponent />, { width: 40, height: 5 })\n\n      await testSetup.renderOnce()\n      const centeredFrame = testSetup.captureCharFrame()\n      const centeredLines = centeredFrame.split(\"\\n\")\n      const centeredTextLine = centeredLines.find((line) => line.includes(\"Centered\"))\n      expect(centeredTextLine).toBeDefined()\n      expect(centeredTextLine!.trimStart()).not.toBe(centeredTextLine)\n\n      act(() => {\n        setToggle(true)\n      })\n      await testSetup.renderOnce()\n      const defaultFrame = testSetup.captureCharFrame()\n      const defaultLines = defaultFrame.split(\"\\n\")\n      const defaultTextLine = defaultLines.find((line) => line.includes(\"Default\"))\n      expect(defaultTextLine).toBeDefined()\n      expect(defaultTextLine!.indexOf(\"Default\")).toBe(0)\n    })\n\n    it(\"should use default alignment when alignItems is not specified\", async () => {\n      testSetup = await testRender(\n        <box width={40} height={3}>\n          <text>Left aligned</text>\n        </box>,\n        {\n          width: 40,\n          height: 5,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      const lines = frame.split(\"\\n\")\n      const textLine = lines.find((line) => line.includes(\"Left aligned\"))\n      expect(textLine).toBeDefined()\n      expect(textLine!.indexOf(\"Left aligned\")).toBe(0)\n    })\n\n    it(\"should reset alignItems when removed from style prop\", async () => {\n      let setStyle: (style: Record<string, string>) => void\n\n      function TestComponent() {\n        const [style, _setStyle] = useState<Record<string, string>>({ alignItems: \"center\" })\n        setStyle = _setStyle\n\n        return (\n          <box style={style} width={40} height={3}>\n            <text>Test</text>\n          </box>\n        )\n      }\n\n      testSetup = await testRender(<TestComponent />, { width: 40, height: 5 })\n\n      await testSetup.renderOnce()\n      const centeredFrame = testSetup.captureCharFrame()\n      const centeredLines = centeredFrame.split(\"\\n\")\n      const centeredTextLine = centeredLines.find((line) => line.includes(\"Test\"))\n      expect(centeredTextLine).toBeDefined()\n      expect(centeredTextLine!.trimStart()).not.toBe(centeredTextLine)\n\n      act(() => {\n        setStyle({})\n      })\n      await testSetup.renderOnce()\n      const defaultFrame = testSetup.captureCharFrame()\n      const defaultLines = defaultFrame.split(\"\\n\")\n      const defaultTextLine = defaultLines.find((line) => line.includes(\"Test\"))\n      expect(defaultTextLine).toBeDefined()\n      expect(defaultTextLine!.indexOf(\"Test\")).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/react/tests/link.test.tsx",
    "content": "import { describe, expect, test, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../src/test-utils.js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"Link Rendering Tests\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  test(\"should render link with href correctly\", async () => {\n    testSetup = await testRender(\n      <text>\n        Visit <a href=\"https://opentui.com\">opentui.com</a> for more info\n      </text>,\n      {\n        width: 50,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Visit opentui.com for more info\")\n  })\n\n  test(\"should render styled link with underline\", async () => {\n    testSetup = await testRender(\n      <text>\n        <u>\n          <a href=\"https://opentui.com\" fg=\"blue\">\n            opentui.com\n          </a>\n        </u>\n      </text>,\n      {\n        width: 50,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"opentui.com\")\n  })\n\n  test(\"should render link inside text with other elements\", async () => {\n    testSetup = await testRender(\n      <text>\n        Check out <a href=\"https://github.com/anomalyco/opentui\">GitHub</a> and{\" \"}\n        <a href=\"https://opentui.com\">our website</a>\n      </text>,\n      {\n        width: 60,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"GitHub\")\n    expect(frame).toContain(\"our website\")\n  })\n})\n"
  },
  {
    "path": "packages/react/tests/runtime-plugin-support.fixture.ts",
    "content": "import { mkdtempSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { plugin as registerPlugin } from \"bun\"\nimport * as coreRuntime from \"@opentui/core\"\nimport * as reactRuntime from \"react\"\nimport * as reactJsxRuntime from \"react/jsx-runtime\"\nimport * as reactJsxDevRuntime from \"react/jsx-dev-runtime\"\nimport * as opentuiReactRuntime from \"../src/index\"\n\ntype FixtureState = typeof globalThis & {\n  __reactRuntimeHost__?: {\n    core: Record<string, unknown>\n    coreTesting: Record<string, unknown>\n    opentuiReact: Record<string, unknown>\n    opentuiReactJsx: Record<string, unknown>\n    opentuiReactJsxDev: Record<string, unknown>\n    react: Record<string, unknown>\n    reactJsx: Record<string, unknown>\n    reactJsxDev: Record<string, unknown>\n  }\n}\n\nconst tempRoot = mkdtempSync(join(tmpdir(), \"react-runtime-plugin-support-fixture-\"))\nconst entryPath = join(tempRoot, \"entry.ts\")\n\nconst source = [\n  'import * as core from \"@opentui/core\"',\n  'import * as coreTesting from \"@opentui/core/testing\"',\n  'import * as opentuiReact from \"@opentui/react\"',\n  'import * as opentuiReactJsx from \"@opentui/react/jsx-runtime\"',\n  'import * as opentuiReactJsxDev from \"@opentui/react/jsx-dev-runtime\"',\n  'import * as react from \"react\"',\n  'import * as reactJsx from \"react/jsx-runtime\"',\n  'import * as reactJsxDev from \"react/jsx-dev-runtime\"',\n  \"const state = globalThis as { __reactRuntimeHost__?: { core: Record<string, unknown>; coreTesting: Record<string, unknown>; opentuiReact: Record<string, unknown>; opentuiReactJsx: Record<string, unknown>; opentuiReactJsxDev: Record<string, unknown>; react: Record<string, unknown>; reactJsx: Record<string, unknown>; reactJsxDev: Record<string, unknown> } }\",\n  \"const host = state.__reactRuntimeHost__\",\n  \"const checks = [\",\n  \"  `core=${core.engine === host?.core.engine}`,\",\n  \"  `coreTesting=${coreTesting.createTestRenderer === host?.coreTesting.createTestRenderer}`,\",\n  \"  `opentuiReact=${opentuiReact.render === host?.opentuiReact.render}`,\",\n  \"  `opentuiReactJsx=${opentuiReactJsx.jsx === host?.opentuiReactJsx.jsx}`,\",\n  \"  `opentuiReactJsxDev=${opentuiReactJsxDev.jsxDEV === host?.opentuiReactJsxDev.jsxDEV}`,\",\n  \"  `react=${react.useState === host?.react.useState}`,\",\n  \"  `reactJsx=${reactJsx.jsx === host?.reactJsx.jsx}`,\",\n  \"  `reactJsxDev=${reactJsxDev.jsxDEV === host?.reactJsxDev.jsxDEV}`,\",\n  \"]\",\n  \"console.log(checks.join(';'))\",\n  \"export const noop = 1\",\n].join(\"\\n\")\n\nwriteFileSync(entryPath, source)\n\nconst state = globalThis as FixtureState\nstate.__reactRuntimeHost__ = {\n  core: coreRuntime as Record<string, unknown>,\n  coreTesting: (await import(\"@opentui/core/testing\")) as Record<string, unknown>,\n  opentuiReact: opentuiReactRuntime as Record<string, unknown>,\n  opentuiReactJsx: (await import(\"../jsx-runtime.js\")) as Record<string, unknown>,\n  opentuiReactJsxDev: (await import(\"../jsx-dev-runtime.js\")) as Record<string, unknown>,\n  react: reactRuntime as Record<string, unknown>,\n  reactJsx: reactJsxRuntime as Record<string, unknown>,\n  reactJsxDev: reactJsxDevRuntime as Record<string, unknown>,\n}\n\nregisterPlugin.clearAll()\n\ntry {\n  await import(\"../scripts/runtime-plugin-support\")\n  await import(entryPath)\n} finally {\n  registerPlugin.clearAll()\n  delete state.__reactRuntimeHost__\n  rmSync(tempRoot, { recursive: true, force: true })\n}\n"
  },
  {
    "path": "packages/react/tests/runtime-plugin-support.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { join } from \"node:path\"\n\ndescribe(\"react runtime plugin support\", () => {\n  it(\"loads external modules against host runtime exports\", () => {\n    const fixturePath = join(import.meta.dir, \"runtime-plugin-support.fixture.ts\")\n    const result = Bun.spawnSync([process.execPath, fixturePath], {\n      cwd: join(import.meta.dir, \"..\"),\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: process.env,\n    })\n\n    const stdout = result.stdout.toString().trim()\n    const stderr = result.stderr.toString().trim()\n\n    if (stdout) {\n      console.debug(`[runtime-plugin-support.fixture] stdout:\\n${stdout}`)\n    }\n\n    if (stderr) {\n      console.debug(`[runtime-plugin-support.fixture] stderr:\\n${stderr}`)\n    }\n\n    expect(result.exitCode).toBe(0)\n    expect(stdout).toContain(\"core=true\")\n    expect(stdout).toContain(\"coreTesting=true\")\n    expect(stdout).toContain(\"opentuiReact=true\")\n    expect(stdout).toContain(\"opentuiReactJsx=true\")\n    expect(stdout).toContain(\"opentuiReactJsxDev=true\")\n    expect(stdout).toContain(\"react=true\")\n    expect(stdout).toContain(\"reactJsx=true\")\n    expect(stdout).toContain(\"reactJsxDev=true\")\n  })\n})\n"
  },
  {
    "path": "packages/react/tests/slot.test.tsx",
    "content": "import { afterEach, beforeEach, describe, expect, it } from \"bun:test\"\nimport { createTestRenderer, type TestRendererOptions } from \"@opentui/core/testing\"\nimport { createContext, useContext, useEffect, useMemo, useState } from \"react\"\nimport { act, type ReactNode } from \"react\"\nimport { createReactSlotRegistry, createSlot, Slot, type ReactPlugin } from \"../src/plugins/slot\"\nimport { useKeyboard } from \"../src/hooks/use-keyboard\"\nimport { createRoot, type Root } from \"../src/reconciler/renderer\"\n\ninterface AppSlots {\n  statusbar: { user: string }\n  sidebar: { items: string[] }\n}\n\nconst hostContext = {\n  appName: \"react-slot-tests\",\n  version: \"1.0.0\",\n}\n\nlet testSetup: Awaited<ReturnType<typeof createTestRenderer>>\n\nfunction setIsReactActEnvironment(isReactActEnvironment: boolean) {\n  // @ts-expect-error - this is a test environment\n  globalThis.IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment\n}\n\nasync function setupSlotTest(\n  createNode: (registry: ReturnType<typeof createReactSlotRegistry<AppSlots>>) => ReactNode,\n  options: TestRendererOptions,\n) {\n  let root: Root | null = null\n  setIsReactActEnvironment(true)\n\n  const setup = await createTestRenderer({\n    ...options,\n    onDestroy() {\n      act(() => {\n        if (root) {\n          root.unmount()\n          root = null\n        }\n      })\n      options.onDestroy?.()\n      setIsReactActEnvironment(false)\n    },\n  })\n\n  const registry = createReactSlotRegistry<AppSlots>(setup.renderer, hostContext)\n  root = createRoot(setup.renderer)\n\n  act(() => {\n    if (root) {\n      root.render(createNode(registry))\n    }\n  })\n\n  return { setup, registry }\n}\n\ndescribe(\"React Slot System\", () => {\n  beforeEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  it(\"reuses one registry per renderer and rejects different context\", async () => {\n    const setup = await createTestRenderer({ width: 20, height: 4 })\n    testSetup = setup\n\n    const context = { appName: \"react-slot-tests\", version: \"1.0.0\" }\n    const first = createReactSlotRegistry<AppSlots, typeof context>(setup.renderer, context)\n    const second = createReactSlotRegistry<AppSlots, typeof context>(setup.renderer, context)\n\n    expect(first).toBe(second)\n\n    expect(() => {\n      createReactSlotRegistry<AppSlots, typeof context>(setup.renderer, { appName: \"other\", version: \"2.0.0\" })\n    }).toThrow(\"different context\")\n  })\n\n  it(\"renders fallback content when no plugin matches\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        const AppSlot = Slot<AppSlots, typeof hostContext>\n        return (\n          <AppSlot registry={registry} name=\"statusbar\" user=\"sam\">\n            <text>fallback-only</text>\n          </AppSlot>\n        )\n      },\n      { width: 50, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"fallback-only\")\n  })\n\n  it(\"appends plugin output after fallback content by default\", async () => {\n    const plugin: ReactPlugin<AppSlots, typeof hostContext> = {\n      id: \"append-plugin\",\n      slots: {\n        statusbar(ctx, props) {\n          return <text>{`plugin:${ctx.appName}:${props.user}`}</text>\n        },\n      },\n    }\n\n    const { setup, registry } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register(plugin)\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"ava\">\n            <text>base-content</text>\n          </Slot>\n        )\n      },\n      { width: 60, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"base-content\")\n    expect(frame).toContain(\"plugin:react-slot-tests:ava\")\n  })\n\n  it(\"replace mode hides fallback and renders all ordered plugins\", async () => {\n    const { setup, registry } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"late\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <text>late-plugin</text>\n            },\n          },\n        })\n\n        slotRegistry.register({\n          id: \"early\",\n          order: 0,\n          slots: {\n            statusbar() {\n              return <text>early-plugin</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"replace\">\n            <text>replace-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 40, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"early-plugin\")\n    expect(frame).toContain(\"late-plugin\")\n    expect(frame).not.toContain(\"replace-fallback\")\n  })\n\n  it(\"replace mode does not invoke fallback components when plugin content wins\", async () => {\n    const fallbackLifecycle: string[] = []\n\n    function FallbackProbe() {\n      fallbackLifecycle.push(\"render\")\n\n      useEffect(() => {\n        fallbackLifecycle.push(\"mount\")\n\n        return () => {\n          fallbackLifecycle.push(\"cleanup\")\n        }\n      }, [])\n\n      return <text>fallback-probe</text>\n    }\n\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"replace-plugin\",\n          slots: {\n            statusbar() {\n              return <text>plugin-only</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"replace\">\n            <FallbackProbe />\n          </Slot>\n        )\n      },\n      { width: 40, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"plugin-only\")\n    expect(frame).not.toContain(\"fallback-probe\")\n    expect(fallbackLifecycle).toEqual([])\n  })\n\n  it(\"single_winner mode renders only the highest-priority plugin\", async () => {\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"late\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <text>late-plugin</text>\n            },\n          },\n        })\n\n        slotRegistry.register({\n          id: \"early\",\n          order: 0,\n          slots: {\n            statusbar() {\n              return <text>early-plugin</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"single_winner\">\n            <text>single-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 40, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"early-plugin\")\n    expect(frame).not.toContain(\"late-plugin\")\n    expect(frame).not.toContain(\"single-fallback\")\n  })\n\n  it(\"replace mode keeps healthy plugin output when another plugin fails\", async () => {\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"broken-plugin\",\n          order: 0,\n          slots: {\n            statusbar() {\n              throw new Error(\"broken render\")\n            },\n          },\n        })\n\n        slotRegistry.register({\n          id: \"healthy-plugin\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <text>healthy-plugin</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"replace\">\n            <text>replace-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 50, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"healthy-plugin\")\n    expect(frame).not.toContain(\"replace-fallback\")\n  })\n\n  it(\"single_winner mode falls back when highest-priority plugin fails\", async () => {\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"broken-winner\",\n          order: 0,\n          slots: {\n            statusbar() {\n              throw new Error(\"winner failed\")\n            },\n          },\n        })\n\n        slotRegistry.register({\n          id: \"healthy-second\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <text>healthy-second</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"single_winner\">\n            <text>single-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 50, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"single-fallback\")\n    expect(frame).not.toContain(\"healthy-second\")\n  })\n\n  it(\"reacts to plugin registration and unregistering\", async () => {\n    const { setup, registry } = await setupSlotTest(\n      (slotRegistry) => {\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"kai\" mode=\"replace\">\n            <text>dynamic-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 40, height: 6 },\n    )\n    testSetup = setup\n\n    const plugin: ReactPlugin<AppSlots> = {\n      id: \"dynamic-plugin\",\n      slots: {\n        statusbar() {\n          return <text>dynamic-plugin</text>\n        },\n      },\n    }\n\n    await testSetup.renderOnce()\n    expect(testSetup.captureCharFrame()).toContain(\"dynamic-fallback\")\n\n    act(() => {\n      registry.register(plugin)\n    })\n    await testSetup.renderOnce()\n    const withPlugin = testSetup.captureCharFrame()\n    expect(withPlugin).toContain(\"dynamic-plugin\")\n    expect(withPlugin).not.toContain(\"dynamic-fallback\")\n\n    act(() => {\n      registry.unregister(\"dynamic-plugin\")\n    })\n    await testSetup.renderOnce()\n    const withoutPlugin = testSetup.captureCharFrame()\n    expect(withoutPlugin).toContain(\"dynamic-fallback\")\n    expect(withoutPlugin).not.toContain(\"dynamic-plugin\")\n  })\n\n  it(\"switches rendered slot when props.name changes\", async () => {\n    function DynamicNameHarness({ registry }: { registry: ReturnType<typeof createReactSlotRegistry<AppSlots>> }) {\n      const Slot = useMemo(() => createSlot(registry), [registry])\n      const [slotName, setSlotName] = useState<keyof AppSlots>(\"statusbar\")\n\n      useKeyboard((key) => {\n        if (key.name === \"tab\") {\n          setSlotName((current) => (current === \"statusbar\" ? \"sidebar\" : \"statusbar\"))\n        }\n      })\n\n      const dynamicProps =\n        slotName === \"statusbar\"\n          ? ({ name: \"statusbar\", user: \"sam\", mode: \"replace\" } as const)\n          : ({ name: \"sidebar\", items: [\"one\"], mode: \"replace\" } as const)\n\n      return (\n        <Slot {...(dynamicProps as any)}>\n          <text>dynamic-name-fallback</text>\n        </Slot>\n      )\n    }\n\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"status-plugin\",\n          slots: {\n            statusbar() {\n              return <text>status-plugin</text>\n            },\n          },\n        })\n\n        slotRegistry.register({\n          id: \"sidebar-plugin\",\n          slots: {\n            sidebar() {\n              return <text>sidebar-plugin</text>\n            },\n          },\n        })\n\n        return <DynamicNameHarness registry={slotRegistry} />\n      },\n      { width: 60, height: 8 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const initialFrame = testSetup.captureCharFrame()\n    expect(initialFrame).toContain(\"status-plugin\")\n    expect(initialFrame).not.toContain(\"sidebar-plugin\")\n\n    act(() => {\n      testSetup.renderer.keyInput.emit(\"keypress\", { name: \"tab\" } as any)\n    })\n\n    await testSetup.renderOnce()\n    const switchedFrame = testSetup.captureCharFrame()\n    expect(switchedFrame).toContain(\"sidebar-plugin\")\n    expect(switchedFrame).not.toContain(\"status-plugin\")\n    expect(switchedFrame).not.toContain(\"dynamic-name-fallback\")\n  })\n\n  it(\"renders plugin nodes within provider context\", async () => {\n    const ValueContext = createContext(\"missing\")\n\n    function ContextReader() {\n      const value = useContext(ValueContext)\n      return <text>{`ctx:${value}`}</text>\n    }\n\n    const { setup, registry } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"context-plugin\",\n          slots: {\n            statusbar() {\n              return <ContextReader />\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <ValueContext.Provider value=\"inside-provider\">\n            <Slot name=\"statusbar\" user=\"max\" />\n          </ValueContext.Provider>\n        )\n      },\n      { width: 60, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toContain(\"ctx:inside-provider\")\n  })\n\n  it(\"keeps plugin identity stable when append order changes\", async () => {\n    const mountLog: string[] = []\n\n    function StatefulPluginNode({ pluginId }: { pluginId: string }) {\n      const [createdBy] = useState(pluginId)\n\n      useEffect(() => {\n        mountLog.push(`mount:${pluginId}:${createdBy}`)\n        return () => {\n          mountLog.push(`unmount:${pluginId}:${createdBy}`)\n        }\n      }, [pluginId, createdBy])\n\n      return <text>{`${pluginId}:${createdBy}`}</text>\n    }\n\n    const { setup, registry } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"alpha\",\n          order: 0,\n          slots: {\n            statusbar() {\n              return <StatefulPluginNode pluginId=\"alpha\" />\n            },\n          },\n        })\n\n        slotRegistry.register({\n          id: \"beta\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <StatefulPluginNode pluginId=\"beta\" />\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return <Slot name=\"statusbar\" user=\"sam\" />\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const beforeReorder = testSetup.captureCharFrame()\n\n    expect(beforeReorder).toContain(\"alpha:alpha\")\n    expect(beforeReorder).toContain(\"beta:beta\")\n\n    act(() => {\n      registry.updateOrder(\"beta\", -1)\n    })\n\n    await testSetup.renderOnce()\n    const afterReorder = testSetup.captureCharFrame()\n\n    expect(afterReorder).toContain(\"beta:beta\")\n    expect(afterReorder).toContain(\"alpha:alpha\")\n    expect(afterReorder).not.toContain(\"beta:alpha\")\n    expect(afterReorder).not.toContain(\"alpha:beta\")\n    expect(mountLog).toEqual([\"mount:alpha:alpha\", \"mount:beta:beta\"])\n  })\n\n  it(\"captures plugin render invocation errors and reports plugin metadata\", async () => {\n    const errors: string[] = []\n\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.source}:${event.error.message}`)\n        })\n\n        slotRegistry.register({\n          id: \"broken-plugin\",\n          slots: {\n            statusbar() {\n              throw new Error(\"render failed\")\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>fallback-visible</text>\n          </Slot>\n        )\n      },\n      { width: 70, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"fallback-visible\")\n    expect(errors).toEqual([\"broken-plugin:statusbar:render:react:render failed\"])\n  })\n\n  it(\"replace mode falls back when plugin fails and no placeholder is configured\", async () => {\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"broken-plugin\",\n          slots: {\n            statusbar() {\n              throw new Error(\"render failed\")\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\" mode=\"replace\">\n            <text>replace-fallback-visible</text>\n          </Slot>\n        )\n      },\n      { width: 70, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"replace-fallback-visible\")\n  })\n\n  it(\"replace mode falls back when plugin subtree crashes and no placeholder is configured\", async () => {\n    const errors: string[] = []\n\n    function ExplodingPluginNode() {\n      throw new Error(\"replace subtree exploded\")\n    }\n\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.source}:${event.error.message}`)\n        })\n\n        slotRegistry.register({\n          id: \"replace-exploding-plugin\",\n          slots: {\n            statusbar() {\n              return <ExplodingPluginNode />\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\" mode=\"replace\">\n            <text>replace-safe-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"replace-safe-fallback\")\n    expect(errors).toContain(\"replace-exploding-plugin:statusbar:render:react:replace subtree exploded\")\n  })\n\n  it(\"reports error_placeholder and keeps fallback when placeholder throws after plugin render error\", async () => {\n    const errors: string[] = []\n\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.source}:${event.error.message}`)\n        })\n\n        slotRegistry.register({\n          id: \"broken-plugin\",\n          slots: {\n            statusbar() {\n              throw new Error(\"render failed\")\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry, {\n          pluginFailurePlaceholder() {\n            throw new Error(\"placeholder failed\")\n          },\n        })\n\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>fallback-visible</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"fallback-visible\")\n    expect(errors).toContain(\"broken-plugin:statusbar:render:react:render failed\")\n    expect(errors).toContain(\"broken-plugin:statusbar:error_placeholder:react:placeholder failed\")\n  })\n\n  it(\"reports error_placeholder and keeps fallback when placeholder throws after subtree crash\", async () => {\n    const errors: string[] = []\n\n    function ExplodingPluginNode() {\n      throw new Error(\"component exploded\")\n    }\n\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.source}:${event.error.message}`)\n        })\n\n        slotRegistry.register({\n          id: \"exploding-plugin\",\n          slots: {\n            statusbar() {\n              return <ExplodingPluginNode />\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry, {\n          pluginFailurePlaceholder() {\n            throw new Error(\"placeholder crashed\")\n          },\n        })\n\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>safe-host-content</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"safe-host-content\")\n    expect(errors).toContain(\"exploding-plugin:statusbar:render:react:component exploded\")\n    expect(errors).toContain(\"exploding-plugin:statusbar:error_placeholder:react:placeholder crashed\")\n  })\n\n  it(\"catches plugin subtree errors via per-plugin boundary\", async () => {\n    const errors: string[] = []\n\n    function ExplodingPluginNode() {\n      throw new Error(\"component exploded\")\n    }\n\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.error.message}`)\n        })\n\n        slotRegistry.register({\n          id: \"exploding-component-plugin\",\n          slots: {\n            statusbar() {\n              return <ExplodingPluginNode />\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>safe-host-content</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"safe-host-content\")\n    expect(errors).toEqual([\"exploding-component-plugin:statusbar:render:component exploded\"])\n  })\n\n  it(\"renders optional plugin failure placeholder when configured\", async () => {\n    const { setup } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"broken-plugin\",\n          slots: {\n            statusbar() {\n              throw new Error(\"render failed\")\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry, {\n          pluginFailurePlaceholder(failure) {\n            return <text>{`plugin-error:${failure.pluginId}:${failure.slot}`}</text>\n          },\n        })\n\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>fallback-visible</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"fallback-visible\")\n    expect(frame).toContain(\"plugin-error:broken-plugin:statusbar\")\n  })\n\n  it(\"does not continuously emit plugin errors after pressing e then d\", async () => {\n    const debugEvents: string[] = []\n    let pluginErrorEventCount = 0\n    let listenerStateUpdates = 0\n    const maxListenerStateUpdates = 20\n\n    function ClockCrashNode() {\n      throw new Error(\"Forced subtree crash in clock-plugin\")\n    }\n\n    function createClockPlugin(crash: boolean): ReactPlugin<AppSlots, typeof hostContext> {\n      return {\n        id: \"clock-plugin\",\n        order: 0,\n        slots: {\n          statusbar() {\n            if (crash) {\n              return <ClockCrashNode />\n            }\n\n            return <text>clock-ok</text>\n          },\n          sidebar() {\n            return <text>clock-sidebar-ok</text>\n          },\n        },\n      }\n    }\n\n    function createActivityPlugin(crash: boolean): ReactPlugin<AppSlots, typeof hostContext> {\n      return {\n        id: \"activity-plugin\",\n        order: 10,\n        slots: {\n          statusbar() {\n            if (crash) {\n              throw new Error(\"Forced activity render failure\")\n            }\n\n            return <text>activity-ok</text>\n          },\n        },\n      }\n    }\n\n    function ErrorSequenceHarness({ registry }: { registry: ReturnType<typeof createReactSlotRegistry<AppSlots>> }) {\n      const Slot = useMemo(\n        () =>\n          createSlot(registry, {\n            pluginFailurePlaceholder(failure) {\n              return <text>{`placeholder:${failure.pluginId}:${failure.phase}`}</text>\n            },\n          }),\n        [registry],\n      )\n\n      const [clockCrashEnabled, setClockCrashEnabled] = useState(false)\n      const [activityCrashEnabled, setActivityCrashEnabled] = useState(false)\n      const [errorLines, setErrorLines] = useState<string[]>([])\n\n      useEffect(() => {\n        return registry.onPluginError((event) => {\n          pluginErrorEventCount++\n          const line = `${event.pluginId}:${event.phase}:${event.source}:${event.error.message}`\n\n          if (debugEvents.length < 40) {\n            debugEvents.push(`event#${pluginErrorEventCount} ${line}`)\n          }\n\n          if (listenerStateUpdates < maxListenerStateUpdates) {\n            listenerStateUpdates++\n            setErrorLines((current) => [line, ...current].slice(0, 6))\n          }\n        })\n      }, [registry])\n\n      useEffect(() => {\n        const unregisterCallbacks: Array<() => void> = []\n\n        unregisterCallbacks.push(registry.register(createClockPlugin(clockCrashEnabled)))\n        unregisterCallbacks.push(registry.register(createActivityPlugin(activityCrashEnabled)))\n\n        return () => {\n          for (const unregister of unregisterCallbacks.reverse()) {\n            unregister()\n          }\n        }\n      }, [registry, clockCrashEnabled, activityCrashEnabled])\n\n      useKeyboard((key) => {\n        if (key.name === \"e\") {\n          setClockCrashEnabled((current) => !current)\n          return\n        }\n\n        if (key.name === \"d\") {\n          setActivityCrashEnabled((current) => !current)\n        }\n      })\n\n      return (\n        <>\n          <Slot name=\"statusbar\" user=\"sam\" mode=\"append\">\n            <text>fallback-statusbar</text>\n          </Slot>\n          <Slot name=\"sidebar\" items={[\"x\"]} mode=\"replace\">\n            <text>fallback-sidebar</text>\n          </Slot>\n          <text>{`errors:${errorLines.length}`}</text>\n        </>\n      )\n    }\n\n    const { setup } = await setupSlotTest((registry) => <ErrorSequenceHarness registry={registry} />, {\n      width: 80,\n      height: 10,\n    })\n    testSetup = setup\n\n    await testSetup.renderOnce()\n\n    act(() => {\n      testSetup.renderer.keyInput.emit(\"keypress\", { name: \"e\" } as any)\n    })\n    await testSetup.renderOnce()\n\n    act(() => {\n      testSetup.renderer.keyInput.emit(\"keypress\", { name: \"d\" } as any)\n    })\n\n    for (let index = 0; index < 5; index++) {\n      await testSetup.renderOnce()\n    }\n\n    const frame = testSetup.captureCharFrame()\n    if (pluginErrorEventCount > 4 || listenerStateUpdates > 4) {\n      console.log(\"[react-slot-debug] frame after e,d:\\n\" + frame)\n      console.log(\"[react-slot-debug] plugin error events:\", pluginErrorEventCount)\n      console.log(\"[react-slot-debug] listener state updates:\", listenerStateUpdates)\n      console.log(\"[react-slot-debug] sample events:\\n\" + debugEvents.join(\"\\n\"))\n    }\n\n    expect(pluginErrorEventCount).toBeLessThanOrEqual(4)\n    expect(listenerStateUpdates).toBeLessThanOrEqual(4)\n  })\n})\n"
  },
  {
    "path": "packages/react/tsconfig.build.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./dist\",\n    \"noEmit\": false,\n    \"rootDir\": \".\",\n    \"types\": [\"bun\", \"node\", \"react\"],\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@opentui/react\",\n    \"moduleResolution\": \"bundler\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@opentui/core\": [\"../core/dist\"],\n      \"@opentui/core/*\": [\"../core/dist/*\"]\n    }\n  },\n  \"include\": [\n    \"src/**/*\",\n    \"jsx-runtime.d.ts\",\n    \"jsx-dev-runtime.d.ts\",\n    \"jsx-namespace.d.ts\",\n    \"scripts/runtime-plugin-support.ts\"\n  ],\n  \"exclude\": [\n    \"**/*.test.ts\",\n    \"**/*.spec.ts\",\n    \"examples/**/*\",\n    \"node_modules/**/*\",\n    \"../core/**/*\",\n    \"scripts/build.ts\",\n    \"scripts/publish.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/react/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // Enable latest features\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@opentui/react\",\n    \"allowJs\": true,\n\n    // Bundler mode\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n\n    // Best practices\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    // Some stricter flags (disabled by default)\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false\n  }\n}\n"
  },
  {
    "path": "packages/solid/README.md",
    "content": "# @opentui/solid\n\nSolid.js support for [OpenTUI](https://github.com/anomalyco/opentui).\n\n## Installation\n\n```bash\nbun install solid-js @opentui/solid\n```\n\n## Usage\n\n1. Add jsx config to tsconfig.json:\n\n```json\n{\n  \"compilerOptions\": {\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"@opentui/solid\"\n  }\n}\n```\n\n2. Add preload script to bunfig.toml:\n\n```toml\npreload = [\"@opentui/solid/preload\"]\n```\n\n3. Add render function to index.tsx:\n\n```tsx\nimport { render } from \"@opentui/solid\"\n\nrender(() => <text>Hello, World!</text>)\n```\n\n4. Run with `bun index.tsx`.\n\n5. To build use [Bun.build](https://bun.com/docs/bundler) ([source](https://github.com/anomalyco/opentui/issues/122)):\n\n```ts\nimport solidPlugin from \"@opentui/solid/bun-plugin\"\n\nawait Bun.build({\n  entrypoints: [\"./index.tsx\"],\n  target: \"bun\",\n  outdir: \"./build\",\n  plugins: [solidPlugin],\n  compile: {\n    target: \"bun-darwin-arm64\",\n    outfile: \"app-macos\",\n  },\n})\n```\n\n## Table of Contents\n\n- [Core Concepts](#core-concepts)\n  - [Components](#components)\n- [API Reference](#api-reference)\n  - [render(node, rendererOrConfig?)](#rendernode-rendererorconfig)\n  - [testRender(node, options?)](#testrendernode-options)\n  - [extend(components)](#extendcomponents)\n  - [getComponentCatalogue()](#getcomponentcatalogue)\n  - [Hooks](#hooks)\n  - [Portal](#portal)\n  - [Dynamic](#dynamic)\n- [Components](#components-1)\n  - [Layout & Display](#layout--display)\n  - [Input](#input)\n  - [Code & Diff](#code--diff)\n  - [Text Modifiers](#text-modifiers)\n\n## Core Concepts\n\n### Components\n\nOpenTUI Solid exposes intrinsic JSX elements that map to OpenTUI renderables:\n\n- **Layout & Display:** `text`, `box`, `scrollbox`, `ascii_font`\n- **Input:** `input`, `textarea`, `select`, `tab_select`\n- **Code & Diff:** `code`, `line_number`, `diff`\n- **Text Modifiers:** `span`, `strong`, `b`, `em`, `i`, `u`, `br`, `a`\n\n## API Reference\n\n### `render(node, rendererOrConfig?)`\n\nRender a Solid component tree into a CLI renderer. If `rendererOrConfig` is omitted, a renderer is created with default options.\n\n```tsx\nimport { render } from \"@opentui/solid\"\n\nrender(() => <App />)\n```\n\n**Parameters:**\n\n- `node`: Function returning a JSX element.\n- `rendererOrConfig?`: `CliRenderer` instance or `CliRendererConfig`.\n\n### `testRender(node, options?)`\n\nCreate a test renderer for snapshots and interaction tests.\n\n```tsx\nimport { testRender } from \"@opentui/solid\"\n\nconst testSetup = await testRender(() => <App />, { width: 40, height: 10 })\n```\n\n### `extend(components)`\n\nRegister custom renderables as JSX intrinsic elements.\n\n```tsx\nimport { extend } from \"@opentui/solid\"\n\nextend({ customBox: CustomBoxRenderable })\n```\n\n### `getComponentCatalogue()`\n\nReturns the current component catalogue that powers JSX tag lookup.\n\n### Hooks\n\n- `useRenderer()`\n- `onResize(callback)`\n- `onFocus(callback)`\n- `onBlur(callback)`\n- `useTerminalDimensions()`\n- `useKeyboard(handler, options?)`\n- `usePaste(handler)`\n- `useSelectionHandler(handler)`\n- `useTimeline(options?)`\n\n### `Portal`\n\nRender children into a different mount node, useful for overlays and tooltips.\n\n```tsx\nimport { Portal } from \"@opentui/solid\"\n;<Portal mount={renderer.root}>\n  <box border>Overlay</box>\n</Portal>\n```\n\n### `Dynamic`\n\nRender arbitrary intrinsic elements or components dynamically.\n\n```tsx\nimport { Dynamic } from \"@opentui/solid\"\n;<Dynamic component={isMultiline() ? \"textarea\" : \"input\"} />\n```\n\n## Components\n\n### Layout & Display\n\n- `text`: styled text container\n- `box`: layout container with borders, padding, and flex settings\n- `scrollbox`: scrollable container\n- `ascii_font`: ASCII art text renderer\n\n### Input\n\n- `input`: single-line text input\n- `textarea`: multi-line text input\n- `select`: list selection\n- `tab_select`: tab-based selection\n\n### Code & Diff\n\n- `code`: syntax-highlighted code blocks\n- `line_number`: line-numbered code display with diff/diagnostic helpers\n- `diff`: unified or split diff viewer\n\n### Text Modifiers\n\nThese must appear inside a `text` component:\n\n- `span`: inline styled text\n- `strong`/`b`: bold text\n- `em`/`i`: italic text\n- `u`: underline text\n- `br`: line break\n- `a`: link text with `href`\n"
  },
  {
    "path": "packages/solid/bunfig.toml",
    "content": "preload = [\"@opentui/solid/preload\"]\n\n[test]\npreload = [\"@opentui/solid/preload\"]\nroot = \"tests\"\n"
  },
  {
    "path": "packages/solid/examples/.plugin/index.tsx",
    "content": "import { OptimizedBuffer, RGBA, type RenderContext } from \"@opentui/core\"\nimport { ThreeRenderable, THREE } from \"@opentui/core/3d\"\nimport { extend, type SolidPlugin } from \"@opentui/solid\"\nimport { ExternalSidebarPanel, ExternalStatusCard } from \"./slot-components.tsx\"\n\nexport type ExternalPluginSlots = {\n  statusbar: { label: string }\n  sidebar: { section: string }\n}\n\nexport type ExternalPluginContext = {\n  appName: string\n  version: string\n}\n\nconst CAPABILITIES = [\"statusbar extension\", \"sidebar extension\", \"external jsx components\", \"core 3d entrypoint\"]\n\nclass ExternalCubeRenderable extends ThreeRenderable {\n  private cube: THREE.Mesh\n\n  constructor(ctx: RenderContext, options: ConstructorParameters<typeof ThreeRenderable>[1]) {\n    const scene = new THREE.Scene()\n    const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100)\n    camera.position.set(0, 0, 2.55)\n    camera.lookAt(0, 0, 0)\n\n    const ambientLight = new THREE.AmbientLight(new THREE.Color(\"#666666\"), 1.0)\n    scene.add(ambientLight)\n\n    const keyLight = new THREE.DirectionalLight(new THREE.Color(\"#fff2e6\"), 1.2)\n    keyLight.position.set(2.5, 2.0, 3.0)\n    scene.add(keyLight)\n\n    const fillLight = new THREE.DirectionalLight(new THREE.Color(\"#80b3ff\"), 0.6)\n    fillLight.position.set(-2.0, -1.5, 2.5)\n    scene.add(fillLight)\n\n    const cubeGeometry = new THREE.BoxGeometry(1.0, 1.0, 1.0)\n    const cubeMaterial = new THREE.MeshPhongMaterial({\n      color: new THREE.Color(\"#40ccff\"),\n      shininess: 80,\n      specular: new THREE.Color(\"#e6e6ff\"),\n    })\n    const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)\n    cube.scale.setScalar(1.12)\n    scene.add(cube)\n\n    super(ctx, {\n      ...options,\n      scene,\n      camera,\n      renderer: {\n        ...(options.renderer ?? {}),\n        focalLength: 8,\n        alpha: true,\n        backgroundColor: RGBA.fromValues(0, 0, 0, 0),\n      },\n    })\n\n    this.cube = cube\n  }\n\n  protected override renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n    const delta = deltaTime / 1000\n    this.cube.rotation.x += delta * 0.6\n    this.cube.rotation.y += delta * 0.4\n    this.cube.rotation.z += delta * 0.2\n    super.renderSelf(buffer, deltaTime)\n  }\n}\n\ndeclare module \"@opentui/solid\" {\n  interface OpenTUIComponents {\n    external_cube: typeof ExternalCubeRenderable\n  }\n}\n\nextend({ external_cube: ExternalCubeRenderable })\n\nexport function loadExternalPlugin(): SolidPlugin<ExternalPluginSlots, ExternalPluginContext> {\n  return {\n    id: \"external-jsx-plugin\",\n    order: 20,\n    slots: {\n      statusbar(ctx, props) {\n        return <ExternalStatusCard host={ctx.appName} label={props.label} version={ctx.version} />\n      },\n      sidebar(_ctx, props) {\n        return (\n          <box flexDirection=\"column\">\n            <ExternalSidebarPanel section={props.section} capabilities={CAPABILITIES} />\n            <box marginTop={1} border borderStyle=\"single\" borderColor=\"#334155\" flexDirection=\"column\">\n              <text fg=\"#93c5fd\">3D cube from @opentui/core/3d</text>\n              <external_cube width=\"100%\" height={16} />\n            </box>\n          </box>\n        )\n      },\n    },\n  }\n}\n"
  },
  {
    "path": "packages/solid/examples/.plugin/package.json",
    "content": "{\n  \"name\": \"@opentui/solid-external-plugin-example\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"jimp\": \"1.6.0\",\n    \"solid-js\": \"1.9.11\",\n    \"three\": \"0.177.0\",\n    \"bun-webgpu\": \"0.1.5\"\n  }\n}\n"
  },
  {
    "path": "packages/solid/examples/.plugin/slot-components.tsx",
    "content": "import { For } from \"solid-js\"\n\ntype ExternalStatusCardProps = {\n  host: string\n  label: string\n  version: string\n}\n\nexport const ExternalStatusCard = (props: ExternalStatusCardProps) => {\n  return (\n    <box border borderStyle=\"single\" borderColor=\"#16a34a\" marginLeft={1} paddingLeft={1} paddingRight={1} height={3}>\n      <text fg=\"#bbf7d0\">{`${props.host} -> ${props.label} (${props.version})`}</text>\n    </box>\n  )\n}\n\ntype ExternalSidebarPanelProps = {\n  section: string\n  capabilities: string[]\n}\n\nexport const ExternalSidebarPanel = (props: ExternalSidebarPanelProps) => {\n  return (\n    <box border borderStyle=\"single\" borderColor=\"#06b6d4\" flexDirection=\"column\" paddingLeft={1} paddingRight={1}>\n      <text fg=\"#67e8f9\">{`External plugin section: ${props.section}`}</text>\n      <For each={props.capabilities}>{(capability) => <text fg=\"#bae6fd\">{`- ${capability}`}</text>}</For>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/build.ts",
    "content": "#!/usr/bin/env bun\n\nimport { chmodSync, cpSync, existsSync, mkdirSync, rmSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport process from \"node:process\"\nimport { type BunPlugin } from \"bun\"\nimport { createSolidTransformPlugin } from \"../scripts/solid-plugin\"\n\ntype BuildTarget = {\n  platform: \"darwin\" | \"linux\" | \"windows\"\n  arch: \"x64\" | \"arm64\"\n}\n\nconst ALL_TARGETS: BuildTarget[] = [\n  { platform: \"darwin\", arch: \"x64\" },\n  { platform: \"darwin\", arch: \"arm64\" },\n  { platform: \"linux\", arch: \"x64\" },\n  { platform: \"windows\", arch: \"x64\" },\n]\n\nfunction normalizePlatform(platform: NodeJS.Platform): BuildTarget[\"platform\"] | null {\n  if (platform === \"win32\") {\n    return \"windows\"\n  }\n\n  if (platform === \"darwin\" || platform === \"linux\") {\n    return platform\n  }\n\n  return null\n}\n\nfunction getHostTarget(): BuildTarget {\n  const platform = normalizePlatform(process.platform)\n  if (!platform) {\n    throw new Error(`Unsupported host platform: ${process.platform}`)\n  }\n\n  if (process.arch !== \"x64\" && process.arch !== \"arm64\") {\n    throw new Error(`Unsupported host architecture: ${process.arch}`)\n  }\n\n  return {\n    platform,\n    arch: process.arch,\n  }\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst packageRoot = resolve(__dirname, \"..\")\nconst distDir = join(__dirname, \"dist\")\nconst externalPluginSourceDir = join(__dirname, \".plugin\")\n\nconst packageJson = JSON.parse(await Bun.file(join(packageRoot, \"package.json\")).text()) as { version?: string }\nconst version = packageJson.version ?? \"0.0.0\"\n\nconst args = process.argv.slice(2)\nconst buildAll = args.includes(\"--all\")\nconst targets = buildAll ? ALL_TARGETS : [getHostTarget()]\n\nconst workspaceAliasPlugin: BunPlugin = {\n  name: \"workspace-alias\",\n  setup(build) {\n    build.onResolve({ filter: /^@opentui\\/solid$/ }, () => {\n      return {\n        path: join(packageRoot, \"index.ts\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/core$/ }, () => {\n      return {\n        path: join(packageRoot, \"..\", \"core\", \"src\", \"index.ts\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/core\\/testing$/ }, () => {\n      return {\n        path: join(packageRoot, \"..\", \"core\", \"src\", \"testing.ts\"),\n      }\n    })\n\n    build.onResolve({ filter: /^@opentui\\/solid\\/runtime-plugin-support$/ }, () => {\n      return {\n        path: join(packageRoot, \"scripts\", \"runtime-plugin-support.ts\"),\n      }\n    })\n  },\n}\n\nmkdirSync(distDir, { recursive: true })\n\nfunction syncExternalPluginFiles(targetDir: string): void {\n  if (!existsSync(externalPluginSourceDir)) {\n    return\n  }\n\n  const pluginOutDir = join(targetDir, \".plugin\")\n  rmSync(pluginOutDir, { recursive: true, force: true })\n  cpSync(externalPluginSourceDir, pluginOutDir, { recursive: true })\n}\n\nconsole.log(`Building Solid examples executable${buildAll ? \"s\" : \"\"}...`)\nconsole.log(`Output directory: ${distDir}`)\nconsole.log()\n\nlet successCount = 0\nlet failCount = 0\n\nfor (const { platform, arch } of targets) {\n  const exeName = platform === \"windows\" ? \"opentui-solid-examples.exe\" : \"opentui-solid-examples\"\n  const nullConfigPath = platform === \"windows\" ? \"NUL\" : \"/dev/null\"\n  const outfile = join(distDir, `${platform}-${arch}`, exeName)\n\n  mkdirSync(dirname(outfile), { recursive: true })\n\n  console.log(`Building for ${platform}-${arch}...`)\n\n  try {\n    const buildResult = await Bun.build({\n      entrypoints: [join(__dirname, \"index.tsx\")],\n      tsconfig: join(__dirname, \"tsconfig.json\"),\n      sourcemap: \"external\",\n      plugins: [workspaceAliasPlugin, createSolidTransformPlugin()],\n      compile: {\n        target: `bun-${platform}-${arch}` as any,\n        outfile,\n        execArgv: [\n          `--user-agent=opentui-solid-examples/${version}`,\n          `--config=${nullConfigPath}`,\n          `--env-file=\"\"`,\n          `--`,\n        ],\n        windows: {},\n      },\n    })\n\n    if (buildResult.logs.length > 0) {\n      console.log(`  Build logs for ${platform}-${arch}:`)\n      buildResult.logs.forEach((log) => {\n        if (log.level === \"error\") {\n          console.error(\"  ERROR:\", log.message)\n        } else if (log.level === \"warning\") {\n          console.warn(\"  WARNING:\", log.message)\n        } else {\n          console.log(\"  INFO:\", log.message)\n        }\n      })\n    }\n\n    if (!buildResult.success) {\n      console.error(`  ❌ Build failed for ${platform}-${arch}`)\n      failCount++\n      console.log()\n      continue\n    }\n\n    if (platform !== \"windows\") {\n      chmodSync(outfile, 0o755)\n    }\n\n    syncExternalPluginFiles(dirname(outfile))\n\n    console.log(`  ✅ Successfully built: ${outfile}`)\n    successCount++\n  } catch (error) {\n    console.error(`  ❌ Build error for ${platform}-${arch}:`, error)\n    failCount++\n  }\n\n  console.log()\n}\n\nconsole.log(\"=\".repeat(60))\nconsole.log(`Build complete: ${successCount} succeeded, ${failCount} failed`)\nconsole.log(`Output directory: ${distDir}`)\n\nif (failCount > 0) {\n  process.exit(1)\n}\n"
  },
  {
    "path": "packages/solid/examples/components/ExampleSelector.tsx",
    "content": "import { measureText } from \"@opentui/core\"\nimport { TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from \"@opentui/solid\"\nimport { createSignal, Match, onMount, Switch } from \"solid-js\"\nimport { Session } from \"../session.tsx\"\nimport { SplitModeDemo } from \"./animation-demo.tsx\"\nimport AutocompleteDemo from \"./autocomplete-demo.tsx\"\nimport { CodeDemo } from \"./code-demo.tsx\"\nimport DiffDemo from \"./diff-demo.tsx\"\nimport ExtendDemo from \"./extend-demo.tsx\"\nimport InputScene from \"./input-demo.tsx\"\nimport LineNumberDemo from \"./line-number-demo.tsx\"\nimport MouseScene from \"./mouse-demo.tsx\"\nimport PluginSlotsDemo from \"./plugin-slots-demo.tsx\"\nimport { ScrollDemo, ScrollDemoIndex } from \"./scroll-demo.tsx\"\nimport { CustomScrollAccelDemo } from \"./custom-scroll-accel-demo.tsx\"\nimport ExternalPluginSlotsDemo from \"./external-plugin-slots-demo.tsx\"\nimport TabSelectDemo from \"./tab-select-demo.tsx\"\nimport TextSelectionDemo from \"./text-selection-demo.tsx\"\nimport TextStyleScene from \"./text-style-demo.tsx\"\nimport { TextareaDemo } from \"./textarea-demo.tsx\"\nimport { TextareaMinimalDemo } from \"./textarea-minimal-demo.tsx\"\nimport { TextTruncationDemo } from \"./text-truncation-demo.tsx\"\n\nconst EXAMPLES = [\n  {\n    name: \"Diff Viewer Demo\",\n    description: \"Unified and split diff view with syntax highlighting\",\n    scene: \"diff-demo\",\n  },\n  {\n    name: \"Line Numbers Demo\",\n    description: \"Code with line numbers, diff highlights, and diagnostics\",\n    scene: \"line-number-demo\",\n  },\n  {\n    name: \"Code Syntax Highlighting Demo\",\n    description: \"JavaScript syntax highlighting using TreeSitter with <code> component\",\n    scene: \"code-demo\",\n  },\n  {\n    name: \"Text Selection Demo\",\n    description: \"Text selection across multiple renderables with mouse drag\",\n    scene: \"text-selection-demo\",\n  },\n  {\n    name: \"Input Demo\",\n    description: \"Interactive InputElement demo with validation and multiple fields\",\n    scene: \"input-demo\",\n  },\n  {\n    name: \"Autocomplete Demo\",\n    description: \"@ mention autocomplete with keyboard navigation\",\n    scene: \"autocomplete-demo\",\n  },\n  {\n    name: \"Textarea Demo\",\n    description: \"Interactive textarea editor with navigation, editing, and text wrapping\",\n    scene: \"textarea-demo\",\n  },\n  {\n    name: \"Textarea Minimal Demo\",\n    description: \"Minimal prompt-style textarea with submit state\",\n    scene: \"textarea-minimal-demo\",\n  },\n  {\n    name: \"Text Truncation Demo\",\n    description: \"Truncation and wrap mode toggles for varied text blocks\",\n    scene: \"text-truncation-demo\",\n  },\n  {\n    name: \"Mouse demo\",\n    description: \"Mouse interaction\",\n    scene: \"mouse-demo\",\n  },\n  {\n    name: \"Text Style Demo\",\n    description: \"Template literals with styled text, colors, and formatting\",\n    scene: \"text-style-scene\",\n  },\n  {\n    name: \"Animation Demo WIP\",\n    description: \"Keyframs api and split mode demo\",\n    scene: \"split-mode\",\n  },\n  {\n    name: \"Tab Select Demo\",\n    description: \"Tab selection demo\",\n    scene: \"tab-select-demo\",\n  },\n  {\n    name: \"Extend Demo\",\n    description: \"Extend demo\",\n    scene: \"extend-demo\",\n  },\n  {\n    name: \"Scroll Demo\",\n    description: \"Scroll demo\",\n    scene: \"scroll-demo\",\n  },\n  {\n    name: \"Scroll Demo Index\",\n    description: \"Scroll demo Index\",\n    scene: \"scroll-demo-index\",\n  },\n  {\n    name: \"Custom Scroll Acceleration Demo\",\n    description: \"Interactive demo showcasing different scroll acceleration modes\",\n    scene: \"custom-scroll-accel-demo\",\n  },\n  {\n    name: \"Session Scrollbox\",\n    description: \"Live message stream with chunked arrival simulation\",\n    scene: \"session-scrollbox\",\n  },\n  {\n    name: \"Plugin Slots Error Demo\",\n    description: \"Trigger plugin crashes and reset boundary state\",\n    scene: \"plugin-slots-demo\",\n  },\n  {\n    name: \"Plugin Slots External JSX Demo\",\n    description: \"Loads .plugin/index.tsx and renders external JSX slot components\",\n    scene: \"plugin-slots-external-demo\",\n  },\n]\n\nconst ExampleSelector = () => {\n  const renderer = useRenderer()\n\n  onMount(() => {\n    renderer.useConsole = true\n    // renderer.console.show();\n  })\n\n  const terminalDimensions = useTerminalDimensions()\n\n  const titleText = \"OPENTUI EXAMPLES\"\n  const titleFont = \"tiny\"\n  const { width: titleWidth, height: titleHeight } = measureText({ text: titleText, font: titleFont })\n\n  const [selected, setSelected] = createSignal(-1)\n\n  const handleSelect = (idx: number) => {\n    console.log(\"Selected:\", EXAMPLES.at(idx)?.name)\n    setSelected(idx)\n  }\n\n  useKeyboard((key) => {\n    switch (key.name) {\n      case \"escape\":\n        setSelected(-1)\n        break\n      case \"`\":\n        renderer.console.toggle()\n        break\n      case \"t\":\n        renderer.toggleDebugOverlay()\n        break\n      case \"g\":\n        if (key.ctrl) {\n          renderer.dumpHitGrid()\n        }\n        break\n    }\n\n    if (key.ctrl && key.name === \"c\") {\n      key.preventDefault()\n      renderer.destroy()\n    }\n  })\n\n  const selectedScene = () => (selected() === -1 ? \"menu\" : EXAMPLES.at(selected())?.scene)\n\n  return (\n    <Switch>\n      <Match when={selectedScene() === \"diff-demo\"}>\n        <DiffDemo />\n      </Match>\n      <Match when={selectedScene() === \"line-number-demo\"}>\n        <LineNumberDemo />\n      </Match>\n      <Match when={selectedScene() === \"code-demo\"}>\n        <CodeDemo />\n      </Match>\n      <Match when={selectedScene() === \"split-mode\"}>\n        <SplitModeDemo />\n      </Match>\n      <Match when={selectedScene() === \"input-demo\"}>\n        <InputScene />\n      </Match>\n      <Match when={selectedScene() === \"autocomplete-demo\"}>\n        <AutocompleteDemo />\n      </Match>\n      <Match when={selectedScene() === \"textarea-demo\"}>\n        <TextareaDemo />\n      </Match>\n      <Match when={selectedScene() === \"textarea-minimal-demo\"}>\n        <TextareaMinimalDemo />\n      </Match>\n      <Match when={selectedScene() === \"text-truncation-demo\"}>\n        <TextTruncationDemo />\n      </Match>\n      <Match when={selectedScene() === \"mouse-demo\"}>\n        <MouseScene />\n      </Match>\n      <Match when={selectedScene() === \"text-style-scene\"}>\n        <TextStyleScene />\n      </Match>\n      <Match when={selectedScene() === \"text-selection-demo\"}>\n        <TextSelectionDemo />\n      </Match>\n      <Match when={selectedScene() === \"tab-select-demo\"}>\n        <TabSelectDemo />\n      </Match>\n      <Match when={selectedScene() === \"extend-demo\"}>\n        <ExtendDemo />\n      </Match>\n      <Match when={selectedScene() === \"scroll-demo\"}>\n        <ScrollDemo />\n      </Match>\n      <Match when={selectedScene() === \"scroll-demo-index\"}>\n        <ScrollDemoIndex />\n      </Match>\n      <Match when={selectedScene() === \"custom-scroll-accel-demo\"}>\n        <CustomScrollAccelDemo />\n      </Match>\n      <Match when={selectedScene() === \"session-scrollbox\"}>\n        <Session />\n      </Match>\n      <Match when={selectedScene() === \"plugin-slots-demo\"}>\n        <PluginSlotsDemo />\n      </Match>\n      <Match when={selectedScene() === \"plugin-slots-external-demo\"}>\n        <ExternalPluginSlotsDemo />\n      </Match>\n      <Match when={selected() === -1}>\n        <box style={{ height: terminalDimensions().height, backgroundColor: \"#001122\", padding: 1 }}>\n          <box alignItems=\"center\">\n            <ascii_font\n              style={{\n                font: titleFont,\n              }}\n              text={titleText}\n            />\n          </box>\n          <box\n            title=\"Examples\"\n            style={{\n              border: true,\n              flexGrow: 1,\n              marginTop: 1,\n              borderStyle: \"single\",\n              titleAlignment: \"center\",\n              focusedBorderColor: \"#00AAFF\",\n            }}\n          >\n            <select\n              focused\n              onSelect={(index) => {\n                handleSelect(index)\n              }}\n              options={EXAMPLES.map((ex, i) => ({\n                name: ex.name,\n                description: ex.description,\n                value: i,\n              }))}\n              style={{\n                height: \"100%\",\n                backgroundColor: \"transparent\",\n                focusedBackgroundColor: \"transparent\",\n                selectedBackgroundColor: \"#334455\",\n                selectedTextColor: \"#FFFF00\",\n                descriptionColor: \"#888888\",\n              }}\n              showScrollIndicator\n              wrapSelection\n              fastScrollStep={5}\n            />\n          </box>\n          <TimeToFirstDraw />\n          <text style={{ fg: \"#AAAAAA\", marginTop: 1, marginLeft: 1, marginRight: 1 }}>\n            Use ↑↓ or j/k to navigate, Shift+↑↓ or Shift+j/k for fast scroll, Enter to run, Escape to return, ` to\n            toggle console, ctrl+c to quit\n          </text>\n        </box>\n      </Match>\n    </Switch>\n  )\n}\n\nexport default ExampleSelector\n"
  },
  {
    "path": "packages/solid/examples/components/animation-demo.tsx",
    "content": "import { render, useTerminalDimensions, useTimeline } from \"@opentui/solid\"\nimport { createSignal, For } from \"solid-js\"\n\nexport const SplitModeDemo = () => {\n  const tDims = useTerminalDimensions()\n\n  const [animatedSystem, setAnimatedSystem] = createSignal({\n    cpu: 0,\n    memory: 0,\n    network: 0,\n    disk: 0,\n  })\n\n  const systems = [\n    { name: \"CPU\", color: \"#6a5acd\", y: 6, animKey: \"cpu\" },\n    { name: \"MEM\", color: \"#4682b4\", y: 7, animKey: \"memory\" },\n    { name: \"NET\", color: \"#20b2aa\", y: 8, animKey: \"network\" },\n    { name: \"DSK\", color: \"#daa520\", y: 9, animKey: \"disk\" },\n  ] as const\n\n  const timeline = useTimeline({\n    duration: 8000,\n    loop: false,\n  })\n\n  timeline.add(\n    animatedSystem(),\n    {\n      cpu: 85,\n      memory: 70,\n      network: 95,\n      disk: 60,\n      duration: 3000,\n      ease: \"inOutQuad\",\n      onUpdate(values) {\n        setAnimatedSystem({ ...values.targets[0] })\n      },\n    },\n    0,\n  )\n\n  return (\n    <box\n      style={{\n        zIndex: 5,\n      }}\n    >\n      <box\n        title=\"SYSTEM MONITOR\"\n        titleAlignment=\"center\"\n        style={{\n          position: \"absolute\",\n          left: 2,\n          top: 5,\n          width: tDims().width - 6,\n          height: 8,\n          backgroundColor: \"#1a1a2e\",\n          zIndex: 1,\n          border: true,\n          borderStyle: \"double\",\n          borderColor: \"#4a4a4a\",\n        }}\n      >\n        <For each={systems}>\n          {(system) => (\n            <box\n              style={{\n                flexDirection: \"row\",\n                height: 1,\n                width: \"100%\",\n                paddingLeft: 1,\n                paddingRight: 2,\n              }}\n            >\n              <text\n                style={{\n                  fg: system.color,\n                  zIndex: 2,\n                  marginRight: 1,\n                }}\n              >\n                {system.name}\n              </text>\n              <box\n                style={{\n                  height: 1,\n                  backgroundColor: \"#333333\",\n                  zIndex: 1,\n                  flexGrow: 1,\n                }}\n              >\n                <box\n                  style={{\n                    width: `${animatedSystem()[system.animKey]}%`,\n                    height: 1,\n                    backgroundColor: system.color,\n                    zIndex: 2,\n                  }}\n                />\n              </box>\n            </box>\n          )}\n        </For>\n      </box>\n\n      <box\n        title=\"◇ REAL-TIME STATS ◇\"\n        titleAlignment=\"center\"\n        style={{\n          position: \"absolute\",\n          left: 2,\n          top: 14,\n          width: tDims().width - 6,\n          height: 4,\n          backgroundColor: \"#2d1b2e\",\n          zIndex: 1,\n          border: true,\n          borderStyle: \"single\",\n          borderColor: \"#8a4a8a\",\n        }}\n      />\n\n      <For each={[\"PACKETS\", \"CONNECTIONS\", \"PROCESSES\", \"UPTIME\"] as const}>\n        {(label, index) => (\n          <text\n            style={{\n              position: \"absolute\",\n              left: 4 + index() * 15,\n              top: 15,\n              fg: \"#9a9acd\",\n              zIndex: 2,\n            }}\n          >\n            {label}: 0\n          </text>\n        )}\n      </For>\n\n      <For each={[\"#ff6b9d\", \"#4ecdc4\", \"#ffe66d\"] as const}>\n        {(color) => (\n          <box\n            style={{\n              position: \"absolute\",\n              left: 2,\n              top: 2,\n              width: 3,\n              height: 1,\n              backgroundColor: color,\n              zIndex: 3,\n            }}\n          />\n        )}\n      </For>\n\n      <For each={[\"#ff8a80\", \"#80cbc4\", \"#fff176\"] as const}>\n        {(color, index) => (\n          <box\n            style={{\n              position: \"absolute\",\n              left: tDims().width - 8 + index() * 2,\n              top: 1,\n              width: 1,\n              height: 1,\n              backgroundColor: color,\n              zIndex: 3,\n            }}\n          />\n        )}\n      </For>\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  render(SplitModeDemo)\n}\n"
  },
  {
    "path": "packages/solid/examples/components/autocomplete-demo.tsx",
    "content": "import { type InputRenderable, type BoxRenderable, type KeyEvent, TextAttributes } from \"@opentui/core\"\nimport { createSignal, createMemo, For, Show, onMount } from \"solid-js\"\nimport { createStore } from \"solid-js/store\"\nimport { useRenderer } from \"@opentui/solid\"\n\ntype AutocompleteOption = {\n  display: string\n  description?: string\n}\n\nconst SAMPLE_OPTIONS: AutocompleteOption[] = [\n  { display: \"alice\", description: \"Alice Johnson\" },\n  { display: \"bob\", description: \"Bob Smith\" },\n  { display: \"charlie\", description: \"Charlie Brown\" },\n  { display: \"diana\", description: \"Diana Prince\" },\n  { display: \"eve\", description: \"Eve Anderson\" },\n  { display: \"frank\", description: \"Frank Miller\" },\n  { display: \"grace\", description: \"Grace Hopper\" },\n  { display: \"henry\", description: \"Henry Ford\" },\n  { display: \"iris\", description: \"Iris Chang\" },\n  { display: \"jack\", description: \"Jack Dorsey\" },\n  { display: \"karen\", description: \"Karen Walker\" },\n  { display: \"leo\", description: \"Leo Martinez\" },\n  { display: \"maria\", description: \"Maria Garcia\" },\n  { display: \"noah\", description: \"Noah Wilson\" },\n  { display: \"olivia\", description: \"Olivia Taylor\" },\n  { display: \"peter\", description: \"Peter Parker\" },\n  { display: \"quinn\", description: \"Quinn Roberts\" },\n  { display: \"rachel\", description: \"Rachel Green\" },\n  { display: \"sam\", description: \"Sam Anderson\" },\n  { display: \"tina\", description: \"Tina Turner\" },\n  { display: \"uma\", description: \"Uma Thurman\" },\n  { display: \"victor\", description: \"Victor Hugo\" },\n  { display: \"wendy\", description: \"Wendy Williams\" },\n  { display: \"xavier\", description: \"Xavier Thompson\" },\n  { display: \"yuki\", description: \"Yuki Tanaka\" },\n  { display: \"zoe\", description: \"Zoe Chen\" },\n  { display: \"adam\", description: \"Adam Davis\" },\n  { display: \"bella\", description: \"Bella Rodriguez\" },\n  { display: \"carlos\", description: \"Carlos Sanchez\" },\n  { display: \"derek\", description: \"Derek Lee\" },\n  { display: \"emma\", description: \"Emma Watson\" },\n  { display: \"felix\", description: \"Felix White\" },\n  { display: \"gina\", description: \"Gina Lopez\" },\n  { display: \"harry\", description: \"Harry Potter\" },\n  { display: \"isla\", description: \"Isla Fisher\" },\n  { display: \"james\", description: \"James Bond\" },\n  { display: \"kate\", description: \"Kate Middleton\" },\n  { display: \"luke\", description: \"Luke Skywalker\" },\n  { display: \"maya\", description: \"Maya Angelou\" },\n  { display: \"nick\", description: \"Nick Fury\" },\n  { display: \"oscar\", description: \"Oscar Wilde\" },\n  { display: \"paul\", description: \"Paul McCartney\" },\n  { display: \"queenie\", description: \"Queenie Goldstein\" },\n  { display: \"ryan\", description: \"Ryan Reynolds\" },\n  { display: \"sara\", description: \"Sara Connor\" },\n  { display: \"tony\", description: \"Tony Stark\" },\n  { display: \"ursula\", description: \"Ursula Le Guin\" },\n  { display: \"vera\", description: \"Vera Wang\" },\n  { display: \"will\", description: \"Will Smith\" },\n  { display: \"xena\", description: \"Xena Warrior\" },\n  { display: \"yasmin\", description: \"Yasmin Khan\" },\n  { display: \"zack\", description: \"Zack Morris\" },\n  { display: \"amber\", description: \"Amber Heard\" },\n  { display: \"blake\", description: \"Blake Lively\" },\n  { display: \"chris\", description: \"Chris Evans\" },\n  { display: \"donna\", description: \"Donna Noble\" },\n  { display: \"ethan\", description: \"Ethan Hunt\" },\n  { display: \"fiona\", description: \"Fiona Apple\" },\n  { display: \"george\", description: \"George Clooney\" },\n  { display: \"hannah\", description: \"Hannah Montana\" },\n  { display: \"ivan\", description: \"Ivan Drago\" },\n  { display: \"julia\", description: \"Julia Roberts\" },\n  { display: \"keith\", description: \"Keith Richards\" },\n  { display: \"linda\", description: \"Linda Hamilton\" },\n  { display: \"mark\", description: \"Mark Zuckerberg\" },\n  { display: \"nina\", description: \"Nina Simone\" },\n  { display: \"oliver\", description: \"Oliver Twist\" },\n  { display: \"penny\", description: \"Penny Lane\" },\n  { display: \"quincy\", description: \"Quincy Jones\" },\n  { display: \"rose\", description: \"Rose Tyler\" },\n  { display: \"steve\", description: \"Steve Jobs\" },\n  { display: \"tracy\", description: \"Tracy Chapman\" },\n  { display: \"umar\", description: \"Umar Johnson\" },\n  { display: \"violet\", description: \"Violet Baudelaire\" },\n  { display: \"wade\", description: \"Wade Wilson\" },\n  { display: \"xander\", description: \"Xander Harris\" },\n  { display: \"yvonne\", description: \"Yvonne Strahovski\" },\n  { display: \"zeus\", description: \"Zeus King\" },\n]\n\nconst AutocompleteDemo = () => {\n  const renderer = useRenderer()\n  let input: InputRenderable\n  let anchor: BoxRenderable\n\n  const [inputValue, setInputValue] = createSignal(\"\")\n  const [store, setStore] = createStore({\n    visible: false,\n    selected: 0,\n    index: 0,\n    position: { x: 0, y: 0, width: 0 },\n  })\n\n  const filter = createMemo(() => {\n    if (!store.visible) return \"\"\n    return inputValue().substring(store.index + 1)\n  })\n\n  const options = createMemo(() => {\n    const filterText = filter().toLowerCase()\n    if (!filterText) return SAMPLE_OPTIONS.slice(0, 8)\n    return SAMPLE_OPTIONS.filter(\n      (opt) => opt.display.toLowerCase().includes(filterText) || opt.description?.toLowerCase().includes(filterText),\n    ).slice(0, 8)\n  })\n\n  const height = createMemo(() => {\n    if (options().length) return Math.min(8, options().length)\n    return 1\n  })\n\n  function move(direction: -1 | 1) {\n    if (!store.visible) return\n    if (!options().length) return\n    let next = store.selected + direction\n    if (next < 0) next = options().length - 1\n    if (next >= options().length) next = 0\n    setStore(\"selected\", next)\n  }\n\n  function select() {\n    const selected = options()[store.selected]\n    if (!selected) return\n    const newValue = inputValue().slice(0, store.index) + \"@\" + selected.display + \" \"\n    setInputValue(newValue)\n    input.value = newValue\n    input.cursorPosition = newValue.length\n    hide()\n  }\n\n  function show() {\n    setStore({\n      visible: true,\n      selected: 0,\n      index: input.cursorPosition,\n      position: {\n        x: anchor.x,\n        y: anchor.y,\n        width: anchor.width,\n      },\n    })\n  }\n\n  function hide() {\n    setStore(\"visible\", false)\n  }\n\n  function handleKeyDown(e: KeyEvent) {\n    if (store.visible) {\n      if (e.name === \"up\") {\n        e.preventDefault()\n        move(-1)\n      }\n      if (e.name === \"down\") {\n        e.preventDefault()\n        move(1)\n      }\n      if (e.name === \"escape\") {\n        e.preventDefault()\n        hide()\n      }\n      if (e.name === \"return\") {\n        e.preventDefault()\n        select()\n      }\n    } else {\n      if (e.name === \"@\") {\n        const last = inputValue().at(-1)\n        if (last === \" \" || last === undefined) {\n          show()\n        }\n      }\n    }\n  }\n\n  function handleInput(value: string) {\n    setInputValue(value)\n    if (store.visible && value.length <= store.index) {\n      hide()\n    }\n  }\n\n  onMount(() => {\n    renderer.setBackgroundColor(\"#1a1b26\")\n    input.focus()\n  })\n\n  return (\n    <box height=\"100%\" width=\"100%\" flexDirection=\"column\" gap={1} padding={2}>\n      <box>\n        <text attributes={TextAttributes.BOLD} fg=\"#7aa2f7\">\n          Autocomplete Demo\n        </text>\n        <text attributes={TextAttributes.DIM} fg=\"#9aa5ce\">\n          Type @ to trigger autocomplete. Use arrow keys to navigate, Enter to select.\n        </text>\n      </box>\n\n      <box ref={(r) => (anchor = r)} flexDirection=\"column\">\n        <box border borderColor=\"#3b4261\" padding={1}>\n          <input\n            ref={(r) => (input = r)}\n            value={inputValue()}\n            onInput={handleInput}\n            onKeyDown={handleKeyDown}\n            placeholder=\"Type @ to mention someone...\"\n            cursorColor=\"#7aa2f7\"\n            backgroundColor=\"#1a1b26\"\n            focusedBackgroundColor=\"#1a1b26\"\n          />\n        </box>\n\n        {/* Autocomplete popup */}\n        <box\n          visible={store.visible}\n          position=\"absolute\"\n          top={store.position.y - height()}\n          left={store.position.x}\n          width={store.position.width}\n          zIndex={100}\n          border\n          borderColor=\"#7aa2f7\"\n        >\n          <box backgroundColor=\"#24283b\" height={height()}>\n            <For\n              each={options()}\n              fallback={\n                <box paddingLeft={1} paddingRight={1}>\n                  <text fg=\"#9aa5ce\">No matching items</text>\n                </box>\n              }\n            >\n              {(option, index) => (\n                <box\n                  paddingLeft={1}\n                  paddingRight={1}\n                  backgroundColor={index() === store.selected ? \"#7aa2f7\" : undefined}\n                  flexDirection=\"row\"\n                >\n                  <text fg={index() === store.selected ? \"#1a1b26\" : \"#c0caf5\"}>@{option.display}</text>\n                  <Show when={option.description}>\n                    <text fg={index() === store.selected ? \"#1a1b26\" : \"#9aa5ce\"}> - {option.description}</text>\n                  </Show>\n                </box>\n              )}\n            </For>\n          </box>\n        </box>\n      </box>\n\n      <box marginTop={2}>\n        <text fg=\"#9aa5ce\">Current input: </text>\n        <text fg=\"#c0caf5\">{inputValue() || \"(empty)\"}</text>\n      </box>\n    </box>\n  )\n}\n\nexport default AutocompleteDemo\n"
  },
  {
    "path": "packages/solid/examples/components/code-demo.tsx",
    "content": "import { SyntaxStyle, RGBA } from \"@opentui/core\"\n\nexport function CodeDemo() {\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    keyword: { fg: RGBA.fromHex(\"#ff6b6b\"), bold: true }, // red, bold\n    string: { fg: RGBA.fromHex(\"#51cf66\") }, // green\n    comment: { fg: RGBA.fromHex(\"#868e96\"), italic: true }, // gray, italic\n    number: { fg: RGBA.fromHex(\"#ffd43b\") }, // yellow\n    default: { fg: RGBA.fromHex(\"#ffffff\") }, // white\n  })\n\n  const codeExample = `function hello() {\n  // This is a comment\n  const message = \"Hello, world!\"\n  const count = 42\n  return message + \" \" + count\n}`\n\n  return (\n    <box title=\"Code Syntax Highlighting Demo\" width={60} height={15}>\n      <code content={codeExample} filetype=\"javascript\" syntaxStyle={syntaxStyle} />\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/custom-scroll-accel-demo.tsx",
    "content": "import { createMemo, For, createSignal } from \"solid-js\"\nimport { LinearScrollAccel, MacOSScrollAccel } from \"@opentui/core\"\nimport { useKeyboard } from \"@opentui/solid\"\n\n/**\n * Custom scroll acceleration that applies a simple quadratic curve\n */\nclass QuadraticScrollAccel {\n  private lastTickTime = 0\n  private tickCount = 0\n  private readonly streakTimeout = 150\n  private readonly maxMultiplier = 3\n\n  tick(now = Date.now()): number {\n    const dt = this.lastTickTime ? now - this.lastTickTime : Infinity\n\n    // Reset streak if too much time has passed\n    if (dt === Infinity || dt > this.streakTimeout) {\n      this.lastTickTime = now\n      this.tickCount = 0\n      return 1\n    }\n\n    this.lastTickTime = now\n    this.tickCount++\n\n    // Apply quadratic acceleration: multiplier grows with consecutive ticks\n    // Formula: 1 + (tickCount / 8)^2\n    const multiplier = 1 + Math.pow(this.tickCount / 8, 2)\n\n    return Math.min(multiplier, this.maxMultiplier)\n  }\n\n  reset(): void {\n    this.lastTickTime = 0\n    this.tickCount = 0\n  }\n}\n\n/**\n * Custom scroll acceleration with aggressive exponential curve\n */\nclass AggressiveScrollAccel {\n  private lastTickTime = 0\n  private velocityHistory: number[] = []\n  private readonly historySize = 4\n  private readonly streakTimeout = 180\n\n  tick(now = Date.now()): number {\n    const dt = this.lastTickTime ? now - this.lastTickTime : Infinity\n\n    if (dt === Infinity || dt > this.streakTimeout) {\n      this.lastTickTime = now\n      this.velocityHistory = []\n      return 1\n    }\n\n    this.lastTickTime = now\n    this.velocityHistory.push(dt)\n\n    if (this.velocityHistory.length > this.historySize) {\n      this.velocityHistory.shift()\n    }\n\n    const avgInterval = this.velocityHistory.reduce((a, b) => a + b, 0) / this.velocityHistory.length\n\n    // More aggressive curve: smaller intervals = higher multiplier\n    // Scaled down to be less extreme\n    const multiplier = 1 + Math.pow(120 / avgInterval, 1.2)\n\n    return Math.min(multiplier, 4)\n  }\n\n  reset(): void {\n    this.lastTickTime = 0\n    this.velocityHistory = []\n  }\n}\n\nexport const CustomScrollAccelDemo = () => {\n  const items = createMemo(() => Array.from({ length: 1000 }).map((_, i) => ({ count: i + 1 })))\n  const [accelType, setAccelType] = createSignal<\"linear\" | \"macos\" | \"quadratic\" | \"aggressive\">(\"macos\")\n\n  const scrollAcceleration = createMemo(() => {\n    switch (accelType()) {\n      case \"linear\":\n        return new LinearScrollAccel()\n      case \"macos\":\n        return new MacOSScrollAccel({ A: 0.5, tau: 4, maxMultiplier: 4 })\n      case \"quadratic\":\n        return new QuadraticScrollAccel()\n      case \"aggressive\":\n        return new AggressiveScrollAccel()\n    }\n  })\n\n  const modeNames = {\n    linear: \"Linear (no accel)\",\n    macos: \"macOS (smooth)\",\n    quadratic: \"Quadratic\",\n    aggressive: \"Aggressive\",\n  }\n\n  useKeyboard((key) => {\n    if (key.raw === \"1\") setAccelType(\"linear\")\n    else if (key.raw === \"2\") setAccelType(\"macos\")\n    else if (key.raw === \"3\") setAccelType(\"quadratic\")\n    else if (key.raw === \"4\") setAccelType(\"aggressive\")\n  })\n\n  return (\n    <box\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        flexDirection: \"column\",\n      }}\n    >\n      <box\n        style={{\n          width: \"100%\",\n          paddingLeft: 2,\n          paddingRight: 2,\n          paddingTop: 1,\n          paddingBottom: 1,\n          backgroundColor: \"#24283b\",\n          flexShrink: 0,\n        }}\n      >\n        <text\n          content={`Scroll Acceleration: ${modeNames[accelType()]} (Press 1=Linear, 2=macOS, 3=Quadratic, 4=Aggressive)`}\n        />\n      </box>\n\n      <scrollbox\n        style={{\n          width: \"100%\",\n          flexGrow: 1,\n          rootOptions: {\n            backgroundColor: \"#24283b\",\n            border: true,\n          },\n          wrapperOptions: {\n            backgroundColor: \"#1f2335\",\n          },\n          viewportOptions: {\n            backgroundColor: \"#1a1b26\",\n          },\n          contentOptions: {\n            backgroundColor: \"#16161e\",\n          },\n          scrollbarOptions: {\n            showArrows: true,\n            trackOptions: {\n              foregroundColor: \"#7aa2f7\",\n              backgroundColor: \"#414868\",\n            },\n          },\n        }}\n        scrollAcceleration={scrollAcceleration()}\n        focused\n      >\n        <For each={items()}>\n          {(item) => (\n            <box\n              style={{\n                width: \"100%\",\n                padding: 1,\n                marginBottom: 1,\n                backgroundColor: item.count % 2 === 0 ? \"#292e42\" : \"#2f3449\",\n              }}\n            >\n              <text content={`Item ${item.count}`} />\n            </box>\n          )}\n        </For>\n      </scrollbox>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/diff-demo.tsx",
    "content": "import { createSignal } from \"solid-js\"\nimport { SyntaxStyle } from \"@opentui/core\"\nimport { useKeyboard } from \"@opentui/solid\"\n\nexport default function DiffDemo() {\n  const [currentView, setCurrentView] = createSignal<\"unified\" | \"split\">(\"unified\")\n  const [showLineNumbers, setShowLineNumbers] = createSignal(true)\n\n  const exampleDiff = `--- a/calculator.ts\n+++ b/calculator.ts\n@@ -1,15 +1,20 @@\n class Calculator {\n   add(a: number, b: number): number {\n     return a + b;\n   }\n \n-  subtract(a: number, b: number): number {\n-    return a - b;\n+  subtract(a: number, b: number, c: number = 0): number {\n+    return a - b - c;\n   }\n \n   multiply(a: number, b: number): number {\n     return a * b;\n   }\n+\n+  divide(a: number, b: number): number {\n+    if (b === 0) {\n+      throw new Error(\"Division by zero\");\n+    }\n+    return a / b;\n+  }\n }`\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    keyword: { fg: \"#C792EA\" } as any,\n    \"keyword.import\": { fg: \"#C792EA\" } as any,\n    string: { fg: \"#C3E88D\" } as any,\n    comment: { fg: \"#546E7A\" } as any,\n    number: { fg: \"#F78C6C\" } as any,\n    boolean: { fg: \"#F78C6C\" } as any,\n    constant: { fg: \"#F78C6C\" } as any,\n    function: { fg: \"#82AAFF\" } as any,\n    \"function.call\": { fg: \"#82AAFF\" } as any,\n    constructor: { fg: \"#FFCB6B\" } as any,\n    type: { fg: \"#FFCB6B\" } as any,\n    operator: { fg: \"#89DDFF\" } as any,\n    variable: { fg: \"#EEFFFF\" } as any,\n    property: { fg: \"#89DDFF\" } as any,\n    bracket: { fg: \"#FFFFFF\" } as any,\n    punctuation: { fg: \"#FFFFFF\" } as any,\n    default: { fg: \"#A6ACCD\" } as any,\n  })\n\n  useKeyboard((key) => {\n    if (key.name === \"v\" && !key.ctrl && !key.meta) {\n      toggleView()\n    } else if (key.name === \"l\" && !key.ctrl && !key.meta) {\n      toggleLineNumbers()\n    }\n  })\n\n  const toggleView = () => {\n    setCurrentView(currentView() === \"unified\" ? \"split\" : \"unified\")\n  }\n\n  const toggleLineNumbers = () => {\n    setShowLineNumbers(!showLineNumbers())\n  }\n\n  return (\n    <box flexDirection=\"column\" width=\"100%\" height=\"100%\" gap={1}>\n      <box flexDirection=\"column\" backgroundColor=\"#0D1117\" padding={1} border borderColor=\"#30363D\">\n        <text fg=\"#4ECDC4\">Diff Demo - Unified & Split View</text>\n        <text fg=\"#888888\">Keybindings:</text>\n        <text fg=\"#AAAAAA\"> V - Toggle view ({currentView().toUpperCase()})</text>\n        <text fg=\"#AAAAAA\"> L - Toggle line numbers ({showLineNumbers() ? \"ON\" : \"OFF\"})</text>\n      </box>\n\n      <box flexGrow={1} border borderStyle=\"single\" borderColor=\"#4ECDC4\" backgroundColor=\"#0D1117\">\n        <diff\n          diff={exampleDiff}\n          view={currentView()}\n          filetype=\"typescript\"\n          syntaxStyle={syntaxStyle}\n          showLineNumbers={showLineNumbers()}\n          addedBg=\"#1a4d1a\"\n          removedBg=\"#4d1a1a\"\n          addedSignColor=\"#22c55e\"\n          removedSignColor=\"#ef4444\"\n          lineNumberFg=\"#6b7280\"\n          lineNumberBg=\"#161b22\"\n          width=\"100%\"\n          height=\"100%\"\n        />\n      </box>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/extend-demo.tsx",
    "content": "import { BoxRenderable, OptimizedBuffer, RGBA, type BoxOptions, type RenderContext } from \"@opentui/core\"\nimport { extend } from \"@opentui/solid\"\n\n// Custom renderable that extends BoxRenderable\nclass ConsoleButtonRenderable extends BoxRenderable {\n  private _label: string = \"Button\"\n\n  constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {\n    super(ctx, options)\n\n    if (options.label) {\n      this._label = options.label\n    }\n\n    // Set some default styling for buttons\n    this.borderStyle = \"single\"\n    this.padding = 2\n  }\n\n  protected override renderSelf(buffer: OptimizedBuffer): void {\n    super.renderSelf(buffer)\n\n    const centerX = this.x + Math.floor(this.width / 2 - this._label.length / 2)\n    const centerY = this.y + Math.floor(this.height / 2)\n\n    buffer.drawText(this._label, centerX, centerY, RGBA.fromInts(255, 255, 255, 255))\n  }\n\n  get label(): string {\n    return this._label\n  }\n\n  set label(value: string) {\n    this._label = value\n    this.requestRender()\n  }\n}\n\n// TypeScript module augmentation for proper typing\ndeclare module \"@opentui/solid\" {\n  interface OpenTUIComponents {\n    consoleButton: typeof ConsoleButtonRenderable\n  }\n}\n\n// Extend the component catalogue\nextend({ consoleButton: ConsoleButtonRenderable })\n\n// Example usage component\nexport default function ExtendExample() {\n  return (\n    <consoleButton\n      label=\"Another Button\"\n      style={{\n        border: true,\n        backgroundColor: \"green\",\n      }}\n      onMouseUp={() => {\n        console.log(\"Mouse up\")\n      }}\n    />\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/external-plugin-path.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { join } from \"node:path\"\nimport { pathToFileURL } from \"node:url\"\nimport { resolveExternalPluginCandidates } from \"./external-plugin-path\"\n\ndescribe(\"external plugin path\", () => {\n  it(\"prefers the dist sibling plugin before the source plugin\", () => {\n    const root = \"/repo/packages/solid/examples\"\n    const dist = join(root, \"dist\", \"darwin-arm64\")\n    const source = join(root, \"components\", \"external-plugin-slots-demo.tsx\")\n    const candidates = resolveExternalPluginCandidates({\n      cwd: dist,\n      execPath: join(dist, \"opentui-solid-examples\"),\n      moduleUrl: pathToFileURL(source).href,\n    })\n\n    expect(candidates[0]).toBe(join(dist, \".plugin\", \"index.tsx\"))\n    expect(candidates.indexOf(join(dist, \".plugin\", \"index.tsx\"))).toBeLessThan(\n      candidates.indexOf(join(root, \".plugin\", \"index.tsx\")),\n    )\n  })\n\n  it(\"supports running from the workspace root in dev\", () => {\n    const cwd = \"/repo\"\n    const root = join(cwd, \"packages\", \"solid\", \"examples\")\n    const source = join(root, \"components\", \"external-plugin-slots-demo.tsx\")\n    const candidates = resolveExternalPluginCandidates({\n      cwd,\n      execPath: \"/tmp/bun\",\n      moduleUrl: pathToFileURL(source).href,\n    })\n\n    expect(candidates).toContain(join(root, \".plugin\", \"index.tsx\"))\n  })\n})\n"
  },
  {
    "path": "packages/solid/examples/components/external-plugin-path.ts",
    "content": "import { dirname, isAbsolute, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nconst defaultPluginEntry = \".plugin/index.tsx\"\n\ntype ResolveExternalPluginCandidatesInput = {\n  cwd: string\n  execPath: string\n  moduleUrl: string\n  envPath?: string\n}\n\nfunction normalizeExternalPluginPath(input: string, cwd: string): string {\n  if (input.startsWith(\"file://\")) {\n    return fileURLToPath(input)\n  }\n\n  if (isAbsolute(input)) {\n    return input\n  }\n\n  return resolve(cwd, input)\n}\n\nexport function resolveExternalPluginCandidates(input: ResolveExternalPluginCandidatesInput): string[] {\n  const paths = new Set<string>()\n  const moduleDir = dirname(fileURLToPath(input.moduleUrl))\n  const execDir = dirname(input.execPath)\n\n  if (input.envPath && input.envPath.trim().length > 0) {\n    paths.add(normalizeExternalPluginPath(input.envPath.trim(), input.cwd))\n  }\n\n  paths.add(resolve(input.cwd, defaultPluginEntry))\n  paths.add(join(execDir, defaultPluginEntry))\n  paths.add(resolve(execDir, \"..\", defaultPluginEntry))\n  paths.add(resolve(moduleDir, \"..\", defaultPluginEntry))\n  paths.add(resolve(input.cwd, \"packages\", \"solid\", \"examples\", defaultPluginEntry))\n  paths.add(resolve(execDir, \"..\", \"..\", defaultPluginEntry))\n\n  return [...paths]\n}\n"
  },
  {
    "path": "packages/solid/examples/components/external-plugin-runtime.ts",
    "content": "import { plugin as registerBunPlugin } from \"bun\"\nimport * as coreRuntime from \"@opentui/core\"\nimport * as core3dRuntime from \"@opentui/core/3d\"\nimport {\n  createRuntimePlugin,\n  isCoreRuntimeModuleSpecifier,\n  runtimeModuleIdForSpecifier,\n  type RuntimeModuleEntry,\n} from \"@opentui/core/runtime-plugin\"\nimport * as solidRuntime from \"@opentui/solid\"\nimport { ensureSolidTransformPlugin } from \"@opentui/solid/bun-plugin\"\nimport * as solidJsRuntime from \"solid-js\"\nimport * as solidJsStoreRuntime from \"solid-js/store\"\n\nconst externalPluginRuntimeSupportInstalledKey = Symbol.for(\"opentui.solid.examples.external-plugin-runtime\")\n\ntype ExternalPluginRuntimeSupportState = typeof globalThis & {\n  [externalPluginRuntimeSupportInstalledKey]?: boolean\n}\n\nconst additionalRuntimeModules: Record<string, RuntimeModuleEntry> = {\n  \"@opentui/core/3d\": core3dRuntime as Record<string, unknown>,\n  \"@opentui/solid\": solidRuntime as Record<string, unknown>,\n  \"solid-js\": solidJsRuntime as Record<string, unknown>,\n  \"solid-js/store\": solidJsStoreRuntime as Record<string, unknown>,\n}\n\nconst resolveRuntimeSpecifier = (specifier: string): string | null => {\n  if (!isCoreRuntimeModuleSpecifier(specifier) && !additionalRuntimeModules[specifier]) {\n    return null\n  }\n\n  return runtimeModuleIdForSpecifier(specifier)\n}\n\nexport function ensureExternalPluginRuntimeSupport(): boolean {\n  const state = globalThis as ExternalPluginRuntimeSupportState\n\n  if (state[externalPluginRuntimeSupportInstalledKey]) {\n    return false\n  }\n\n  ensureSolidTransformPlugin({\n    moduleName: runtimeModuleIdForSpecifier(\"@opentui/solid\"),\n    resolvePath(specifier) {\n      return resolveRuntimeSpecifier(specifier)\n    },\n  })\n\n  registerBunPlugin(\n    createRuntimePlugin({\n      core: coreRuntime as Record<string, unknown>,\n      additional: additionalRuntimeModules,\n    }),\n  )\n\n  state[externalPluginRuntimeSupportInstalledKey] = true\n  return true\n}\n\nensureExternalPluginRuntimeSupport()\n"
  },
  {
    "path": "packages/solid/examples/components/external-plugin-slots-demo.tsx",
    "content": "import { existsSync, readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport process from \"node:process\"\nimport { pathToFileURL } from \"node:url\"\nimport \"./external-plugin-runtime\"\nimport {\n  Slot,\n  createSolidSlotRegistry,\n  type SlotMode,\n  type SolidPlugin,\n  useKeyboard,\n  useRenderer,\n} from \"@opentui/solid\"\nimport { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show } from \"solid-js\"\nimport { resolveExternalPluginCandidates } from \"./external-plugin-path\"\n\nconst STATUSBAR_LABEL = \"host-status\"\nconst SIDEBAR_SECTION = \"external-plugins\"\nconst EXTERNAL_PLUGIN_PATH_ENV = \"OPENTUI_SOLID_EXTERNAL_PLUGIN_PATH\"\nconst EXTERNAL_PLUGIN_PACKAGE_JSON = \"package.json\"\nconst MAX_INSTALL_OUTPUT_LENGTH = 1200\nconst BUN_INSTALL_COMMAND = [\"install\", \"--no-save\"]\nconst BUN_BE_BUN_ENV = \"BUN_BE_BUN\"\nconst NODE_MODULES_DIR = \"node_modules\"\n\nconst installedPluginDependencyManifestsByDir = new Map<string, string>()\n\ntype ExternalPluginSlots = {\n  statusbar: { label: string }\n  sidebar: { section: string }\n}\n\ntype ExternalPluginContext = {\n  appName: string\n  version: string\n}\n\ntype ExternalPluginModule = {\n  loadExternalPlugin(): SolidPlugin<ExternalPluginSlots, ExternalPluginContext>\n}\n\nfunction resolveExternalPluginPath(): string {\n  const candidates = resolveExternalPluginCandidates({\n    cwd: process.cwd(),\n    execPath: process.execPath,\n    moduleUrl: import.meta.url,\n    envPath: process.env[EXTERNAL_PLUGIN_PATH_ENV],\n  })\n\n  for (const candidate of candidates) {\n    if (existsSync(candidate)) {\n      return candidate\n    }\n  }\n\n  throw new Error(`Unable to locate external plugin. Checked: ${candidates.join(\", \")}`)\n}\n\nfunction formatInstallOutput(stdout: string, stderr: string): string {\n  const output = [stdout.trim(), stderr.trim()].filter((line) => line.length > 0).join(\"\\n\")\n\n  if (output.length <= MAX_INSTALL_OUTPUT_LENGTH) {\n    return output\n  }\n\n  return `${output.slice(0, MAX_INSTALL_OUTPUT_LENGTH)}\\n...(truncated)...`\n}\n\nfunction readPluginPackageManifest(packageJsonPath: string): string {\n  try {\n    return readFileSync(packageJsonPath, \"utf8\")\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error)\n    throw new Error(`Failed to read external plugin package manifest at ${packageJsonPath}: ${message}`)\n  }\n}\n\nfunction ensureExternalPluginDependencies(pluginEntryPath: string): void {\n  const pluginDir = dirname(pluginEntryPath)\n  const pluginPackageJson = join(pluginDir, EXTERNAL_PLUGIN_PACKAGE_JSON)\n  const pluginNodeModulesDir = join(pluginDir, NODE_MODULES_DIR)\n\n  if (!existsSync(pluginPackageJson)) {\n    return\n  }\n\n  const packageManifest = readPluginPackageManifest(pluginPackageJson)\n  const cachedManifest = installedPluginDependencyManifestsByDir.get(pluginDir)\n\n  if (cachedManifest === packageManifest && existsSync(pluginNodeModulesDir)) {\n    return\n  }\n\n  const install = Bun.spawnSync([process.execPath, ...BUN_INSTALL_COMMAND], {\n    cwd: pluginDir,\n    env: {\n      ...process.env,\n      [BUN_BE_BUN_ENV]: \"1\",\n    },\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n  })\n\n  if (install.exitCode !== 0) {\n    const output = formatInstallOutput(install.stdout.toString(), install.stderr.toString())\n    const details = output.length > 0 ? `\\n${output}` : \"\"\n    throw new Error(`Failed to install external plugin dependencies in ${pluginDir}.${details}`)\n  }\n\n  installedPluginDependencyManifestsByDir.set(pluginDir, packageManifest)\n}\n\nasync function loadExternalPluginFromDisk(\n  nonce: number,\n): Promise<{ path: string; plugin: SolidPlugin<ExternalPluginSlots, ExternalPluginContext> }> {\n  const path = resolveExternalPluginPath()\n  ensureExternalPluginDependencies(path)\n  const url = pathToFileURL(path)\n  url.searchParams.set(\"reload\", `${nonce}`)\n  url.searchParams.set(\"ts\", `${Date.now()}`)\n\n  const externalModule = (await import(url.href)) as Partial<ExternalPluginModule>\n\n  if (typeof externalModule.loadExternalPlugin !== \"function\") {\n    throw new Error(\"External plugin module does not export loadExternalPlugin()\")\n  }\n\n  return {\n    path,\n    plugin: externalModule.loadExternalPlugin(),\n  }\n}\n\nconst hostContext: ExternalPluginContext = {\n  appName: \"solid-external-plugin-demo\",\n  version: \"1.0.0\",\n}\n\nfunction nextStatusbarMode(mode: SlotMode): SlotMode {\n  if (mode === \"append\") {\n    return \"replace\"\n  }\n\n  if (mode === \"replace\") {\n    return \"single_winner\"\n  }\n\n  return \"append\"\n}\n\nexport default function ExternalPluginSlotsDemo() {\n  const renderer = useRenderer()\n  const registry = createSolidSlotRegistry<ExternalPluginSlots, ExternalPluginContext>(renderer, hostContext)\n  const AppSlot = Slot<ExternalPluginSlots, ExternalPluginContext>\n\n  const [statusbarMode, setStatusbarMode] = createSignal<SlotMode>(\"append\")\n  const [pluginEnabled, setPluginEnabled] = createSignal(true)\n  const [reloadNonce, setReloadNonce] = createSignal(0)\n  const [loadedPluginPath, setLoadedPluginPath] = createSignal(\"(not loaded yet)\")\n  const [lastPluginId, setLastPluginId] = createSignal(\"(none)\")\n  const [lastLoadError, setLastLoadError] = createSignal<string | null>(null)\n\n  onMount(() => {\n    renderer.setBackgroundColor(\"#000000\")\n  })\n\n  const unsubscribePluginErrors = registry.onPluginError((event) => {\n    setLastLoadError(`${event.phase}: ${event.error.message}`)\n  })\n  onCleanup(unsubscribePluginErrors)\n\n  createEffect(\n    on(\n      [pluginEnabled, reloadNonce],\n      ([currentPluginEnabled, currentReloadNonce]) => {\n        let cleanedUp = false\n        let unregisterPlugin: (() => void) | null = null\n\n        if (!currentPluginEnabled) {\n          setLastPluginId(\"(disabled)\")\n          setLastLoadError(null)\n          return\n        }\n\n        setLastLoadError(null)\n\n        void (async () => {\n          try {\n            const { path, plugin } = await loadExternalPluginFromDisk(currentReloadNonce)\n            if (cleanedUp) {\n              return\n            }\n\n            const unregister = registry.register(plugin)\n            unregisterPlugin = () => {\n              unregister()\n            }\n\n            setLoadedPluginPath(path)\n            setLastPluginId(plugin.id)\n            setLastLoadError(null)\n          } catch (error) {\n            const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error)\n            setLastPluginId(\"(load failed)\")\n            setLastLoadError(message)\n          }\n        })()\n\n        onCleanup(() => {\n          cleanedUp = true\n          if (unregisterPlugin) {\n            unregisterPlugin()\n            unregisterPlugin = null\n          }\n        })\n      },\n      { defer: false },\n    ),\n  )\n\n  useKeyboard((key) => {\n    switch (key.name) {\n      case \"m\":\n        setStatusbarMode((current) => nextStatusbarMode(current))\n        return\n      case \"p\":\n        setPluginEnabled((current) => !current)\n        return\n      case \"r\":\n        setReloadNonce((current) => current + 1)\n        return\n      case \"c\":\n        if (key.ctrl) {\n          key.preventDefault()\n          renderer.destroy()\n        }\n        return\n    }\n  })\n\n  const info = createMemo(() => {\n    return [\n      \"Solid External Plugin Slot Demo\",\n      \"\",\n      `External plugin env override: ${EXTERNAL_PLUGIN_PATH_ENV}`,\n      `External plugin resolved path: ${loadedPluginPath()}`,\n      `Last loaded plugin id: ${lastPluginId()}`,\n      `Last plugin load error: ${lastLoadError() ?? \"(none)\"}`,\n      \"\",\n      `Plugin enabled: ${pluginEnabled() ? \"ON\" : \"OFF\"} (press p)`,\n      `Statusbar mode: ${statusbarMode().toUpperCase()} (press m to cycle)`,\n      \"Press r to reload external plugin from disk and re-register.\",\n      \"\",\n      `Statusbar slot label: ${STATUSBAR_LABEL}`,\n      `Sidebar slot section: ${SIDEBAR_SECTION}`,\n      \"\",\n      \"The plugin renders external JSX components for both slots.\",\n    ].join(\"\\n\")\n  })\n\n  return (\n    <box width=\"100%\" height=\"100%\" flexDirection=\"column\" padding={1} backgroundColor=\"#020617\">\n      <box\n        height={5}\n        width=\"100%\"\n        border\n        borderStyle=\"single\"\n        borderColor=\"#334155\"\n        alignItems=\"center\"\n        flexDirection=\"row\"\n        paddingLeft={1}\n        marginBottom={1}\n      >\n        <Show when={statusbarMode()} keyed>\n          {(currentMode: SlotMode) => (\n            <AppSlot registry={registry} name=\"statusbar\" label={STATUSBAR_LABEL} mode={currentMode}>\n              <text fg=\"#94a3b8\">Fallback statusbar content</text>\n            </AppSlot>\n          )}\n        </Show>\n      </box>\n\n      <box width=\"100%\" flexGrow={1} flexDirection=\"row\">\n        <box\n          width={44}\n          border\n          borderStyle=\"single\"\n          borderColor=\"#334155\"\n          flexDirection=\"column\"\n          padding={1}\n          marginRight={1}\n        >\n          <AppSlot registry={registry} name=\"sidebar\" section={SIDEBAR_SECTION} mode=\"replace\">\n            <text fg=\"#94a3b8\">No external sidebar plugin loaded</text>\n          </AppSlot>\n        </box>\n\n        <box flexGrow={1} border borderStyle=\"single\" borderColor=\"#334155\" flexDirection=\"column\" padding={1}>\n          <text fg=\"#e2e8f0\" content={info()} />\n        </box>\n      </box>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/input-demo.tsx",
    "content": "import type { InputRenderable } from \"@opentui/core\"\nimport { usePaste, useRenderer } from \"@opentui/solid\"\nimport { createSignal, onMount } from \"solid-js\"\n\nconst InputScene = () => {\n  const renderer = useRenderer()\n  const [nameValue, setNameValue] = createSignal(\"\")\n  let inputRef: InputRenderable | null = null\n\n  usePaste((event) => {\n    inputRef?.handlePaste(event)\n  })\n\n  onMount(() => {\n    renderer.setBackgroundColor(\"#001122\")\n  })\n\n  return (\n    <box height={4} border>\n      <text>Name: {nameValue()}</text>\n      <input ref={(r) => (inputRef = r)} focused onInput={(value) => setNameValue(value)} />\n    </box>\n  )\n}\n\nexport default InputScene\n"
  },
  {
    "path": "packages/solid/examples/components/line-number-demo.tsx",
    "content": "import { RGBA, SyntaxStyle, TextAttributes } from \"@opentui/core\"\nimport { useKeyboard } from \"@opentui/solid\"\nimport { createSignal, onMount } from \"solid-js\"\n\nexport default function LineNumberDemo() {\n  const [showLineNumbers, setShowLineNumbers] = createSignal(true)\n  const [showDiffHighlights, setShowDiffHighlights] = createSignal(false)\n  const [showDiagnostics, setShowDiagnostics] = createSignal(false)\n\n  const codeContent = `function fibonacci(n: number): number {\n  if (n <= 1) return n\n  return fibonacci(n - 1) + fibonacci(n - 2)\n}\n\nconst results = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n  .map(fibonacci)\n\nconsole.log('Fibonacci sequence:', results)\n\n// Calculate the sum\nconst sum = results.reduce((acc, val) => acc + val, 0)\nconsole.log('Sum:', sum)\n\n// Find even numbers\nconst evens = results.filter(n => n % 2 === 0)\nconsole.log('Even numbers:', evens)`\n\n  const syntaxStyle = SyntaxStyle.fromStyles({\n    keyword: { fg: RGBA.fromHex(\"#C792EA\") },\n    function: { fg: RGBA.fromHex(\"#82AAFF\") },\n    string: { fg: RGBA.fromHex(\"#C3E88D\") },\n    number: { fg: RGBA.fromHex(\"#F78C6C\") },\n    comment: { fg: RGBA.fromHex(\"#546E7A\") },\n    type: { fg: RGBA.fromHex(\"#FFCB6B\") },\n    operator: { fg: RGBA.fromHex(\"#89DDFF\") },\n    variable: { fg: RGBA.fromHex(\"#EEFFFF\") },\n    default: { fg: RGBA.fromHex(\"#A6ACCD\") },\n  })\n\n  let lineNumberRef: any\n\n  onMount(() => {\n    // Set up diff highlights\n    if (showDiffHighlights()) {\n      lineNumberRef?.setLineColor(1, \"#1a4d1a\") // Line 2: added\n      lineNumberRef?.setLineSign(1, { after: \" +\", afterColor: \"#22c55e\" })\n\n      lineNumberRef?.setLineColor(5, \"#4d1a1a\") // Line 6: removed\n      lineNumberRef?.setLineSign(5, { after: \" -\", afterColor: \"#ef4444\" })\n\n      lineNumberRef?.setLineColor(10, \"#1a4d1a\") // Line 11: added\n      lineNumberRef?.setLineSign(10, { after: \" +\", afterColor: \"#22c55e\" })\n    }\n\n    // Set up diagnostics\n    if (showDiagnostics()) {\n      lineNumberRef?.setLineSign(0, { before: \"⚠️\", beforeColor: \"#f59e0b\" })\n      lineNumberRef?.setLineSign(7, { before: \"💡\", beforeColor: \"#3b82f6\" })\n      lineNumberRef?.setLineSign(13, { before: \"❌\", beforeColor: \"#ef4444\" })\n    }\n  })\n\n  useKeyboard((key) => {\n    if (key.name === \"l\" && !key.ctrl && !key.meta) {\n      toggleLineNumbers()\n    } else if (key.name === \"h\" && !key.ctrl && !key.meta) {\n      toggleDiffHighlights()\n    } else if (key.name === \"d\" && !key.ctrl && !key.meta) {\n      toggleDiagnostics()\n    }\n  })\n\n  const toggleLineNumbers = () => {\n    setShowLineNumbers(!showLineNumbers())\n  }\n\n  const toggleDiffHighlights = () => {\n    const newValue = !showDiffHighlights()\n    setShowDiffHighlights(newValue)\n\n    if (newValue) {\n      lineNumberRef?.setLineColor(1, \"#1a4d1a\")\n      lineNumberRef?.setLineSign(1, { after: \" +\", afterColor: \"#22c55e\" })\n      lineNumberRef?.setLineColor(5, \"#4d1a1a\")\n      lineNumberRef?.setLineSign(5, { after: \" -\", afterColor: \"#ef4444\" })\n      lineNumberRef?.setLineColor(10, \"#1a4d1a\")\n      lineNumberRef?.setLineSign(10, { after: \" +\", afterColor: \"#22c55e\" })\n    } else {\n      lineNumberRef?.clearAllLineColors()\n      // Clear only after signs\n      if (!showDiagnostics()) {\n        lineNumberRef?.clearAllLineSigns()\n      } else {\n        lineNumberRef?.setLineSign(1, {})\n        lineNumberRef?.setLineSign(5, {})\n        lineNumberRef?.setLineSign(10, {})\n      }\n    }\n  }\n\n  const toggleDiagnostics = () => {\n    const newValue = !showDiagnostics()\n    setShowDiagnostics(newValue)\n\n    if (newValue) {\n      lineNumberRef?.setLineSign(0, { before: \"⚠️\", beforeColor: \"#f59e0b\" })\n      lineNumberRef?.setLineSign(7, { before: \"💡\", beforeColor: \"#3b82f6\" })\n      lineNumberRef?.setLineSign(13, { before: \"❌\", beforeColor: \"#ef4444\" })\n    } else {\n      // Clear only before signs\n      if (!showDiffHighlights()) {\n        lineNumberRef?.clearAllLineSigns()\n      } else {\n        lineNumberRef?.setLineSign(0, {})\n        lineNumberRef?.setLineSign(7, {})\n        lineNumberRef?.setLineSign(13, {})\n      }\n    }\n  }\n\n  return (\n    <box flexDirection=\"column\" width=\"100%\" height=\"100%\" gap={1}>\n      <box flexDirection=\"column\" backgroundColor=\"#0D1117\" padding={1} border borderColor=\"#30363D\" flexShrink={0}>\n        <text fg=\"#4ECDC4\" attributes={TextAttributes.BOLD}>\n          Line Numbers Demo\n        </text>\n        <text fg=\"#888888\">Keybindings:</text>\n        <text fg=\"#AAAAAA\"> L - Toggle line numbers ({showLineNumbers() ? \"ON\" : \"OFF\"})</text>\n        <text fg=\"#AAAAAA\"> H - Toggle diff highlights ({showDiffHighlights() ? \"ON\" : \"OFF\"})</text>\n        <text fg=\"#AAAAAA\"> D - Toggle diagnostics ({showDiagnostics() ? \"ON\" : \"OFF\"})</text>\n      </box>\n\n      <box flexGrow={1} border borderStyle=\"single\" borderColor=\"#4ECDC4\" backgroundColor=\"#0D1117\">\n        <line_number\n          ref={lineNumberRef}\n          fg=\"#6b7280\"\n          bg=\"#161b22\"\n          minWidth={3}\n          paddingRight={1}\n          showLineNumbers={showLineNumbers()}\n          width=\"100%\"\n          height=\"100%\"\n        >\n          <code\n            content={codeContent}\n            filetype=\"typescript\"\n            syntaxStyle={syntaxStyle}\n            selectable\n            selectionBg=\"#264F78\"\n            selectionFg=\"#FFFFFF\"\n            width=\"100%\"\n            height=\"100%\"\n          />\n        </line_number>\n      </box>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/mouse-demo.tsx",
    "content": "import {\n  BoxRenderable,\n  OptimizedBuffer,\n  RGBA,\n  MouseEvent,\n  t,\n  bold,\n  underline,\n  fg,\n  type RenderContext,\n} from \"@opentui/core\"\nimport { useContext } from \"solid-js\"\nimport { RendererContext } from \"../../src/elements/hooks.js\"\n\nlet nextZIndex = 101\nclass DraggableTransparentBox extends BoxRenderable {\n  private isDragging = false\n  private dragOffsetX = 0\n  private dragOffsetY = 0\n  private alphaPercentage: number\n  private screenSizeX = 199\n  private screenSizeY = 52\n\n  protected override onResize(width: number, height: number): void {\n    // this.screenSizeX = width;\n    // this.screenSizeY = height;\n    this.screenSizeX = 199\n    this.screenSizeY = 52\n  }\n\n  constructor(ctx: RenderContext, x: number, y: number, width: number, height: number, bg: RGBA, zIndex: number) {\n    super(ctx, {\n      width,\n      height,\n      zIndex,\n      backgroundColor: bg,\n      titleAlignment: \"center\",\n      position: \"absolute\",\n      left: x,\n      top: y,\n      border: true,\n    })\n    this.alphaPercentage = Math.round(bg.a * 100)\n  }\n\n  normalizeCoordinates(x: number, y: number): { x: number; y: number } {\n    if (this.screenSizeX === 0) {\n      return { x, y }\n    }\n    return {\n      x: x / this.screenSizeX,\n      y: y / this.screenSizeY,\n    }\n  }\n\n  protected override renderSelf(buffer: OptimizedBuffer): void {\n    super.renderSelf(buffer)\n\n    const alphaText = `${this.alphaPercentage}%`\n    const centerX = this.x + Math.floor(this.width / 2 - alphaText.length / 2)\n    const centerY = this.y + Math.floor(this.height / 2)\n\n    buffer.drawText(alphaText, centerX, centerY, RGBA.fromInts(255, 255, 255, 220))\n\n    const id = RGBA.fromInts(68, 69, 69)\n\n    const nm = this.normalizeCoordinates(this.x, this.y)\n    const nms = this.normalizeCoordinates(this.x + this.width, this.y + this.height)\n\n    const topLeft = RGBA.fromValues(nm.x, nm.y, 0)\n    const bottomRight = RGBA.fromValues(nms.x, nms.y, 0)\n    buffer.drawText(\"x\", 1, 0, id, id)\n    buffer.drawText(\"x\", 1, 0, topLeft, topLeft)\n    buffer.drawText(\"x\", 2, 0, bottomRight, bottomRight)\n    // buffer.drawText(`${nm.x}-${nm.y}`, 1, 1, RGBA.fromHex(\"#ffffff\"), RGBA.fromHex(\"#000000\"));\n    // buffer.drawText(`${nms.x}-${nms.y}`, 1, 2, RGBA.fromHex(\"#ffffff\"), RGBA.fromHex(\"#000000\"));\n  }\n\n  protected override onMouseEvent(event: MouseEvent): void {\n    switch (event.type) {\n      case \"down\":\n        this.isDragging = true\n        this.dragOffsetX = event.x - this.x\n        this.dragOffsetY = event.y - this.y\n        this.zIndex = nextZIndex++\n        event.stopPropagation()\n        break\n\n      case \"drag-end\":\n        if (this.isDragging) {\n          this.isDragging = false\n          event.stopPropagation()\n        }\n        break\n\n      case \"drag\":\n        if (this.isDragging) {\n          this.x = event.x - this.dragOffsetX\n          this.y = event.y - this.dragOffsetY\n\n          this.x = Math.max(0, Math.min(this.x, this._ctx.width - this.width))\n          this.y = Math.max(4, Math.min(this.y, this._ctx.height - this.height))\n\n          event.stopPropagation()\n        }\n        break\n    }\n  }\n}\n\nexport default function MouseDraggableScene() {\n  const solidRenderer = useContext(RendererContext)\n  if (!solidRenderer) {\n    throw new Error(\"No renderer found\")\n  }\n  const alphaBox50 = new DraggableTransparentBox(\n    solidRenderer,\n    15,\n    5,\n    25,\n    8,\n    RGBA.fromValues(64 / 255, 176 / 255, 255 / 255, 128 / 255),\n    50,\n  )\n\n  const headerText = t`${bold(underline(fg(\"#00D4AA\")(\"Interactive Alpha Transparency & Blending Demo - Drag the boxes!\")))}\n${fg(\"#A8A8B2\")(\"Click and drag any transparent box to move it around • Watch how transparency layers blend\")}`\n\n  return (\n    <box zIndex={10} marginTop={1}>\n      <text content={headerText} />\n      {alphaBox50}\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/plugin-slots-demo.tsx",
    "content": "import type { PluginErrorEvent } from \"@opentui/core\"\nimport {\n  Slot,\n  createSolidSlotRegistry,\n  type SlotMode,\n  type SolidPlugin,\n  useKeyboard,\n  useRenderer,\n} from \"@opentui/solid\"\nimport { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show, type JSX } from \"solid-js\"\n\ntype DemoSlots = {\n  statusbar: { label: string }\n  sidebar: { section: string }\n}\n\nconst DEMO_STATUS_LABEL = \"host-status\"\nconst DEMO_SIDEBAR_SECTION = \"plugins\"\n\nconst hostContext = {\n  appName: \"solid-plugin-slots-demo\",\n  version: \"1.0.0\",\n}\n\nfunction nextStatusbarMode(mode: SlotMode): SlotMode {\n  if (mode === \"append\") {\n    return \"replace\"\n  }\n\n  if (mode === \"replace\") {\n    return \"single_winner\"\n  }\n\n  return \"append\"\n}\n\nfunction formatPluginError(event: PluginErrorEvent): string {\n  return `${event.pluginId} [${event.phase}/${event.source}] @ ${event.slot ?? \"<none>\"}: ${event.error.message}`\n}\n\nconst CrashNode = (props: { pluginId: string }) => {\n  throw new Error(`Forced subtree crash in ${props.pluginId}`)\n  return null as unknown as JSX.Element\n}\n\nconst ClockStatusText = (props: { label: string }) => {\n  const [time, setTime] = createSignal(new Date().toLocaleTimeString())\n\n  onMount(() => {\n    const timer = setInterval(() => {\n      setTime(new Date().toLocaleTimeString())\n    }, 1000)\n\n    onCleanup(() => {\n      clearInterval(timer)\n    })\n  })\n\n  return <text fg=\"#93c5fd\">{`Clock plugin -> ${props.label} (${time()})`}</text>\n}\n\nfunction createClockPlugin(crash: boolean): SolidPlugin<DemoSlots, typeof hostContext> {\n  return {\n    id: \"clock-plugin\",\n    order: 0,\n    slots: {\n      statusbar(_ctx, props) {\n        if (crash) {\n          return <CrashNode pluginId=\"clock-plugin\" />\n        }\n\n        return (\n          <box\n            border\n            borderStyle=\"single\"\n            borderColor=\"#2563eb\"\n            marginLeft={1}\n            paddingLeft={1}\n            paddingRight={1}\n            height={3}\n          >\n            <ClockStatusText label={props.label} />\n          </box>\n        )\n      },\n      sidebar(_ctx, props) {\n        return (\n          <box\n            border\n            borderStyle=\"single\"\n            borderColor=\"#0ea5e9\"\n            flexDirection=\"column\"\n            paddingLeft={1}\n            paddingRight={1}\n          >\n            <text fg=\"#38bdf8\">{`Clock Sidebar (${props.section})`}</text>\n            <text fg=\"#e2e8f0\">Healthy</text>\n          </box>\n        )\n      },\n    },\n  }\n}\n\nfunction createActivityPlugin(crash: boolean): SolidPlugin<DemoSlots, typeof hostContext> {\n  return {\n    id: \"activity-plugin\",\n    order: 10,\n    slots: {\n      statusbar() {\n        if (crash) {\n          throw new Error(\"Forced activity render failure\")\n        }\n\n        return (\n          <box\n            border\n            borderStyle=\"single\"\n            borderColor=\"#16a34a\"\n            marginLeft={1}\n            paddingLeft={1}\n            paddingRight={1}\n            height={3}\n          >\n            <text fg=\"#86efac\">Activity plugin healthy</text>\n          </box>\n        )\n      },\n    },\n  }\n}\n\nexport default function PluginSlotsDemo() {\n  const renderer = useRenderer()\n  const registry = createSolidSlotRegistry<DemoSlots, typeof hostContext>(renderer, hostContext)\n\n  const [statusbarMode, setStatusbarMode] = createSignal<SlotMode>(\"append\")\n  const [clockEnabled, setClockEnabled] = createSignal(true)\n  const [activityEnabled, setActivityEnabled] = createSignal(true)\n  const [clockCrashEnabled, setClockCrashEnabled] = createSignal(false)\n  const [activityCrashEnabled, setActivityCrashEnabled] = createSignal(false)\n  const [showPlaceholder, setShowPlaceholder] = createSignal(true)\n  const [refreshNonce, setRefreshNonce] = createSignal(0)\n  const [errorLines, setErrorLines] = createSignal<string[]>([])\n\n  const AppSlot = Slot<DemoSlots, typeof hostContext>\n\n  const pluginFailurePlaceholder = (failure: PluginErrorEvent) => {\n    return (\n      <box border borderStyle=\"single\" borderColor=\"#fb7185\" marginLeft={1} paddingLeft={1} paddingRight={1}>\n        <text fg=\"#fecaca\">{`Plugin error: ${failure.pluginId}`}</text>\n        <text fg=\"#fca5a5\">{`${failure.phase}/${failure.source} @ ${failure.slot ?? \"unknown\"}`}</text>\n      </box>\n    )\n  }\n\n  onMount(() => {\n    renderer.setBackgroundColor(\"#000000\")\n  })\n\n  const unsubscribePluginErrors = registry.onPluginError((event) => {\n    setErrorLines((current) => [formatPluginError(event), ...current].slice(0, 6))\n  })\n  onCleanup(unsubscribePluginErrors)\n\n  createEffect(\n    on(\n      [refreshNonce, clockEnabled, activityEnabled, clockCrashEnabled, activityCrashEnabled],\n      ([\n        _currentRefreshNonce,\n        currentClockEnabled,\n        currentActivityEnabled,\n        currentClockCrashEnabled,\n        currentActivityCrashEnabled,\n      ]) => {\n        const unregisterCallbacks: Array<() => void> = []\n\n        if (currentClockEnabled) {\n          unregisterCallbacks.push(registry.register(createClockPlugin(currentClockCrashEnabled)))\n        }\n\n        if (currentActivityEnabled) {\n          unregisterCallbacks.push(registry.register(createActivityPlugin(currentActivityCrashEnabled)))\n        }\n\n        onCleanup(() => {\n          for (const unregister of unregisterCallbacks.reverse()) {\n            unregister()\n          }\n        })\n      },\n      { defer: false },\n    ),\n  )\n\n  useKeyboard((key) => {\n    switch (key.name) {\n      case \"1\":\n        setClockEnabled((current) => !current)\n        return\n      case \"2\":\n        setActivityEnabled((current) => !current)\n        return\n      case \"m\":\n        setStatusbarMode((current) => nextStatusbarMode(current))\n        return\n      case \"e\":\n        setClockCrashEnabled((current) => !current)\n        return\n      case \"d\":\n        setActivityCrashEnabled((current) => !current)\n        return\n      case \"p\":\n        setShowPlaceholder((current) => !current)\n        return\n      case \"r\":\n        setRefreshNonce((current) => current + 1)\n        return\n      case \"x\":\n        setClockCrashEnabled(false)\n        setActivityCrashEnabled(false)\n        setErrorLines([])\n        registry.clearPluginErrors()\n        setRefreshNonce((current) => current + 1)\n        return\n      case \"c\":\n        if (key.ctrl) {\n          key.preventDefault()\n          renderer.destroy()\n        }\n        return\n    }\n  })\n\n  const info = createMemo(() => {\n    return [\n      \"Solid Plugin Slot Demo\",\n      \"\",\n      `Statusbar mode: ${statusbarMode().toUpperCase()} (press m to cycle)`,\n      `Clock plugin: ${clockEnabled() ? \"ON\" : \"OFF\"} (press 1)`,\n      `Activity plugin: ${activityEnabled() ? \"ON\" : \"OFF\"} (press 2)`,\n      `Clock subtree crash: ${clockCrashEnabled() ? \"ON\" : \"OFF\"} (press e)`,\n      `Activity throw: ${activityCrashEnabled() ? \"ON\" : \"OFF\"} (press d)`,\n      `Show placeholders: ${showPlaceholder() ? \"YES\" : \"NO\"} (press p)`,\n      \"\",\n      `Statusbar slot label: ${DEMO_STATUS_LABEL}`,\n      `Sidebar slot section: ${DEMO_SIDEBAR_SECTION}`,\n      \"\",\n      \"Press r to re-register active plugins.\",\n      \"Press x to reset errors and clear history.\",\n      \"\",\n      \"Recent plugin errors:\",\n      ...(errorLines().length > 0 ? errorLines() : [\"(none)\"]),\n    ].join(\"\\n\")\n  })\n\n  const renderStatusbarSlot = () => {\n    const mode = statusbarMode()\n\n    if (showPlaceholder()) {\n      return (\n        <Show when={mode} keyed>\n          {(currentMode) => (\n            <AppSlot\n              registry={registry}\n              name=\"statusbar\"\n              label={DEMO_STATUS_LABEL}\n              mode={currentMode}\n              pluginFailurePlaceholder={pluginFailurePlaceholder}\n            >\n              <text fg=\"#94a3b8\">Fallback statusbar content</text>\n            </AppSlot>\n          )}\n        </Show>\n      )\n    }\n\n    return (\n      <Show when={mode} keyed>\n        {(currentMode) => (\n          <AppSlot registry={registry} name=\"statusbar\" label={DEMO_STATUS_LABEL} mode={currentMode}>\n            <text fg=\"#94a3b8\">Fallback statusbar content</text>\n          </AppSlot>\n        )}\n      </Show>\n    )\n  }\n\n  const renderSidebarSlot = () => {\n    if (showPlaceholder()) {\n      return (\n        <AppSlot\n          registry={registry}\n          name=\"sidebar\"\n          section={DEMO_SIDEBAR_SECTION}\n          mode=\"replace\"\n          pluginFailurePlaceholder={pluginFailurePlaceholder}\n        >\n          <text fg=\"#94a3b8\">No sidebar plugin active</text>\n        </AppSlot>\n      )\n    }\n\n    return (\n      <AppSlot registry={registry} name=\"sidebar\" section={DEMO_SIDEBAR_SECTION} mode=\"replace\">\n        <text fg=\"#94a3b8\">No sidebar plugin active</text>\n      </AppSlot>\n    )\n  }\n\n  return (\n    <box width=\"100%\" height=\"100%\" flexDirection=\"column\" padding={1} backgroundColor=\"#020617\">\n      <box\n        height={5}\n        width=\"100%\"\n        border\n        borderStyle=\"single\"\n        borderColor=\"#334155\"\n        alignItems=\"center\"\n        flexDirection=\"row\"\n        paddingLeft={1}\n        marginBottom={1}\n      >\n        {renderStatusbarSlot()}\n      </box>\n\n      <box width=\"100%\" flexGrow={1} flexDirection=\"row\">\n        <box\n          width={36}\n          border\n          borderStyle=\"single\"\n          borderColor=\"#334155\"\n          flexDirection=\"column\"\n          padding={1}\n          marginRight={1}\n        >\n          {renderSidebarSlot()}\n        </box>\n\n        <box flexGrow={1} border borderStyle=\"single\" borderColor=\"#334155\" flexDirection=\"column\" padding={1}>\n          <text fg=\"#e2e8f0\" content={info()} />\n        </box>\n      </box>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/scroll-demo.tsx",
    "content": "import { createMemo, For, Index } from \"solid-js\"\n\nexport const ScrollDemo = () => {\n  const objectItems = createMemo(() => Array.from({ length: 1000 }).map((_, i) => ({ count: i + 1 })))\n\n  return (\n    <scrollbox\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        flexGrow: 1,\n        rootOptions: {\n          backgroundColor: \"#24283b\",\n          border: true,\n        },\n        wrapperOptions: {\n          backgroundColor: \"#1f2335\",\n        },\n        viewportOptions: {\n          backgroundColor: \"#1a1b26\",\n        },\n        contentOptions: {\n          backgroundColor: \"#16161e\",\n        },\n        scrollbarOptions: {\n          showArrows: true,\n          trackOptions: {\n            foregroundColor: \"#7aa2f7\",\n            backgroundColor: \"#414868\",\n          },\n        },\n      }}\n      focused\n    >\n      <For each={objectItems()}>\n        {(item) => (\n          <box\n            style={{\n              width: \"100%\",\n              padding: 1,\n              marginBottom: 1,\n              backgroundColor: item.count % 2 === 0 ? \"#292e42\" : \"#2f3449\",\n            }}\n          >\n            <text content={`Box ${item.count}`} />\n          </box>\n        )}\n      </For>\n    </scrollbox>\n  )\n}\n\nexport const ScrollDemoIndex = () => {\n  const primitiveItems = createMemo(() => Array.from({ length: 1000 }).map((_, i) => i + 1))\n\n  return (\n    <scrollbox\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        flexGrow: 1,\n        rootOptions: {\n          backgroundColor: \"#24283b\",\n          border: true,\n        },\n        wrapperOptions: {\n          backgroundColor: \"#1f2335\",\n        },\n        viewportOptions: {\n          backgroundColor: \"#1a1b26\",\n        },\n        contentOptions: {\n          backgroundColor: \"#16161e\",\n        },\n        scrollbarOptions: {\n          showArrows: true,\n          trackOptions: {\n            foregroundColor: \"#7aa2f7\",\n            backgroundColor: \"#414868\",\n          },\n        },\n      }}\n      focused\n    >\n      <Index each={primitiveItems()}>\n        {(item) => (\n          <box\n            style={{\n              width: \"100%\",\n              padding: 1,\n              marginBottom: 1,\n              backgroundColor: item() % 2 === 0 ? \"#292e42\" : \"#2f3449\",\n            }}\n          >\n            <text content={`Box ${item()}`} />\n          </box>\n        )}\n      </Index>\n    </scrollbox>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/tab-select-demo.tsx",
    "content": "import { createSignal, For, Match, onMount, Switch } from \"solid-js\"\nimport { EventEmitter } from \"events\"\nimport { render, useKeyboard, useRenderer } from \"@opentui/solid\"\nimport { ConsolePosition } from \"@opentui/core\"\n\nconst Tab = (props: { title: string; active: boolean; index: number }) => {\n  return (\n    <box\n      style={{\n        height: 3,\n        paddingTop: 1,\n        border: props.active && [\"bottom\"],\n        marginLeft: props.index === 0 ? 0 : 1,\n        flexGrow: 1,\n        justifyContent: \"center\",\n        alignItems: \"center\",\n      }}\n    >\n      <text>{props.title}</text>\n    </box>\n  )\n}\n\nconst tabs = [\n  { title: \"Text & Attributes\" },\n  { title: \"Basics\" },\n  { title: \"Borders\" },\n  { title: \"Animation\" },\n  { title: \"Titles\" },\n  { title: \"Interactive\" },\n]\n\nexport default function TabSelectDemo() {\n  const renderer = useRenderer()\n  const [activeTab, setActiveTab] = createSignal(0)\n\n  onMount(() => {\n    renderer.useConsole = true\n    renderer.console.show()\n  })\n\n  useKeyboard((key) => {})\n\n  return (\n    <box style={{ flexDirection: \"column\", flexGrow: 1 }}>\n      <tab_select\n        height={2}\n        width=\"100%\"\n        options={tabs.map((tab, index) => ({\n          name: tab.title,\n          value: index,\n          description: \"\",\n        }))}\n        showDescription={false}\n        onChange={(index) => {\n          setActiveTab(index)\n        }}\n        focused\n      />\n      <Switch>\n        <Match when={activeTab() === 0}>\n          <text>Tab 1/6 - Use Left/Right arrows to navigate | Press Ctrl+C to exit | D: toggle debug</text>\n        </Match>\n        <Match when={activeTab() === 1}>\n          <text>Tab 2/6 - Use Left/Right arrows to navigate | Press Ctrl+C to exit | D: toggle debug</text>\n        </Match>\n      </Switch>\n    </box>\n  )\n}\n\nif (import.meta.main) {\n  render(TabSelectDemo, {\n    consoleOptions: {\n      position: ConsolePosition.BOTTOM,\n      maxStoredLogs: 1000,\n      sizePercent: 40,\n    },\n  })\n}\n"
  },
  {
    "path": "packages/solid/examples/components/text-selection-demo.tsx",
    "content": "import { Selection } from \"@opentui/core\"\nimport { ConsolePosition } from \"@opentui/core\"\nimport { render, useRenderer, useSelectionHandler, type TextProps } from \"@opentui/solid\"\nimport { createEffect, createSignal, onMount } from \"solid-js\"\n\nconst words = [\"Hello\", \"World\", \"OpenTUI\", \"SolidJS\", \"ReactJS\", \"TypeScript\", \"JavaScript\", \"CSS\", \"HTML\", \"JSX\"]\n\nexport default function TextSelectionDemo() {\n  const renderer = useRenderer()\n\n  const [selectedWord, setSelectedWord] = createSignal(0)\n\n  onMount(() => {\n    renderer.setBackgroundColor(\"#0d1117\")\n    setInterval(() => {\n      setSelectedWord((w) => (w === words.length - 1 ? 0 : w + 1))\n    }, 1000)\n  })\n\n  const [statusText, setStatusText] = createSignal(\"No selection - try selecting across different nested elements\")\n  const [selectionStartText, setSelectionStartText] = createSignal(\"\")\n  const [selectionMiddleText, setSelectionMiddleText] = createSignal(\"\")\n  const [selectionEndText, setSelectionEndText] = createSignal(\"\")\n\n  const section1TextStyle: TextProps[\"style\"] = {\n    fg: \"#f0f6fc\",\n    zIndex: 21,\n    flexShrink: 0,\n  }\n\n  const updateSelectionTexts = (selectedText: string) => {\n    const lines = selectedText.split(\"\\n\")\n    const totalLength = selectedText.length\n\n    if (lines.length > 1) {\n      setStatusText(`Selected ${lines.length} lines (${totalLength} chars):`)\n      setSelectionStartText(lines[0] || \"\")\n      setSelectionMiddleText(\"...\")\n      setSelectionEndText(lines[lines.length - 1] || \"\")\n    } else if (selectedText.length > 60) {\n      setStatusText(`Selected ${totalLength} chars:`)\n      setSelectionStartText(selectedText.substring(0, 30))\n      setSelectionMiddleText(\"...\")\n      setSelectionEndText(selectedText.substring(selectedText.length - 30))\n    } else {\n      setStatusText(`Selected ${totalLength} chars:`)\n      setSelectionStartText(`\"${selectedText}\"`)\n      setSelectionMiddleText(\"\")\n      setSelectionEndText(\"\")\n    }\n  }\n\n  let selectionRef: Selection | null = null\n\n  const selectionHandler = (selection: Selection) => {\n    selectionRef = selection\n    const selectedText = selection.getSelectedText()\n\n    if (selectedText) {\n      updateSelectionTexts(selectedText)\n    }\n  }\n\n  useSelectionHandler(selectionHandler)\n\n  const selectedWordText = () => words[selectedWord()]\n\n  createEffect(() => {\n    selectedWord()\n    if (!selectionRef) return\n    updateSelectionTexts(selectionRef.getSelectedText())\n  })\n\n  return (\n    <>\n      <box\n        style={{\n          position: \"absolute\",\n          left: 1,\n          top: 1,\n          width: 88,\n          height: 22,\n          backgroundColor: \"#161b22\",\n          zIndex: 1,\n          borderColor: \"#50565d\",\n          titleAlignment: \"center\",\n          border: true,\n        }}\n        title=\"Text Selection Demo\"\n      >\n        <box\n          style={{\n            position: \"absolute\",\n            left: 3,\n            top: 3,\n            zIndex: 10,\n          }}\n        >\n          <box\n            style={{\n              width: 45,\n              height: 7,\n              backgroundColor: \"#1e2936\",\n              zIndex: 20,\n              borderColor: \"#58a6ff\",\n              flexDirection: \"column\",\n              padding: 1,\n              border: true,\n            }}\n            title=\"Document Section 1\"\n          >\n            <text style={section1TextStyle}>This is a paragraph in the first box.</text>\n            <text style={section1TextStyle}>dynamic: {selectedWordText()}</text>\n            <text style={section1TextStyle}>it contains multiple lines of text</text>\n            <text style={section1TextStyle}>世界, 你好世界, 中文, 한글</text>\n          </box>\n          <box\n            style={{\n              left: 2,\n              top: 1,\n              width: 31,\n              height: 4,\n              backgroundColor: \"#2d1b69\",\n              zIndex: 25,\n              borderColor: \"#a371f7\",\n              borderStyle: \"double\",\n              border: true,\n            }}\n          >\n            <text style={{ width: 27, height: 1, zIndex: 26, selectionBg: \"#4a5568\", selectionFg: \"#ffffff\" }}>\n              <span style={{ fg: \"yellow\" }}>Important:</span>{\" \"}\n              <span style={{ bold: true, fg: \"cyan\" }}>Nested content</span>{\" \"}\n              <span style={{ italic: true, fg: \"green\" }}></span>\n            </text>\n          </box>\n        </box>\n        <box\n          style={{\n            position: \"absolute\",\n            left: 49,\n            top: 3,\n            zIndex: 10,\n          }}\n        >\n          <box\n            style={{\n              left: 2,\n              top: 0,\n              width: 35,\n              height: 12,\n              backgroundColor: \"#1c2128\",\n              zIndex: 20,\n              borderColor: \"#f85149\",\n              borderStyle: \"rounded\",\n              flexDirection: \"column\",\n              padding: 1,\n              border: true,\n            }}\n            title=\"Code Example\"\n          >\n            <text style={{ fg: \"#f0f6fc\", zIndex: 21 }}>\n              <span style={{ fg: \"magenta\" }}>function</span> <span style={{ fg: \"cyan\" }}>handleSelection</span>(){\" \"}\n              {\"{\"}\n            </text>\n            <text style={{ fg: \"#f0f6fc\", zIndex: 21 }}>\n              {\"  \"}\n              <span style={{ fg: \"magenta\" }}>const</span> selected ={\" \"}\n              <span style={{ fg: \"cyan\" }}>getSelectedText</span>()\n            </text>\n            <text style={{ fg: \"#f0f6fc\", zIndex: 21 }}>\n              {\"  \"}\n              <span style={{ fg: \"yellow\" }}>console</span>.<span style={{ fg: \"green\" }}>log</span>(selected)\n            </text>\n            <text style={{ fg: \"#e6edf3\", zIndex: 21 }}>{\"}\"}</text>\n          </box>\n        </box>\n        <text style={{ left: 2, top: 17, zIndex: 2 }}>\n          Click and drag to select text across any elements. Press 'C' to clear selection.\n        </text>\n      </box>\n      <box\n        style={{\n          position: \"absolute\",\n          left: 90,\n          top: 11,\n          width: 31,\n          height: 6,\n          backgroundColor: \"#1b2f23\",\n          zIndex: 30,\n          borderColor: \"#2ea043\",\n          borderStyle: \"single\",\n          border: true,\n        }}\n        title=\"README\"\n      >\n        <text style={{ fg: \"#f0f6fc\", zIndex: 31, height: \"auto\" }}>\n          <span style={{ bold: true, fg: \"cyan\" }}>Selection Demo</span>\n          {\"\\n\"}\n          <span style={{ fg: \"green\" }}>✓</span> Cross-renderable selection\n          {\"\\n\"}\n          <span style={{ fg: \"green\" }}>✓</span> Nested boxes\n          {\"\\n\"}\n          <span style={{ fg: \"green\" }}>✓</span> Styled text support\n        </text>\n      </box>\n      <box\n        style={{\n          position: \"absolute\",\n          left: 1,\n          top: 24,\n          width: 88,\n          height: 9,\n          backgroundColor: \"#0d1117\",\n          zIndex: 1,\n          borderColor: \"#50565d\",\n          titleAlignment: \"left\",\n          padding: 1,\n          border: true,\n        }}\n        title=\"Selection Status\"\n      >\n        <text style={{ fg: \"#f0f6fc\", zIndex: 2 }}>{statusText()}</text>\n        <text style={{ fg: \"#7dd3fc\", zIndex: 2 }}>{selectionStartText()}</text>\n        <text style={{ fg: \"#94a3b8\", zIndex: 2 }}>{selectionMiddleText()}</text>\n        <text style={{ fg: \"#7dd3fc\", zIndex: 2 }}>{selectionEndText()}</text>\n      </box>\n    </>\n  )\n}\n\nif (import.meta.main) {\n  render(() => <TextSelectionDemo />, {\n    consoleOptions: {\n      position: ConsolePosition.BOTTOM,\n      maxStoredLogs: 1000,\n      sizePercent: 40,\n    },\n  })\n}\n"
  },
  {
    "path": "packages/solid/examples/components/text-style-demo.tsx",
    "content": "import { TextAttributes } from \"@opentui/core\"\nimport { createSignal, onCleanup, onMount, Show } from \"solid-js\"\n\nexport default function TextStyleScene() {\n  const [counter, setCounter] = createSignal(0)\n\n  let interval: NodeJS.Timeout\n\n  onMount(() => {\n    interval = setInterval(() => {\n      setCounter((c) => c + 1)\n    }, 1000)\n  })\n\n  onCleanup(() => {\n    clearInterval(interval)\n  })\n\n  return (\n    <box>\n      <text>Simple text works! {counter()} times</text>\n      <text>\n        line break\n        <br />\n        works! {counter()} times\n      </text>\n      <text style={{ bg: \"red\", fg: \"black\" }}>\n        Hello {counter()} <span style={{ bg: \"yellow\", fg: \"black\" }}>World</span>{\" \"}\n        <span style={{ attributes: TextAttributes.UNDERLINE, bg: \"blue\", fg: \"yellow\" }}>{counter()}</span>\n      </text>\n      <text>\n        Toggle{\" \"}\n        <Show when={counter() % 2 === 0}>\n          <b>\n            <u>text</u>\n          </b>\n        </Show>\n      </text>\n      <text>\n        Toggle <Show when={counter() % 2 === 0}>text</Show>\n      </text>\n      <text>\n        Hyperlinks:{\" \"}\n        <u>\n          <a href=\"https://opentui.com\" style={{ fg: \"blue\" }}>\n            opentui.com\n          </a>\n        </u>{\" \"}\n        - Click if your terminal supports OSC 8\n      </text>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/text-truncation-demo.tsx",
    "content": "import { bold, cyan, green, magenta, t, yellow } from \"@opentui/core\"\nimport { useKeyboard, useRenderer, useSelectionHandler } from \"@opentui/solid\"\nimport { createMemo, createSignal, onMount, onCleanup } from \"solid-js\"\n\nconst singleLineText1 =\n  \"This is a very long single line of text that will definitely exceed the width of most terminal windows and should be truncated when truncation is enabled\"\nconst singleLineText2 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz\"\nconst singleLineText3 = \"🌟 Unicode test: こんにちは世界 Hello World 你好世界 안녕하세요 🚀 More emoji: 🎨🎭🎪🎬🎮🎯\"\nconst multilineText1 =\n  \"This is a multiline text block that demonstrates how truncation works with word wrapping enabled. Each line that exceeds the viewport width will be truncated independently. Try resizing the terminal to see how it behaves!\"\nconst multilineText2 = `Line 1: This is a long line without wrapping\nLine 2: Another very long line that will be truncated when enabled\nLine 3: Short line\nLine 4: Yet another extremely long line with lots of text to demonstrate middle truncation behavior`\n\nexport function TextTruncationDemo() {\n  const renderer = useRenderer()\n  const [truncateEnabled, setTruncateEnabled] = createSignal(false)\n  const [wrapMode, setWrapMode] = createSignal<\"none\" | \"char\" | \"word\">(\"none\")\n  const [leftGrow, setLeftGrow] = createSignal(1)\n  const [rightGrow, setRightGrow] = createSignal(1)\n\n  const [statusText, setStatusText] = createSignal(\"Select text to see details here\")\n  const [selectionStartText, setSelectionStartText] = createSignal(\"\")\n  const [selectionMiddleText, setSelectionMiddleText] = createSignal(\"\")\n  const [selectionEndText, setSelectionEndText] = createSignal(\"\")\n\n  onMount(() => {\n    renderer.setBackgroundColor(\"#0d1117\")\n  })\n\n  const footerContent = createMemo(() => {\n    const truncateStatus = truncateEnabled() ? \"ENABLED\" : \"DISABLED\"\n    const truncateColor = truncateEnabled() ? green : yellow\n    const wrapColor = wrapMode() === \"none\" ? yellow : cyan\n    return t`Truncate: ${truncateColor(bold(truncateStatus))} | Wrap: ${wrapColor(bold(wrapMode().toUpperCase()))} | ${cyan(\"T\")}: toggle truncate | ${cyan(\"W\")}: cycle wrap | ${cyan(\"R\")}: resize | ${cyan(\"C\")}: clear selection | ${cyan(\"Ctrl+C\")}: exit`\n  })\n\n  const styledContent = createMemo(\n    () =>\n      t`This paragraph mixes ${bold(cyan(\"styled\"))} text with ${magenta(\"color\")} accents, ${green(\"emphasis\")}, and ${yellow(\"highlighted\")}\nsegments to exercise truncation on formatted content.`,\n  )\n\n  const updateSelectionTexts = (selectedText: string) => {\n    const lines = selectedText.split(\"\\n\")\n    const totalLength = selectedText.length\n\n    if (lines.length > 1) {\n      setStatusText(`Selected ${lines.length} lines (${totalLength} chars):`)\n      setSelectionStartText(lines[0] || \"\")\n      setSelectionMiddleText(\"...\")\n      setSelectionEndText(lines[lines.length - 1] || \"\")\n    } else if (selectedText.length > 60) {\n      setStatusText(`Selected ${totalLength} chars:`)\n      setSelectionStartText(selectedText.substring(0, 30))\n      setSelectionMiddleText(\"...\")\n      setSelectionEndText(selectedText.substring(selectedText.length - 30))\n    } else {\n      setStatusText(`Selected ${totalLength} chars:`)\n      setSelectionStartText(`\"${selectedText}\"`)\n      setSelectionMiddleText(\"\")\n      setSelectionEndText(\"\")\n    }\n  }\n\n  useSelectionHandler((selection) => {\n    const selectedText = selection?.getSelectedText()\n    if (selectedText) {\n      updateSelectionTexts(selectedText)\n    } else {\n      setStatusText(\"Empty selection\")\n      setSelectionStartText(\"\")\n      setSelectionMiddleText(\"\")\n      setSelectionEndText(\"\")\n    }\n  })\n\n  useKeyboard((key) => {\n    if (key.ctrl && key.name === \"c\") {\n      key.preventDefault()\n      renderer.destroy()\n      return\n    }\n    if (key.name === \"t\") {\n      setTruncateEnabled((current) => !current)\n    }\n    if (key.name === \"w\") {\n      setWrapMode((current) => (current === \"none\" ? \"char\" : current === \"char\" ? \"word\" : \"none\"))\n    }\n    if (key.name === \"r\") {\n      const left = leftGrow()\n      const right = rightGrow()\n      if (left === 1 && right === 1) {\n        setLeftGrow(2)\n        setRightGrow(1)\n      } else if (left === 2 && right === 1) {\n        setLeftGrow(1)\n        setRightGrow(2)\n      } else {\n        setLeftGrow(1)\n        setRightGrow(1)\n      }\n    }\n    if (key.name === \"c\" && !key.ctrl) {\n      renderer.clearSelection()\n      setStatusText(\"Selection cleared\")\n      setSelectionStartText(\"\")\n      setSelectionMiddleText(\"\")\n      setSelectionEndText(\"\")\n    }\n  })\n\n  return (\n    <box flexDirection=\"column\" width=\"100%\" height=\"100%\" backgroundColor=\"#0d1117\">\n      <box\n        height={3}\n        backgroundColor=\"#161b22\"\n        borderStyle=\"single\"\n        borderColor=\"#30363d\"\n        alignItems=\"center\"\n        justifyContent=\"center\"\n        border\n      >\n        <text fg=\"#58a6ff\" content=\"Text Truncation Demo - Press 'T' to toggle truncation\" />\n      </box>\n      <box flexGrow={1} flexDirection=\"row\" gap={1} padding={1}>\n        <box flexGrow={leftGrow()} flexDirection=\"column\" gap={1}>\n          <box\n            minHeight={5}\n            backgroundColor=\"#161b22\"\n            borderStyle=\"rounded\"\n            borderColor=\"#58a6ff\"\n            title=\"Single Line Text 1\"\n            padding={1}\n            border\n          >\n            <text content={singleLineText1} fg=\"#c9d1d9\" wrapMode={wrapMode()} truncate={truncateEnabled()} />\n          </box>\n          <box\n            minHeight={5}\n            backgroundColor=\"#161b22\"\n            borderStyle=\"rounded\"\n            borderColor=\"#3fb950\"\n            title=\"Single Line Text 2\"\n            padding={1}\n            border\n          >\n            <text content={singleLineText2} fg=\"#3fb950\" wrapMode={wrapMode()} truncate={truncateEnabled()} />\n          </box>\n          <box\n            minHeight={7}\n            backgroundColor=\"#161b22\"\n            borderStyle=\"rounded\"\n            borderColor=\"#d29922\"\n            title=\"Single Line Text 3 (Unicode)\"\n            padding={1}\n            border\n          >\n            <text content={singleLineText3} fg=\"#d29922\" wrapMode={wrapMode()} truncate={truncateEnabled()} />\n          </box>\n        </box>\n        <box flexGrow={rightGrow()} flexDirection=\"column\" gap={1}>\n          <box\n            flexGrow={1}\n            backgroundColor=\"#161b22\"\n            borderStyle=\"rounded\"\n            borderColor=\"#f778ba\"\n            title=\"Multiline Text (Word Wrap)\"\n            padding={1}\n            border\n          >\n            <text content={multilineText1} fg=\"#f778ba\" wrapMode={wrapMode()} truncate={truncateEnabled()} />\n          </box>\n          <box\n            flexGrow={1}\n            backgroundColor=\"#161b22\"\n            borderStyle=\"rounded\"\n            borderColor=\"#bc8cff\"\n            title=\"Multiline Text\"\n            padding={1}\n            border\n          >\n            <text content={multilineText2} fg=\"#bc8cff\" wrapMode={wrapMode()} truncate={truncateEnabled()} />\n          </box>\n          <box\n            flexGrow={1}\n            backgroundColor=\"#161b22\"\n            borderStyle=\"rounded\"\n            borderColor=\"#ff7b72\"\n            title=\"Styled Text with Truncation\"\n            padding={1}\n            border\n          >\n            <text content={styledContent()} fg=\"#c9d1d9\" wrapMode={wrapMode()} truncate={truncateEnabled()} />\n          </box>\n        </box>\n      </box>\n      <box\n        height={3}\n        backgroundColor=\"#161b22\"\n        borderStyle=\"single\"\n        borderColor=\"#30363d\"\n        alignItems=\"center\"\n        justifyContent=\"center\"\n        border\n      >\n        <text fg=\"#8b949e\" content={footerContent()} />\n      </box>\n      <box\n        height={7}\n        backgroundColor=\"#0d1117\"\n        borderStyle=\"single\"\n        borderColor=\"#30363d\"\n        title=\"Selection\"\n        titleAlignment=\"left\"\n        flexDirection=\"column\"\n        gap={1}\n        padding={1}\n        border\n      >\n        <text fg=\"#8b949e\" content={statusText()} />\n        <text fg=\"#7dd3fc\" content={selectionStartText()} />\n        <text fg=\"#94a3b8\" content={selectionMiddleText()} />\n        <text fg=\"#7dd3fc\" content={selectionEndText()} />\n      </box>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/textarea-demo.tsx",
    "content": "import { useKeyboard, useRenderer } from \"@opentui/solid\"\nimport { createSignal, onMount } from \"solid-js\"\nimport { bold, cyan, fg, t, type TextareaRenderable, type CursorStyleOptions } from \"@opentui/core\"\n\nconst initialContent = `Welcome to the TextareaRenderable Demo!\n\nThis is an interactive text editor powered by EditBuffer and EditorView.\n\nNAVIGATION:\n  • Arrow keys to move cursor\n  • Home/End for line navigation\n  • Ctrl+A/Ctrl+E for buffer start/end\n  • Alt+F/Alt+B for word forward/backward\n  • Alt+Left/Alt+Right for word forward/backward\n\nSELECTION:\n  • Shift+Arrow keys to select\n  • Shift+Home/End to select to line start/end\n  • Alt+Shift+F/B to select word forward/backward\n  • Alt+Shift+Left/Right to select word forward/backward\n\nEDITING:\n  • Type any text to insert\n  • Backspace/Delete to remove text\n  • Enter to create new lines\n  • Ctrl+D to delete current line\n  • Ctrl+K to delete to line end\n  • Alt+D to delete word forward\n  • Alt+Backspace or Ctrl+W to delete word backward\n\nUNDO/REDO:\n  • Ctrl+Z to undo\n  • Ctrl+Shift+Z or Ctrl+Y to redo\n\nVIEW:\n  • Shift+W to toggle wrap mode (word/char/none)\n  • Tab to toggle cursor style\n\nFEATURES:\n  ✓ Grapheme-aware cursor movement\n  ✓ Unicode (emoji 🌟 and CJK 世界)\n  ✓ Incremental editing\n  ✓ Text wrapping and viewport management\n  ✓ Undo/redo support\n  ✓ Word-based navigation and deletion\n  ✓ Text selection with shift keys\n\nPress ESC to return to main menu`\n\nexport function TextareaDemo() {\n  const renderer = useRenderer()\n  const [cursorStyle, setCursorStyle] = createSignal<CursorStyleOptions>({ style: \"block\", blinking: true })\n  const [wrapMode, setWrapMode] = createSignal<\"word\" | \"char\" | \"none\">(\"word\")\n  const [statusText, setStatusText] = createSignal(\"\")\n  let textareaRef: TextareaRenderable | null = null\n\n  onMount(() => {\n    renderer.setBackgroundColor(\"#0D1117\")\n\n    // Set up frame callback for status updates\n    renderer.setFrameCallback(async () => {\n      if (textareaRef && !textareaRef.isDestroyed) {\n        try {\n          const cursor = textareaRef.logicalCursor\n          const wrap = wrapMode().toUpperCase()\n          const cursorOptions = cursorStyle()\n          const styleLabel = cursorOptions.style.toUpperCase()\n          const blinkLabel = cursorOptions.blinking ? \"Blinking\" : \"Steady\"\n          setStatusText(\n            `Line ${cursor.row + 1}, Col ${cursor.col + 1} | Wrap: ${wrap} | Cursor: ${styleLabel} (${blinkLabel})`,\n          )\n        } catch (error) {\n          // Ignore errors during shutdown\n        }\n      }\n    })\n  })\n\n  useKeyboard((key) => {\n    if (key.shift && key.name === \"w\") {\n      key.preventDefault()\n      if (textareaRef && !textareaRef.isDestroyed) {\n        const currentMode = wrapMode()\n        const nextMode = currentMode === \"word\" ? \"char\" : currentMode === \"char\" ? \"none\" : \"word\"\n        setWrapMode(nextMode)\n        textareaRef.wrapMode = nextMode\n      }\n    }\n    if (key.name === \"tab\") {\n      key.preventDefault()\n      if (textareaRef && !textareaRef.isDestroyed) {\n        const currentStyle = cursorStyle()\n        const nextStyle: CursorStyleOptions =\n          currentStyle.style === \"block\" ? { style: \"line\", blinking: false } : { style: \"block\", blinking: true }\n        setCursorStyle(nextStyle)\n        textareaRef.cursorStyle = nextStyle\n      }\n    }\n    if (key.ctrl && (key.name === \"pageup\" || key.name === \"pagedown\")) {\n      key.preventDefault()\n      if (textareaRef && !textareaRef.isDestroyed) {\n        if (key.name === \"pageup\") {\n          textareaRef.editBuffer.setCursor(0, 0)\n        } else {\n          textareaRef.gotoBufferEnd()\n        }\n      }\n    }\n  })\n\n  return (\n    <box style={{ padding: 1 }}>\n      <box\n        title=\"Interactive Editor (TextareaRenderable)\"\n        borderStyle=\"single\"\n        borderColor=\"#6BCF7F\"\n        backgroundColor=\"#0D1117\"\n        titleAlignment=\"left\"\n        paddingLeft={1}\n        paddingRight={1}\n        border\n        style={{ flexGrow: 1 }}\n      >\n        <textarea\n          ref={(r: TextareaRenderable) => (textareaRef = r)}\n          initialValue={initialContent}\n          placeholder={t`${fg(\"#333333\")(\"Enter\")} ${cyan(bold(\"text\"))} ${fg(\"#333333\")(\"here...\")}`}\n          textColor=\"#F0F6FC\"\n          selectionBg=\"#264F78\"\n          selectionFg=\"#FFFFFF\"\n          wrapMode={wrapMode()}\n          showCursor\n          cursorColor=\"#4ECDC4\"\n          cursorStyle={cursorStyle()}\n          focused\n          style={{ flexGrow: 1 }}\n        />\n      </box>\n      <text style={{ fg: \"#A5D6FF\", height: 1 }}>{statusText()}</text>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/components/textarea-keybindings.ts",
    "content": "import { createMemo } from \"solid-js\"\nimport type { KeyBinding } from \"@opentui/core\"\n\nconst TEXTAREA_ACTIONS = [\n  \"submit\",\n  \"newline\",\n  \"move-left\",\n  \"move-right\",\n  \"move-up\",\n  \"move-down\",\n  \"select-left\",\n  \"select-right\",\n  \"select-up\",\n  \"select-down\",\n  \"line-home\",\n  \"line-end\",\n  \"select-line-home\",\n  \"select-line-end\",\n  \"visual-line-home\",\n  \"visual-line-end\",\n  \"select-visual-line-home\",\n  \"select-visual-line-end\",\n  \"buffer-home\",\n  \"buffer-end\",\n  \"select-buffer-home\",\n  \"select-buffer-end\",\n  \"delete-line\",\n  \"delete-to-line-end\",\n  \"delete-to-line-start\",\n  \"backspace\",\n  \"delete\",\n  \"undo\",\n  \"redo\",\n  \"word-forward\",\n  \"word-backward\",\n  \"select-word-forward\",\n  \"select-word-backward\",\n  \"delete-word-forward\",\n  \"delete-word-backward\",\n] as const\n\nexport function useTextareaKeybindings() {\n  return createMemo(() => {\n    return [\n      { name: \"return\", action: \"submit\" },\n      { name: \"return\", meta: true, action: \"newline\" },\n    ] satisfies KeyBinding[]\n  })\n}\n"
  },
  {
    "path": "packages/solid/examples/components/textarea-minimal-demo.tsx",
    "content": "import { TextAttributes, type TextareaRenderable } from \"@opentui/core\"\nimport { useTextareaKeybindings } from \"./textarea-keybindings.js\"\n\nexport function TextareaMinimalDemo() {\n  const bindings = useTextareaKeybindings()\n  let textarea: TextareaRenderable | undefined\n\n  return (\n    <box paddingLeft={2} paddingRight={2} gap={1}>\n      <box flexDirection=\"row\" justifyContent=\"space-between\">\n        <text attributes={TextAttributes.BOLD} fg=\"#E8EDF2\">\n          Minimal Textarea\n        </text>\n        <text fg=\"#8B98A5\">esc</text>\n      </box>\n      <box paddingLeft={1} gap={1}>\n        <box>\n          <text fg=\"#E8EDF2\">Custom answer</text>\n        </box>\n        <box>\n          <box flexDirection=\"row\">\n            <box paddingRight={1}>\n              <text fg=\"#8B98A5\">1.</text>\n            </box>\n            <box>\n              <text fg=\"#E8EDF2\">Type your own answer</text>\n            </box>\n          </box>\n          <box paddingLeft={3}>\n            <textarea\n              ref={(val: TextareaRenderable) => {\n                textarea = val\n                queueMicrotask(() => {\n                  val.focus()\n                  val.gotoLineEnd()\n                })\n              }}\n              initialValue=\"\"\n              placeholder=\"Type your own answer\"\n              textColor=\"#E8EDF2\"\n              focusedTextColor=\"#E8EDF2\"\n              cursorColor=\"#86B7FF\"\n              keyBindings={bindings()}\n            />\n          </box>\n        </box>\n      </box>\n      <box paddingBottom={1} gap={1} flexDirection=\"row\">\n        <text fg=\"#E8EDF2\">\n          enter <span style={{ fg: \"#8B98A5\" }}>submit</span>\n        </text>\n      </box>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/index.tsx",
    "content": "import { render } from \"@opentui/solid\"\nimport { ConsolePosition } from \"@opentui/core\"\nimport ExampleSelector from \"./components/ExampleSelector.js\"\n\n// Uncomment to debug solidjs reconciler\n// process.env.DEBUG = \"true\"\n\nconst App = () => <ExampleSelector />\n\nrender(App, {\n  targetFps: 30,\n  exitOnCtrlC: false,\n  consoleOptions: {\n    position: ConsolePosition.BOTTOM,\n    maxStoredLogs: 1000,\n    sizePercent: 40,\n  },\n})\n"
  },
  {
    "path": "packages/solid/examples/package.json",
    "content": "{\n  \"name\": \"@opentui/solid-examples\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@opentui/core\": \"workspace:*\",\n    \"@opentui/solid\": \"workspace:*\",\n    \"solid-js\": \"1.9.11\"\n  },\n  \"scripts\": {\n    \"dev\": \"bun --preload ../scripts/preload.ts index.tsx\",\n    \"build\": \"bun build.ts\",\n    \"build:all\": \"bun build.ts --all\"\n  }\n}\n"
  },
  {
    "path": "packages/solid/examples/repro-empty-styled-text.tsx",
    "content": "import { createSignal, Show } from \"solid-js\"\nimport { render, useKeyboard, useRenderer } from \"@opentui/solid\"\nimport { t } from \"@opentui/core\"\n\nprocess.env.DEBUG = \"true\"\n\nconst EmptyStyledTextTest = () => {\n  const renderer = useRenderer()\n\n  renderer.useConsole = true\n  renderer.console.show()\n\n  const [showBox, setShowBox] = createSignal(false)\n  const [cont, setCont] = createSignal<string | null>(\"text\")\n\n  useKeyboard((key) => {\n    if (key.name === \"space\") {\n      console.log(\"==== TOGGLING BOX ====\")\n      setShowBox((s) => !s)\n    } else if (key.name === \"tab\") {\n      console.log(\"==== TOGGLING STYLED CONTENT ====\")\n      setCont((s) => (s ? null : \"text\"))\n    }\n  })\n\n  return (\n    <box border title=\"empty styled text test\">\n      {/* Only instance where I have found solid creating empty text nodes naturally  */}\n      <Show when={showBox()}>\n        <box border title=\"conditional box\">\n          <text>Box is visible!</text>\n        </box>\n      </Show>\n      <text>Press space to toggle box</text>\n\n      {/* Forced instance of empty styled box. Doesn't work without fixes put in place previously */}\n      <text></text>\n\n      {/* Dynamically going null*/}\n      <text></text>\n    </box>\n  )\n}\n\nrender(EmptyStyledTextTest)\n"
  },
  {
    "path": "packages/solid/examples/repro-filter-list.tsx",
    "content": "import { createSignal, For } from \"solid-js\"\nimport { render, useRenderer } from \"@opentui/solid\"\n\nprocess.env.DEBUG = \"true\"\n\nconst FilterListTest = () => {\n  const renderer = useRenderer()\n\n  renderer.useConsole = true\n  renderer.console.show()\n\n  const [filter, setFilter] = createSignal(\"\")\n\n  const items = [\"Apple\", \"Apple Pie\", \"Apple Sauce\", \"Apple key\", \"Apple fiy\", \"Grape\"]\n  /*\n   * [ FIXED now ]\n   * 1. Type \"pie\"\n   * 2. Most items go away\n   * 3. Backspace out\n   * 4. ~not all items get added back~\n   * */\n\n  const filteredItems = () => {\n    const f = filter().toLowerCase()\n    return items.filter((item) => item.toLowerCase().includes(f))\n  }\n\n  return (\n    <box border title=\"filter list test\" flexDirection=\"column\">\n      <box border title=\"search\" height={3}>\n        <input focused placeholder=\"Type to filter...\" onInput={setFilter} />\n      </box>\n      <box border title=\"results\" flexDirection=\"column\">\n        <For each={filteredItems()}>{(item) => <text>{item}</text>}</For>\n      </box>\n    </box>\n  )\n}\n\nrender(FilterListTest)\n"
  },
  {
    "path": "packages/solid/examples/repro-onSubmit.tsx",
    "content": "import { createSignal, Match, Show, Switch } from \"solid-js\"\nimport { render, useKeyboard, useRenderer } from \"@opentui/solid\"\n\nprocess.env.DEBUG = \"true\"\n\nconst InputTest = () => {\n  const renderer = useRenderer()\n\n  renderer.useConsole = true\n  renderer.console.show()\n\n  const [sig, setS] = createSignal(0)\n\n  useKeyboard((key) => {\n    if (key.name === \"tab\") {\n      setS((s) => (s + 1) % 3)\n    }\n  })\n\n  const onSubmit = () => {\n    console.log(\"input\")\n  }\n\n  return (\n    <box border title=\"input\">\n      <Switch>\n        <Match when={sig() === 0}>\n          <box border title=\"input 0\" height={3}>\n            <input\n              focused\n              placeholder=\"input0\"\n              // onSubmit={onSubmit}\n              onSubmit={() => {\n                console.log(\"input 0\")\n              }}\n            />\n          </box>\n        </Match>\n        <Match when={sig() === 1}>\n          <Show when={sig() > 0}>\n            <box border title=\"input 1\" height={3}>\n              <input\n                focused\n                placeholder=\"input1\"\n                // onSubmit={onSubmit}\n                onSubmit={() => {\n                  console.log(\"input 1\")\n                }}\n              />\n            </box>\n          </Show>\n        </Match>\n        <Match when={sig() === 2}>\n          <box border title=\"input 2\" height={3}>\n            <input\n              focused\n              placeholder=\"input2\"\n              // onSubmit={onSubmit}\n              onSubmit={() => {\n                console.log(\"input 2\")\n              }}\n            />\n          </box>\n        </Match>\n      </Switch>\n    </box>\n  )\n}\n\nrender(InputTest)\n"
  },
  {
    "path": "packages/solid/examples/session.tsx",
    "content": "import { createMemo, createSignal, For, onCleanup, onMount, Show } from \"solid-js\"\nimport { createStore, produce } from \"solid-js/store\"\n\n// Message types\ntype Message = {\n  id: string\n  role: \"user\" | \"assistant\"\n  content: string\n  fullContent: string\n  timestamp: Date\n  isComplete: boolean\n}\n\n// Sample message templates with different sizes\nconst messageTemplates = [\n  \"Hello! How can I help you today?\",\n  \"I understand you're looking for information about React development.\\n\\nLet me provide you with some detailed guidance on best practices\\nfor building modern web applications.\",\n  \"That's a great question! When working with SolidJS, you should\\nconsider the reactive principles that make it different from other frameworks.\\n\\nThe key is understanding signals and how they automatically\\ntrack dependencies.\",\n  \"Based on your requirements, I recommend the following approach:\\n\\n1) Set up your development environment\\n2) Create the basic component structure\\n3) Implement the core functionality\\n4) Add error handling and testing\\n\\nThis systematic approach will ensure you build a robust solution.\",\n  \"The scrollbox component in OpenTUI provides excellent performance\\ncharacteristics for handling large amounts of content.\\n\\nIt uses virtual scrolling under the hood to efficiently render\\nonly the visible portion of your data, making it ideal for chat\\napplications, logs, or any scenario where you need to display\\npotentially unbounded content streams.\",\n  \"Debugging reactive systems can be tricky, but SolidJS provides\\nexcellent developer tools.\\n\\nRemember to use the reactive debugger to inspect signal values\\nand track how changes propagate through your component tree.\\n\\nThe key insight is that effects run immediately when their\\ndependencies change, unlike other frameworks where updates\\nmight be batched.\",\n  \"Performance optimization in frontend applications often comes\\ndown to minimizing unnecessary re-renders.\\n\\nIn SolidJS, this means being careful about how you structure\\nyour signals and computations.\\n\\nUse createMemo for expensive calculations and avoid creating\\nsignals inside loops or conditional blocks when possible.\",\n  \"TypeScript provides excellent type safety for JavaScript applications,\\nbut it requires careful consideration of how types interact\\nwith reactive systems.\\n\\nThe key is to properly type your signals and ensure that type\\ninformation flows correctly through your component hierarchy.\",\n  \"When designing user interfaces, accessibility should always be\\na primary consideration.\\n\\nThis means providing proper ARIA labels, keyboard navigation\\nsupport, and ensuring that your components work well with\\nscreen readers.\\n\\nOpenTUI makes this easier by providing semantic components\\nthat handle many accessibility concerns automatically.\",\n  \"Testing reactive applications requires a different mindset\\nthan traditional unit testing.\\n\\nYou need to think about testing the reactive flow - how signals\\nupdate when their dependencies change, how effects respond\\nto those changes, and whether your UI correctly reflects\\nthe current state of your application data.\",\n]\n\nexport function Session() {\n  const [messages, setMessages] = createStore<{ data: Message[] }>({ data: [] })\n  let [isChunkingActive, setIsChunkingActive] = createSignal(false)\n\n  // Generate a random message\n  const generateMessage = (): Message => {\n    const role = Math.random() > 0.5 ? \"user\" : \"assistant\"\n    let fullContent = messageTemplates[Math.floor(Math.random() * messageTemplates.length)]\n    if (!fullContent) {\n      fullContent = messageTemplates[0]!\n    }\n    return {\n      id: Math.random().toString(36).substring(2, 9),\n      role,\n      content: \"\", // Start empty, will be filled in chunks\n      fullContent,\n      timestamp: new Date(),\n      isComplete: false,\n    }\n  }\n\n  // Add a new message to the list (only if not already chunking)\n  const addMessage = () => {\n    if (isChunkingActive()) return // Don't add new messages while one is being chunked\n\n    const newMessage = generateMessage()\n\n    // Set the full content on the message but mark it as incomplete\n    newMessage.content = \"\" // Start empty\n\n    setMessages(\"data\", messages.data.length, newMessage)\n\n    // Start chunking this message\n\n    setIsChunkingActive(true)\n    startChunkingMessage(newMessage.id, newMessage.fullContent)\n  }\n\n  // Simulate chunked arrival for a specific message\n  let chunkInterval: ReturnType<typeof setInterval> | undefined\n  let initialTimeout: ReturnType<typeof setTimeout> | undefined\n\n  const stopChunking = () => {\n    if (chunkInterval) {\n      clearInterval(chunkInterval)\n      chunkInterval = undefined\n    }\n    if (initialTimeout) {\n      clearTimeout(initialTimeout)\n      initialTimeout = undefined\n    }\n    setIsChunkingActive(false)\n  }\n\n  const startChunkingMessage = (messageId: string, fullContent: string) => {\n    let currentIndex = 0\n    const chunkSize = Math.floor(Math.random() * 5) + 1 // 1-5 characters per chunk\n\n    chunkInterval = setInterval(() => {\n      setMessages(\n        \"data\",\n        produce((ms) => {\n          const message = ms.find((m) => m.id === messageId)\n          if (message) {\n            message.content = fullContent.slice(0, currentIndex + chunkSize)\n            message.isComplete = currentIndex + chunkSize >= fullContent.length\n          }\n        }),\n      )\n\n      currentIndex += chunkSize\n\n      if (currentIndex >= fullContent.length) {\n        stopChunking()\n        // Immediately start the next message\n        addMessage()\n      }\n    }, 16)\n  }\n\n  onMount(() => {\n    initialTimeout = setTimeout(addMessage, 500)\n  })\n\n  onCleanup(() => {\n    stopChunking()\n  })\n\n  return (\n    <box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexGrow={1} maxHeight=\"100%\">\n      <box paddingBottom={1}>\n        <text>\n          <span style={{ fg: \"#00ff00\" }}>📨</span> <span style={{ fg: \"#ffffff\" }}>Live Message Stream</span>\n        </text>\n        <text fg=\"#666666\">Messages arrive in chunks - watch them build character by character!</text>\n      </box>\n\n      <scrollbox\n        scrollbarOptions={{ visible: true }}\n        stickyScroll={true}\n        stickyStart=\"bottom\"\n        paddingTop={1}\n        paddingBottom={1}\n        contentOptions={{\n          flexGrow: 1,\n          gap: 1,\n        }}\n      >\n        <For each={messages.data}>{(message) => <MessageItem message={message} />}</For>\n      </scrollbox>\n\n      <box paddingTop={1}>\n        <text fg=\"#666666\">\n          Messages: {messages.data.length} |{\" \"}\n          <Show when={isChunkingActive()} fallback=\"Waiting for next message...\">\n            Receiving message...\n          </Show>\n        </text>\n      </box>\n    </box>\n  )\n}\n\n// Simple message display component\nfunction MessageItem(props: { message: Message }) {\n  const timeString = createMemo(() =>\n    props.message.timestamp.toLocaleTimeString([], {\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      second: \"2-digit\",\n    }),\n  )\n\n  return (\n    <box\n      paddingTop={1}\n      paddingBottom={1}\n      paddingLeft={2}\n      paddingRight={2}\n      border={[\"left\"]}\n      borderColor={props.message.role === \"user\" ? \"#00ff00\" : \"#0088ff\"}\n      backgroundColor={props.message.role === \"user\" ? \"#001100\" : \"#000022\"}\n    >\n      <box flexDirection=\"row\" paddingBottom={0.5}>\n        <text>\n          <Show when={props.message.role === \"user\"} fallback={<span style={{ fg: \"#0088ff\" }}>🤖 Assistant</span>}>\n            <span style={{ fg: \"#00ff00\" }}>👤 You</span>\n          </Show>\n        </text>\n        <box flexGrow={1} />\n        <text fg=\"#666666\">{timeString()}</text>\n      </box>\n\n      <text>\n        {props.message.content}\n        <Show when={!props.message.isComplete} fallback={\"\"}>\n          <span style={{ fg: \"#ffff00\" }}>▊</span>\n        </Show>\n      </text>\n\n      <Show when={!props.message.isComplete}>\n        {() => {\n          const progress = createMemo(() =>\n            Math.round((props.message.content.length / props.message.fullContent!.length) * 100),\n          )\n          return (\n            <text fg=\"#666666\" paddingTop={0.5}>\n              Receiving message... ({progress()}%)\n            </text>\n          )\n        }}\n      </Show>\n    </box>\n  )\n}\n"
  },
  {
    "path": "packages/solid/examples/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // Environment setup & latest features\n    \"lib\": [\"ESNext\"],\n    \"target\": \"ESNext\",\n    \"module\": \"Preserve\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"@opentui/solid\",\n    \"allowJs\": true,\n    // Bundler mode\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    // Best practices\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noImplicitOverride\": true,\n    // Some stricter flags (disabled by default)\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false\n  }\n}\n"
  },
  {
    "path": "packages/solid/index.ts",
    "content": "import { CliRenderer, createCliRenderer, engine, type CliRendererConfig } from \"@opentui/core\"\nimport { createTestRenderer, type TestRendererOptions } from \"@opentui/core/testing\"\nimport type { JSX } from \"./jsx-runtime\"\nimport { RendererContext } from \"./src/elements/index.js\"\nimport { _render as renderInternal, createComponent } from \"./src/reconciler.js\"\n\ntype DisposeFn = () => void\n\nconst mountSolidRoot = (renderer: CliRenderer, node: () => JSX.Element) => {\n  let dispose: DisposeFn | undefined\n  let disposeRequested = false\n  let disposed = false\n  let mounting = true\n  let destroyRequested = false\n\n  const originalDestroy = renderer.destroy.bind(renderer)\n\n  const runDispose = () => {\n    if (disposed) {\n      return\n    }\n\n    if (!dispose) {\n      disposeRequested = true\n      return\n    }\n\n    disposed = true\n    dispose()\n  }\n\n  renderer.once(\"destroy\", runDispose)\n\n  renderer.destroy = () => {\n    if (mounting) {\n      destroyRequested = true\n      return\n    }\n\n    originalDestroy()\n  }\n\n  try {\n    dispose = renderInternal(\n      () =>\n        createComponent(RendererContext.Provider, {\n          get value() {\n            return renderer\n          },\n          get children() {\n            return createComponent(node, {})\n          },\n        }),\n      renderer.root,\n    )\n  } finally {\n    mounting = false\n    renderer.destroy = originalDestroy\n  }\n\n  if (disposeRequested) {\n    runDispose()\n  }\n\n  if (destroyRequested) {\n    originalDestroy()\n  }\n}\n\nexport const render = async (node: () => JSX.Element, rendererOrConfig: CliRenderer | CliRendererConfig = {}) => {\n  const renderer =\n    rendererOrConfig instanceof CliRenderer\n      ? rendererOrConfig\n      : await createCliRenderer({\n          ...rendererOrConfig,\n          onDestroy: () => {\n            rendererOrConfig.onDestroy?.()\n          },\n        })\n\n  engine.attach(renderer)\n  mountSolidRoot(renderer, node)\n}\n\nexport const testRender = async (node: () => JSX.Element, renderConfig: TestRendererOptions = {}) => {\n  const testSetup = await createTestRenderer({\n    ...renderConfig,\n    onDestroy: () => {\n      renderConfig.onDestroy?.()\n    },\n  })\n\n  engine.attach(testSetup.renderer)\n  mountSolidRoot(testSetup.renderer, node)\n\n  return testSetup\n}\n\nexport * from \"./src/reconciler.js\"\nexport * from \"./src/elements/index.js\"\nexport * from \"./src/time-to-first-draw.js\"\nexport * from \"./src/plugins/slot.js\"\nexport * from \"./src/types/elements.js\"\nexport { type JSX }\n"
  },
  {
    "path": "packages/solid/jsx-runtime.d.ts",
    "content": "import { Renderable } from \"@opentui/core\"\nimport type {\n  AsciiFontProps,\n  BoxProps,\n  CodeProps,\n  ExtendedIntrinsicElements,\n  InputProps,\n  LinkProps,\n  MarkdownProps,\n  OpenTUIComponents,\n  ScrollBoxProps,\n  SelectProps,\n  SpanProps,\n  TabSelectProps,\n  TextareaProps,\n  TextProps,\n} from \"./src/types/elements.js\"\nimport type { DomNode } from \"./dist\"\n\ndeclare namespace JSX {\n  // Replace Node with Renderable\n  type Element = DomNode | ArrayElement | string | number | boolean | null | undefined\n\n  type ArrayElement = Array<Element>\n\n  interface IntrinsicElements extends ExtendedIntrinsicElements<OpenTUIComponents> {\n    box: BoxProps\n    text: TextProps\n    span: SpanProps\n    input: InputProps\n    select: SelectProps\n    ascii_font: AsciiFontProps\n    tab_select: TabSelectProps\n    scrollbox: ScrollBoxProps\n    code: CodeProps\n    textarea: TextareaProps\n    markdown: MarkdownProps\n\n    b: SpanProps\n    strong: SpanProps\n    i: SpanProps\n    em: SpanProps\n    u: SpanProps\n    br: {}\n    a: LinkProps\n  }\n\n  interface ElementChildrenAttribute {\n    children: {}\n  }\n}\n"
  },
  {
    "path": "packages/solid/package.json",
    "content": "{\n  \"name\": \"@opentui/solid\",\n  \"version\": \"0.1.90\",\n  \"description\": \"SolidJS renderer for OpenTUI\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/anomalyco/opentui\",\n    \"directory\": \"packages/solid\"\n  },\n  \"module\": \"index.ts\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"main\": \"index.ts\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"bun scripts/build.ts\",\n    \"build:examples\": \"bun examples/build.ts\",\n    \"publish\": \"bun scripts/publish.ts\",\n    \"test\": \"bun test\"\n  },\n  \"exports\": {\n    \".\": {\n      \"types\": \"./index.ts\",\n      \"import\": \"./index.ts\"\n    },\n    \"./preload\": {\n      \"import\": \"./scripts/preload.ts\"\n    },\n    \"./bun-plugin\": {\n      \"import\": \"./scripts/solid-plugin.ts\"\n    },\n    \"./runtime-plugin-support\": {\n      \"import\": \"./scripts/runtime-plugin-support.ts\"\n    },\n    \"./jsx-runtime\": \"./jsx-runtime.d.ts\",\n    \"./jsx-dev-runtime\": \"./jsx-runtime.d.ts\"\n  },\n  \"devDependencies\": {\n    \"@types/babel__core\": \"7.20.5\",\n    \"@types/bun\": \"latest\",\n    \"@types/node\": \"^24.0.0\",\n    \"solid-js\": \"1.9.11\",\n    \"typescript\": \"^5\"\n  },\n  \"dependencies\": {\n    \"@babel/core\": \"7.28.0\",\n    \"@babel/preset-typescript\": \"7.27.1\",\n    \"@opentui/core\": \"workspace:*\",\n    \"babel-plugin-module-resolver\": \"5.0.2\",\n    \"babel-preset-solid\": \"1.9.10\",\n    \"entities\": \"7.0.1\",\n    \"s-js\": \"^0.4.9\"\n  },\n  \"peerDependencies\": {\n    \"solid-js\": \"1.9.11\"\n  }\n}\n"
  },
  {
    "path": "packages/solid/scripts/build.ts",
    "content": "import { spawnSync, type SpawnSyncReturns } from \"node:child_process\"\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"fs\"\nimport { dirname, join, resolve } from \"path\"\nimport { fileURLToPath } from \"url\"\nimport process from \"process\"\nimport { createSolidTransformPlugin } from \"./solid-plugin.js\"\n\ninterface PackageJson {\n  name: string\n  version: string\n  license?: string\n  repository?: any\n  description?: string\n  homepage?: string\n  author?: string\n  bugs?: any\n  keywords?: string[]\n  module?: string\n  main?: string\n  types?: string\n  type?: string\n  exports?: any\n  dependencies?: Record<string, string>\n  devDependencies?: Record<string, string>\n  peerDependencies?: Record<string, string>\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = resolve(__dirname, \"..\")\nconst projectRootDir = resolve(rootDir, \"../..\")\nconst licensePath = join(projectRootDir, \"LICENSE\")\nconst packageJson: PackageJson = JSON.parse(readFileSync(join(rootDir, \"package.json\"), \"utf8\"))\n\nconst args = process.argv.slice(2)\nconst isDev = args.includes(\"--dev\")\nconst isCi = args.includes(\"--ci\")\n\nconst replaceLinks = (text: string): string => {\n  return packageJson.homepage\n    ? text.replace(\n        /(\\[.*?\\]\\()(\\.\\/.*?\\))/g,\n        (_, p1: string, p2: string) => `${p1}${packageJson.homepage}/blob/HEAD/${p2.replace(\"./\", \"\")}`,\n      )\n    : text\n}\n\nconst requiredFields: (keyof PackageJson)[] = [\"name\", \"version\", \"description\"]\nconst missingRequired = requiredFields.filter((field) => !packageJson[field])\nif (missingRequired.length > 0) {\n  console.error(`Error: Missing required fields in package.json: ${missingRequired.join(\", \")}`)\n  process.exit(1)\n}\n\nconsole.log(`Building @opentui/solid library${isDev ? \" (dev mode)\" : \"\"}...`)\n\nconst distDir = join(rootDir, \"dist\")\nrmSync(distDir, { recursive: true, force: true })\nmkdirSync(distDir, { recursive: true })\n\nconst externalDeps: string[] = [\n  ...Object.keys(packageJson.dependencies || {}),\n  ...Object.keys(packageJson.peerDependencies || {}),\n]\n\nif (!packageJson.module) {\n  console.error(\"Error: 'module' field not found in package.json\")\n  process.exit(1)\n}\n\nconsole.log(\"Building main entry point...\")\nconst mainBuildResult = await Bun.build({\n  entrypoints: [join(rootDir, packageJson.module)],\n  target: \"bun\",\n  outdir: join(rootDir, \"dist\"),\n  external: externalDeps,\n  plugins: [createSolidTransformPlugin()],\n  splitting: true,\n})\n\nif (!mainBuildResult.success) {\n  console.error(\"Build failed for main entry point:\", mainBuildResult.logs)\n  process.exit(1)\n}\n\nconsole.log(\"Generating TypeScript declarations...\")\n\nconst tsconfigBuildPath = join(rootDir, \"tsconfig.build.json\")\n\nconst coreRootDir = resolve(rootDir, \"../core\")\nconst corePackageJsonPath = join(coreRootDir, \"package.json\")\n\nif (existsSync(corePackageJsonPath)) {\n  console.log(\"Ensuring @opentui/core declarations are up to date...\")\n\n  const coreBuildResult: SpawnSyncReturns<Buffer> = spawnSync(\"bun\", [\"run\", \"build:lib\"], {\n    cwd: coreRootDir,\n    stdio: \"inherit\",\n  })\n\n  if (coreBuildResult.status !== 0) {\n    console.error(\"Error: Failed to build @opentui/core declarations required by @opentui/solid\")\n    process.exit(1)\n  }\n}\n\nconst tscResult: SpawnSyncReturns<Buffer> = spawnSync(\"bunx\", [\"tsc\", \"-p\", tsconfigBuildPath], {\n  cwd: rootDir,\n  stdio: \"inherit\",\n})\n\nif (tscResult.status !== 0) {\n  if (isCi) {\n    console.error(\"Error: TypeScript declaration generation failed\")\n    process.exit(1)\n  }\n  console.warn(\"Warning: TypeScript declaration generation failed\")\n} else {\n  console.log(\"TypeScript declarations generated\")\n}\n\nif (existsSync(join(rootDir, \"jsx-runtime.d.ts\"))) {\n  copyFileSync(join(rootDir, \"jsx-runtime.d.ts\"), join(distDir, \"jsx-runtime.d.ts\"))\n}\n\nmkdirSync(join(distDir, \"scripts\"), { recursive: true })\n\nif (existsSync(join(rootDir, \"scripts\", \"solid-plugin.ts\"))) {\n  copyFileSync(join(rootDir, \"scripts\", \"solid-plugin.ts\"), join(distDir, \"scripts\", \"solid-plugin.ts\"))\n}\n\nif (existsSync(join(rootDir, \"scripts\", \"preload.ts\"))) {\n  copyFileSync(join(rootDir, \"scripts\", \"preload.ts\"), join(distDir, \"scripts\", \"preload.ts\"))\n}\n\nif (existsSync(join(rootDir, \"scripts\", \"runtime-plugin-support.ts\"))) {\n  copyFileSync(\n    join(rootDir, \"scripts\", \"runtime-plugin-support.ts\"),\n    join(distDir, \"scripts\", \"runtime-plugin-support.ts\"),\n  )\n}\n\nconst exports = {\n  \".\": {\n    types: \"./index.d.ts\",\n    import: \"./index.js\",\n    require: \"./index.js\",\n  },\n  \"./preload\": {\n    import: \"./scripts/preload.ts\",\n  },\n  \"./bun-plugin\": {\n    types: \"./scripts/solid-plugin.d.ts\",\n    import: \"./scripts/solid-plugin.ts\",\n  },\n  \"./runtime-plugin-support\": {\n    types: \"./scripts/runtime-plugin-support.d.ts\",\n    import: \"./scripts/runtime-plugin-support.ts\",\n  },\n  \"./jsx-runtime\": \"./jsx-runtime.d.ts\",\n  \"./jsx-dev-runtime\": \"./jsx-runtime.d.ts\",\n}\n\n// Process dependencies to replace workspace references with actual versions\nconst processedDependencies = { ...packageJson.dependencies }\nif (processedDependencies[\"@opentui/core\"] === \"workspace:*\") {\n  processedDependencies[\"@opentui/core\"] = packageJson.version\n}\n\nwriteFileSync(\n  join(distDir, \"package.json\"),\n  JSON.stringify(\n    {\n      name: packageJson.name,\n      module: \"index.js\",\n      main: \"index.js\",\n      types: \"index.d.ts\",\n      type: packageJson.type,\n      version: packageJson.version,\n      description: packageJson.description,\n      keywords: packageJson.keywords,\n      license: packageJson.license,\n      author: packageJson.author,\n      homepage: packageJson.homepage,\n      repository: packageJson.repository,\n      bugs: packageJson.bugs,\n      exports,\n      dependencies: processedDependencies,\n      devDependencies: packageJson.devDependencies,\n      peerDependencies: packageJson.peerDependencies,\n    },\n    null,\n    2,\n  ),\n)\n\nconst readmePath = join(rootDir, \"README.md\")\nif (existsSync(readmePath)) {\n  writeFileSync(join(distDir, \"README.md\"), replaceLinks(readFileSync(readmePath, \"utf8\")))\n} else {\n  console.warn(\"Warning: README.md not found in solid package\")\n}\n\nif (existsSync(licensePath)) {\n  copyFileSync(licensePath, join(distDir, \"LICENSE\"))\n} else {\n  console.warn(\"Warning: LICENSE file not found in project root\")\n}\n\nconsole.log(\"Library built at:\", distDir)\n"
  },
  {
    "path": "packages/solid/scripts/preload.ts",
    "content": "import { ensureSolidTransformPlugin } from \"./solid-plugin.js\"\n\nensureSolidTransformPlugin()\n"
  },
  {
    "path": "packages/solid/scripts/publish.ts",
    "content": "import { spawnSync, type SpawnSyncReturns } from \"node:child_process\"\nimport { readFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport process from \"node:process\"\nimport { fileURLToPath } from \"node:url\"\n\ninterface PackageJson {\n  name: string\n  version: string\n  dependencies?: Record<string, string>\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = resolve(__dirname, \"..\")\n\nconst packageJson: PackageJson = JSON.parse(readFileSync(join(rootDir, \"package.json\"), \"utf8\"))\n\nconsole.log(`Publishing @opentui/solid@${packageJson.version}...`)\nconsole.log(\"Make sure you've run the pre-publish validation script first!\")\n\nconst distDir = join(rootDir, \"dist\")\n\nconsole.log(`\\nPublishing ${packageJson.name}@${packageJson.version}...`)\n\nconst isSnapshot = packageJson.version.includes(\"-snapshot\") || /^0\\.0\\.0-\\d{8}-[a-f0-9]{8}$/.test(packageJson.version)\nconst publishArgs = [\"publish\", \"--access=public\"]\n\nif (isSnapshot) {\n  publishArgs.push(\"--tag\", \"snapshot\")\n  console.log(`  Publishing as snapshot (--tag snapshot)`)\n}\n\nconst publish: SpawnSyncReturns<Buffer> = spawnSync(\"npm\", publishArgs, {\n  cwd: distDir,\n  stdio: \"inherit\",\n})\n\nif (publish.status !== 0) {\n  console.error(`Failed to publish '${packageJson.name}@${packageJson.version}'.`)\n  process.exit(1)\n}\n\nconsole.log(`Successfully published '${packageJson.name}@${packageJson.version}'`)\n"
  },
  {
    "path": "packages/solid/scripts/runtime-plugin-support.ts",
    "content": "import { plugin as registerBunPlugin } from \"bun\"\nimport * as coreRuntime from \"@opentui/core\"\nimport {\n  createRuntimePlugin,\n  isCoreRuntimeModuleSpecifier,\n  runtimeModuleIdForSpecifier,\n  type RuntimeModuleEntry,\n} from \"@opentui/core/runtime-plugin\"\nimport * as solidJsRuntime from \"solid-js\"\nimport * as solidJsStoreRuntime from \"solid-js/store\"\nimport * as solidRuntime from \"../index\"\nimport { ensureSolidTransformPlugin } from \"./solid-plugin\"\n\nconst runtimePluginSupportInstalledKey = Symbol.for(\"opentui.solid.runtime-plugin-support\")\n\ntype RuntimePluginSupportState = typeof globalThis & {\n  [runtimePluginSupportInstalledKey]?: boolean\n}\n\nconst additionalRuntimeModules: Record<string, RuntimeModuleEntry> = {\n  \"@opentui/solid\": solidRuntime as Record<string, unknown>,\n  \"solid-js\": solidJsRuntime as Record<string, unknown>,\n  \"solid-js/store\": solidJsStoreRuntime as Record<string, unknown>,\n}\n\nconst resolveRuntimeSpecifier = (specifier: string): string | null => {\n  if (!isCoreRuntimeModuleSpecifier(specifier) && !additionalRuntimeModules[specifier]) {\n    return null\n  }\n\n  return runtimeModuleIdForSpecifier(specifier)\n}\n\nexport function ensureRuntimePluginSupport(): boolean {\n  const state = globalThis as RuntimePluginSupportState\n\n  if (state[runtimePluginSupportInstalledKey]) {\n    return false\n  }\n\n  ensureSolidTransformPlugin({\n    moduleName: runtimeModuleIdForSpecifier(\"@opentui/solid\"),\n    resolvePath(specifier) {\n      return resolveRuntimeSpecifier(specifier)\n    },\n  })\n\n  registerBunPlugin(\n    createRuntimePlugin({\n      core: coreRuntime as Record<string, unknown>,\n      additional: additionalRuntimeModules,\n    }),\n  )\n\n  state[runtimePluginSupportInstalledKey] = true\n  return true\n}\n\nensureRuntimePluginSupport()\n"
  },
  {
    "path": "packages/solid/scripts/solid-plugin.ts",
    "content": "import { transformAsync } from \"@babel/core\"\n// @ts-expect-error - Types not important.\nimport ts from \"@babel/preset-typescript\"\n// @ts-expect-error - Types not important.\nimport moduleResolver from \"babel-plugin-module-resolver\"\n// @ts-expect-error - Types not important.\nimport solid from \"babel-preset-solid\"\nimport { plugin as registerBunPlugin, type BunPlugin } from \"bun\"\n\nexport type ResolveImportPath = (specifier: string) => string | null\n\nconst solidTransformStateKey = Symbol.for(\"opentui.solid.transform\")\n\ntype SolidTransformRuntime = {\n  moduleName?: string\n  resolvePath?: ResolveImportPath\n}\n\ntype SolidTransformState = {\n  installed: boolean\n  runtime?: SolidTransformRuntime\n}\n\ntype GlobalSolidTransformState = typeof globalThis & {\n  [solidTransformStateKey]?: SolidTransformState\n}\n\nexport interface CreateSolidTransformPluginOptions {\n  moduleName?: string\n  resolvePath?: ResolveImportPath\n}\n\nconst getSolidTransformState = (): SolidTransformState => {\n  const state = globalThis as GlobalSolidTransformState\n  state[solidTransformStateKey] ??= { installed: false }\n  return state[solidTransformStateKey]\n}\n\nconst getSolidTransformRuntime = (): SolidTransformRuntime => {\n  return getSolidTransformState().runtime ?? {}\n}\n\nconst sourcePath = (path: string): string => {\n  const searchIndex = path.indexOf(\"?\")\n  const hashIndex = path.indexOf(\"#\")\n  const end = [searchIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0]\n  return end === undefined ? path : path.slice(0, end)\n}\n\nconst hasSolidTransformRuntime = (input: CreateSolidTransformPluginOptions): boolean => {\n  return input.moduleName !== undefined || input.resolvePath !== undefined\n}\n\nexport function ensureSolidTransformPlugin(input: CreateSolidTransformPluginOptions = {}): boolean {\n  const state = getSolidTransformState()\n\n  if (hasSolidTransformRuntime(input)) {\n    state.runtime = {\n      moduleName: input.moduleName,\n      resolvePath: input.resolvePath,\n    }\n  }\n\n  if (state.installed) {\n    return false\n  }\n\n  registerBunPlugin(createSolidTransformPlugin())\n  state.installed = true\n  return true\n}\n\nexport function resetSolidTransformPluginState(): void {\n  const state = getSolidTransformState()\n  state.installed = false\n  delete state.runtime\n}\n\nexport function createSolidTransformPlugin(input: CreateSolidTransformPluginOptions = {}): BunPlugin {\n  return {\n    name: \"bun-plugin-solid\",\n    setup: (build) => {\n      build.onLoad(\n        { filter: /[\\/\\\\]node_modules[\\/\\\\]solid-js[\\/\\\\]dist[\\/\\\\]server\\.js(?:[?#].*)?$/ },\n        async (args) => {\n          const path = sourcePath(args.path).replace(\"server.js\", \"solid.js\")\n          const file = Bun.file(path)\n          const code = await file.text()\n          return { contents: code, loader: \"js\" }\n        },\n      )\n\n      build.onLoad(\n        { filter: /[\\/\\\\]node_modules[\\/\\\\]solid-js[\\/\\\\]store[\\/\\\\]dist[\\/\\\\]server\\.js(?:[?#].*)?$/ },\n        async (args) => {\n          const path = sourcePath(args.path).replace(\"server.js\", \"store.js\")\n          const file = Bun.file(path)\n          const code = await file.text()\n          return { contents: code, loader: \"js\" }\n        },\n      )\n\n      build.onLoad({ filter: /\\.(js|ts)x(?:[?#].*)?$/ }, async (args) => {\n        const path = sourcePath(args.path)\n        const file = Bun.file(path)\n        const code = await file.text()\n        const runtime = getSolidTransformRuntime()\n        const moduleName = input.moduleName ?? runtime.moduleName ?? \"@opentui/solid\"\n        const resolvePath = input.resolvePath ?? runtime.resolvePath\n        const plugins = resolvePath\n          ? [\n              [\n                moduleResolver,\n                {\n                  resolvePath(specifier: string) {\n                    return resolvePath(specifier) ?? specifier\n                  },\n                },\n              ],\n            ]\n          : []\n\n        const transforms = await transformAsync(code, {\n          filename: path,\n          plugins,\n          presets: [\n            [\n              solid,\n              {\n                moduleName,\n                generate: \"universal\",\n              },\n            ],\n            [ts],\n          ],\n        })\n\n        return {\n          contents: transforms?.code ?? \"\",\n          loader: \"js\",\n        }\n      })\n    },\n  }\n}\n\nconst solidTransformPlugin = createSolidTransformPlugin()\n\nexport default solidTransformPlugin\n"
  },
  {
    "path": "packages/solid/src/elements/extras.ts",
    "content": "import { createEffect, createMemo, getOwner, onCleanup, runWithOwner, splitProps, untrack } from \"solid-js\"\nimport { createSlotNode, createElement, insert, spread, type DomNode } from \"../reconciler.js\"\nimport type { JSX } from \"../../jsx-runtime\"\nimport type { ValidComponent, ComponentProps } from \"solid-js\"\nimport { useRenderer } from \"./hooks.js\"\n\n/**\n * Renders components somewhere else in the DOM\n *\n * Useful for inserting modals and tooltips outside of an cropping layout. If no mount point is given, the portal is inserted on the root renderable; it is wrapped in a `<box>`\n *\n * @description https://docs.solidjs.com/reference/components/portal\n */\nexport function Portal(props: { mount?: DomNode; ref?: (el: {}) => void; children: JSX.Element }): DomNode {\n  const renderer = useRenderer()\n\n  const marker = createSlotNode(),\n    mount = () => props.mount || renderer.root,\n    owner = getOwner()\n  let content: undefined | (() => JSX.Element)\n\n  createEffect(\n    () => {\n      // basically we backdoor into a sort of renderEffect here\n      content || (content = runWithOwner(owner, () => createMemo(() => props.children)))\n      const el = mount()\n      const container = createElement(\"box\"),\n        renderRoot = container\n\n      Object.defineProperty(container, \"_$host\", {\n        get() {\n          return marker.parent\n        },\n        configurable: true,\n      })\n      insert(renderRoot, content)\n      el.add(container)\n      props.ref && (props as any).ref(container)\n      onCleanup(() => el.remove(container.id))\n    },\n    undefined,\n    { render: true },\n  )\n  return marker\n}\n\nexport type DynamicProps<T extends ValidComponent, P = ComponentProps<T>> = {\n  [K in keyof P]: P[K]\n} & {\n  component: T | undefined\n}\n\n/**\n * Renders an arbitrary component or element with the given props\n *\n * This is a lower level version of the `Dynamic` component, useful for\n * performance optimizations in libraries. Do not use this unless you know\n * what you are doing.\n * ```typescript\n * const element = () => multiline() ? 'textarea' : 'input';\n * createDynamic(element, { value: value() });\n * ```\n * @description https://docs.solidjs.com/reference/components/dynamic\n */\nexport function createDynamic<T extends ValidComponent>(\n  component: () => T | undefined,\n  props: ComponentProps<T>,\n): JSX.Element {\n  const cached = createMemo<Function | string | undefined>(component)\n  return createMemo(() => {\n    const component = cached()\n    switch (typeof component) {\n      case \"function\":\n        // if (isDev) Object.assign(component, { [$DEVCOMP]: true })\n        return untrack(() => component(props))\n\n      case \"string\":\n        const el = createElement(component)\n        spread(el, props)\n        return el\n\n      default:\n        break\n    }\n  }) as unknown as JSX.Element\n}\n\n/**\n * Renders an arbitrary custom or native component and passes the other props\n * ```typescript\n * <Dynamic component={multiline() ? 'textarea' : 'input'} value={value()} />\n * ```\n * @description https://docs.solidjs.com/reference/components/dynamic\n */\nexport function Dynamic<T extends ValidComponent>(props: DynamicProps<T>): JSX.Element {\n  const [, others] = splitProps(props, [\"component\"])\n  return createDynamic(() => props.component, others as ComponentProps<T>)\n}\n"
  },
  {
    "path": "packages/solid/src/elements/hooks.ts",
    "content": "import {\n  engine,\n  PasteEvent,\n  Selection,\n  Timeline,\n  type CliRenderer,\n  type KeyEvent,\n  type TimelineOptions,\n} from \"@opentui/core\"\nimport { createContext, createSignal, onCleanup, onMount, useContext } from \"solid-js\"\n\nexport const RendererContext = createContext<CliRenderer>()\n\nexport const useRenderer = () => {\n  const renderer = useContext(RendererContext)\n\n  if (!renderer) {\n    throw new Error(\"No renderer found\")\n  }\n\n  return renderer\n}\n\nexport const onResize = (callback: (width: number, height: number) => void) => {\n  const renderer = useRenderer()\n\n  onMount(() => {\n    renderer.on(\"resize\", callback)\n  })\n\n  onCleanup(() => {\n    renderer.off(\"resize\", callback)\n  })\n}\n\nexport const useTerminalDimensions = () => {\n  const renderer = useRenderer()\n  const [terminalDimensions, setTerminalDimensions] = createSignal<{\n    width: number\n    height: number\n  }>({ width: renderer.width, height: renderer.height })\n\n  const callback = (width: number, height: number) => {\n    setTerminalDimensions({ width, height })\n  }\n\n  onResize(callback)\n\n  return terminalDimensions\n}\n\nexport interface UseKeyboardOptions {\n  /** Include release events - callback receives events with eventType: \"release\" */\n  release?: boolean\n}\n\n/**\n * Subscribe to keyboard events.\n *\n * By default, only receives press events (including key repeats with `repeated: true`).\n * Use `options.release` to also receive release events.\n *\n * @example\n * // Basic press handling (includes repeats)\n * useKeyboard((e) => console.log(e.name, e.repeated ? \"(repeat)\" : \"\"))\n *\n * // With release events\n * useKeyboard((e) => {\n *   if (e.eventType === \"release\") keys.delete(e.name)\n *   else keys.add(e.name)\n * }, { release: true })\n */\nexport const useKeyboard = (callback: (key: KeyEvent) => void, options?: UseKeyboardOptions) => {\n  const renderer = useRenderer()\n  const keyHandler = renderer.keyInput\n  onMount(() => {\n    keyHandler.on(\"keypress\", callback)\n    if (options?.release) {\n      keyHandler.on(\"keyrelease\", callback)\n    }\n  })\n\n  onCleanup(() => {\n    keyHandler.off(\"keypress\", callback)\n    if (options?.release) {\n      keyHandler.off(\"keyrelease\", callback)\n    }\n  })\n}\n\nexport const usePaste = (callback: (event: PasteEvent) => void) => {\n  const renderer = useRenderer()\n  const keyHandler = renderer.keyInput\n  onMount(() => {\n    keyHandler.on(\"paste\", callback)\n  })\n\n  onCleanup(() => {\n    keyHandler.off(\"paste\", callback)\n  })\n}\n\n/**\n * @deprecated renamed to useKeyboard\n */\nexport const useKeyHandler = useKeyboard\n\nexport const onFocus = (callback: () => void) => {\n  const renderer = useRenderer()\n\n  onMount(() => {\n    renderer.on(\"focus\", callback)\n  })\n\n  onCleanup(() => {\n    renderer.off(\"focus\", callback)\n  })\n}\n\nexport const onBlur = (callback: () => void) => {\n  const renderer = useRenderer()\n\n  onMount(() => {\n    renderer.on(\"blur\", callback)\n  })\n\n  onCleanup(() => {\n    renderer.off(\"blur\", callback)\n  })\n}\n\nexport const useSelectionHandler = (callback: (selection: Selection) => void) => {\n  const renderer = useRenderer()\n\n  onMount(() => {\n    renderer.on(\"selection\", callback)\n  })\n\n  onCleanup(() => {\n    renderer.off(\"selection\", callback)\n  })\n}\n\nexport const useTimeline = (options: TimelineOptions = {}): Timeline => {\n  const timeline = new Timeline(options)\n\n  onMount(() => {\n    if (options.autoplay !== false) {\n      timeline.play()\n    }\n    engine.register(timeline)\n  })\n\n  onCleanup(() => {\n    timeline.pause()\n    engine.unregister(timeline)\n  })\n\n  return timeline\n}\n"
  },
  {
    "path": "packages/solid/src/elements/index.ts",
    "content": "import {\n  ASCIIFontRenderable,\n  BoxRenderable,\n  CodeRenderable,\n  DiffRenderable,\n  InputRenderable,\n  LineNumberRenderable,\n  MarkdownRenderable,\n  ScrollBoxRenderable,\n  SelectRenderable,\n  TabSelectRenderable,\n  TextareaRenderable,\n  TextAttributes,\n  TextNodeRenderable,\n  TextRenderable,\n  type RenderContext,\n  type TextNodeOptions,\n} from \"@opentui/core\"\nimport type { RenderableConstructor } from \"../types/elements.js\"\nexport * from \"./hooks.js\"\nexport * from \"./extras.js\"\nexport * from \"./slot.js\"\n\nclass SpanRenderable extends TextNodeRenderable {\n  constructor(\n    private readonly _ctx: RenderContext | null,\n    options: TextNodeOptions,\n  ) {\n    super(options)\n  }\n}\n\nexport const textNodeKeys = [\"span\", \"b\", \"strong\", \"i\", \"em\", \"u\", \"a\"] as const\nexport type TextNodeKey = (typeof textNodeKeys)[number]\n\nclass TextModifierRenderable extends SpanRenderable {\n  constructor(options: any, modifier?: TextNodeKey) {\n    super(null, options)\n\n    // Set appropriate attributes based on modifier type\n    if (modifier === \"b\" || modifier === \"strong\") {\n      this.attributes = (this.attributes || 0) | TextAttributes.BOLD\n    } else if (modifier === \"i\" || modifier === \"em\") {\n      this.attributes = (this.attributes || 0) | TextAttributes.ITALIC\n    } else if (modifier === \"u\") {\n      this.attributes = (this.attributes || 0) | TextAttributes.UNDERLINE\n    }\n  }\n}\n\nexport class BoldSpanRenderable extends TextModifierRenderable {\n  constructor(options: any) {\n    super(options, \"b\")\n  }\n}\n\nexport class ItalicSpanRenderable extends TextModifierRenderable {\n  constructor(options: any) {\n    super(options, \"i\")\n  }\n}\n\nexport class UnderlineSpanRenderable extends TextModifierRenderable {\n  constructor(options: any) {\n    super(options, \"u\")\n  }\n}\n\nexport class LineBreakRenderable extends SpanRenderable {\n  constructor(_ctx: RenderContext | null, options: TextNodeOptions) {\n    super(null, options)\n    this.add()\n  }\n\n  public override add(): number {\n    return super.add(\"\\n\")\n  }\n}\n\nexport interface LinkOptions extends TextNodeOptions {\n  href: string\n}\n\nexport class LinkRenderable extends SpanRenderable {\n  constructor(_ctx: RenderContext | null, options: LinkOptions) {\n    const linkOptions: TextNodeOptions = {\n      ...options,\n      link: { url: options.href },\n    }\n    super(null, linkOptions)\n  }\n}\n\nexport const baseComponents = {\n  box: BoxRenderable,\n  text: TextRenderable,\n  input: InputRenderable,\n  select: SelectRenderable,\n  textarea: TextareaRenderable,\n  ascii_font: ASCIIFontRenderable,\n  tab_select: TabSelectRenderable,\n  scrollbox: ScrollBoxRenderable,\n  code: CodeRenderable,\n  diff: DiffRenderable,\n  line_number: LineNumberRenderable,\n  markdown: MarkdownRenderable,\n\n  span: SpanRenderable,\n  strong: BoldSpanRenderable,\n  b: BoldSpanRenderable,\n  em: ItalicSpanRenderable,\n  i: ItalicSpanRenderable,\n  u: UnderlineSpanRenderable,\n  br: LineBreakRenderable,\n  a: LinkRenderable,\n}\n\ntype ComponentCatalogue = Record<string, RenderableConstructor>\n\nexport const componentCatalogue: ComponentCatalogue = { ...baseComponents }\n\n/**\n * Extend the component catalogue with new renderable components\n *\n * @example\n * ```tsx\n * // Extend with an object of components\n * extend({\n *   consoleButton: ConsoleButtonRenderable,\n *   customBox: CustomBoxRenderable\n * })\n * ```\n */\nexport function extend<T extends ComponentCatalogue>(objects: T): void {\n  Object.assign(componentCatalogue, objects)\n}\n\nexport function getComponentCatalogue(): ComponentCatalogue {\n  return componentCatalogue\n}\n\nexport type { ExtendedComponentProps, ExtendedIntrinsicElements, RenderableConstructor } from \"../types/elements.js\"\n"
  },
  {
    "path": "packages/solid/src/elements/slot.ts",
    "content": "import { BaseRenderable, isTextNodeRenderable, TextNodeRenderable, TextRenderable, Yoga } from \"@opentui/core\"\n\nclass SlotBaseRenderable extends BaseRenderable {\n  constructor(id: string) {\n    super({\n      id,\n    })\n  }\n\n  public add(obj: BaseRenderable | unknown, index?: number): number {\n    throw new Error(\"Can't add children on an Slot renderable\")\n  }\n\n  public getChildren(): BaseRenderable[] {\n    return []\n  }\n\n  public remove(id: string): void {}\n\n  public insertBefore(obj: BaseRenderable | unknown, anchor: BaseRenderable | unknown): void {\n    throw new Error(\"Can't add children on an Slot renderable\")\n  }\n\n  public getRenderable(id: string): BaseRenderable | undefined {\n    return undefined\n  }\n\n  public getChildrenCount(): number {\n    return 0\n  }\n\n  public requestRender(): void {}\n\n  public findDescendantById(id: string): BaseRenderable | undefined {\n    return undefined\n  }\n}\n\nexport class TextSlotRenderable extends TextNodeRenderable {\n  protected slotParent?: SlotRenderable\n  protected destroyed: boolean = false\n\n  constructor(id: string, parent?: SlotRenderable) {\n    super({ id: id })\n    this._visible = false\n    this.slotParent = parent\n  }\n\n  public override destroy(): void {\n    if (this.destroyed) {\n      return\n    }\n    this.destroyed = true\n\n    this.slotParent?.destroy()\n    super.destroy()\n  }\n}\n\nexport class LayoutSlotRenderable extends SlotBaseRenderable {\n  protected yogaNode: Yoga.Node\n  protected slotParent?: SlotRenderable\n  protected destroyed: boolean = false\n\n  constructor(id: string, parent?: SlotRenderable) {\n    super(id)\n\n    this._visible = false\n    this.slotParent = parent\n    this.yogaNode = Yoga.default.Node.create()\n    this.yogaNode.setDisplay(Yoga.Display.None)\n  }\n\n  public getLayoutNode(): Yoga.Node {\n    return this.yogaNode\n  }\n\n  public updateFromLayout() {}\n\n  public updateLayout() {}\n\n  public onRemove() {}\n\n  public override destroy(): void {\n    if (this.destroyed) {\n      return\n    }\n    this.destroyed = true\n\n    super.destroy()\n    this.slotParent?.destroy()\n  }\n}\n\nexport class SlotRenderable extends SlotBaseRenderable {\n  layoutNode?: LayoutSlotRenderable\n  textNode?: TextSlotRenderable\n  protected destroyed: boolean = false\n\n  constructor(id: string) {\n    super(id)\n\n    this._visible = false\n  }\n\n  getSlotChild(parent: BaseRenderable) {\n    if (isTextNodeRenderable(parent) || parent instanceof TextRenderable) {\n      if (!this.textNode) {\n        this.textNode = new TextSlotRenderable(`slot-text-${this.id}`, this)\n      }\n      return this.textNode\n    }\n\n    if (!this.layoutNode) {\n      this.layoutNode = new LayoutSlotRenderable(`slot-layout-${this.id}`, this)\n    }\n    return this.layoutNode\n  }\n\n  public override destroy(): void {\n    if (this.destroyed) {\n      return\n    }\n    this.destroyed = true\n\n    if (this.layoutNode) {\n      this.layoutNode.destroy()\n    }\n    if (this.textNode) {\n      this.textNode.destroy()\n    }\n  }\n}\n"
  },
  {
    "path": "packages/solid/src/plugins/slot.tsx",
    "content": "import {\n  createSlotRegistry,\n  SlotRegistry,\n  type CliRenderer,\n  type Plugin,\n  type PluginContext,\n  type PluginErrorEvent,\n  type ResolvedSlotRenderer,\n  type SlotMode,\n  type SlotRegistryOptions,\n} from \"@opentui/core\"\nimport { children, createMemo, createSignal, ErrorBoundary, For, onCleanup, splitProps, type JSX } from \"solid-js\"\n\nexport type { SlotMode }\ntype SlotMap = Record<string, object>\n\nexport type SolidPlugin<TSlots extends SlotMap, TContext extends PluginContext = PluginContext> = Plugin<\n  JSX.Element,\n  TSlots,\n  TContext\n>\n\nexport type SolidSlotProps<\n  TSlots extends SlotMap,\n  K extends keyof TSlots,\n  TContext extends PluginContext = PluginContext,\n> = {\n  registry: SlotRegistry<JSX.Element, TSlots, TContext>\n  name: K\n  mode?: SlotMode\n  children?: JSX.Element\n  pluginFailurePlaceholder?: (failure: PluginErrorEvent) => JSX.Element\n} & TSlots[K]\n\nexport type SolidBoundSlotProps<TSlots extends SlotMap, K extends keyof TSlots> = {\n  name: K\n  mode?: SlotMode\n  children?: JSX.Element\n} & TSlots[K]\n\nexport type SolidRegistrySlotComponent<TSlots extends SlotMap, TContext extends PluginContext = PluginContext> = <\n  K extends keyof TSlots,\n>(\n  props: SolidSlotProps<TSlots, K, TContext>,\n) => JSX.Element\n\nexport type SolidSlotComponent<TSlots extends SlotMap> = <K extends keyof TSlots>(\n  props: SolidBoundSlotProps<TSlots, K>,\n) => JSX.Element\n\nexport interface SolidSlotOptions {\n  pluginFailurePlaceholder?: (failure: PluginErrorEvent) => JSX.Element\n}\n\nexport function createSolidSlotRegistry<TSlots extends SlotMap, TContext extends PluginContext = PluginContext>(\n  renderer: CliRenderer,\n  context: TContext,\n  options: SlotRegistryOptions = {},\n): SlotRegistry<JSX.Element, TSlots, TContext> {\n  // Solid slots intentionally use one registry key per renderer instance.\n  // Use createSlotRegistry from @opentui/core with a custom key for independent registries.\n  return createSlotRegistry<JSX.Element, TSlots, TContext>(renderer, \"solid:slot-registry\", context, options)\n}\n\nexport function createSlot<TSlots extends SlotMap, TContext extends PluginContext = PluginContext>(\n  registry: SlotRegistry<JSX.Element, TSlots, TContext>,\n  options: SolidSlotOptions = {},\n): SolidSlotComponent<TSlots> {\n  return function BoundSlot<K extends keyof TSlots>(props: SolidBoundSlotProps<TSlots, K>): JSX.Element {\n    return (\n      <Slot<TSlots, TContext, K>\n        {...(props as SolidBoundSlotProps<TSlots, K>)}\n        registry={registry}\n        pluginFailurePlaceholder={options.pluginFailurePlaceholder}\n      />\n    )\n  }\n}\n\nexport function Slot<\n  TSlots extends SlotMap,\n  TContext extends PluginContext = PluginContext,\n  K extends keyof TSlots = keyof TSlots,\n>(props: SolidSlotProps<TSlots, K, TContext>): JSX.Element {\n  const [local, slotProps] = splitProps(props as SolidSlotProps<TSlots, K, TContext>, [\n    \"registry\",\n    \"name\",\n    \"mode\",\n    \"children\",\n    \"pluginFailurePlaceholder\",\n  ])\n  const registry = () => local.registry\n  const pluginFailurePlaceholder = () => local.pluginFailurePlaceholder\n  const [version, setVersion] = createSignal(0)\n\n  const unsubscribe = registry().subscribe(() => {\n    setVersion((current) => current + 1)\n  })\n  onCleanup(unsubscribe)\n\n  const entries = createMemo<Array<ResolvedSlotRenderer<JSX.Element, TSlots[K], TContext>>>((previousEntries = []) => {\n    version()\n    const resolvedEntries = registry().resolveEntries(local.name as K) as Array<\n      ResolvedSlotRenderer<JSX.Element, TSlots[K], TContext>\n    >\n    const previousById = new Map(previousEntries.map((entry) => [entry.id, entry]))\n\n    return resolvedEntries.map((entry) => {\n      const previousEntry = previousById.get(entry.id)\n      if (previousEntry && previousEntry.renderer === entry.renderer) {\n        return previousEntry\n      }\n\n      return entry\n    })\n  })\n\n  const entryIds = createMemo(() => entries().map((entry) => entry.id))\n  const entriesById = createMemo(() => new Map(entries().map((entry) => [entry.id, entry])))\n\n  const slotName = () => String(local.name)\n\n  const FallbackView = (): JSX.Element => {\n    const resolvedFallbackChildren = children(() => local.children)\n    return <>{resolvedFallbackChildren()}</>\n  }\n\n  const renderFallback = (): JSX.Element => {\n    return <FallbackView />\n  }\n\n  const resolveFallback = (fallbackValue?: (() => JSX.Element) | undefined): JSX.Element => fallbackValue?.() ?? null\n\n  const renderPluginFailurePlaceholder = (\n    failure: PluginErrorEvent,\n    fallbackValue?: (() => JSX.Element) | undefined,\n  ): JSX.Element => {\n    if (!pluginFailurePlaceholder()) {\n      return resolveFallback(fallbackValue)\n    }\n\n    try {\n      return pluginFailurePlaceholder()!(failure)\n    } catch (error) {\n      registry().reportPluginError({\n        pluginId: failure.pluginId,\n        slot: failure.slot ?? slotName(),\n        phase: \"error_placeholder\",\n        source: \"solid\",\n        error,\n      })\n\n      return resolveFallback(fallbackValue)\n    }\n  }\n\n  const renderEntry = (\n    entry: ResolvedSlotRenderer<JSX.Element, TSlots[K], TContext>,\n    fallbackOnError?: () => JSX.Element,\n  ): JSX.Element => {\n    let initialRender: JSX.Element\n\n    try {\n      initialRender = entry.renderer(registry().context, slotProps as TSlots[K])\n    } catch (error) {\n      const failure = registry().reportPluginError({\n        pluginId: entry.id,\n        slot: slotName(),\n        phase: \"render\",\n        source: \"solid\",\n        error,\n      })\n\n      return renderPluginFailurePlaceholder(failure, fallbackOnError)\n    }\n\n    const resolvedInitialRender = children(() => initialRender)\n    const hasInitialOutput = resolvedInitialRender\n      .toArray()\n      .some((node) => node !== null && node !== undefined && node !== false)\n\n    if (!hasInitialOutput) {\n      return resolveFallback(fallbackOnError)\n    }\n\n    return (\n      <ErrorBoundary\n        fallback={(error: unknown) => {\n          const failure = registry().reportPluginError({\n            pluginId: entry.id,\n            slot: slotName(),\n            phase: \"render\",\n            source: \"solid\",\n            error,\n          })\n\n          return renderPluginFailurePlaceholder(failure, fallbackOnError)\n        }}\n      >\n        {resolvedInitialRender()}\n      </ErrorBoundary>\n    )\n  }\n\n  const AppendEntry = (appendProps: { entryId: string }): JSX.Element => {\n    const entry = createMemo(() => entriesById().get(appendProps.entryId))\n\n    return (\n      <>\n        {(() => {\n          const resolvedEntry = entry()\n          if (!resolvedEntry) {\n            return null\n          }\n\n          return renderEntry(resolvedEntry)\n        })()}\n      </>\n    )\n  }\n\n  const appendView = (\n    <>\n      {renderFallback}\n      <For each={entryIds()}>{(entryId) => <AppendEntry entryId={entryId} />}</For>\n    </>\n  )\n\n  return (\n    <>\n      {(() => {\n        const resolvedEntries = entries()\n        const mode = local.mode ?? \"append\"\n\n        if (resolvedEntries.length === 0) {\n          return renderFallback()\n        }\n\n        if (mode === \"single_winner\") {\n          const winner = resolvedEntries[0]\n          if (!winner) {\n            return renderFallback()\n          }\n\n          return renderEntry(winner, renderFallback)\n        }\n\n        if (mode === \"replace\") {\n          const renderedEntries = resolvedEntries.map((entry) => renderEntry(entry))\n          const hasPluginOutput = renderedEntries.some(\n            (entry) => entry !== null && entry !== undefined && entry !== false,\n          )\n\n          if (!hasPluginOutput) {\n            return renderFallback()\n          }\n\n          return <>{renderedEntries}</>\n        }\n\n        return appendView\n      })()}\n    </>\n  )\n}\n"
  },
  {
    "path": "packages/solid/src/reconciler.ts",
    "content": "/* @refresh skip */\nimport {\n  BaseRenderable,\n  createTextAttributes,\n  InputRenderable,\n  InputRenderableEvents,\n  isTextNodeRenderable,\n  parseColor,\n  Renderable,\n  RootTextNodeRenderable,\n  ScrollBoxRenderable,\n  SelectRenderable,\n  SelectRenderableEvents,\n  TabSelectRenderable,\n  TabSelectRenderableEvents,\n  TextNodeRenderable,\n  TextRenderable,\n  type TextNodeOptions,\n} from \"@opentui/core\"\nimport { decodeHTML } from \"entities\"\nimport { useContext } from \"solid-js\"\nimport { createRenderer } from \"./renderer/index.js\"\nimport { getComponentCatalogue, RendererContext, SlotRenderable } from \"./elements/index.js\"\nimport { getNextId } from \"./utils/id-counter.js\"\nimport { log } from \"./utils/log.js\"\n\nclass TextNode extends TextNodeRenderable {\n  public static override fromString(text: string, options: Partial<TextNodeOptions> = {}): TextNode {\n    const node = new TextNode(options)\n    node.add(text)\n    return node\n  }\n}\n\nexport type DomNode = BaseRenderable\n\n/**\n * Gets the id of a node, or content if it's a text chunk.\n * Intended for use in logging.\n * @param node The node to get the id of.\n * @returns Log-friendly id of the node.\n */\nconst logId = (node?: DomNode): string | undefined => {\n  if (!node) return undefined\n  return node.id\n}\n\nconst getNodeChildren = (node: DomNode) => {\n  let children\n  if (node instanceof TextRenderable) {\n    children = node.getTextChildren()\n  } else {\n    children = node.getChildren()\n  }\n  return children\n}\n\nfunction _insertNode(parent: DomNode, node: DomNode, anchor?: DomNode): void {\n  log(\n    \"Inserting node:\",\n    logId(node),\n    \"into parent:\",\n    logId(parent),\n    \"with anchor:\",\n    logId(anchor),\n    node instanceof TextNode,\n  )\n\n  if (node instanceof SlotRenderable) {\n    node.parent = parent\n    node = node.getSlotChild(parent)\n  }\n\n  if (anchor && anchor instanceof SlotRenderable) {\n    anchor = anchor.getSlotChild(parent)\n  }\n\n  if (isTextNodeRenderable(node)) {\n    if (!(parent instanceof TextRenderable) && !isTextNodeRenderable(parent)) {\n      throw new Error(\n        `Orphan text error: \"${node\n          .toChunks()\n          .map((c) => c.text)\n          .join(\"\")}\" must have a <text> as a parent: ${parent.id} above ${node.id}`,\n      )\n    }\n  }\n\n  // Renderable nodes\n  if (!(parent instanceof BaseRenderable)) {\n    console.error(\"[INSERT]\", \"Tried to mount a non base renderable\")\n    // Can't be a noop, have to panic\n    throw new Error(\"Tried to mount a non base renderable\")\n  }\n\n  if (!anchor) {\n    parent.add(node)\n    return\n  }\n\n  const children = getNodeChildren(parent)\n\n  const anchorIndex = children.findIndex((el) => el.id === anchor.id)\n  if (anchorIndex === -1) {\n    log(\"[INSERT]\", \"Could not find anchor\", logId(parent), logId(anchor), \"[children]\", ...children.map((c) => c.id))\n  }\n\n  parent.add(node, anchorIndex)\n}\n\nfunction _removeNode(parent: DomNode, node: DomNode): void {\n  log(\"Removing node:\", logId(node), \"from parent:\", logId(parent))\n\n  if (node instanceof SlotRenderable) {\n    node.parent = null\n    node = node.getSlotChild(parent)\n  }\n\n  parent.remove(node.id)\n\n  process.nextTick(() => {\n    if (node instanceof BaseRenderable && !node.parent) {\n      node.destroyRecursively()\n      return\n    }\n  })\n}\n\nfunction _createTextNode(value: string | number): TextNode {\n  log(\"Creating text node:\", value)\n\n  const id = getNextId(\"text-node\")\n\n  if (typeof value === \"number\") {\n    value = value.toString()\n  }\n\n  return TextNode.fromString(decodeHTML(value), { id })\n}\n\nexport function createSlotNode(): SlotRenderable {\n  const id = getNextId(\"slot-node\")\n  log(\"Creating slot node\", id)\n  return new SlotRenderable(id)\n}\n\nfunction _getParentNode(childNode: DomNode): DomNode | undefined {\n  log(\"Getting parent of node:\", logId(childNode))\n\n  let parent = childNode.parent ?? undefined\n  if (parent instanceof RootTextNodeRenderable) {\n    parent = parent.textParent ?? undefined\n  }\n  // ScrollBox delegates add/remove to its internal `content` wrapper\n  // (scrollbox → wrapper → viewport → content), so children report\n  // `content` as their parent. Return the ScrollBox so the identity\n  // check in cleanChildren (getParentNode(el) === parent) succeeds.\n  const scrollBoxCandidate = parent?.parent?.parent?.parent\n  if (scrollBoxCandidate instanceof ScrollBoxRenderable && scrollBoxCandidate.content === parent) {\n    parent = scrollBoxCandidate\n  }\n  return parent\n}\n\nexport const {\n  render: _render,\n  effect,\n  memo,\n  createComponent,\n  createElement,\n  createTextNode,\n  insertNode,\n  insert,\n  spread,\n  setProp,\n  mergeProps,\n  use,\n} = createRenderer<DomNode>({\n  createElement(tagName: string): DomNode {\n    log(\"Creating element:\", tagName)\n    const id = getNextId(tagName)\n    const solidRenderer = useContext(RendererContext)\n    if (!solidRenderer) {\n      throw new Error(\"No renderer found\")\n    }\n    const elements = getComponentCatalogue()\n\n    if (!elements[tagName]) {\n      throw new Error(`[Reconciler] Unknown component type: ${tagName}`)\n    }\n\n    const element = new elements[tagName](solidRenderer, { id })\n    log(\"Element created with id:\", id)\n    return element\n  },\n\n  createTextNode: _createTextNode,\n\n  createSlotNode,\n\n  replaceText(textNode: TextNode, value: string): void {\n    log(\"Replacing text:\", value, \"in node:\", logId(textNode))\n\n    if (!(textNode instanceof TextNode)) return\n    textNode.replace(decodeHTML(value), 0)\n  },\n\n  setProperty(node: DomNode, name: string, value: any, prev: any): void {\n    if (name.startsWith(\"on:\")) {\n      const eventName = name.slice(3)\n      if (value) {\n        node.on(eventName, value)\n      }\n      if (prev) {\n        node.off(eventName, prev)\n      }\n\n      return\n    }\n\n    if (isTextNodeRenderable(node)) {\n      if (name === \"href\") {\n        node.link = { url: value }\n        return\n      }\n\n      if (name === \"style\") {\n        node.attributes |= createTextAttributes(value)\n        node.fg = value.fg ? parseColor(value.fg) : node.fg\n        node.bg = value.bg ? parseColor(value.bg) : node.bg\n        return\n      }\n\n      return\n    }\n\n    switch (name) {\n      case \"id\":\n        log(\"Id mapped\", node.id, \"=\", value)\n        node[name] = value\n        break\n      case \"focused\":\n        if (!(node instanceof Renderable)) return\n        if (value) {\n          node.focus()\n        } else {\n          node.blur()\n        }\n        break\n      case \"onChange\":\n        let event: string | undefined = undefined\n        if (node instanceof SelectRenderable) {\n          event = SelectRenderableEvents.SELECTION_CHANGED\n        } else if (node instanceof TabSelectRenderable) {\n          event = TabSelectRenderableEvents.SELECTION_CHANGED\n        } else if (node instanceof InputRenderable) {\n          event = InputRenderableEvents.CHANGE\n        }\n        if (!event) break\n\n        if (value) {\n          node.on(event, value)\n        }\n        if (prev) {\n          node.off(event, prev)\n        }\n        break\n      case \"onInput\":\n        if (node instanceof InputRenderable) {\n          if (value) {\n            node.on(InputRenderableEvents.INPUT, value)\n          }\n\n          if (prev) {\n            node.off(InputRenderableEvents.INPUT, prev)\n          }\n        }\n\n        break\n      case \"onSubmit\":\n        if (node instanceof InputRenderable) {\n          if (value) {\n            node.on(InputRenderableEvents.ENTER, value)\n          }\n\n          if (prev) {\n            node.off(InputRenderableEvents.ENTER, prev)\n          }\n        } else {\n          // @ts-expect-error todo validate if prop is actually settable\n          node[name] = value\n        }\n        break\n      case \"onSelect\":\n        if (node instanceof SelectRenderable) {\n          if (value) {\n            node.on(SelectRenderableEvents.ITEM_SELECTED, value)\n          }\n\n          if (prev) {\n            node.off(SelectRenderableEvents.ITEM_SELECTED, prev)\n          }\n        } else if (node instanceof TabSelectRenderable) {\n          if (value) {\n            node.on(TabSelectRenderableEvents.ITEM_SELECTED, value)\n          }\n\n          if (prev) {\n            node.off(TabSelectRenderableEvents.ITEM_SELECTED, prev)\n          }\n        }\n        break\n      case \"style\":\n        for (const prop in value) {\n          const propVal = value[prop]\n          if (prev !== undefined && propVal === prev[prop]) continue\n          // @ts-expect-error todo validate if prop is actually settable\n          node[prop] = propVal\n        }\n        break\n      case \"text\":\n      case \"content\": {\n        const textValue = typeof value === \"string\" ? value : Array.isArray(value) ? value.join(\"\") : `${value}`\n        // @ts-expect-error todo validate if prop is actually settable\n        node[name] = decodeHTML(textValue)\n        break\n      }\n\n      default:\n        // @ts-expect-error todo validate if prop is actually settable\n        node[name] = value\n    }\n  },\n\n  isTextNode(node: DomNode): boolean {\n    return node instanceof TextNode\n  },\n\n  insertNode: _insertNode,\n\n  removeNode: _removeNode,\n\n  getParentNode: _getParentNode,\n\n  getFirstChild(node: DomNode): DomNode | undefined {\n    log(\"Getting first child of node:\", logId(node))\n\n    const firstChild = getNodeChildren(node)[0]\n\n    if (!firstChild) {\n      log(\"No first child found for node:\", logId(node))\n      return undefined\n    }\n\n    log(\"First child found:\", logId(firstChild), \"for node:\", logId(node))\n    return firstChild\n  },\n\n  getNextSibling(node: DomNode): DomNode | undefined {\n    log(\"Getting next sibling of node:\", logId(node))\n\n    const parent = _getParentNode(node)\n    if (!parent) {\n      log(\"No parent found for node:\", logId(node))\n      return undefined\n    }\n    const siblings = getNodeChildren(parent)\n\n    const index = siblings.indexOf(node)\n\n    if (index === -1 || index === siblings.length - 1) {\n      log(\"No next sibling found for node:\", logId(node))\n      return undefined\n    }\n\n    const nextSibling = siblings[index + 1]\n\n    if (!nextSibling) {\n      log(\"Next sibling is null for node:\", logId(node))\n      return undefined\n    }\n\n    log(\"Next sibling found:\", logId(nextSibling), \"for node:\", logId(node))\n    return nextSibling\n  },\n})\n"
  },
  {
    "path": "packages/solid/src/renderer/index.ts",
    "content": "import { createRenderer as createRendererDX } from \"./universal.js\"\nimport type { RendererOptions, Renderer } from \"./universal.js\"\nimport { mergeProps } from \"solid-js\"\n\nexport type { RendererOptions, Renderer } from \"./universal.js\"\n\nexport function createRenderer<NodeType>(options: RendererOptions<NodeType>): Renderer<NodeType> {\n  const renderer = createRendererDX(options)\n  renderer.mergeProps = mergeProps\n  return renderer\n}\n"
  },
  {
    "path": "packages/solid/src/renderer/universal.d.ts",
    "content": "export interface RendererOptions<NodeType> {\n  createElement(tag: string): NodeType\n  createTextNode(value: string): NodeType\n  createSlotNode(): NodeType\n  replaceText(textNode: NodeType, value: string): void\n  isTextNode(node: NodeType): boolean\n  setProperty<T>(node: NodeType, name: string, value: T, prev?: T): void\n  insertNode(parent: NodeType, node: NodeType, anchor?: NodeType): void\n  removeNode(parent: NodeType, node: NodeType): void\n  getParentNode(node: NodeType): NodeType | undefined\n  getFirstChild(node: NodeType): NodeType | undefined\n  getNextSibling(node: NodeType): NodeType | undefined\n}\n\nexport interface Renderer<NodeType> {\n  render(code: () => NodeType, node: NodeType): () => void\n  effect<T>(fn: (prev?: T) => T, init?: T): void\n  memo<T>(fn: () => T, equal: boolean): () => T\n  createComponent<T>(Comp: (props: T) => NodeType, props: T): NodeType\n  createElement(tag: string): NodeType\n  createTextNode(value: string): NodeType\n  insertNode(parent: NodeType, node: NodeType, anchor?: NodeType): void\n  insert<T>(parent: any, accessor: (() => T) | T, marker?: any | null, initial?: any): NodeType\n  spread<T>(node: any, accessor: (() => T) | T, skipChildren?: boolean): void\n  setProp<T>(node: NodeType, name: string, value: T, prev?: T): T\n  mergeProps(...sources: unknown[]): unknown\n  use<A, T>(fn: (element: NodeType, arg: A) => T, element: NodeType, arg: A): T\n}\n\nexport function createRenderer<NodeType>(options: RendererOptions<NodeType>): Renderer<NodeType>\n"
  },
  {
    "path": "packages/solid/src/renderer/universal.js",
    "content": "import { createRoot, createRenderEffect, createMemo, createComponent, untrack, mergeProps } from \"solid-js\"\n\nconst memo = (fn) => createMemo(() => fn())\n\nexport function createRenderer({\n  createElement,\n  createTextNode,\n  createSlotNode,\n  isTextNode,\n  replaceText,\n  insertNode,\n  removeNode,\n  setProperty,\n  getParentNode,\n  getFirstChild,\n  getNextSibling,\n}) {\n  function insert(parent, accessor, marker, initial) {\n    if (marker !== undefined && !initial) initial = []\n    if (typeof accessor !== \"function\") return insertExpression(parent, accessor, initial, marker)\n    createRenderEffect((current) => insertExpression(parent, accessor(), current, marker), initial)\n  }\n  function insertExpression(parent, value, current, marker, unwrapArray) {\n    while (typeof current === \"function\") current = current()\n    if (value === current) return current\n    const t = typeof value,\n      multi = marker !== undefined\n    if (t === \"string\" || t === \"number\") {\n      if (t === \"number\") value = value.toString()\n      if (multi) {\n        let node = current[0]\n        if (node && isTextNode(node)) {\n          replaceText(node, value)\n        } else node = createTextNode(value)\n        current = cleanChildren(parent, current, marker, node)\n      } else {\n        if (current !== \"\" && typeof current === \"string\") {\n          replaceText(getFirstChild(parent), (current = value))\n        } else {\n          cleanChildren(parent, current, marker, createTextNode(value))\n          current = value\n        }\n      }\n    } else if (value == null || t === \"boolean\") {\n      current = cleanChildren(parent, current, marker)\n    } else if (t === \"function\") {\n      createRenderEffect(() => {\n        let v = value()\n        while (typeof v === \"function\") v = v()\n        current = insertExpression(parent, v, current, marker)\n      })\n      return () => current\n    } else if (Array.isArray(value)) {\n      const array = []\n      if (normalizeIncomingArray(array, value, unwrapArray)) {\n        createRenderEffect(() => (current = insertExpression(parent, array, current, marker, true)))\n        return () => current\n      }\n      if (array.length === 0) {\n        const replacement = cleanChildren(parent, current, marker)\n        if (multi) return (current = replacement)\n      } else {\n        if (Array.isArray(current)) {\n          if (current.length === 0) {\n            appendNodes(parent, array, marker)\n          } else reconcileArrays(parent, current, array)\n        } else if (current == null || current === \"\") {\n          appendNodes(parent, array)\n        } else {\n          reconcileArrays(parent, (multi && current) || [getFirstChild(parent)], array)\n        }\n      }\n      current = array\n    } else {\n      if (Array.isArray(current)) {\n        if (multi) return (current = cleanChildren(parent, current, marker, value))\n        cleanChildren(parent, current, null, value)\n      } else if (current == null || current === \"\" || !getFirstChild(parent)) {\n        insertNode(parent, value)\n      } else replaceNode(parent, value, getFirstChild(parent))\n      current = value\n    }\n    return current\n  }\n  function normalizeIncomingArray(normalized, array, unwrap) {\n    let dynamic = false\n    for (let i = 0, len = array.length; i < len; i++) {\n      let item = array[i],\n        t\n      if (item == null || item === true || item === false);\n      else if (Array.isArray(item)) {\n        dynamic = normalizeIncomingArray(normalized, item) || dynamic\n      } else if ((t = typeof item) === \"string\" || t === \"number\") {\n        normalized.push(createTextNode(item))\n      } else if (t === \"function\") {\n        if (unwrap) {\n          while (typeof item === \"function\") item = item()\n          dynamic = normalizeIncomingArray(normalized, Array.isArray(item) ? item : [item]) || dynamic\n        } else {\n          normalized.push(item)\n          dynamic = true\n        }\n      } else normalized.push(item)\n    }\n    return dynamic\n  }\n  function reconcileArrays(parentNode, a, b) {\n    let bLength = b.length,\n      aEnd = a.length,\n      bEnd = bLength,\n      aStart = 0,\n      bStart = 0,\n      after = getNextSibling(a[aEnd - 1]),\n      map = null\n    while (aStart < aEnd || bStart < bEnd) {\n      if (a[aStart] === b[bStart]) {\n        aStart++\n        bStart++\n        continue\n      }\n      while (a[aEnd - 1] === b[bEnd - 1]) {\n        aEnd--\n        bEnd--\n      }\n      if (aEnd === aStart) {\n        const node = bEnd < bLength ? (bStart ? getNextSibling(b[bStart - 1]) : b[bEnd - bStart]) : after\n        while (bStart < bEnd) insertNode(parentNode, b[bStart++], node)\n      } else if (bEnd === bStart) {\n        while (aStart < aEnd) {\n          if (!map || !map.has(a[aStart])) removeNode(parentNode, a[aStart])\n          aStart++\n        }\n      } else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {\n        const node = getNextSibling(a[--aEnd])\n        insertNode(parentNode, b[bStart++], getNextSibling(a[aStart++]))\n        insertNode(parentNode, b[--bEnd], node)\n        a[aEnd] = b[bEnd]\n      } else {\n        if (!map) {\n          map = new Map()\n          let i = bStart\n          while (i < bEnd) map.set(b[i], i++)\n        }\n        const index = map.get(a[aStart])\n        if (index != null) {\n          if (bStart < index && index < bEnd) {\n            let i = aStart,\n              sequence = 1,\n              t\n            while (++i < aEnd && i < bEnd) {\n              if ((t = map.get(a[i])) == null || t !== index + sequence) break\n              sequence++\n            }\n            if (sequence > index - bStart) {\n              const node = a[aStart]\n              while (bStart < index) insertNode(parentNode, b[bStart++], node)\n            } else replaceNode(parentNode, b[bStart++], a[aStart++])\n          } else aStart++\n        } else removeNode(parentNode, a[aStart++])\n      }\n    }\n  }\n  function cleanChildren(parent, current, marker, replacement) {\n    if (marker === undefined) {\n      let removed\n      while ((removed = getFirstChild(parent))) removeNode(parent, removed)\n      replacement && insertNode(parent, replacement)\n      return replacement ?? \"\"\n    }\n    const node = replacement || createSlotNode()\n    if (current.length) {\n      let inserted = false\n      for (let i = current.length - 1; i >= 0; i--) {\n        const el = current[i]\n        if (node !== el) {\n          const isParent = getParentNode(el) === parent\n          if (!inserted && !i) isParent ? replaceNode(parent, node, el) : insertNode(parent, node, marker)\n          else isParent && removeNode(parent, el)\n        } else inserted = true\n      }\n    } else insertNode(parent, node, marker)\n    return [node]\n  }\n  function appendNodes(parent, array, marker) {\n    for (let i = 0, len = array.length; i < len; i++) insertNode(parent, array[i], marker)\n  }\n  function replaceNode(parent, newNode, oldNode) {\n    insertNode(parent, newNode, oldNode)\n    removeNode(parent, oldNode)\n  }\n  function spreadExpression(node, props, prevProps = {}, skipChildren) {\n    props || (props = {})\n    if (!skipChildren) {\n      createRenderEffect(() => (prevProps.children = insertExpression(node, props.children, prevProps.children)))\n    }\n    createRenderEffect(() => props.ref && props.ref(node))\n    createRenderEffect(() => {\n      for (const prop in props) {\n        if (prop === \"children\" || prop === \"ref\") continue\n        const value = props[prop]\n        if (value === prevProps[prop]) continue\n        setProperty(node, prop, value, prevProps[prop])\n        prevProps[prop] = value\n      }\n    })\n    return prevProps\n  }\n  return {\n    render(code, element) {\n      let disposer\n      createRoot((dispose) => {\n        disposer = dispose\n        insert(element, code())\n      })\n      return disposer\n    },\n    insert,\n    spread(node, accessor, skipChildren) {\n      if (typeof accessor === \"function\") {\n        createRenderEffect((current) => spreadExpression(node, accessor(), current, skipChildren))\n      } else spreadExpression(node, accessor, undefined, skipChildren)\n    },\n    createElement,\n    createTextNode,\n    insertNode,\n    setProp(node, name, value, prev) {\n      setProperty(node, name, value, prev)\n      return value\n    },\n    mergeProps,\n    effect: createRenderEffect,\n    memo,\n    createComponent,\n    use(fn, element, arg) {\n      return untrack(() => fn(element, arg))\n    },\n  }\n}\n"
  },
  {
    "path": "packages/solid/src/time-to-first-draw.tsx",
    "content": "import { TimeToFirstDrawRenderable } from \"@opentui/core\"\nimport { extend } from \"./elements\"\nimport type { ExtendedComponentProps } from \"./types/elements\"\n\ndeclare module \"@opentui/solid\" {\n  interface OpenTUIComponents {\n    time_to_first_draw: typeof TimeToFirstDrawRenderable\n  }\n}\n\nextend({ time_to_first_draw: TimeToFirstDrawRenderable })\n\nexport type TimeToFirstDrawProps = ExtendedComponentProps<typeof TimeToFirstDrawRenderable>\n\nexport const TimeToFirstDraw = (props: TimeToFirstDrawProps) => {\n  return <time_to_first_draw {...props} />\n}\n"
  },
  {
    "path": "packages/solid/src/types/elements.ts",
    "content": "import type {\n  ASCIIFontOptions,\n  ASCIIFontRenderable,\n  BaseRenderable,\n  BoxOptions,\n  BoxRenderable,\n  CodeOptions,\n  CodeRenderable,\n  InputRenderable,\n  InputRenderableOptions,\n  KeyEvent,\n  MarkdownOptions,\n  MarkdownRenderable,\n  RenderableOptions,\n  RenderContext,\n  ScrollBoxOptions,\n  ScrollBoxRenderable,\n  SelectOption,\n  SelectRenderable,\n  SelectRenderableOptions,\n  TabSelectOption,\n  TabSelectRenderable,\n  TabSelectRenderableOptions,\n  TextareaOptions,\n  TextareaRenderable,\n  TextNodeRenderable,\n  TextOptions,\n  TextRenderable,\n} from \"@opentui/core\"\nimport type { Ref } from \"solid-js\"\nimport type { JSX } from \"../../jsx-runtime\"\n\n// ============================================================================\n// Core Type System\n// ============================================================================\n\n/** Properties that should not be included in the style prop */\nexport type NonStyledProps =\n  | \"id\"\n  | \"buffered\"\n  | \"live\"\n  | \"enableLayout\"\n  | \"selectable\"\n  | \"renderAfter\"\n  | \"renderBefore\"\n  | `on${string}`\n\n/** Solid-specific props for all components */\nexport type ElementProps<TRenderable = unknown> = {\n  ref?: Ref<TRenderable>\n}\n\n/** Base type for any renderable constructor */\nexport type RenderableConstructor<TRenderable extends BaseRenderable = BaseRenderable> = new (\n  ctx: RenderContext,\n  options: any,\n) => TRenderable\n\n/** Extract the options type from a renderable constructor */\ntype ExtractRenderableOptions<TConstructor> = TConstructor extends new (\n  ctx: RenderContext,\n  options: infer TOptions,\n) => any\n  ? TOptions\n  : never\n\n/** Extract the renderable type from a constructor */\ntype ExtractRenderable<TConstructor> = TConstructor extends new (ctx: RenderContext, options: any) => infer TRenderable\n  ? TRenderable\n  : never\n\n/** Determine which properties should be excluded from styling for different renderable types */\nexport type GetNonStyledProperties<TConstructor> =\n  TConstructor extends RenderableConstructor<TextRenderable>\n    ? NonStyledProps | \"content\"\n    : TConstructor extends RenderableConstructor<BoxRenderable>\n      ? NonStyledProps | \"title\"\n      : TConstructor extends RenderableConstructor<ASCIIFontRenderable>\n        ? NonStyledProps | \"text\" | \"selectable\"\n        : TConstructor extends RenderableConstructor<InputRenderable>\n          ? NonStyledProps | \"placeholder\" | \"value\"\n          : TConstructor extends RenderableConstructor<CodeRenderable>\n            ? NonStyledProps | \"content\" | \"filetype\" | \"syntaxStyle\" | \"treeSitterClient\"\n            : TConstructor extends RenderableConstructor<MarkdownRenderable>\n              ? NonStyledProps | \"content\" | \"syntaxStyle\" | \"treeSitterClient\" | \"conceal\" | \"renderNode\"\n              : NonStyledProps\n\n// ============================================================================\n// Component Props System\n// ============================================================================\n\n/** Base props for container components that accept children */\ntype ContainerProps<TOptions> = TOptions & { children?: JSX.Element }\n\n/** Smart component props that automatically determine excluded properties */\ntype ComponentProps<TOptions extends RenderableOptions<TRenderable>, TRenderable extends BaseRenderable> = TOptions & {\n  style?: Partial<Omit<TOptions, GetNonStyledProperties<RenderableConstructor<TRenderable>>>>\n} & ElementProps<TRenderable>\n\n/** Valid text content types for Text component children */\ntype TextChildren = string | number | boolean | null | undefined | JSX.Element\n\n// ============================================================================\n// Built-in Component Props\n// ============================================================================\n\nexport type TextProps = ComponentProps<TextOptions, TextRenderable> & {\n  children?: TextChildren | Array<TextChildren>\n}\n\nexport type SpanProps = ComponentProps<{}, TextNodeRenderable> & {\n  children?: TextChildren | Array<TextChildren>\n}\n\nexport type LinkProps = SpanProps & {\n  href: string\n}\n\nexport type BoxProps = ComponentProps<ContainerProps<BoxOptions>, BoxRenderable> & {\n  focused?: boolean\n}\n\nexport type InputProps = ComponentProps<InputRenderableOptions, InputRenderable> & {\n  focused?: boolean\n  onInput?: (value: string) => void\n  onChange?: (value: string) => void\n  onSubmit?: (value: string) => void\n}\n\nexport type TextareaProps = ComponentProps<TextareaOptions, TextareaRenderable> & {\n  focused?: boolean\n  onSubmit?: () => void\n  onContentChange?: (value: string) => void\n  onCursorChange?: (value: { line: number; visualColumn: number }) => void\n  onKeyDown?: (event: KeyEvent) => void\n  onKeyPress?: (event: KeyEvent) => void\n}\n\nexport type SelectProps = ComponentProps<SelectRenderableOptions, SelectRenderable> & {\n  focused?: boolean\n  onChange?: (index: number, option: SelectOption | null) => void\n  onSelect?: (index: number, option: SelectOption | null) => void\n}\n\nexport type AsciiFontProps = ComponentProps<ASCIIFontOptions, ASCIIFontRenderable>\n\nexport type TabSelectProps = ComponentProps<TabSelectRenderableOptions, TabSelectRenderable> & {\n  focused?: boolean\n  onChange?: (index: number, option: TabSelectOption | null) => void\n  onSelect?: (index: number, option: TabSelectOption | null) => void\n}\n\nexport type ScrollBoxProps = ComponentProps<ContainerProps<ScrollBoxOptions>, ScrollBoxRenderable> & {\n  focused?: boolean\n  stickyScroll?: boolean\n  stickyStart?: \"bottom\" | \"top\" | \"left\" | \"right\"\n}\n\nexport type CodeProps = ComponentProps<CodeOptions, CodeRenderable>\n\nexport type MarkdownProps = ComponentProps<MarkdownOptions, MarkdownRenderable>\n\n// ============================================================================\n// Extended/Dynamic Component System\n// ============================================================================\n\n/** Convert renderable constructor to component props with proper style exclusions */\nexport type ExtendedComponentProps<\n  TConstructor extends RenderableConstructor,\n  TOptions = ExtractRenderableOptions<TConstructor>,\n> = TOptions & {\n  children?: JSX.Element\n  style?: Partial<Omit<TOptions, GetNonStyledProperties<TConstructor>>>\n} & ElementProps<ExtractRenderable<TConstructor>>\n\n/** Helper type to create JSX element properties from a component catalogue */\nexport type ExtendedIntrinsicElements<TComponentCatalogue extends Record<string, RenderableConstructor>> = {\n  [TComponentName in keyof TComponentCatalogue]: ExtendedComponentProps<TComponentCatalogue[TComponentName]>\n}\n\n/**\n * Global augmentation interface for extended components\n * This will be augmented by user code using module augmentation\n */\nexport interface OpenTUIComponents {\n  [componentName: string]: RenderableConstructor\n}\n\n// Note: JSX.IntrinsicElements extension is handled in jsx-namespace.d.ts\n"
  },
  {
    "path": "packages/solid/src/utils/id-counter.ts",
    "content": "const idCounter = new Map<string, number>()\n\nexport function getNextId(elementType: string): string {\n  if (!idCounter.has(elementType)) {\n    idCounter.set(elementType, 0)\n  }\n\n  const value = idCounter.get(elementType)! + 1\n  idCounter.set(elementType, value)\n  return `${elementType}-${value}`\n}\n"
  },
  {
    "path": "packages/solid/src/utils/log.ts",
    "content": "export const log = (...args: any[]) => {\n  if (process.env.DEBUG) {\n    console.log(\"[Reconciler]\", ...args)\n  }\n}\n"
  },
  {
    "path": "packages/solid/tests/__snapshots__/control-flow.test.tsx.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`SolidJS Renderer - Control Flow Components Combined Control Flow should be able to anchor to slot nodes 1`] = `\n\"┌─A─────────────────────┐\n└───────────────────────┘\n┌─B─────────────────────┐\n└───────────────────────┘\n                         \n                         \n                         \n                         \n                         \n                         \n\"\n`;\n"
  },
  {
    "path": "packages/solid/tests/__snapshots__/dynamic-collections.test.tsx.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`SolidJS Renderer - Dynamic Collections Edge Cases should handle rapid collection updates 1`] = `\n\"First     \nSecond    \nFourth    \n\"\n`;\n\nexports[`SolidJS Renderer - Dynamic Collections Collection Transformations should handle sorting collections 1`] = `\n\"Number: 1 \nNumber: 1 \nNumber: 3 \nNumber: 4 \nNumber: 5 \n\"\n`;\n\nexports[`SolidJS Renderer - Dynamic Collections Collection Transformations should handle sorting collections 2`] = `\n\"Number: 5 \nNumber: 4 \nNumber: 3 \nNumber: 1 \nNumber: 1 \n\"\n`;\n\nexports[`SolidJS Renderer - Dynamic Collections Collection Transformations should handle filtering collections 1`] = `\n\"Apple (fruit)       \nBanana (fruit)      \n                    \n                    \n                    \n\"\n`;\n"
  },
  {
    "path": "packages/solid/tests/__snapshots__/dynamic-portal.test.tsx.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`SolidJS Renderer - Dynamic and Portal Components <Dynamic> Component should pass props correctly to dynamic components 1`] = `\n\"Updated text        \n                    \n                    \n\"\n`;\n\nexports[`SolidJS Renderer - Dynamic and Portal Components <Dynamic> + <Portal> Integration should handle Portal with Dynamic mount point 1`] = `\n\"Custom target                 \nDynamic mount content         \nStatic content                \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n"
  },
  {
    "path": "packages/solid/tests/__snapshots__/layout.test.tsx.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`SolidJS Renderer Integration Tests Basic Text Rendering should render simple text correctly 1`] = `\n\"Hello World         \n                    \n                    \n                    \n                    \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Basic Text Rendering should render multiline text correctly 1`] = `\n\"Line 1         \nLine 2         \nLine 3         \n               \n               \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Basic Text Rendering should render text with dynamic content 1`] = `\n\"Counter: 42         \n                    \n                    \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Box Layout Rendering should render basic box layout correctly 1`] = `\n\"┌──────────────────┐     \n│Inside Box        │     \n│                  │     \n│                  │     \n└──────────────────┘     \n                         \n                         \n                         \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Box Layout Rendering should render nested boxes correctly 1`] = `\n\"┌─Parent Box─────────────────┐     \n│                            │     \n│                            │     \n│  ┌────────┐                │     \n│  │Nested  │                │     \n│  └────────┘                │     \n│               Sibling      │     \n│                            │     \n│                            │     \n└────────────────────────────┘     \n                                   \n                                   \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Box Layout Rendering should render absolute positioned boxes 1`] = `\n\"┌────────┐               \n│Box 1   │               \n└────────┘  ┌────────┐   \n            │Box 2   │   \n            └────────┘   \n                         \n                         \n                         \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Reactive Updates should handle reactive state changes 1`] = `\n\"Counter: 0     \n               \n               \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Reactive Updates should handle reactive state changes 2`] = `\n\"Counter: 5     \n               \n               \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Reactive Updates should handle conditional rendering 1`] = `\n\"Always visible - Conditional t\n                              \n                              \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Reactive Updates should handle conditional rendering 2`] = `\n\"Always visible                \n                              \n                              \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Complex Layouts should render complex nested layout correctly 1`] = `\n\"┌─Complex Layout───────────────────────┐     \n│  ┌─────────────┐                     │     \n│  │Header Sectio│                     │     \n│  │Menu Item 1  │                     │     \n│  │Menu Item 2  │                     │     \n│  └─────────────┘                     │     \n│                  ┌────────────────┐  │     \n│                  │Content Area    │  │     \n│                  │Some content her│  │     \n│                  │More content    │  │     \n│                  │Footer text     │  │     \n│                  │                │  │     \n│                  │                │  │     \n│                  └────────────────┘  │     \n│  Status: Ready                       │     \n└──────────────────────────────────────┘     \n                                             \n                                             \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Complex Layouts should render text with mixed styling and layout 1`] = `\n\"┌─────────────────────────────────┐     \n│ERROR: Something went wrong      │     \n│WARNING: Check your settings     │     \n│SUCCESS: All systems operational │     \n│                                 │     \n│                                 │     \n│                                 │     \n└─────────────────────────────────┘     \n                                        \n                                        \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Complex Layouts should render scrollbox with sticky scroll and spacer 1`] = `\n\"┌─scroll area────────────────┐\n│                            │\n│┌─hi───────────────────────┐│\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n││                          ││\n│└──────────────────────────┘│\n│                            │\n│                            │\n└────────────────────────────┘\n┌─spacer─────────────────────┐\n│spacer                      │\n│                            │\n│                            │\n│                            │\n│                            │\n│                            │\n│                            │\n│                            │\n└────────────────────────────┘\n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Empty and Edge Cases should handle empty component 1`] = `\n\"          \n          \n          \n          \n          \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Empty and Edge Cases should handle component with no children 1`] = `\n\"               \n               \n               \n               \n               \n               \n               \n               \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Empty and Edge Cases should handle very small dimensions 1`] = `\n\"Hi   \n     \n     \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Box Layout Rendering should auto-enable border when borderColor is set 1`] = `\n\"┌──────────────────┐     \n│Colored Border    │     \n│                  │     \n│                  │     \n└──────────────────┘     \n                         \n                         \n                         \n\"\n`;\n\nexports[`SolidJS Renderer Integration Tests Box Layout Rendering should auto-enable border when focusedBorderColor is set 1`] = `\n\"┌──────────────────┐     \n│Focused Border    │     \n│                  │     \n│                  │     \n└──────────────────┘     \n                         \n                         \n                         \n\"\n`;\n\n\nexports[`SolidJS Renderer Integration Tests Box Layout Rendering should auto-enable border when borderStyle is set 1`] = `\n\"┌──────────────────┐     \n│With Border       │     \n│                  │     \n│                  │     \n└──────────────────┘     \n                         \n                         \n                         \n\"\n`;\n"
  },
  {
    "path": "packages/solid/tests/__snapshots__/line-number-debug.test.tsx.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`LineNumber Debug - Visual Border Tests DEBUG: line_number with borders to see actual height 1`] = `\n\"┌─Outer Container────────────────────────────────┐\n│┌─ScrollBox────────────────────────────────────┐│\n││ 1 function hello() {                         ││\n││ 2   console.log(\"Hello\");                    ││\n││ 3   return 42;                               ││\n││ 4 }                                          ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n││                                              ││\n│└──────────────────────────────────────────────┘│\n└────────────────────────────────────────────────┘\n\"\n`;\n\nexports[`LineNumber Debug - Visual Border Tests DEBUG: multiple line_number blocks with borders 1`] = `\n\"┌────────────────────────────────────────────────┐\n│┌──────────────────────────────────────────────┐│\n││┌─Block 1 Container──────────────────────────┐││\n│││ 1 const x = 1;                             │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n│││                                            │││\n││└────────────────────────────────────────────┘││\n│└──────────────────────────────────────────────┘│\n└────────────────────────────────────────────────┘\n\"\n`;\n\nexports[`LineNumber Debug - Visual Border Tests DEBUG: add markers between blocks to see spacing 1`] = `\n\"┌────────────────────────────────────────────────┐\n│▼1▼constTx▼=▼1;                                 │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n│                                                │\n└────────────────────────────────────────────────┘\n\"\n`;\n"
  },
  {
    "path": "packages/solid/tests/__snapshots__/line-number-scrollbox.test.tsx.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues single line_number with code in scrollbox should not have excessive height 1`] = `\n\" 1 function hello() {                   \n 2   console.log(\"Hello, World!\");      \n 3   return 42;                         \n 4 }                                    \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues WORKAROUND: flexShrink=0 fixes the height issue 1`] = `\n\" 1 function hello() {                   \n 2   console.log(\"Hello, World!\");      \n 3   return 42;                         \n 4 }                                    \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues REPRODUCES BUG: single line_number with code in scrollbox has excessive height 1`] = `\n\" 1 function hello() {                   \n 2   console.log(\"Hello, World!\");      \n 3   return 42;                         \n 4 }                                    \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues multiple line_number blocks should not overlap - realistic chat scenario 1`] = `\n\"  Wrote src/hello.ts                              \n   1 export function hello() {                    \n   2   return \"Hello, World!\";                    \n   3 }                                            \n  I've created the hello function.                \n  Wrote src/test.ts                               \n   1 import { hello } from \"./hello\";             \n   2                                              \n   3 test(\"hello returns greeting\", () => {       \n   4   expect(hello()).toBe(\"Hello, World!\");     \n   5 });                                          \n  I've also added a test file.                    \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues line_number height should match code content height, not double 1`] = `\n\"--- START MARKER ---                    \n 1 const x = 1;                         \n 2 const y = 2;                         \n--- END MARKER ---                      \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues scrollbox with box container around line_number - no excessive height 1`] = `\n\"                                                  \n  Message 1                                       \n  ┌────────────────────────────────────────────┐  \n  │ 1 function test() {                        │  \n  │ 2   return true;                           │  \n  │ 3 }                                        │  \n  └────────────────────────────────────────────┘  \n  Message 2                                       \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues multiple messages with mixed content - verify no overlapping 1`] = `\n\"                                                            \n  Let me create a file for you.                             \n  Wrote src/greet.ts                                        \n   1 export const greet = (name: string) => {               \n   2   return \\`Hello, \\${name}!\\`;                            \n   3 };                                                     \n  I've created the greet function.                          \n  Wrote src/index.ts                                        \n   1 import { greet } from \"./greet\";                       \n   2                                                        \n   3 console.log(greet(\"World\"));                           \n  Error [2:5]: Unused variable                              \n  And here's the main file.                                 \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues scroll behavior - content should remain visible after scroll 1`] = `\n\"Message 1                               \n 1 const a = 1;                         \nMessage 2                               \n 1 const b = 2;                         \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues scroll behavior - content should remain visible after scroll 2`] = `\n\"Message 8                               \n 1 const var8 = 8;                      \nMessage 9                               \n 1 const var9 = 9;                      \nMessage 10                              \n 1 const var10 = 10;                    \nMessage 11                              \n 1 const var11 = 11;                    \nMessage 12                              \n 1 const var12 = 12;                    \nMessage 13                              \n 1 const var13 = 13;                    \nMessage 14                              \n 1 const var14 = 14;                    \nMessage 15                              \n 1 const var15 = 15;                    \nMessage 16                              \n 1 const var16 = 16;                    \nMessage 17                              \n 1 const var17 = 17;                    \nMessage 18                              \n 1 const var18 = 18;                    \nMessage 19                              \n 1 const var19 = 19;                    \nMessage 20                              \n 1 const var20 = 20;                    \nMessage 21                              \n 1 const var21 = 21;                    \nMessage 22                              \n 1 const var22 = 22;                    \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues scroll behavior - content should remain visible after scroll 3`] = `\n\"Message 8                               \n 1 const var8 = 8;                      \nMessage 9                               \n 1 const var9 = 9;                      \nMessage 10                              \n 1 const var10 = 10;                    \nMessage 11                              \n 1 const var11 = 11;                    \nMessage 12                              \n 1 const var12 = 12;                    \nMessage 13                              \n 1 const var13 = 13;                    \nMessage 14                              \n 1 const var14 = 14;                    \nMessage 15                              \n 1 const var15 = 15;                    \nMessage 16                              \n 1 const var16 = 16;                    \nMessage 17                              \n 1 const var17 = 17;                    \nMessage 18                              \n 1 const var18 = 18;                    \nMessage 19                              \n 1 const var19 = 19;                    \nMessage 20                              \n 1 const var20 = 20;                    \nMessage 21                              \n 1 const var21 = 21;                    \nMessage 22                              \n 1 const var22 = 22;                    \n\"\n`;\n\nexports[`LineNumber in ScrollBox - Height and Overlap Issues VISUAL CHECK: box with line_number should have clean spacing 1`] = `\n\"                                                  \n                                                  \n  ═══ Code Block 1 ═══                            \n  ┌────────────────────────────────────────────┐  \n  │ 1 const x = 1;                             │  \n  │ 2 const y = 2;                             │  \n  │ 3 const z = 3;                             │  \n  └────────────────────────────────────────────┘  \n  ═══ Code Block 2 ═══                            \n  ┌────────────────────────────────────────────┐  \n  │ 1 function test() {                        │  \n  │ 2   return 42;                             │  \n  │ 3 }                                        │  \n  └────────────────────────────────────────────┘  \n  ═══ End ═══                                     \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n"
  },
  {
    "path": "packages/solid/tests/__snapshots__/textarea.test.tsx.snap",
    "content": "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\nexports[`Textarea Layout Tests Basic Textarea Rendering should render simple textarea correctly 1`] = `\n\"Hello World                   \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n\nexports[`Textarea Layout Tests Basic Textarea Rendering should render multiline textarea content 1`] = `\n\"Line 1                        \nLine 2                        \nLine 3                        \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n\nexports[`Textarea Layout Tests Basic Textarea Rendering should render textarea with word wrapping 1`] = `\n\"This is a very long           \nline that should              \nwrap to multiple              \nlines when word               \nwrapping is enabled           \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n\nexports[`Textarea Layout Tests Basic Textarea Rendering should render textarea with placeholder 1`] = `\n\"Type something here...                  \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea Layout Tests Prompt-like Layout should render textarea in prompt-style layout with indicator 1`] = `\n\"┌──────────────────────────────────────────────────────────┐\n│                                                          │\n│ > Hello from the prompt                                  │\n│                                                          │\n│provider model-name                        ctrl+p commands│\n└──────────────────────────────────────────────────────────┘\n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`Textarea Layout Tests Prompt-like Layout should render textarea with long wrapping text in prompt layout 1`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│   This is a very long prompt that will wrap    │\n│ > across multiple lines in the textarea. It    │\n│   should maintain proper layout with the       │\n│   indicator on the left.                       │\n│                                                │\n│openai gpt-4                                    │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Prompt-like Layout should render textarea in shell mode with different indicator 1`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│ ! ls -la                                       │\n│                                                │\n│shell mode                                      │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Reactive Textarea Updates should update textarea content reactively 1`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│ > Initial text                                 │\n│                                                │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Reactive Textarea Updates should update textarea content reactively 2`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│   Updated text that is much longer and should  │\n│ > wrap to multiple lines if word wrapping is   │\n│   enabled                                      │\n│                                                │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Reactive Textarea Updates should handle textarea growth with word wrapping 1`] = `\n\"┌──────────────────────────────────────┐\n│>                                     │\n│   Short                              │\n│                                      │\n│Status line below textarea            │\n└──────────────────────────────────────┘\n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea Layout Tests Reactive Textarea Updates should handle textarea growth with word wrapping 2`] = `\n\"┌──────────────────────────────────────┐\n│>                                     │\n│   This is a very long text that will │\n│   definitely wrap to multiple lines  │\n│   and cause the textarea to grow in  │\n│   height. It should push down the    │\n│   status line below it.              │\n│                                      │\n│Status line below textarea            │\n└──────────────────────────────────────┘\n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea Layout Tests Complex Layouts with Multiple Textareas should render multiple textareas in a column layout 1`] = `\n\"┌─Chat─────────────────────────────────────────────────────┐\n│┌────────────────────────────────────────────────────────┐│\n││User  What is the weather like today?                   ││\n│└────────────────────────────────────────────────────────┘│\n│                                                          │\n│┌────────────────────────────────────────────────────────┐│\n││AI    I don't have access to real-time weather data,    ││\n││      but I can help you find that information through  ││\n││      various weather services.                         ││\n│└────────────────────────────────────────────────────────┘│\n└──────────────────────────────────────────────────────────┘\n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`Textarea Layout Tests Complex Layouts with Multiple Textareas should handle nested boxes with textareas at different positions 1`] = `\n\"┌─Layout Test────────────────────────────────────┐     \n│┌──────────────────┐ ┌─────────────────────────┐│     \n││Input 1:          │ │Input 2:                 ││     \n││Left panel content│ │Right panel with longer  ││     \n││                  │ │content that may wrap    ││     \n│└──────────────────┘ └─────────────────────────┘│     \n│                                                │     \n│┌──────────────────────────────────────────────┐│     \n││Bottom input:                                 ││     \n││Bottom panel spanning full width              ││     \n│└──────────────────────────────────────────────┘│     \n└────────────────────────────────────────────────┘     \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n                                                       \n\"\n`;\n\nexports[`Textarea Layout Tests Text Component Comparison should render text in prompt-style layout with indicator 1`] = `\n\"┌──────────────────────────────────────────────────────────┐\n│                                                          │\n│ > Hello from the prompt                                  │\n│                                                          │\n│provider model-name                        ctrl+p commands│\n└──────────────────────────────────────────────────────────┘\n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`Textarea Layout Tests Text Component Comparison should render text with long wrapping content in prompt layout 1`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│   This is a very long prompt that will wrap    │\n│ > across multiple lines in the text component. │\n│    It should maintain proper layout with the   │\n│   indicator on the left.                       │\n│                                                │\n│openai gpt-4                                    │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Text Component Comparison should update text content reactively in prompt layout 1`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│ > Initial text                                 │\n│                                                │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Text Component Comparison should update text content reactively in prompt layout 2`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│   Updated text that is much longer and should  │\n│ > wrap to multiple lines if word wrapping is   │\n│   enabled                                      │\n│                                                │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Text Component Comparison should render text in shell mode with different indicator 1`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│ ! ls -la                                       │\n│                                                │\n│shell mode                                      │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Text Component Comparison should render full prompt layout with text component 1`] = `\n\"┌────────────────────────────────────────────────────────────────────┐\n│                                                                    │\n│ > Explain how async/await works in JavaScript and provide some     │\n│   examples                                                         │\n│                                                                    │\n│openai gpt-4-turbo                                   ctrl+p commands│\n└────────────────────────────────────────────────────────────────────┘\n                                                                      \nTip: Use arrow keys to navigate through history when cursor is at the \nstart                                                                 \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n\"\n`;\n\nexports[`Textarea Layout Tests Text Component Comparison should handle very long single-line text in prompt layout 1`] = `\n\"┌──────────────────────────────────────┐\n│>                                     │\n│   ThisIsAVeryLongLineWithNoSpacesThat│\n│   WillWrapByCharacterWhenCharWrapping│\n│   IsEnabled                          │\n│                                      │\n└──────────────────────────────────────┘\n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea Layout Tests Text Component Comparison should render multiline text in prompt layout 1`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│   Line 1: First line of text                   │\n│ > Line 2: Second line of text                  │\n│   Line 3: Third line of text                   │\n│                                                │\n│multiline example                               │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests FlexShrink Regression Tests should not shrink box when width is set via setter 1`] = `\n\"┌────────────────────────────┐\n│>    Content that takes up  │\n│     space                  │\n└────────────────────────────┘\n                              \n\"\n`;\n\nexports[`Textarea Layout Tests FlexShrink Regression Tests should not shrink box when height is set via setter in column layout 1`] = `\n\"┌───────────────────────┐     \n│Header                 │     \n│                       │     \n│                       │     \n│Line1                  │     \n│Line2                  │     \n│Line3                  │     \n│Footer                 │     \n│                       │     \n└───────────────────────┘     \n                              \n                              \n                              \n                              \n                              \n\"\n`;\n\nexports[`Textarea Layout Tests Edge Cases and Styling should render textarea with focused colors 1`] = `\n\"┌──────────────────────────────────────┐\n│>                                     │\n│   Focused textarea                   │\n│                                      │\n└──────────────────────────────────────┘\n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea Layout Tests Edge Cases and Styling should render empty textarea with placeholder in prompt layout 1`] = `\n\"┌────────────────────────────────────────────────┐\n│                                                │\n│ > Enter your prompt here...                    │\n│                                                │\n│Ready to chat                                   │\n└────────────────────────────────────────────────┘\n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Edge Cases and Styling should render textarea with very long single line 1`] = `\n\"┌──────────────────────────────────────┐\n│>                                     │\n│   ThisIsAVeryLongLineWithNoSpacesThat│\n│   WillWrapByCharacterWhenCharWrapping│\n│   IsEnabled                          │\n│                                      │\n└──────────────────────────────────────┘\n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea Layout Tests Edge Cases and Styling should render full prompt-like layout with all components 1`] = `\n\"┌────────────────────────────────────────────────────────────────────┐\n│                                                                    │\n│ > Explain how async/await works in JavaScript and provide some     │\n│   examples                                                         │\n│                                                                    │\n│openai gpt-4-turbo                                   ctrl+p commands│\n└────────────────────────────────────────────────────────────────────┘\n                                                                      \nTip: Use arrow keys to navigate through history when cursor is at the \nstart                                                                 \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n                                                                      \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should correctly measure text after content change 1`] = `\n\"┌──────────────────────────────────────┐          \n│Short text                            │          \n└──────────────────────────────────────┘          \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should correctly measure text after content change 2`] = `\n\"┌──────────────────────────────────────┐          \n│This is a much longer text that will  │          \n│definitely wrap to multiple lines     │          \n│when rendered                         │          \n└──────────────────────────────────────┘          \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should handle rapid content updates correctly 1`] = `\n\"┌────────────────────────────┐          \n│Update 4: some text here    │          \n└────────────────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should handle width changes with cached measures 1`] = `\n\"┌────────────────────────────┐                              \n│Content that will wrap      │                              \n│differently at different    │                              \n│widths                      │                              \n└────────────────────────────┘                              \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should handle width changes with cached measures 2`] = `\n\"┌────────────────────────────────────────────────┐          \n│Content that will wrap differently at different │          \n│widths                                          │          \n└────────────────────────────────────────────────┘          \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should handle width changes with cached measures 3`] = `\n\"┌──────────────────┐                                        \n│Content that will │                                        \n│wrap differently  │                                        \n│at different      │                                        \n│widths            │                                        \n└──────────────────┘                                        \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n                                                            \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should handle empty to non-empty content transition 1`] = `\n\"┌──────────────────────────────────────┐          \n│                                      │          \n└──────────────────────────────────────┘          \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should handle empty to non-empty content transition 2`] = `\n\"┌──────────────────────────────────────┐          \n│Now with content                      │          \n└──────────────────────────────────────┘          \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should handle empty to non-empty content transition 3`] = `\n\"┌──────────────────────────────────────┐          \n│                                      │          \n└──────────────────────────────────────┘          \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n                                                  \n\"\n`;\n\nexports[`Textarea Layout Tests Measure Cache Edge Cases should correctly measure multiline content with unicode 1`] = `\n\"┌────────────────────────────┐          \n│Hello 世界                  │          \n│こんにちは                  │          \n│🌟 Emoji 🚀                 │          \n└────────────────────────────┘          \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n                                        \n\"\n`;\n"
  },
  {
    "path": "packages/solid/tests/box.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal } from \"solid-js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"Box Component\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  it(\"should support focusable prop and controlled focus state\", async () => {\n    let boxRef: any\n    const [focused, setFocused] = createSignal(false)\n\n    testSetup = await testRender(\n      () => <box ref={boxRef} focusable focused={focused()} style={{ width: 10, height: 5, border: true }} />,\n      { width: 15, height: 8 },\n    )\n\n    await testSetup.renderOnce()\n\n    expect(boxRef.focusable).toBe(true)\n    expect(boxRef.focused).toBe(false)\n\n    setFocused(true)\n    await testSetup.renderOnce()\n\n    expect(boxRef.focused).toBe(true)\n\n    setFocused(false)\n    await testSetup.renderOnce()\n\n    expect(boxRef.focused).toBe(false)\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/control-flow.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach, test } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal, createEffect, createMemo, For, Show, Switch, Match, Index, ErrorBoundary } from \"solid-js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"SolidJS Renderer - Control Flow Components\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  describe(\"<For> Component\", () => {\n    it(\"should render items with <For> component\", async () => {\n      const items = [\"First\", \"Second\", \"Third\"]\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <For each={items}>{(item, index) => <text>{`${index() + 1}. ${item}`}</text>}</For>\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n\n      expect(frame).toContain(\"1. First\")\n      expect(frame).toContain(\"2. Second\")\n      expect(frame).toContain(\"3. Third\")\n\n      const children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(3)\n    })\n\n    it(\"should handle reactive updates with <For>\", async () => {\n      const [items, setItems] = createSignal([\"A\", \"B\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <For each={items()}>{(item) => <text>Item: {item}</text>}</For>\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      setItems([\"A\", \"B\", \"C\", \"D\"])\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(4)\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Item: A\")\n      expect(frame).toContain(\"Item: D\")\n    })\n\n    it(\"should handle empty arrays with <For>\", async () => {\n      const [items, setItems] = createSignal([\"Item\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <For each={items()}>{(item) => <text>{item}</text>}</For>\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(1)\n\n      setItems([])\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(0)\n    })\n\n    it(\"should handle complex objects with <For>\", async () => {\n      const [todos, setTodos] = createSignal([\n        { id: 1, text: \"Learn SolidJS\", done: false },\n        { id: 2, text: \"Build TUI\", done: true },\n      ])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <For each={todos()}>\n              {(todo, index) => (\n                <text>\n                  {index() + 1}. {todo.done ? \"✓\" : \"○\"} {todo.text}\n                </text>\n              )}\n            </For>\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n\n      expect(frame).toContain(\"1. ○ Learn SolidJS\")\n      expect(frame).toContain(\"2. ✓ Build TUI\")\n    })\n\n    it(\"should handle array reversal correctly\", async () => {\n      const [items, setItems] = createSignal([1, 2, 3, 4])\n\n      testSetup = await testRender(\n        () => (\n          <box id=\"container\">\n            <For each={items()}>{(item) => <box id={`item-${item}`} />}</For>\n          </box>\n        ),\n        { width: 30, height: 15 },\n      )\n\n      await testSetup.renderOnce()\n      const container = testSetup.renderer.root.getChildren()[0]!\n      let children = container.getChildren()\n\n      expect(children.length).toBe(4)\n      expect(children.map((c) => c.id)).toEqual([\"item-1\", \"item-2\", \"item-3\", \"item-4\"])\n\n      setItems([4, 3, 2, 1])\n      await testSetup.renderOnce()\n\n      children = container.getChildren()\n      expect(children.length).toBe(4)\n      expect(children.map((c) => c.id)).toEqual([\"item-4\", \"item-3\", \"item-2\", \"item-1\"])\n\n      setItems([1, 2, 3, 4, 5])\n      await testSetup.renderOnce()\n\n      children = container.getChildren()\n      expect(children.length).toBe(5)\n      expect(children.map((c) => c.id)).toEqual([\"item-1\", \"item-2\", \"item-3\", \"item-4\", \"item-5\"])\n\n      setItems([5, 4, 3, 2, 1])\n      await testSetup.renderOnce()\n\n      children = container.getChildren()\n      expect(children.length).toBe(5)\n      expect(children.map((c) => c.id)).toEqual([\"item-5\", \"item-4\", \"item-3\", \"item-2\", \"item-1\"])\n    })\n  })\n\n  describe(\"<Show> Component\", () => {\n    it(\"should conditionally render content with <Show>\", async () => {\n      const [showContent, setShowContent] = createSignal(true)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Show when={showContent()} fallback={<text>Fallback content</text>}>\n              <text>Main content</text>\n            </Show>\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Main content\")\n      expect(frame).not.toContain(\"Fallback content\")\n\n      setShowContent(false)\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Fallback content\")\n      expect(frame).not.toContain(\"Main content\")\n    })\n\n    it(\"should handle reactive condition changes with <Show>\", async () => {\n      const [count, setCount] = createSignal(5)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Show when={count() > 3} fallback={<text>Count too low</text>}>\n              <text>Count is high: {count()}</text>\n            </Show>\n          </box>\n        ),\n        { width: 25, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Count is high: 5\")\n\n      setCount(2)\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Count too low\")\n      expect(frame).not.toContain(\"Count is high\")\n    })\n\n    it(\"should handle <Show> without fallback\", async () => {\n      const [visible, setVisible] = createSignal(true)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Show when={visible()}>\n              <text>Visible content</text>\n            </Show>\n            <text>Always visible</text>\n          </box>\n        ),\n        { width: 20, height: 8 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Visible content\")\n      expect(frame).toContain(\"Always visible\")\n\n      setVisible(false)\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).not.toContain(\"Visible content\")\n      expect(frame).toContain(\"Always visible\")\n    })\n\n    it(\"should conditionally render content with <Show> in the correct order\", async () => {\n      const [showContent, setShowContent] = createSignal(false)\n\n      testSetup = await testRender(\n        () => {\n          return (\n            <box id=\"container\">\n              <box id=\"first\"></box>\n              <Show when={showContent()}>\n                <box id=\"second\" />\n              </Show>\n              <box id=\"third\"></box>\n            </box>\n          )\n        },\n        { width: 20, height: 5 },\n      )\n\n      setShowContent(true)\n      await testSetup.renderOnce()\n\n      const children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n\n      expect(children.length).toBe(3)\n      expect(children[0]!.id).toBe(\"first\")\n      expect(children[1]!.id).toBe(\"second\")\n      expect(children[2]!.id).toBe(\"third\")\n    })\n\n    it(\"should conditionally render content in fragment with <Show> in the correct order\", async () => {\n      const [showContent, setShowContent] = createSignal(false)\n\n      testSetup = await testRender(\n        () => {\n          return (\n            <>\n              <box id=\"first\"></box>\n              <Show when={showContent()}>\n                <box id=\"second\" />\n              </Show>\n              <box id=\"third\"></box>\n            </>\n          )\n        },\n        { width: 20, height: 5 },\n      )\n\n      setShowContent(true)\n      await testSetup.renderOnce()\n\n      const children = testSetup.renderer.root.getChildren()\n\n      expect(children.length).toBe(3)\n      expect(children[0]!.id).toBe(\"first\")\n      expect(children[1]!.id).toBe(\"second\")\n      expect(children[2]!.id).toBe(\"third\")\n    })\n  })\n\n  describe(\"<Switch> and <Match> Components\", () => {\n    it(\"should render first matching <Match> in <Switch>\", async () => {\n      const [value, setValue] = createSignal(\"option1\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Switch fallback={<text>No match</text>}>\n              <Match when={value() === \"option1\"}>\n                <text>Option 1 selected</text>\n              </Match>\n              <Match when={value() === \"option2\"}>\n                <text>Option 2 selected</text>\n              </Match>\n              <Match when={value() === \"option3\"}>\n                <text>Option 3 selected</text>\n              </Match>\n            </Switch>\n          </box>\n        ),\n        { width: 25, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Option 1 selected\")\n      expect(frame).not.toContain(\"Option 2 selected\")\n\n      setValue(\"option2\")\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Option 2 selected\")\n      expect(frame).not.toContain(\"Option 1 selected\")\n\n      setValue(\"unknown\")\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"No match\")\n    })\n\n    it(\"should handle reactive conditions with <Switch>\", async () => {\n      const [score, setScore] = createSignal(85)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Switch>\n              <Match when={score() >= 90}>\n                <text>Grade: A</text>\n              </Match>\n              <Match when={score() >= 80}>\n                <text>Grade: B</text>\n              </Match>\n              <Match when={score() >= 70}>\n                <text>Grade: C</text>\n              </Match>\n              <Match when={true}>\n                <text>Grade: F</text>\n              </Match>\n            </Switch>\n          </box>\n        ),\n        { width: 15, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Grade: B\")\n\n      setScore(95)\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Grade: A\")\n\n      setScore(65)\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Grade: F\")\n    })\n  })\n\n  describe(\"<Index> Component\", () => {\n    it(\"should iterate over array with <Index> providing index access\", async () => {\n      const items = [\"Apple\", \"Banana\", \"Cherry\"]\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Index each={items}>{(item, index) => <text>{`${index}. ${item()}`}</text>}</Index>\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n\n      expect(frame).toContain(\"0. Apple\")\n      expect(frame).toContain(\"1. Banana\")\n      expect(frame).toContain(\"2. Cherry\")\n    })\n\n    it(\"should handle reactive updates with <Index>\", async () => {\n      const [items, setItems] = createSignal([10, 20])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Index each={items()}>\n              {(item, index) => (\n                <text>\n                  Index {index}: {item()}\n                </text>\n              )}\n            </Index>\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Index 0: 10\")\n      expect(frame).toContain(\"Index 1: 20\")\n\n      setItems([10, 20, 30, 40])\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Index 0: 10\")\n      expect(frame).toContain(\"Index 3: 40\")\n    })\n\n    it(\"should work with complex data structures in <Index>\", async () => {\n      const data = [\n        { name: \"Alice\", score: 95 },\n        { name: \"Bob\", score: 87 },\n        { name: \"Charlie\", score: 92 },\n      ]\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Index each={data}>\n              {(item, index) => (\n                <text>\n                  #{index + 1} {item().name}: {item().score} points\n                </text>\n              )}\n            </Index>\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n\n      expect(frame).toContain(\"#1 Alice: 95 points\")\n      expect(frame).toContain(\"#2 Bob: 87 points\")\n      expect(frame).toContain(\"#3 Charlie: 92 points\")\n    })\n  })\n\n  describe(\"<ErrorBoundary> Component\", () => {\n    it(\"should catch and handle errors with <ErrorBoundary>\", async () => {\n      const [shouldError, setShouldError] = createSignal(false)\n\n      const ErrorComponent = ({ shouldError }: { shouldError: () => boolean }) => {\n        createEffect(() => {\n          if (shouldError()) {\n            throw new Error(\"Test error\")\n          }\n        })\n        return <text>Normal content</text>\n      }\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <ErrorBoundary fallback={(err: any) => <text>Error caught: {err.message}</text>}>\n              <ErrorComponent shouldError={shouldError} />\n            </ErrorBoundary>\n          </box>\n        ),\n        { width: 30, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Normal content\")\n      expect(frame).not.toContain(\"Error caught\")\n\n      setShouldError(true)\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Error caught: Test error\")\n      expect(frame).not.toContain(\"Normal content\")\n    })\n\n    it(\"should handle nested error boundaries\", async () => {\n      const [shouldErrorOuter, setShouldErrorOuter] = createSignal(false)\n      const [shouldErrorInner, setShouldErrorInner] = createSignal(false)\n\n      const InnerComponent = () => {\n        createEffect(() => {\n          if (shouldErrorInner()) {\n            throw new Error(\"Inner error\")\n          }\n        })\n        return <text>Inner content</text>\n      }\n\n      const OuterComponent = () => {\n        createEffect(() => {\n          if (shouldErrorOuter()) {\n            throw new Error(\"Outer error\")\n          }\n        })\n        return (\n          <ErrorBoundary fallback={(err: any) => <text>Inner boundary: {err.message}</text>}>\n            <InnerComponent />\n          </ErrorBoundary>\n        )\n      }\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <ErrorBoundary fallback={(err: any) => <text>Outer boundary: {err.message}</text>}>\n              <OuterComponent />\n            </ErrorBoundary>\n          </box>\n        ),\n        { width: 40, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Inner content\")\n\n      setShouldErrorInner(true)\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Inner boundary: Inner error\")\n\n      // Note: Once an ErrorBoundary catches an error, it stays in error state\n    })\n  })\n\n  describe(\"Combined Control Flow\", () => {\n    it(\"should handle <For> inside <Show>\", async () => {\n      const [showList, setShowList] = createSignal(true)\n      const [items, setItems] = createSignal([\"A\", \"B\", \"C\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Show when={showList()} fallback={<text>List is hidden</text>}>\n              <For each={items()}>{(item) => <text>Item: {item}</text>}</For>\n            </Show>\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(3)\n\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Item: A\")\n      expect(frame).toContain(\"Item: C\")\n\n      setShowList(false)\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(1)\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"List is hidden\")\n      expect(frame).not.toContain(\"Item: A\")\n    })\n\n    it(\"should handle <Show> inside <text>\", async () => {\n      const [showExtra, setShowExtra] = createSignal(true)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <text>\n              Base text\n              <Show when={showExtra()}>\n                <span style={{ fg: \"red\" }}> extra styled text</span>\n              </Show>\n            </text>\n          </box>\n        ),\n        { width: 30, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Base text\")\n      expect(frame).toContain(\"extra styled text\")\n\n      setShowExtra(false)\n      await testSetup.renderOnce()\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Base text\")\n      expect(frame).not.toContain(\"extra styled text\")\n    })\n\n    it(\"should handle <Show> inside <span>/<b>\", async () => {\n      const [showExtra, setShowExtra] = createSignal(true)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <text>\n              Base text\n              <br />\n              <span style={{ fg: \"red\" }}>\n                <Show when={showExtra()}>extra styled text</Show>\n              </span>\n              <br />\n              <b>\n                <Show when={showExtra()}>extra bold text</Show>\n              </b>\n            </text>\n          </box>\n        ),\n        { width: 30, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      console.log(frame)\n      expect(frame).toContain(\"Base text\")\n      expect(frame).toContain(\"extra styled text\")\n      expect(frame).toContain(\"extra bold text\")\n\n      setShowExtra(false)\n      await testSetup.renderOnce()\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Base text\")\n      expect(frame).not.toContain(\"extra styled text\")\n      expect(frame).not.toContain(\"extra bold text\")\n    })\n\n    it(\"should handle <Show> inside <For>\", async () => {\n      const items = [\"A\", \"B\", \"C\", \"D\"]\n      const [visibleItems, setVisibleItems] = createSignal(new Set([\"A\", \"C\"]))\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <For each={items}>\n              {(item) => (\n                <Show when={visibleItems().has(item)}>\n                  <text>Item: {item}</text>\n                </Show>\n              )}\n            </For>\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Item: A\")\n      expect(frame).toContain(\"Item: C\")\n      expect(frame).not.toContain(\"Item: B\")\n      expect(frame).not.toContain(\"Item: D\")\n\n      setVisibleItems(new Set([\"B\", \"D\"]))\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Item: B\")\n      expect(frame).toContain(\"Item: D\")\n      expect(frame).not.toContain(\"Item: A\")\n      expect(frame).not.toContain(\"Item: C\")\n    })\n\n    it(\"should handle <Switch> with <For> inside matches\", async () => {\n      const [mode, setMode] = createSignal<\"list\" | \"grid\">(\"list\")\n      const items = [\"One\", \"Two\", \"Three\"]\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Switch>\n              <Match when={mode() === \"list\"}>\n                <For each={items}>{(item) => <text>• {item}</text>}</For>\n              </Match>\n              <Match when={mode() === \"grid\"}>\n                <For each={items}>{(item) => <text>[{item}]</text>}</For>\n              </Match>\n            </Switch>\n          </box>\n        ),\n        { width: 25, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"• One\")\n      expect(frame).toContain(\"• Two\")\n      expect(frame).toContain(\"• Three\")\n\n      setMode(\"grid\")\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"[One]\")\n      expect(frame).toContain(\"[Two]\")\n      expect(frame).toContain(\"[Three]\")\n      expect(frame).not.toContain(\"• One\")\n    })\n\n    it(\"should be able to anchor to slot nodes\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box>\n            <box border title=\"A\" />\n            <Show when={false}>\n              <box border title=\"C\" />\n            </Show>\n            <Show when={true}>\n              <box border title=\"B\" />\n            </Show>\n          </box>\n        ),\n        { width: 25, height: 10 },\n      )\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"A\")\n      expect(frame).toContain(\"B\")\n      expect(frame).not.toContain(\"C\")\n      // Consistent ordering\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should find descendants by id through slot renderables in scrollbox\", async () => {\n      const [showContent, setShowContent] = createSignal(false) // Start with FALSE to keep slot in tree\n\n      testSetup = await testRender(\n        () => (\n          <box id=\"parent-box\">\n            <box id=\"always-visible\" border title=\"Always\" />\n            <Show when={showContent()}>\n              <box id=\"conditional-child\" border title=\"Conditional\">\n                <box id=\"nested-child\" border title=\"Nested\" />\n              </box>\n            </Show>\n            <box id=\"another-visible\" border title=\"Another\" />\n          </box>\n        ),\n        { width: 30, height: 15 },\n      )\n\n      await testSetup.renderOnce()\n\n      const parentBox = testSetup.renderer.root.findDescendantById(\"parent-box\")\n      expect(parentBox).toBeDefined()\n\n      // This should work - findDescendantById should be able to traverse through or skip slot renderables\n      // Currently fails because LayoutSlotRenderable (from Show when={false}) doesn't have findDescendantById\n      const anotherVisible = parentBox?.findDescendantById(\"another-visible\")\n      expect(anotherVisible).toBeDefined()\n      expect(anotherVisible?.id).toBe(\"another-visible\")\n    })\n\n    it(\"REPRODUCE BUG: For component has incorrect ordering after array reordering\", async () => {\n      interface Option {\n        id: string\n        display: string\n        description?: string\n      }\n\n      const [options, setOptions] = createSignal<Option[]>([])\n\n      testSetup = await testRender(\n        () => (\n          <box id=\"container\">\n            <For each={options()}>\n              {(option, index) => (\n                <box id={`option-${option.id}`}>\n                  <text>\n                    {option.display}\n                    <Show when={option.description}>\n                      <span> - {option.description}</span>\n                    </Show>\n                  </text>\n                </box>\n              )}\n            </For>\n          </box>\n        ),\n        { width: 50, height: 25 },\n      )\n\n      await testSetup.renderOnce()\n\n      // === BUG: Array reversal causes incorrect ordering ===\n      const orderedItems = [\n        { id: \"order-1\", display: \"First\" },\n        { id: \"order-2\", display: \"Second\" },\n        { id: \"order-3\", display: \"Third\" },\n        { id: \"order-4\", display: \"Fourth\" },\n        { id: \"order-5\", display: \"Fifth\" },\n      ]\n\n      setOptions(orderedItems)\n      await testSetup.renderOnce()\n\n      const container = testSetup.renderer.root.findDescendantById(\"container\")!\n      let children = container.getChildren()\n\n      // Verify initial order\n      expect(children.length).toBe(5)\n      expect(children[0]?.id).toBe(\"option-order-1\")\n      expect(children[1]?.id).toBe(\"option-order-2\")\n      expect(children[2]?.id).toBe(\"option-order-3\")\n      expect(children[3]?.id).toBe(\"option-order-4\")\n      expect(children[4]?.id).toBe(\"option-order-5\")\n\n      // Reverse the array - THIS EXPOSES THE BUG\n      setOptions([...orderedItems].reverse())\n      await testSetup.renderOnce()\n\n      children = container.getChildren()\n\n      // BUG: The order is INCORRECT after reversing!\n      // Expected: [order-5, order-4, order-3, order-2, order-1]\n      // Actual might have swapped elements\n      expect(children.length).toBe(5)\n      expect(children[0]?.id).toBe(\"option-order-5\")\n      expect(children[1]?.id).toBe(\"option-order-4\")\n      expect(children[2]?.id).toBe(\"option-order-3\")\n      expect(children[3]?.id).toBe(\"option-order-2\") // ← BUG: This might be order-1\n      expect(children[4]?.id).toBe(\"option-order-1\") // ← BUG: This might be order-2\n    })\n  })\n  describe(\"Text escaping\", () => {\n    it(\"renders angle brackets in text content without HTML entities\", async () => {\n      const content = \"with some > text < like this </>\"\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <text>{content}</text>\n          </box>\n        ),\n        { width: 60, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n\n      expect(frame).toContain(content)\n      expect(frame).not.toContain(\"&lt;\")\n      expect(frame).not.toContain(\"&gt;\")\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/cursor-behavior.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal, onMount, Show } from \"solid-js\"\nimport type { TextareaRenderable } from \"@opentui/core\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"Textarea Cursor Behavior Tests\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  describe(\"Cursor Visibility\", () => {\n    it(\"should show cursor when textarea is focused\", async () => {\n      testSetup = await testRender(() => <textarea focused initialValue=\"Hello\" width={20} height={5} />, {\n        width: 30,\n        height: 10,\n      })\n\n      await testSetup.renderOnce()\n\n      const cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n    })\n\n    it(\"should not change cursor state when textarea is never focused\", async () => {\n      testSetup = await testRender(() => <textarea initialValue=\"Hello\" width={20} height={5} />, {\n        width: 30,\n        height: 10,\n      })\n\n      const beforeRender = testSetup.renderer.getCursorState()\n\n      await testSetup.renderOnce()\n\n      const afterRender = testSetup.renderer.getCursorState()\n\n      expect(afterRender.visible).toBe(beforeRender.visible)\n      expect(afterRender.x).toBe(beforeRender.x)\n      expect(afterRender.y).toBe(beforeRender.y)\n    })\n\n    it(\"should hide cursor when showCursor is set to false while focused\", async () => {\n      const [showCursor, setShowCursor] = createSignal(true)\n\n      testSetup = await testRender(\n        () => <textarea focused initialValue=\"Hello\" width={20} height={5} showCursor={showCursor()} />,\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n\n      let cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n\n      setShowCursor(false)\n      await testSetup.renderOnce()\n\n      cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(false)\n    })\n\n    it(\"should show cursor again when showCursor is set back to true\", async () => {\n      const [showCursor, setShowCursor] = createSignal(true)\n\n      testSetup = await testRender(\n        () => <textarea focused initialValue=\"Hello\" width={20} height={5} showCursor={showCursor()} />,\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n\n      setShowCursor(false)\n      await testSetup.renderOnce()\n      cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(false)\n\n      setShowCursor(true)\n      await testSetup.renderOnce()\n      cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n    })\n\n    it(\"should hide cursor when textarea loses focus\", async () => {\n      const [isFocused, setIsFocused] = createSignal(true)\n\n      testSetup = await testRender(\n        () => <textarea focused={isFocused()} initialValue=\"Hello\" width={20} height={5} />,\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n\n      setIsFocused(false)\n      await testSetup.renderOnce()\n\n      cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(false)\n    })\n\n    it(\"should show cursor when textarea gains focus\", async () => {\n      const [isFocused, setIsFocused] = createSignal(false)\n\n      testSetup = await testRender(\n        () => <textarea focused={isFocused()} initialValue=\"Hello\" width={20} height={5} />,\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(false)\n\n      setIsFocused(true)\n      await testSetup.renderOnce()\n\n      cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n    })\n\n    it(\"should not show cursor if showCursor is false even when focused\", async () => {\n      testSetup = await testRender(\n        () => <textarea focused initialValue=\"Hello\" width={20} height={5} showCursor={false} />,\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n\n      const cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(false)\n    })\n  })\n\n  describe(\"Cursor Position\", () => {\n    it(\"should position cursor at the end of text initially\", async () => {\n      testSetup = await testRender(() => <textarea focused initialValue=\"Hello\" width={20} height={5} />, {\n        width: 30,\n        height: 10,\n      })\n\n      await testSetup.renderOnce()\n\n      const cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n      expect(cursorState.x).toBeGreaterThan(0)\n      expect(cursorState.y).toBeGreaterThan(0)\n    })\n\n    it(\"should update cursor position when typing\", async () => {\n      testSetup = await testRender(() => <textarea focused initialValue=\"X\" width={20} height={5} />, {\n        width: 30,\n        height: 10,\n      })\n\n      await testSetup.renderOnce()\n\n      const initialState = testSetup.renderer.getCursorState()\n      const initialX = initialState.x\n\n      await testSetup.mockInput.typeText(\"ABC\")\n      await testSetup.renderOnce()\n\n      const afterTypingState = testSetup.renderer.getCursorState()\n      expect(afterTypingState.x).toBe(initialX + 3)\n    })\n\n    it(\"should position cursor correctly with multiline text\", async () => {\n      testSetup = await testRender(() => <textarea focused initialValue={\"Line1\\nLine2\"} width={20} height={5} />, {\n        width: 30,\n        height: 10,\n      })\n\n      await testSetup.renderOnce()\n\n      const cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n      expect(cursorState.x).toBeGreaterThan(0)\n      expect(cursorState.y).toBeGreaterThanOrEqual(1)\n    })\n\n    it(\"should update cursor position when navigating with arrow keys\", async () => {\n      testSetup = await testRender(() => <textarea focused initialValue=\"Hello\" width={20} height={5} />, {\n        width: 30,\n        height: 10,\n      })\n\n      await testSetup.renderOnce()\n\n      const initialState = testSetup.renderer.getCursorState()\n      expect(initialState.visible).toBe(true)\n      const initialX = initialState.x\n\n      testSetup.mockInput.pressArrow(\"left\")\n      await testSetup.renderOnce()\n\n      const afterLeftState = testSetup.renderer.getCursorState()\n      expect(afterLeftState.visible).toBe(true)\n      expect(afterLeftState.x).toBeLessThanOrEqual(initialX)\n    })\n  })\n\n  describe(\"Cursor Style and Color\", () => {\n    it(\"should apply default cursor style when focused\", async () => {\n      testSetup = await testRender(() => <textarea focused initialValue=\"Hello\" width={20} height={5} />, {\n        width: 30,\n        height: 10,\n      })\n\n      await testSetup.renderOnce()\n\n      const cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n      expect(cursorState.style).toBe(\"block\")\n      expect(cursorState.blinking).toBe(true)\n    })\n\n    it(\"should apply custom cursor style\", async () => {\n      testSetup = await testRender(\n        () => (\n          <textarea\n            focused\n            initialValue=\"Hello\"\n            width={20}\n            height={5}\n            cursorStyle={{ style: \"line\", blinking: false }}\n          />\n        ),\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n\n      const cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n      expect(cursorState.style).toBe(\"line\")\n      expect(cursorState.blinking).toBe(false)\n    })\n\n    it(\"should apply custom cursor color\", async () => {\n      testSetup = await testRender(\n        () => <textarea focused initialValue=\"Hello\" width={20} height={5} cursorColor=\"#ff0000\" />,\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n\n      const cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n      expect(cursorState.color.r).toBeCloseTo(1, 1)\n      expect(cursorState.color.g).toBeCloseTo(0, 1)\n      expect(cursorState.color.b).toBeCloseTo(0, 1)\n    })\n  })\n\n  describe(\"Cursor with Multiple Textareas\", () => {\n    it(\"should only show cursor for the focused textarea\", async () => {\n      const [focused1, setFocused1] = createSignal(true)\n      const [focused2, setFocused2] = createSignal(false)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <textarea focused={focused1()} initialValue=\"First\" width={20} height={3} />\n            <textarea focused={focused2()} initialValue=\"Second\" width={20} height={3} />\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n\n      let cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n      const firstY = cursorState.y\n\n      setFocused1(false)\n      setFocused2(true)\n      await testSetup.renderOnce()\n\n      cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n      expect(cursorState.y).toBeGreaterThan(firstY)\n    })\n\n    it(\"should hide cursor when all textareas are unfocused\", async () => {\n      const [focused1, setFocused1] = createSignal(true)\n      const [focused2, setFocused2] = createSignal(false)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <textarea focused={focused1()} initialValue=\"First\" width={20} height={3} />\n            <textarea focused={focused2()} initialValue=\"Second\" width={20} height={3} />\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(true)\n\n      setFocused1(false)\n      await testSetup.renderOnce()\n\n      cursorState = testSetup.renderer.getCursorState()\n      expect(cursorState.visible).toBe(false)\n    })\n  })\n\n  describe(\"Multiline Paste\", () => {\n    it(\"keeps viewport offsets stable when pasting multiline content\", async () => {\n      let textareaRef: TextareaRenderable | undefined\n      const [editing, setEditing] = createSignal(false)\n\n      const textareaKeybindings = () => [\n        { name: \"return\", action: \"submit\" },\n        { name: \"return\", meta: true, action: \"newline\" },\n      ]\n\n      const PasteTestComponent = () => {\n        onMount(() => {\n          setEditing(true)\n        })\n\n        return (\n          <box paddingLeft={2} paddingRight={2} gap={1}>\n            <box paddingLeft={1} gap={1}>\n              <box>\n                <text fg=\"#E8EDF2\">Custom answer</text>\n              </box>\n              <box>\n                <box flexDirection=\"row\">\n                  <box paddingRight={1}>\n                    <text fg=\"#8B98A5\">1.</text>\n                  </box>\n                  <box>\n                    <text fg=\"#E8EDF2\">Type your own answer</text>\n                  </box>\n                </box>\n                <Show when={editing()}>\n                  <box paddingLeft={3}>\n                    <textarea\n                      ref={(val: TextareaRenderable) => {\n                        textareaRef = val\n                        queueMicrotask(() => {\n                          val.focus()\n                          val.gotoLineEnd()\n                        })\n                      }}\n                      initialValue=\"\"\n                      placeholder=\"Type your own answer\"\n                      textColor=\"#E8EDF2\"\n                      focusedTextColor=\"#E8EDF2\"\n                      cursorColor=\"#86B7FF\"\n                      keyBindings={textareaKeybindings()}\n                    />\n                  </box>\n                </Show>\n              </box>\n            </box>\n            <box paddingBottom={1} gap={1} flexDirection=\"row\">\n              <text fg=\"#E8EDF2\">\n                enter <span style={{ fg: \"#8B98A5\" }}>submit</span>\n              </text>\n            </box>\n          </box>\n        )\n      }\n\n      testSetup = await testRender(() => <PasteTestComponent />, { width: 50, height: 12 })\n\n      await testSetup.renderOnce()\n      await new Promise((resolve) => setTimeout(resolve, 0))\n\n      const viewOffsets: Array<{ offsetY: number }> = []\n      const captureOffsets = () => {\n        const viewport = textareaRef?.editorView.getViewport()\n        if (viewport) {\n          viewOffsets.push({ offsetY: viewport.offsetY })\n        }\n      }\n\n      testSetup.renderer.addPostProcessFn(captureOffsets)\n      testSetup.renderer.start()\n      await Bun.sleep(30)\n\n      viewOffsets.length = 0\n      await testSetup.mockInput.pasteBracketedText(\"Line 1\\nLine 2\\nLine 3\")\n\n      await Bun.sleep(200)\n      testSetup.renderer.pause()\n      await testSetup.renderer.idle()\n\n      testSetup.renderer.removePostProcessFn(captureOffsets)\n\n      let transitions = 0\n      for (let i = 1; i < viewOffsets.length; i += 1) {\n        if (viewOffsets[i]!.offsetY !== viewOffsets[i - 1]!.offsetY) {\n          transitions += 1\n        }\n      }\n\n      expect(textareaRef?.plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n      expect(viewOffsets.length).toBeGreaterThan(4)\n      expect(transitions).toBeLessThanOrEqual(1)\n    })\n\n    it(\"keeps viewport offsets steady after multiline paste\", async () => {\n      let textareaRef: TextareaRenderable | undefined\n      const [editing, setEditing] = createSignal(false)\n\n      const textareaKeybindings = () => [\n        { name: \"return\", action: \"submit\" },\n        { name: \"return\", meta: true, action: \"newline\" },\n      ]\n\n      const PasteTestComponent = () => {\n        onMount(() => {\n          setEditing(true)\n        })\n\n        return (\n          <box paddingLeft={2} paddingRight={2} gap={1}>\n            <box paddingLeft={1} gap={1}>\n              <box>\n                <text fg=\"#E8EDF2\">Custom answer</text>\n              </box>\n              <box>\n                <box flexDirection=\"row\">\n                  <box paddingRight={1}>\n                    <text fg=\"#8B98A5\">1.</text>\n                  </box>\n                  <box>\n                    <text fg=\"#E8EDF2\">Type your own answer</text>\n                  </box>\n                </box>\n                <Show when={editing()}>\n                  <box paddingLeft={3}>\n                    <textarea\n                      ref={(val: TextareaRenderable) => {\n                        textareaRef = val\n                        queueMicrotask(() => {\n                          val.focus()\n                          val.gotoLineEnd()\n                        })\n                      }}\n                      height={1}\n                      initialValue=\"\"\n                      placeholder=\"Type your own answer\"\n                      textColor=\"#E8EDF2\"\n                      focusedTextColor=\"#E8EDF2\"\n                      cursorColor=\"#86B7FF\"\n                      keyBindings={textareaKeybindings()}\n                    />\n                  </box>\n                </Show>\n              </box>\n            </box>\n            <box paddingBottom={1} gap={1} flexDirection=\"row\">\n              <text fg=\"#E8EDF2\">\n                enter <span style={{ fg: \"#8B98A5\" }}>submit</span>\n              </text>\n            </box>\n          </box>\n        )\n      }\n\n      testSetup = await testRender(() => <PasteTestComponent />, { width: 50, height: 12 })\n\n      await testSetup.renderOnce()\n      await new Promise((resolve) => setTimeout(resolve, 0))\n\n      const viewOffsets: Array<{ offsetY: number }> = []\n      const captureOffsets = () => {\n        const viewport = textareaRef?.editorView.getViewport()\n        if (viewport) {\n          viewOffsets.push({ offsetY: viewport.offsetY })\n        }\n      }\n\n      testSetup.renderer.addPostProcessFn(captureOffsets)\n\n      testSetup.renderer.start()\n      await Bun.sleep(30)\n\n      viewOffsets.length = 0\n      await testSetup.mockInput.pasteBracketedText(\"Line 1\\nLine 2\\nLine 3\")\n\n      await Bun.sleep(200)\n      testSetup.renderer.pause()\n      await testSetup.renderer.idle()\n\n      testSetup.renderer.removePostProcessFn(captureOffsets)\n\n      let transitions = 0\n      for (let i = 1; i < viewOffsets.length; i += 1) {\n        if (viewOffsets[i]!.offsetY !== viewOffsets[i - 1]!.offsetY) {\n          transitions += 1\n        }\n      }\n\n      expect(textareaRef?.plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n      expect(viewOffsets.length).toBeGreaterThan(4)\n      expect(transitions).toBeLessThanOrEqual(1)\n    })\n\n    it(\"expands height after multiline paste when maxHeight allows\", async () => {\n      let textareaRef: TextareaRenderable | undefined\n      const [editing, setEditing] = createSignal(false)\n\n      const textareaKeybindings = () => [\n        { name: \"return\", action: \"submit\" },\n        { name: \"return\", meta: true, action: \"newline\" },\n      ]\n\n      const PasteTestComponent = () => {\n        onMount(() => {\n          setEditing(true)\n        })\n\n        return (\n          <box paddingLeft={2} paddingRight={2} gap={1}>\n            <box paddingLeft={1} gap={1}>\n              <box>\n                <text fg=\"#E8EDF2\">Custom answer</text>\n              </box>\n              <box>\n                <box flexDirection=\"row\">\n                  <box paddingRight={1}>\n                    <text fg=\"#8B98A5\">1.</text>\n                  </box>\n                  <box>\n                    <text fg=\"#E8EDF2\">Type your own answer</text>\n                  </box>\n                </box>\n                <Show when={editing()}>\n                  <box paddingLeft={3}>\n                    <textarea\n                      ref={(val: TextareaRenderable) => {\n                        textareaRef = val\n                        queueMicrotask(() => {\n                          val.focus()\n                          val.gotoLineEnd()\n                        })\n                      }}\n                      minHeight={1}\n                      maxHeight={6}\n                      initialValue=\"\"\n                      placeholder=\"Type your own answer\"\n                      textColor=\"#E8EDF2\"\n                      focusedTextColor=\"#E8EDF2\"\n                      cursorColor=\"#86B7FF\"\n                      keyBindings={textareaKeybindings()}\n                    />\n                  </box>\n                </Show>\n              </box>\n            </box>\n            <box paddingBottom={1} gap={1} flexDirection=\"row\">\n              <text fg=\"#E8EDF2\">\n                enter <span style={{ fg: \"#8B98A5\" }}>submit</span>\n              </text>\n            </box>\n          </box>\n        )\n      }\n\n      testSetup = await testRender(() => <PasteTestComponent />, { width: 50, height: 12 })\n\n      await testSetup.renderOnce()\n      await new Promise((resolve) => setTimeout(resolve, 0))\n\n      const heights: number[] = []\n      const captureHeight = () => {\n        if (textareaRef) {\n          heights.push(textareaRef.getLayoutNode().getComputedHeight())\n        }\n      }\n\n      testSetup.renderer.addPostProcessFn(captureHeight)\n\n      testSetup.renderer.start()\n      await Bun.sleep(30)\n\n      heights.length = 0\n      await testSetup.mockInput.pasteBracketedText(\"Line 1\\nLine 2\\nLine 3\")\n\n      await Bun.sleep(200)\n      testSetup.renderer.pause()\n      await testSetup.renderer.idle()\n\n      testSetup.renderer.removePostProcessFn(captureHeight)\n\n      const uniqueHeights = new Set(heights)\n\n      expect(textareaRef?.plainText).toBe(\"Line 1\\nLine 2\\nLine 3\")\n      expect(heights.length).toBeGreaterThan(4)\n      expect(Math.max(...uniqueHeights)).toBeGreaterThan(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/destroy-crash.test.tsx",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { createTestRenderer } from \"@opentui/core/testing\"\nimport { For, createSignal, onCleanup, onMount } from \"solid-js\"\nimport { render } from \"../index\"\n\ndescribe(\"Renderer destroy with pending Solid updates\", () => {\n  it(\"disposes Solid root when renderer is destroyed externally\", async () => {\n    const testSetup = await createTestRenderer({\n      width: 40,\n      height: 20,\n    })\n\n    const startedAt = Date.now()\n    const log = (message: string) => {\n      const elapsed = Date.now() - startedAt\n      console.debug(`[solid-destroy-debug +${elapsed}ms] ${message}`)\n    }\n\n    let cleanupCalls = 0\n    let destroyEvents = 0\n    let intervalTicks = 0\n    let intervalTicksAfterDestroy = 0\n    let destroyCalled = false\n\n    testSetup.renderer.on(\"destroy\", () => {\n      destroyEvents += 1\n      log(`renderer destroy event fired (#${destroyEvents})`)\n    })\n\n    function App() {\n      const [lines, setLines] = createSignal<string[]>([])\n      let interval: ReturnType<typeof setInterval> | undefined\n\n      onMount(() => {\n        log(\"app mounted, starting interval\")\n        interval = setInterval(() => {\n          intervalTicks += 1\n          if (destroyCalled) {\n            intervalTicksAfterDestroy += 1\n          }\n\n          log(`interval tick #${intervalTicks} (destroyCalled=${destroyCalled})`)\n          setLines((prev) => [...prev, `Line ${prev.length + 1}`])\n        }, 5)\n      })\n\n      onCleanup(() => {\n        cleanupCalls += 1\n        log(`app cleanup called (#${cleanupCalls}), clearing interval`)\n\n        if (interval) {\n          clearInterval(interval)\n          interval = undefined\n        }\n      })\n\n      return (\n        <box flexDirection=\"column\" border borderStyle=\"single\">\n          <text bold>Solid destroy repro</text>\n          <For each={lines().slice(-10)}>{(line) => <text>{line}</text>}</For>\n        </box>\n      )\n    }\n\n    try {\n      await render(() => <App />, testSetup.renderer)\n\n      await testSetup.renderOnce()\n      await Bun.sleep(30)\n      await testSetup.renderOnce()\n\n      log(`ticks before destroy: ${intervalTicks}`)\n      destroyCalled = true\n      log(\"calling renderer.destroy()\")\n      testSetup.renderer.destroy()\n\n      await Bun.sleep(30)\n      const ticksSoonAfterDestroy = intervalTicks\n      log(`ticks soon after destroy: ${ticksSoonAfterDestroy}`)\n\n      await Bun.sleep(60)\n      const ticksLaterAfterDestroy = intervalTicks\n      log(`ticks later after destroy: ${ticksLaterAfterDestroy}`)\n\n      expect(destroyEvents).toBe(1)\n      expect(cleanupCalls).toBe(1)\n      expect(testSetup.renderer.isDestroyed).toBe(true)\n      expect(ticksLaterAfterDestroy).toBe(ticksSoonAfterDestroy)\n      expect(intervalTicksAfterDestroy).toBeLessThanOrEqual(1)\n    } finally {\n      if (!testSetup.renderer.isDestroyed) {\n        testSetup.renderer.destroy()\n      }\n    }\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/destroy-race-repro.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { join } from \"node:path\"\n\nconst fixturePath = join(import.meta.dir, \"destroy-race.fixture.tsx\")\n\ntype Mode = \"external\" | \"helper\" | \"external-onmount\" | \"helper-onmount\" | \"external-active\" | \"helper-active\"\n\nconst runFixture = (mode: Mode) => {\n  const result = Bun.spawnSync([process.execPath, fixturePath, mode], {\n    cwd: join(import.meta.dir, \"..\"),\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n    env: process.env,\n  })\n\n  const stdout = result.stdout.toString()\n  const stderr = result.stderr.toString()\n\n  console.debug(`[destroy-race-repro ${mode}] exit=${result.exitCode}`)\n  if (stdout.trim()) {\n    console.debug(`[destroy-race-repro ${mode}] stdout:\\n${stdout.trimEnd()}`)\n  }\n  if (stderr.trim()) {\n    console.debug(`[destroy-race-repro ${mode}] stderr:\\n${stderr.trimEnd()}`)\n  }\n\n  return { result, stdout, stderr }\n}\n\ndescribe(\"destroy race regressions\", () => {\n  it(\"does not crash when renderer is destroyed during initial render (external renderer path)\", () => {\n    const { result } = runFixture(\"external\")\n\n    expect(result.exitCode).toBe(0)\n  })\n\n  it(\"does not crash when renderer is destroyed during initial render (testRender helper path)\", () => {\n    const { result } = runFixture(\"helper\")\n\n    expect(result.exitCode).toBe(0)\n  })\n\n  it(\"does not crash when renderer is destroyed from onMount (external renderer path)\", () => {\n    const { result } = runFixture(\"external-onmount\")\n\n    expect(result.exitCode).toBe(0)\n  })\n\n  it(\"does not crash when renderer is destroyed from onMount (testRender helper path)\", () => {\n    const { result } = runFixture(\"helper-onmount\")\n\n    expect(result.exitCode).toBe(0)\n  })\n\n  it(\"does not crash when renderer is destroyed in an active render pass (external renderer path)\", () => {\n    const { result } = runFixture(\"external-active\")\n\n    expect(result.exitCode).toBe(0)\n  })\n\n  it(\"does not crash when renderer is destroyed in an active render pass (testRender helper path)\", () => {\n    const { result } = runFixture(\"helper-active\")\n\n    expect(result.exitCode).toBe(0)\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/destroy-race.fixture.tsx",
    "content": "import { createTestRenderer } from \"@opentui/core/testing\"\nimport { For, createSignal, onCleanup, onMount } from \"solid-js\"\nimport { render, testRender, useRenderer } from \"../index\"\n\ntype Mode = \"external\" | \"helper\" | \"external-onmount\" | \"helper-onmount\" | \"external-active\" | \"helper-active\"\n\nconst mode = process.argv[2] as Mode | undefined\nconst startedAt = Date.now()\n\nconst log = (message: string): void => {\n  const elapsed = Date.now() - startedAt\n  console.debug(`[solid-destroy-race-fixture ${mode ?? \"unknown\"} +${elapsed}ms] ${message}`)\n}\n\nlet didDestroy = false\n\nfunction InitialRaceApp() {\n  const renderer = useRenderer()\n  log(\"InitialRaceApp render started\")\n\n  if (!didDestroy) {\n    didDestroy = true\n    log(\"Destroying renderer from render body\")\n    renderer.destroy()\n    log(\"renderer.destroy() returned\")\n  }\n\n  return <text>race repro</text>\n}\n\nfunction OnMountRaceApp() {\n  const renderer = useRenderer()\n\n  onMount(() => {\n    log(\"OnMountRaceApp mounted, destroying renderer\")\n    renderer.destroy()\n    log(\"renderer.destroy() returned\")\n  })\n\n  return <text>onmount race repro</text>\n}\n\nlet appendLine: ((line: string) => void) | undefined\n\nfunction ActivePassApp() {\n  const [lines, setLines] = createSignal<string[]>([])\n\n  appendLine = (line: string) => {\n    setLines((prev) => [...prev, line])\n  }\n\n  onMount(() => {\n    log(\"ActivePassApp mounted\")\n  })\n\n  onCleanup(() => {\n    log(\"ActivePassApp cleanup\")\n    appendLine = undefined\n  })\n\n  return (\n    <box flexDirection=\"column\" border borderStyle=\"single\">\n      <text>active pass race repro</text>\n      <For each={lines().slice(-5)}>{(line) => <text>{line}</text>}</For>\n    </box>\n  )\n}\n\nasync function runActivePassScenario(withTestRender: boolean): Promise<void> {\n  const testSetup = withTestRender\n    ? await testRender(() => <ActivePassApp />, { width: 40, height: 12 })\n    : await createTestRenderer({ width: 40, height: 12 })\n\n  if (!withTestRender) {\n    await render(() => <ActivePassApp />, testSetup.renderer)\n  }\n\n  let frame = 0\n  testSetup.renderer.setFrameCallback(async () => {\n    frame += 1\n    log(\n      `frame callback #${frame}, rendering=${String((testSetup.renderer as any).rendering)}, destroyed=${testSetup.renderer.isDestroyed}`,\n    )\n\n    if (frame === 1) {\n      appendLine?.(\"frame-1\")\n      return\n    }\n\n    if (frame === 2) {\n      log(\"Destroying renderer from frame callback (active render pass)\")\n      testSetup.renderer.destroy()\n      log(`renderer.destroy() returned (destroyed=${testSetup.renderer.isDestroyed})`)\n\n      appendLine?.(\"after-destroy-sync\")\n      queueMicrotask(() => {\n        log(\"queueMicrotask update firing after destroy request\")\n        appendLine?.(\"after-destroy-microtask\")\n      })\n      return\n    }\n\n    appendLine?.(`frame-${frame}`)\n  })\n\n  try {\n    await testSetup.renderOnce()\n    await testSetup.renderOnce()\n    await testSetup.renderOnce()\n    await Bun.sleep(30)\n    await testSetup.renderOnce()\n\n    log(\"Active pass scenario completed renderOnce calls\")\n  } finally {\n    if (!testSetup.renderer.isDestroyed) {\n      testSetup.renderer.destroy()\n    }\n  }\n}\n\nif (mode === \"external\") {\n  const testSetup = await createTestRenderer({ width: 30, height: 10 })\n  await render(() => <InitialRaceApp />, testSetup.renderer)\n  await Bun.sleep(10)\n\n  if (!testSetup.renderer.isDestroyed) {\n    testSetup.renderer.destroy()\n  }\n\n  log(\"External initial mode completed\")\n} else if (mode === \"helper\") {\n  const testSetup = await testRender(() => <InitialRaceApp />, { width: 30, height: 10 })\n\n  if (!testSetup.renderer.isDestroyed) {\n    await testSetup.renderOnce()\n  }\n\n  await Bun.sleep(10)\n\n  if (!testSetup.renderer.isDestroyed) {\n    testSetup.renderer.destroy()\n  }\n\n  log(\"Helper initial mode completed\")\n} else if (mode === \"external-onmount\") {\n  const testSetup = await createTestRenderer({ width: 30, height: 10 })\n  await render(() => <OnMountRaceApp />, testSetup.renderer)\n  await Bun.sleep(10)\n\n  if (!testSetup.renderer.isDestroyed) {\n    testSetup.renderer.destroy()\n  }\n\n  log(\"External onMount mode completed\")\n} else if (mode === \"helper-onmount\") {\n  const testSetup = await testRender(() => <OnMountRaceApp />, { width: 30, height: 10 })\n\n  if (!testSetup.renderer.isDestroyed) {\n    await testSetup.renderOnce()\n  }\n\n  await Bun.sleep(10)\n\n  if (!testSetup.renderer.isDestroyed) {\n    testSetup.renderer.destroy()\n  }\n\n  log(\"Helper onMount mode completed\")\n} else if (mode === \"external-active\") {\n  await runActivePassScenario(false)\n  log(\"External active mode completed\")\n} else if (mode === \"helper-active\") {\n  await runActivePassScenario(true)\n  log(\"Helper active mode completed\")\n} else {\n  throw new Error(`Unknown mode: ${String(mode)}`)\n}\n"
  },
  {
    "path": "packages/solid/tests/diff.test.tsx",
    "content": "import { describe, test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { SyntaxStyle, RGBA } from \"@opentui/core\"\nimport { createSignal, Show } from \"solid-js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"DiffRenderable with SolidJS\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  test(\"renders unified diff without glitching\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      keyword: { fg: RGBA.fromValues(0.78, 0.57, 0.92, 1) },\n      function: { fg: RGBA.fromValues(0.51, 0.67, 1, 1) },\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const diffContent = `--- a/test.js\n+++ b/test.js\n@@ -1,7 +1,11 @@\n function add(a, b) {\n   return a + b;\n }\n \n+function subtract(a, b) {\n+  return a - b;\n+}\n+\n function multiply(a, b) {\n-  return a * b;\n+  return a * b * 1;\n }`\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <diff\n          id=\"test-diff\"\n          diff={diffContent}\n          view=\"unified\"\n          filetype=\"javascript\"\n          syntaxStyle={syntaxStyle}\n          showLineNumbers={true}\n          width=\"100%\"\n          height=\"100%\"\n        />\n      </box>\n    ))\n\n    // Wait for automatic initial render\n    await Bun.sleep(50)\n\n    const boxRenderable = testSetup.renderer.root.getRenderable(\"root\")\n    const diffRenderable = boxRenderable?.getRenderable(\"test-diff\") as any\n    const leftSide = diffRenderable?.getRenderable(\"test-diff-left\") as any\n    const gutterAfterAutoRender = leftSide?.[\"gutter\"]\n    const widthAfterAutoRender = gutterAfterAutoRender?.width\n\n    // First explicit render\n    await testSetup.renderOnce()\n    const firstFrame = testSetup.captureCharFrame()\n    const widthAfterFirst = leftSide?.[\"gutter\"]?.width\n\n    // Second render to check stability\n    await testSetup.renderOnce()\n    const secondFrame = testSetup.captureCharFrame()\n    const widthAfterSecond = leftSide?.[\"gutter\"]?.width\n\n    // EXPECTATION: No width glitch - width should be correct from auto render\n    expect(widthAfterAutoRender).toBeDefined()\n    expect(widthAfterFirst).toBeDefined()\n    expect(widthAfterSecond).toBeDefined()\n    expect(widthAfterAutoRender).toBe(widthAfterFirst)\n    expect(widthAfterFirst).toBe(widthAfterSecond)\n    expect(widthAfterFirst!).toBeGreaterThan(0)\n\n    // Frames should be identical (no visual changes)\n    expect(firstFrame).toBe(secondFrame)\n\n    // Check content is present\n    expect(firstFrame).toContain(\"function add\")\n    expect(firstFrame).toContain(\"function subtract\")\n    expect(firstFrame).toContain(\"function multiply\")\n\n    // Check for diff markers\n    expect(firstFrame).toContain(\"+\")\n    expect(firstFrame).toContain(\"-\")\n  })\n\n  test(\"renders split diff correctly\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      keyword: { fg: RGBA.fromValues(0.78, 0.57, 0.92, 1) },\n      function: { fg: RGBA.fromValues(0.51, 0.67, 1, 1) },\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const diffContent = `--- a/test.js\n+++ b/test.js\n@@ -1,3 +1,3 @@\n function hello() {\n-  console.log(\"Hello\");\n+  console.log(\"Hello, World!\");\n }`\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <diff\n          id=\"test-diff\"\n          diff={diffContent}\n          view=\"split\"\n          filetype=\"javascript\"\n          syntaxStyle={syntaxStyle}\n          showLineNumbers={true}\n          width=\"100%\"\n          height=\"100%\"\n        />\n      </box>\n    ))\n\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n\n    // Both sides should be visible\n    expect(frame).toContain(\"function hello\")\n    expect(frame).toContain(\"console.log\")\n    expect(frame).toContain(\"Hello\")\n  })\n\n  test(\"handles double-digit line numbers with proper left padding\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      keyword: { fg: RGBA.fromValues(0.78, 0.57, 0.92, 1) },\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const diffWith10PlusLines = `--- a/test.js\n+++ b/test.js\n@@ -8,10 +8,12 @@\n line8\n line9\n line10\n-line11_old\n+line11_new\n line12\n+line13_added\n+line14_added\n line15\n line16\n-line17_old\n+line17_new\n line18\n line19`\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <diff\n          id=\"test-diff\"\n          diff={diffWith10PlusLines}\n          view=\"unified\"\n          syntaxStyle={syntaxStyle}\n          showLineNumbers={true}\n          width=\"100%\"\n          height=\"100%\"\n        />\n      </box>\n    ))\n\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n    const frameLines = frame.split(\"\\n\")\n\n    // Find lines with single and double digit numbers\n    const line8 = frameLines.find((l) => l.includes(\"line8\"))\n    const line10 = frameLines.find((l) => l.includes(\"line10\"))\n    const line16 = frameLines.find((l) => l.includes(\"line16\"))\n\n    // All lines should have proper left padding\n    if (!line8 || !line10 || !line16) {\n      throw new Error(\"Expected lines not found in output\")\n    }\n\n    // Verify proper left padding for single-digit line numbers\n    const line8Match = line8.match(/^( +)\\d+ /)\n    if (!line8Match || !line8Match[1]) throw new Error(\"Line 8 format incorrect\")\n    expect(line8Match[1].length).toBeGreaterThanOrEqual(1)\n\n    // Verify proper left padding for double-digit line numbers (line10)\n    const line10Match = line10.match(/^( +)\\d+ /)\n    if (!line10Match || !line10Match[1]) throw new Error(\"Line 10 format incorrect\")\n    expect(line10Match[1].length).toBeGreaterThanOrEqual(1)\n\n    // Verify proper left padding for double-digit line numbers (line16)\n    // Note: In unified diff, removed lines show old file line numbers, added lines show new file line numbers\n    const line16Match = line16.match(/^( +)\\d+ /)\n    if (!line16Match || !line16Match[1]) throw new Error(\"Line 16 format incorrect\")\n    expect(line16Match[1].length).toBeGreaterThanOrEqual(1)\n  })\n\n  test(\"handles conditional removal of diff element\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      keyword: { fg: RGBA.fromValues(0.78, 0.57, 0.92, 1) },\n      function: { fg: RGBA.fromValues(0.51, 0.67, 1, 1) },\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const diffContent = `--- a/test.js\n+++ b/test.js\n@@ -1,7 +1,11 @@\n function add(a, b) {\n   return a + b;\n }\n \n+function subtract(a, b) {\n+  return a - b;\n+}\n+\n function multiply(a, b) {\n-  return a * b;\n+  return a * b * 1;\n }`\n\n    const [showDiff, setShowDiff] = createSignal(true)\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <Show\n          when={showDiff()}\n          fallback={\n            <text id=\"fallback-text\" width=\"100%\" height=\"100%\">\n              No diff to display\n            </text>\n          }\n        >\n          <diff\n            id=\"test-diff\"\n            diff={diffContent}\n            view=\"unified\"\n            filetype=\"javascript\"\n            syntaxStyle={syntaxStyle}\n            showLineNumbers={true}\n            width=\"100%\"\n            height=\"100%\"\n          />\n        </Show>\n      </box>\n    ))\n\n    await testSetup.renderOnce()\n\n    let frame = testSetup.captureCharFrame()\n\n    // Initially shows diff content\n    expect(frame).toContain(\"function add\")\n    expect(frame).toContain(\"function subtract\")\n    expect(frame).toContain(\"+\")\n    expect(frame).toContain(\"-\")\n\n    // Toggle to hide diff - this should trigger destruction of DiffRenderable\n    setShowDiff(false)\n    await testSetup.renderOnce()\n\n    frame = testSetup.captureCharFrame()\n\n    // Should show fallback text\n    expect(frame).toContain(\"No diff to display\")\n    // Diff content should not be present\n    expect(frame).not.toContain(\"function add\")\n    expect(frame).not.toContain(\"function subtract\")\n\n    // Toggle back to show diff - this should create a new DiffRenderable\n    setShowDiff(true)\n    await testSetup.renderOnce()\n\n    frame = testSetup.captureCharFrame()\n\n    // Diff should be visible again\n    expect(frame).toContain(\"function add\")\n    expect(frame).toContain(\"function subtract\")\n  })\n\n  test(\"handles conditional removal of split diff element\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      keyword: { fg: RGBA.fromValues(0.78, 0.57, 0.92, 1) },\n      function: { fg: RGBA.fromValues(0.51, 0.67, 1, 1) },\n      default: { fg: RGBA.fromValues(1, 1, 1, 1) },\n    })\n\n    const diffContent = `--- a/test.js\n+++ b/test.js\n@@ -1,3 +1,3 @@\n function hello() {\n-  console.log(\"Hello\");\n+  console.log(\"Hello, World!\");\n }`\n\n    const [showDiff, setShowDiff] = createSignal(true)\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <Show\n          when={showDiff()}\n          fallback={\n            <text id=\"fallback-text\" width=\"100%\" height=\"100%\">\n              No diff to display\n            </text>\n          }\n        >\n          <diff\n            id=\"test-diff\"\n            diff={diffContent}\n            view=\"split\"\n            filetype=\"javascript\"\n            syntaxStyle={syntaxStyle}\n            showLineNumbers={true}\n            width=\"100%\"\n            height=\"100%\"\n          />\n        </Show>\n      </box>\n    ))\n\n    await testSetup.renderOnce()\n\n    let frame = testSetup.captureCharFrame()\n\n    // Initially shows diff content in split view\n    expect(frame).toContain(\"function hello\")\n    expect(frame).toContain(\"console.log\")\n\n    // Toggle to hide diff - this should trigger destruction of DiffRenderable with split view\n    setShowDiff(false)\n    await testSetup.renderOnce()\n\n    frame = testSetup.captureCharFrame()\n\n    // Should show fallback text\n    expect(frame).toContain(\"No diff to display\")\n    // Diff content should not be present\n    expect(frame).not.toContain(\"function hello\")\n\n    // Toggle back to show diff - this should create a new DiffRenderable\n    setShowDiff(true)\n    await testSetup.renderOnce()\n\n    frame = testSetup.captureCharFrame()\n\n    // Diff should be visible again\n    expect(frame).toContain(\"function hello\")\n  })\n\n  test(\"split diff with word wrapping: toggling vs setting from start should match\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      keyword: { fg: RGBA.fromValues(0.78, 0.57, 0.92, 1) },\n      \"keyword.import\": { fg: RGBA.fromValues(0.78, 0.57, 0.92, 1) },\n      string: { fg: RGBA.fromValues(0.65, 0.84, 1, 1) },\n      comment: { fg: RGBA.fromValues(0.55, 0.58, 0.62, 1), italic: true },\n      function: { fg: RGBA.fromValues(0.51, 0.67, 1, 1) },\n      default: { fg: RGBA.fromValues(0.9, 0.93, 0.95, 1) },\n    })\n\n    // Use the actual diff content from the demo\n    const diffContent = `Index: packages/core/src/examples/index.ts\n===================================================================\n--- packages/core/src/examples/index.ts\tbefore\n+++ packages/core/src/examples/index.ts\tafter\n@@ -56,6 +56,7 @@\n import * as terminalDemo from \"./terminal\"\n import * as diffDemo from \"./diff-demo\"\n import * as keypressDebugDemo from \"./keypress-debug-demo\"\n+import * as textTruncationDemo from \"./text-truncation-demo\"\n import { setupCommonDemoKeys } from \"./lib/standalone-keys\"\n \n interface Example {\n@@ -85,6 +86,12 @@\n     destroy: textSelectionExample.destroy,\n   },\n   {\n+    name: \"Text Truncation Demo\",\n+    description: \"Middle truncation with ellipsis - toggle with 'T' key and resize to test responsive behavior\",\n+    run: textTruncationDemo.run,\n+    destroy: textTruncationDemo.destroy,\n+  },\n+  {\n     name: \"ASCII Font Selection Demo\",\n     description: \"Text selection with ASCII fonts - precise character-level selection across different font types\",\n     run: asciiFontSelectionExample.run,`\n\n    const [wrapMode, setWrapMode] = createSignal<\"none\" | \"word\">(\"none\")\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <diff\n          id=\"test-diff-toggle\"\n          diff={diffContent}\n          view=\"split\"\n          filetype=\"typescript\"\n          syntaxStyle={syntaxStyle}\n          showLineNumbers={true}\n          wrapMode={wrapMode()}\n          width=\"100%\"\n          height=\"100%\"\n        />\n      </box>\n    ))\n\n    await testSetup.renderOnce()\n    setWrapMode(\"word\")\n    await Bun.sleep(10)\n    await testSetup.renderer.idle()\n\n    const frameAfterToggle = testSetup.captureCharFrame()\n\n    testSetup.renderer.destroy()\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <diff\n          id=\"test-diff-from-start\"\n          diff={diffContent}\n          view=\"split\"\n          filetype=\"typescript\"\n          syntaxStyle={syntaxStyle}\n          showLineNumbers={true}\n          wrapMode=\"word\"\n          width=\"100%\"\n          height=\"100%\"\n        />\n      </box>\n    ))\n\n    await Bun.sleep(10)\n    await testSetup.renderer.idle()\n\n    const frameFromStart = testSetup.captureCharFrame()\n\n    expect(frameAfterToggle).toBe(frameFromStart)\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/dynamic-collections.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal } from \"solid-js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"SolidJS Renderer - Dynamic Collections\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  describe(\"Basic Array Operations\", () => {\n    it(\"should render initial array items correctly\", async () => {\n      const items = [\"Item 1\", \"Item 2\", \"Item 3\"]\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items.map((item, index) => (\n              <text>{item}</text>\n            ))}\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n\n      expect(frame).toContain(\"Item 1\")\n      expect(frame).toContain(\"Item 2\")\n      expect(frame).toContain(\"Item 3\")\n\n      const children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(3)\n    })\n\n    it(\"should handle adding items to array\", async () => {\n      const [items, setItems] = createSignal([\"Item 1\", \"Item 2\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items().map((item, index) => (\n              <text>{item}</text>\n            ))}\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      setItems([\"Item 1\", \"Item 2\", \"Item 3\"])\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(3)\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Item 3\")\n    })\n\n    it(\"should handle removing items from array\", async () => {\n      const [items, setItems] = createSignal([\"Item 1\", \"Item 2\", \"Item 3\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items().map((item, index) => (\n              <text>{item}</text>\n            ))}\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(3)\n\n      setItems([\"Item 1\", \"Item 3\"])\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Item 1\")\n      expect(frame).toContain(\"Item 3\")\n      expect(frame).not.toContain(\"Item 2\")\n    })\n\n    it(\"should handle updating specific array items\", async () => {\n      const [items, setItems] = createSignal([\"First\", \"Second\", \"Third\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items().map((item, index) => (\n              <text>{item}</text>\n            ))}\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Second\")\n\n      setItems([\"First\", \"Updated\", \"Third\"])\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Updated\")\n      expect(frame).not.toContain(\"Second\")\n    })\n\n    it(\"should handle empty array\", async () => {\n      const [items, setItems] = createSignal([\"Item 1\", \"Item 2\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items().map((item, index) => (\n              <text>{item}</text>\n            ))}\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      setItems([])\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(0)\n    })\n  })\n\n  describe(\"Reactive Collection Updates\", () => {\n    it(\"should handle reactive signal updates to collections\", async () => {\n      const [count, setCount] = createSignal(3)\n      const items = () => Array.from({ length: count() }, (_, i) => `Item ${i + 1}`)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items().map((item, index) => (\n              <text>{item}</text>\n            ))}\n          </box>\n        ),\n        { width: 20, height: 15 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(3)\n\n      setCount(5)\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(5)\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Item 5\")\n    })\n\n    it(\"should handle complex object collections\", async () => {\n      const [todos, setTodos] = createSignal([\n        { id: 1, text: \"Learn SolidJS\", completed: false },\n        { id: 2, text: \"Build TUI\", completed: true },\n      ])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {todos().map((todo) => (\n              <text>\n                {todo.completed ? \"✓\" : \"○\"} {todo.text}\n              </text>\n            ))}\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"✓ Build TUI\")\n      expect(frame).toContain(\"○ Learn SolidJS\")\n\n      setTodos([\n        { id: 1, text: \"Learn SolidJS\", completed: true },\n        { id: 2, text: \"Build TUI\", completed: true },\n        { id: 3, text: \"Write Tests\", completed: false },\n      ])\n      await testSetup.renderOnce()\n\n      const updatedFrame = testSetup.captureCharFrame()\n      expect(updatedFrame).toContain(\"✓ Learn SolidJS\")\n      expect(updatedFrame).toContain(\"Write Tests\")\n    })\n\n    it(\"should handle collection with conditional rendering\", async () => {\n      const [items, setItems] = createSignal([1, 2, 3, 4, 5])\n      const [showEven, setShowEven] = createSignal(false)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items()\n              .filter((item) => !showEven() || item % 2 === 0)\n              .map((item, index) => (\n                <text>Number: {item}</text>\n              ))}\n          </box>\n        ),\n        { width: 20, height: 15 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(5)\n\n      setShowEven(true)\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2) // Only even numbers: 2, 4\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Number: 2\")\n      expect(frame).toContain(\"Number: 4\")\n      expect(frame).not.toContain(\"Number: 1\")\n    })\n  })\n\n  describe(\"Nested Dynamic Collections\", () => {\n    it(\"should handle nested arrays\", async () => {\n      const [matrix, setMatrix] = createSignal([\n        [1, 2],\n        [3, 4],\n        [5, 6],\n      ])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {matrix().map((row, rowIndex) => (\n              <box>\n                {row.map((cell, cellIndex) => (\n                  <text>{cell}</text>\n                ))}\n              </box>\n            ))}\n          </box>\n        ),\n        { width: 20, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      let rootChildren = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(rootChildren.length).toBe(3) // 3 rows\n\n      // Each row should have 2 children\n      rootChildren.forEach((row) => {\n        expect(row.getChildren().length).toBe(2)\n      })\n\n      setMatrix([\n        [1, 2, 3],\n        [4, 5, 6],\n      ])\n      await testSetup.renderOnce()\n\n      rootChildren = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(rootChildren.length).toBe(2) // 2 rows\n\n      rootChildren.forEach((row) => {\n        expect(row.getChildren().length).toBe(3) // 3 columns\n      })\n    })\n\n    it(\"should handle tree-like structures\", async () => {\n      const [tree, setTree] = createSignal([\n        {\n          name: \"Root 1\",\n          children: [{ name: \"Child 1.1\" }, { name: \"Child 1.2\" }],\n        },\n        {\n          name: \"Root 2\",\n          children: [{ name: \"Child 2.1\" }],\n        },\n      ])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {tree().map((node, index) => (\n              <box>\n                <text>{node.name}</text>\n                {node.children.map((child, childIndex) => (\n                  <text> └─ {child.name}</text>\n                ))}\n              </box>\n            ))}\n          </box>\n        ),\n        { width: 30, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Root 1\")\n      expect(frame).toContain(\"└─ Child 1.1\")\n      expect(frame).toContain(\"└─ Child 1.2\")\n      expect(frame).toContain(\"Root 2\")\n      expect(frame).toContain(\"└─ Child 2.1\")\n    })\n  })\n\n  describe(\"Edge Cases\", () => {\n    it(\"should handle collections with null/undefined values\", async () => {\n      const [items, setItems] = createSignal([\"Valid\", null, \"Another\", undefined, \"Last\"])\n\n      testSetup = await testRender(\n        () => <box>{items().map((item, index) => (item ? <text>{item}</text> : <text>[null]</text>))}</box>,\n        { width: 20, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(5)\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Valid\")\n      expect(frame).toContain(\"[null]\")\n      expect(frame).toContain(\"Another\")\n      expect(frame).toContain(\"Last\")\n    })\n\n    it(\"should handle rapid collection updates\", async () => {\n      const [items, setItems] = createSignal([\"Initial\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items().map((item, index) => (\n              <text>{item}</text>\n            ))}\n          </box>\n        ),\n        { width: 10, height: 3 },\n      )\n\n      await testSetup.renderOnce()\n\n      // Rapid updates\n      setItems([\"First\"])\n      setItems([\"First\", \"Second\"])\n      setItems([\"First\", \"Second\", \"Third\"])\n      setItems([\"First\", \"Second\"]) // Remove one\n      setItems([\"First\", \"Second\", \"Fourth\"]) // Update last\n\n      await testSetup.renderOnce()\n\n      const children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(3)\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"First\")\n      expect(frame).toContain(\"Second\")\n      expect(frame).toContain(\"Fourth\")\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle collections with mixed component types\", async () => {\n      const [items, setItems] = createSignal([\n        { type: \"text\", content: \"First text\" },\n        { type: \"text\", content: \"Second text\" },\n        { type: \"box\", title: \"Container\" },\n      ])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items().map((item, index) => {\n              switch (item.type) {\n                case \"text\":\n                  return <text>{item.content}</text>\n                case \"box\":\n                  return (\n                    <box title={item.title}>\n                      <text>Box content</text>\n                    </box>\n                  )\n                default:\n                  return null\n              }\n            })}\n          </box>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(3)\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"First text\")\n      expect(frame).toContain(\"Second text\")\n      expect(frame).toContain(\"Box content\")\n    })\n  })\n\n  describe(\"Collection Transformations\", () => {\n    it(\"should handle sorting collections\", async () => {\n      const [items, setItems] = createSignal([3, 1, 4, 1, 5])\n      const [sortOrder, setSortOrder] = createSignal<\"asc\" | \"desc\">(\"asc\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items()\n              .slice()\n              .sort((a, b) => (sortOrder() === \"asc\" ? a - b : b - a))\n              .map((item, index) => (\n                <text>Number: {item}</text>\n              ))}\n          </box>\n        ),\n        { width: 10, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n\n      setSortOrder(\"desc\")\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle filtering collections\", async () => {\n      const [items, setItems] = createSignal([\n        { name: \"Apple\", category: \"fruit\" },\n        { name: \"Carrot\", category: \"vegetable\" },\n        { name: \"Banana\", category: \"fruit\" },\n        { name: \"Broccoli\", category: \"vegetable\" },\n      ])\n      const [filter, setFilter] = createSignal<string>(\"all\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items()\n              .filter((item) => filter() === \"all\" || item.category === filter())\n              .map((item, index) => (\n                <text>\n                  {item.name} ({item.category})\n                </text>\n              ))}\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(4)\n\n      setFilter(\"fruit\")\n      await testSetup.renderOnce()\n\n      children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(2)\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Apple\")\n      expect(frame).toContain(\"Banana\")\n      expect(frame).not.toContain(\"Carrot\")\n      expect(frame).toMatchSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/dynamic-portal.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender, Dynamic, Portal } from \"../index.js\"\nimport { createSignal, Show } from \"solid-js\"\nimport { createSpy } from \"@opentui/core/testing\"\nimport type { BoxRenderable } from \"@opentui/core\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"SolidJS Renderer - Dynamic and Portal Components\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  describe(\"<Dynamic> Component\", () => {\n    it(\"should handle undefined component gracefully\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Dynamic component={undefined}>This should not render</Dynamic>\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      const children = testSetup.renderer.root.getChildren()[0]!.getChildren()\n      expect(children.length).toBe(0)\n    })\n\n    it(\"should pass props correctly to dynamic components\", async () => {\n      const [color, setColor] = createSignal(\"red\")\n      const [text, setText] = createSignal(\"Initial text\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Dynamic component=\"text\" style={{ fg: color() }}>\n              {text()}\n            </Dynamic>\n          </box>\n        ),\n        { width: 20, height: 3 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Initial text\")\n\n      setColor(\"blue\")\n      setText(\"Updated text\")\n      await testSetup.renderOnce()\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Updated text\")\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle event handlers in dynamic components\", async () => {\n      const onInputSpy = createSpy()\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Dynamic component=\"input\" focused={true} onInput={onInputSpy} />\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.mockInput.typeText(\"test\")\n\n      expect(onInputSpy.callCount()).toBe(4)\n      expect(onInputSpy.calls[0]?.[0]).toBe(\"t\")\n      expect(onInputSpy.calls[3]?.[0]).toBe(\"test\")\n    })\n\n    it(\"should handle false Show inside dynamic that switches between text and box\", async () => {\n      /* Tests for slot renderable being able handle switching between a LayoutSlot and a TextSlot\n       * Expected to just run without crash\n       */\n      const [componentType, setComponentType] = createSignal<\"text\" | \"box\">(\"text\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Dynamic component={componentType()}>\n              <Show when={false}>This should never render</Show>\n            </Dynamic>\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n\n      setComponentType(\"box\")\n      await testSetup.renderOnce()\n    })\n  })\n\n  describe(\"<Portal> Component\", () => {\n    it(\"should render content to default mount point\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box>\n            <text>Before portal</text>\n            <Portal>\n              <text>Portal content</text>\n            </Portal>\n            <text>After portal</text>\n          </box>\n        ),\n        { width: 25, height: 8 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Portal content\")\n    })\n\n    it(\"should render content to custom mount point\", async () => {\n      let customMount!: BoxRenderable\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <box ref={customMount} />\n            <Portal mount={customMount}>\n              <box style={{ border: true }} title=\"Portal Box\">\n                <text>Portal content</text>\n              </box>\n            </Portal>\n          </box>\n        ),\n        { width: 25, height: 8 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Portal content\")\n      expect(customMount.getChildren().length).toBe(1)\n    })\n\n    it(\"should handle complex nested content in portal\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Portal>\n              <text>Nested text 1</text>\n              <text>Nested text 2</text>\n            </Portal>\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Nested text 1\")\n      expect(frame).toContain(\"Nested text 2\")\n    })\n\n    it(\"should handle portal cleanup on unmount\", async () => {\n      const [showPortal, setShowPortal] = createSignal(true)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Show when={showPortal()}>\n              <Portal>\n                <text>Portal content</text>\n              </Portal>\n            </Show>\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Portal content\")\n\n      setShowPortal(false)\n      await testSetup.renderOnce()\n      frame = testSetup.captureCharFrame()\n      expect(frame).not.toContain(\"Portal content\")\n    })\n\n    it(\"should handle multiple portals\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box>\n            <Portal>\n              <text>First portal</text>\n            </Portal>\n            <Portal>\n              <text>Second portal</text>\n            </Portal>\n          </box>\n        ),\n        { width: 25, height: 8 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"First portal\")\n      expect(frame).toContain(\"Second portal\")\n      expect(testSetup.renderer.root.getChildren().length).toBe(3)\n    })\n  })\n\n  describe(\"<Dynamic> + <Portal> Integration\", () => {\n    it(\"should handle Dynamic component inside Portal\", async () => {\n      const [useComponentA, setUseComponentA] = createSignal(true)\n\n      testSetup = await testRender(\n        () => {\n          const ComponentA = () => <text>Portal Component A</text>\n          const ComponentB = () => <text>Portal Component B</text>\n\n          return (\n            <box>\n              <Portal>\n                <Dynamic component={useComponentA() ? ComponentA : ComponentB} />\n              </Portal>\n            </box>\n          )\n        },\n        { width: 25, height: 8 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Portal Component A\")\n\n      setUseComponentA(false)\n      await testSetup.renderOnce()\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Portal Component B\")\n    })\n\n    it(\"should handle Portal with Dynamic mount point\", async () => {\n      const [useCustomMount, setUseCustomMount] = createSignal(false)\n\n      let ref!: BoxRenderable\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <box ref={ref}>\n              <text>Custom target</text>\n            </box>\n            <Portal mount={useCustomMount() ? ref : undefined}>\n              <text>Dynamic mount content</text>\n            </Portal>\n            <text>Static content</text>\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Dynamic mount content\")\n      expect(ref.getChildren().length).toBe(1)\n\n      setUseCustomMount(true)\n      await testSetup.renderOnce()\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Dynamic mount content\")\n      expect(ref.getChildren().length).toBe(2)\n\n      setUseCustomMount(false)\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Dynamic mount content\")\n      expect(ref.getChildren().length).toBe(1)\n    })\n\n    it(\"should handle switching between Portal and non-Portal with Dynamic\", async () => {\n      const [usePortal, setUsePortal] = createSignal(true)\n\n      let ref!: BoxRenderable\n\n      testSetup = await testRender(\n        () => (\n          <box ref={ref}>\n            <Dynamic component={usePortal() ? Portal : \"box\"} {...(usePortal() ? {} : { style: { border: true } })}>\n              <text>Conditional portal content</text>\n            </Dynamic>\n          </box>\n        ),\n        { width: 30, height: 8 },\n      )\n\n      await testSetup.renderOnce()\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Conditional portal content\")\n      expect(testSetup.renderer.root.getChildren().length).toBe(2)\n\n      setUsePortal(false)\n      await testSetup.renderOnce()\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Conditional portal content\")\n      expect(testSetup.renderer.root.getChildren().length).toBe(1)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/events.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal } from \"solid-js\"\nimport { decodePasteBytes } from \"@opentui/core\"\nimport { createSpy } from \"@opentui/core/testing\"\nimport { onBlur, onFocus, usePaste, useKeyboard } from \"../src/elements/hooks.js\"\nimport type { PasteEvent } from \"@opentui/core\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"SolidJS Renderer Integration Tests\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  describe(\"Event Scenarios\", () => {\n    it(\"should handle input onInput events\", async () => {\n      const onInputSpy = createSpy()\n      const [value, setValue] = createSignal(\"\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <input\n              focused\n              onInput={(val) => {\n                onInputSpy(val)\n                setValue(val)\n              }}\n            />\n            <text>Value: {value()}</text>\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.mockInput.typeText(\"hello\")\n\n      expect(onInputSpy.callCount()).toBe(5)\n      expect(onInputSpy.calls[0]?.[0]).toBe(\"h\")\n      expect(onInputSpy.calls[4]?.[0]).toBe(\"hello\")\n\n      expect(value()).toBe(\"hello\")\n    })\n\n    it(\"should handle input onSubmit events\", async () => {\n      const onSubmitSpy = createSpy()\n      const [submittedValue, setSubmittedValue] = createSignal(\"\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <input focused onInput={(val) => setSubmittedValue(val)} onSubmit={(val) => onSubmitSpy(val)} />\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.mockInput.typeText(\"test input\")\n\n      testSetup.mockInput.pressEnter()\n\n      expect(onSubmitSpy.callCount()).toBe(1)\n      expect(onSubmitSpy.calls[0]?.[0]).toBe(\"test input\")\n      expect(submittedValue()).toBe(\"test input\")\n    })\n\n    it(\"should handle select onChange events\", async () => {\n      const onChangeSpy = createSpy()\n      const [selectedIndex, setSelectedIndex] = createSignal(0)\n\n      const options = [\n        { name: \"Option 1\", value: 1, description: \"First option\" },\n        { name: \"Option 2\", value: 2, description: \"Second option\" },\n        { name: \"Option 3\", value: 3, description: \"Third option\" },\n      ]\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <select\n              focused\n              options={options}\n              onChange={(index, option) => {\n                onChangeSpy(index, option)\n                setSelectedIndex(index)\n              }}\n            />\n            <text>Selected: {selectedIndex()}</text>\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      testSetup.mockInput.pressArrow(\"down\")\n\n      expect(onChangeSpy.callCount()).toBe(1)\n      expect(onChangeSpy.calls[0]?.[0]).toBe(1)\n      expect(onChangeSpy.calls[0]?.[1]).toEqual(options[1])\n\n      expect(selectedIndex()).toBe(1)\n    })\n\n    it(\"should handle tab_select onSelect events\", async () => {\n      const onSelectSpy = createSpy()\n      const [activeTab, setActiveTab] = createSignal(0)\n\n      const tabs = [{ title: \"Tab 1\" }, { title: \"Tab 2\" }, { title: \"Tab 3\" }]\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <tab_select\n              focused\n              options={tabs.map((tab, index) => ({\n                name: tab.title,\n                value: index,\n                description: \"\",\n              }))}\n              onSelect={(index) => {\n                onSelectSpy(index)\n                setActiveTab(index)\n              }}\n            />\n            <text>Active tab: {activeTab()}</text>\n          </box>\n        ),\n        { width: 40, height: 8 },\n      )\n\n      testSetup.mockInput.pressArrow(\"right\")\n      testSetup.mockInput.pressArrow(\"right\")\n\n      testSetup.mockInput.pressEnter()\n\n      expect(onSelectSpy.callCount()).toBe(1)\n      expect(onSelectSpy.calls[0]?.[0]).toBe(2)\n\n      expect(activeTab()).toBe(2)\n    })\n\n    it(\"should handle focus management\", async () => {\n      const input1Spy = createSpy()\n      const input2Spy = createSpy()\n      const [input1Focused, setInput1Focused] = createSignal(true)\n      const [input2Focused, setInput2Focused] = createSignal(false)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <input focused={input1Focused()} onInput={input1Spy} />\n            <input focused={input2Focused()} onInput={input2Spy} />\n          </box>\n        ),\n        { width: 30, height: 8 },\n      )\n\n      await testSetup.mockInput.typeText(\"first\")\n\n      expect(input1Spy.callCount()).toBe(5) // \"f\", \"i\", \"r\", \"s\", \"t\"\n      expect(input2Spy.callCount()).toBe(0)\n\n      // NOTE: Here tabbing would switch focus when it is implemented\n      setInput1Focused(false)\n      setInput2Focused(true)\n\n      input1Spy.reset()\n      input2Spy.reset()\n\n      await testSetup.mockInput.typeText(\"second\")\n\n      expect(input1Spy.callCount()).toBe(0)\n      expect(input2Spy.callCount()).toBe(6) // \"s\", \"e\", \"c\", \"o\", \"n\", \"d\"\n    })\n\n    it(\"should handle event handler attachment\", async () => {\n      const inputSpy = createSpy()\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <input focused onInput={inputSpy} />\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      await testSetup.mockInput.typeText(\"test\")\n\n      expect(inputSpy.callCount()).toBe(4)\n      expect(inputSpy.calls[0]?.[0]).toBe(\"t\")\n      expect(inputSpy.calls[3]?.[0]).toBe(\"test\")\n    })\n\n    it(\"should handle keyboard navigation on select components\", async () => {\n      const changeSpy = createSpy()\n      const [selectedValue, setSelectedValue] = createSignal(\"\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <select\n              focused\n              options={[\n                { name: \"Option 1\", value: \"opt1\", description: \"First option\" },\n                { name: \"Option 2\", value: \"opt2\", description: \"Second option\" },\n                { name: \"Option 3\", value: \"opt3\", description: \"Third option\" },\n              ]}\n              onChange={(index, option) => {\n                changeSpy(index, option)\n                setSelectedValue(option?.value || \"\")\n              }}\n            />\n            <text>Selected: {selectedValue()}</text>\n          </box>\n        ),\n        { width: 25, height: 10 },\n      )\n\n      testSetup.mockInput.pressArrow(\"down\")\n\n      expect(changeSpy.callCount()).toBe(1)\n      expect(changeSpy.calls[0]?.[0]).toBe(1)\n      expect(changeSpy.calls[0]?.[1]?.value).toBe(\"opt2\")\n      expect(selectedValue()).toBe(\"opt2\")\n\n      testSetup.mockInput.pressArrow(\"down\")\n\n      expect(changeSpy.callCount()).toBe(2)\n      expect(changeSpy.calls[1]?.[0]).toBe(2)\n      expect(changeSpy.calls[1]?.[1]?.value).toBe(\"opt3\")\n      expect(selectedValue()).toBe(\"opt3\")\n    })\n\n    it(\"should handle dynamic arrays and list updates\", async () => {\n      const [items, setItems] = createSignal([\"Item 1\", \"Item 2\"])\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            {items().map((item) => (\n              <text>{item}</text>\n            ))}\n          </box>\n        ),\n        { width: 20, height: 10 },\n      )\n\n      let children = testSetup.renderer.root.getChildren()\n      expect(children.length).toBe(1)\n      let boxChildren = children[0]!.getChildren()\n      expect(boxChildren.length).toBe(2)\n\n      setItems([\"Item 1\", \"Item 2\", \"Item 3\"])\n\n      children = testSetup.renderer.root.getChildren()\n      boxChildren = children[0]!.getChildren()\n      expect(boxChildren.length).toBe(3)\n\n      setItems([\"Item 1\", \"Item 3\"])\n\n      children = testSetup.renderer.root.getChildren()\n      boxChildren = children[0]!.getChildren()\n      expect(boxChildren.length).toBe(2)\n    })\n\n    it(\"should handle text modifier components\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box>\n            <text>\n              <b>Bold text</b> and <i>italic text</i> with <u>underline</u>\n            </text>\n          </box>\n        ),\n        { width: 40, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n\n      // The text node should contain the styled text content\n      // We can verify this by checking the visual output\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Bold text\")\n      expect(frame).toContain(\"italic text\")\n      expect(frame).toContain(\"underline\")\n    })\n\n    it(\"should handle dynamic text content\", async () => {\n      const [dynamicText, setDynamicText] = createSignal(\"Initial\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <text>Static: {dynamicText()}</text>\n            <text>Direct content</text>\n          </box>\n        ),\n        { width: 30, height: 8 },\n      )\n\n      await testSetup.renderOnce()\n\n      let frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Static: Initial\")\n      expect(frame).toContain(\"Direct content\")\n\n      setDynamicText(\"Updated\")\n      await testSetup.renderOnce()\n\n      frame = testSetup.captureCharFrame()\n      expect(frame).toContain(\"Static: Updated\")\n      expect(frame).toContain(\"Direct content\")\n    })\n\n    it(\"should handle usePaste hook\", async () => {\n      const pasteSpy = createSpy()\n      const [pastedText, setPastedText] = createSignal(\"\")\n\n      const TestComponent = () => {\n        usePaste((event) => {\n          const text = decodePasteBytes(event.bytes)\n          pasteSpy(text)\n          setPastedText(text)\n        })\n\n        return (\n          <box>\n            <text>Pasted: {pastedText()}</text>\n          </box>\n        )\n      }\n\n      testSetup = await testRender(() => <TestComponent />, { width: 30, height: 5 })\n\n      await testSetup.mockInput.pasteBracketedText(\"pasted content\")\n\n      expect(pasteSpy.callCount()).toBe(1)\n      expect(pasteSpy.calls[0]?.[0]).toBe(\"pasted content\")\n      expect(pastedText()).toBe(\"pasted content\")\n    })\n\n    it(\"should handle terminal focus hooks\", async () => {\n      const focusSpy = createSpy()\n      const blurSpy = createSpy()\n      const [status, setStatus] = createSignal(\"idle\")\n\n      const TestComponent = () => {\n        onFocus(() => {\n          focusSpy()\n          setStatus(\"focused\")\n        })\n\n        onBlur(() => {\n          blurSpy()\n          setStatus(\"blurred\")\n        })\n\n        return (\n          <box>\n            <text>Status: {status()}</text>\n          </box>\n        )\n      }\n\n      testSetup = await testRender(() => <TestComponent />, { width: 30, height: 5 })\n\n      testSetup.renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[I\"))\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(focusSpy.callCount()).toBe(1)\n      expect(blurSpy.callCount()).toBe(0)\n      expect(status()).toBe(\"focused\")\n\n      testSetup.renderer.stdin.emit(\"data\", Buffer.from(\"\\x1b[O\"))\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(focusSpy.callCount()).toBe(1)\n      expect(blurSpy.callCount()).toBe(1)\n      expect(status()).toBe(\"blurred\")\n    })\n\n    it(\"should handle global preventDefault for keyboard events\", async () => {\n      const inputSpy = createSpy()\n      const globalHandlerSpy = createSpy()\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <input focused onInput={inputSpy} />\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      // Register global handler that prevents 'a' key\n      testSetup.renderer.keyInput.on(\"keypress\", (event) => {\n        globalHandlerSpy(event.name)\n        if (event.name === \"a\") {\n          event.preventDefault()\n        }\n      })\n\n      await testSetup.mockInput.typeText(\"abc\")\n\n      // Global handler should be called for all keys\n      expect(globalHandlerSpy.callCount()).toBe(3)\n      expect(globalHandlerSpy.calls[0]?.[0]).toBe(\"a\")\n      expect(globalHandlerSpy.calls[1]?.[0]).toBe(\"b\")\n      expect(globalHandlerSpy.calls[2]?.[0]).toBe(\"c\")\n\n      // Input should only receive 'b' and 'c' (not 'a')\n      expect(inputSpy.callCount()).toBe(2)\n      expect(inputSpy.calls[0]?.[0]).toBe(\"b\")\n      expect(inputSpy.calls[1]?.[0]).toBe(\"bc\")\n    })\n\n    it(\"should handle global preventDefault for paste events\", async () => {\n      const pasteSpy = createSpy()\n      const globalHandlerSpy = createSpy()\n      const [pastedText, setPastedText] = createSignal(\"\")\n\n      const TestComponent = () => {\n        return (\n          <box>\n            <input\n              focused\n              onPaste={(val) => {\n                pasteSpy(val)\n                setPastedText(decodePasteBytes(val.bytes))\n              }}\n            />\n          </box>\n        )\n      }\n\n      testSetup = await testRender(() => <TestComponent />, { width: 30, height: 5 })\n\n      // Register global handler that prevents paste containing \"forbidden\"\n      testSetup.renderer.keyInput.on(\"paste\", (event: PasteEvent) => {\n        const text = decodePasteBytes(event.bytes)\n        globalHandlerSpy(text)\n        if (text.includes(\"forbidden\")) {\n          event.preventDefault()\n        }\n      })\n\n      // First paste should go through\n      await testSetup.mockInput.pasteBracketedText(\"allowed content\")\n      expect(globalHandlerSpy.callCount()).toBe(1)\n      expect(pasteSpy.callCount()).toBe(1)\n      expect(pastedText()).toBe(\"allowed content\")\n\n      // Reset spies\n      globalHandlerSpy.reset()\n      pasteSpy.reset()\n\n      // Second paste should be prevented\n      await testSetup.mockInput.pasteBracketedText(\"forbidden content\")\n      expect(globalHandlerSpy.callCount()).toBe(1)\n      expect(globalHandlerSpy.calls[0]?.[0]).toBe(\"forbidden content\")\n      expect(pasteSpy.callCount()).toBe(0)\n      expect(pastedText()).toBe(\"allowed content\") // Should remain unchanged\n    })\n\n    it(\"should handle global handler registered after component mount\", async () => {\n      const inputSpy = createSpy()\n      const [value, setValue] = createSignal(\"\")\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <input\n              focused\n              onInput={(val) => {\n                inputSpy(val)\n                setValue(val)\n              }}\n            />\n            <text>Value: {value()}</text>\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      // Type before global handler exists\n      await testSetup.mockInput.typeText(\"hello\")\n      expect(inputSpy.callCount()).toBe(5)\n      expect(value()).toBe(\"hello\")\n\n      inputSpy.reset()\n\n      testSetup.renderer.keyInput.on(\"keypress\", (event) => {\n        if (/^[0-9]$/.test(event.name)) {\n          event.preventDefault()\n        }\n      })\n\n      // Type mixed content\n      await testSetup.mockInput.typeText(\"abc123xyz\")\n\n      // Only letters should reach the input\n      expect(inputSpy.callCount()).toBe(6) // a, b, c, x, y, z (not 1, 2, 3)\n      expect(value()).toBe(\"helloabcxyz\")\n    })\n\n    it(\"should handle dynamic preventDefault conditions\", async () => {\n      const inputSpy = createSpy()\n      let preventNumbers = false\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <input focused onInput={inputSpy} />\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      // Register handler with dynamic condition\n      testSetup.renderer.keyInput.on(\"keypress\", (event) => {\n        if (preventNumbers && /^[0-9]$/.test(event.name)) {\n          event.preventDefault()\n        }\n      })\n\n      // Initially allow numbers\n      await testSetup.mockInput.typeText(\"a1\")\n      expect(inputSpy.callCount()).toBe(2)\n      expect(inputSpy.calls[1]?.[0]).toBe(\"a1\")\n\n      // Enable number prevention\n      preventNumbers = true\n      inputSpy.reset()\n\n      // Now numbers should be prevented\n      await testSetup.mockInput.typeText(\"b2c3\")\n      expect(inputSpy.callCount()).toBe(2) // Only 'b' and 'c'\n      expect(inputSpy.calls[0]?.[0]).toBe(\"a1b\")\n      expect(inputSpy.calls[1]?.[0]).toBe(\"a1bc\")\n\n      // Disable prevention again\n      preventNumbers = false\n      inputSpy.reset()\n\n      // Numbers should work again\n      await testSetup.mockInput.typeText(\"4\")\n      expect(inputSpy.callCount()).toBe(1)\n      expect(inputSpy.calls[0]?.[0]).toBe(\"a1bc4\")\n    })\n\n    it(\"should handle preventDefault for select components\", async () => {\n      const changeSpy = createSpy()\n      const globalHandlerSpy = createSpy()\n      const [selectedIndex, setSelectedIndex] = createSignal(0)\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <select\n              focused\n              wrapSelection\n              options={[\n                { name: \"Option 1\", value: 1, description: \"First\" },\n                { name: \"Option 2\", value: 2, description: \"Second\" },\n                { name: \"Option 3\", value: 3, description: \"Third\" },\n              ]}\n              onChange={(index, option) => {\n                changeSpy(index, option)\n                setSelectedIndex(index)\n              }}\n            />\n            <text>Selected: {selectedIndex()}</text>\n          </box>\n        ),\n        { width: 30, height: 10 },\n      )\n\n      // Register global handler that prevents down arrow\n      testSetup.renderer.keyInput.on(\"keypress\", (event) => {\n        globalHandlerSpy(event.name)\n        if (event.name === \"down\") {\n          event.preventDefault()\n        }\n      })\n\n      // Try to press down arrow - should be prevented\n      testSetup.mockInput.pressArrow(\"down\")\n      expect(globalHandlerSpy.callCount()).toBe(1)\n      expect(changeSpy.callCount()).toBe(0) // Should not change\n      expect(selectedIndex()).toBe(0) // Should remain at 0\n\n      // Up arrow should still work\n      testSetup.mockInput.pressArrow(\"up\")\n      expect(globalHandlerSpy.callCount()).toBe(2)\n      expect(changeSpy.callCount()).toBe(1) // Should wrap to last option\n      expect(selectedIndex()).toBe(2) // Should be at last option\n    })\n\n    it(\"should handle multiple global handlers with preventDefault\", async () => {\n      const inputSpy = createSpy()\n      const firstHandlerSpy = createSpy()\n      const secondHandlerSpy = createSpy()\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <input focused onInput={inputSpy} />\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      // First handler prevents 'x'\n      testSetup.renderer.keyInput.on(\"keypress\", (event) => {\n        firstHandlerSpy(event.name)\n        if (event.name === \"x\") {\n          event.preventDefault()\n        }\n      })\n\n      // Second handler also runs but can't undo preventDefault\n      testSetup.renderer.keyInput.on(\"keypress\", (event) => {\n        secondHandlerSpy(event.name)\n      })\n\n      await testSetup.mockInput.typeText(\"xyz\")\n\n      // Both handlers should be called for all keys\n      expect(firstHandlerSpy.callCount()).toBe(3)\n      expect(secondHandlerSpy.callCount()).toBe(3)\n\n      // But input should only receive 'y' and 'z'\n      expect(inputSpy.callCount()).toBe(2)\n      expect(inputSpy.calls[0]?.[0]).toBe(\"y\")\n      expect(inputSpy.calls[1]?.[0]).toBe(\"yz\")\n    })\n\n    it(\"should handle textarea onSubmit events\", async () => {\n      const onSubmitSpy = createSpy()\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <textarea focused initialValue=\"test content\" onSubmit={() => onSubmitSpy()} />\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      testSetup.mockInput.pressKey(\"RETURN\", { meta: true })\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(onSubmitSpy.callCount()).toBe(1)\n    })\n\n    it(\"should not trigger textarea onSubmit when return is preventDefault in another component\", async () => {\n      const textareaSubmitSpy = createSpy()\n      const globalReturnHandlerSpy = createSpy()\n\n      const GlobalReturnHandler = () => {\n        useKeyboard((event) => {\n          if (event.name === \"return\") {\n            globalReturnHandlerSpy()\n            event.preventDefault()\n          }\n        })\n        return null\n      }\n\n      testSetup = await testRender(\n        () => (\n          <box>\n            <GlobalReturnHandler />\n            <textarea focused initialValue=\"test content\" onSubmit={() => textareaSubmitSpy()} />\n          </box>\n        ),\n        { width: 20, height: 5 },\n      )\n\n      testSetup.mockInput.pressEnter()\n      await new Promise((resolve) => setTimeout(resolve, 10))\n\n      expect(globalReturnHandlerSpy.callCount()).toBe(1)\n      expect(textareaSubmitSpy.callCount()).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/layout.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal, Show } from \"solid-js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"SolidJS Renderer Integration Tests\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  describe(\"Basic Text Rendering\", () => {\n    it(\"should render simple text correctly\", async () => {\n      testSetup = await testRender(() => <text>Hello World</text>, {\n        width: 20,\n        height: 5,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render multiline text correctly\", async () => {\n      testSetup = await testRender(\n        () => (\n          <text>\n            Line 1\n            <br />\n            Line 2\n            <br />\n            Line 3\n          </text>\n        ),\n        {\n          width: 15,\n          height: 5,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should throw on rendering text without parent <text> element\", async () => {\n      expect(\n        testRender(() => <box>This text is not wrapped in a text element</box>, {\n          width: 30,\n          height: 5,\n        }),\n      ).rejects.toThrow()\n    })\n\n    it(\"should throw on rendering span without parent <text> element\", async () => {\n      expect(\n        testRender(\n          () => (\n            <box>\n              <span>This text is not wrapped in a text element</span>\n            </box>\n          ),\n          {\n            width: 30,\n            height: 5,\n          },\n        ),\n      ).rejects.toThrow()\n    })\n\n    it(\"should render text with dynamic content\", async () => {\n      const counter = () => 42\n\n      testSetup = await testRender(() => <text>Counter: {counter()}</text>, {\n        width: 20,\n        height: 3,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Box Layout Rendering\", () => {\n    it(\"should render basic box layout correctly\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box style={{ width: 20, height: 5, border: true }}>\n            <text>Inside Box</text>\n          </box>\n        ),\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render nested boxes correctly\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box style={{ width: 30, height: 10, border: true }} title=\"Parent Box\">\n            <box style={{ left: 2, top: 2, width: 10, height: 3, border: true }}>\n              <text>Nested</text>\n            </box>\n            <text style={{ left: 15, top: 2 }}>Sibling</text>\n          </box>\n        ),\n        {\n          width: 35,\n          height: 12,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render absolute positioned boxes\", async () => {\n      testSetup = await testRender(\n        () => (\n          <>\n            <box\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                width: 10,\n                height: 3,\n                border: true,\n                backgroundColor: \"red\",\n              }}\n            >\n              <text>Box 1</text>\n            </box>\n            <box\n              style={{\n                position: \"absolute\",\n                left: 12,\n                top: 2,\n                width: 10,\n                height: 3,\n                border: true,\n                backgroundColor: \"blue\",\n              }}\n            >\n              <text>Box 2</text>\n            </box>\n          </>\n        ),\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should auto-enable border when borderStyle is set\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box style={{ width: 20, height: 5 }} borderStyle=\"single\">\n            <text>With Border</text>\n          </box>\n        ),\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should auto-enable border when borderColor is set\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box style={{ width: 20, height: 5 }} borderColor=\"cyan\">\n            <text>Colored Border</text>\n          </box>\n        ),\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should auto-enable border when focusedBorderColor is set\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box style={{ width: 20, height: 5 }} focusedBorderColor=\"yellow\">\n            <text>Focused Border</text>\n          </box>\n        ),\n        {\n          width: 25,\n          height: 8,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Reactive Updates\", () => {\n    it(\"should handle reactive state changes\", async () => {\n      const [counter, setCounter] = createSignal(0)\n\n      testSetup = await testRender(() => <text>Counter: {counter()}</text>, {\n        width: 15,\n        height: 3,\n      })\n\n      await testSetup.renderOnce()\n      const initialFrame = testSetup.captureCharFrame()\n\n      setCounter(5)\n      await testSetup.renderOnce()\n      const updatedFrame = testSetup.captureCharFrame()\n\n      expect(initialFrame).toMatchSnapshot()\n      expect(updatedFrame).toMatchSnapshot()\n      expect(updatedFrame).not.toBe(initialFrame)\n    })\n\n    it(\"should handle conditional rendering\", async () => {\n      const [showText, setShowText] = createSignal(true)\n\n      testSetup = await testRender(\n        () => (\n          <text wrapMode=\"none\">\n            Always visible\n            <Show when={showText()} fallback=\"\">\n              {\" - Conditional text\"}\n            </Show>\n          </text>\n        ),\n        {\n          width: 30,\n          height: 3,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const visibleFrame = testSetup.captureCharFrame()\n\n      setShowText(false)\n      await testSetup.renderOnce()\n      const hiddenFrame = testSetup.captureCharFrame()\n\n      expect(visibleFrame).toMatchSnapshot()\n      expect(hiddenFrame).toMatchSnapshot()\n      expect(hiddenFrame).not.toBe(visibleFrame)\n    })\n  })\n\n  describe(\"Complex Layouts\", () => {\n    it(\"should render complex nested layout correctly\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box style={{ width: 40, border: true }} title=\"Complex Layout\">\n            <box style={{ left: 2, width: 15, height: 5, border: true, backgroundColor: \"#333\" }}>\n              <text wrapMode=\"none\" style={{ fg: \"cyan\" }}>\n                Header Section\n              </text>\n              <text wrapMode=\"none\" style={{ fg: \"yellow\" }}>\n                Menu Item 1\n              </text>\n              <text wrapMode=\"none\" style={{ fg: \"yellow\" }}>\n                Menu Item 2\n              </text>\n            </box>\n            <box style={{ left: 18, width: 18, height: 8, border: true, backgroundColor: \"#222\" }}>\n              <text wrapMode=\"none\" style={{ fg: \"green\" }}>\n                Content Area\n              </text>\n              <text wrapMode=\"none\" style={{ fg: \"white\" }}>\n                Some content here\n              </text>\n              <text wrapMode=\"none\" style={{ fg: \"white\" }}>\n                More content\n              </text>\n              <text wrapMode=\"none\" style={{ fg: \"magenta\" }}>\n                Footer text\n              </text>\n            </box>\n            <text style={{ left: 2, fg: \"gray\" }}>Status: Ready</text>\n          </box>\n        ),\n        {\n          width: 45,\n          height: 18,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text with mixed styling and layout\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box style={{ width: 35, height: 8, border: true }}>\n            <text>\n              <span style={{ fg: \"red\", bold: true }}>ERROR:</span> Something went wrong\n            </text>\n            <text>\n              <span style={{ fg: \"yellow\" }}>WARNING:</span> Check your settings\n            </text>\n            <text>\n              <span style={{ fg: \"green\" }}>SUCCESS:</span> All systems operational\n            </text>\n          </box>\n        ),\n        {\n          width: 40,\n          height: 10,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render scrollbox with sticky scroll and spacer\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box maxHeight={\"100%\"} maxWidth={\"100%\"}>\n            <scrollbox\n              scrollbarOptions={{ visible: false }}\n              stickyScroll={true}\n              stickyStart=\"bottom\"\n              paddingTop={1}\n              paddingBottom={1}\n              title=\"scroll area\"\n              rootOptions={{\n                flexGrow: 0,\n              }}\n              border\n            >\n              <box border height={10} title=\"hi\" />\n            </scrollbox>\n            <box border height={10} title=\"spacer\" flexShrink={0}>\n              <text>spacer</text>\n            </box>\n          </box>\n        ),\n        {\n          width: 30,\n          height: 25,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Empty and Edge Cases\", () => {\n    it(\"should handle empty component\", async () => {\n      testSetup = await testRender(() => <></>, {\n        width: 10,\n        height: 5,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle component with no children\", async () => {\n      testSetup = await testRender(() => <box style={{ width: 10, height: 5 }} />, {\n        width: 15,\n        height: 8,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle very small dimensions\", async () => {\n      testSetup = await testRender(() => <text>Hi</text>, {\n        width: 5,\n        height: 3,\n      })\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/line-number-scrollbox.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { For, Show, createSignal } from \"solid-js\"\nimport type { ScrollBoxRenderable } from \"../../core/src/renderables/index.js\"\nimport { SyntaxStyle } from \"../../core/src/syntax-style.js\"\nimport { MockTreeSitterClient } from \"@opentui/core/testing\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\nlet mockTreeSitterClient: MockTreeSitterClient\n\ndescribe(\"LineNumber in ScrollBox - Height and Overlap Issues\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n    mockTreeSitterClient = new MockTreeSitterClient()\n    mockTreeSitterClient.setMockResult({ highlights: [] })\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  it(\"REPRODUCES BUG: single line_number with code in scrollbox has excessive height\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    const codeContent = `function hello() {\n  console.log(\"Hello, World!\");\n  return 42;\n}`\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\">\n          <scrollbox flexGrow={1} scrollbarOptions={{ visible: false }}>\n            <line_number fg=\"#888888\" minWidth={3} paddingRight={1}>\n              <code\n                fg=\"#ffffff\"\n                filetype=\"javascript\"\n                syntaxStyle={syntaxStyle}\n                content={codeContent}\n                treeSitterClient={mockTreeSitterClient}\n              />\n            </line_number>\n          </scrollbox>\n        </box>\n      ),\n      {\n        width: 40,\n        height: 30, // Portrait aspect ratio (taller than wide)\n      },\n    )\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Count the number of lines that have actual content vs empty lines\n    const lines = frame.split(\"\\n\")\n    const contentLines = lines.filter((line) => line.trim().length > 0)\n    const emptyLines = lines.filter((line) => line.trim().length === 0)\n\n    // The code has 4 lines, so we expect roughly 4 lines of content\n    // There shouldn't be massive amounts of empty space\n    const emptyToContentRatio = emptyLines.length / contentLines.length\n\n    // BUG: This ratio is 6.75 (way too high!)\n    // Line_number fills entire viewport height instead of wrapping content\n    expect(emptyToContentRatio).toBeGreaterThan(5) // Documenting the bug\n\n    // Check that the code content is actually visible\n    expect(frame).toContain(\"function hello\")\n    expect(frame).toContain(\"console.log\")\n  })\n\n  it(\"WORKAROUND: flexShrink=0 fixes the height issue\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    const codeContent = `function hello() {\n  console.log(\"Hello, World!\");\n  return 42;\n}`\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\">\n          <scrollbox flexGrow={1} scrollbarOptions={{ visible: false }}>\n            <line_number flexShrink={0} fg=\"#888888\" minWidth={3} paddingRight={1}>\n              <code\n                fg=\"#ffffff\"\n                filetype=\"javascript\"\n                syntaxStyle={syntaxStyle}\n                content={codeContent}\n                treeSitterClient={mockTreeSitterClient}\n              />\n            </line_number>\n          </scrollbox>\n        </box>\n      ),\n      {\n        width: 40,\n        height: 30,\n      },\n    )\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    const lines = frame.split(\"\\n\")\n    const contentLines = lines.filter((line) => line.trim().length > 0)\n    const emptyLines = lines.filter((line) => line.trim().length === 0)\n    const emptyToContentRatio = emptyLines.length / contentLines.length\n\n    // With flexShrink=0, the ratio should be reasonable\n    expect(emptyToContentRatio).toBeLessThan(7)\n\n    expect(frame).toContain(\"function hello\")\n    expect(frame).toContain(\"console.log\")\n  })\n\n  it(\"multiple line_number blocks should not overlap - realistic chat scenario\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    const messages = [\n      {\n        role: \"assistant\",\n        tool: \"write\",\n        filePath: \"src/hello.ts\",\n        code: `export function hello() {\n  return \"Hello, World!\";\n}`,\n      },\n      {\n        role: \"assistant\",\n        text: \"I've created the hello function.\",\n      },\n      {\n        role: \"assistant\",\n        tool: \"write\",\n        filePath: \"src/test.ts\",\n        code: `import { hello } from \"./hello\";\n\ntest(\"hello returns greeting\", () => {\n  expect(hello()).toBe(\"Hello, World!\");\n});`,\n      },\n      {\n        role: \"assistant\",\n        text: \"I've also added a test file.\",\n      },\n    ]\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\" paddingLeft={2} paddingRight={2} gap={1}>\n          <scrollbox scrollbarOptions={{ visible: false }} stickyScroll={true} stickyStart=\"bottom\" flexGrow={1}>\n            <For each={messages}>\n              {(message) => (\n                <>\n                  <Show when={message.tool === \"write\"}>\n                    <box flexShrink={0}>\n                      <text fg=\"#00aaff\">Wrote {message.filePath}</text>\n                    </box>\n                    <line_number fg=\"#888888\" minWidth={3} paddingRight={1}>\n                      <code\n                        flexGrow={1}\n                        fg=\"#ffffff\"\n                        filetype=\"typescript\"\n                        syntaxStyle={syntaxStyle}\n                        content={message.code}\n                        treeSitterClient={mockTreeSitterClient}\n                      />\n                    </line_number>\n                  </Show>\n                  <Show when={message.text}>\n                    <box flexShrink={0}>\n                      <text fg=\"#ffffff\">{message.text}</text>\n                    </box>\n                  </Show>\n                </>\n              )}\n            </For>\n          </scrollbox>\n        </box>\n      ),\n      {\n        width: 50,\n        height: 40, // Portrait\n      },\n    )\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Check all content is visible and not overlapping\n    expect(frame).toContain(\"Wrote src/hello.ts\")\n    expect(frame).toContain(\"export function hello\")\n    expect(frame).toContain(\"I've created the hello function\")\n    expect(frame).toContain(\"Wrote src/test.ts\")\n    expect(frame).toContain(\"import { hello }\")\n    expect(frame).toContain(\"I've also added a test file\")\n\n    // Count how many times we see each unique piece of content\n    // If content is overlapping, we might see duplicates or garbled text\n    const helloCount = (frame.match(/Wrote src\\/hello\\.ts/g) || []).length\n    const testCount = (frame.match(/Wrote src\\/test\\.ts/g) || []).length\n\n    // Each should appear exactly once\n    expect(helloCount).toBe(1)\n    expect(testCount).toBe(1)\n  })\n\n  it(\"line_number height should match code content height, not double\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    const shortCode = \"const x = 1;\\nconst y = 2;\"\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\">\n          <box flexShrink={0}>\n            <text>--- START MARKER ---</text>\n          </box>\n          <line_number fg=\"#888888\" minWidth={3} paddingRight={1}>\n            <code\n              flexGrow={1}\n              fg=\"#ffffff\"\n              filetype=\"javascript\"\n              syntaxStyle={syntaxStyle}\n              content={shortCode}\n              treeSitterClient={mockTreeSitterClient}\n            />\n          </line_number>\n          <box flexShrink={0}>\n            <text>--- END MARKER ---</text>\n          </box>\n        </box>\n      ),\n      {\n        width: 40,\n        height: 25,\n      },\n    )\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Find the line indices of the markers\n    const lines = frame.split(\"\\n\")\n    const startIdx = lines.findIndex((line) => line.includes(\"START MARKER\"))\n    const endIdx = lines.findIndex((line) => line.includes(\"END MARKER\"))\n\n    expect(startIdx).toBeGreaterThanOrEqual(0)\n    expect(endIdx).toBeGreaterThan(startIdx)\n\n    // The code has 2 lines, so distance should be roughly 2-3 lines\n    // (2 code lines + maybe 1 for spacing)\n    // NOT 4-6 lines which would indicate double height\n    const distance = endIdx - startIdx - 1 // -1 to exclude the start marker line itself\n\n    // Distance should be reasonable (2-4 lines for 2 lines of code)\n    // If it's 6+ lines, that indicates excessive spacing/height\n    expect(distance).toBeLessThanOrEqual(5)\n    expect(distance).toBeGreaterThanOrEqual(2)\n\n    // Verify code is visible\n    expect(frame).toContain(\"const x = 1\")\n    expect(frame).toContain(\"const y = 2\")\n  })\n\n  it(\"scrollbox with box container around line_number - no excessive height\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    const code = `function test() {\n  return true;\n}`\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"row\">\n          <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>\n            <scrollbox scrollbarOptions={{ visible: false }} stickyScroll={true} stickyStart=\"bottom\" flexGrow={1}>\n              <box flexShrink={0}>\n                <text fg=\"#888888\">Message 1</text>\n              </box>\n              <box border={true} borderColor=\"#333333\">\n                <line_number fg=\"#888888\" minWidth={3} paddingRight={1}>\n                  <code\n                    flexGrow={1}\n                    fg=\"#ffffff\"\n                    filetype=\"typescript\"\n                    syntaxStyle={syntaxStyle}\n                    content={code}\n                    treeSitterClient={mockTreeSitterClient}\n                  />\n                </line_number>\n              </box>\n              <box flexShrink={0}>\n                <text fg=\"#888888\">Message 2</text>\n              </box>\n            </scrollbox>\n          </box>\n        </box>\n      ),\n      {\n        width: 50,\n        height: 30,\n      },\n    )\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Check content is visible\n    expect(frame).toContain(\"Message 1\")\n    expect(frame).toContain(\"function test\")\n    expect(frame).toContain(\"Message 2\")\n\n    // Find distance between Message 1 and Message 2\n    const lines = frame.split(\"\\n\")\n    const msg1Idx = lines.findIndex((line) => line.includes(\"Message 1\"))\n    const msg2Idx = lines.findIndex((line) => line.includes(\"Message 2\"))\n\n    expect(msg1Idx).toBeGreaterThanOrEqual(0)\n    expect(msg2Idx).toBeGreaterThan(msg1Idx)\n\n    // Code is 3 lines + border (2 lines) + some spacing\n    // Should be roughly 5-8 lines total, NOT 12-16\n    const distance = msg2Idx - msg1Idx\n    expect(distance).toBeLessThan(12)\n  })\n\n  it(\"multiple messages with mixed content - verify no overlapping\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    interface Message {\n      type: \"text\" | \"tool\"\n      content: string\n      filePath?: string\n      diagnostics?: Array<{ line: number; char: number; message: string }>\n    }\n\n    const messages: Message[] = [\n      { type: \"text\", content: \"Let me create a file for you.\" },\n      {\n        type: \"tool\",\n        content: `export const greet = (name: string) => {\n  return \\`Hello, \\${name}!\\`;\n};`,\n        filePath: \"src/greet.ts\",\n      },\n      { type: \"text\", content: \"I've created the greet function.\" },\n      {\n        type: \"tool\",\n        content: `import { greet } from \"./greet\";\n\nconsole.log(greet(\"World\"));`,\n        filePath: \"src/index.ts\",\n        diagnostics: [{ line: 2, char: 5, message: \"Unused variable\" }],\n      },\n      { type: \"text\", content: \"And here's the main file.\" },\n    ]\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"row\">\n          <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>\n            <scrollbox scrollbarOptions={{ visible: false }} stickyScroll={true} stickyStart=\"bottom\" flexGrow={1}>\n              <For each={messages}>\n                {(message) => (\n                  <>\n                    <Show when={message.type === \"text\"}>\n                      <box flexShrink={0}>\n                        <text fg=\"#ffffff\">{message.content}</text>\n                      </box>\n                    </Show>\n                    <Show when={message.type === \"tool\"}>\n                      <box flexShrink={0}>\n                        <text fg=\"#00aaff\">Wrote {message.filePath}</text>\n                      </box>\n                      <line_number fg=\"#888888\" minWidth={3} paddingRight={1}>\n                        <code\n                          flexGrow={1}\n                          fg=\"#ffffff\"\n                          filetype=\"typescript\"\n                          syntaxStyle={syntaxStyle}\n                          content={message.content}\n                          treeSitterClient={mockTreeSitterClient}\n                        />\n                      </line_number>\n                      <Show when={message.diagnostics && message.diagnostics.length > 0}>\n                        <For each={message.diagnostics}>\n                          {(diagnostic) => (\n                            <text fg=\"#ff0000\">\n                              Error [{diagnostic.line}:{diagnostic.char}]: {diagnostic.message}\n                            </text>\n                          )}\n                        </For>\n                      </Show>\n                    </Show>\n                  </>\n                )}\n              </For>\n            </scrollbox>\n          </box>\n        </box>\n      ),\n      {\n        width: 60,\n        height: 50, // Tall portrait\n      },\n    )\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Verify all content appears exactly once\n    expect(frame).toContain(\"Let me create a file for you\")\n    expect(frame).toContain(\"Wrote src/greet.ts\")\n    expect(frame).toContain(\"export const greet\")\n    expect(frame).toContain(\"I've created the greet function\")\n    expect(frame).toContain(\"Wrote src/index.ts\")\n    expect(frame).toContain(\"import { greet }\")\n    expect(frame).toContain(\"And here's the main file\")\n    expect(frame).toContain(\"Error [2:5]: Unused variable\")\n\n    // Check no duplication\n    const greetFileCount = (frame.match(/Wrote src\\/greet\\.ts/g) || []).length\n    const indexFileCount = (frame.match(/Wrote src\\/index\\.ts/g) || []).length\n\n    expect(greetFileCount).toBe(1)\n    expect(indexFileCount).toBe(1)\n\n    // Verify diagnostic appears right after code, not overlapping\n    const lines = frame.split(\"\\n\")\n    const importLine = lines.findIndex((line) => line.includes(\"import { greet }\"))\n    const errorLine = lines.findIndex((line) => line.includes(\"Error [2:5]\"))\n\n    // Error should appear after the code block (within ~5 lines)\n    expect(errorLine).toBeGreaterThan(importLine)\n    expect(errorLine - importLine).toBeLessThan(8)\n  })\n\n  it(\"scroll behavior - content should remain visible after scroll\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    const [messages, setMessages] = createSignal([\n      { id: 1, text: \"Message 1\", code: \"const a = 1;\" },\n      { id: 2, text: \"Message 2\", code: \"const b = 2;\" },\n    ])\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\" gap={1}>\n          <scrollbox\n            ref={(r) => (scrollRef = r)}\n            scrollbarOptions={{ visible: false }}\n            stickyScroll={true}\n            stickyStart=\"bottom\"\n            flexGrow={1}\n          >\n            <For each={messages()}>\n              {(message) => (\n                <>\n                  <box flexShrink={0}>\n                    <text fg=\"#ffffff\">{message.text}</text>\n                  </box>\n                  <line_number fg=\"#888888\" minWidth={2} paddingRight={1}>\n                    <code\n                      flexGrow={1}\n                      fg=\"#ffffff\"\n                      filetype=\"javascript\"\n                      syntaxStyle={syntaxStyle}\n                      content={message.code}\n                      treeSitterClient={mockTreeSitterClient}\n                    />\n                  </line_number>\n                </>\n              )}\n            </For>\n          </scrollbox>\n        </box>\n      ),\n      {\n        width: 40,\n        height: 30,\n      },\n    )\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const initialFrame = testSetup.captureCharFrame()\n    expect(initialFrame).toMatchSnapshot()\n\n    // Add many more messages\n    setMessages((prev) => [\n      ...prev,\n      ...Array.from({ length: 20 }, (_, i) => ({\n        id: i + 3,\n        text: `Message ${i + 3}`,\n        code: `const var${i + 3} = ${i + 3};`,\n      })),\n    ])\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    // Scroll to bottom\n    if (scrollRef) {\n      scrollRef.scrollTo(scrollRef.scrollHeight)\n      await testSetup.renderOnce()\n    }\n\n    const scrolledFrame = testSetup.captureCharFrame()\n    expect(scrolledFrame).toMatchSnapshot()\n\n    // Should see later messages\n    expect(scrolledFrame).toContain(\"Message\")\n    expect(scrolledFrame).toContain(\"const var\")\n\n    // Content should be visible (not all whitespace)\n    const nonWhitespace = scrolledFrame.replace(/\\s/g, \"\").length\n    expect(nonWhitespace).toBeGreaterThan(50)\n\n    // Scroll to middle\n    if (scrollRef) {\n      scrollRef.scrollTo(Math.floor(scrollRef.scrollHeight / 2))\n      await testSetup.renderOnce()\n    }\n\n    const middleFrame = testSetup.captureCharFrame()\n    expect(middleFrame).toMatchSnapshot()\n\n    // Should see middle messages\n    const hasMiddleContent = /Message \\d+/.test(middleFrame)\n    expect(hasMiddleContent).toBe(true)\n  })\n\n  it(\"VISUAL CHECK: box with line_number should have clean spacing\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\" padding={2}>\n          <text fg=\"#00aaff\">═══ Code Block 1 ═══</text>\n          <box border={true} borderColor=\"#333333\">\n            <line_number fg=\"#666666\" minWidth={3} paddingRight={1}>\n              <code\n                flexGrow={1}\n                fg=\"#ffffff\"\n                filetype=\"javascript\"\n                syntaxStyle={syntaxStyle}\n                content=\"const x = 1;\\nconst y = 2;\\nconst z = 3;\"\n                treeSitterClient={mockTreeSitterClient}\n              />\n            </line_number>\n          </box>\n          <text fg=\"#00aaff\">═══ Code Block 2 ═══</text>\n          <box border={true} borderColor=\"#333333\">\n            <line_number fg=\"#666666\" minWidth={3} paddingRight={1}>\n              <code\n                flexGrow={1}\n                fg=\"#ffffff\"\n                filetype=\"javascript\"\n                syntaxStyle={syntaxStyle}\n                content=\"function test() {\\n  return 42;\\n}\"\n                treeSitterClient={mockTreeSitterClient}\n              />\n            </line_number>\n          </box>\n          <text fg=\"#00aaff\">═══ End ═══</text>\n        </box>\n      ),\n      {\n        width: 50,\n        height: 35,\n      },\n    )\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toMatchSnapshot()\n\n    // Visual inspection via snapshot should show:\n    // - No excessive blank lines between blocks\n    // - Blocks don't overlap\n    // - Clean spacing\n\n    const lines = frame.split(\"\\n\")\n    const block1Idx = lines.findIndex((line) => line.includes(\"Code Block 1\"))\n    const block2Idx = lines.findIndex((line) => line.includes(\"Code Block 2\"))\n    const endIdx = lines.findIndex((line) => line.includes(\"End\"))\n\n    expect(block1Idx).toBeGreaterThanOrEqual(0)\n    expect(block2Idx).toBeGreaterThan(block1Idx)\n    expect(endIdx).toBeGreaterThan(block2Idx)\n\n    // Block 1 has 3 lines of code + borders (2) = 5 lines\n    // Should be about 5-7 lines between markers, NOT 10+\n    const block1Height = block2Idx - block1Idx\n    expect(block1Height).toBeLessThan(10)\n\n    // Block 2 has 3 lines of code + borders (2) = 5 lines\n    const block2Height = endIdx - block2Idx\n    expect(block2Height).toBeLessThan(10)\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/line-number.test.tsx",
    "content": "import { describe, test, expect, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { SyntaxStyle } from \"@opentui/core\"\nimport { MockTreeSitterClient } from \"@opentui/core/testing\"\nimport { createSignal, Show } from \"solid-js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\nlet mockTreeSitterClient: MockTreeSitterClient\n\ndescribe(\"LineNumberRenderable with SolidJS\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n    mockTreeSitterClient = new MockTreeSitterClient()\n    mockTreeSitterClient.setMockResult({ highlights: [] })\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  test(\"renders code with line numbers\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      keyword: { fg: \"#C792EA\" },\n      function: { fg: \"#82AAFF\" },\n      default: { fg: \"#FFFFFF\" },\n    })\n\n    const codeContent = `function test() {\n  return 42\n}\nconsole.log(test())`\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <line_number\n          id=\"line-numbers\"\n          fg=\"#888888\"\n          bg=\"#000000\"\n          minWidth={3}\n          paddingRight={1}\n          width=\"100%\"\n          height=\"100%\"\n        >\n          <code\n            id=\"code-content\"\n            content={codeContent}\n            filetype=\"javascript\"\n            syntaxStyle={syntaxStyle}\n            treeSitterClient={mockTreeSitterClient}\n            width=\"100%\"\n            height=\"100%\"\n          />\n        </line_number>\n      </box>\n    ))\n\n    await testSetup.renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n\n    // Basic checks\n    expect(frame).toContain(\"function test()\")\n    expect(frame).toContain(\" 1 \") // Line number 1\n    expect(frame).toContain(\" 2 \") // Line number 2\n    expect(frame).toContain(\" 3 \") // Line number 3\n    expect(frame).toContain(\" 4 \") // Line number 4\n  })\n\n  test(\"handles conditional removal of line number element\", async () => {\n    const syntaxStyle = SyntaxStyle.fromStyles({\n      keyword: { fg: \"#C792EA\" },\n      function: { fg: \"#82AAFF\" },\n      default: { fg: \"#FFFFFF\" },\n    })\n\n    const codeContent = `function test() {\n  return 42\n}\nconsole.log(test())`\n\n    const [showLineNumbers, setShowLineNumbers] = createSignal(true)\n\n    testSetup = await testRender(() => (\n      <box id=\"root\" width=\"100%\" height=\"100%\">\n        <Show\n          when={showLineNumbers()}\n          fallback={\n            <code\n              id=\"code-content-no-lines\"\n              content={codeContent}\n              filetype=\"javascript\"\n              syntaxStyle={syntaxStyle}\n              treeSitterClient={mockTreeSitterClient}\n              width=\"100%\"\n              height=\"100%\"\n            />\n          }\n        >\n          <line_number\n            id=\"line-numbers\"\n            fg=\"#888888\"\n            bg=\"#000000\"\n            minWidth={3}\n            paddingRight={1}\n            width=\"100%\"\n            height=\"100%\"\n          >\n            <code\n              id=\"code-content\"\n              content={codeContent}\n              filetype=\"javascript\"\n              syntaxStyle={syntaxStyle}\n              treeSitterClient={mockTreeSitterClient}\n              width=\"100%\"\n              height=\"100%\"\n            />\n          </line_number>\n        </Show>\n      </box>\n    ))\n\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    let frame = testSetup.captureCharFrame()\n\n    // Initially shows line numbers\n    expect(frame).toContain(\" 1 \")\n    expect(frame).toContain(\" 2 \")\n\n    // Toggle to hide line numbers - this should trigger destruction of LineNumberRenderable\n    setShowLineNumbers(false)\n    await testSetup.renderOnce()\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    frame = testSetup.captureCharFrame()\n\n    // Should still show code but without line numbers\n    expect(frame).toContain(\"function test()\")\n    // Line numbers should not be present\n    expect(frame).not.toContain(\" 1 function\")\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/link.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport type { TextRenderable } from \"@opentui/core\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\n// Helper to get text renderable from renderer\nfunction getTextRenderable(renderer: any): TextRenderable {\n  return renderer.root.getChildren()[0] as TextRenderable\n}\n\ndescribe(\"Link Rendering Tests\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  it(\"should render link with href correctly\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          Visit <a href=\"https://opentui.com\">opentui.com</a> for more info\n        </text>\n      ),\n      {\n        width: 50,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Visit opentui.com for more info\")\n  })\n\n  it(\"should render styled link with underline\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          <u>\n            <a href=\"https://opentui.com\" style={{ fg: \"blue\" }}>\n              opentui.com\n            </a>\n          </u>\n        </text>\n      ),\n      {\n        width: 50,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"opentui.com\")\n  })\n\n  it(\"should render link inside text with other elements\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          Check out <a href=\"https://github.com/anomalyco/opentui\">GitHub</a> and{\" \"}\n          <a href=\"https://opentui.com\">our website</a>\n        </text>\n      ),\n      {\n        width: 60,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"GitHub\")\n    expect(frame).toContain(\"our website\")\n  })\n\n  it(\"should inherit link from parent to nested styled span\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          <a href=\"https://opentui.com\">\n            <span style={{ fg: \"blue\", bold: true }}>styled text</span> default style\n          </a>\n        </text>\n      ),\n      {\n        width: 60,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    // Both parts should be rendered\n    expect(frame).toContain(\"styled text default style\")\n  })\n\n  it(\"should inherit link from parent to multiple nested elements\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          Visit{\" \"}\n          <a href=\"https://opentui.com\">\n            <b>our</b> <i>awesome</i> <u>website</u>\n          </a>{\" \"}\n          today\n        </text>\n      ),\n      {\n        width: 60,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Visit our awesome website today\")\n  })\n\n  it(\"should inherit link to deeply nested spans\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          <a href=\"https://example.com\">\n            <span style={{ fg: \"red\" }}>\n              Level 1<span style={{ bg: \"white\" }}> Level 2</span>\n            </span>\n          </a>\n        </text>\n      ),\n      {\n        width: 60,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Level 1 Level 2\")\n  })\n\n  it(\"should handle mixed linked and non-linked text\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          Plain text <a href=\"https://example.com\">linked text</a> more plain <a href=\"https://other.com\">other link</a>\n        </text>\n      ),\n      {\n        width: 80,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Plain text linked text more plain other link\")\n  })\n\n  it(\"should preserve styles when inheriting link\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          <a href=\"https://opentui.com\">\n            <b>Bold</b> <i>Italic</i> <u>Underline</u> Normal\n          </a>\n        </text>\n      ),\n      {\n        width: 80,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Bold Italic Underline Normal\")\n  })\n\n  it(\"should not override child link with parent link\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          <a href=\"https://parent.com\">\n            Parent link <a href=\"https://child.com\">child link</a> parent again\n          </a>\n        </text>\n      ),\n      {\n        width: 80,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Parent link child link parent again\")\n  })\n\n  it(\"should handle empty link content\", async () => {\n    testSetup = await testRender(\n      () => (\n        <text>\n          Before <a href=\"https://example.com\"></a> After\n        </text>\n      ),\n      {\n        width: 80,\n        height: 5,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Before  After\")\n  })\n\n  describe(\"Link Chunk Verification\", () => {\n    it(\"should create chunks with link for all nested content\", async () => {\n      testSetup = await testRender(\n        () => (\n          <text>\n            <a href=\"https://opentui.com\">\n              <span style={{ fg: \"blue\" }}>styled</span> plain\n            </a>\n          </text>\n        ),\n        {\n          width: 80,\n          height: 5,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const textRenderable = getTextRenderable(testSetup.renderer)\n      const chunks = textRenderable.textNode.gatherWithInheritedStyle()\n\n      // All chunks should have the link\n      for (const chunk of chunks) {\n        if (chunk.text.trim()) {\n          // Skip empty chunks\n          expect(chunk.link).toBeDefined()\n          expect(chunk.link?.url).toBe(\"https://opentui.com\")\n        }\n      }\n    })\n\n    it(\"should inherit link through multiple nesting levels\", async () => {\n      testSetup = await testRender(\n        () => (\n          <text>\n            <a href=\"https://example.com\">\n              <b>\n                <i>\n                  <u>deeply nested</u>\n                </i>\n              </b>\n            </a>\n          </text>\n        ),\n        {\n          width: 80,\n          height: 5,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const textRenderable = getTextRenderable(testSetup.renderer)\n      const chunks = textRenderable.textNode.gatherWithInheritedStyle()\n\n      // Find the chunk with text\n      const textChunk = chunks.find((c) => c.text.includes(\"deeply nested\"))\n      expect(textChunk).toBeDefined()\n      expect(textChunk?.link).toBeDefined()\n      expect(textChunk?.link?.url).toBe(\"https://example.com\")\n    })\n\n    it(\"should respect child link over parent link\", async () => {\n      testSetup = await testRender(\n        () => (\n          <text>\n            <a href=\"https://parent.com\">\n              parent <a href=\"https://child.com\">child</a> parent\n            </a>\n          </text>\n        ),\n        {\n          width: 80,\n          height: 5,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const textRenderable = getTextRenderable(testSetup.renderer)\n      const chunks = textRenderable.textNode.gatherWithInheritedStyle()\n\n      // Find chunks\n      const parentChunks = chunks.filter((c) => c.text.includes(\"parent\"))\n      const childChunk = chunks.find((c) => c.text.includes(\"child\"))\n\n      // Parent chunks should have parent link\n      for (const chunk of parentChunks) {\n        expect(chunk.link?.url).toBe(\"https://parent.com\")\n      }\n\n      // Child chunk should have child link\n      expect(childChunk?.link?.url).toBe(\"https://child.com\")\n    })\n\n    it(\"should handle mixed styled content with inherited link\", async () => {\n      testSetup = await testRender(\n        () => (\n          <text>\n            <a href=\"https://opentui.com\">\n              <b>Bold</b> <i>Italic</i> Plain\n            </a>\n          </text>\n        ),\n        {\n          width: 80,\n          height: 5,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const textRenderable = getTextRenderable(testSetup.renderer)\n      const chunks = textRenderable.textNode.gatherWithInheritedStyle()\n\n      // All text chunks should have the same link\n      const textChunks = chunks.filter((c) => c.text.trim().length > 0)\n      expect(textChunks.length).toBeGreaterThan(0)\n\n      for (const chunk of textChunks) {\n        expect(chunk.link?.url).toBe(\"https://opentui.com\")\n      }\n    })\n\n    it(\"should only apply link to content within link element\", async () => {\n      testSetup = await testRender(\n        () => (\n          <text>\n            before <a href=\"https://example.com\">linked</a> after\n          </text>\n        ),\n        {\n          width: 80,\n          height: 5,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const textRenderable = getTextRenderable(testSetup.renderer)\n      const chunks = textRenderable.textNode.gatherWithInheritedStyle()\n\n      const beforeChunk = chunks.find((c) => c.text.includes(\"before\"))\n      const linkedChunk = chunks.find((c) => c.text.includes(\"linked\"))\n      const afterChunk = chunks.find((c) => c.text.includes(\"after\"))\n\n      // Only the linked chunk should have the link\n      expect(beforeChunk?.link).toBeUndefined()\n      expect(linkedChunk?.link?.url).toBe(\"https://example.com\")\n      expect(afterChunk?.link).toBeUndefined()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/runtime-plugin-support-preload.fixture.ts",
    "content": "import { mkdtempSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport * as coreRuntime from \"@opentui/core\"\nimport * as solidJsRuntime from \"solid-js\"\nimport * as solidRuntime from \"../index\"\n\ntype FixtureState = typeof globalThis & {\n  __solidRuntimeHost__?: {\n    solid: Record<string, unknown>\n    core: Record<string, unknown>\n    coreTesting: Record<string, unknown>\n    solidJs: Record<string, unknown>\n  }\n}\n\nconst tempRoot = mkdtempSync(join(tmpdir(), \"solid-runtime-plugin-support-preload-fixture-\"))\nconst entryPath = join(tempRoot, \"entry.tsx\")\n\nconst source = [\n  'import * as solid from \"@opentui/solid\"',\n  'import * as core from \"@opentui/core\"',\n  'import * as coreTesting from \"@opentui/core/testing\"',\n  'import { createSignal } from \"solid-js\"',\n  \"const state = globalThis as { __solidRuntimeHost__?: { solid: Record<string, unknown>; core: Record<string, unknown>; coreTesting: Record<string, unknown>; solidJs: Record<string, unknown> } }\",\n  \"const [value] = createSignal('ok')\",\n  \"const makeNode = () => <text>{value()}</text>\",\n  \"const host = state.__solidRuntimeHost__\",\n  \"const checks = [\",\n  \"  `solid=${solid.extend === host?.solid.extend}`,\",\n  \"  `core=${core.engine === host?.core.engine}`,\",\n  \"  `coreTesting=${coreTesting.createTestRenderer === host?.coreTesting.createTestRenderer}`,\",\n  \"  `solidJs=${createSignal === host?.solidJs.createSignal}`,\",\n  \"  `jsx=${typeof makeNode === 'function'}`,\",\n  \"]\",\n  \"console.log(checks.join(';'))\",\n  \"export const noop = 1\",\n].join(\"\\n\")\n\nwriteFileSync(entryPath, source)\n\nconst state = globalThis as FixtureState\nstate.__solidRuntimeHost__ = {\n  solid: solidRuntime as Record<string, unknown>,\n  core: coreRuntime as Record<string, unknown>,\n  coreTesting: (await import(\"@opentui/core/testing\")) as Record<string, unknown>,\n  solidJs: solidJsRuntime as Record<string, unknown>,\n}\n\ntry {\n  await import(\"../scripts/runtime-plugin-support\")\n  await import(entryPath)\n} finally {\n  delete state.__solidRuntimeHost__\n  rmSync(tempRoot, { recursive: true, force: true })\n}\n"
  },
  {
    "path": "packages/solid/tests/runtime-plugin-support-preload.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { join } from \"node:path\"\n\ndescribe(\"solid runtime plugin support with preload\", () => {\n  it(\"rewrites external TSX modules even when the preload plugin is already active\", () => {\n    const fixturePath = join(import.meta.dir, \"runtime-plugin-support-preload.fixture.ts\")\n    const result = Bun.spawnSync([process.execPath, fixturePath], {\n      cwd: join(import.meta.dir, \"..\"),\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: process.env,\n    })\n\n    const stdout = result.stdout.toString().trim()\n    const stderr = result.stderr.toString().trim()\n\n    if (stdout) {\n      console.debug(`[runtime-plugin-support-preload.fixture] stdout:\\n${stdout}`)\n    }\n\n    if (stderr) {\n      console.debug(`[runtime-plugin-support-preload.fixture] stderr:\\n${stderr}`)\n    }\n\n    expect(result.exitCode).toBe(0)\n    expect(stdout).toContain(\"solid=true\")\n    expect(stdout).toContain(\"core=true\")\n    expect(stdout).toContain(\"coreTesting=true\")\n    expect(stdout).toContain(\"solidJs=true\")\n    expect(stdout).toContain(\"jsx=true\")\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/runtime-plugin-support.fixture.ts",
    "content": "import { mkdtempSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { plugin as registerPlugin } from \"bun\"\nimport * as coreRuntime from \"@opentui/core\"\nimport * as solidJsRuntime from \"solid-js\"\nimport * as solidRuntime from \"../index\"\nimport { resetSolidTransformPluginState } from \"../scripts/solid-plugin\"\n\ntype FixtureState = typeof globalThis & {\n  __solidRuntimeHost__?: {\n    solid: Record<string, unknown>\n    core: Record<string, unknown>\n    coreTesting: Record<string, unknown>\n    solidJs: Record<string, unknown>\n  }\n}\n\nconst tempRoot = mkdtempSync(join(tmpdir(), \"solid-runtime-plugin-support-fixture-\"))\nconst entryPath = join(tempRoot, \"entry.tsx\")\n\nconst source = [\n  'import * as solid from \"@opentui/solid\"',\n  'import * as core from \"@opentui/core\"',\n  'import * as coreTesting from \"@opentui/core/testing\"',\n  'import { createSignal } from \"solid-js\"',\n  \"const state = globalThis as { __solidRuntimeHost__?: { solid: Record<string, unknown>; core: Record<string, unknown>; coreTesting: Record<string, unknown>; solidJs: Record<string, unknown> } }\",\n  \"const [value] = createSignal('ok')\",\n  \"const makeNode = () => <text>{value()}</text>\",\n  \"const host = state.__solidRuntimeHost__\",\n  \"const checks = [\",\n  \"  `solid=${solid.extend === host?.solid.extend}`,\",\n  \"  `core=${core.engine === host?.core.engine}`,\",\n  \"  `coreTesting=${coreTesting.createTestRenderer === host?.coreTesting.createTestRenderer}`,\",\n  \"  `solidJs=${createSignal === host?.solidJs.createSignal}`,\",\n  \"  `jsx=${typeof makeNode === 'function'}`,\",\n  \"]\",\n  \"console.log(checks.join(';'))\",\n  \"export const noop = 1\",\n].join(\"\\n\")\n\nwriteFileSync(entryPath, source)\n\nconst state = globalThis as FixtureState\nstate.__solidRuntimeHost__ = {\n  solid: solidRuntime as Record<string, unknown>,\n  core: coreRuntime as Record<string, unknown>,\n  coreTesting: (await import(\"@opentui/core/testing\")) as Record<string, unknown>,\n  solidJs: solidJsRuntime as Record<string, unknown>,\n}\n\nregisterPlugin.clearAll()\nresetSolidTransformPluginState()\n\ntry {\n  await import(\"../scripts/runtime-plugin-support\")\n  await import(entryPath)\n} finally {\n  registerPlugin.clearAll()\n  delete state.__solidRuntimeHost__\n  rmSync(tempRoot, { recursive: true, force: true })\n}\n"
  },
  {
    "path": "packages/solid/tests/runtime-plugin-support.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { join } from \"node:path\"\n\ndescribe(\"solid runtime plugin support\", () => {\n  it(\"loads external TSX modules against host runtime modules\", () => {\n    const fixturePath = join(import.meta.dir, \"runtime-plugin-support.fixture.ts\")\n    const result = Bun.spawnSync([process.execPath, fixturePath], {\n      cwd: join(import.meta.dir, \"..\"),\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: process.env,\n    })\n\n    const stdout = result.stdout.toString().trim()\n    const stderr = result.stderr.toString().trim()\n\n    if (stdout) {\n      console.debug(`[runtime-plugin-support.fixture] stdout:\\n${stdout}`)\n    }\n\n    if (stderr) {\n      console.debug(`[runtime-plugin-support.fixture] stderr:\\n${stderr}`)\n    }\n\n    expect(result.exitCode).toBe(0)\n    expect(stdout).toContain(\"solid=true\")\n    expect(stdout).toContain(\"core=true\")\n    expect(stdout).toContain(\"coreTesting=true\")\n    expect(stdout).toContain(\"solidJs=true\")\n    expect(stdout).toContain(\"jsx=true\")\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/scrollbox-cleanchildren.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index\"\nimport { createSignal, For, Show, Index } from \"solid-js\"\nimport { createStore, produce, reconcile } from \"solid-js/store\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\n/**\n * Tests for the ScrollBox getParentNode fix in reconciler.ts.\n *\n * ScrollBox delegates add/remove to its internal `content` wrapper, so\n * children report `content` as their parent. The reconciler passes the\n * ScrollBox itself, causing the identity check in cleanChildren\n * (getParentNode(el) === parent) to fail — stale nodes were never removed.\n *\n * The fix makes _getParentNode walk up from `content` to return the owning\n * ScrollBox. The bug only manifests when `marker !== undefined` (multiple\n * dynamic siblings), so all tests use scrollbox with 2+ sibling expressions.\n */\n\n// Helper: count children whose id starts with a given prefix\nfunction countById(parent: { getChildren(): { id: string }[] }, prefix: string) {\n  return parent.getChildren().filter((c) => c.id.startsWith(prefix)).length\n}\n\nfunction idsOf(parent: { getChildren(): { id: string }[] }, ...prefixes: string[]) {\n  return parent\n    .getChildren()\n    .filter((c) => prefixes.some((p) => c.id.startsWith(p)))\n    .map((c) => c.id)\n}\n\ndescribe(\"scrollbox cleanChildren: multi-sibling cleanup\", () => {\n  beforeEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  // ─── Two <For> lists in scrollbox ───\n\n  describe(\"two <For> lists in scrollbox\", () => {\n    it(\"clear first list, keep second\", async () => {\n      const [headers, setHeaders] = createSignal([\"h1\", \"h2\"])\n      const [items, setItems] = createSignal([\"a\", \"b\", \"c\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={headers()}>{(h) => <box id={`h-${h}`} />}</For>\n            <For each={items()}>{(item) => <box id={`i-${item}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n      expect(countById(scrollbox, \"h-\")).toBe(2)\n      expect(countById(scrollbox, \"i-\")).toBe(3)\n\n      setHeaders([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"h-\")).toBe(0)\n      expect(countById(scrollbox, \"i-\")).toBe(3)\n    })\n\n    it(\"clear second list, keep first\", async () => {\n      const [headers, setHeaders] = createSignal([\"h1\", \"h2\"])\n      const [items, setItems] = createSignal([\"a\", \"b\", \"c\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={headers()}>{(h) => <box id={`h-${h}`} />}</For>\n            <For each={items()}>{(item) => <box id={`i-${item}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n      expect(countById(scrollbox, \"h-\")).toBe(2)\n      expect(countById(scrollbox, \"i-\")).toBe(3)\n\n      setItems([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"h-\")).toBe(2)\n      expect(countById(scrollbox, \"i-\")).toBe(0)\n    })\n\n    it(\"clear both lists simultaneously\", async () => {\n      const [headers, setHeaders] = createSignal([\"h1\", \"h2\"])\n      const [items, setItems] = createSignal([\"a\", \"b\", \"c\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={headers()}>{(h) => <box id={`h-${h}`} />}</For>\n            <For each={items()}>{(item) => <box id={`i-${item}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      setHeaders([])\n      setItems([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"h-\")).toBe(0)\n      expect(countById(scrollbox, \"i-\")).toBe(0)\n    })\n\n    it(\"clear both then repopulate both\", async () => {\n      const [headers, setHeaders] = createSignal([\"h1\", \"h2\"])\n      const [items, setItems] = createSignal([\"a\", \"b\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={headers()}>{(h) => <box id={`h-${h}`} />}</For>\n            <For each={items()}>{(item) => <box id={`i-${item}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      setHeaders([])\n      setItems([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"h-\")).toBe(0)\n      expect(countById(scrollbox, \"i-\")).toBe(0)\n\n      setHeaders([\"x1\"])\n      setItems([\"y1\", \"y2\", \"y3\"])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"h-\")).toBe(1)\n      expect(countById(scrollbox, \"i-\")).toBe(3)\n      expect(idsOf(scrollbox, \"h-\", \"i-\")).toEqual([\"h-x1\", \"i-y1\", \"i-y2\", \"i-y3\"])\n    })\n  })\n\n  // ─── Three <For> lists in scrollbox ───\n\n  describe(\"three <For> lists in scrollbox\", () => {\n    it(\"clear middle list, keep outer lists\", async () => {\n      const [aList, setAList] = createSignal([\"a1\", \"a2\"])\n      const [bList, setBList] = createSignal([\"b1\", \"b2\", \"b3\"])\n      const [cList, setCList] = createSignal([\"c1\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={aList()}>{(a) => <box id={`a-${a}`} />}</For>\n            <For each={bList()}>{(b) => <box id={`b-${b}`} />}</For>\n            <For each={cList()}>{(c) => <box id={`c-${c}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      setBList([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"a-\")).toBe(2)\n      expect(countById(scrollbox, \"b-\")).toBe(0)\n      expect(countById(scrollbox, \"c-\")).toBe(1)\n    })\n  })\n\n  // ─── Store + reconcile ───\n\n  describe(\"store + reconcile with two <For> in scrollbox\", () => {\n    it(\"reconcile both to empty\", async () => {\n      const [state, setState] = createStore<{\n        headers: { id: string }[]\n        items: { id: string }[]\n      }>({\n        headers: [{ id: \"h1\" }, { id: \"h2\" }],\n        items: [{ id: \"i1\" }, { id: \"i2\" }, { id: \"i3\" }],\n      })\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={state.headers}>{(h) => <box id={`h-${h.id}`} />}</For>\n            <For each={state.items}>{(item) => <box id={`i-${item.id}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n      expect(countById(scrollbox, \"h-\")).toBe(2)\n      expect(countById(scrollbox, \"i-\")).toBe(3)\n\n      setState(\"headers\", reconcile([]))\n      setState(\"items\", reconcile([]))\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"h-\")).toBe(0)\n      expect(countById(scrollbox, \"i-\")).toBe(0)\n    })\n\n    it(\"reconcile to completely new data\", async () => {\n      const [state, setState] = createStore<{\n        headers: { id: string }[]\n        items: { id: string }[]\n      }>({\n        headers: [{ id: \"h1\" }],\n        items: [{ id: \"i1\" }, { id: \"i2\" }],\n      })\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={state.headers}>{(h) => <box id={`h-${h.id}`} />}</For>\n            <For each={state.items}>{(item) => <box id={`i-${item.id}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      setState(\"headers\", reconcile([{ id: \"h10\" }, { id: \"h11\" }]))\n      setState(\"items\", reconcile([{ id: \"i10\" }]))\n      await testSetup.renderOnce()\n\n      expect(countById(scrollbox, \"h-\")).toBe(2)\n      expect(countById(scrollbox, \"i-\")).toBe(1)\n      expect(idsOf(scrollbox, \"h-\", \"i-\")).toEqual([\"h-h10\", \"h-h11\", \"i-i10\"])\n    })\n  })\n\n  // ─── Continuous renderer (the exact bug conditions) ───\n\n  describe(\"continuous renderer + two <For> in scrollbox\", () => {\n    it(\"clear second list with continuous renderer\", async () => {\n      const [headers, setHeaders] = createSignal([\"h1\"])\n      const [items, setItems] = createSignal<string[]>([])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={headers()}>{(h) => <box id={`h-${h}`} />}</For>\n            <For each={items()}>{(item) => <box id={`i-${item}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      testSetup.renderer.start()\n      await new Promise((r) => setTimeout(r, 30))\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      setItems([\"a\", \"b\", \"c\"])\n      await new Promise((r) => setTimeout(r, 50))\n      expect(countById(scrollbox, \"i-\")).toBe(3)\n      expect(countById(scrollbox, \"h-\")).toBe(1)\n\n      setItems([])\n      await new Promise((r) => setTimeout(r, 50))\n      expect(countById(scrollbox, \"i-\")).toBe(0)\n      expect(countById(scrollbox, \"h-\")).toBe(1)\n\n      testSetup.renderer.stop()\n    })\n\n    it(\"produce + reconcile clear\", async () => {\n      const [state, setState] = createStore<{\n        tags: { id: string }[]\n        rows: { id: string }[]\n      }>({\n        tags: [{ id: \"t1\" }],\n        rows: [],\n      })\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={state.tags}>{(tag) => <box id={`tag-${tag.id}`} />}</For>\n            <For each={state.rows}>{(row) => <box id={`row-${row.id}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      testSetup.renderer.start()\n      await new Promise((r) => setTimeout(r, 30))\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      for (let i = 1; i <= 4; i++) {\n        setState(\n          produce((s) => {\n            s.rows.push({ id: `r${i}` })\n          }),\n        )\n        await new Promise((r) => setTimeout(r, 15))\n      }\n\n      await new Promise((r) => setTimeout(r, 50))\n      expect(countById(scrollbox, \"row-\")).toBe(4)\n      expect(countById(scrollbox, \"tag-\")).toBe(1)\n\n      setState(\"rows\", reconcile([]))\n      await new Promise((r) => setTimeout(r, 50))\n\n      expect(countById(scrollbox, \"row-\")).toBe(0)\n      expect(countById(scrollbox, \"tag-\")).toBe(1)\n\n      testSetup.renderer.stop()\n    })\n\n    it(\"multiple clear-repopulate cycles\", async () => {\n      const [state, setState] = createStore<{\n        sys: { id: string }[]\n        data: { id: string }[]\n      }>({\n        sys: [{ id: \"s0\" }],\n        data: [],\n      })\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={state.sys}>{(s) => <box id={`sys-${s.id}`} />}</For>\n            <For each={state.data}>{(d) => <box id={`data-${d.id}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      testSetup.renderer.start()\n      await new Promise((r) => setTimeout(r, 30))\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      for (let cycle = 1; cycle <= 3; cycle++) {\n        // Populate via produce\n        for (let i = 1; i <= 3; i++) {\n          setState(\n            produce((s) => {\n              s.data.push({ id: `d${cycle}-${i}` })\n            }),\n          )\n          await new Promise((r) => setTimeout(r, 10))\n        }\n        await new Promise((r) => setTimeout(r, 30))\n        expect(countById(scrollbox, \"data-\")).toBe(3)\n\n        // Clear via reconcile\n        setState(\"data\", reconcile([]))\n        await new Promise((r) => setTimeout(r, 50))\n        expect(countById(scrollbox, \"data-\")).toBe(0)\n        expect(countById(scrollbox, \"sys-\")).toBe(1)\n      }\n\n      testSetup.renderer.stop()\n    })\n  })\n\n  // ─── <Show> creates markers too — test cleanup with <For> sibling ───\n\n  describe(\"<Show> + <For> in scrollbox (Show creates marker)\", () => {\n    it(\"<For> + <Show> with <For>: clear inner list\", async () => {\n      const [headers, setHeaders] = createSignal([\"h1\", \"h2\"])\n      const [showItems, setShowItems] = createSignal(true)\n      const [items, setItems] = createSignal([\"a\", \"b\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={headers()}>{(h) => <box id={`h-${h}`} />}</For>\n            <Show when={showItems()}>\n              <For each={items()}>{(item) => <box id={`i-${item}`} />}</For>\n            </Show>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n      expect(countById(scrollbox, \"h-\")).toBe(2)\n      expect(countById(scrollbox, \"i-\")).toBe(2)\n\n      setShowItems(false)\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"h-\")).toBe(2)\n      expect(countById(scrollbox, \"i-\")).toBe(0)\n\n      setShowItems(true)\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"h-\")).toBe(2)\n      expect(countById(scrollbox, \"i-\")).toBe(2)\n    })\n\n    it(\"<Show> toggling between two <For> lists\", async () => {\n      const [mode, setMode] = createSignal<\"a\" | \"b\">(\"a\")\n      const listA = [\"a1\", \"a2\", \"a3\"]\n      const listB = [\"b1\", \"b2\"]\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <Show when={mode() === \"a\"}>\n              <For each={listA}>{(item) => <box id={`a-${item}`} />}</For>\n            </Show>\n            <Show when={mode() === \"b\"}>\n              <For each={listB}>{(item) => <box id={`b-${item}`} />}</For>\n            </Show>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n      expect(countById(scrollbox, \"a-\")).toBe(3)\n      expect(countById(scrollbox, \"b-\")).toBe(0)\n\n      setMode(\"b\")\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"a-\")).toBe(0)\n      expect(countById(scrollbox, \"b-\")).toBe(2)\n\n      setMode(\"a\")\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"a-\")).toBe(3)\n      expect(countById(scrollbox, \"b-\")).toBe(0)\n    })\n  })\n\n  // ─── Static children create markers for adjacent <For> ───\n\n  describe(\"static children + <For> in scrollbox\", () => {\n    it(\"static before <For>: clear list keeps static\", async () => {\n      const [items, setItems] = createSignal([\"a\", \"b\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <box id=\"static-header\">\n              <text>Header</text>\n            </box>\n            <For each={items()}>{(item) => <box id={`item-${item}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n      expect(countById(scrollbox, \"static-header\")).toBe(1)\n      expect(countById(scrollbox, \"item-\")).toBe(2)\n\n      setItems([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"static-header\")).toBe(1)\n      expect(countById(scrollbox, \"item-\")).toBe(0)\n\n      setItems([\"x\", \"y\", \"z\"])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"static-header\")).toBe(1)\n      expect(countById(scrollbox, \"item-\")).toBe(3)\n    })\n\n    it(\"static between two <For>: clear both keeps divider\", async () => {\n      const [topItems, setTopItems] = createSignal([\"t1\", \"t2\"])\n      const [bottomItems, setBottomItems] = createSignal([\"b1\", \"b2\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={topItems()}>{(item) => <box id={`top-${item}`} />}</For>\n            <box id=\"divider\">\n              <text>---</text>\n            </box>\n            <For each={bottomItems()}>{(item) => <box id={`bot-${item}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      setTopItems([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"top-\")).toBe(0)\n      expect(countById(scrollbox, \"divider\")).toBe(1)\n      expect(countById(scrollbox, \"bot-\")).toBe(2)\n\n      setBottomItems([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"divider\")).toBe(1)\n      expect(countById(scrollbox, \"bot-\")).toBe(0)\n    })\n\n    it(\"static after <For>: clear list keeps footer\", async () => {\n      const [items, setItems] = createSignal([\"a\", \"b\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <For each={items()}>{(item) => <box id={`item-${item}`} />}</For>\n            <box id=\"static-footer\">\n              <text>Footer</text>\n            </box>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n\n      setItems([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"item-\")).toBe(0)\n      expect(countById(scrollbox, \"static-footer\")).toBe(1)\n    })\n  })\n\n  // ─── <Index> + <For> siblings (Index also creates marker) ───\n\n  describe(\"<Index> + <For> siblings in scrollbox\", () => {\n    it(\"clear <Index>, keep <For>\", async () => {\n      const [indexItems, setIndexItems] = createSignal([\"x\", \"y\"])\n      const [forItems, setForItems] = createSignal([\"a\", \"b\"])\n\n      testSetup = await testRender(\n        () => (\n          <scrollbox id=\"scroll\" flexGrow={1}>\n            <Index each={indexItems()}>{(item, idx) => <box id={`idx-${idx}`} />}</Index>\n            <For each={forItems()}>{(item) => <box id={`for-${item}`} />}</For>\n          </scrollbox>\n        ),\n        { width: 40, height: 20 },\n      )\n\n      await testSetup.renderOnce()\n      const scrollbox = testSetup.renderer.root.findDescendantById(\"scroll\")!\n      expect(countById(scrollbox, \"idx-\")).toBe(2)\n      expect(countById(scrollbox, \"for-\")).toBe(2)\n\n      setIndexItems([])\n      await testSetup.renderOnce()\n      expect(countById(scrollbox, \"idx-\")).toBe(0)\n      expect(countById(scrollbox, \"for-\")).toBe(2)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/scrollbox-content.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal, createMemo, createEffect, For } from \"solid-js\"\nimport type { ScrollBoxRenderable } from \"../../core/src/renderables/index.js\"\nimport { SyntaxStyle } from \"../../core/src/syntax-style.js\"\nimport { MockTreeSitterClient } from \"@opentui/core/testing\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\nlet mockTreeSitterClient: MockTreeSitterClient\n\ndescribe(\"ScrollBox Content Visibility\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n    mockTreeSitterClient = new MockTreeSitterClient()\n    mockTreeSitterClient.setMockResult({ highlights: [] })\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  it(\"maintains content visibility when adding many items and scrolling\", async () => {\n    const [count, setCount] = createSignal(0)\n    const messages = createMemo(() => Array.from({ length: count() }, (_, i) => `Message ${i + 1}`))\n\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\" gap={1}>\n          <box flexShrink={0}>\n            <text>Header Content</text>\n          </box>\n          <scrollbox ref={(r) => (scrollRef = r)} focused stickyScroll={true} stickyStart=\"bottom\" flexGrow={1}>\n            <For each={messages()}>\n              {(msg) => (\n                <box marginTop={1} marginBottom={1}>\n                  <text>{msg}</text>\n                </box>\n              )}\n            </For>\n          </scrollbox>\n          <box flexShrink={0}>\n            <text>Footer Content</text>\n          </box>\n        </box>\n      ),\n      {\n        width: 40,\n        height: 20,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const initialFrame = testSetup.captureCharFrame()\n    expect(initialFrame).toContain(\"Header Content\")\n    expect(initialFrame).toContain(\"Footer Content\")\n\n    setCount(100)\n    await testSetup.renderOnce()\n\n    if (scrollRef) {\n      scrollRef.scrollTo(scrollRef.scrollHeight)\n      await testSetup.renderOnce()\n    }\n\n    const frameAfterScroll = testSetup.captureCharFrame()\n\n    expect(frameAfterScroll).toContain(\"Header Content\")\n    expect(frameAfterScroll).toContain(\"Footer Content\")\n\n    const hasMessageContent = /Message \\d+/.test(frameAfterScroll)\n    expect(hasMessageContent).toBe(true)\n\n    const nonWhitespaceChars = frameAfterScroll.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(20)\n  })\n\n  it(\"should maintain content visibility with code blocks in scrollbox\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    const codeBlock = `\n\n# HELLO\n\nworld\n\n## HELLO World\n\n\\`\\`\\`html\n<div\n  class=\"min-h-screen bg-gradient-to-br from-amber-50 via-orange-50 to-rose-50 relative overflow-hidden\"\n>\n  <!-- Sakura Petals Background Animation -->\n  <div class=\"absolute inset-0 pointer-events-none\">\n    <div class=\"sakura-petal absolute top-10 left-20 animate-pulse opacity-60\">\n      🌸\n    </div>\n    <div\n      class=\"sakura-petal absolute top-1/2 right-20 animate-pulse opacity-45\"\n      style=\"animation-delay: 1.5s\"\n    >\n      🌸\n    </div>\n    <div\n      class=\"sakura-petal absolute bottom-40 right-1/3 animate-pulse opacity-55\"\n      style=\"animation-delay: 0.5s\"\n    >\n      🌸\n    </div>\n  </div>\n/div>\n\\`\\`\\`\n\n\n`\n\n    const [count, setCount] = createSignal(0)\n    const messages = createMemo(() => Array.from({ length: count() }, (_, i) => codeBlock))\n\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\" gap={1}>\n          <box flexShrink={0}>\n            <text>Some visual content</text>\n          </box>\n          <scrollbox ref={(r) => (scrollRef = r)} focused stickyScroll={true} stickyStart=\"bottom\" flexGrow={1}>\n            <For each={messages()}>\n              {(code) => (\n                <box marginTop={2} marginBottom={2}>\n                  <code\n                    drawUnstyledText={false}\n                    syntaxStyle={syntaxStyle}\n                    content={code}\n                    filetype=\"markdown\"\n                    treeSitterClient={mockTreeSitterClient}\n                  />\n                </box>\n              )}\n            </For>\n          </scrollbox>\n          <box flexShrink={0}>\n            <text>Some visual content</text>\n          </box>\n        </box>\n      ),\n      {\n        width: 80,\n        height: 30,\n      },\n    )\n\n    await testSetup.renderOnce()\n    const initialFrame = testSetup.captureCharFrame()\n    expect(initialFrame).toContain(\"Some visual content\")\n\n    setCount(100)\n    await testSetup.renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    if (scrollRef) {\n      scrollRef.scrollTo(scrollRef.scrollHeight)\n      await testSetup.renderOnce()\n    }\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frameAfterScroll = testSetup.captureCharFrame()\n\n    expect(frameAfterScroll).toContain(\"Some visual content\")\n\n    const hasCodeContent =\n      frameAfterScroll.includes(\"HELLO\") ||\n      frameAfterScroll.includes(\"world\") ||\n      frameAfterScroll.includes(\"<div\") ||\n      frameAfterScroll.includes(\"```\") ||\n      frameAfterScroll.includes(\"class=\")\n\n    expect(hasCodeContent).toBe(true)\n\n    const nonWhitespaceChars = frameAfterScroll.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(50)\n  })\n\n  it(\"maintains visibility with many Code elements\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    const [count, setCount] = createSignal(0)\n\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\" gap={1}>\n          <box flexShrink={0}>\n            <text>Header</text>\n          </box>\n          <scrollbox ref={(r) => (scrollRef = r)} focused stickyScroll={true} stickyStart=\"bottom\" flexGrow={1}>\n            <For each={Array.from({ length: count() }, (_, i) => i)}>\n              {(i) => (\n                <box marginTop={1} marginBottom={1}>\n                  <code\n                    drawUnstyledText={false}\n                    syntaxStyle={syntaxStyle}\n                    content={`Item ${i}`}\n                    filetype=\"markdown\"\n                    treeSitterClient={mockTreeSitterClient}\n                  />\n                </box>\n              )}\n            </For>\n          </scrollbox>\n          <box flexShrink={0}>\n            <text>Footer</text>\n          </box>\n        </box>\n      ),\n      {\n        width: 40,\n        height: 20,\n      },\n    )\n\n    await testSetup.renderOnce()\n\n    setCount(50)\n    await testSetup.renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    if (scrollRef) {\n      scrollRef.scrollTo(scrollRef.scrollHeight)\n    }\n    await testSetup.renderOnce()\n\n    mockTreeSitterClient.resolveAllHighlightOnce()\n    await new Promise((resolve) => setTimeout(resolve, 10))\n    await testSetup.renderOnce()\n\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"Header\")\n    expect(frame).toContain(\"Footer\")\n\n    const hasItems = /Item \\d+/.test(frame)\n    expect(hasItems).toBe(true)\n\n    const nonWhitespaceChars = frame.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(18)\n  })\n\n  it(\"should maintain content when rapidly updating and scrolling\", async () => {\n    const [items, setItems] = createSignal<string[]>([])\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"column\">\n          <scrollbox ref={(r) => (scrollRef = r)} focused stickyScroll={true} flexGrow={1}>\n            <For each={items()}>\n              {(item) => (\n                <box>\n                  <text>{item}</text>\n                </box>\n              )}\n            </For>\n          </scrollbox>\n        </box>\n      ),\n      {\n        width: 40,\n        height: 15,\n      },\n    )\n\n    await testSetup.renderOnce()\n\n    for (let i = 0; i < 50; i++) {\n      setItems((prev) => [...prev, `Item ${i + 1}`])\n    }\n    await testSetup.renderOnce()\n\n    if (scrollRef) {\n      scrollRef.scrollTo(scrollRef.scrollHeight)\n      await testSetup.renderOnce()\n    }\n\n    const frame = testSetup.captureCharFrame()\n\n    const hasItems = /Item \\d+/.test(frame)\n    expect(hasItems).toBe(true)\n\n    const nonWhitespaceChars = frame.replace(/\\s/g, \"\").length\n    expect(nonWhitespaceChars).toBeGreaterThan(10)\n  })\n\n  it(\"does not split 'uses' in last message between widths 80-100\", async () => {\n    const syntaxStyle = SyntaxStyle.fromTheme([])\n    const [items, setItems] = createSignal<string[]>([])\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    const opencodeMessage =\n      \"We use `-c core.autocrlf=false` in multiple spots as a defensive override, even though the snapshot repo is configured once.\\n\\n\" +\n      \"Why duplicate it:\\n\" +\n      \"- Repo config only exists after `Snapshot.track()` successfully initializes the snapshot git dir. Commands like `diff`/`show` can run later, but the override guarantees consistent behavior even if init was skipped, failed, or the git dir was pruned/rewritten.\\n\" +\n      \"- It protects against a user\\u2019s global/system Git config that might otherwise override or interfere.\\n\" +\n      \"- It\\u2019s especially important on commands that output content (`diff`, `show`, `numstat`) because newline conversion changes the text we return.\\n\\n\" +\n      \"So: the per\\u2011repo config is the baseline; the `-c` flags are a \\u201Cdon\\u2019t depend on baseline\\u201D guard for commands where output consistency matters. Revert uses checkout, which is less about output formatting and already respects the repo config, so it didn\\u2019t get the extra guard. If you want stricter consistency, we can add `-c core.autocrlf=false` there too.\"\n\n    testSetup = await testRender(\n      () => (\n        <box flexDirection=\"row\">\n          <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>\n            <box flexShrink={0}>\n              <text>Header</text>\n            </box>\n            <scrollbox\n              ref={(r) => (scrollRef = r)}\n              stickyScroll={true}\n              stickyStart=\"bottom\"\n              flexGrow={1}\n              viewportOptions={{ paddingRight: 1 }}\n              verticalScrollbarOptions={{ paddingLeft: 1, visible: true }}\n            >\n              <For each={items()}>\n                {(item) => (\n                  <box marginTop={1} flexShrink={0} paddingLeft={3}>\n                    <code\n                      filetype=\"markdown\"\n                      drawUnstyledText={false}\n                      streaming={true}\n                      syntaxStyle={syntaxStyle}\n                      content={item.trim()}\n                    />\n                  </box>\n                )}\n              </For>\n            </scrollbox>\n            <box flexShrink={0}>\n              <text>Prompt</text>\n            </box>\n          </box>\n        </box>\n      ),\n      {\n        width: 100,\n        height: 24,\n      },\n    )\n\n    await testSetup.renderOnce()\n\n    const filler = Array.from({ length: 12 }, (_, i) => `Message ${i + 1}`)\n    setItems([...filler, opencodeMessage])\n    await testSetup.renderOnce()\n    await Bun.sleep(20)\n    await testSetup.renderOnce()\n\n    if (scrollRef) {\n      scrollRef.scrollTo(scrollRef.scrollHeight)\n      await testSetup.renderOnce()\n    }\n\n    const splitMatches: Array<{ width: number; line: string; nextLine: string; scrollTop: number }> = []\n    const normalize = (line: string) => line.replace(/^\\s+/, \"\").replace(/\\s+$/, \"\")\n\n    const scanForSplit = (width: number, scrollTop: number) => {\n      const lines = testSetup.captureCharFrame().split(\"\\n\")\n      let hasRevert = false\n      for (let i = 0; i < lines.length - 1; i++) {\n        const current = normalize(lines[i])\n        const next = normalize(lines[i + 1])\n        if (current.includes(\"Revert uses\")) {\n          hasRevert = true\n        } else if (current.endsWith(\"Revert\") && next.startsWith(\"uses \")) {\n          hasRevert = true\n        }\n        const splitU = current.endsWith(\"Revert u\") && next.startsWith(\"ses checkout\")\n        const splitUs = current.endsWith(\"Revert us\") && next.startsWith(\"es checkout\")\n        const splitUse = current.endsWith(\"Revert use\") && next.startsWith(\"s checkout\")\n        if (splitU || splitUs || splitUse) {\n          splitMatches.push({ width, line: current, nextLine: next, scrollTop })\n          return { foundSplit: true, hasRevert }\n        }\n      }\n      return { foundSplit: false, hasRevert }\n    }\n\n    for (let width = 100; width >= 80; width -= 1) {\n      testSetup.resize(width, 24)\n      await testSetup.renderOnce()\n      await Bun.sleep(20)\n      await testSetup.renderOnce()\n      if (scrollRef) {\n        scrollRef.scrollTo(scrollRef.scrollHeight)\n        await testSetup.renderOnce()\n      }\n\n      let foundRevert = false\n      if (scrollRef) {\n        const maxScroll = Math.max(0, scrollRef.scrollHeight - scrollRef.viewport.height)\n        const step = Math.max(1, Math.floor(scrollRef.viewport.height / 3))\n\n        for (let scrollTop = maxScroll; scrollTop >= 0; scrollTop -= step) {\n          scrollRef.scrollTo(scrollTop)\n          await testSetup.renderOnce()\n          const { foundSplit, hasRevert } = scanForSplit(width, scrollTop)\n          if (hasRevert) {\n            foundRevert = true\n          }\n          if (foundSplit) {\n            foundRevert = true\n            break\n          }\n        }\n      }\n    }\n\n    expect(splitMatches).toEqual([])\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/slot.test.tsx",
    "content": "import { afterEach, beforeEach, describe, expect, it } from \"bun:test\"\nimport { createTestRenderer, type TestRendererOptions } from \"@opentui/core/testing\"\nimport { createContext, createComponent, createSignal, onCleanup, onMount, useContext, type JSX } from \"solid-js\"\nimport { createSlot, createSolidSlotRegistry, Slot, type SolidPlugin } from \"../src/plugins/slot\"\nimport { _render as renderInternal } from \"../src/reconciler\"\nimport { RendererContext } from \"../src/elements\"\n\ninterface AppSlots {\n  statusbar: { user: string }\n  sidebar: { items: string[] }\n}\n\nconst hostContext = {\n  appName: \"solid-slot-tests\",\n  version: \"1.0.0\",\n}\n\nlet testSetup: Awaited<ReturnType<typeof createTestRenderer>>\n\nasync function setupSlotTest(\n  createNode: (registry: ReturnType<typeof createSolidSlotRegistry<AppSlots>>) => JSX.Element,\n  options: TestRendererOptions,\n) {\n  let isDisposed = false\n  let dispose: (() => void) | undefined\n\n  const setup = await createTestRenderer({\n    ...options,\n    onDestroy: () => {\n      if (!isDisposed) {\n        isDisposed = true\n        dispose?.()\n      }\n      options.onDestroy?.()\n    },\n  })\n\n  const registry = createSolidSlotRegistry<AppSlots>(setup.renderer, hostContext)\n\n  dispose = renderInternal(\n    () =>\n      createComponent(RendererContext.Provider, {\n        get value() {\n          return setup.renderer\n        },\n        get children() {\n          return createNode(registry)\n        },\n      }),\n    setup.renderer.root,\n  )\n\n  return { setup, registry }\n}\n\ndescribe(\"Solid Slot System\", () => {\n  beforeEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  it(\"reuses one registry per renderer and rejects different context\", async () => {\n    const setup = await createTestRenderer({ width: 20, height: 4 })\n    testSetup = setup\n\n    const context = { appName: \"solid-slot-tests\", version: \"1.0.0\" }\n    const first = createSolidSlotRegistry<AppSlots, typeof context>(setup.renderer, context)\n    const second = createSolidSlotRegistry<AppSlots, typeof context>(setup.renderer, context)\n\n    expect(first).toBe(second)\n\n    expect(() => {\n      createSolidSlotRegistry<AppSlots, typeof context>(setup.renderer, { appName: \"other\", version: \"2.0.0\" })\n    }).toThrow(\"different context\")\n  })\n\n  it(\"renders fallback content when no plugin matches\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        const AppSlot = Slot<AppSlots, typeof hostContext>\n        return (\n          <AppSlot registry={registry} name=\"statusbar\" user=\"sam\">\n            <text>fallback-only</text>\n          </AppSlot>\n        )\n      },\n      { width: 50, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"fallback-only\")\n  })\n\n  it(\"appends plugin output after fallback content by default\", async () => {\n    const plugin: SolidPlugin<AppSlots, typeof hostContext> = {\n      id: \"append-plugin\",\n      slots: {\n        statusbar(ctx, props) {\n          return <text>{`plugin:${ctx.appName}:${props.user}`}</text>\n        },\n      },\n    }\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register(plugin)\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"ava\">\n            <text>base-content</text>\n          </Slot>\n        )\n      },\n      { width: 60, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"base-content\")\n    expect(frame).toContain(\"plugin:solid-slot-tests:ava\")\n  })\n\n  it(\"replace mode hides fallback and renders all ordered plugins\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"late\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <text>late-plugin</text>\n            },\n          },\n        })\n\n        registry.register({\n          id: \"early\",\n          order: 0,\n          slots: {\n            statusbar() {\n              return <text>early-plugin</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"replace\">\n            <text>replace-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 40, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"early-plugin\")\n    expect(frame).toContain(\"late-plugin\")\n    expect(frame).not.toContain(\"replace-fallback\")\n  })\n\n  it(\"replace mode does not invoke fallback components when plugin content wins\", async () => {\n    const fallbackLifecycle: string[] = []\n\n    const FallbackProbe = () => {\n      fallbackLifecycle.push(\"render\")\n\n      onMount(() => {\n        fallbackLifecycle.push(\"mount\")\n      })\n\n      onCleanup(() => {\n        fallbackLifecycle.push(\"cleanup\")\n      })\n\n      return <text>fallback-probe</text>\n    }\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"replace-plugin\",\n          slots: {\n            statusbar() {\n              return <text>plugin-only</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"replace\">\n            <FallbackProbe />\n          </Slot>\n        )\n      },\n      { width: 40, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"plugin-only\")\n    expect(frame).not.toContain(\"fallback-probe\")\n    expect(fallbackLifecycle).toEqual([])\n  })\n\n  it(\"single_winner mode renders only the highest-priority plugin\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"late\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <text>late-plugin</text>\n            },\n          },\n        })\n\n        registry.register({\n          id: \"early\",\n          order: 0,\n          slots: {\n            statusbar() {\n              return <text>early-plugin</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"single_winner\">\n            <text>single-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 40, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"early-plugin\")\n    expect(frame).not.toContain(\"late-plugin\")\n    expect(frame).not.toContain(\"single-fallback\")\n  })\n\n  it(\"replace mode keeps healthy plugin output when another plugin fails\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"broken-plugin\",\n          order: 0,\n          slots: {\n            statusbar() {\n              throw new Error(\"broken render\")\n            },\n          },\n        })\n\n        registry.register({\n          id: \"healthy-plugin\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <text>healthy-plugin</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"replace\">\n            <text>replace-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 50, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"healthy-plugin\")\n    expect(frame).not.toContain(\"replace-fallback\")\n  })\n\n  it(\"single_winner mode falls back when highest-priority plugin fails\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"broken-winner\",\n          order: 0,\n          slots: {\n            statusbar() {\n              throw new Error(\"winner failed\")\n            },\n          },\n        })\n\n        registry.register({\n          id: \"healthy-second\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <text>healthy-second</text>\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"lee\" mode=\"single_winner\">\n            <text>single-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 50, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"single-fallback\")\n    expect(frame).not.toContain(\"healthy-second\")\n  })\n\n  it(\"reacts to plugin registration and unregistering\", async () => {\n    const { setup, registry } = await setupSlotTest(\n      (slotRegistry) => {\n        const Slot = createSlot(slotRegistry)\n        return (\n          <Slot name=\"statusbar\" user=\"kai\" mode=\"replace\">\n            <text>dynamic-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 40, height: 6 },\n    )\n    testSetup = setup\n\n    const plugin: SolidPlugin<AppSlots> = {\n      id: \"dynamic-plugin\",\n      slots: {\n        statusbar() {\n          return <text>dynamic-plugin</text>\n        },\n      },\n    }\n\n    await testSetup.renderOnce()\n    expect(testSetup.captureCharFrame()).toContain(\"dynamic-fallback\")\n\n    registry.register(plugin)\n    await testSetup.renderOnce()\n    const withPlugin = testSetup.captureCharFrame()\n    expect(withPlugin).toContain(\"dynamic-plugin\")\n    expect(withPlugin).not.toContain(\"dynamic-fallback\")\n\n    registry.unregister(\"dynamic-plugin\")\n    await testSetup.renderOnce()\n    const withoutPlugin = testSetup.captureCharFrame()\n    expect(withoutPlugin).toContain(\"dynamic-fallback\")\n    expect(withoutPlugin).not.toContain(\"dynamic-plugin\")\n  })\n\n  it(\"switches rendered slot when props.name changes\", async () => {\n    let switchSlot: (() => void) | null = null\n\n    const DynamicNameHarness = (props: { registry: ReturnType<typeof createSolidSlotRegistry<AppSlots>> }) => {\n      const Slot = createSlot(props.registry)\n      const [slotName, setSlotName] = createSignal<keyof AppSlots>(\"statusbar\")\n\n      switchSlot = () => {\n        setSlotName((current) => (current === \"statusbar\" ? \"sidebar\" : \"statusbar\"))\n      }\n\n      const dynamicProps = () =>\n        slotName() === \"statusbar\"\n          ? ({ name: \"statusbar\", user: \"sam\", mode: \"replace\" } as const)\n          : ({ name: \"sidebar\", items: [\"one\"], mode: \"replace\" } as const)\n\n      return (\n        <Slot {...(dynamicProps() as any)}>\n          <text>dynamic-name-fallback</text>\n        </Slot>\n      )\n    }\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"status-plugin\",\n          slots: {\n            statusbar() {\n              return <text>status-plugin</text>\n            },\n          },\n        })\n\n        registry.register({\n          id: \"sidebar-plugin\",\n          slots: {\n            sidebar() {\n              return <text>sidebar-plugin</text>\n            },\n          },\n        })\n\n        return <DynamicNameHarness registry={registry} />\n      },\n      { width: 60, height: 8 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const initialFrame = testSetup.captureCharFrame()\n    expect(initialFrame).toContain(\"status-plugin\")\n    expect(initialFrame).not.toContain(\"sidebar-plugin\")\n\n    switchSlot?.()\n\n    await testSetup.renderOnce()\n    const switchedFrame = testSetup.captureCharFrame()\n    expect(switchedFrame).toContain(\"sidebar-plugin\")\n    expect(switchedFrame).not.toContain(\"status-plugin\")\n    expect(switchedFrame).not.toContain(\"dynamic-name-fallback\")\n  })\n\n  it(\"keeps plugin identity stable when append order changes\", async () => {\n    const mountLog: string[] = []\n\n    const StatefulPluginNode = (props: { pluginId: string }) => {\n      const createdBy = props.pluginId\n\n      onMount(() => {\n        mountLog.push(`mount:${props.pluginId}:${createdBy}`)\n      })\n\n      onCleanup(() => {\n        mountLog.push(`unmount:${props.pluginId}:${createdBy}`)\n      })\n\n      return <text>{`${props.pluginId}:${createdBy}`}</text>\n    }\n\n    const { setup, registry } = await setupSlotTest(\n      (slotRegistry) => {\n        slotRegistry.register({\n          id: \"alpha\",\n          order: 0,\n          slots: {\n            statusbar() {\n              return <StatefulPluginNode pluginId=\"alpha\" />\n            },\n          },\n        })\n\n        slotRegistry.register({\n          id: \"beta\",\n          order: 10,\n          slots: {\n            statusbar() {\n              return <StatefulPluginNode pluginId=\"beta\" />\n            },\n          },\n        })\n\n        const Slot = createSlot(slotRegistry)\n        return <Slot name=\"statusbar\" user=\"sam\" />\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const beforeReorder = testSetup.captureCharFrame()\n\n    expect(beforeReorder).toContain(\"alpha:alpha\")\n    expect(beforeReorder).toContain(\"beta:beta\")\n\n    registry.updateOrder(\"beta\", -1)\n\n    await testSetup.renderOnce()\n    const afterReorder = testSetup.captureCharFrame()\n\n    expect(afterReorder).toContain(\"beta:beta\")\n    expect(afterReorder).toContain(\"alpha:alpha\")\n    expect(afterReorder).not.toContain(\"beta:alpha\")\n    expect(afterReorder).not.toContain(\"alpha:beta\")\n    expect(mountLog).toEqual([\"mount:alpha:alpha\", \"mount:beta:beta\"])\n  })\n\n  it(\"renders plugin nodes within provider context\", async () => {\n    const ValueContext = createContext(\"missing\")\n\n    const ContextReader = () => {\n      const value = useContext(ValueContext)\n      return <text>{`ctx:${value}`}</text>\n    }\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"context-plugin\",\n          slots: {\n            statusbar() {\n              return <ContextReader />\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <ValueContext.Provider value=\"inside-provider\">\n            <Slot name=\"statusbar\" user=\"max\" />\n          </ValueContext.Provider>\n        )\n      },\n      { width: 60, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n    expect(frame).toContain(\"ctx:inside-provider\")\n  })\n\n  it(\"captures plugin render invocation errors and reports metadata\", async () => {\n    const errors: string[] = []\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.source}:${event.error.message}`)\n        })\n\n        registry.register({\n          id: \"broken-plugin\",\n          slots: {\n            statusbar() {\n              throw new Error(\"render failed\")\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>fallback-visible</text>\n          </Slot>\n        )\n      },\n      { width: 70, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"fallback-visible\")\n    expect(errors).toEqual([\"broken-plugin:statusbar:render:solid:render failed\"])\n  })\n\n  it(\"replace mode falls back when plugin fails and no placeholder is configured\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"broken-plugin\",\n          slots: {\n            statusbar() {\n              throw new Error(\"render failed\")\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\" mode=\"replace\">\n            <text>replace-fallback-visible</text>\n          </Slot>\n        )\n      },\n      { width: 70, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"replace-fallback-visible\")\n  })\n\n  it(\"replace mode falls back when plugin renders empty output\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"empty-plugin\",\n          slots: {\n            statusbar() {\n              return <></>\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\" mode=\"replace\">\n            <text>replace-fallback-empty</text>\n          </Slot>\n        )\n      },\n      { width: 70, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"replace-fallback-empty\")\n  })\n\n  it(\"replace mode falls back when plugin subtree crashes and no placeholder is configured\", async () => {\n    const errors: string[] = []\n\n    const ExplodingPluginNode = () => {\n      throw new Error(\"replace subtree exploded\")\n    }\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.source}:${event.error.message}`)\n        })\n\n        registry.register({\n          id: \"replace-exploding-plugin\",\n          slots: {\n            statusbar() {\n              return <ExplodingPluginNode />\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\" mode=\"replace\">\n            <text>replace-safe-fallback</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"replace-safe-fallback\")\n    expect(errors).toContain(\"replace-exploding-plugin:statusbar:render:solid:replace subtree exploded\")\n  })\n\n  it(\"reports error_placeholder and keeps fallback when placeholder throws after plugin render error\", async () => {\n    const errors: string[] = []\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.source}:${event.error.message}`)\n        })\n\n        registry.register({\n          id: \"broken-plugin\",\n          slots: {\n            statusbar() {\n              throw new Error(\"render failed\")\n            },\n          },\n        })\n\n        const Slot = createSlot(registry, {\n          pluginFailurePlaceholder() {\n            throw new Error(\"placeholder failed\")\n          },\n        })\n\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>fallback-visible</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"fallback-visible\")\n    expect(errors).toContain(\"broken-plugin:statusbar:render:solid:render failed\")\n    expect(errors).toContain(\"broken-plugin:statusbar:error_placeholder:solid:placeholder failed\")\n  })\n\n  it(\"reports error_placeholder and keeps fallback when placeholder throws after subtree crash\", async () => {\n    const errors: string[] = []\n\n    const ExplodingPluginNode = () => {\n      throw new Error(\"component exploded\")\n    }\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.source}:${event.error.message}`)\n        })\n\n        registry.register({\n          id: \"exploding-plugin\",\n          slots: {\n            statusbar() {\n              return <ExplodingPluginNode />\n            },\n          },\n        })\n\n        const Slot = createSlot(registry, {\n          pluginFailurePlaceholder() {\n            throw new Error(\"placeholder crashed\")\n          },\n        })\n\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>safe-host-content</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"safe-host-content\")\n    expect(errors).toContain(\"exploding-plugin:statusbar:render:solid:component exploded\")\n    expect(errors).toContain(\"exploding-plugin:statusbar:error_placeholder:solid:placeholder crashed\")\n  })\n\n  it(\"catches plugin subtree errors via per-plugin boundary\", async () => {\n    const errors: string[] = []\n\n    const ExplodingPluginNode = () => {\n      throw new Error(\"component exploded\")\n    }\n\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.onPluginError((event) => {\n          errors.push(`${event.pluginId}:${event.slot}:${event.phase}:${event.error.message}`)\n        })\n\n        registry.register({\n          id: \"exploding-component-plugin\",\n          slots: {\n            statusbar() {\n              return <ExplodingPluginNode />\n            },\n          },\n        })\n\n        const Slot = createSlot(registry)\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>safe-host-content</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"safe-host-content\")\n    expect(errors).toEqual([\"exploding-component-plugin:statusbar:render:component exploded\"])\n  })\n\n  it(\"renders optional plugin failure placeholder when configured\", async () => {\n    const { setup } = await setupSlotTest(\n      (registry) => {\n        registry.register({\n          id: \"broken-plugin\",\n          slots: {\n            statusbar() {\n              throw new Error(\"render failed\")\n            },\n          },\n        })\n\n        const Slot = createSlot(registry, {\n          pluginFailurePlaceholder(failure) {\n            return <text>{`plugin-error:${failure.pluginId}:${failure.slot}`}</text>\n          },\n        })\n\n        return (\n          <Slot name=\"statusbar\" user=\"sam\">\n            <text>fallback-visible</text>\n          </Slot>\n        )\n      },\n      { width: 80, height: 6 },\n    )\n    testSetup = setup\n\n    await testSetup.renderOnce()\n    const frame = testSetup.captureCharFrame()\n\n    expect(frame).toContain(\"fallback-visible\")\n    expect(frame).toContain(\"plugin-error:broken-plugin:statusbar\")\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/solid-plugin.fixture.ts",
    "content": "import { rmSync, mkdtempSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { plugin as registerPlugin } from \"bun\"\nimport { createRuntimePlugin, runtimeModuleIdForSpecifier, type RuntimeModuleEntry } from \"@opentui/core/runtime-plugin\"\nimport * as solidRuntime from \"../index\"\nimport { createSolidTransformPlugin } from \"../scripts/solid-plugin\"\n\nconst tempRoot = mkdtempSync(join(tmpdir(), \"solid-plugin-fixture-\"))\nconst entryPath = join(tempRoot, \"entry.tsx\")\n\nconst additionalRuntimeModules: Record<string, RuntimeModuleEntry> = {\n  \"@opentui/solid\": solidRuntime as Record<string, unknown>,\n  \"fixture-sync\": { value: \"sync-value\" },\n  \"@fixture/async-module\": async () => ({ value: \"async-value\" }),\n}\n\nconst runtimeResolvedSpecifiers = new Set<string>([\"@opentui/core\", ...Object.keys(additionalRuntimeModules)])\n\nconst source = [\n  'import { value as syncValue } from \"fixture-sync\"',\n  'import { value as asyncValue } from \"@fixture/async-module\"',\n  \"const makeNode = () => <text>{`sync=${syncValue};async=${asyncValue}`}</text>\",\n  \"console.log(`sync=${syncValue};async=${asyncValue};jsx=${typeof makeNode === 'function'}`)\",\n  \"export const noop = 1\",\n].join(\"\\n\")\n\nwriteFileSync(entryPath, source)\n\nregisterPlugin.clearAll()\n\nregisterPlugin(\n  createSolidTransformPlugin({\n    moduleName: runtimeModuleIdForSpecifier(\"@opentui/solid\"),\n    resolvePath(specifier) {\n      if (!runtimeResolvedSpecifiers.has(specifier)) {\n        return null\n      }\n\n      return runtimeModuleIdForSpecifier(specifier)\n    },\n  }),\n)\n\nregisterPlugin(\n  createRuntimePlugin({\n    additional: additionalRuntimeModules,\n  }),\n)\n\ntry {\n  await import(entryPath)\n} finally {\n  registerPlugin.clearAll()\n  rmSync(tempRoot, { recursive: true, force: true })\n}\n"
  },
  {
    "path": "packages/solid/tests/solid-plugin.test.ts",
    "content": "import { describe, expect, it } from \"bun:test\"\nimport { mkdtempSync, rmSync, writeFileSync } from \"node:fs\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"node:path\"\nimport { runtimeModuleIdForSpecifier } from \"@opentui/core/runtime-plugin\"\nimport { createSolidTransformPlugin } from \"../scripts/solid-plugin\"\n\ntype ResolveCallback = (args: { path: string; importer: string }) => unknown | Promise<unknown>\ntype LoadResult = { contents: string; loader: string } | void\ntype LoadCallback = (args: { path: string }) => LoadResult | Promise<LoadResult>\ntype ModuleCallback = () => unknown | Promise<unknown>\n\ntype LoadHandler = {\n  filter: RegExp\n  callback: LoadCallback\n}\n\ntype MockBuild = {\n  onResolve: (args: { filter: RegExp }, callback: ResolveCallback) => void\n  onLoad: (args: { filter: RegExp }, callback: LoadCallback) => void\n  module: (path: string, callback: ModuleCallback) => void\n}\n\nconst createMockBuild = (): {\n  build: MockBuild\n  resolveFilters: RegExp[]\n  loadHandlers: LoadHandler[]\n  modules: Map<string, ModuleCallback>\n} => {\n  const resolveFilters: RegExp[] = []\n  const loadHandlers: LoadHandler[] = []\n  const modules = new Map<string, ModuleCallback>()\n\n  const build: MockBuild = {\n    onResolve(args) {\n      resolveFilters.push(args.filter)\n    },\n    onLoad(args, callback) {\n      loadHandlers.push({ filter: args.filter, callback })\n    },\n    module(path, callback) {\n      modules.set(path, callback)\n    },\n  }\n\n  return { build, resolveFilters, loadHandlers, modules }\n}\n\nconst runLoad = async (handlers: LoadHandler[], path: string): Promise<LoadResult> => {\n  for (const handler of handlers) {\n    if (!handler.filter.test(path)) continue\n\n    const result = await handler.callback({ path })\n\n    if (result) {\n      return result\n    }\n  }\n\n  return undefined\n}\n\nconst createTempTsxFile = (source: string): { path: string; dispose: () => void } => {\n  const tempRoot = mkdtempSync(join(tmpdir(), \"solid-plugin-test-\"))\n  const path = join(tempRoot, \"fixture.tsx\")\n  writeFileSync(path, source)\n\n  return {\n    path,\n    dispose: () => {\n      rmSync(tempRoot, { recursive: true, force: true })\n    },\n  }\n}\n\ndescribe(\"solid transform plugin\", () => {\n  it(\"does not register runtime module resolvers by default\", () => {\n    const { build, resolveFilters, modules } = createMockBuild()\n    createSolidTransformPlugin().setup(build as any)\n\n    expect(resolveFilters).toHaveLength(0)\n    expect(modules.size).toBe(0)\n  })\n\n  it(\"uses @opentui/solid as the default JSX runtime module\", async () => {\n    const tempFile = createTempTsxFile(\n      ['import { value } from \"fixture-sync\"', \"const node = <text>{value}</text>\", \"export { node }\"].join(\"\\n\"),\n    )\n\n    try {\n      const { build, loadHandlers } = createMockBuild()\n      createSolidTransformPlugin().setup(build as any)\n\n      const transformed = await runLoad(loadHandlers, tempFile.path)\n\n      expect(transformed).toBeDefined()\n\n      if (!transformed) {\n        throw new Error(\"Expected transformed output\")\n      }\n\n      expect(transformed.loader).toBe(\"js\")\n      expect(transformed.contents).toContain(\"@opentui/solid\")\n      expect(transformed.contents).toContain(\"fixture-sync\")\n      expect(transformed.contents).not.toContain(\"react/jsx-runtime\")\n    } finally {\n      tempFile.dispose()\n    }\n  })\n\n  it(\"applies custom module resolver rewrites when configured\", async () => {\n    const runtimeSolidModule = runtimeModuleIdForSpecifier(\"@opentui/solid\")\n    const runtimeCoreModule = runtimeModuleIdForSpecifier(\"@opentui/core\")\n    const runtimeFixtureModule = runtimeModuleIdForSpecifier(\"fixture-sync\")\n\n    const tempFile = createTempTsxFile(\n      [\n        'import { engine } from \"@opentui/core\"',\n        'import { value } from \"fixture-sync\"',\n        \"const node = <text>{engine ? value : value}</text>\",\n        \"export { node }\",\n      ].join(\"\\n\"),\n    )\n\n    try {\n      const { build, loadHandlers } = createMockBuild()\n\n      createSolidTransformPlugin({\n        moduleName: runtimeSolidModule,\n        resolvePath(specifier) {\n          if (specifier === \"@opentui/core\") {\n            return runtimeCoreModule\n          }\n\n          if (specifier === \"fixture-sync\") {\n            return runtimeFixtureModule\n          }\n\n          return null\n        },\n      }).setup(build as any)\n\n      const transformed = await runLoad(loadHandlers, tempFile.path)\n\n      expect(transformed).toBeDefined()\n\n      if (!transformed) {\n        throw new Error(\"Expected transformed output\")\n      }\n\n      expect(transformed.contents).toContain(runtimeSolidModule)\n      expect(transformed.contents).toContain(runtimeCoreModule)\n      expect(transformed.contents).toContain(runtimeFixtureModule)\n      expect(transformed.contents).not.toContain('from \"@opentui/core\"')\n      expect(transformed.contents).not.toContain('from \"fixture-sync\"')\n    } finally {\n      tempFile.dispose()\n    }\n  })\n\n  it(\"transforms queried TSX paths\", async () => {\n    const tempFile = createTempTsxFile(\"const node = <text>ok</text>\\nexport { node }\")\n\n    try {\n      const { build, loadHandlers } = createMockBuild()\n      createSolidTransformPlugin().setup(build as any)\n\n      const transformed = await runLoad(loadHandlers, `${tempFile.path}?reload=1`)\n\n      expect(transformed).toBeDefined()\n\n      if (!transformed) {\n        throw new Error(\"Expected transformed output\")\n      }\n\n      expect(transformed.loader).toBe(\"js\")\n      expect(transformed.contents).toContain(\"@opentui/solid\")\n    } finally {\n      tempFile.dispose()\n    }\n  })\n\n  it(\"transforms runtime-resolved modules end-to-end in a subprocess\", () => {\n    const fixturePath = join(import.meta.dir, \"solid-plugin.fixture.ts\")\n    const result = Bun.spawnSync([process.execPath, fixturePath], {\n      cwd: join(import.meta.dir, \"..\"),\n      stdout: \"pipe\",\n      stderr: \"pipe\",\n      env: process.env,\n    })\n\n    const stdout = result.stdout.toString().trim()\n    const stderr = result.stderr.toString().trim()\n\n    if (stdout) {\n      console.debug(`[solid-plugin.fixture] stdout:\\n${stdout}`)\n    }\n\n    if (stderr) {\n      console.debug(`[solid-plugin.fixture] stderr:\\n${stderr}`)\n    }\n\n    expect(result.exitCode).toBe(0)\n    expect(stdout).toContain(\"sync=sync-value;async=async-value;jsx=true\")\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/sticky-scroll.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal, For } from \"solid-js\"\nimport type { ScrollBoxRenderable } from \"../../core/src/renderables/index.js\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"ScrollBox Sticky Scroll Behavior\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  it(\"sticky scroll bottom stays at bottom after scrollBy/scrollTo is called (setter-based)\", async () => {\n    const [items, setItems] = createSignal<string[]>([\"Line 0\"])\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    testSetup = await testRender(\n      () => (\n        <scrollbox\n          ref={(r) => {\n            scrollRef = r\n          }}\n          width={40}\n          height={10}\n          stickyScroll={true}\n          stickyStart=\"bottom\"\n        >\n          <For each={items()}>\n            {(item) => (\n              <box>\n                <text>{item}</text>\n              </box>\n            )}\n          </For>\n        </scrollbox>\n      ),\n      {\n        width: 80,\n        height: 24,\n      },\n    )\n\n    await testSetup.renderOnce()\n\n    // Call scrollBy and scrollTo - this mimics what happens when content is dynamically added\n    if (scrollRef) {\n      scrollRef.scrollBy(100000)\n      await testSetup.renderOnce()\n\n      scrollRef.scrollTo(scrollRef.scrollHeight)\n      await testSetup.renderOnce()\n    }\n\n    // Now gradually add content\n    for (let i = 1; i < 30; i++) {\n      setItems((prev) => [...prev, `Line ${i}`])\n      await testSetup.renderOnce()\n\n      const maxScroll = Math.max(0, scrollRef!.scrollHeight - scrollRef!.viewport.height)\n\n      // Check at line 16 (when content definitely overflows)\n      if (i === 16) {\n        expect(scrollRef!.scrollTop).toBe(maxScroll)\n      }\n    }\n\n    // Final check - should still be at bottom\n    const finalMaxScroll = Math.max(0, scrollRef!.scrollHeight - scrollRef!.viewport.height)\n    expect(scrollRef!.scrollTop).toBe(finalMaxScroll)\n  })\n\n  it(\"sticky scroll can still scroll up and down after scrollBy/scrollTo (setter-based)\", async () => {\n    const [items, setItems] = createSignal<string[]>([])\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    testSetup = await testRender(\n      () => (\n        <scrollbox\n          ref={(r) => {\n            scrollRef = r\n          }}\n          width={40}\n          height={10}\n          stickyScroll={true}\n          stickyStart=\"bottom\"\n        >\n          <For each={items()}>\n            {(item) => (\n              <box>\n                <text>{item}</text>\n              </box>\n            )}\n          </For>\n        </scrollbox>\n      ),\n      {\n        width: 80,\n        height: 24,\n      },\n    )\n\n    await testSetup.renderOnce()\n\n    // Add enough content to overflow\n    const newItems = Array.from({ length: 50 }, (_, i) => `Line ${i}`)\n    setItems(newItems)\n    await testSetup.renderOnce()\n\n    if (scrollRef) {\n      // Try to scroll to top\n      scrollRef.scrollTo(0)\n      await testSetup.renderOnce()\n      expect(scrollRef.scrollTop).toBe(0)\n\n      // Try to scroll down a bit\n      scrollRef.scrollBy(5)\n      await testSetup.renderOnce()\n      expect(scrollRef.scrollTop).toBe(5)\n\n      // Try to scroll down more\n      scrollRef.scrollBy(5)\n      await testSetup.renderOnce()\n      expect(scrollRef.scrollTop).toBe(10)\n\n      // Scroll back to bottom\n      const maxScroll = Math.max(0, scrollRef.scrollHeight - scrollRef.viewport.height)\n      scrollRef.scrollTo(maxScroll)\n      await testSetup.renderOnce()\n      expect(scrollRef.scrollTop).toBe(maxScroll)\n    }\n  })\n\n  it(\"accidental scroll when no scrollable content does not disable sticky\", async () => {\n    const [items, setItems] = createSignal<string[]>([])\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    testSetup = await testRender(\n      () => (\n        <scrollbox\n          ref={(r) => {\n            scrollRef = r\n          }}\n          width={40}\n          height={10}\n          stickyScroll={true}\n          stickyStart=\"bottom\"\n        >\n          <For each={items()}>\n            {(item) => (\n              <box>\n                <text>{item}</text>\n              </box>\n            )}\n          </For>\n        </scrollbox>\n      ),\n      {\n        width: 80,\n        height: 24,\n      },\n    )\n\n    await testSetup.renderOnce()\n\n    // Try to scroll when there's no scrollable content (accidental scroll)\n    if (scrollRef) {\n      // Simulate accidental scroll attempts when there's no meaningful content\n      scrollRef.scrollBy(100)\n      await testSetup.renderOnce()\n      scrollRef.scrollTo(50)\n      await testSetup.renderOnce()\n      scrollRef.scrollTop = 10\n      await testSetup.renderOnce()\n\n      // _hasManualScroll should still be false because there was no meaningful scrollable content\n      expect((scrollRef as any)._hasManualScroll).toBe(false)\n    }\n\n    // Now add content to make it scrollable\n    for (let i = 0; i < 30; i++) {\n      setItems((prev) => [...prev, `Line ${i}`])\n      await testSetup.renderOnce()\n\n      const maxScroll = Math.max(0, scrollRef!.scrollHeight - scrollRef!.viewport.height)\n\n      // Should still be at bottom due to sticky scroll\n      if (i === 16) {\n        expect(scrollRef!.scrollTop).toBe(maxScroll)\n        expect((scrollRef as any)._hasManualScroll).toBe(false)\n      }\n    }\n\n    // Final check - should still be at bottom\n    const finalMaxScroll = Math.max(0, scrollRef!.scrollHeight - scrollRef!.viewport.height)\n    expect(scrollRef!.scrollTop).toBe(finalMaxScroll)\n  })\n\n  it(\"sticky scroll with stickyStart set via setter (not constructor)\", async () => {\n    const [items, setItems] = createSignal<string[]>([\"Line 0\"])\n    let scrollRef: ScrollBoxRenderable | undefined\n\n    testSetup = await testRender(\n      () => {\n        const ref = (r: ScrollBoxRenderable) => {\n          scrollRef = r\n          // Set sticky properties via setters (like SolidJS does)\n          if (r && !(r as any).__stickyConfigure) {\n            ;(r as any).__stickyConfigure = true\n            r.stickyScroll = true\n            r.stickyStart = \"bottom\"\n          }\n        }\n\n        return (\n          <scrollbox ref={ref} width={40} height={10}>\n            <For each={items()}>\n              {(item) => (\n                <box>\n                  <text>{item}</text>\n                </box>\n              )}\n            </For>\n          </scrollbox>\n        )\n      },\n      {\n        width: 80,\n        height: 24,\n      },\n    )\n\n    await testSetup.renderOnce()\n\n    if (scrollRef) {\n      scrollRef.scrollBy(100000)\n      await testSetup.renderOnce()\n    }\n\n    // Add content\n    for (let i = 1; i < 30; i++) {\n      setItems((prev) => [...prev, `Line ${i}`])\n      await testSetup.renderOnce()\n\n      const maxScroll = Math.max(0, scrollRef!.scrollHeight - scrollRef!.viewport.height)\n\n      if (i === 16) {\n        expect(scrollRef!.scrollTop).toBe(maxScroll)\n      }\n    }\n  })\n})\n"
  },
  {
    "path": "packages/solid/tests/textarea.test.tsx",
    "content": "import { describe, expect, it, beforeEach, afterEach } from \"bun:test\"\nimport { testRender } from \"../index.js\"\nimport { createSignal } from \"solid-js\"\nimport { TextAttributes } from \"@opentui/core\"\n\nlet testSetup: Awaited<ReturnType<typeof testRender>>\n\ndescribe(\"Textarea Layout Tests\", () => {\n  beforeEach(async () => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  afterEach(() => {\n    if (testSetup) {\n      testSetup.renderer.destroy()\n    }\n  })\n\n  describe(\"Basic Textarea Rendering\", () => {\n    it(\"should render simple textarea correctly\", async () => {\n      testSetup = await testRender(\n        () => (\n          <textarea initialValue=\"Hello World\" width={20} height={5} backgroundColor=\"#1e1e1e\" textColor=\"#ffffff\" />\n        ),\n        {\n          width: 30,\n          height: 10,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render multiline textarea content\", async () => {\n      testSetup = await testRender(\n        () => (\n          <textarea\n            initialValue={\"Line 1\\nLine 2\\nLine 3\"}\n            width={20}\n            height={10}\n            backgroundColor=\"#1e1e1e\"\n            textColor=\"#ffffff\"\n          />\n        ),\n        {\n          width: 30,\n          height: 15,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render textarea with word wrapping\", async () => {\n      testSetup = await testRender(\n        () => (\n          <textarea\n            initialValue=\"This is a very long line that should wrap to multiple lines when word wrapping is enabled\"\n            wrapMode=\"word\"\n            width={20}\n            backgroundColor=\"#1e1e1e\"\n            textColor=\"#ffffff\"\n          />\n        ),\n        {\n          width: 30,\n          height: 15,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render textarea with placeholder\", async () => {\n      testSetup = await testRender(\n        () => (\n          <textarea\n            initialValue=\"\"\n            placeholder=\"Type something here...\"\n            placeholderColor=\"#666666\"\n            width={30}\n            height={5}\n            backgroundColor=\"#1e1e1e\"\n            textColor=\"#ffffff\"\n          />\n        ),\n        {\n          width: 40,\n          height: 10,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Prompt-like Layout\", () => {\n    it(\"should render textarea in prompt-style layout with indicator\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border borderColor=\"#444444\">\n            <box flexDirection=\"row\">\n              {/* Indicator box */}\n              <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                <text attributes={TextAttributes.BOLD} fg=\"#00ff00\">\n                  {\">\"}\n                </text>\n              </box>\n\n              {/* Textarea container */}\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <textarea\n                  initialValue=\"Hello from the prompt\"\n                  flexShrink={1}\n                  backgroundColor=\"#1e1e1e\"\n                  textColor=\"#ffffff\"\n                  cursorColor=\"#00ff00\"\n                />\n              </box>\n\n              {/* Spacer */}\n              <box backgroundColor=\"#1e1e1e\" width={1} />\n            </box>\n\n            {/* Footer */}\n            <box flexDirection=\"row\" justifyContent=\"space-between\">\n              <text wrapMode=\"none\">\n                <span style={{ fg: \"#888888\" }}>provider</span> <span style={{ bold: true }}>model-name</span>\n              </text>\n              <text fg=\"#888888\">ctrl+p commands</text>\n            </box>\n          </box>\n        ),\n        {\n          width: 60,\n          height: 15,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render textarea with long wrapping text in prompt layout\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border borderColor=\"#444444\" width=\"100%\">\n            <box flexDirection=\"row\" width=\"100%\">\n              <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                <text attributes={TextAttributes.BOLD} fg=\"#00ff00\">\n                  {\">\"}\n                </text>\n              </box>\n\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <textarea\n                  initialValue=\"This is a very long prompt that will wrap across multiple lines in the textarea. It should maintain proper layout with the indicator on the left.\"\n                  wrapMode=\"word\"\n                  flexShrink={1}\n                  backgroundColor=\"#1e1e1e\"\n                  textColor=\"#ffffff\"\n                />\n              </box>\n\n              <box backgroundColor=\"#1e1e1e\" width={1} />\n            </box>\n\n            <box flexDirection=\"row\">\n              <text wrapMode=\"none\">\n                <span style={{ fg: \"#888888\" }}>openai</span> <span style={{ bold: true }}>gpt-4</span>\n              </text>\n            </box>\n          </box>\n        ),\n        {\n          width: 50,\n          height: 20,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render textarea in shell mode with different indicator\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border borderColor=\"#ff9900\">\n            <box flexDirection=\"row\">\n              <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                <text attributes={TextAttributes.BOLD} fg=\"#ff9900\">\n                  {\"!\"}\n                </text>\n              </box>\n\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <textarea\n                  initialValue=\"ls -la\"\n                  flexShrink={1}\n                  backgroundColor=\"#1e1e1e\"\n                  textColor=\"#ffffff\"\n                  cursorColor=\"#ff9900\"\n                />\n              </box>\n\n              <box backgroundColor=\"#1e1e1e\" width={1} />\n            </box>\n\n            <box flexDirection=\"row\">\n              <text fg=\"#888888\">shell mode</text>\n            </box>\n          </box>\n        ),\n        {\n          width: 50,\n          height: 12,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Complex Layouts with Multiple Textareas\", () => {\n    it(\"should render multiple textareas in a column layout\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border title=\"Chat\">\n            {/* Message 1 */}\n            <box border borderColor=\"#00ff00\" marginBottom={1}>\n              <box flexDirection=\"row\">\n                <box width={5} backgroundColor=\"#2d2d2d\">\n                  <text fg=\"#00ff00\">User</text>\n                </box>\n                <box paddingLeft={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                  <textarea\n                    initialValue=\"What is the weather like today?\"\n                    wrapMode=\"word\"\n                    backgroundColor=\"#1e1e1e\"\n                    textColor=\"#ffffff\"\n                  />\n                </box>\n              </box>\n            </box>\n\n            {/* Message 2 */}\n            <box border borderColor=\"#0088ff\">\n              <box flexDirection=\"row\">\n                <box width={5} backgroundColor=\"#2d2d2d\">\n                  <text fg=\"#0088ff\">AI</text>\n                </box>\n                <box paddingLeft={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                  <textarea\n                    initialValue=\"I don't have access to real-time weather data, but I can help you find that information through various weather services.\"\n                    wrapMode=\"word\"\n                    backgroundColor=\"#1e1e1e\"\n                    textColor=\"#ffffff\"\n                  />\n                </box>\n              </box>\n            </box>\n          </box>\n        ),\n        {\n          width: 60,\n          height: 25,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle nested boxes with textareas at different positions\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box style={{ width: 50, border: true }} title=\"Layout Test\">\n            <box flexDirection=\"row\" gap={1}>\n              {/* Left panel */}\n              <box width={20} border borderColor=\"#00ff00\">\n                <text fg=\"#00ff00\">Input 1:</text>\n                <textarea\n                  initialValue=\"Left panel content\"\n                  wrapMode=\"word\"\n                  backgroundColor=\"#1e1e1e\"\n                  textColor=\"#ffffff\"\n                  flexShrink={1}\n                />\n              </box>\n\n              {/* Right panel */}\n              <box flexGrow={1} border borderColor=\"#0088ff\">\n                <text fg=\"#0088ff\">Input 2:</text>\n                <textarea\n                  initialValue=\"Right panel with longer content that may wrap\"\n                  wrapMode=\"word\"\n                  backgroundColor=\"#1e1e1e\"\n                  textColor=\"#ffffff\"\n                  flexShrink={1}\n                />\n              </box>\n            </box>\n\n            {/* Bottom panel */}\n            <box border borderColor=\"#ff9900\" marginTop={1}>\n              <text fg=\"#ff9900\">Bottom input:</text>\n              <textarea\n                initialValue=\"Bottom panel spanning full width\"\n                wrapMode=\"word\"\n                backgroundColor=\"#1e1e1e\"\n                textColor=\"#ffffff\"\n                flexShrink={1}\n              />\n            </box>\n          </box>\n        ),\n        {\n          width: 55,\n          height: 25,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Text Component Comparison\", () => {\n    it(\"should render text in prompt-style layout with indicator\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border borderColor=\"#444444\">\n            <box flexDirection=\"row\">\n              {/* Indicator box */}\n              <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                <text attributes={TextAttributes.BOLD} fg=\"#00ff00\">\n                  {\">\"}\n                </text>\n              </box>\n\n              {/* Text container */}\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <text wrapMode=\"none\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n                  Hello from the prompt\n                </text>\n              </box>\n\n              {/* Spacer */}\n              <box backgroundColor=\"#1e1e1e\" width={1} />\n            </box>\n\n            {/* Footer */}\n            <box flexDirection=\"row\" justifyContent=\"space-between\">\n              <text wrapMode=\"none\">\n                <span style={{ fg: \"#888888\" }}>provider</span> <span style={{ bold: true }}>model-name</span>\n              </text>\n              <text fg=\"#888888\">ctrl+p commands</text>\n            </box>\n          </box>\n        ),\n        {\n          width: 60,\n          height: 15,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render text with long wrapping content in prompt layout\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border borderColor=\"#444444\" width=\"100%\">\n            <box flexDirection=\"row\" width=\"100%\">\n              <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                <text attributes={TextAttributes.BOLD} fg=\"#00ff00\">\n                  {\">\"}\n                </text>\n              </box>\n\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <text wrapMode=\"word\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n                  This is a very long prompt that will wrap across multiple lines in the text component. It should\n                  maintain proper layout with the indicator on the left.\n                </text>\n              </box>\n\n              <box backgroundColor=\"#1e1e1e\" width={1} />\n            </box>\n\n            <box flexDirection=\"row\">\n              <text wrapMode=\"none\">\n                <span style={{ fg: \"#888888\" }}>openai</span> <span style={{ bold: true }}>gpt-4</span>\n              </text>\n            </box>\n          </box>\n        ),\n        {\n          width: 50,\n          height: 20,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should update text content reactively in prompt layout\", async () => {\n      const [value, setValue] = createSignal(\"Initial text\")\n\n      testSetup = await testRender(\n        () => (\n          <box border width=\"100%\">\n            <box flexDirection=\"row\" width=\"100%\">\n              <box width={3} backgroundColor=\"#2d2d2d\" justifyContent=\"center\" alignItems=\"center\">\n                <text fg=\"#00ff00\">{\">\"}</text>\n              </box>\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <text wrapMode=\"word\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n                  {value()}\n                </text>\n              </box>\n            </box>\n          </box>\n        ),\n        {\n          width: 50,\n          height: 12,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const initialFrame = testSetup.captureCharFrame()\n\n      setValue(\"Updated text that is much longer and should wrap to multiple lines if word wrapping is enabled\")\n      await testSetup.renderOnce()\n      const updatedFrame = testSetup.captureCharFrame()\n\n      expect(initialFrame).toMatchSnapshot()\n      expect(updatedFrame).toMatchSnapshot()\n      expect(updatedFrame).not.toBe(initialFrame)\n    })\n\n    it(\"should render text in shell mode with different indicator\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border borderColor=\"#ff9900\">\n            <box flexDirection=\"row\">\n              <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                <text attributes={TextAttributes.BOLD} fg=\"#ff9900\">\n                  {\"!\"}\n                </text>\n              </box>\n\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <text wrapMode=\"none\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n                  ls -la\n                </text>\n              </box>\n\n              <box backgroundColor=\"#1e1e1e\" width={1} />\n            </box>\n\n            <box flexDirection=\"row\">\n              <text fg=\"#888888\">shell mode</text>\n            </box>\n          </box>\n        ),\n        {\n          width: 50,\n          height: 12,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render full prompt layout with text component\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box>\n            {/* Main prompt box */}\n            <box border borderColor=\"#444444\">\n              <box flexDirection=\"row\">\n                {/* Indicator */}\n                <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                  <text attributes={TextAttributes.BOLD} fg=\"#00ff00\">\n                    {\">\"}\n                  </text>\n                </box>\n\n                {/* Input area */}\n                <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                  <text wrapMode=\"word\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n                    Explain how async/await works in JavaScript and provide some examples\n                  </text>\n                </box>\n\n                {/* Right spacer */}\n                <box backgroundColor=\"#1e1e1e\" width={1} justifyContent=\"center\" alignItems=\"center\" />\n              </box>\n\n              {/* Status bar */}\n              <box flexDirection=\"row\" justifyContent=\"space-between\">\n                <text flexShrink={0} wrapMode=\"none\">\n                  <span style={{ fg: \"#888888\" }}>openai</span> <span style={{ bold: true }}>gpt-4-turbo</span>\n                </text>\n                <text>\n                  ctrl+p <span style={{ fg: \"#888888\" }}>commands</span>\n                </text>\n              </box>\n            </box>\n\n            {/* Helper text below */}\n            <box marginTop={1}>\n              <text fg=\"#666666\" wrapMode=\"word\">\n                Tip: Use arrow keys to navigate through history when cursor is at the start\n              </text>\n            </box>\n          </box>\n        ),\n        {\n          width: 70,\n          height: 20,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should handle very long single-line text in prompt layout\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border width=\"100%\">\n            <box flexDirection=\"row\" width=\"100%\">\n              <box width={3} backgroundColor=\"#2d2d2d\">\n                <text>{\">\"}</text>\n              </box>\n              <box backgroundColor=\"#1e1e1e\" flexGrow={1} paddingTop={1} paddingBottom={1}>\n                <text wrapMode=\"char\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n                  ThisIsAVeryLongLineWithNoSpacesThatWillWrapByCharacterWhenCharWrappingIsEnabled\n                </text>\n              </box>\n            </box>\n          </box>\n        ),\n        {\n          width: 40,\n          height: 15,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render multiline text in prompt layout\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border borderColor=\"#444444\" width=\"100%\">\n            <box flexDirection=\"row\" width=\"100%\">\n              <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                <text attributes={TextAttributes.BOLD} fg=\"#00ff00\">\n                  {\">\"}\n                </text>\n              </box>\n\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <text wrapMode=\"word\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n                  Line 1: First line of text\n                  <br />\n                  Line 2: Second line of text\n                  <br />\n                  Line 3: Third line of text\n                </text>\n              </box>\n\n              <box backgroundColor=\"#1e1e1e\" width={1} />\n            </box>\n\n            <box flexDirection=\"row\">\n              <text wrapMode=\"none\">\n                <span style={{ fg: \"#888888\" }}>multiline</span> <span style={{ bold: true }}>example</span>\n              </text>\n            </box>\n          </box>\n        ),\n        {\n          width: 50,\n          height: 20,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"FlexShrink Regression Tests\", () => {\n    it(\"should not shrink box when width is set via setter\", async () => {\n      const [indicatorWidth, setIndicatorWidth] = createSignal<number | undefined>(undefined)\n\n      testSetup = await testRender(\n        () => (\n          <box border>\n            <box flexDirection=\"row\">\n              <box width={indicatorWidth()} backgroundColor=\"#f00\">\n                <text>{\">\"}</text>\n              </box>\n              <box backgroundColor=\"#0f0\" flexGrow={1}>\n                <text>Content that takes up space</text>\n              </box>\n            </box>\n          </box>\n        ),\n        { width: 30, height: 5 },\n      )\n\n      await testSetup.renderOnce()\n\n      setIndicatorWidth(5)\n      await testSetup.renderOnce()\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should not shrink box when height is set via setter in column layout\", async () => {\n      const [headerHeight, setHeaderHeight] = createSignal<number | undefined>(undefined)\n\n      testSetup = await testRender(\n        () => (\n          <box border width={25} height={10}>\n            <box flexDirection=\"column\" height=\"100%\">\n              <box height={headerHeight()} backgroundColor=\"#f00\">\n                <text>Header</text>\n              </box>\n              <box backgroundColor=\"#0f0\" flexGrow={1}>\n                <textarea initialValue={\"Line1\\nLine2\\nLine3\\nLine4\\nLine5\\nLine6\\nLine7\\nLine8\"} />\n              </box>\n              <box height={2} backgroundColor=\"#00f\">\n                <text>Footer</text>\n              </box>\n            </box>\n          </box>\n        ),\n        { width: 30, height: 15 },\n      )\n\n      await testSetup.renderOnce()\n\n      setHeaderHeight(3)\n      await testSetup.renderOnce()\n\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Edge Cases and Styling\", () => {\n    it(\"should render textarea with focused colors\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border>\n            <box flexDirection=\"row\">\n              <box width={3} backgroundColor=\"#2d2d2d\">\n                <text>{\">\"}</text>\n              </box>\n              <box backgroundColor=\"#1e1e1e\" flexGrow={1} paddingTop={1} paddingBottom={1}>\n                <textarea\n                  initialValue=\"Focused textarea\"\n                  backgroundColor=\"#1e1e1e\"\n                  textColor=\"#888888\"\n                  focusedBackgroundColor=\"#2d2d2d\"\n                  focusedTextColor=\"#ffffff\"\n                  flexShrink={1}\n                />\n              </box>\n            </box>\n          </box>\n        ),\n        {\n          width: 40,\n          height: 10,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render empty textarea with placeholder in prompt layout\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border borderColor=\"#444444\">\n            <box flexDirection=\"row\">\n              <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                <text attributes={TextAttributes.BOLD} fg=\"#00ff00\">\n                  {\">\"}\n                </text>\n              </box>\n\n              <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                <textarea\n                  initialValue=\"\"\n                  placeholder=\"Enter your prompt here...\"\n                  placeholderColor=\"#666666\"\n                  flexShrink={1}\n                  backgroundColor=\"#1e1e1e\"\n                  textColor=\"#ffffff\"\n                />\n              </box>\n\n              <box backgroundColor=\"#1e1e1e\" width={1} />\n            </box>\n\n            <box flexDirection=\"row\">\n              <text fg=\"#888888\">Ready to chat</text>\n            </box>\n          </box>\n        ),\n        {\n          width: 50,\n          height: 12,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render textarea with very long single line\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border>\n            <box flexDirection=\"row\">\n              <box width={3} backgroundColor=\"#2d2d2d\">\n                <text>{\">\"}</text>\n              </box>\n              <box backgroundColor=\"#1e1e1e\" flexGrow={1} paddingTop={1} paddingBottom={1}>\n                <textarea\n                  initialValue=\"ThisIsAVeryLongLineWithNoSpacesThatWillWrapByCharacterWhenCharWrappingIsEnabled\"\n                  wrapMode=\"char\"\n                  flexShrink={1}\n                  backgroundColor=\"#1e1e1e\"\n                  textColor=\"#ffffff\"\n                />\n              </box>\n            </box>\n          </box>\n        ),\n        {\n          width: 40,\n          height: 15,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n\n    it(\"should render full prompt-like layout with all components\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box>\n            {/* Main prompt box */}\n            <box border borderColor=\"#444444\">\n              <box flexDirection=\"row\">\n                {/* Indicator */}\n                <box width={3} justifyContent=\"center\" alignItems=\"center\" backgroundColor=\"#2d2d2d\">\n                  <text attributes={TextAttributes.BOLD} fg=\"#00ff00\">\n                    {\">\"}\n                  </text>\n                </box>\n\n                {/* Input area */}\n                <box paddingTop={1} paddingBottom={1} backgroundColor=\"#1e1e1e\" flexGrow={1}>\n                  <textarea\n                    initialValue=\"Explain how async/await works in JavaScript and provide some examples\"\n                    wrapMode=\"word\"\n                    flexShrink={1}\n                    backgroundColor=\"#1e1e1e\"\n                    textColor=\"#ffffff\"\n                    cursorColor=\"#00ff00\"\n                  />\n                </box>\n\n                {/* Right spacer */}\n                <box backgroundColor=\"#1e1e1e\" width={1} justifyContent=\"center\" alignItems=\"center\" />\n              </box>\n\n              {/* Status bar */}\n              <box flexDirection=\"row\" justifyContent=\"space-between\">\n                <text flexShrink={0} wrapMode=\"none\">\n                  <span style={{ fg: \"#888888\" }}>openai</span> <span style={{ bold: true }}>gpt-4-turbo</span>\n                </text>\n                <text>\n                  ctrl+p <span style={{ fg: \"#888888\" }}>commands</span>\n                </text>\n              </box>\n            </box>\n\n            {/* Helper text below */}\n            <box marginTop={1}>\n              <text fg=\"#666666\" wrapMode=\"word\">\n                Tip: Use arrow keys to navigate through history when cursor is at the start\n              </text>\n            </box>\n          </box>\n        ),\n        {\n          width: 70,\n          height: 20,\n        },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n\n  describe(\"Measure Cache Edge Cases\", () => {\n    it(\"should correctly measure text after content change\", async () => {\n      const [value, setValue] = createSignal(\"Short text\")\n\n      testSetup = await testRender(\n        () => (\n          <box border width={40}>\n            <text wrapMode=\"word\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n              {value()}\n            </text>\n          </box>\n        ),\n        { width: 50, height: 15 },\n      )\n\n      await testSetup.renderOnce()\n      const initialFrame = testSetup.captureCharFrame()\n\n      // Change to longer content that should cause more wrapping\n      setValue(\"This is a much longer text that will definitely wrap to multiple lines when rendered\")\n      await testSetup.renderOnce()\n      const updatedFrame = testSetup.captureCharFrame()\n\n      expect(initialFrame).toMatchSnapshot()\n      expect(updatedFrame).toMatchSnapshot()\n      expect(updatedFrame).not.toBe(initialFrame)\n    })\n\n    it(\"should handle rapid content updates correctly\", async () => {\n      const [value, setValue] = createSignal(\"Initial\")\n\n      testSetup = await testRender(\n        () => (\n          <box border width={30}>\n            <text wrapMode=\"char\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n              {value()}\n            </text>\n          </box>\n        ),\n        { width: 40, height: 10 },\n      )\n\n      // Rapid updates to simulate typing\n      for (let i = 0; i < 5; i++) {\n        setValue(`Update ${i}: some text here`)\n        await testSetup.renderOnce()\n      }\n\n      const finalFrame = testSetup.captureCharFrame()\n      expect(finalFrame).toMatchSnapshot()\n    })\n\n    it(\"should handle width changes with cached measures\", async () => {\n      const [width, setWidth] = createSignal(30)\n\n      testSetup = await testRender(\n        () => (\n          <box border width={width()}>\n            <text wrapMode=\"word\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n              Content that will wrap differently at different widths\n            </text>\n          </box>\n        ),\n        { width: 60, height: 15 },\n      )\n\n      await testSetup.renderOnce()\n      const frame30 = testSetup.captureCharFrame()\n\n      setWidth(50)\n      await testSetup.renderOnce()\n      const frame50 = testSetup.captureCharFrame()\n\n      setWidth(20)\n      await testSetup.renderOnce()\n      const frame20 = testSetup.captureCharFrame()\n\n      expect(frame30).toMatchSnapshot()\n      expect(frame50).toMatchSnapshot()\n      expect(frame20).toMatchSnapshot()\n    })\n\n    it(\"should handle empty to non-empty content transition\", async () => {\n      const [value, setValue] = createSignal(\"\")\n\n      testSetup = await testRender(\n        () => (\n          <box border width={40}>\n            <text wrapMode=\"word\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n              {value() || \" \"}\n            </text>\n          </box>\n        ),\n        { width: 50, height: 10 },\n      )\n\n      await testSetup.renderOnce()\n      const emptyFrame = testSetup.captureCharFrame()\n\n      setValue(\"Now with content\")\n      await testSetup.renderOnce()\n      const contentFrame = testSetup.captureCharFrame()\n\n      setValue(\"\")\n      await testSetup.renderOnce()\n      const emptyAgainFrame = testSetup.captureCharFrame()\n\n      expect(emptyFrame).toMatchSnapshot()\n      expect(contentFrame).toMatchSnapshot()\n      expect(emptyAgainFrame).toMatchSnapshot()\n    })\n\n    it(\"should correctly measure multiline content with unicode\", async () => {\n      testSetup = await testRender(\n        () => (\n          <box border width={30}>\n            <text wrapMode=\"word\" bg=\"#1e1e1e\" fg=\"#ffffff\">\n              Hello 世界\n              <br />\n              こんにちは\n              <br />\n              🌟 Emoji 🚀\n            </text>\n          </box>\n        ),\n        { width: 40, height: 15 },\n      )\n\n      await testSetup.renderOnce()\n      const frame = testSetup.captureCharFrame()\n      expect(frame).toMatchSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/solid/tsconfig.build.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./dist\",\n    \"noEmit\": false,\n    \"rootDir\": \".\",\n    \"types\": [\"bun\", \"node\"],\n    \"skipLibCheck\": true,\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"@opentui/solid\",\n    \"moduleResolution\": \"bundler\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@opentui/core\": [\"../core/dist\"],\n      \"@opentui/core/*\": [\"../core/dist/*\"]\n    }\n  },\n  \"include\": [\n    \"index.ts\",\n    \"src/**/*\",\n    \"jsx-runtime.d.ts\",\n    // See the comment below\n    \"scripts/solid-plugin.ts\",\n    \"scripts/runtime-plugin-support.ts\"\n  ],\n  \"exclude\": [\n    \"**/*.test.ts\",\n    \"**/*.spec.ts\",\n    \"examples/**/*\",\n    \"node_modules/**/*\",\n    \"../core/**/*\",\n    // We have to explicitly list each script to exclude rather than using \"scripts/**/*\"\n    // \"exclude\" is applied after \"include\"\n    // See: https://www.typescriptlang.org/tsconfig#exclude\n    \"scripts/build.ts\",\n    \"scripts/publish.ts\",\n    \"scripts/preload.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/solid/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    // Environment setup & latest features\n    \"lib\": [\"ESNext\"],\n    \"target\": \"ESNext\",\n    \"module\": \"Preserve\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"@opentui/solid\",\n    \"allowJs\": true,\n\n    // Bundler mode\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n\n    // Best practices\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noImplicitOverride\": true,\n\n    // Some stricter flags (disabled by default)\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noPropertyAccessFromIndexSignature\": false\n  }\n}\n"
  },
  {
    "path": "packages/web/.gitignore",
    "content": ".astro\n\n# Generated by tsc type-checking\nsrc/examples/*.js\n"
  },
  {
    "path": "packages/web/astro.config.mjs",
    "content": "import { defineConfig } from \"astro/config\"\nimport mdx from \"@astrojs/mdx\"\n\nconst copyButtonTransformer = {\n  name: \"copy-button\",\n  pre(node) {\n    node.properties[\"data-code\"] = this.source\n  },\n}\n\nexport default defineConfig({\n  integrations: [mdx()],\n  site: \"https://opentui.com\",\n  markdown: {\n    shikiConfig: {\n      themes: {\n        light: \"min-light\",\n        dark: \"github-dark\",\n      },\n      transformers: [copyButtonTransformer],\n    },\n  },\n})\n"
  },
  {
    "path": "packages/web/package.json",
    "content": "{\n  \"name\": \"@opentui/web\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\",\n    \"scaffold\": \"bun scripts/test-doc-examples.ts\"\n  },\n  \"dependencies\": {\n    \"@astrojs/mdx\": \"^4.3.1\",\n    \"@opentui/core\": \"workspace:*\",\n    \"astro\": \"^5.10.2\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"latest\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "packages/web/scripts/test-doc-examples.ts",
    "content": "#!/usr/bin/env bun\n/**\n * Creates a scaffold environment for testing documentation examples.\n *\n * Usage:\n *   bun scripts/test-doc-examples.ts [dir]\n *\n * This creates a directory with @opentui/core installed where you can\n * copy-paste code examples from the docs to verify they work.\n */\n\nimport { existsSync } from \"node:fs\"\nimport { mkdir } from \"node:fs/promises\"\nimport { join } from \"node:path\"\n\nconst DEFAULT_DIR = \"/tmp/opentui-test\"\n\nasync function main() {\n  const targetDir = process.argv[2] || DEFAULT_DIR\n\n  console.log(`Setting up test environment in: ${targetDir}\\n`)\n\n  // Create directory\n  if (!existsSync(targetDir)) {\n    await mkdir(targetDir, { recursive: true })\n  }\n\n  // Initialize bun project\n  console.log(\"Initializing project...\")\n  const init = Bun.spawnSync([\"bun\", \"init\", \"-y\"], {\n    cwd: targetDir,\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n  })\n  if (init.exitCode !== 0) {\n    console.error(\"Failed to init:\", new TextDecoder().decode(init.stderr))\n    process.exit(1)\n  }\n\n  // Install @opentui/core\n  console.log(\"Installing @opentui/core...\")\n  const install = Bun.spawnSync([\"bun\", \"add\", \"@opentui/core\"], {\n    cwd: targetDir,\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n  })\n  if (install.exitCode !== 0) {\n    console.error(\"Failed to install:\", new TextDecoder().decode(install.stderr))\n    process.exit(1)\n  }\n\n  // install @opentui/solid\n  console.log(\"Installing @opentui/solid...\")\n  const installSolid = Bun.spawnSync([\"bun\", \"add\", \"@opentui/solid\"], {\n    cwd: targetDir,\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n  })\n  if (installSolid.exitCode !== 0) {\n    console.error(\"Failed to install:\", new TextDecoder().decode(installSolid.stderr))\n    process.exit(1)\n  }\n\n  // Create a template file\n  const template = `import { createCliRenderer, Text, Box } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: true,\n})\n\n// Paste your example code here\nrenderer.root.add(\n  Text({\n    content: \"Hello, OpenTUI!\",\n    fg: \"#00FF00\",\n  }),\n)\n`\n\n  await Bun.write(join(targetDir, \"test.ts\"), template)\n\n  console.log(`\nDone! Test environment ready.\n\nTo test an example:\n  1. Edit ${targetDir}/test.ts\n  2. Run: bun ${targetDir}/test.ts\n  3. Press Ctrl+C to exit\n\nOr:\n  cd ${targetDir}\n  bun test.ts\n`)\n}\n\nmain().catch((err) => {\n  console.error(\"Error:\", err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/web/scripts/verify-doc-examples.ts",
    "content": "#!/usr/bin/env bun\n/**\n * Verifies that documentation code examples are accurate by type-checking them.\n *\n * Usage:\n *   bun scripts/verify-doc-examples.ts [file-pattern]\n *\n * This script:\n * 1. Extracts TypeScript/JavaScript code blocks from MDX files\n * 2. Type-checks them against @opentui/core\n * 3. Reports any type errors found\n */\n\nimport { readFile, writeFile, mkdir, rm } from \"node:fs/promises\"\nimport { join, relative } from \"node:path\"\nimport { existsSync } from \"node:fs\"\n\nconst DOCS_DIR = join(import.meta.dir, \"../src/content/docs\")\nconst CORE_PACKAGE = join(import.meta.dir, \"../../core\")\nconst CORE_DIST = join(CORE_PACKAGE, \"dist\")\nconst TEST_DIR = \"/tmp/opentui-doc-verify\"\n\ninterface CodeBlock {\n  code: string\n  language: string\n  lineNumber: number\n  file: string\n}\n\ninterface Issue {\n  type: \"error\" | \"warning\"\n  message: string\n}\n\ninterface VerificationResult {\n  file: string\n  lineNumber: number\n  issues: Issue[]\n  codePreview: string\n}\n\n// Extract code blocks from MDX content\nfunction extractCodeBlocks(content: string, file: string): CodeBlock[] {\n  const blocks: CodeBlock[] = []\n  const codeBlockRegex = /```(typescript|ts|javascript|js|tsx|jsx)\\n([\\s\\S]*?)```/g\n\n  let match\n  while ((match = codeBlockRegex.exec(content)) !== null) {\n    const lineNumber = content.substring(0, match.index).split(\"\\n\").length\n    blocks.push({\n      code: match[2],\n      language: match[1],\n      lineNumber,\n      file,\n    })\n  }\n\n  return blocks\n}\n\n// Check if a code block is a complete example (has imports) vs a fragment\nfunction isCompleteExample(code: string): boolean {\n  return code.includes(\"import \") && code.includes(\"from \")\n}\n\n// Check if code block is just showing object properties (not runnable code)\nfunction isPropertyFragment(code: string): boolean {\n  const trimmed = code.trim()\n  // Matches things like: { borderStyle: \"single\" }\n  return trimmed.startsWith(\"{\") && !trimmed.includes(\"const \") && !trimmed.includes(\"function \")\n}\n\n// Check if code contains JSX syntax\nfunction hasJSX(code: string): boolean {\n  // Look for JSX patterns: <Component />, <Component>, </Component>\n  return /<[A-Z][a-zA-Z]*[\\s/>]/.test(code) || /<\\/[A-Z][a-zA-Z]*>/.test(code)\n}\n\n// Wrap a code block to make it type-checkable\nfunction wrapCodeForTypeCheck(code: string, blockIndex: number): string {\n  // Skip property-only fragments\n  if (isPropertyFragment(code)) {\n    return \"\"\n  }\n\n  // Skip JSX - would need separate handling with tsx\n  if (hasJSX(code)) {\n    return \"\"\n  }\n\n  // Skip fragments without imports - they're incomplete by design\n  if (!isCompleteExample(code)) {\n    return \"\"\n  }\n\n  // Split into imports and body\n  const lines = code.split(\"\\n\")\n  const importLines: string[] = []\n  const bodyLines: string[] = []\n\n  let pastImports = false\n  for (const line of lines) {\n    if (!pastImports && (line.trim().startsWith(\"import \") || line.trim() === \"\")) {\n      importLines.push(line)\n    } else {\n      pastImports = true\n      bodyLines.push(line)\n    }\n  }\n\n  const body = bodyLines.join(\"\\n\")\n\n  // Add renderer declaration if body uses it but doesn't define it\n  const usesRenderer = body.includes(\"renderer.\") || body.includes(\"renderer,\") || body.includes(\"renderer)\")\n  const definesRenderer = body.includes(\"const renderer\") || body.includes(\"let renderer\")\n\n  let preamble = importLines.join(\"\\n\")\n\n  if (usesRenderer && !definesRenderer) {\n    // Add renderer declaration and createCliRenderer import if not already imported\n    if (!preamble.includes(\"createCliRenderer\")) {\n      preamble = `import { createCliRenderer } from \"@opentui/core\"\\n` + preamble\n    }\n    preamble += `\\ndeclare const renderer: Awaited<ReturnType<typeof createCliRenderer>>\\n`\n  }\n\n  // Wrap body in async function if it uses await\n  if (body.includes(\"await \")) {\n    return `${preamble}\\n\\nasync function __example${blockIndex}() {\\n${body}\\n}\\n`\n  }\n\n  return preamble + \"\\n\" + body\n}\n\n// Setup the test environment\nasync function setupTestEnv(): Promise<boolean> {\n  if (existsSync(TEST_DIR)) {\n    await rm(TEST_DIR, { recursive: true })\n  }\n  await mkdir(TEST_DIR, { recursive: true })\n\n  // Check that dist exists\n  if (!existsSync(CORE_DIST)) {\n    console.error(`ERROR: ${CORE_DIST} not found. Run 'bun run build' in packages/core first.`)\n    return false\n  }\n\n  // Create package.json - use dist for proper type declarations\n  await writeFile(\n    join(TEST_DIR, \"package.json\"),\n    JSON.stringify({\n      name: \"doc-verify\",\n      type: \"module\",\n      dependencies: {\n        \"@opentui/core\": `file:${CORE_DIST}`,\n      },\n    }),\n  )\n\n  // Create tsconfig.json\n  await writeFile(\n    join(TEST_DIR, \"tsconfig.json\"),\n    JSON.stringify({\n      compilerOptions: {\n        target: \"ESNext\",\n        module: \"ESNext\",\n        moduleResolution: \"bundler\",\n        strict: true,\n        skipLibCheck: true,\n        esModuleInterop: true,\n        noEmit: true,\n        types: [\"bun-types\"],\n      },\n      include: [\"*.ts\"],\n    }),\n  )\n\n  // Install dependencies\n  const install = Bun.spawnSync([\"bun\", \"install\"], {\n    cwd: TEST_DIR,\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n  })\n\n  if (install.exitCode !== 0) {\n    console.error(\"Failed to install dependencies:\", install.stderr.toString())\n    return false\n  }\n\n  return true\n}\n\n// Type-check a code block\nasync function typeCheckBlock(block: CodeBlock, blockIndex: number): Promise<Issue[]> {\n  const issues: Issue[] = []\n\n  const wrappedCode = wrapCodeForTypeCheck(block.code, blockIndex)\n  if (!wrappedCode) {\n    return issues // Skip fragments that can't be checked\n  }\n\n  const testFile = join(TEST_DIR, `example-${blockIndex}.ts`)\n  await writeFile(testFile, wrappedCode)\n\n  // Run tsc on this specific file\n  const result = Bun.spawnSync([\"bunx\", \"tsc\", \"--noEmit\", \"--skipLibCheck\", testFile], {\n    cwd: TEST_DIR,\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n  })\n\n  if (result.exitCode !== 0) {\n    const output = result.stdout.toString() + result.stderr.toString()\n\n    // Parse errors, filter out noise\n    const lines = output.split(\"\\n\")\n    for (const line of lines) {\n      // Match TypeScript errors like: example-0.ts(5,3): error TS2304: Cannot find name 'foo'.\n      const match = line.match(/example-\\d+\\.ts\\(\\d+,\\d+\\): error TS\\d+: (.+)/)\n      if (match) {\n        const msg = match[1]\n        // Skip some noise errors\n        if (msg.includes(\"Cannot find module './assets/\")) continue\n        if (msg.includes(\"@ts-expect-error\")) continue\n\n        issues.push({\n          type: \"error\",\n          message: msg,\n        })\n      }\n    }\n  }\n\n  return issues\n}\n\n// Process a single MDX file\nasync function processFile(filePath: string): Promise<VerificationResult[]> {\n  const content = await readFile(filePath, \"utf-8\")\n  const blocks = extractCodeBlocks(content, filePath)\n  const results: VerificationResult[] = []\n\n  for (let i = 0; i < blocks.length; i++) {\n    const block = blocks[i]\n    const issues = await typeCheckBlock(block, i)\n\n    if (issues.length > 0) {\n      results.push({\n        file: filePath,\n        lineNumber: block.lineNumber,\n        issues,\n        codePreview: block.code.split(\"\\n\")[0].substring(0, 60),\n      })\n    }\n  }\n\n  return results\n}\n\nasync function main() {\n  const pattern = process.argv[2] || \"**/*.mdx\"\n\n  console.log(`Verifying documentation examples in: ${DOCS_DIR}`)\n  console.log(`Pattern: ${pattern}\\n`)\n\n  // Setup test environment\n  console.log(\"Setting up test environment...\")\n  const setupOk = await setupTestEnv()\n  if (!setupOk) {\n    process.exit(1)\n  }\n  console.log(\"Test environment ready.\\n\")\n\n  // Find MDX files\n  const globber = new Bun.Glob(pattern)\n  const files = [...globber.scanSync({ cwd: DOCS_DIR, absolute: true })]\n\n  console.log(`Found ${files.length} MDX files to verify\\n`)\n\n  let totalErrors = 0\n  let filesWithIssues = 0\n  const allResults: VerificationResult[] = []\n\n  for (const file of files) {\n    const relPath = relative(DOCS_DIR, file)\n    process.stdout.write(`Checking ${relPath}...`)\n\n    const results = await processFile(file)\n    allResults.push(...results)\n\n    if (results.length > 0) {\n      filesWithIssues++\n      console.log(` ${results.reduce((sum, r) => sum + r.issues.length, 0)} issues`)\n    } else {\n      console.log(\" OK\")\n    }\n  }\n\n  // Print detailed results\n  if (allResults.length > 0) {\n    console.log(\"\\n\" + \"=\".repeat(60))\n    console.log(\"ISSUES FOUND:\")\n    console.log(\"=\".repeat(60))\n\n    // Group by file\n    const byFile = new Map<string, VerificationResult[]>()\n    for (const result of allResults) {\n      const relPath = relative(DOCS_DIR, result.file)\n      if (!byFile.has(relPath)) {\n        byFile.set(relPath, [])\n      }\n      byFile.get(relPath)!.push(result)\n    }\n\n    for (const [file, results] of byFile) {\n      console.log(`\\n${file}:`)\n      for (const result of results) {\n        console.log(`  Line ${result.lineNumber}: ${result.codePreview}...`)\n        for (const issue of result.issues) {\n          console.log(`    - ${issue.message}`)\n          totalErrors++\n        }\n      }\n    }\n  }\n\n  console.log(`\\n${\"=\".repeat(60)}`)\n  console.log(`Summary:`)\n  console.log(`  Files checked: ${files.length}`)\n  console.log(`  Files with issues: ${filesWithIssues}`)\n  console.log(`  Total errors: ${totalErrors}`)\n\n  // Cleanup\n  await rm(TEST_DIR, { recursive: true })\n\n  process.exit(totalErrors > 0 ? 1 : 0)\n}\n\nmain().catch((err) => {\n  console.error(\"Error:\", err)\n  process.exit(1)\n})\n"
  },
  {
    "path": "packages/web/src/components/TuiSurface.astro",
    "content": "<div class=\"tui-visual-container\">\n  <canvas id=\"tui-canvas\"></canvas>\n</div>\n\n<style>\n  .tui-visual-container {\n    width: 100%;\n    height: 320px;\n    position: relative;\n  }\n\n  canvas {\n    display: block;\n    width: 100%;\n    height: 100%;\n  }\n</style>\n\n<script>\n  // TUI Constants\n  const CELL_W = 10;\n  const CELL_H = 18;\n  const FONT = '14px \"IBM Plex Mono\", monospace';\n  \n  class Renderer {\n    canvas: HTMLCanvasElement;\n    ctx: CanvasRenderingContext2D;\n    cols: number = 0;\n    rows: number = 0;\n    dpr: number = 1;\n    \n    // Theme colors\n    colors = {\n      bg: 'transparent',\n      fg: '#666',      // --color-text\n      fgStrong: '#1f1f1f', // --color-text-strong\n      fgWeak: '#999',  // --color-text-weak\n      border: '#e5e5e5', // --color-border\n    };\n    \n    syntax = {\n      keyword: '#6366f1',\n      string: '#059669',\n      function: '#0891b2',\n      component: '#9333ea',\n      punctuation: '#71717a',\n      text: '#3f3f46',\n    };\n\n    constructor(canvas: HTMLCanvasElement) {\n      this.canvas = canvas;\n      this.ctx = canvas.getContext('2d')!;\n      this.updateTheme();\n      this.resize();\n      \n      const resizeObserver = new ResizeObserver(() => this.resize());\n      resizeObserver.observe(canvas.parentElement!);\n      \n      // Watch for system theme changes\n      window.matchMedia(\"(prefers-color-scheme: dark)\").addEventListener(\"change\", () => this.updateTheme());\n    }\n    \n    updateTheme() {\n      const style = getComputedStyle(document.documentElement);\n      this.colors.fg = style.getPropertyValue('--color-text').trim() || '#666';\n      this.colors.fgStrong = style.getPropertyValue('--color-text-strong').trim() || '#111';\n      this.colors.fgWeak = style.getPropertyValue('--color-text-weak').trim() || '#999';\n      this.colors.border = style.getPropertyValue('--color-border').trim() || '#eee';\n      \n      // Update syntax colors based on theme\n      const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n      if (isDark) {\n        this.syntax = {\n          keyword: '#c9a0dc',\n          string: '#a5d6a7',\n          function: '#7dd3fc',\n          component: '#e0b0ff',\n          punctuation: '#a0a0a0',\n          text: '#e0e0e0',\n        };\n      } else {\n        this.syntax = {\n          keyword: '#6366f1',\n          string: '#059669',\n          function: '#0891b2',\n          component: '#9333ea',\n          punctuation: '#71717a',\n          text: '#3f3f46',\n        };\n      }\n    }\n\n    resize() {\n      const parent = this.canvas.parentElement!;\n      this.dpr = window.devicePixelRatio || 1;\n      const rect = parent.getBoundingClientRect();\n      \n      this.canvas.width = rect.width * this.dpr;\n      this.canvas.height = rect.height * this.dpr;\n      this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);\n      \n      this.cols = Math.floor(rect.width / CELL_W);\n      this.rows = Math.floor(rect.height / CELL_H);\n    }\n\n    clear() {\n      this.ctx.clearRect(0, 0, this.canvas.width / this.dpr, this.canvas.height / this.dpr);\n    }\n\n    drawChar(char: string, x: number, y: number, fg: string, bg?: string) {\n      if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) return;\n      \n      const px = x * CELL_W;\n      const py = y * CELL_H;\n\n      if (bg) {\n        this.ctx.fillStyle = bg;\n        this.ctx.fillRect(px, py, CELL_W, CELL_H);\n      }\n\n      this.ctx.font = FONT;\n      this.ctx.textBaseline = 'top';\n      this.ctx.fillStyle = fg;\n      this.ctx.fillText(char, px, py + 2);\n    }\n\n    drawBox(x: number, y: number, w: number, h: number, fg: string, style: 'single' | 'double' | 'bold' = 'single', label?: string) {\n      const c = style === 'double' ? { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' } :\n                style === 'bold'   ? { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' } :\n                                     { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' };\n\n      // Corners\n      this.drawChar(c.tl, x, y, fg);\n      this.drawChar(c.tr, x + w - 1, y, fg);\n      this.drawChar(c.bl, x, y + h - 1, fg);\n      this.drawChar(c.br, x + w - 1, y + h - 1, fg);\n\n      // Edges\n      for (let i = 1; i < w - 1; i++) {\n        this.drawChar(c.h, x + i, y, fg);\n        this.drawChar(c.h, x + i, y + h - 1, fg);\n      }\n      for (let i = 1; i < h - 1; i++) {\n        this.drawChar(c.v, x, y + i, fg);\n        this.drawChar(c.v, x + w - 1, y + i, fg);\n      }\n      \n      // Label with spacing: ┌─ Label ─┐\n      if (label) {\n        const paddedLabel = ` ${label} `;\n        for (let i = 0; i < paddedLabel.length; i++) {\n          const lx = x + 1 + i;\n          if (lx < x + w - 1) {\n            // Clear the cell first to remove the border character\n            const px = lx * CELL_W;\n            const py = y * CELL_H;\n            this.ctx.clearRect(px, py, CELL_W, CELL_H);\n            this.drawChar(paddedLabel[i], lx, y, fg);\n          }\n        }\n      }\n    }\n    \n    drawText(text: string, x: number, y: number, fg: string) {\n      for (let i = 0; i < text.length; i++) {\n        this.drawChar(text[i], x + i, y, fg);\n      }\n    }\n  }\n\n  // State\n  let activeFeature = 'layout';\n  \n  // Listen for feature changes\n  window.addEventListener('feature-change', (e: any) => {\n    activeFeature = e.detail;\n  });\n\n  const canvas = document.getElementById('tui-canvas') as HTMLCanvasElement;\n  if (canvas) {\n    const r = new Renderer(canvas);\n    \n    function animate(time: number) {\n      r.clear();\n      const t = time / 1000; // seconds\n      \n      const cx = Math.floor(r.cols / 2);\n      const cy = Math.floor(r.rows / 2);\n\n      switch (activeFeature) {\n        case 'layout': {\n          // Slow breathing layout\n          const phase = (Math.sin(t) + 1) / 2; // 0 to 1\n          const gap = 2;\n          const totalW = 40;\n          const h = 10;\n          \n          const wLeft = Math.floor(10 + phase * 10); // 10 to 20\n          const wRight = totalW - wLeft;\n          \n          const startX = cx - (totalW + gap) / 2;\n          const startY = cy - h / 2;\n          \n          r.drawBox(Math.floor(startX), Math.floor(startY), wLeft, h, r.colors.fgStrong, 'single', 'Nav');\n          r.drawBox(Math.floor(startX + wLeft + gap), Math.floor(startY), wRight, h, r.colors.fg, 'single', 'Content');\n          \n          // Show dimensions\n          r.drawText(`${wLeft}w`, Math.floor(startX), Math.floor(startY) - 1, r.colors.fgWeak);\n          r.drawText(`${wRight}w`, Math.floor(startX + wLeft + gap), Math.floor(startY) - 1, r.colors.fgWeak);\n          break;\n        }\n        \n        case 'syntax': {\n          const code = [\n            \"import { Text } from 'tui'\",\n            \"\",\n            \"function App() {\",\n            \"  return (\",\n            \"    <Text color='green'>\",\n            \"      Hello World\",\n            \"    </Text>\",\n            \"  )\",\n            \"}\"\n          ];\n          \n          const startX = cx - 15;\n          const startY = cy - 5;\n          \n          const cycleLength = code.length + 4;\n          const scanRow = Math.floor(t * 2) % cycleLength;\n          \n          const keywords = ['import', 'from', 'function', 'return', 'const'];\n          const components = ['Text'];\n          \n          code.forEach((line, i) => {\n            const isCurrentLine = i === scanRow;\n            const isHighlighted = i <= scanRow;\n            \n            const tokens = line.split(/(\\s+|[{}()<>='\",;/]|'[^']*')/).filter(Boolean);\n            let lx = startX;\n            \n            tokens.forEach(token => {\n               let color = r.colors.fgWeak;\n               \n               if (isHighlighted) {\n                  if (keywords.includes(token)) {\n                     color = r.syntax.keyword;\n                  } else if (components.includes(token)) {\n                     color = r.syntax.component;\n                  } else if (token.startsWith(\"'\") && token.endsWith(\"'\")) {\n                     color = r.syntax.string;\n                  } else if (/^[{}()<>=,;]$/.test(token)) {\n                     color = r.syntax.punctuation;\n                  } else if (token === 'App') {\n                     color = r.syntax.function;\n                  } else if (token.trim()) {\n                     color = r.syntax.text;\n                  }\n               }\n               \n               r.drawText(token, lx, startY + i, color);\n               lx += token.length;\n            });\n            \n            if (isCurrentLine && scanRow < code.length) {\n               r.drawChar('│', startX - 2, startY + i, r.colors.fgStrong);\n            }\n          });\n          break;\n        }\n\n        case 'components': {\n          // Focus cycling\n          const step = Math.floor(t / 1.5) % 3;\n          \n          const startX = cx - 12;\n          const startY = cy - 6;\n          \n          // Input\n          r.drawText(\"Username:\", startX, startY, r.colors.fg);\n          const f1 = step === 0;\n          r.drawBox(startX, startY + 1, 24, 3, f1 ? r.colors.fgStrong : r.colors.border, f1 ? 'double' : 'single');\n          r.drawText(f1 ? \"Simon|\" : \"Simon\", startX + 2, startY + 2, f1 ? r.colors.fgStrong : r.colors.fg);\n          \n          // Select\n          r.drawText(\"Role:\", startX, startY + 5, r.colors.fg);\n          const f2 = step === 1;\n          r.drawBox(startX, startY + 6, 24, 3, f2 ? r.colors.fgStrong : r.colors.border, f2 ? 'double' : 'single');\n          r.drawText(\"Developer ▼\", startX + 2, startY + 7, f2 ? r.colors.fgStrong : r.colors.fg);\n          \n          // Button\n          const f3 = step === 2;\n          r.drawBox(startX + 14, startY + 10, 10, 3, f3 ? r.colors.fgStrong : r.colors.border, f3 ? 'bold' : 'single');\n          r.drawText(\"Save\", startX + 17, startY + 11, f3 ? r.colors.fgStrong : r.colors.fg);\n          break;\n        }\n        \n        case 'keyboard': {\n           const items = [\" Dashboard\", \" Settings\", \" Profile\", \" Logout\"];\n           const idx = Math.floor(t * 1.5) % 4;\n           \n           const startX = cx - 10;\n           const startY = cy - 5;\n           \n           r.drawBox(startX, startY, 20, 8, r.colors.border, 'single', 'Menu');\n           \n           items.forEach((item, i) => {\n             const active = i === idx;\n             r.drawText(\n               (active ? \">\" : \" \") + item, \n               startX + 2, \n               startY + 2 + i, \n               active ? r.colors.fgStrong : r.colors.fg\n             );\n           });\n           break;\n        }\n        \n        case 'react': {\n           // Component tree\n           const startX = cx;\n           const startY = cy - 4;\n           \n           r.drawBox(startX - 4, startY, 8, 3, r.colors.fgStrong, 'single', 'App');\n           \n           // Lines\n           r.drawChar('│', startX, startY + 3, r.colors.border);\n           r.drawChar('┌', startX - 6, startY + 4, r.colors.border);\n           r.drawChar('─', startX - 5, startY + 4, r.colors.border);\n           r.drawChar('─', startX - 4, startY + 4, r.colors.border);\n           r.drawChar('─', startX - 3, startY + 4, r.colors.border);\n           r.drawChar('─', startX - 2, startY + 4, r.colors.border);\n           r.drawChar('─', startX - 1, startY + 4, r.colors.border);\n           r.drawChar('┴', startX, startY + 4, r.colors.border);\n           r.drawChar('─', startX + 1, startY + 4, r.colors.border);\n           r.drawChar('─', startX + 2, startY + 4, r.colors.border);\n           r.drawChar('─', startX + 3, startY + 4, r.colors.border);\n           r.drawChar('─', startX + 4, startY + 4, r.colors.border);\n           r.drawChar('─', startX + 5, startY + 4, r.colors.border);\n           r.drawChar('┐', startX + 6, startY + 4, r.colors.border);\n           \n           r.drawChar('│', startX - 6, startY + 5, r.colors.border);\n           r.drawChar('│', startX + 6, startY + 5, r.colors.border);\n           \n           // Update pulse\n           const pulse = Math.floor(t) % 2 === 0;\n           \n           r.drawBox(startX - 10, startY + 6, 8, 3, pulse ? r.colors.fgStrong : r.colors.fgWeak, 'single', 'List');\n           r.drawBox(startX + 2, startY + 6, 8, 3, !pulse ? r.colors.fgStrong : r.colors.fgWeak, 'single', 'Item');\n           \n           break;\n        }\n\n        case 'animations': {\n           // Ball bounce\n           const w = 30;\n           const h = 12;\n           const startX = cx - w/2;\n           const startY = cy - h/2;\n           \n           r.drawBox(startX, startY, w, h, r.colors.border);\n           \n           const bx = Math.abs(Math.sin(t) * (w - 3));\n           const by = Math.abs(Math.cos(t * 0.8) * (h - 3));\n           \n           // Draw trail\n           for(let i=1; i<4; i++) {\n             const ti = t - i * 0.1;\n             const tx = Math.abs(Math.sin(ti) * (w - 3));\n             const ty = Math.abs(Math.cos(ti * 0.8) * (h - 3));\n             r.drawChar('·', startX + 1 + tx, startY + 1 + ty, r.colors.border);\n           }\n           \n           r.drawChar('●', startX + 1 + bx, startY + 1 + by, r.colors.fgStrong);\n           break;\n        }\n      }\n      \n      requestAnimationFrame(animate);\n    }\n    \n    requestAnimationFrame(animate);\n  }\n</script>\n"
  },
  {
    "path": "packages/web/src/content/config.ts",
    "content": "import { defineCollection, z } from \"astro:content\"\n\nconst docs = defineCollection({\n  type: \"content\",\n  schema: z.object({\n    title: z.string(),\n    description: z.string().optional(),\n    order: z.number().optional(),\n  }),\n})\n\nexport const collections = {\n  docs,\n}\n"
  },
  {
    "path": "packages/web/src/content/docs/bindings/react.mdx",
    "content": "---\ntitle: React\ndescription: Build terminal UIs with React and OpenTUI\norder: 2\n---\n\n# React bindings\n\nBuild terminal user interfaces using React with familiar patterns and components.\n\n## Installation\n\nQuick start with [bun](https://bun.sh) and [create-tui](https://github.com/msmps/create-tui):\n\n```bash\nbun create tui --template react\n```\n\nManual installation:\n\n```bash\nbun install @opentui/react @opentui/core react\n```\n\n## Quick start\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nfunction App() {\n  return <text>Hello, world!</text>\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n## TypeScript configuration\n\nConfigure your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@opentui/react\",\n    \"strict\": true,\n    \"skipLibCheck\": true\n  }\n}\n```\n\n## Runtime-loaded plugin support (if needed)\n\nIf your app loads external TS/TSX modules at runtime (for example a file-based plugin system), import this once in your app entry before dynamic imports:\n\n```ts\nimport \"@opentui/react/runtime-plugin-support\"\n```\n\nUse this for both normal Bun runs and standalone compiled executables.\n\n## Components\n\nOpenTUI React provides JSX intrinsic elements that map to core renderables:\n\n### Layout & display\n\n- `<text>` - Text display with styling\n- `<box>` - Container with borders and layout\n- `<scrollbox>` - Scrollable container\n- `<ascii-font>` - ASCII art text\n\n### Input\n\n- `<input>` - Single-line text input\n- `<textarea>` - Multi-line text input\n- `<select>` - Selection list\n- `<tab-select>` - Tab-based selection\n\n### Code & diff\n\n- `<code>` - Syntax-highlighted code\n- `<line-number>` - Line numbers with diff/diagnostic support\n- `<diff>` - Unified or split diff viewer\n- `<markdown>` - Markdown rendering\n\n### Text modifiers\n\nUse inside `<text>` components:\n\n- `<span>` - Inline styled text\n- `<strong>`, `<b>` - Bold text\n- `<em>`, `<i>` - Italic text\n- `<u>` - Underlined text\n- `<br>` - Line break\n- `<a>` - Link text\n\n## API reference\n\n### `createRoot(renderer)`\n\nCreates a React root for rendering into the terminal.\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createRoot } from \"@opentui/react\"\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\nFor plugin slots, see [Plugin Slots overview](/docs/plugins/slots) and [React slots](/docs/plugins/react).\n\n## Hooks\n\n### `useRenderer()`\n\nAccess the OpenTUI renderer instance.\n\n```tsx\nimport { useRenderer } from \"@opentui/react\"\nimport { useEffect } from \"react\"\n\nfunction App() {\n  const renderer = useRenderer()\n\n  useEffect(() => {\n    renderer.console.show()\n    console.log(\"Hello from console!\")\n  }, [])\n\n  return <box />\n}\n```\n\n### `useKeyboard(handler, options?)`\n\nHandle keyboard events.\n\n```tsx\nimport { useKeyboard } from \"@opentui/react\"\n\nfunction App() {\n  useKeyboard((key) => {\n    if (key.name === \"escape\") {\n      process.exit(0)\n    }\n  })\n\n  return <text>Press ESC to exit</text>\n}\n```\n\nTo handle release events:\n\n```tsx\nuseKeyboard(\n  (event) => {\n    if (event.eventType === \"release\") {\n      console.log(\"Key released:\", event.name)\n    } else {\n      console.log(\"Key pressed:\", event.name)\n    }\n  },\n  { release: true },\n)\n```\n\n### `useOnResize(callback)`\n\nHandle terminal resize events.\n\n```tsx\nimport { useOnResize } from \"@opentui/react\"\n\nfunction App() {\n  useOnResize((width, height) => {\n    console.log(`Resized to ${width}x${height}`)\n  })\n\n  return <text>Resize-aware component</text>\n}\n```\n\n### `useTerminalDimensions()`\n\nGet reactive terminal dimensions.\n\n```tsx\nimport { useTerminalDimensions } from \"@opentui/react\"\n\nfunction App() {\n  const { width, height } = useTerminalDimensions()\n\n  return (\n    <text>\n      Terminal: {width}x{height}\n    </text>\n  )\n}\n```\n\n### `useTimeline(options?)`\n\nCreate and manage animations.\n\n```tsx\nimport { useTimeline } from \"@opentui/react\"\nimport { useEffect, useState } from \"react\"\n\nfunction App() {\n  const [width, setWidth] = useState(0)\n\n  const timeline = useTimeline({\n    duration: 2000,\n    loop: false,\n  })\n\n  useEffect(() => {\n    timeline.add(\n      { width },\n      {\n        width: 50,\n        duration: 2000,\n        ease: \"linear\",\n        onUpdate: (animation) => {\n          setWidth(animation.targets[0].width)\n        },\n      },\n    )\n  }, [])\n\n  return <box style={{ width, backgroundColor: \"#6a5acd\" }} />\n}\n```\n\n**Options:**\n\n- `duration` - Animation duration in ms (default: 1000)\n- `loop` - Whether to loop (default: false)\n- `autoplay` - Auto-start (default: true)\n- `onComplete` - Completion callback\n- `onPause` - Pause callback\n\n## Styling\n\nStyle components with props or the `style` prop:\n\n```tsx\n// Direct props\n<box backgroundColor=\"blue\" padding={2}>\n  <text>Hello</text>\n</box>\n\n// Style prop\n<box style={{ backgroundColor: \"blue\", padding: 2 }}>\n  <text>Hello</text>\n</box>\n```\n\n## Example: Login form\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createRoot, useKeyboard } from \"@opentui/react\"\nimport { useCallback, useState } from \"react\"\n\nfunction App() {\n  const [username, setUsername] = useState(\"\")\n  const [password, setPassword] = useState(\"\")\n  const [focused, setFocused] = useState<\"username\" | \"password\">(\"username\")\n  const [status, setStatus] = useState(\"idle\")\n\n  useKeyboard((key) => {\n    if (key.name === \"tab\") {\n      setFocused((prev) => (prev === \"username\" ? \"password\" : \"username\"))\n    }\n  })\n\n  const handleSubmit = useCallback(() => {\n    if (username === \"admin\" && password === \"secret\") {\n      setStatus(\"success\")\n    } else {\n      setStatus(\"error\")\n    }\n  }, [username, password])\n\n  return (\n    <box style={{ border: true, padding: 2, flexDirection: \"column\", gap: 1 }}>\n      <text fg=\"#FFFF00\">Login Form</text>\n\n      <box title=\"Username\" style={{ border: true, width: 40, height: 3 }}>\n        <input\n          placeholder=\"Enter username...\"\n          onInput={setUsername}\n          onSubmit={handleSubmit}\n          focused={focused === \"username\"}\n        />\n      </box>\n\n      <box title=\"Password\" style={{ border: true, width: 40, height: 3 }}>\n        <input\n          placeholder=\"Enter password...\"\n          onInput={setPassword}\n          onSubmit={handleSubmit}\n          focused={focused === \"password\"}\n        />\n      </box>\n\n      <text fg={status === \"success\" ? \"green\" : status === \"error\" ? \"red\" : \"#999\"}>{status.toUpperCase()}</text>\n    </box>\n  )\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n## Component extension\n\nRegister custom renderables as JSX elements:\n\n```tsx\nimport { BoxRenderable, createCliRenderer, type BoxOptions, type RenderContext } from \"@opentui/core\"\nimport { createRoot, extend } from \"@opentui/react\"\n\nclass ConsoleButtonRenderable extends BoxRenderable {\n  private _label: string = \"Button\"\n\n  constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {\n    super(ctx, options)\n    if (options.label) this._label = options.label\n    this.borderStyle = \"single\"\n    this.padding = 2\n  }\n\n  get label(): string {\n    return this._label\n  }\n\n  set label(value: string) {\n    this._label = value\n    this.requestRender()\n  }\n}\n\n// Add TypeScript support\ndeclare module \"@opentui/react\" {\n  interface OpenTUIComponents {\n    consoleButton: typeof ConsoleButtonRenderable\n  }\n}\n\n// Register the component\nextend({ consoleButton: ConsoleButtonRenderable })\n\n// Use in JSX\nfunction App() {\n  return <consoleButton label=\"Click me!\" style={{ border: true, backgroundColor: \"green\" }} />\n}\n\nconst renderer = await createCliRenderer()\ncreateRoot(renderer).render(<App />)\n```\n\n## React DevTools\n\nOpenTUI React supports React DevTools for debugging:\n\n1. Install:\n\n```bash\nbun add --dev react-devtools-core@7\n```\n\n2. Start DevTools:\n\n```bash\nnpx react-devtools@7\n```\n\n3. Run with DEV flag:\n\n```bash\nDEV=true bun run your-app.ts\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/bindings/solid.mdx",
    "content": "---\ntitle: Solid.js\ndescription: Build terminal UIs with Solid.js and OpenTUI\norder: 1\n---\n\n# Solid.js bindings\n\nBuild terminal user interfaces with Solid.js's fine-grained reactivity and OpenTUI.\n\n## Installation\n\n```bash\nbun install solid-js @opentui/solid\n```\n\n## Setup\n\n### 1. Configure TypeScript\n\nAdd JSX config to `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"@opentui/solid\"\n  }\n}\n```\n\n### 2. Configure Bun\n\nAdd preload script to `bunfig.toml`:\n\n```toml\npreload = [\"@opentui/solid/preload\"]\n```\n\n### 3. Enable runtime-loaded plugin support (if needed)\n\nIf your app loads external TS/TSX modules at runtime (for example a file-based plugin system), import this once in your entry file before dynamic imports:\n\n```ts\nimport \"@opentui/solid/runtime-plugin-support\"\n```\n\n### 4. Create your app\n\n```tsx\nimport { render } from \"@opentui/solid\"\n\nconst App = () => <text>Hello, World!</text>\n\nrender(App)\n```\n\nRun with `bun index.tsx`.\n\n## Components\n\nOpenTUI Solid provides JSX intrinsic elements that map to core renderables.\n\n**Note:** Solid uses snake_case for multi-word component names (e.g., `ascii_font`, `tab_select`).\n\n### Layout & display\n\n- `<text>` - Styled text container\n- `<box>` - Layout container with borders\n- `<scrollbox>` - Scrollable container\n- `<ascii_font>` - ASCII art text\n- `<markdown>` - Render Markdown content\n\n### Input\n\n- `<input>` - Single-line text input\n- `<textarea>` - Multi-line text input\n- `<select>` - List selection\n- `<tab_select>` - Tab-based selection\n\n### Code & diff\n\n- `<code>` - Syntax-highlighted code\n- `<line_number>` - Line numbers with diff/diagnostic support\n- `<diff>` - Unified or split diff viewer\n\n### Text modifiers\n\nUse inside `<text>` components:\n\n- `<span>` - Inline styled text\n- `<strong>`, `<b>` - Bold text\n- `<em>`, `<i>` - Italic text\n- `<u>` - Underlined text\n- `<br>` - Line break\n- `<a>` - Link text with href\n\n## API reference\n\n### `render(node, rendererOrConfig?)`\n\nRender a Solid component tree into a CLI renderer.\n\n```tsx\nimport { render } from \"@opentui/solid\"\n\n// Simple usage\nrender(() => <App />)\n\n// With renderer config\nrender(() => <App />, {\n  targetFps: 30,\n  exitOnCtrlC: false,\n})\n```\n\n**Parameters:**\n\n- `node` - Function returning a JSX element\n- `rendererOrConfig` - Optional `CliRenderer` instance or `CliRendererConfig`\n\n### `testRender(node, options?)`\n\nCreate a test renderer for snapshots and interaction tests.\n\n```tsx\nimport { testRender } from \"@opentui/solid\"\n\nconst testSetup = await testRender(() => <App />, { width: 40, height: 10 })\n```\n\n### `extend(components)`\n\nRegister custom renderables as JSX intrinsic elements.\n\n```tsx\nimport { extend } from \"@opentui/solid\"\n\nextend({ custom_box: CustomBoxRenderable })\n```\n\n### `getComponentCatalogue()`\n\nReturns the current component catalogue that powers JSX tag lookup.\n\nFor plugin slots, see [Plugin Slots overview](/docs/plugins/slots) and [Solid slots](/docs/plugins/solid).\n\n## Hooks\n\n### `useRenderer()`\n\nAccess the OpenTUI renderer instance.\n\n```tsx\nimport { useRenderer } from \"@opentui/solid\"\nimport { onMount } from \"solid-js\"\n\nconst App = () => {\n  const renderer = useRenderer()\n\n  onMount(() => {\n    renderer.console.show()\n    console.log(\"Hello from console!\")\n  })\n\n  return <box />\n}\n```\n\n### `useKeyboard(handler, options?)`\n\nSubscribe to keyboard events.\n\n```tsx\nimport { useKeyboard } from \"@opentui/solid\"\n\nconst App = () => {\n  useKeyboard((key) => {\n    if (key.name === \"escape\") {\n      process.exit(0)\n    }\n  })\n\n  return <text>Press ESC to exit</text>\n}\n```\n\nWith release events:\n\n```tsx\nimport { createSignal } from \"solid-js\"\n\nconst App = () => {\n  const [pressedKeys, setPressedKeys] = createSignal(new Set<string>())\n\n  useKeyboard(\n    (event) => {\n      setPressedKeys((keys) => {\n        const newKeys = new Set(keys)\n        if (event.eventType === \"release\") {\n          newKeys.delete(event.name)\n        } else {\n          newKeys.add(event.name)\n        }\n        return newKeys\n      })\n    },\n    { release: true },\n  )\n\n  return <text>Pressed: {Array.from(pressedKeys()).join(\", \") || \"none\"}</text>\n}\n```\n\n### `onResize(callback)`\n\nHandle terminal resize events.\n\n```tsx\nimport { onResize } from \"@opentui/solid\"\n\nconst App = () => {\n  onResize((width, height) => {\n    console.log(`Resized to ${width}x${height}`)\n  })\n\n  return <text>Resize-aware component</text>\n}\n```\n\n### `onFocus(callback)`\n\nRun side effects when the terminal window gains focus.\n\n```tsx\nimport { onFocus } from \"@opentui/solid\"\n\nconst App = () => {\n  onFocus(() => {\n    console.log(\"Terminal focused\")\n  })\n\n  return <text>Switch away and back to trigger focus events</text>\n}\n```\n\n### `onBlur(callback)`\n\nRun side effects when the terminal window loses focus.\n\n```tsx\nimport { onBlur } from \"@opentui/solid\"\n\nconst App = () => {\n  onBlur(() => {\n    console.log(\"Terminal blurred\")\n  })\n\n  return <text>Switch away and back to trigger blur events</text>\n}\n```\n\nThese hooks listen for terminal focus-in/focus-out events when the terminal emulator supports them.\n\n### `useTerminalDimensions()`\n\nGet reactive terminal dimensions (returns a Solid signal).\n\n```tsx\nimport { useTerminalDimensions } from \"@opentui/solid\"\n\nconst App = () => {\n  const dimensions = useTerminalDimensions()\n\n  return (\n    <text>\n      Terminal: {dimensions().width}x{dimensions().height}\n    </text>\n  )\n}\n```\n\n### `usePaste(handler)`\n\nSubscribe to paste events.\n\n```tsx\nimport { usePaste } from \"@opentui/solid\"\n\nconst textDecoder = new TextDecoder()\n\nconst App = () => {\n  usePaste((event) => {\n    console.log(\"Pasted:\", textDecoder.decode(event.bytes))\n  })\n\n  return <text>Paste something!</text>\n}\n```\n\n### `useSelectionHandler(callback)`\n\nHandle text selection events.\n\n```tsx\nimport { useSelectionHandler } from \"@opentui/solid\"\n\nconst App = () => {\n  useSelectionHandler((selection) => {\n    console.log(\"Selected:\", selection)\n  })\n\n  return <text selectable>Select me!</text>\n}\n```\n\n### `useTimeline(options?)`\n\nCreate and manage animations.\n\n```tsx\nimport { useTimeline } from \"@opentui/solid\"\nimport { createSignal, onMount } from \"solid-js\"\n\nconst App = () => {\n  const [width, setWidth] = createSignal(0)\n\n  const timeline = useTimeline({\n    duration: 2000,\n    loop: false,\n  })\n\n  onMount(() => {\n    timeline.add(\n      { width: width() },\n      {\n        width: 50,\n        duration: 2000,\n        ease: \"linear\",\n        onUpdate: (animation) => {\n          setWidth(animation.targets[0].width)\n        },\n      },\n    )\n  })\n\n  return <box style={{ width: width(), backgroundColor: \"#6a5acd\" }} />\n}\n```\n\n## Special components\n\n### `Portal`\n\nRender children into a different mount point (useful for modals and overlays).\n\n```tsx\nimport { Portal, useRenderer } from \"@opentui/solid\"\n\nconst App = () => {\n  const renderer = useRenderer()\n\n  return (\n    <box>\n      <text>Main content</text>\n      <Portal mount={renderer.root}>\n        <box border>Overlay</box>\n      </Portal>\n    </box>\n  )\n}\n```\n\n### `Dynamic`\n\nRender arbitrary intrinsic elements or components dynamically.\n\n```tsx\nimport { Dynamic } from \"@opentui/solid\"\nimport { createSignal } from \"solid-js\"\n\nconst App = () => {\n  const [isMultiline, setIsMultiline] = createSignal(false)\n\n  return <Dynamic component={isMultiline() ? \"textarea\" : \"input\"} />\n}\n```\n\n## Building for production\n\nUse [Bun.build](https://bun.sh/docs/bundler) with the Solid plugin:\n\n```ts\nimport solidPlugin from \"@opentui/solid/bun-plugin\"\n\nawait Bun.build({\n  entrypoints: [\"./index.tsx\"],\n  target: \"bun\",\n  outdir: \"./build\",\n  plugins: [solidPlugin],\n})\n```\n\nTo compile to a standalone executable:\n\n```ts\nawait Bun.build({\n  entrypoints: [\"./index.tsx\"],\n  plugins: [solidPlugin],\n  compile: {\n    target: \"bun-darwin-arm64\",\n    outfile: \"./app-macos\",\n  },\n})\n```\n\nIf that executable loads external plugins/modules at runtime, keep `import \"@opentui/solid/runtime-plugin-support\"` in your app entry.\n\n## Example: counter\n\n```tsx\nimport { render, useKeyboard } from \"@opentui/solid\"\nimport { createSignal } from \"solid-js\"\n\nconst App = () => {\n  const [count, setCount] = createSignal(0)\n\n  useKeyboard((key) => {\n    if (key.name === \"up\") setCount((c) => c + 1)\n    if (key.name === \"down\") setCount((c) => c - 1)\n    if (key.name === \"escape\") process.exit(0)\n  })\n\n  return (\n    <box border padding={2}>\n      <text>Count: {count()}</text>\n      <text fg=\"#888\">Up/Down to change, ESC to exit</text>\n    </box>\n  )\n}\n\nrender(App)\n```\n\n## Differences from React bindings\n\n| Aspect             | Solid                                                  | React                                  |\n| ------------------ | ------------------------------------------------------ | -------------------------------------- |\n| Render function    | `render(() => <App />)`                                | `createRoot(renderer).render(<App />)` |\n| Component naming   | snake_case (`ascii_font`)                              | kebab-case (`ascii-font`)              |\n| State              | `createSignal`                                         | `useState`                             |\n| Effects            | `onMount`, `onCleanup`                                 | `useEffect`                            |\n| Resize hook        | `onResize(callback)`                                   | `useOnResize(callback)`                |\n| Dimensions         | Returns signal: `dimensions().width`                   | Returns object: `dimensions.width`     |\n| Extra hooks        | `onFocus`, `onBlur`, `usePaste`, `useSelectionHandler` | -                                      |\n| Special components | `Portal`, `Dynamic`                                    | -                                      |\n"
  },
  {
    "path": "packages/web/src/content/docs/components/ascii-font.mdx",
    "content": "---\ntitle: ASCIIFont\ndescription: Display text using ASCII art fonts\norder: 6\n---\n\n# ASCIIFont\n\nDisplay text using ASCII art fonts with multiple font styles available. Great for titles, headers, and decorative text.\n\n## Basic Usage\n\n### Renderable API\n\n```typescript\nimport { ASCIIFontRenderable, RGBA, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst title = new ASCIIFontRenderable(renderer, {\n  id: \"title\",\n  text: \"OPENTUI\",\n  font: \"tiny\",\n  color: RGBA.fromInts(255, 255, 255, 255),\n})\n\nrenderer.root.add(title)\n```\n\n### Construct API\n\n```typescript\nimport { ASCIIFont, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nrenderer.root.add(\n  ASCIIFont({\n    text: \"HELLO\",\n    font: \"block\",\n    color: \"#00FF00\",\n  }),\n)\n```\n\n## Available Fonts\n\nOpenTUI includes several ASCII art font styles:\n\n```typescript\n// Small, compact font\n{\n  font: \"tiny\"\n}\n\n// Block style font\n{\n  font: \"block\"\n}\n\n// Shaded style font\n{\n  font: \"shade\"\n}\n\n// Slick style font\n{\n  font: \"slick\"\n}\n\n// Large font\n{\n  font: \"huge\"\n}\n\n// Grid style font\n{\n  font: \"grid\"\n}\n\n// Pallet style font\n{\n  font: \"pallet\"\n}\n```\n\n## Positioning\n\nPosition the ASCII text anywhere on screen:\n\n```typescript\nconst title = new ASCIIFontRenderable(renderer, {\n  id: \"title\",\n  text: \"TITLE\",\n  font: \"block\",\n  color: RGBA.fromHex(\"#FFFF00\"),\n  x: 10,\n  y: 2,\n})\n```\n\n## Properties\n\n| Property          | Type                         | Default         | Description                |\n| ----------------- | ---------------------------- | --------------- | -------------------------- |\n| `text`            | `string`                     | `\"\"`            | Text to display            |\n| `font`            | `ASCIIFontName`              | `\"tiny\"`        | Font style to use          |\n| `color`           | `ColorInput \\| ColorInput[]` | `\"#FFFFFF\"`     | Text color(s)              |\n| `backgroundColor` | `ColorInput`                 | `\"transparent\"` | Background color           |\n| `selectable`      | `boolean`                    | `true`          | Whether text is selectable |\n| `selectionBg`     | `ColorInput`                 | -               | Selection background color |\n| `selectionFg`     | `ColorInput`                 | -               | Selection foreground color |\n| `x`               | `number`                     | -               | X position offset          |\n| `y`               | `number`                     | -               | Y position offset          |\n\n## Example: Welcome Screen\n\n```typescript\nimport { Box, ASCIIFont, Text, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst welcomeScreen = Box(\n  {\n    width: \"100%\",\n    height: \"100%\",\n    flexDirection: \"column\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  },\n  ASCIIFont({\n    text: \"OPENTUI\",\n    font: \"huge\",\n    color: \"#00FFFF\",\n  }),\n  Text({\n    content: \"Terminal UI Framework\",\n    fg: \"#888888\",\n  }),\n  Text({\n    content: \"Press any key to continue...\",\n    fg: \"#444444\",\n  }),\n)\n\nrenderer.root.add(welcomeScreen)\n```\n\n## Dynamic Text\n\nUpdate the text content dynamically:\n\n```typescript\nconst counter = new ASCIIFontRenderable(renderer, {\n  id: \"counter\",\n  text: \"0\",\n  font: \"block\",\n  color: RGBA.fromHex(\"#FF0000\"),\n})\n\nlet count = 0\nsetInterval(() => {\n  count++\n  counter.text = count.toString()\n}, 1000)\n```\n\n## Color Effects\n\nCreate gradient-like effects by positioning multiple ASCII fonts:\n\n```typescript\nimport { Box, ASCIIFont } from \"@opentui/core\"\n\nconst gradientTitle = Box(\n  {},\n  ASCIIFont({\n    text: \"HELLO\",\n    font: \"block\",\n    color: \"#FF0000\",\n  }),\n  // Overlay with offset for shadow effect\n  ASCIIFont({\n    text: \"HELLO\",\n    font: \"block\",\n    color: \"#880000\",\n    left: 1,\n    top: 1,\n  }),\n)\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/components/box.mdx",
    "content": "---\ntitle: Box\ndescription: Container component with borders and layout capabilities\norder: 2\n---\n\n# Box\n\nA container component with borders, background colors, and layout capabilities. Use it to create panels, frames, and organized sections.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport { BoxRenderable, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst panel = new BoxRenderable(renderer, {\n  id: \"panel\",\n  width: 30,\n  height: 10,\n  backgroundColor: \"#333366\",\n  borderStyle: \"double\",\n  borderColor: \"#FFFFFF\",\n})\n\nrenderer.root.add(panel)\n```\n\n### Construct API\n\n```typescript\nimport { Box, Text, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nrenderer.root.add(\n  Box(\n    {\n      width: 30,\n      height: 10,\n      backgroundColor: \"#333366\",\n      borderStyle: \"rounded\",\n    },\n    Text({ content: \"Inside the box!\" }),\n  ),\n)\n```\n\n## Border styles\n\n```typescript\n// No border\n{\n  border: false\n}\n\n// Simple border (default style)\n{\n  border: true\n}\n\n// Specific border styles\n{\n  borderStyle: \"single\"\n} // Single line: ┌─┐│└─┘\n{\n  borderStyle: \"double\"\n} // Double line: ╔═╗║╚═╝\n{\n  borderStyle: \"rounded\"\n} // Rounded corners: ╭─╮│╰─╯\n{\n  borderStyle: \"heavy\"\n} // Heavy lines: ┏━┓┃┗━┛\n```\n\n## Titles\n\nAdd a title to the box border:\n\n```typescript\nconst panel = new BoxRenderable(renderer, {\n  id: \"settings\",\n  width: 40,\n  height: 15,\n  borderStyle: \"rounded\",\n  title: \"Settings\",\n  titleAlignment: \"center\",\n})\n```\n\n### Title alignment\n\n```typescript\n{\n  titleAlignment: \"left\"\n} // ┌─ Title ────────┐\n{\n  titleAlignment: \"center\"\n} // ┌──── Title ─────┐\n{\n  titleAlignment: \"right\"\n} // ┌────────── Title ┐\n```\n\n## Layout container\n\nBox works as a flex container for child elements:\n\n```typescript\nconst container = Box(\n  {\n    flexDirection: \"column\",\n    justifyContent: \"space-between\",\n    alignItems: \"stretch\",\n    width: 50,\n    height: 20,\n    padding: 1,\n    gap: 1,\n  },\n  Text({ content: \"Header\" }),\n  Box({ flexGrow: 1, backgroundColor: \"#222\" }, Text({ content: \"Content area\" })),\n  Text({ content: \"Footer\" }),\n)\n```\n\n## Mouse events\n\nHandle mouse interactions on the box:\n\n```typescript\nconst button = new BoxRenderable(renderer, {\n  id: \"button\",\n  width: 12,\n  height: 3,\n  border: true,\n  backgroundColor: \"#444\",\n  onMouseDown: () => {\n    console.log(\"Button clicked!\")\n  },\n  onMouseOver: () => {\n    button.backgroundColor = \"#666\"\n  },\n  onMouseOut: () => {\n    button.backgroundColor = \"#444\"\n  },\n})\n```\n\n## Properties\n\n| Property          | Type               | Default        | Description                       |\n| ----------------- | ------------------ | -------------- | --------------------------------- |\n| `width`           | `number \\| string` | -              | Width in characters or percentage |\n| `height`          | `number \\| string` | -              | Height in rows or percentage      |\n| `backgroundColor` | `string \\| RGBA`   | -              | Background fill color             |\n| `border`          | `boolean`          | `false`        | Show border                       |\n| `borderStyle`     | `string`           | `\"single\"`     | Border style                      |\n| `borderColor`     | `string \\| RGBA`   | -              | Border color                      |\n| `title`           | `string`           | -              | Title text in border              |\n| `titleAlignment`  | `string`           | `\"left\"`       | Title position                    |\n| `padding`         | `number`           | `0`            | Internal padding                  |\n| `gap`             | `number \\| string` | -              | Gap between children              |\n| `flexDirection`   | `string`           | `\"column\"`     | Child layout direction            |\n| `justifyContent`  | `string`           | `\"flex-start\"` | Main axis alignment               |\n| `alignItems`      | `string`           | `\"stretch\"`    | Cross axis alignment              |\n\n## Example: Card component\n\n```typescript\nimport { Box, Text, t, bold, fg } from \"@opentui/core\"\n\nfunction Card(props: { title: string; description: string }) {\n  return Box(\n    {\n      width: 40,\n      borderStyle: \"rounded\",\n      borderColor: \"#666\",\n      padding: 1,\n      margin: 1,\n    },\n    Text({\n      content: t`${bold(fg(\"#00FFFF\")(props.title))}`,\n    }),\n    Text({\n      content: props.description,\n      fg: \"#AAAAAA\",\n    }),\n  )\n}\n\nrenderer.root.add(\n  Box(\n    { flexDirection: \"row\", flexWrap: \"wrap\" },\n    Card({ title: \"Feature 1\", description: \"Description of feature 1\" }),\n    Card({ title: \"Feature 2\", description: \"Description of feature 2\" }),\n    Card({ title: \"Feature 3\", description: \"Description of feature 3\" }),\n  ),\n)\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/components/code.mdx",
    "content": "---\ntitle: Code\ndescription: Syntax-highlighted code display component with Tree-sitter support\norder: 6\n---\n\n# Code\n\nDisplays syntax-highlighted code using Tree-sitter. Supports many languages with accurate, fast highlighting.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport { CodeRenderable, createCliRenderer, SyntaxStyle, RGBA } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  keyword: { fg: RGBA.fromHex(\"#FF7B72\"), bold: true },\n  string: { fg: RGBA.fromHex(\"#A5D6FF\") },\n  comment: { fg: RGBA.fromHex(\"#8B949E\"), italic: true },\n  number: { fg: RGBA.fromHex(\"#79C0FF\") },\n  function: { fg: RGBA.fromHex(\"#D2A8FF\") },\n  default: { fg: RGBA.fromHex(\"#E6EDF3\") },\n})\n\nconst code = new CodeRenderable(renderer, {\n  id: \"code\",\n  content: `function hello() {\n  // This is a comment\n  const message = \"Hello, world!\"\n  return message\n}`,\n  filetype: \"javascript\",\n  syntaxStyle,\n  width: 50,\n  height: 10,\n})\n\nrenderer.root.add(code)\n```\n\n### Construct API\n\n```typescript\nimport { Code, Box, createCliRenderer, SyntaxStyle, RGBA } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  keyword: { fg: RGBA.fromHex(\"#FF7B72\"), bold: true },\n  string: { fg: RGBA.fromHex(\"#A5D6FF\") },\n  default: { fg: RGBA.fromHex(\"#E6EDF3\") },\n})\n\nrenderer.root.add(\n  Box(\n    { border: true, width: 50, height: 10 },\n    Code({\n      content: 'const x = \"hello\"',\n      filetype: \"javascript\",\n      syntaxStyle,\n    }),\n  ),\n)\n```\n\n## Creating syntax styles\n\nUse `SyntaxStyle.fromStyles()` to define colors and attributes for syntax tokens:\n\n```typescript\nimport { SyntaxStyle, RGBA, parseColor } from \"@opentui/core\"\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  // Basic tokens\n  keyword: { fg: RGBA.fromHex(\"#FF7B72\"), bold: true },\n  \"keyword.import\": { fg: RGBA.fromHex(\"#FF7B72\"), bold: true },\n  \"keyword.operator\": { fg: RGBA.fromHex(\"#FF7B72\") },\n\n  string: { fg: RGBA.fromHex(\"#A5D6FF\") },\n  comment: { fg: RGBA.fromHex(\"#8B949E\"), italic: true },\n  number: { fg: RGBA.fromHex(\"#79C0FF\") },\n  boolean: { fg: RGBA.fromHex(\"#79C0FF\") },\n  constant: { fg: RGBA.fromHex(\"#79C0FF\") },\n\n  // Functions and types\n  function: { fg: RGBA.fromHex(\"#D2A8FF\") },\n  \"function.call\": { fg: RGBA.fromHex(\"#D2A8FF\") },\n  \"function.method.call\": { fg: RGBA.fromHex(\"#D2A8FF\") },\n  type: { fg: RGBA.fromHex(\"#FFA657\") },\n  constructor: { fg: RGBA.fromHex(\"#FFA657\") },\n\n  // Variables and properties\n  variable: { fg: RGBA.fromHex(\"#E6EDF3\") },\n  \"variable.member\": { fg: RGBA.fromHex(\"#79C0FF\") },\n  property: { fg: RGBA.fromHex(\"#79C0FF\") },\n\n  // Operators and punctuation\n  operator: { fg: RGBA.fromHex(\"#FF7B72\") },\n  punctuation: { fg: RGBA.fromHex(\"#F0F6FC\") },\n  \"punctuation.bracket\": { fg: RGBA.fromHex(\"#F0F6FC\") },\n  \"punctuation.delimiter\": { fg: RGBA.fromHex(\"#C9D1D9\") },\n\n  // Default fallback\n  default: { fg: RGBA.fromHex(\"#E6EDF3\") },\n})\n```\n\n### Style properties\n\nEach style definition can include:\n\n| Property    | Type      | Description             |\n| ----------- | --------- | ----------------------- |\n| `fg`        | `RGBA`    | Foreground (text) color |\n| `bg`        | `RGBA`    | Background color        |\n| `bold`      | `boolean` | Bold text               |\n| `italic`    | `boolean` | Italic text             |\n| `underline` | `boolean` | Underlined text         |\n| `dim`       | `boolean` | Dimmed text             |\n\n## Supported languages\n\nCode uses Tree-sitter for parsing. Tree-sitter supports these languages:\n\n- TypeScript / JavaScript\n- Markdown\n- Zig\n- And any language with a Tree-sitter grammar\n\n## Streaming mode\n\nEnable streaming mode when content arrives incrementally, like LLM output:\n\n```typescript\nconst code = new CodeRenderable(renderer, {\n  id: \"streaming-code\",\n  content: \"\",\n  filetype: \"typescript\",\n  syntaxStyle,\n  streaming: true, // Enable streaming mode\n})\n\n// Later, append content\ncode.content += \"const x = 1\\n\"\ncode.content += \"const y = 2\\n\"\n```\n\nStreaming mode optimizes highlighting for incremental updates.\n\n## Text selection\n\nEnable text selection for copy operations:\n\n```typescript\nconst code = new CodeRenderable(renderer, {\n  id: \"code\",\n  content: sourceCode,\n  filetype: \"typescript\",\n  syntaxStyle,\n  selectable: true,\n  selectionBg: \"#264F78\",\n  selectionFg: \"#FFFFFF\",\n})\n```\n\n## Concealment\n\nThe `conceal` option controls whether certain syntax elements (like markdown formatting characters) are hidden:\n\n```typescript\nconst code = new CodeRenderable(renderer, {\n  id: \"markdown\",\n  content: \"# Heading\\n**bold** text\",\n  filetype: \"markdown\",\n  syntaxStyle,\n  conceal: true, // Hide formatting characters\n})\n```\n\n## With line numbers\n\nUse `LineNumberRenderable` to add line numbers:\n\n```typescript\nimport { CodeRenderable, LineNumberRenderable, ScrollBoxRenderable } from \"@opentui/core\"\n\nconst code = new CodeRenderable(renderer, {\n  id: \"code\",\n  content: sourceCode,\n  filetype: \"typescript\",\n  syntaxStyle,\n  width: \"100%\",\n})\n\nconst lineNumbers = new LineNumberRenderable(renderer, {\n  id: \"code-with-lines\",\n  target: code,\n  minWidth: 3,\n  paddingRight: 1,\n  fg: \"#6b7280\",\n  bg: \"#161b22\",\n  width: \"100%\",\n})\n\n// Wrap in ScrollBox for scrolling\nconst scrollbox = new ScrollBoxRenderable(renderer, {\n  id: \"scrollbox\",\n  width: 60,\n  height: 20,\n})\nscrollbox.add(lineNumbers)\n```\n\n## Properties\n\n| Property           | Type               | Default  | Description                              |\n| ------------------ | ------------------ | -------- | ---------------------------------------- |\n| `content`          | `string`           | `\"\"`     | Source code to display                   |\n| `filetype`         | `string`           | -        | Language for syntax highlighting         |\n| `syntaxStyle`      | `SyntaxStyle`      | required | Syntax highlighting theme                |\n| `streaming`        | `boolean`          | `false`  | Optimize for incremental content updates |\n| `conceal`          | `boolean`          | `true`   | Hide concealed syntax elements           |\n| `drawUnstyledText` | `boolean`          | `true`   | Show text before highlighting completes  |\n| `treeSitterClient` | `TreeSitterClient` | -        | Custom Tree-sitter client instance       |\n\n### Inherited from TextBufferRenderable\n\n| Property       | Type               | Default  | Description                                 |\n| -------------- | ------------------ | -------- | ------------------------------------------- |\n| `fg`           | `string \\| RGBA`   | -        | Default foreground color                    |\n| `bg`           | `string \\| RGBA`   | -        | Background color                            |\n| `selectable`   | `boolean`          | `false`  | Enable text selection                       |\n| `selectionBg`  | `string \\| RGBA`   | -        | Selection background color                  |\n| `selectionFg`  | `string \\| RGBA`   | -        | Selection foreground color                  |\n| `wrapMode`     | `string`           | `\"word\"` | Text wrapping: `\"none\"`, `\"char\"`, `\"word\"` |\n| `tabIndicator` | `string \\| number` | -        | Tab display character or width              |\n\n## Additional properties\n\n| Property         | Type      | Description                                  |\n| ---------------- | --------- | -------------------------------------------- |\n| `lineCount`      | `number`  | Number of lines in content                   |\n| `scrollY`        | `number`  | Current vertical scroll position (get/set)   |\n| `scrollX`        | `number`  | Current horizontal scroll position (get/set) |\n| `scrollWidth`    | `number`  | Total scrollable width (read-only)           |\n| `scrollHeight`   | `number`  | Total scrollable height (read-only)          |\n| `isHighlighting` | `boolean` | Whether highlighting is in progress          |\n| `plainText`      | `string`  | Raw text content                             |\n\n## Markdown styles\n\nFor markdown highlighting, use markup-prefixed style names:\n\n```typescript\nconst markdownStyle = SyntaxStyle.fromStyles({\n  \"markup.heading\": { fg: RGBA.fromHex(\"#58A6FF\"), bold: true },\n  \"markup.heading.1\": { fg: RGBA.fromHex(\"#00FF88\"), bold: true, underline: true },\n  \"markup.heading.2\": { fg: RGBA.fromHex(\"#00D7FF\"), bold: true },\n  \"markup.bold\": { fg: RGBA.fromHex(\"#F0F6FC\"), bold: true },\n  \"markup.strong\": { fg: RGBA.fromHex(\"#F0F6FC\"), bold: true },\n  \"markup.italic\": { fg: RGBA.fromHex(\"#F0F6FC\"), italic: true },\n  \"markup.list\": { fg: RGBA.fromHex(\"#FF7B72\") },\n  \"markup.quote\": { fg: RGBA.fromHex(\"#8B949E\"), italic: true },\n  \"markup.raw\": { fg: RGBA.fromHex(\"#A5D6FF\") },\n  \"markup.raw.block\": { fg: RGBA.fromHex(\"#A5D6FF\") },\n  \"markup.link\": { fg: RGBA.fromHex(\"#58A6FF\"), underline: true },\n  \"markup.link.url\": { fg: RGBA.fromHex(\"#58A6FF\"), underline: true },\n  default: { fg: RGBA.fromHex(\"#E6EDF3\") },\n})\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/components/diff.mdx",
    "content": "---\ntitle: Diff\ndescription: Unified or split diff viewer\norder: 15\n---\n\n# Diff\n\nRender unified or split diffs with syntax highlighting and optional line numbers.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport { DiffRenderable, SyntaxStyle, RGBA, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  default: { fg: RGBA.fromHex(\"#E6EDF3\") },\n  string: { fg: RGBA.fromHex(\"#A5D6FF\") },\n  keyword: { fg: RGBA.fromHex(\"#FF7B72\"), bold: true },\n})\n\nconst diff = new DiffRenderable(renderer, {\n  id: \"diff\",\n  width: \"100%\",\n  height: 16,\n  diff: `diff --git a/app.ts b/app.ts\\nindex 1111111..2222222 100644\\n--- a/app.ts\\n+++ b/app.ts\\n@@ -1,3 +1,3 @@\\n-const a = 1\\n+const a = 2\\n`,\n  view: \"split\",\n  filetype: \"typescript\",\n  syntaxStyle,\n  showLineNumbers: true,\n})\n\nrenderer.root.add(diff)\n```\n\n## Construct API\n\n> Not available yet. Use `DiffRenderable` for now.\n\n## Properties\n\n| Property              | Type                            | Default     | Description                           |\n| --------------------- | ------------------------------- | ----------- | ------------------------------------- |\n| `diff`                | `string`                        | `\"\"`        | Unified diff string                   |\n| `view`                | `\"unified\"` or `\"split\"`        | `\"unified\"` | Layout style                          |\n| `filetype`            | `string`                        | -           | Syntax highlighting language          |\n| `syntaxStyle`         | `SyntaxStyle`                   | -           | Syntax style for code                 |\n| `wrapMode`            | `\"word\"`, `\"char\"`, or `\"none\"` | -           | Code wrapping mode                    |\n| `conceal`             | `boolean`                       | `false`     | Conceal markup when highlighting      |\n| `treeSitterClient`    | `TreeSitterClient`              | -           | Custom Tree-sitter client             |\n| `showLineNumbers`     | `boolean`                       | `true`      | Show line numbers                     |\n| `lineNumberFg`        | `string` or `RGBA`              | `#888888`   | Line number text color                |\n| `lineNumberBg`        | `string` or `RGBA`              | transparent | Line number background                |\n| `addedLineNumberBg`   | `string` or `RGBA`              | transparent | Line number background for added      |\n| `removedLineNumberBg` | `string` or `RGBA`              | transparent | Line number background for removed    |\n| `addedBg`             | `string` or `RGBA`              | `#1a4d1a`   | Background for added lines            |\n| `removedBg`           | `string` or `RGBA`              | `#4d1a1a`   | Background for removed lines          |\n| `contextBg`           | `string` or `RGBA`              | transparent | Background for context lines          |\n| `addedContentBg`      | `string` or `RGBA`              | -           | Optional content background (added)   |\n| `removedContentBg`    | `string` or `RGBA`              | -           | Optional content background (removed) |\n| `contextContentBg`    | `string` or `RGBA`              | -           | Optional content background (context) |\n| `addedSignColor`      | `string` or `RGBA`              | `#22c55e`   | Sign color for added lines            |\n| `removedSignColor`    | `string` or `RGBA`              | `#ef4444`   | Sign color for removed lines          |\n"
  },
  {
    "path": "packages/web/src/content/docs/components/frame-buffer.mdx",
    "content": "---\ntitle: FrameBuffer\ndescription: Low-level rendering surface for custom graphics\norder: 7\n---\n\n# FrameBuffer\n\nA low-level rendering surface for custom graphics and complex visual effects. FrameBuffer provides a 2D array of cells with methods optimized for performance and memory.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport { FrameBufferRenderable, RGBA, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst canvas = new FrameBufferRenderable(renderer, {\n  id: \"canvas\",\n  width: 50,\n  height: 20,\n})\n\n// Draw on the frame buffer\ncanvas.frameBuffer.fillRect(5, 2, 20, 10, RGBA.fromHex(\"#FF0000\"))\ncanvas.frameBuffer.drawText(\"Hello!\", 8, 6, RGBA.fromHex(\"#FFFFFF\"))\n\nrenderer.root.add(canvas)\n```\n\n### Construct API\n\n```typescript\nimport { FrameBuffer, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nrenderer.root.add(\n  FrameBuffer({\n    width: 50,\n    height: 20,\n  }),\n)\n```\n\n## Drawing methods\n\n### setCell\n\nSet a single cell's content and colors:\n\n```typescript\ncanvas.frameBuffer.setCell(\n  x, // X position\n  y, // Y position\n  char, // Character to display\n  fg, // Foreground color (RGBA)\n  bg, // Background color (RGBA)\n  attributes, // Text attributes (optional, default: 0)\n)\n\n// Example\ncanvas.frameBuffer.setCell(10, 5, \"@\", RGBA.fromHex(\"#FFFF00\"), RGBA.fromHex(\"#000000\"))\n```\n\n### setCellWithAlphaBlending\n\nSet a cell with alpha blending for transparency effects:\n\n```typescript\nconst semiTransparent = RGBA.fromValues(1.0, 0.0, 0.0, 0.5)\nconst transparent = RGBA.fromValues(0, 0, 0, 0)\ncanvas.frameBuffer.setCellWithAlphaBlending(10, 5, \" \", transparent, semiTransparent)\n```\n\n### drawText\n\nDraw a string of text at a position:\n\n```typescript\ncanvas.frameBuffer.drawText(\n  text, // String to draw\n  x, // Starting X position\n  y, // Y position\n  fg, // Text color (RGBA)\n  bg, // Background color (RGBA, optional)\n  attributes, // Text attributes (optional, default: 0)\n)\n\n// Example\ncanvas.frameBuffer.drawText(\"Score: 100\", 2, 1, RGBA.fromHex(\"#00FF00\"))\n```\n\n### fillRect\n\nFill a rectangular area with a color:\n\n```typescript\ncanvas.frameBuffer.fillRect(\n  x, // X position\n  y, // Y position\n  width, // Rectangle width\n  height, // Rectangle height\n  color, // Fill color (RGBA)\n)\n\n// Example: Draw a red rectangle\ncanvas.frameBuffer.fillRect(10, 5, 20, 8, RGBA.fromHex(\"#FF0000\"))\n```\n\n### drawFrameBuffer\n\nCopy another frame buffer onto this one:\n\n```typescript\ncanvas.frameBuffer.drawFrameBuffer(\n  destX, // Destination X\n  destY, // Destination Y\n  sourceBuffer, // Source FrameBuffer (OptimizedBuffer)\n  sourceX, // Source X offset (optional)\n  sourceY, // Source Y offset (optional)\n  sourceWidth, // Width to copy (optional)\n  sourceHeight, // Height to copy (optional)\n)\n```\n\n## Properties\n\n| Property                         | Type      | Default      | Description                           |\n| -------------------------------- | --------- | ------------ | ------------------------------------- |\n| `width`                          | `number`  | -            | Buffer width in characters (required) |\n| `height`                         | `number`  | -            | Buffer height in rows (required)      |\n| `respectAlpha`                   | `boolean` | `false`      | Enable alpha blending when drawing    |\n| `position`                       | `string`  | `\"relative\"` | Positioning mode                      |\n| `left`, `top`, `right`, `bottom` | `number`  | -            | Position offsets                      |\n\n## Example: Simple game canvas\n\n```typescript\nimport { FrameBufferRenderable, RGBA, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst gameCanvas = new FrameBufferRenderable(renderer, {\n  id: \"game\",\n  width: 40,\n  height: 20,\n  position: \"absolute\",\n  left: 5,\n  top: 2,\n})\n\n// Game state\nlet playerX = 20\nlet playerY = 10\n\nfunction render() {\n  const fb = gameCanvas.frameBuffer\n  const BG = RGBA.fromHex(\"#111111\")\n\n  // Clear the canvas\n  fb.fillRect(0, 0, 40, 20, BG)\n\n  // Draw border\n  for (let x = 0; x < 40; x++) {\n    fb.setCell(x, 0, \"-\", RGBA.fromHex(\"#444444\"), BG)\n    fb.setCell(x, 19, \"-\", RGBA.fromHex(\"#444444\"), BG)\n  }\n  for (let y = 0; y < 20; y++) {\n    fb.setCell(0, y, \"|\", RGBA.fromHex(\"#444444\"), BG)\n    fb.setCell(39, y, \"|\", RGBA.fromHex(\"#444444\"), BG)\n  }\n\n  // Draw player\n  fb.setCell(playerX, playerY, \"@\", RGBA.fromHex(\"#00FF00\"), BG)\n\n  // Draw score\n  fb.drawText(\"Score: 0\", 2, 0, RGBA.fromHex(\"#FFFF00\"))\n}\n\n// Handle input\nrenderer.keyInput.on(\"keypress\", (key) => {\n  switch (key.name) {\n    case \"up\":\n      playerY = Math.max(1, playerY - 1)\n      break\n    case \"down\":\n      playerY = Math.min(18, playerY + 1)\n      break\n    case \"left\":\n      playerX = Math.max(1, playerX - 1)\n      break\n    case \"right\":\n      playerX = Math.min(38, playerX + 1)\n      break\n  }\n  render()\n})\n\nrender()\nrenderer.root.add(gameCanvas)\n```\n\n## Example: Progress bar\n\n```typescript\nconst EMPTY_BG = RGBA.fromHex(\"#222222\")\n\nfunction drawProgressBar(fb, x, y, width, progress, color) {\n  const filled = Math.floor(width * progress)\n\n  // Draw filled portion\n  for (let i = 0; i < filled; i++) {\n    fb.setCell(x + i, y, \"█\", color, EMPTY_BG)\n  }\n\n  // Draw empty portion\n  for (let i = filled; i < width; i++) {\n    fb.setCell(x + i, y, \"░\", RGBA.fromHex(\"#333333\"), EMPTY_BG)\n  }\n}\n\n// Usage\ndrawProgressBar(canvas.frameBuffer, 5, 10, 30, 0.75, RGBA.fromHex(\"#00FF00\"))\n```\n\n## Performance tips\n\n1. **Batch updates**: Make multiple changes to the frame buffer before the next render cycle\n2. **Minimize fillRect calls**: Use individual setCell calls for complex shapes\n3. **Reuse RGBA objects**: Create color constants instead of calling `fromHex` repeatedly\n\n```typescript\n// Good: Create once, reuse\nconst RED = RGBA.fromHex(\"#FF0000\")\nconst GREEN = RGBA.fromHex(\"#00FF00\")\nconst BG = RGBA.fromHex(\"#000000\")\n\nfor (let i = 0; i < 100; i++) {\n  fb.setCell(i, 5, \"*\", RED, BG)\n}\n\n// Avoid: Creating new RGBA objects in loops\nfor (let i = 0; i < 100; i++) {\n  fb.setCell(i, 5, \"*\", RGBA.fromHex(\"#FF0000\"), RGBA.fromHex(\"#000000\")) // Creates 200 objects\n}\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/components/input.mdx",
    "content": "---\ntitle: Input\ndescription: Text input field with cursor and focus states\norder: 3\n---\n\n# Input\n\nText input field with cursor, placeholder text, and focus states. Focus the input to receive keyboard input.\n\n## Basic usage\n\n### Renderable api\n\n```typescript\nimport { InputRenderable, InputRenderableEvents, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst input = new InputRenderable(renderer, {\n  id: \"name-input\",\n  width: 25,\n  placeholder: \"Enter your name...\",\n})\n\ninput.on(InputRenderableEvents.CHANGE, (value) => {\n  console.log(\"Input value:\", value)\n})\n\ninput.focus()\nrenderer.root.add(input)\n```\n\n### Construct api\n\n```typescript\nimport { Input, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst input = Input({\n  placeholder: \"Enter your name...\",\n  width: 25,\n})\n\ninput.focus()\nrenderer.root.add(input)\n```\n\n## Focus states\n\nThe input changes appearance when focused:\n\n```typescript\nconst input = new InputRenderable(renderer, {\n  id: \"styled-input\",\n  width: 30,\n  placeholder: \"Type here...\",\n  backgroundColor: \"#1a1a1a\",\n  focusedBackgroundColor: \"#2a2a2a\",\n  textColor: \"#FFFFFF\",\n  cursorColor: \"#00FF00\",\n})\n```\n\n## Events\n\n### Input event\n\nFires on every keystroke as the value changes:\n\n```typescript\nimport { InputRenderableEvents } from \"@opentui/core\"\n\ninput.on(InputRenderableEvents.INPUT, (value: string) => {\n  console.log(\"Current value:\", value)\n})\n```\n\n### Change event\n\nFires when the input loses focus (blur) or when you press Enter, but only if the value changed since focus:\n\n```typescript\ninput.on(InputRenderableEvents.CHANGE, (value: string) => {\n  console.log(\"Value committed:\", value)\n})\n```\n\n### Enter event\n\nFires when the user presses Enter/Return:\n\n```typescript\ninput.on(InputRenderableEvents.ENTER, (value: string) => {\n  console.log(\"Submitted value:\", value)\n})\n```\n\n### Getting the current value\n\n```typescript\nconst currentValue = input.value\n```\n\n### Setting the value\n\n```typescript\ninput.value = \"New value\"\n```\n\n## Properties\n\n| Property                 | Type                                   | Default      | Description                  |\n| ------------------------ | -------------------------------------- | ------------ | ---------------------------- |\n| `width`                  | `number`                               | -            | Input field width            |\n| `value`                  | `string`                               | `\"\"`         | Initial text value           |\n| `placeholder`            | `string`                               | `\"\"`         | Placeholder text when empty  |\n| `maxLength`              | `number`                               | `1000`       | Maximum number of characters |\n| `backgroundColor`        | `string \\| RGBA`                       | -            | Background when unfocused    |\n| `focusedBackgroundColor` | `string \\| RGBA`                       | -            | Background when focused      |\n| `textColor`              | `string \\| RGBA`                       | -            | Text color                   |\n| `cursorColor`            | `string \\| RGBA`                       | -            | Cursor color                 |\n| `position`               | `\"static\" \\| \"relative\" \\| \"absolute\"` | `\"relative\"` | Positioning mode             |\n\n## Example: Login form\n\n```typescript\nimport { Box, Text, Input, delegate, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nfunction LabeledInput(props: { id: string; label: string; placeholder: string }) {\n  return delegate(\n    { focus: `${props.id}-input` },\n    Box(\n      { flexDirection: \"row\", marginBottom: 1 },\n      Text({ content: props.label.padEnd(12), fg: \"#888888\" }),\n      Input({\n        id: `${props.id}-input`,\n        placeholder: props.placeholder,\n        width: 20,\n        backgroundColor: \"#222\",\n        focusedBackgroundColor: \"#333\",\n        textColor: \"#FFF\",\n        cursorColor: \"#0F0\",\n      }),\n    ),\n  )\n}\n\nconst usernameInput = LabeledInput({\n  id: \"username\",\n  label: \"Username:\",\n  placeholder: \"Enter username\",\n})\n\nconst passwordInput = LabeledInput({\n  id: \"password\",\n  label: \"Password:\",\n  placeholder: \"Enter password\",\n})\n\nconst form = Box(\n  {\n    width: 40,\n    borderStyle: \"rounded\",\n    title: \"Login\",\n    padding: 1,\n  },\n  usernameInput,\n  passwordInput,\n)\n\n// Focus the username input\nusernameInput.focus()\n\nrenderer.root.add(form)\n```\n\n## Tab navigation\n\nAdd tab navigation between inputs:\n\n```typescript\nconst inputs = [usernameInput, passwordInput]\nlet focusIndex = 0\n\nrenderer.keyInput.on(\"keypress\", (key) => {\n  if (key.name === \"tab\") {\n    focusIndex = (focusIndex + 1) % inputs.length\n    inputs[focusIndex].focus()\n  }\n})\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/components/line-number.mdx",
    "content": "---\ntitle: Line numbers\ndescription: Line number gutter for code and text renderables\norder: 12\n---\n\n# Line numbers\n\nAdd a line number gutter to renderables that provide line info, such as `CodeRenderable` and text editor components.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport {\n  CodeRenderable,\n  LineNumberRenderable,\n  ScrollBoxRenderable,\n  SyntaxStyle,\n  RGBA,\n  createCliRenderer,\n} from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  default: { fg: RGBA.fromHex(\"#E6EDF3\") },\n})\n\nconst code = new CodeRenderable(renderer, {\n  id: \"code\",\n  content: \"const x = 1\\nconst y = 2\\n\",\n  filetype: \"typescript\",\n  syntaxStyle,\n  width: \"100%\",\n})\n\nconst lineNumbers = new LineNumberRenderable(renderer, {\n  id: \"code-lines\",\n  target: code,\n  minWidth: 3,\n  paddingRight: 1,\n  fg: \"#6b7280\",\n  bg: \"#161b22\",\n})\n\nconst scrollbox = new ScrollBoxRenderable(renderer, {\n  id: \"scrollbox\",\n  width: 70,\n  height: 18,\n})\n\nscrollbox.add(lineNumbers)\nrenderer.root.add(scrollbox)\n```\n\n## Line signs and colors\n\n```typescript\nlineNumbers.setLineColor(3, \"#2b6cb0\")\nlineNumbers.setLineSign(3, { before: \">\", beforeColor: \"#2b6cb0\" })\n```\n\n## Construct API\n\n> Not available yet. Use `LineNumberRenderable` for now.\n\n## Properties\n\n| Property           | Type                                             | Default     | Description                    |\n| ------------------ | ------------------------------------------------ | ----------- | ------------------------------ |\n| `target`           | `Renderable & LineInfoProvider`                  | -           | Target renderable to number    |\n| `fg`               | `string` or `RGBA`                               | `#888888`   | Gutter text color              |\n| `bg`               | `string` or `RGBA`                               | transparent | Gutter background color        |\n| `minWidth`         | `number`                                         | `3`         | Minimum gutter width           |\n| `paddingRight`     | `number`                                         | `1`         | Right padding for gutter       |\n| `lineColors`       | `Map<number, string or RGBA or LineColorConfig>` | -           | Per-line background colors     |\n| `lineSigns`        | `Map<number, LineSign>`                          | -           | Per-line signs (before/after)  |\n| `lineNumberOffset` | `number`                                         | `0`         | Offset for line numbering      |\n| `hideLineNumbers`  | `Set<number>`                                    | -           | Lines to hide numbers for      |\n| `lineNumbers`      | `Map<number, number>`                            | -           | Override line numbers per line |\n| `showLineNumbers`  | `boolean`                                        | `true`      | Toggle gutter visibility       |\n\n## Methods\n\n| Method                 | Description                           |\n| ---------------------- | ------------------------------------- |\n| `setLineColor()`       | Set a background color for a line     |\n| `clearLineColor()`     | Clear a line background color         |\n| `setLineSign()`        | Set a sign before/after a line number |\n| `clearLineSign()`      | Clear a line sign                     |\n| `setLineNumbers()`     | Override multiple line numbers        |\n| `setHideLineNumbers()` | Hide line numbers for specific lines  |\n"
  },
  {
    "path": "packages/web/src/content/docs/components/markdown.mdx",
    "content": "---\ntitle: Markdown\ndescription: Render markdown with syntax-aware styling\norder: 11\n---\n\n# Markdown\n\nRender markdown content with syntax-aware styling and optional Tree-sitter highlighting for code blocks.\n\n## Basic usage\n\n### Renderable API\n\n````typescript\nimport { MarkdownRenderable, SyntaxStyle, RGBA, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  \"markup.heading.1\": { fg: RGBA.fromHex(\"#58A6FF\"), bold: true },\n  \"markup.list\": { fg: RGBA.fromHex(\"#FF7B72\") },\n  \"markup.raw\": { fg: RGBA.fromHex(\"#A5D6FF\") },\n  default: { fg: RGBA.fromHex(\"#E6EDF3\") },\n})\n\nconst markdown = new MarkdownRenderable(renderer, {\n  id: \"readme\",\n  width: 60,\n  content: \"# Hello\\n\\n- One\\n- Two\\n\\n```ts\\nconst x = 1\\n```\",\n  syntaxStyle,\n})\n\nrenderer.root.add(markdown)\n````\n\n## Fenced language normalization\n\nCode fence info strings are normalized before Tree-sitter highlighting.\n\n- `tsx` -> `typescriptreact`\n- `.jsx` -> `javascriptreact`\n- `TSX title=Button.tsx` -> `typescriptreact`\n- `Dockerfile` -> `dockerfile`\n\nNormalization uses `infoStringToFiletype()`. You can extend or override mappings at runtime:\n\n```typescript\nimport { extensionToFiletype, basenameToFiletype } from \"@opentui/core\"\n\nextensionToFiletype.set(\"templ\", \"html\")\nbasenameToFiletype.set(\"mytoolrc\", \"yaml\")\n```\n\n## Concealment\n\nHide markdown markers (backticks, emphasis markers, etc.) when `conceal` is true:\n\n```typescript\nconst markdown = new MarkdownRenderable(renderer, {\n  content: \"**bold** and `code`\",\n  syntaxStyle,\n  conceal: true,\n})\n```\n\nUse `concealCode` to control concealment inside fenced code blocks independently (`false` by default).\n\n## Streaming updates\n\nEnable streaming mode for incremental updates. Keep it `true` while appending chunks, then set `markdown.streaming = false` when complete to finalize trailing block parsing. Tables include trailing partial rows, with missing cells rendered empty.\n\n```typescript\nconst markdown = new MarkdownRenderable(renderer, {\n  content: \"\",\n  syntaxStyle,\n  streaming: true,\n})\n\nmarkdown.content += \"# Live log\\n\"\nmarkdown.content += \"- line 1\\n\"\nmarkdown.streaming = false\n```\n\n## Markdown tables\n\nMarkdown tables support `tableOptions` for sizing, wrapping, borders, and selection.\n\n```typescript\nconst markdown = new MarkdownRenderable(renderer, {\n  content: \"| Service | Status |\\n| --- | --- |\\n| api | ok |\",\n  syntaxStyle,\n  tableOptions: {\n    widthMode: \"full\",\n    columnFitter: \"balanced\",\n    wrapMode: \"word\",\n    cellPadding: 1,\n    borders: true,\n    outerBorder: true,\n    borderStyle: \"rounded\",\n    borderColor: \"#6b7280\",\n    selectable: true,\n  },\n})\n```\n\n### `tableOptions`\n\n| Option         | Type                           | Default                 | Description                                      |\n| -------------- | ------------------------------ | ----------------------- | ------------------------------------------------ |\n| `widthMode`    | `\"content\" \\| \"full\"`          | `\"full\"`                | `\"full\"` expands columns to fill available width |\n| `columnFitter` | `\"proportional\" \\| \"balanced\"` | `\"proportional\"`        | How columns shrink when space is constrained     |\n| `wrapMode`     | `\"none\" \\| \"char\" \\| \"word\"`   | `\"word\"`                | Wrapping mode inside each table cell             |\n| `cellPadding`  | `number`                       | `0`                     | Padding on all sides of each cell                |\n| `borders`      | `boolean`                      | `true`                  | Enable inner and outer borders                   |\n| `outerBorder`  | `boolean`                      | `borders`               | Override outer border visibility                 |\n| `borderStyle`  | `BorderStyle`                  | `\"single\"`              | Table border character set                       |\n| `borderColor`  | `ColorInput`                   | conceal fg or `#888888` | Border color for markdown tables                 |\n| `selectable`   | `boolean`                      | `true`                  | Enable table cell text selection                 |\n\n## Custom node rendering\n\nOverride rendering for a token and fall back to default rendering:\n\n```typescript\nconst markdown = new MarkdownRenderable(renderer, {\n  content: \"# Title\\n\\nHello\",\n  syntaxStyle,\n  renderNode: (token, context) => {\n    if (token.type === \"heading\") {\n      return context.defaultRender()\n    }\n    return undefined\n  },\n})\n```\n\n## Construct API\n\n> Not available yet. Use `MarkdownRenderable` for now.\n\n## Properties\n\n| Property           | Type                                                                            | Default  | Description                               |\n| ------------------ | ------------------------------------------------------------------------------- | -------- | ----------------------------------------- |\n| `content`          | `string`                                                                        | `\"\"`     | Markdown source                           |\n| `syntaxStyle`      | `SyntaxStyle`                                                                   | required | Style definitions for tokens              |\n| `conceal`          | `boolean`                                                                       | `true`   | Hide markdown markers in markdown text    |\n| `concealCode`      | `boolean`                                                                       | `false`  | Hide markers inside fenced code blocks    |\n| `streaming`        | `boolean`                                                                       | `false`  | Incremental mode; set `false` to finalize |\n| `tableOptions`     | `MarkdownTableOptions`                                                          | -        | Options for markdown table rendering      |\n| `treeSitterClient` | `TreeSitterClient`                                                              | -        | Custom Tree-sitter client for code blocks |\n| `renderNode`       | `(token: Token, context: RenderNodeContext) => Renderable \\| null \\| undefined` | -        | Custom render hook per markdown block     |\n"
  },
  {
    "path": "packages/web/src/content/docs/components/scrollbar.mdx",
    "content": "---\ntitle: ScrollBar\ndescription: Standalone scrollbar with arrows and keyboard control\norder: 8\n---\n\n# ScrollBar\n\nA standalone scrollbar component with optional arrows, keyboard navigation, and a draggable thumb.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport { ScrollBarRenderable, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst scrollbar = new ScrollBarRenderable(renderer, {\n  id: \"scrollbar\",\n  orientation: \"vertical\",\n  height: 10,\n  showArrows: true,\n  trackOptions: {\n    backgroundColor: \"#222222\",\n    foregroundColor: \"#888888\",\n  },\n  onChange: (position) => {\n    console.log(\"Scroll position:\", position)\n  },\n})\n\n// Connect the scrollbar to your content\nscrollbar.scrollSize = 200\nscrollbar.viewportSize = 20\nscrollbar.scrollPosition = 0\n\nrenderer.root.add(scrollbar)\nscrollbar.focus()\n```\n\n## Keyboard controls\n\nWhen focused, the scrollbar responds to:\n\n- `Up`/`Down` or `k`/`j` for vertical bars\n- `Left`/`Right` or `h`/`l` for horizontal bars\n- `PageUp`/`PageDown` for larger jumps\n- `Home`/`End` to jump to start/end\n\n## Construct API\n\n> Not available yet. Use `ScrollBarRenderable` for now.\n\n## Properties\n\n| Property         | Type                           | Default | Description                            |\n| ---------------- | ------------------------------ | ------- | -------------------------------------- |\n| `orientation`    | `\"vertical\"` or `\"horizontal\"` | -       | Scrollbar direction                    |\n| `showArrows`     | `boolean`                      | `false` | Show arrow buttons                     |\n| `arrowOptions`   | `ArrowOptions`                 | -       | Styling for arrow buttons              |\n| `trackOptions`   | `Partial<SliderOptions>`       | -       | Styling for the track and thumb        |\n| `scrollSize`     | `number`                       | `0`     | Total scrollable size                  |\n| `viewportSize`   | `number`                       | `0`     | Visible size of the viewport           |\n| `scrollPosition` | `number`                       | `0`     | Current scroll position                |\n| `scrollStep`     | `number`                       | -       | Step size when `scrollBy(..., \"step\")` |\n| `onChange`       | `(position: number) => void`   | -       | Fired when scroll position changes     |\n"
  },
  {
    "path": "packages/web/src/content/docs/components/scrollbox.mdx",
    "content": "---\ntitle: ScrollBox\ndescription: A scrollable container component with customizable scrollbars\norder: 5\n---\n\n# ScrollBox\n\nA scrollable container that supports horizontal and vertical scrolling, sticky scroll behavior, viewport culling, and customizable scrollbars.\n\n## Basic usage\n\n### Renderable api\n\n```typescript\nimport { ScrollBoxRenderable, TextRenderable, BoxRenderable, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst scrollbox = new ScrollBoxRenderable(renderer, {\n  id: \"scrollbox\",\n  width: 40,\n  height: 20,\n})\n\n// Add content to the scrollbox\nfor (let i = 0; i < 100; i++) {\n  scrollbox.add(\n    new BoxRenderable(renderer, {\n      id: `item-${i}`,\n      width: \"100%\",\n      height: 2,\n      backgroundColor: i % 2 === 0 ? \"#292e42\" : \"#2f3449\",\n    }),\n  )\n}\n\nrenderer.root.add(scrollbox)\n```\n\n### Construct API\n\n```typescript\nimport { ScrollBox, Box, Text, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nrenderer.root.add(\n  ScrollBox(\n    {\n      width: 40,\n      height: 20,\n    },\n    ...Array.from({ length: 100 }, (_, i) =>\n      Box(\n        { width: \"100%\", padding: 1, backgroundColor: i % 2 === 0 ? \"#292e42\" : \"#2f3449\" },\n        Text({ content: `Item ${i}` }),\n      ),\n    ),\n  ),\n)\n```\n\n## Sticky scroll\n\nEnable sticky scroll to keep content pinned to an edge as new content arrives. Useful for log viewers or chat interfaces.\n\n```typescript\nconst scrollbox = new ScrollBoxRenderable(renderer, {\n  id: \"logs\",\n  width: 60,\n  height: 20,\n  stickyScroll: true,\n  stickyStart: \"bottom\", // New content will keep the view scrolled to bottom\n})\n```\n\n### Sticky positions\n\n- `\"bottom\"` - Stay scrolled to bottom (default for chat/logs)\n- `\"top\"` - Stay scrolled to top\n- `\"left\"` - Stay scrolled to left\n- `\"right\"` - Stay scrolled to right\n\nWhen you scroll away from the sticky position, sticky behavior pauses until you scroll back to the sticky edge.\n\n## Bidirectional scrolling\n\nEnable scrolling in both directions:\n\n```typescript\nconst scrollbox = new ScrollBoxRenderable(renderer, {\n  id: \"canvas\",\n  width: 60,\n  height: 30,\n  scrollX: true,\n  scrollY: true,\n})\n```\n\nBy default, `scrollY` is `true` and `scrollX` is `false`.\n\n## Viewport culling\n\nEnable viewport culling for better performance with large content. When enabled, ScrollBox renders only visible children:\n\n```typescript\nconst scrollbox = new ScrollBoxRenderable(renderer, {\n  id: \"large-list\",\n  width: 40,\n  height: 20,\n  viewportCulling: true, // Only render visible items\n})\n```\n\n## Customizing scrollbars\n\nStyle the scrollbars using nested options:\n\n```typescript\nconst scrollbox = new ScrollBoxRenderable(renderer, {\n  id: \"styled-scroll\",\n  width: 40,\n  height: 20,\n  scrollbarOptions: {\n    showArrows: true,\n    trackOptions: {\n      foregroundColor: \"#7aa2f7\",\n      backgroundColor: \"#414868\",\n    },\n  },\n  // Or customize vertical and horizontal separately\n  verticalScrollbarOptions: {\n    trackOptions: { backgroundColor: \"#333\" },\n  },\n  horizontalScrollbarOptions: {\n    trackOptions: { backgroundColor: \"#333\" },\n  },\n})\n```\n\n## Customizing sub-components\n\nScrollBox contains several internal components that you can style individually:\n\n```typescript\nconst scrollbox = new ScrollBoxRenderable(renderer, {\n  id: \"custom-scroll\",\n  width: 40,\n  height: 20,\n  rootOptions: {\n    backgroundColor: \"#24283b\",\n  },\n  wrapperOptions: {\n    backgroundColor: \"#1f2335\",\n  },\n  viewportOptions: {\n    backgroundColor: \"#1a1b26\",\n  },\n  contentOptions: {\n    backgroundColor: \"#16161e\",\n  },\n})\n```\n\n## Scroll methods\n\n### scrollBy\n\nScroll by a relative amount:\n\n```typescript\n// Scroll down 5 lines\nscrollbox.scrollBy(5)\n\n// Scroll with both x and y\nscrollbox.scrollBy({ x: 10, y: 5 })\n\n// Scroll by viewport (page)\nscrollbox.scrollBy(1, \"viewport\")\n```\n\n### scrollTo\n\nScroll to an absolute position:\n\n```typescript\n// Scroll to top\nscrollbox.scrollTo(0)\n\n// Scroll to specific position\nscrollbox.scrollTo({ x: 0, y: 100 })\n```\n\n## Keyboard navigation\n\nWhen focused, ScrollBox responds to keyboard input:\n\n- **Arrow keys** - Scroll by line\n- **Page Up/Down** - Scroll by page\n- **Home/End** - Scroll to start/end\n\n## Properties\n\n| Property                     | Type                                     | Default | Description                            |\n| ---------------------------- | ---------------------------------------- | ------- | -------------------------------------- |\n| `scrollX`                    | `boolean`                                | `false` | Enable horizontal scrolling            |\n| `scrollY`                    | `boolean`                                | `true`  | Enable vertical scrolling              |\n| `stickyScroll`               | `boolean`                                | `false` | Keep scroll position pinned to an edge |\n| `stickyStart`                | `\"top\" \\| \"bottom\" \\| \"left\" \\| \"right\"` | -       | Which edge to stick to                 |\n| `viewportCulling`            | `boolean`                                | `true`  | Only render visible children           |\n| `scrollAcceleration`         | `ScrollAcceleration`                     | -       | Custom scroll acceleration algorithm   |\n| `rootOptions`                | `BoxOptions`                             | -       | Style options for root container       |\n| `wrapperOptions`             | `BoxOptions`                             | -       | Style options for wrapper              |\n| `viewportOptions`            | `BoxOptions`                             | -       | Style options for viewport             |\n| `contentOptions`             | `BoxOptions`                             | -       | Style options for content container    |\n| `scrollbarOptions`           | `ScrollBarOptions`                       | -       | Options for both scrollbars            |\n| `verticalScrollbarOptions`   | `ScrollBarOptions`                       | -       | Options for vertical scrollbar only    |\n| `horizontalScrollbarOptions` | `ScrollBarOptions`                       | -       | Options for horizontal scrollbar only  |\n\n## Additional properties\n\n| Property       | Type     | Description                                  |\n| -------------- | -------- | -------------------------------------------- |\n| `scrollTop`    | `number` | Current vertical scroll position (get/set)   |\n| `scrollLeft`   | `number` | Current horizontal scroll position (get/set) |\n| `scrollWidth`  | `number` | Total scrollable width (read-only)           |\n| `scrollHeight` | `number` | Total scrollable height (read-only)          |\n\n## Internal components\n\nScrollBox exposes its internal components for advanced use:\n\n```typescript\nscrollbox.wrapper // BoxRenderable - outer wrapper\nscrollbox.viewport // BoxRenderable - visible area\nscrollbox.content // ContentRenderable - holds children\nscrollbox.horizontalScrollBar // ScrollBarRenderable\nscrollbox.verticalScrollBar // ScrollBarRenderable\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/components/select.mdx",
    "content": "---\ntitle: Select\ndescription: List selection component for choosing from options\norder: 4\n---\n\n# Select\n\nA vertical list for choosing from multiple options. Focus the select to enable keyboard input.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport { SelectRenderable, SelectRenderableEvents, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst menu = new SelectRenderable(renderer, {\n  id: \"menu\",\n  width: 30,\n  height: 8,\n  options: [\n    { name: \"New File\", description: \"Create a new file\" },\n    { name: \"Open File\", description: \"Open an existing file\" },\n    { name: \"Save\", description: \"Save current file\" },\n    { name: \"Exit\", description: \"Exit the application\" },\n  ],\n})\n\nmenu.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {\n  console.log(\"Selected:\", option.name)\n})\n\nmenu.focus()\nrenderer.root.add(menu)\n```\n\n### Construct API\n\n```typescript\nimport { Select, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst menu = Select({\n  width: 30,\n  height: 8,\n  options: [\n    { name: \"Option 1\", description: \"First option\" },\n    { name: \"Option 2\", description: \"Second option\" },\n    { name: \"Option 3\", description: \"Third option\" },\n  ],\n})\n\nmenu.focus()\nrenderer.root.add(menu)\n```\n\n## Keyboard navigation\n\nWhen focused, the select responds to these keys:\n\n| Key                       | Action                |\n| ------------------------- | --------------------- |\n| `Up` / `k`                | Move selection up     |\n| `Down` / `j`              | Move selection down   |\n| `Shift+Up` / `Shift+Down` | Fast scroll (5 items) |\n| `Enter`                   | Select current item   |\n\n## Events\n\n### Item selected\n\nFires when the user presses Enter on an option:\n\n```typescript\nimport { SelectRenderableEvents } from \"@opentui/core\"\n\nmenu.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {\n  console.log(`Selected index ${index}: ${option.name}`)\n})\n```\n\n### Selection changed\n\nFires when the highlighted item changes:\n\n```typescript\nmenu.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {\n  console.log(`Highlighted: ${option.name}`)\n  // Update a preview pane, for example\n})\n```\n\n## Option structure\n\n```typescript\ninterface SelectOption {\n  name: string // Display text\n  description: string // Displays below the name when showDescription is true\n  value?: any // Optional value\n}\n```\n\n## Styling\n\n```typescript\nconst styledMenu = new SelectRenderable(renderer, {\n  id: \"styled-menu\",\n  width: 40,\n  height: 10,\n  options: [...],\n  backgroundColor: \"#1a1a1a\",\n  selectedBackgroundColor: \"#333366\",\n  selectedTextColor: \"#FFFFFF\",\n  textColor: \"#AAAAAA\",\n  descriptionColor: \"#666666\",\n})\n```\n\n## Properties\n\n| Property                   | Type             | Default       | Description                       |\n| -------------------------- | ---------------- | ------------- | --------------------------------- |\n| `width`                    | `number`         | -             | Component width                   |\n| `height`                   | `number`         | -             | Component height                  |\n| `options`                  | `SelectOption[]` | `[]`          | Available options                 |\n| `selectedIndex`            | `number`         | `0`           | Initially selected index          |\n| `backgroundColor`          | `string \\| RGBA` | `transparent` | Background color                  |\n| `textColor`                | `string \\| RGBA` | `#FFFFFF`     | Normal text color                 |\n| `focusedBackgroundColor`   | `string \\| RGBA` | `#1a1a1a`     | Background when focused           |\n| `focusedTextColor`         | `string \\| RGBA` | `#FFFFFF`     | Text color when focused           |\n| `selectedBackgroundColor`  | `string \\| RGBA` | `#334455`     | Selected item background          |\n| `selectedTextColor`        | `string \\| RGBA` | `#FFFF00`     | Selected item text color          |\n| `descriptionColor`         | `string \\| RGBA` | `#888888`     | Description text color            |\n| `selectedDescriptionColor` | `string \\| RGBA` | `#CCCCCC`     | Selected item description color   |\n| `showDescription`          | `boolean`        | `true`        | Show option descriptions          |\n| `showScrollIndicator`      | `boolean`        | `false`       | Show scroll position indicator    |\n| `wrapSelection`            | `boolean`        | `false`       | Wrap selection at list boundaries |\n| `itemSpacing`              | `number`         | `0`           | Spacing between items             |\n| `fastScrollStep`           | `number`         | `5`           | Items to skip with Shift+Up/Down  |\n\n## Example: file menu\n\n```typescript\nimport { Box, Select, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst fileMenu = Select({\n  width: 25,\n  height: 12,\n  options: [\n    { name: \"New\", description: \"Create new file (Ctrl+N)\" },\n    { name: \"Open...\", description: \"Open file (Ctrl+O)\" },\n    { name: \"Save\", description: \"Save file (Ctrl+S)\" },\n    { name: \"Save As...\", description: \"Save with new name\" },\n    { name: \"---\", description: \"\" }, // Separator (visual only)\n    { name: \"Exit\", description: \"Quit application (Ctrl+Q)\" },\n  ],\n})\n\nconst menuPanel = Box(\n  {\n    borderStyle: \"single\",\n    borderColor: \"#666\",\n  },\n  fileMenu,\n)\n\nfileMenu.focus()\nrenderer.root.add(menuPanel)\n```\n\n## Programmatic control\n\n```typescript\n// Get current selection index\nconst currentIndex = menu.getSelectedIndex()\n\n// Get currently selected option\nconst option = menu.getSelectedOption()\n\n// Set selection programmatically\nmenu.setSelectedIndex(2)\n\n// Navigate programmatically\nmenu.moveUp() // Move up one item\nmenu.moveDown() // Move down one item\nmenu.moveUp(3) // Move up multiple items\nmenu.selectCurrent() // Trigger selection of current item\n\n// Update options dynamically\nmenu.options = [\n  { name: \"New Option 1\", description: \"First\" },\n  { name: \"New Option 2\", description: \"Second\" },\n]\n\n// Toggle display options\nmenu.showDescription = false\nmenu.showScrollIndicator = true\nmenu.wrapSelection = true\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/components/slider.mdx",
    "content": "---\ntitle: Slider\ndescription: A draggable slider for continuous values\norder: 9\n---\n\n# Slider\n\nA draggable slider for continuous values. Supports vertical and horizontal orientations.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport { SliderRenderable, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst slider = new SliderRenderable(renderer, {\n  id: \"volume\",\n  orientation: \"horizontal\",\n  width: 30,\n  height: 1,\n  min: 0,\n  max: 100,\n  value: 25,\n  onChange: (value) => {\n    console.log(\"Value:\", value)\n  },\n})\n\nrenderer.root.add(slider)\n```\n\n## Vertical slider\n\n```typescript\nconst slider = new SliderRenderable(renderer, {\n  orientation: \"vertical\",\n  width: 2,\n  height: 10,\n  min: 0,\n  max: 1,\n  value: 0.5,\n})\n```\n\n## Construct API\n\n> Not available yet. Use `SliderRenderable` for now.\n\n## Properties\n\n| Property          | Type                           | Default      | Description                    |\n| ----------------- | ------------------------------ | ------------ | ------------------------------ |\n| `orientation`     | `\"vertical\"` or `\"horizontal\"` | -            | Slider direction               |\n| `value`           | `number`                       | `min`        | Current value                  |\n| `min`             | `number`                       | `0`          | Minimum value                  |\n| `max`             | `number`                       | `100`        | Maximum value                  |\n| `viewPortSize`    | `number`                       | range \\* 0.1 | Thumb size relative to content |\n| `backgroundColor` | `string` or `RGBA`             | -            | Track color                    |\n| `foregroundColor` | `string` or `RGBA`             | -            | Thumb color                    |\n| `onChange`        | `(value: number) => void`      | -            | Fired when value changes       |\n"
  },
  {
    "path": "packages/web/src/content/docs/components/tab-select.mdx",
    "content": "---\ntitle: TabSelect\ndescription: Horizontal tab-based selection component\norder: 5\n---\n\n# TabSelect\n\nHorizontal tab-based selection component with descriptions and scroll support. The component must be focused to receive keyboard input.\n\n## Basic Usage\n\n### Renderable API\n\n```typescript\nimport { TabSelectRenderable, TabSelectRenderableEvents, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst tabs = new TabSelectRenderable(renderer, {\n  id: \"tabs\",\n  width: 60,\n  options: [\n    { name: \"Home\", description: \"Dashboard and overview\" },\n    { name: \"Files\", description: \"File management\" },\n    { name: \"Settings\", description: \"Application settings\" },\n  ],\n  tabWidth: 20,\n})\n\ntabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {\n  console.log(\"Tab selected:\", option.name)\n})\n\ntabs.focus()\nrenderer.root.add(tabs)\n```\n\n### Construct API\n\n```typescript\nimport { TabSelect, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst tabs = TabSelect({\n  width: 60,\n  tabWidth: 15,\n  options: [\n    { name: \"Tab 1\", description: \"First tab\" },\n    { name: \"Tab 2\", description: \"Second tab\" },\n    { name: \"Tab 3\", description: \"Third tab\" },\n  ],\n})\n\ntabs.focus()\nrenderer.root.add(tabs)\n```\n\n## Keyboard Navigation\n\nWhen focused, the tab select responds to these keys:\n\n| Key           | Action               |\n| ------------- | -------------------- |\n| `Left` / `[`  | Move to previous tab |\n| `Right` / `]` | Move to next tab     |\n| `Enter`       | Select current tab   |\n\n## Events\n\n### Item Selected\n\nEmitted when the user presses Enter on a tab:\n\n```typescript\nimport { TabSelectRenderableEvents, type TabSelectOption } from \"@opentui/core\"\n\ntabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index: number, option: TabSelectOption) => {\n  console.log(`Selected tab ${index}: ${option.name}`)\n  // Switch to the corresponding panel\n})\n```\n\n### Selection Changed\n\nEmitted when the highlighted tab changes:\n\n```typescript\nimport { TabSelectRenderableEvents, type TabSelectOption } from \"@opentui/core\"\n\ntabs.on(TabSelectRenderableEvents.SELECTION_CHANGED, (index: number, option: TabSelectOption) => {\n  console.log(`Hovering: ${option.name}`)\n})\n```\n\n## Properties\n\n| Property                   | Type                    | Default       | Description                    |\n| -------------------------- | ----------------------- | ------------- | ------------------------------ |\n| `width`                    | `number`                | -             | Total component width          |\n| `options`                  | `TabSelectOption[]`     | `[]`          | Available tabs                 |\n| `tabWidth`                 | `number`                | `20`          | Width of each tab              |\n| `backgroundColor`          | `string \\| RGBA`        | `transparent` | Background color               |\n| `textColor`                | `string \\| RGBA`        | `#FFFFFF`     | Normal tab text                |\n| `focusedBackgroundColor`   | `string \\| RGBA`        | `#1a1a1a`     | Background color when focused  |\n| `focusedTextColor`         | `string \\| RGBA`        | `#FFFFFF`     | Text color when focused        |\n| `selectedBackgroundColor`  | `string \\| RGBA`        | `#334455`     | Selected tab background        |\n| `selectedTextColor`        | `string \\| RGBA`        | `#FFFF00`     | Selected tab text              |\n| `selectedDescriptionColor` | `string \\| RGBA`        | `#CCCCCC`     | Description text color         |\n| `showScrollArrows`         | `boolean`               | `true`        | Show scroll indicators         |\n| `showDescription`          | `boolean`               | `true`        | Show tab descriptions          |\n| `showUnderline`            | `boolean`               | `true`        | Show underline on selected tab |\n| `wrapSelection`            | `boolean`               | `false`       | Wrap around when navigating    |\n| `keyBindings`              | `TabSelectKeyBinding[]` | -             | Custom key bindings            |\n| `keyAliasMap`              | `KeyAliasMap`           | -             | Key alias mappings             |\n\n## Example: Tabbed Interface\n\n```typescript\nimport { Box, Text, TabSelect, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\n// Create content panels\nconst panels = {\n  home: Box({ padding: 1 }, Text({ content: \"Home content here\" })),\n  files: Box({ padding: 1 }, Text({ content: \"File browser here\" })),\n  settings: Box({ padding: 1 }, Text({ content: \"Settings form here\" })),\n}\n\n// Create the tabbed container\nconst container = Box({\n  width: 60,\n  height: 20,\n  borderStyle: \"rounded\",\n})\n\nconst tabs = TabSelect({\n  width: 60,\n  tabWidth: 20,\n  options: [\n    { name: \"Home\", description: \"Dashboard\" },\n    { name: \"Files\", description: \"Browse files\" },\n    { name: \"Settings\", description: \"Preferences\" },\n  ],\n})\n\n// Content area\nlet currentPanel = panels.home\nconst contentArea = Box({\n  flexGrow: 1,\n  padding: 1,\n})\ncontentArea.add(currentPanel)\n\n// Handle tab changes\ntabs.on(\"itemSelected\", (index, option) => {\n  // Remove current panel\n  if (currentPanel) {\n    contentArea.remove(currentPanel.id)\n  }\n  // Add new panel based on selection\n  switch (option.name) {\n    case \"Home\":\n      currentPanel = panels.home\n      break\n    case \"Files\":\n      currentPanel = panels.files\n      break\n    case \"Settings\":\n      currentPanel = panels.settings\n      break\n  }\n  contentArea.add(currentPanel)\n})\n\ncontainer.add(tabs)\ncontainer.add(contentArea)\n\ntabs.focus()\nrenderer.root.add(container)\n```\n\n## Programmatic Control\n\n```typescript\n// Get current tab index\nconst currentIndex = tabs.getSelectedIndex()\n\n// Set tab programmatically\ntabs.setSelectedIndex(1)\n\n// Update tabs dynamically\ntabs.setOptions([\n  { name: \"New Tab 1\", description: \"Updated\" },\n  { name: \"New Tab 2\", description: \"Also updated\" },\n])\n```\n\n## Scroll Behavior\n\nWhen there are more tabs than fit in the width, the component automatically handles horizontal scrolling as you navigate with the keyboard.\n"
  },
  {
    "path": "packages/web/src/content/docs/components/text.mdx",
    "content": "---\ntitle: Text\ndescription: Display styled text content\norder: 1\n---\n\n# Text\n\nDisplay styled text content with support for colors, attributes, and text selection.\n\n## Basic Usage\n\n### Renderable API\n\n```typescript\nimport { TextRenderable, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst text = new TextRenderable(renderer, {\n  id: \"greeting\",\n  content: \"Hello, OpenTUI!\",\n  fg: \"#00FF00\",\n})\n\nrenderer.root.add(text)\n```\n\n### Construct API\n\n```typescript\nimport { Text, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nrenderer.root.add(\n  Text({\n    content: \"Hello, OpenTUI!\",\n    fg: \"#00FF00\",\n  }),\n)\n```\n\n## Text attributes\n\nCombine multiple text attributes using bitwise OR:\n\n```typescript\nimport { TextRenderable, TextAttributes } from \"@opentui/core\"\n\nconst styledText = new TextRenderable(renderer, {\n  id: \"styled\",\n  content: \"Important Message\",\n  fg: \"#FFFF00\",\n  attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n})\n```\n\n### Available attributes\n\n| Attribute                      | Description        |\n| ------------------------------ | ------------------ |\n| `TextAttributes.BOLD`          | Bold text          |\n| `TextAttributes.DIM`           | Dimmed text        |\n| `TextAttributes.ITALIC`        | Italic text        |\n| `TextAttributes.UNDERLINE`     | Underlined text    |\n| `TextAttributes.BLINK`         | Blinking text      |\n| `TextAttributes.INVERSE`       | Inverted colors    |\n| `TextAttributes.HIDDEN`        | Hidden text        |\n| `TextAttributes.STRIKETHROUGH` | Strikethrough text |\n\n## Template literals for rich text\n\nUse the `t` template literal for inline styling within a single text element:\n\n```typescript\nimport { TextRenderable, t, bold, underline, fg, bg, italic } from \"@opentui/core\"\n\nconst richText = new TextRenderable(renderer, {\n  id: \"rich\",\n  content: t`${bold(\"Important:\")} ${fg(\"#FF0000\")(underline(\"Warning!\"))} Normal text`,\n})\n```\n\n### Available style functions\n\n```typescript\nimport { t, bold, dim, italic, underline, blink, reverse, strikethrough, fg, bg } from \"@opentui/core\"\n\n// Basic attributes\nt`${bold(\"bold text\")}`\nt`${italic(\"italic text\")}`\nt`${underline(\"underlined\")}`\nt`${strikethrough(\"deleted\")}`\n\n// Colors\nt`${fg(\"#FF0000\")(\"red text\")}`\nt`${bg(\"#0000FF\")(\"blue background\")}`\n\n// Combining styles\nt`${bold(fg(\"#FFFF00\")(\"bold yellow\"))}`\n```\n\n## Positioning\n\n```typescript\nconst text = new TextRenderable(renderer, {\n  id: \"positioned\",\n  content: \"Absolute position\",\n  position: \"absolute\",\n  left: 10,\n  top: 5,\n})\n```\n\n## Text selection\n\nEnable text selection for copying:\n\n```typescript\nconst selectableText = new TextRenderable(renderer, {\n  id: \"selectable\",\n  content: \"Select me!\",\n  selectable: true, // Default is true\n})\n\nconst nonSelectable = new TextRenderable(renderer, {\n  id: \"label\",\n  content: \"Button Label\",\n  selectable: false, // Disable selection\n})\n```\n\n## Properties\n\n| Property                         | Type                              | Default      | Description                  |\n| -------------------------------- | --------------------------------- | ------------ | ---------------------------- |\n| `content`                        | `string \\| StyledText`            | `\"\"`         | The text content to display  |\n| `fg`                             | `string \\| RGBA`                  | -            | Foreground (text) color      |\n| `bg`                             | `string \\| RGBA`                  | -            | Background color             |\n| `attributes`                     | `TextAttributes`                  | `0`          | Text styling attributes      |\n| `selectable`                     | `boolean`                         | `true`       | Whether text can be selected |\n| `position`                       | `\"relative\" \\| \"absolute\"`        | `\"relative\"` | Positioning mode             |\n| `left`, `top`, `right`, `bottom` | `number \\| \"auto\" \\| \"{number}%\"` | -            | Position offsets             |\n\n## Example: status bar\n\n```typescript\nimport { Text, Box, t, bold, fg } from \"@opentui/core\"\n\nconst statusBar = Box(\n  {\n    position: \"absolute\",\n    bottom: 0,\n    width: \"100%\",\n    height: 1,\n    backgroundColor: \"#333333\",\n    flexDirection: \"row\",\n    justifyContent: \"space-between\",\n    paddingLeft: 1,\n    paddingRight: 1,\n  },\n  Text({\n    content: t`${bold(\"myfile.ts\")} - ${fg(\"#888888\")(\"TypeScript\")}`,\n  }),\n  Text({\n    content: t`Ln ${fg(\"#00FF00\")(\"42\")}, Col ${fg(\"#00FF00\")(\"15\")}`,\n  }),\n)\n\nrenderer.root.add(statusBar)\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/components/textarea.mdx",
    "content": "---\ntitle: Textarea\ndescription: Multi-line text input with selection and keybindings\norder: 4\n---\n\n# Textarea\n\nMulti-line text input with cursor, selection, and rich keybindings.\n\n## Basic usage\n\n### Renderable API\n\n```typescript\nimport { TextareaRenderable, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst textarea = new TextareaRenderable(renderer, {\n  id: \"notes\",\n  width: 50,\n  height: 6,\n  placeholder: \"Type notes here...\",\n  backgroundColor: \"#1a1a1a\",\n  focusedBackgroundColor: \"#222222\",\n  textColor: \"#FFFFFF\",\n  cursorColor: \"#00FF88\",\n})\n\nrenderer.root.add(textarea)\ntextarea.focus()\n```\n\n## Submit handling\n\nBind a submit action and listen for `onSubmit`:\n\n```typescript\nimport { TextareaRenderable } from \"@opentui/core\"\n\nconst textarea = new TextareaRenderable(renderer, {\n  width: 50,\n  height: 6,\n  onSubmit: () => {\n    console.log(\"Submitted:\", textarea.plainText)\n  },\n  keyBindings: [{ name: \"return\", ctrl: true, action: \"submit\" }],\n})\n```\n\n## Placeholder styling\n\n```typescript\nconst textarea = new TextareaRenderable(renderer, {\n  width: 40,\n  height: 4,\n  placeholder: \"Type here\",\n  placeholderColor: \"#666666\",\n})\n```\n\n## Construct API\n\n> Not available yet. Use `TextareaRenderable` for now.\n\n## Properties\n\n| Property                 | Type                                  | Default     | Description                       |\n| ------------------------ | ------------------------------------- | ----------- | --------------------------------- |\n| `width`                  | `number` or `string`                  | -           | Width in characters or percentage |\n| `height`                 | `number` or `string`                  | -           | Height in rows or percentage      |\n| `initialValue`           | `string`                              | `\"\"`        | Initial text content              |\n| `placeholder`            | `string`, `StyledText`, or `null`     | `null`      | Placeholder content               |\n| `placeholderColor`       | `string` or `RGBA`                    | `#666666`   | Placeholder color                 |\n| `backgroundColor`        | `string` or `RGBA`                    | transparent | Background when unfocused         |\n| `textColor`              | `string` or `RGBA`                    | `#FFFFFF`   | Text color when unfocused         |\n| `focusedBackgroundColor` | `string` or `RGBA`                    | transparent | Background when focused           |\n| `focusedTextColor`       | `string` or `RGBA`                    | `#FFFFFF`   | Text color when focused           |\n| `wrapMode`               | `\"none\"`, `\"char\"`, or `\"word\"`       | `\"word\"`    | Line wrapping mode                |\n| `selectionBg`            | `string` or `RGBA`                    | -           | Selection background              |\n| `selectionFg`            | `string` or `RGBA`                    | -           | Selection foreground              |\n| `cursorColor`            | `string` or `RGBA`                    | `#FFFFFF`   | Cursor color                      |\n| `cursorStyle`            | `CursorStyleOptions`                  | -           | Cursor style and blinking         |\n| `keyBindings`            | `KeyBinding[]`                        | -           | Custom keybindings                |\n| `keyAliasMap`            | `KeyAliasMap`                         | -           | Key alias mapping                 |\n| `onSubmit`               | `(event: SubmitEvent) => void`        | -           | Submit handler                    |\n| `onContentChange`        | `(event: ContentChangeEvent) => void` | -           | Fired on content changes          |\n| `onCursorChange`         | `(event: CursorChangeEvent) => void`  | -           | Fired on cursor movement          |\n\n## Useful properties\n\n| Property       | Type     | Description                 |\n| -------------- | -------- | --------------------------- |\n| `plainText`    | `string` | Current text content        |\n| `cursorOffset` | `number` | Cursor offset in the buffer |\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/colors.mdx",
    "content": "---\ntitle: Colors\ndescription: Working with RGBA colors in OpenTUI\norder: 2\n---\n\n# Colors\n\nOpenTUI uses the `RGBA` class for color representation. The class stores colors as normalized float values (0.0-1.0) internally, but provides methods for working with different color formats.\n\n## Creating colors\n\n### From integers (0-255)\n\n```typescript\nimport { RGBA } from \"@opentui/core\"\n\nconst red = RGBA.fromInts(255, 0, 0, 255)\nconst semiTransparentBlue = RGBA.fromInts(0, 0, 255, 128)\n```\n\n### From float values (0.0-1.0)\n\n```typescript\nconst green = RGBA.fromValues(0.0, 1.0, 0.0, 1.0)\nconst transparent = RGBA.fromValues(1.0, 1.0, 1.0, 0.5)\n```\n\n### From hex strings\n\n```typescript\nconst purple = RGBA.fromHex(\"#800080\")\nconst withAlpha = RGBA.fromHex(\"#FF000080\") // Semi-transparent red\n```\n\n## String colors\n\nMost component properties accept both `RGBA` objects and color strings:\n\n```typescript\nimport { Text, Box } from \"@opentui/core\"\n\n// Using hex strings\nText({ content: \"Hello\", fg: \"#00FF00\" })\n\n// Using CSS color names\nBox({ backgroundColor: \"red\", borderColor: \"white\" })\n\n// Using RGBA objects\nconst customColor = RGBA.fromInts(100, 150, 200, 255)\nText({ content: \"Custom\", fg: customColor })\n\n// Transparent\nBox({ backgroundColor: \"transparent\" })\n```\n\n## The parseColor utility\n\nThe `parseColor()` function converts various color formats to RGBA:\n\n```typescript\nimport { parseColor } from \"@opentui/core\"\n\nconst color1 = parseColor(\"#FF0000\") // Hex\nconst color2 = parseColor(\"blue\") // CSS color name\nconst color3 = parseColor(\"transparent\") // Transparent\nconst color4 = parseColor(RGBA.fromInts(255, 0, 0, 255)) // Pass-through\n```\n\n## Alpha blending\n\nYou can use transparent cells and alpha blending for layered effects:\n\n```typescript\nimport { FrameBufferRenderable, RGBA } from \"@opentui/core\"\n\nconst canvas = new FrameBufferRenderable(renderer, {\n  id: \"canvas\",\n  width: 50,\n  height: 20,\n})\n\n// Draw with alpha blending\nconst semiTransparent = RGBA.fromValues(1.0, 0.0, 0.0, 0.5)\nconst transparent = RGBA.fromInts(0, 0, 0, 0)\ncanvas.frameBuffer.setCellWithAlphaBlending(10, 5, \" \", transparent, semiTransparent)\n```\n\n## Text attributes with colors\n\nCombine colors with text attributes:\n\n```typescript\nimport { TextRenderable, TextAttributes, RGBA } from \"@opentui/core\"\n\nconst styledText = new TextRenderable(renderer, {\n  id: \"styled\",\n  content: \"Important\",\n  fg: RGBA.fromHex(\"#FFFF00\"),\n  bg: RGBA.fromHex(\"#333333\"),\n  attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,\n})\n```\n\n## Color constants\n\nCommon colors:\n\n```typescript\n// Some examples of commonly used colors\nconst white = RGBA.fromInts(255, 255, 255, 255)\nconst black = RGBA.fromInts(0, 0, 0, 255)\nconst red = RGBA.fromInts(255, 0, 0, 255)\nconst green = RGBA.fromInts(0, 255, 0, 255)\nconst blue = RGBA.fromInts(0, 0, 255, 255)\nconst transparent = RGBA.fromInts(0, 0, 0, 0)\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/console.mdx",
    "content": "---\ntitle: Console overlay\ndescription: Built-in debugging console for TUI applications\norder: 4\n---\n\n# Console overlay\n\nOpenTUI includes a built-in console overlay that captures all `console.log`, `console.info`, `console.warn`, `console.error`, and `console.debug` calls. You can position the console at any edge of the terminal. It supports scrolling and focus management.\n\n## Basic usage\n\n```typescript\nimport { createCliRenderer, ConsolePosition } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer({\n  consoleOptions: {\n    position: ConsolePosition.BOTTOM,\n    sizePercent: 30,\n  },\n})\n\n// These appear in the overlay instead of stdout\nconsole.log(\"This appears in the overlay\")\nconsole.error(\"Errors are color-coded red\")\nconsole.warn(\"Warnings appear in yellow\")\n```\n\n## Configuration options\n\n```typescript\nconst renderer = await createCliRenderer({\n  consoleOptions: {\n    position: ConsolePosition.BOTTOM, // Position on screen\n    sizePercent: 30, // Size as percentage of terminal\n    colorInfo: \"#00FFFF\", // Color for console.info\n    colorWarn: \"#FFFF00\", // Color for console.warn\n    colorError: \"#FF0000\", // Color for console.error\n    startInDebugMode: false, // Show file/line info in logs\n  },\n})\n```\n\n## Console positions\n\n```typescript\nimport { ConsolePosition } from \"@opentui/core\"\n\nConsolePosition.TOP // Dock at top\nConsolePosition.BOTTOM // Dock at bottom\nConsolePosition.LEFT // Dock at left\nConsolePosition.RIGHT // Dock at right\n```\n\n## Toggling the console\n\nToggle the console overlay in code:\n\n```typescript\n// Toggle visibility and focus\nrenderer.console.toggle()\n\n// When open but not focused, toggle() focuses the console\n// When focused, toggle() closes the console\n```\n\n## Console shortcuts\n\nWhen the console is focused:\n\n| Key        | Action                     |\n| ---------- | -------------------------- |\n| Arrow keys | Scroll through log history |\n| `+`        | Increase console size      |\n| `-`        | Decrease console size      |\n\n## Keybinding for toggle\n\nAdd a keyboard shortcut to toggle the console:\n\n```typescript\nrenderer.keyInput.on(\"keypress\", (key) => {\n  // Toggle with backtick key\n  if (key.name === \"`\") {\n    renderer.console.toggle()\n  }\n\n  // Or with a modifier\n  if (key.ctrl && key.name === \"l\") {\n    renderer.console.toggle()\n  }\n})\n```\n\n## Why use the console?\n\nTerminal UI applications capture stdout for rendering. Regular `console.log` calls would interfere with your interface. The console overlay solves this problem:\n\n- Captures all console output without disrupting the UI\n- Displays logs in a dedicated overlay area\n- Color-codes different log levels for easy identification\n- Lets you scroll through history for debugging\n\n## Environment variables\n\nControl console behavior with environment variables:\n\n```bash\n# Disable console capture entirely\nOTUI_USE_CONSOLE=false bun app.ts\n\n# Start with console visible\nSHOW_CONSOLE=true bun app.ts\n\n# Dump captured output on exit\nOTUI_DUMP_CAPTURES=true bun app.ts\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/constructs.mdx",
    "content": "---\ntitle: Constructs\ndescription: Declarative component composition with VNodes\norder: 3\n---\n\n# Constructs\n\nConstructs let you compose your UI in a declarative, React-like way. They are factory functions that create VNodes (virtual nodes). A VNode is a lightweight description of a component. When you add a VNode to the tree, it becomes an actual Renderable.\n\n## Basic Usage\n\nConstructs look like function calls that return component descriptions:\n\n```typescript\nimport { createCliRenderer, Box, Text, Input } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nrenderer.root.add(\n  Box(\n    { width: 40, height: 10, borderStyle: \"rounded\", padding: 1 },\n    Text({ content: \"Welcome!\" }),\n    Input({ placeholder: \"Enter your name...\" }),\n  ),\n)\n```\n\nPass children as additional arguments after the props object, not as a property.\n\n## Available Constructs\n\nMost core Renderable classes have matching construct functions:\n\n```typescript\nimport {\n  ASCIIFont,\n  Box,\n  Code,\n  FrameBuffer,\n  Input,\n  ScrollBox,\n  Select,\n  SyntaxStyle,\n  TabSelect,\n  Text,\n  RGBA,\n} from \"@opentui/core\"\n\n// These create VNodes, not actual Renderables\nconst syntaxStyle = SyntaxStyle.fromStyles({\n  default: { fg: RGBA.fromHex(\"#FFFFFF\") },\n})\nconst box = Box({ border: true })\nconst text = Text({ content: \"Hello\" })\nconst input = Input({ placeholder: \"Type here...\" })\nconst code = Code({ content: \"const x = 1\", filetype: \"typescript\", syntaxStyle })\nconst scrollBox = ScrollBox({ width: 40, height: 10 })\nconst frameBuffer = FrameBuffer({ width: 20, height: 10 })\nconst ascii = ASCIIFont({ text: \"OPEN\", font: \"tiny\" })\n```\n\n## How Constructs Work\n\nWhen you call a construct like `Box()`, it creates a VNode - a plain object describing what to create:\n\n```typescript\n// This creates a VNode, not an actual BoxRenderable\nconst myBox = Box({ width: 20, height: 10 })\n\n// The VNode is instantiated when added to the tree\nrenderer.root.add(myBox) // Now it becomes a real BoxRenderable\n```\n\nThis deferred creation lets you:\n\n- Compose UI without a render context\n- Queue method calls before the component exists\n- Write cleaner, more declarative code\n\n## Creating Custom Constructs\n\nCreate reusable components by writing functions that return VNodes:\n\n```typescript\nfunction LabeledInput(props: { label: string; placeholder: string }) {\n  return Box(\n    { flexDirection: \"row\", gap: 1 },\n    Text({ content: props.label }),\n    Input({ placeholder: props.placeholder, width: 20 }),\n  )\n}\n\nrenderer.root.add(\n  Box(\n    { flexDirection: \"column\", padding: 1 },\n    LabeledInput({ label: \"Name:\", placeholder: \"Enter name...\" }),\n    LabeledInput({ label: \"Email:\", placeholder: \"Enter email...\" }),\n  ),\n)\n```\n\n## Method Chaining on VNodes\n\nVNodes support method calls. The system queues these calls and applies them after creating the component:\n\n```typescript\nconst input = Input({ id: \"my-input\", placeholder: \"Type here...\" })\n\n// The VNode queues this call\ninput.focus()\n\n// When added, the system creates the input and calls focus()\nrenderer.root.add(input)\n```\n\nYou can also set properties:\n\n```typescript\nconst box = Box({ id: \"my-box\" })\nbox.backgroundColor = RGBA.fromHex(\"#333366\")\nrenderer.root.add(box)\n```\n\n## The delegate() Function\n\nComposite components often need outer method calls to reach a specific inner component. The `delegate()` function maps method and property names to descendant IDs:\n\n```typescript\nimport { delegate, Box, Text, Input } from \"@opentui/core\"\n\nfunction LabeledInput(props: { id: string; label: string; placeholder: string }) {\n  return delegate(\n    {\n      focus: `${props.id}-input`, // Route focus() to the input\n      value: `${props.id}-input`, // Route value property access\n    },\n    Box(\n      { flexDirection: \"row\" },\n      Text({ content: props.label }),\n      Input({\n        id: `${props.id}-input`,\n        placeholder: props.placeholder,\n        width: 20,\n      }),\n    ),\n  )\n}\n\nconst username = LabeledInput({ id: \"username\", label: \"Username:\", placeholder: \"Enter username...\" })\n\n// This actually focuses the nested Input, not the outer Box\nusername.focus()\n\nrenderer.root.add(username)\n```\n\n### Delegate Mappings\n\nThe mapping object's keys are method or property names, and the values are descendant IDs:\n\n```typescript\ndelegate(\n  {\n    focus: \"inner-input\", // .focus() -> find descendant \"inner-input\" and call focus()\n    blur: \"inner-input\", // .blur() -> same\n    add: \"content-area\", // .add() -> add children to \"content-area\" instead\n    value: \"inner-input\", // .value get/set -> proxy to \"inner-input\"\n  },\n  vnode,\n)\n```\n\n## Composing with Children\n\nCustom constructs can accept and pass through children:\n\n```typescript\nfunction Card(props: { title: string }, ...children: VChild[]) {\n  return Box(\n    { border: true, padding: 1, flexDirection: \"column\" },\n    Text({ content: props.title, fg: \"#FFFF00\" }),\n    Box({ flexDirection: \"column\" }, ...children),\n  )\n}\n\nrenderer.root.add(Card({ title: \"User Info\" }, Text({ content: \"Name: Alice\" }), Text({ content: \"Role: Admin\" })))\n```\n\n## Mixing Renderables and Constructs\n\nYou can mix both approaches:\n\n```typescript\nimport { BoxRenderable, Text, Input } from \"@opentui/core\"\n\n// Create a renderable directly\nconst container = new BoxRenderable(renderer, {\n  id: \"container\",\n  flexDirection: \"column\",\n})\n\n// Add constructs to it\ncontainer.add(Text({ content: \"Title\" }), Input({ placeholder: \"Type here...\" }))\n\nrenderer.root.add(container)\n```\n\nOr use constructs that contain renderables:\n\n```typescript\nconst customRenderable = new CustomRenderable(renderer, { id: \"custom\" })\n\nrenderer.root.add(\n  Box(\n    { padding: 1 },\n    Text({ content: \"Header\" }),\n    customRenderable, // Regular renderable mixed in\n    Text({ content: \"Footer\" }),\n  ),\n)\n```\n\n## Next Steps\n\nSee [Renderables vs Constructs](/docs/core-concepts/renderables-vs-constructs) for a detailed comparison of when to use each approach.\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/keyboard.mdx",
    "content": "---\ntitle: Keyboard input\ndescription: Handling keyboard events in OpenTUI\norder: 3\n---\n\n# Keyboard input\n\nOpenTUI parses terminal input and provides structured key events. The `renderer.keyInput` EventEmitter emits `keypress` events plus raw `paste` events with optional metadata.\n\n## Basic key handling\n\n```typescript\nimport { createCliRenderer, type KeyEvent } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\nconst keyHandler = renderer.keyInput\n\nkeyHandler.on(\"keypress\", (key: KeyEvent) => {\n  console.log(\"Key name:\", key.name)\n  console.log(\"Sequence:\", key.sequence)\n  console.log(\"Ctrl pressed:\", key.ctrl)\n  console.log(\"Shift pressed:\", key.shift)\n  console.log(\"Alt pressed:\", key.meta)\n  console.log(\"Option pressed:\", key.option)\n})\n```\n\n## KeyEvent properties\n\nEach `KeyEvent` contains:\n\n| Property   | Type      | Description                              |\n| ---------- | --------- | ---------------------------------------- |\n| `name`     | `string`  | The key name (e.g., \"a\", \"escape\", \"f1\") |\n| `sequence` | `string`  | The raw escape sequence                  |\n| `ctrl`     | `boolean` | Whether Ctrl was held                    |\n| `shift`    | `boolean` | Whether Shift was held                   |\n| `meta`     | `boolean` | Whether Alt/Meta was held                |\n| `option`   | `boolean` | Whether Option was held (macOS)          |\n\n## Common key patterns\n\n### Single keys\n\n```typescript\nkeyHandler.on(\"keypress\", (key: KeyEvent) => {\n  if (key.name === \"escape\") {\n    console.log(\"Escape pressed!\")\n  }\n\n  if (key.name === \"return\") {\n    console.log(\"Enter pressed!\")\n  }\n\n  if (key.name === \"space\") {\n    console.log(\"Space pressed!\")\n  }\n})\n```\n\n### Modifier combinations\n\n```typescript\nkeyHandler.on(\"keypress\", (key: KeyEvent) => {\n  // Ctrl+C\n  if (key.ctrl && key.name === \"c\") {\n    console.log(\"Ctrl+C pressed!\")\n  }\n\n  // Ctrl+S\n  if (key.ctrl && key.name === \"s\") {\n    console.log(\"Save shortcut!\")\n  }\n\n  // Shift+F1\n  if (key.shift && key.name === \"f1\") {\n    console.log(\"Shift+F1 pressed!\")\n  }\n\n  // Alt+Enter\n  if (key.meta && key.name === \"return\") {\n    console.log(\"Alt+Enter pressed!\")\n  }\n})\n```\n\n### Function keys\n\n```typescript\nkeyHandler.on(\"keypress\", (key: KeyEvent) => {\n  // F1-F12\n  if (key.name === \"f1\") {\n    showHelp()\n  }\n\n  if (key.name === \"f5\") {\n    refresh()\n  }\n})\n```\n\n### Arrow keys\n\n```typescript\nkeyHandler.on(\"keypress\", (key: KeyEvent) => {\n  switch (key.name) {\n    case \"up\":\n      moveCursorUp()\n      break\n    case \"down\":\n      moveCursorDown()\n      break\n    case \"left\":\n      moveCursorLeft()\n      break\n    case \"right\":\n      moveCursorRight()\n      break\n  }\n})\n```\n\n## Paste events\n\nHandle pasted bytes separately from individual keypresses. Decode them explicitly when you expect text:\n\n```typescript\nimport { type PasteEvent } from \"@opentui/core\"\n\nconst textDecoder = new TextDecoder()\n\nkeyHandler.on(\"paste\", (event: PasteEvent) => {\n  console.log(\"Pasted bytes:\", event.bytes)\n  console.log(\"Decoded text:\", textDecoder.decode(event.bytes))\n  console.log(\"Metadata:\", event.metadata)\n})\n```\n\n## Exit on Ctrl+C\n\nConfigure the renderer to automatically exit on Ctrl+C:\n\n```typescript\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: true, // Default behavior\n})\n\n// Or handle it manually\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: false,\n})\n\nrenderer.keyInput.on(\"keypress\", (key: KeyEvent) => {\n  if (key.ctrl && key.name === \"c\") {\n    // Custom cleanup before exit\n    cleanup()\n    process.exit(0)\n  }\n})\n```\n\n## Focus and key routing\n\nFocus components to receive keyboard input. OpenTUI routes events to the focused component:\n\n```typescript\nimport { InputRenderable } from \"@opentui/core\"\n\nconst input = new InputRenderable(renderer, {\n  id: \"my-input\",\n  placeholder: \"Type here...\",\n})\n\n// Focus the input to receive key events\ninput.focus()\n\n// Or with constructs\nimport { Input } from \"@opentui/core\"\n\nconst inputNode = Input({ placeholder: \"Type here...\" })\ninputNode.focus() // Queued for when instantiated\n\nrenderer.root.add(inputNode)\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/layout.mdx",
    "content": "---\ntitle: Layout System\ndescription: Flexible positioning and sizing with Yoga/Flexbox\norder: 1\n---\n\n# Layout System\n\nOpenTUI uses the Yoga layout engine to provide CSS Flexbox-like capabilities for responsive terminal layouts. You can create complex, dynamic interfaces that adapt to terminal size changes.\n\n## Flexbox Basics\n\nThe layout system supports standard flexbox properties:\n\n```typescript\nimport { BoxRenderable, createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst container = new BoxRenderable(renderer, {\n  id: \"container\",\n  flexDirection: \"row\",\n  justifyContent: \"space-between\",\n  alignItems: \"center\",\n  width: \"100%\",\n  height: 10,\n})\n\nconst leftPanel = new BoxRenderable(renderer, {\n  id: \"left\",\n  flexGrow: 1,\n  height: 10,\n  backgroundColor: \"#444\",\n})\n\nconst rightPanel = new BoxRenderable(renderer, {\n  id: \"right\",\n  width: 20,\n  height: 10,\n  backgroundColor: \"#666\",\n})\n\ncontainer.add(leftPanel)\ncontainer.add(rightPanel)\nrenderer.root.add(container)\n```\n\n## Flex Direction\n\nControl the direction of child elements:\n\n```typescript\n// Vertical layout (default)\n{\n  flexDirection: \"column\"\n}\n\n// Horizontal layout\n{\n  flexDirection: \"row\"\n}\n\n// Reversed directions\n{\n  flexDirection: \"row-reverse\"\n}\n{\n  flexDirection: \"column-reverse\"\n}\n```\n\n## Justify Content\n\nAlign children along the main axis:\n\n```typescript\n{\n  justifyContent: \"flex-start\"\n} // Pack at start\n{\n  justifyContent: \"flex-end\"\n} // Pack at end\n{\n  justifyContent: \"center\"\n} // Center children\n{\n  justifyContent: \"space-between\"\n} // Even spacing, no edge gaps\n{\n  justifyContent: \"space-around\"\n} // Even spacing with edge gaps\n{\n  justifyContent: \"space-evenly\"\n} // Truly even spacing\n```\n\n## Align Items\n\nAlign children along the cross axis:\n\n```typescript\n{\n  alignItems: \"flex-start\"\n} // Align to start\n{\n  alignItems: \"flex-end\"\n} // Align to end\n{\n  alignItems: \"center\"\n} // Center on cross axis\n{\n  alignItems: \"stretch\"\n} // Stretch to fill (default)\n{\n  alignItems: \"baseline\"\n} // Align baselines\n```\n\n## Sizing\n\n### Fixed Sizes\n\n```typescript\n{\n  width: 30,   // Fixed width in characters\n  height: 10,  // Fixed height in rows\n}\n```\n\n### Percentage Sizes\n\n```typescript\n{\n  width: \"100%\",  // Full width of parent\n  height: \"50%\",  // Half height of parent\n}\n```\n\n### Flex Growing and Shrinking\n\n```typescript\n{\n  flexGrow: 1,    // Take up available space\n  flexShrink: 0,  // Don't shrink below content size\n  flexBasis: 100, // Initial size before flex adjustments\n}\n```\n\n## Positioning\n\n### Relative (Default)\n\nElements flow normally in the layout:\n\n```typescript\n{\n  position: \"relative\",\n}\n```\n\n### Absolute\n\nPosition elements relative to their parent:\n\n```typescript\n{\n  position: \"absolute\",\n  left: 10,\n  top: 5,\n  right: 10,\n  bottom: 5,\n}\n```\n\n## Padding and Margin\n\n```typescript\n{\n  // Uniform padding\n  padding: 2,\n\n  // Axis-specific\n  paddingX: 4,\n  paddingY: 2,\n\n  // Individual sides\n  paddingTop: 1,\n  paddingRight: 2,\n  paddingBottom: 1,\n  paddingLeft: 2,\n\n  // Margin works the same way\n  margin: 1,\n  marginX: 3,\n  marginY: 1,\n  marginTop: 1,\n  marginRight: 2,\n  marginBottom: 1,\n  marginLeft: 2,\n}\n```\n\n## Using Constructs\n\nThe same layout properties work with the declarative API:\n\n```typescript\nimport { Box, Text } from \"@opentui/core\"\n\nrenderer.root.add(\n  Box(\n    {\n      flexDirection: \"row\",\n      width: \"100%\",\n      height: 10,\n    },\n    Box(\n      {\n        flexGrow: 1,\n        backgroundColor: \"#333\",\n        padding: 1,\n      },\n      Text({ content: \"Left Panel\" }),\n    ),\n    Box(\n      {\n        width: 20,\n        backgroundColor: \"#555\",\n        padding: 1,\n      },\n      Text({ content: \"Right Panel\" }),\n    ),\n  ),\n)\n```\n\n## Responsive Layouts\n\nListen for terminal resize events to create responsive layouts:\n\n```typescript\nconst renderer = await createCliRenderer()\n\nrenderer.on(\"resize\", (width, height) => {\n  // Update layout based on new dimensions\n  if (width < 80) {\n    container.flexDirection = \"column\"\n  } else {\n    container.flexDirection = \"row\"\n  }\n})\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/lifecycle.mdx",
    "content": "---\ntitle: Lifecycle and cleanup\ndescription: Managing renderer lifecycle and terminal cleanup\norder: 6\n---\n\n# Lifecycle and cleanup\n\nOpenTUI gives you control over terminal cleanup. Call `renderer.destroy()` when you shut down. It restores the terminal to its original state, and it frees resources.\n\n## Why you must handle cleanup\n\nOpenTUI does not automatically clean up on `process.exit` or unhandled errors. This design gives you more control over shutdown behavior:\n\n- You may want to handle errors and keep running\n- You may use effect systems, like Effect.ts, with their own shutdown handling\n- You may need custom cleanup order or additional shutdown logic\n\n## Use `renderer.destroy()`\n\nCall `destroy()` when your app exits:\n\n```typescript\nimport { createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\n// ... your application code ...\n\n// Clean shutdown\nrenderer.destroy()\n```\n\nFor reliable cleanup, handle errors and signals in your app:\n\n```typescript\nconst renderer = await createCliRenderer()\n\nprocess.on(\"uncaughtException\", (error) => {\n  console.error(\"Uncaught exception:\", error)\n  renderer.destroy()\n  process.exit(1)\n})\n\nprocess.on(\"unhandledRejection\", (reason) => {\n  console.error(\"Unhandled rejection:\", reason)\n  renderer.destroy()\n  process.exit(1)\n})\n```\n\n## Signal handling\n\nOpenTUI listens for common exit signals and calls `destroy()` when it receives them. By default, it handles these signals:\n\n| Signal     | Description              |\n| ---------- | ------------------------ |\n| `SIGINT`   | Ctrl+C                   |\n| `SIGTERM`  | Termination signal       |\n| `SIGQUIT`  | Ctrl+\\                   |\n| `SIGABRT`  | Abort signal             |\n| `SIGHUP`   | Hangup (terminal closed) |\n| `SIGBREAK` | Ctrl+Break on Windows    |\n| `SIGPIPE`  | Broken pipe              |\n| `SIGBUS`   | Bus error                |\n| `SIGFPE`   | Floating point exception |\n\nYou can customize which signals trigger cleanup:\n\n```typescript\n// Only handle SIGINT and SIGTERM\nconst renderer = await createCliRenderer({\n  exitSignals: [\"SIGINT\", \"SIGTERM\"],\n})\n\n// Disable all signal-based cleanup (handle it yourself)\nconst renderer = await createCliRenderer({\n  exitSignals: [],\n})\n```\n\n## Ctrl+C behavior\n\nBy default, Ctrl+C calls `destroy()`. If you want to handle Ctrl+C yourself, disable the internal handler and remove `SIGINT` from `exitSignals`:\n\n```typescript\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: false,\n  exitSignals: [\"SIGTERM\", \"SIGQUIT\", \"SIGABRT\", \"SIGHUP\", \"SIGBREAK\", \"SIGPIPE\", \"SIGBUS\", \"SIGFPE\"],\n})\n\nrenderer.keyInput.on(\"keypress\", (key) => {\n  if (key.ctrl && key.name === \"c\") {\n    // Custom Ctrl+C handling\n    console.log(\"Ctrl+C pressed, but not exiting\")\n  }\n})\n```\n\n## Destroy callback\n\nRun custom logic when the renderer is destroyed:\n\n```typescript\nconst renderer = await createCliRenderer({\n  onDestroy: () => {\n    console.log(\"Renderer destroyed, performing additional cleanup...\")\n  },\n})\n```\n\n## What `destroy()` cleans up\n\nThe `destroy()` method cleans up these resources:\n\n- Removes the signal and process listeners that OpenTUI adds\n- Clears timers and render loops\n- Destroys all renderables in the tree\n- Restores stdin raw mode\n- Resets terminal state (cursor, alternate screen, etc.)\n- Frees native resources\n\n## Troubleshooting\n\nIf your terminal stays in a broken state after a crash:\n\n1. Run `reset` in your terminal to restore it\n2. Add `uncaughtException` and `unhandledRejection` handlers to your app\n3. Make sure you call `renderer.destroy()` in all exit paths\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/renderables-vs-constructs.mdx",
    "content": "---\ntitle: Renderables vs Constructs\ndescription: Two approaches to building UIs in OpenTUI\norder: 5\n---\n\n# Renderables vs Constructs\n\nOpenTUI provides two ways to build your UI: the imperative Renderable API and the declarative Construct API. Both approaches have different tradeoffs.\n\n## Imperative (Renderables)\n\nYou create `Renderable` instances with a `RenderContext` and compose them using `add()`. You mutate state and behavior directly on instances through setters and methods.\n\n```typescript\nimport { BoxRenderable, TextRenderable, InputRenderable, createCliRenderer, type RenderContext } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst loginForm = new BoxRenderable(renderer, {\n  id: \"login-form\",\n  width: 40,\n  height: 10,\n  padding: 1,\n})\n\n// Compose multiple renderables into one\nfunction createLabeledInput(renderer: RenderContext, props: { label: string; placeholder: string; id: string }) {\n  const container = new BoxRenderable(renderer, {\n    id: `${props.id}-container`,\n    flexDirection: \"row\",\n  })\n\n  container.add(\n    new TextRenderable(renderer, {\n      id: `${props.id}-label`,\n      content: props.label + \" \",\n    }),\n  )\n\n  container.add(\n    new InputRenderable(renderer, {\n      id: `${props.id}-input`,\n      placeholder: props.placeholder,\n      width: 20,\n    }),\n  )\n\n  return container\n}\n\nconst username = createLabeledInput(renderer, {\n  id: \"username\",\n  label: \"Username:\",\n  placeholder: \"Enter username...\",\n})\nloginForm.add(username)\n\n// You must navigate to the nested component to focus it\nusername.getRenderable(\"username-input\")?.focus()\n\nrenderer.root.add(loginForm)\n```\n\n### Characteristics\n\n- Requires `RenderContext` at creation time\n- Direct mutation of instances\n- Manual navigation for nested component access\n- Explicit control over component lifecycle\n\n## Declarative (Constructs)\n\nBuilds a lightweight VNode graph using functional constructs. Instances don't exist until you add the node to the tree. VNodes queue method calls and replay them when instantiated.\n\n```typescript\nimport { Text, Input, Box, createCliRenderer, delegate } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nfunction LabeledInput(props: { id: string; label: string; placeholder: string }) {\n  return delegate(\n    { focus: `${props.id}-input` },\n    Box(\n      { flexDirection: \"row\" },\n      Text({ content: props.label + \" \" }),\n      Input({\n        id: `${props.id}-input`,\n        placeholder: props.placeholder,\n        width: 20,\n      }),\n    ),\n  )\n}\n\nconst usernameInput = LabeledInput({\n  id: \"username\",\n  label: \"Username:\",\n  placeholder: \"Enter username...\",\n})\n\n// delegate() automatically routes focus to the nested input\nusernameInput.focus()\n\nconst loginForm = Box(\n  { width: 40, height: 10, padding: 1 },\n  usernameInput,\n  LabeledInput({\n    id: \"password\",\n    label: \"Password:\",\n    placeholder: \"Enter password...\",\n  }),\n)\n\nrenderer.root.add(loginForm)\n```\n\n### Characteristics\n\n- No `RenderContext` needed until instantiation\n- VNodes queue method calls\n- `delegate()` routes APIs to nested components\n- Declarative, React-like syntax\n\n## The delegate() function\n\nThe `delegate()` function makes constructs ergonomic by routing method calls from the parent to specific children:\n\n```typescript\nfunction Button(props: { id: string; label: string; onClick: () => void }) {\n  return delegate(\n    {\n      focus: `${props.id}-box`, // Route focus() to the box\n    },\n    Box(\n      {\n        id: `${props.id}-box`,\n        border: true,\n        onMouseDown: props.onClick,\n      },\n      Text({ content: props.label }),\n    ),\n  )\n}\n\nconst button = Button({ id: \"submit\", label: \"Submit\", onClick: handleSubmit })\nbutton.focus() // Focuses the inner Box\n```\n\n## When to use which\n\n### Use Renderables when\n\n- You need fine-grained control over component lifecycle\n- You're building low-level custom components\n- You need to access renderable methods immediately\n- Performance is critical and you want to avoid VNode overhead\n\n### Use Constructs when\n\n- You prefer declarative, compositional code\n- You're building higher-level UI components\n- You want cleaner, more readable component definitions\n- You're familiar with React/Solid patterns\n\n## Mixing both\n\nYou can mix both approaches in the same application:\n\n```typescript\nimport { BoxRenderable, Text, Input } from \"@opentui/core\"\n\n// Create a renderable container\nconst container = new BoxRenderable(renderer, {\n  id: \"container\",\n  flexDirection: \"column\",\n})\n\n// Add constructs to it\ncontainer.add(Text({ content: \"Title\" }), Input({ placeholder: \"Type here...\" }))\n\nrenderer.root.add(container)\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/renderables.mdx",
    "content": "---\ntitle: Renderables\ndescription: The building blocks of your terminal UI\norder: 2\n---\n\n# Renderables\n\nRenderables are the building blocks of your UI. You can position, style, and nest them within each other. Each renderable represents a visual element and uses the Yoga layout engine for flexible positioning and sizing.\n\n## Creating Renderables\n\nCreate a renderable by instantiating a class with a render context (the renderer) and options:\n\n```typescript\nimport { createCliRenderer, TextRenderable, BoxRenderable } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer()\n\nconst greeting = new TextRenderable(renderer, {\n  id: \"greeting\",\n  content: \"Hello, OpenTUI!\",\n  fg: \"#00FF00\",\n})\n\nrenderer.root.add(greeting)\n```\n\n## Available Renderables\n\nOpenTUI provides these built-in renderables:\n\n| Class                   | Description                                   |\n| ----------------------- | --------------------------------------------- |\n| `BoxRenderable`         | Container with border, background, and layout |\n| `TextRenderable`        | Read-only styled text display                 |\n| `InputRenderable`       | Single-line text input                        |\n| `TextareaRenderable`    | Multi-line editable text                      |\n| `SelectRenderable`      | Dropdown/list selection                       |\n| `TabSelectRenderable`   | Horizontal tab selection                      |\n| `ScrollBoxRenderable`   | Scrollable container                          |\n| `ScrollBarRenderable`   | Standalone scroll bar control                 |\n| `CodeRenderable`        | Syntax-highlighted code display               |\n| `LineNumberRenderable`  | Line number gutter for code/text views        |\n| `DiffRenderable`        | Unified or split diff viewer                  |\n| `ASCIIFontRenderable`   | ASCII art font display                        |\n| `FrameBufferRenderable` | Raw framebuffer for custom graphics           |\n| `MarkdownRenderable`    | Markdown renderer                             |\n| `SliderRenderable`      | Numeric slider control                        |\n\n## The Renderable Tree\n\nRenderables form a tree structure. Use `add()` and `remove()` to manage children:\n\n```typescript\nconst container = new BoxRenderable(renderer, {\n  id: \"container\",\n  flexDirection: \"column\",\n  padding: 1,\n})\n\nconst title = new TextRenderable(renderer, { id: \"title\", content: \"My App\" })\nconst body = new TextRenderable(renderer, { id: \"body\", content: \"Content here\" })\n\ncontainer.add(title)\ncontainer.add(body)\n\nrenderer.root.add(container)\n\n// Later, remove a child\ncontainer.remove(\"body\")\n```\n\n## Finding Renderables\n\nNavigate the tree to find specific renderables:\n\n```typescript\n// Get a direct child by ID\nconst title = container.getRenderable(\"title\")\n\n// Recursively search all descendants\nconst deepChild = container.findDescendantById(\"nested-input\")\n\n// Get all children\nconst children = container.getChildren()\n```\n\n## Layout Properties\n\nAll renderables support Yoga flexbox properties:\n\n```typescript\nconst panel = new BoxRenderable(renderer, {\n  id: \"panel\",\n\n  // Sizing\n  width: 40,\n  height: \"50%\",\n  minWidth: 20,\n  maxHeight: 30,\n\n  // Flex behavior\n  flexGrow: 1,\n  flexShrink: 0,\n  flexDirection: \"column\",\n  justifyContent: \"center\",\n  alignItems: \"flex-start\",\n\n  // Positioning\n  position: \"absolute\",\n  left: 10,\n  top: 5,\n\n  // Spacing\n  padding: 2,\n  paddingTop: 1,\n  margin: 1,\n})\n```\n\nSee the [Layout](/docs/core-concepts/layout) page for complete details.\n\n## Focus Management\n\nInteractive renderables can receive keyboard focus:\n\n```typescript\nconst input = new InputRenderable(renderer, {\n  id: \"username\",\n  placeholder: \"Enter username...\",\n})\n\nrenderer.root.add(input)\n\n// Give focus to the input\ninput.focus()\n\n// Remove focus\ninput.blur()\n\n// Check focus state\nconsole.log(input.focused) // true\n```\n\nBy default, left-clicking a renderable will auto-focus the closest focusable ancestor. Disable this globally with\n`createCliRenderer({ autoFocus: false })`, or stop it per interaction by calling `event.preventDefault()` in\n`onMouseDown`.\n\nListen for focus changes:\n\n```typescript\nimport { RenderableEvents } from \"@opentui/core\"\n\ninput.on(RenderableEvents.FOCUSED, () => {\n  console.log(\"Input focused\")\n})\n\ninput.on(RenderableEvents.BLURRED, () => {\n  console.log(\"Input blurred\")\n})\n```\n\n## Event Handling\n\n### Mouse Events\n\nHandle mouse interactions via options:\n\n```typescript\nconst button = new BoxRenderable(renderer, {\n  id: \"button\",\n  border: true,\n  onMouseDown: (event) => {\n    console.log(\"Clicked at\", event.x, event.y)\n  },\n  onMouseOver: (event) => {\n    button.borderColor = \"#FFFF00\"\n  },\n  onMouseOut: (event) => {\n    button.borderColor = \"#FFFFFF\"\n  },\n})\n```\n\nAvailable mouse events:\n\n- `onMouseDown`, `onMouseUp`\n- `onMouseMove`, `onMouseDrag`, `onMouseDragEnd`, `onMouseDrop`\n- `onMouseOver`, `onMouseOut`\n- `onMouseScroll`\n- `onMouse` (catch-all)\n\nMouse events bubble up through the tree. Stop propagation with `event.stopPropagation()`.\n\n### Keyboard Events\n\nFor focusable renderables:\n\n```typescript\nconst textDecoder = new TextDecoder()\n\nconst input = new InputRenderable(renderer, {\n  id: \"input\",\n  onKeyDown: (key) => {\n    if (key.name === \"escape\") {\n      input.blur()\n    }\n  },\n  onPaste: (event) => {\n    console.log(\"Pasted:\", textDecoder.decode(event.bytes))\n  },\n})\n```\n\n## Visibility\n\nControl visibility with the `visible` property:\n\n```typescript\n// Hide (also removes from layout)\npanel.visible = false\n\n// Show\npanel.visible = true\n```\n\nWhen `visible` is `false`, Yoga excludes the renderable from layout calculation (equivalent to CSS `display: none`).\n\n## Opacity\n\nSet opacity for semi-transparent rendering:\n\n```typescript\npanel.opacity = 0.5 // 50% transparent\n```\n\nOpacity affects the renderable and all its children.\n\n## Z-Index\n\nControl layering order for overlapping elements:\n\n```typescript\nconst overlay = new BoxRenderable(renderer, {\n  id: \"overlay\",\n  position: \"absolute\",\n  zIndex: 100, // Higher values render on top\n})\n```\n\n## Live Rendering\n\nFor animations, extend the Renderable class and override `onUpdate`:\n\n```typescript\nclass AnimatedBox extends BoxRenderable {\n  onUpdate(deltaTime) {\n    // Update animation state\n    this.translateX += 1\n  }\n}\n\nconst box = new AnimatedBox(renderer, {\n  id: \"anim-box\",\n  live: true, // Enable continuous rendering\n})\n```\n\n## Translation\n\nOffset a renderable from its layout position (useful for scrolling/animation):\n\n```typescript\n// Offset by pixels\nrenderable.translateX = 10\nrenderable.translateY = -5\n```\n\nThis moves the renderable visually without affecting layout.\n\n## Buffered Rendering\n\nEnable offscreen rendering for complex content and use hooks to draw to the buffer:\n\n```typescript\nimport { RGBA } from \"@opentui/core\"\n\nconst complex = new BoxRenderable(renderer, {\n  id: \"complex\",\n  buffered: true, // Render to offscreen buffer first\n  renderAfter: (buffer) => {\n    // Draw directly to the buffer (or offscreen buffer if buffered=true)\n    buffer.fillRect(0, 0, 10, 5, RGBA.fromHex(\"#FF0000\"))\n  },\n})\n```\n\n## Lifecycle Methods\n\nOverride these methods in custom renderables:\n\n```typescript\nclass CustomRenderable extends Renderable {\n  // Called each frame before rendering\n  onUpdate(deltaTime: number) {\n    // Update state, animations, etc.\n  }\n\n  // Called when dimensions change\n  onResize(width: number, height: number) {\n    // Respond to size changes\n  }\n\n  // Called when removed from parent\n  onRemove() {\n    // Cleanup\n  }\n\n  // Override for custom rendering\n  renderSelf(buffer: OptimizedBuffer, deltaTime: number) {\n    // Draw to buffer\n  }\n}\n```\n\n## Destroying Renderables\n\nClean up a renderable and remove it from the tree:\n\n```typescript\n// Remove from parent and free resources\nrenderable.destroy()\n\n// Destroy self and all children\ncontainer.destroyRecursively()\n```\n"
  },
  {
    "path": "packages/web/src/content/docs/core-concepts/renderer.mdx",
    "content": "---\ntitle: Renderer\ndescription: CliRenderer manages terminal output and the render loop\norder: 1\n---\n\n# Renderer\n\nThe `CliRenderer` drives OpenTUI. It manages terminal output, handles input events, runs the rendering loop, and provides context for creating renderables.\n\n## Creating a renderer\n\nCreate a renderer with the async factory function:\n\n```typescript\nimport { createCliRenderer } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: true,\n  targetFps: 30,\n})\n```\n\nThe factory function does three things:\n\n1. Loads the native Zig rendering library\n2. Configures terminal settings (mouse, keyboard protocol, and alternate screen)\n3. Returns an initialized `CliRenderer` instance\n\n## Configuration options\n\n| Option                | Type               | Default   | Description                                                                        |\n| --------------------- | ------------------ | --------- | ---------------------------------------------------------------------------------- |\n| `exitOnCtrlC`         | `boolean`          | `true`    | Call `renderer.destroy()` when Ctrl+C is pressed                                   |\n| `exitSignals`         | `NodeJS.Signals[]` | see below | Signals that trigger cleanup ([details](/core-concepts/lifecycle#signal-handling)) |\n| `targetFps`           | `number`           | `30`      | Target frames per second for the render loop                                       |\n| `maxFps`              | `number`           | `60`      | Maximum FPS for immediate re-renders                                               |\n| `useMouse`            | `boolean`          | `true`    | Enable mouse input and tracking                                                    |\n| `autoFocus`           | `boolean`          | `true`    | Focus nearest focusable on left click                                              |\n| `enableMouseMovement` | `boolean`          | `true`    | Track mouse movement (not just clicks)                                             |\n| `useAlternateScreen`  | `boolean`          | `true`    | Use terminal alternate screen buffer                                               |\n| `consoleOptions`      | `ConsoleOptions`   | -         | Options for the built-in console overlay                                           |\n| `openConsoleOnError`  | `boolean`          | `true`    | Auto-open console when errors occur (dev only)                                     |\n| `onDestroy`           | `() => void`       | -         | Callback executed when renderer is destroyed                                       |\n\n## The root renderable\n\nEvery renderer has a `root` property. It is a special `RootRenderable` at the top of the component tree:\n\n```typescript\nimport { Box, Text } from \"@opentui/core\"\n\n// Add components to the root\nrenderer.root.add(Box({ width: 40, height: 10, borderStyle: \"rounded\" }, Text({ content: \"Hello, OpenTUI!\" })))\n```\n\nThe root renderable fills the entire terminal and adjusts when you resize it.\n\n## Render loop control\n\nYou can use these control modes:\n\n### Automatic mode (default)\n\nIf you do not call `start()`, the renderer re-renders only when the component tree changes:\n\n```typescript\nconst renderer = await createCliRenderer()\nrenderer.root.add(Text({ content: \"Static content\" })) // Triggers render\n```\n\n### Continuous mode\n\nCall `start()` to run the render loop continuously at the target FPS:\n\n```typescript\nrenderer.start() // Start continuous rendering\nrenderer.stop() // Stop the render loop\n```\n\n### Live rendering\n\nFor animations, call `requestLive()` to enable continuous rendering:\n\n```typescript\n// Request live mode (increments internal counter)\nrenderer.requestLive()\n\n// When animation completes, drop the request\nrenderer.dropLive()\n```\n\nMultiple components can request animations at the same time. The renderer stays live until all requests drop.\n\n### Pause and suspend\n\n```typescript\nrenderer.pause() // Pause rendering (use start() or requestLive() to run it again)\n\nrenderer.suspend() // Fully suspend (disables mouse, input, and raw mode)\nrenderer.resume() // Resume from suspended state\n```\n\n## Key properties\n\n| Property                   | Type                 | Description                             |\n| -------------------------- | -------------------- | --------------------------------------- |\n| `root`                     | `RootRenderable`     | Root of the component tree              |\n| `width`                    | `number`             | Current render width in columns         |\n| `height`                   | `number`             | Current render height in rows           |\n| `console`                  | `TerminalConsole`    | Built-in console overlay                |\n| `keyInput`                 | `KeyHandler`         | Keyboard input handler                  |\n| `isRunning`                | `boolean`            | Whether the render loop is active       |\n| `isDestroyed`              | `boolean`            | Whether the renderer has been destroyed |\n| `currentFocusedRenderable` | `Renderable \\| null` | Currently focused component             |\n\n## Events\n\nUse `renderer.on(event, callback)` to subscribe:\n\n| Event                 | Payload                           | Description                                                           |\n| --------------------- | --------------------------------- | --------------------------------------------------------------------- |\n| `resize`              | `(width: number, height: number)` | Terminal window resized                                               |\n| `focus`               | `()`                              | Terminal window gained focus                                          |\n| `blur`                | `()`                              | Terminal window lost focus                                            |\n| `theme_mode`          | `(mode: \"dark\" \\| \"light\")`       | Terminal color scheme changed. See [Theme mode](#theme-mode) section. |\n| `capabilities`        | `(caps: Capabilities)`            | Terminal capabilities detected                                        |\n| `selection`           | `(selection: Selection)`          | Text selection completed                                              |\n| `destroy`             | `()`                              | Renderer destroyed                                                    |\n| `memory:snapshot`     | `(snapshot: MemorySnapshot)`      | Memory usage snapshot available                                       |\n| `debugOverlay:toggle` | `(enabled: boolean)`              | Debug overlay visibility changed                                      |\n\n```typescript\n// Terminal resized\nrenderer.on(\"resize\", (width, height) => {\n  console.log(`Terminal size: ${width}x${height}`)\n})\n\n// Terminal focus events\nrenderer.on(\"focus\", () => {\n  console.log(\"Terminal gained focus\")\n})\n\nrenderer.on(\"blur\", () => {\n  console.log(\"Terminal lost focus\")\n})\n\n// Renderer destroyed\nrenderer.on(\"destroy\", () => {\n  console.log(\"Renderer destroyed\")\n})\n\n// Text selection completed\nrenderer.on(\"selection\", (selection) => {\n  console.log(\"Selected text:\", selection.getSelectedText())\n})\n```\n\n## Theme mode\n\nOpenTUI can detect the terminal's preferred color scheme (dark or light) when the terminal supports DEC mode 2031 color scheme updates. Read the current mode via `renderer.themeMode` and subscribe to `theme_mode` to react to changes. Possible values are `\"dark\"`, `\"light\"`, or `null` when unsupported, and no events fire in the unsupported case.\n\n```typescript\nimport { type ThemeMode } from \"@opentui/core\"\n\nconst mode = renderer.themeMode\n\nrenderer.on(\"theme_mode\", (nextMode: ThemeMode) => {\n  console.log(\"Theme mode changed:\", nextMode)\n})\n```\n\n## Cursor control\n\nUse these methods to control the cursor position and style:\n\n```typescript\n// Position and visibility\nrenderer.setCursorPosition(10, 5, true)\n\n// Cursor style\n//\n// Available styles: \"default\", \"block\", \"underline\", \"line\"\n// The default style is \"default\", which preserves the terminal's native cursor\n// style instead of overriding it.\nrenderer.setCursorStyle({ style: \"block\", blinking: true }) // Blinking block\nrenderer.setCursorStyle({ style: \"underline\", blinking: false }) // Steady underline\nrenderer.setCursorStyle({ style: \"line\", blinking: true }) // Blinking line\nrenderer.setCursorStyle({ style: \"default\" }) // Reset to terminal's native cursor\n\n// Cursor style with color\nrenderer.setCursorStyle({\n  style: \"block\",\n  blinking: true,\n  color: RGBA.fromHex(\"#FF0000\"),\n})\n\n// Cursor style with mouse pointer\n//\n// Types of mouse pointers available: \"default\", \"pointer\", \"text\", \"crosshair\", \"move\",\n// \"not-allowed\"\nrenderer.setCursorStyle({\n  style: \"block\",\n  blinking: false,\n  cursor: \"pointer\",\n})\n```\n\n## Input handling\n\nAdd custom input handlers:\n\n```typescript\nrenderer.addInputHandler((sequence) => {\n  if (sequence === \"\\x1b[A\") {\n    // Up arrow - handle and consume\n    return true\n  }\n  return false // Let other handlers process\n})\n```\n\nBy default, `addInputHandler()` appends handlers to the chain and runs them after built-in handlers. Use `prependInputHandler()` to add a handler at the start of the chain and run it before built-in handlers.\n\n## Debug overlay\n\nUse the debug overlay to show FPS, memory usage, and other stats:\n\n```typescript\nrenderer.toggleDebugOverlay()\n\n// You can also configure it\nimport { DebugOverlayCorner } from \"@opentui/core\"\n\nrenderer.configureDebugOverlay({\n  enabled: true,\n  corner: DebugOverlayCorner.topRight,\n})\n```\n\n## Cleanup\n\nAlways destroy the renderer when you finish so you restore the terminal state:\n\n```typescript\nrenderer.destroy()\n```\n\nDestroying the renderer restores the terminal to its original state, disables mouse tracking, and cleans up resources.\n\n**Important:** OpenTUI does not automatically clean up on `process.exit` or unhandled errors. This design gives you control. See [Lifecycle](/docs/core-concepts/lifecycle/) for signal handling options and best practices.\n\n## Environment variables\n\n| Variable                    | Description                                  |\n| --------------------------- | -------------------------------------------- |\n| `OTUI_USE_ALTERNATE_SCREEN` | Override alternate screen setting            |\n| `OTUI_SHOW_STATS`           | Show debug overlay at startup                |\n| `OTUI_DEBUG`                | Enable debug input capture                   |\n| `OTUI_NO_NATIVE_RENDER`     | Disable native rendering (for debugging)     |\n| `OTUI_DUMP_CAPTURES`        | Dump captured output when the renderer exits |\n| `OTUI_OVERRIDE_STDOUT`      | Override stdout stream (for debugging)       |\n| `OTUI_USE_CONSOLE`          | Enable/disable built-in console              |\n| `SHOW_CONSOLE`              | Show console at startup                      |\n"
  },
  {
    "path": "packages/web/src/content/docs/getting-started.mdx",
    "content": "---\ntitle: Getting started\ndescription: Install OpenTUI and create your first terminal UI\norder: 1\n---\n\n# Getting started\n\nOpenTUI is a native terminal UI core written in Zig with TypeScript bindings. The native core exposes a C ABI and can be used from any language. OpenTUI powers OpenCode in production today and will also power terminal.shop. It is an extensible core with a focus on correctness, stability, and high performance. It provides a component-based architecture with flexible layout capabilities, allowing you to create complex terminal applications.\n\n## Installation\n\nOpenTUI is currently [Bun](https://bun.sh) exclusive but Deno and Node support in-progress.\n\n```bash\nmkdir my-tui && cd my-tui\nbun init -y\nbun add @opentui/core\n```\n\n## Hello world\n\nCreate `index.ts`:\n\n```typescript\nimport { createCliRenderer, Text } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: true,\n})\n\nrenderer.root.add(\n  Text({\n    content: \"Hello, OpenTUI!\",\n    fg: \"#00FF00\",\n  }),\n)\n```\n\nRun it:\n\n```bash\nbun index.ts\n```\n\nYou should see green text. Press `Ctrl+C` to exit.\n\n## Composing components\n\nComponents nest naturally. Here's a bordered panel with content:\n\n```typescript\nimport { createCliRenderer, Box, Text } from \"@opentui/core\"\n\nconst renderer = await createCliRenderer({\n  exitOnCtrlC: true,\n})\n\nrenderer.root.add(\n  Box(\n    { borderStyle: \"rounded\", padding: 1, flexDirection: \"column\", gap: 1 },\n    Text({ content: \"Welcome\", fg: \"#FFFF00\" }),\n    Text({ content: \"Press Ctrl+C to exit\" }),\n  ),\n)\n```\n\n`Box` and `Text` are factory functions. The first argument is props; additional arguments are children.\n\n## What's next\n\n### Core concepts\n\n- [Renderer](/docs/core-concepts/renderer) - The rendering engine\n- [Layout](/docs/core-concepts/layout) - Flexbox positioning\n- [Constructs](/docs/core-concepts/constructs) - The declarative component API\n\n### Components\n\n- [Text](/docs/components/text), [Box](/docs/components/box) - Display and layout\n- [Input](/docs/components/input), [Select](/docs/components/select) - User interaction\n\n### Framework bindings\n\n- [React](/docs/bindings/react)\n- [Solid.js](/docs/bindings/solid)\n"
  },
  {
    "path": "packages/web/src/content/docs/plugins/core.mdx",
    "content": "---\ntitle: Core\ndescription: Framework-free slot hosting with BaseRenderable nodes\norder: 2\n---\n\n# Core slots\n\nThis page shows the core host API for plugin slots.\n\nUse slots when you want external modules to render `BaseRenderable` UI in host-defined layout regions without forking the app. The host keeps control of layout and slot typing; plugins only render through the APIs you expose.\n\nIf you have not read the shared model yet, start with [Plugin Slots](/docs/plugins/slots).\n\n## What core adds\n\nOn top of `createSlotRegistry`, core provides:\n\n- `createCoreSlotRegistry` — create a registry typed for `BaseRenderable` nodes\n- `registerCorePlugin` — register a plugin using the core-specific `CorePlugin` interface\n- `SlotRenderable` — a `Renderable` that mounts a slot into the renderable tree\n- `resolveCoreSlot` — resolve slot entries without mounting\n- `@opentui/core/runtime-plugin-support` / `createRuntimePlugin` — runtime module support for external plugin/module loading in Bun\n\nCore slot renderers receive both the host context and slot data: `(ctx, data) => BaseRenderable`.\n\n`createCoreSlotRegistry` accepts the same [`SlotRegistryOptions`](/docs/plugins/slots#registry-options) as `createSlotRegistry`.\n\n## Runtime-loaded external plugins\n\nIf your app loads plugin modules from disk at runtime, import this once in your app entry:\n\n```ts\nimport \"@opentui/core/runtime-plugin-support\"\n```\n\nThis installs Bun runtime support for `@opentui/core` plus default core runtime entrypoints (`@opentui/core/3d`, `@opentui/core/testing`).\n\nIf you need additional host-resolved modules, register your own plugin:\n\n```ts\nimport { plugin } from \"bun\"\nimport { createRuntimePlugin } from \"@opentui/core/runtime-plugin\"\n\nplugin(\n  createRuntimePlugin({\n    additional: {\n      \"my-runtime-module\": async () => (await import(\"./runtime-module\")) as Record<string, unknown>,\n    },\n  }),\n)\n```\n\n## `CorePlugin` interface\n\n| Field     | Type                                              | Required | Description                                                                                |\n| --------- | ------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------ |\n| `id`      | `string`                                          | yes      | Unique identifier. Duplicate ids throw.                                                    |\n| `order`   | `number`                                          | no       | Sort priority (ascending). Defaults to `0`.                                                |\n| `setup`   | `(ctx, renderer) => void`                         | no       | Called once at registration time. If it throws, the plugin is not registered.              |\n| `dispose` | `() => void`                                      | no       | Called when the plugin is unregistered or the registry is cleared.                         |\n| `slots`   | `Partial<Record<SlotName, CoreSlotContribution>>` | yes      | Each value is a renderer function or a [managed slot object](#managed-slot-contributions). |\n\nA `CoreSlotContribution` is either a plain renderer `(ctx, data) => BaseRenderable` or a `CoreManagedSlot` with lifecycle hooks.\n\n## Basic usage\n\nA `SlotRenderable` extends `Renderable`, so it can be added to any parent like a `BoxRenderable`. It resolves plugins from the registry, manages their lifecycle, and reconciles their output as its children.\n\n```typescript\nimport {\n  BoxRenderable,\n  createCliRenderer,\n  createCoreSlotRegistry,\n  registerCorePlugin,\n  SlotRenderable,\n  TextRenderable,\n} from \"@opentui/core\"\n\ntype Slots = \"statusbar\"\ntype SlotData = { label: string }\nconst context = { appName: \"core-app\", version: \"1.0.0\" }\n\nconst renderer = await createCliRenderer()\n\nconst registry = createCoreSlotRegistry<Slots, typeof context, SlotData>(renderer, context)\n\nconst unregister = registerCorePlugin(registry, {\n  id: \"clock-plugin\",\n  order: 0,\n  slots: {\n    statusbar(_ctx, data) {\n      return new TextRenderable(renderer, {\n        id: \"clock-status\",\n        content: `clock: ${data.label}`,\n      })\n    },\n  },\n})\n\nconst slot = new SlotRenderable(renderer, {\n  id: \"statusbar-slot\",\n  registry,\n  name: \"statusbar\",\n  data: { label: \"ok\" },\n  mode: \"append\",\n  width: \"100%\",\n  height: 3,\n  flexDirection: \"row\",\n  fallback: () =>\n    new TextRenderable(renderer, {\n      id: \"statusbar-fallback\",\n      content: \"fallback\",\n    }),\n})\n\nrenderer.root.add(slot)\n\n// later\nslot.mode = \"replace\"\nslot.data = { label: \"updated\" }\nslot.refresh()\nslot.destroy()\n```\n\n`registerCorePlugin` returns an unregister function (`() => void`). Calling it removes the plugin and invokes its `dispose` hook.\n\nBecause `SlotRenderable` extends `Renderable`, it accepts all standard layout options (`width`, `height`, `flexDirection`, `padding`, etc.) alongside the slot-specific options.\n\n## `SlotRenderable` options\n\n| Option                     | Type                                                                | Required | Description                                                                                                |\n| -------------------------- | ------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |\n| `id`                       | `string`                                                            | yes      | Unique renderable identifier (inherited from `RenderableOptions`)                                          |\n| `registry`                 | `CoreSlotRegistry`                                                  | yes      | The registry to read plugins from                                                                          |\n| `name`                     | slot name                                                           | yes      | Which slot to mount                                                                                        |\n| `data`                     | object                                                              | no       | Slot data passed to plugin renderers as the second argument                                                |\n| `mode`                     | `SlotMode`                                                          | no       | `\"append\"` (default), `\"replace\"`, or `\"single_winner\"`. See [slot modes](/docs/plugins/slots#slot-modes). |\n| `fallback`                 | `BaseRenderable \\| BaseRenderable[] \\| () => ...`                   | no       | Fallback nodes or a factory that creates them                                                              |\n| `pluginFailurePlaceholder` | `(failure, ctx) => BaseRenderable \\| BaseRenderable[] \\| undefined` | no       | Creates placeholder UI when a plugin throws                                                                |\n| _...layout options_        | `RenderableOptions`                                                 | no       | Standard layout props: `width`, `height`, `flexDirection`, `padding`, etc.                                 |\n\n## Instance API\n\n| Member      | Description                                                                                                                                                                                                                                                                                                                                                                                  |\n| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `mode`      | Getter/setter. Changing the mode automatically refreshes the slot.                                                                                                                                                                                                                                                                                                                           |\n| `refresh()` | Re-resolve plugins and reconcile mounted children.                                                                                                                                                                                                                                                                                                                                           |\n| `destroy()` | Inherits from `Renderable`. In addition to the standard cleanup, `SlotRenderable` unsubscribes from the registry, calls `onDeactivate` on active managed slots, then calls `onDispose` on all managed slots. Host-owned nodes (plain function contributions) are destroyed; plugin-owned nodes (managed slot contributions) are detached but left for the plugin to clean up in `onDispose`. |\n\n## `resolveCoreSlot`\n\nResolve slot entries without mounting them. Useful when you need to inspect or render plugin output manually.\n\n```typescript\nimport { resolveCoreSlot } from \"@opentui/core\"\n\nconst entries = resolveCoreSlot(registry, \"statusbar\")\n// Array<{ id: string, renderer: (ctx, data) => BaseRenderable }>\n```\n\n## Plugin failure placeholders\n\nIf a plugin throws during slot render, you can provide host-controlled fallback UI for that plugin failure:\n\n```typescript\nconst slot = new SlotRenderable(renderer, {\n  registry,\n  name: \"statusbar\",\n  fallback: () => new TextRenderable(renderer, { id: \"fallback\", content: \"fallback\" }),\n  pluginFailurePlaceholder(failure, ctx) {\n    return new TextRenderable(renderer, {\n      id: `error-${failure.pluginId}`,\n      content: `plugin error: ${failure.pluginId}`,\n    })\n  },\n})\n```\n\n## Managed slot contributions\n\nA core slot contribution can be a plain function or a managed slot object with lifecycle hooks:\n\n```typescript\nregisterCorePlugin(registry, {\n  id: \"managed-plugin\",\n  slots: {\n    statusbar: {\n      render(_ctx, data) {\n        return new TextRenderable(renderer, { id: \"managed\", content: \"managed\" })\n      },\n      onActivate(ctx) {\n        // plugin became active in this slot (visible)\n      },\n      onDeactivate(ctx) {\n        // plugin is no longer active (e.g. mode changed to single_winner and this plugin lost)\n      },\n      onDispose(ctx) {\n        // plugin is removed/disposed for this slot\n      },\n    },\n  },\n})\n```\n\n### `CoreManagedSlot` interface\n\n| Field          | Type                            | Required | Description                                                          |\n| -------------- | ------------------------------- | -------- | -------------------------------------------------------------------- |\n| `render`       | `(ctx, data) => BaseRenderable` | yes      | Creates the renderable node for this slot                            |\n| `onActivate`   | `(ctx) => void`                 | no       | Called when the plugin becomes active (visible) in the slot          |\n| `onDeactivate` | `(ctx) => void`                 | no       | Called when the plugin is no longer active (e.g. lost single_winner) |\n| `onDispose`    | `(ctx) => void`                 | no       | Called when the plugin is removed or the slot is destroyed           |\n\n### Node ownership\n\nWhen a slot contribution is a **plain function**, the host owns the returned nodes. On deactivation or disposal, the host detaches and destroys them.\n\nWhen a slot contribution is a **managed slot object**, the plugin owns the nodes. On deactivation the host detaches them but does _not_ destroy them — the plugin can reuse them if it becomes active again. On disposal, `onDispose` is called so the plugin can clean up.\n\n## Observability\n\n```typescript\nregistry.onPluginError((event) => {\n  console.error(event.pluginId, event.phase, event.source, event.error.message)\n})\n```\n\nExample: `packages/core/src/examples/core-plugin-slots-demo.ts`\n"
  },
  {
    "path": "packages/web/src/content/docs/plugins/react.mdx",
    "content": "---\ntitle: React\ndescription: React slot components backed by the shared plugin registry\norder: 3\n---\n\n# React slots\n\nThis page shows React integration for plugin slots.\n\nUse slots when you want external modules to contribute `ReactNode` UI in host-defined regions without forking your app. The host keeps ownership of layout and slot typing; plugins only see the context and props you expose.\n\nIf you have not read the shared model yet, start with [Plugin Slots](/docs/plugins/slots).\n\n## Runtime-loaded external plugins\n\nIf your app loads plugins from disk at runtime (for example `await import(fileUrl)`), add this import once in your app entry:\n\n```ts\nimport \"@opentui/react/runtime-plugin-support\"\n```\n\nThis installs Bun runtime support so external TS/TSX plugin modules resolve against the host runtime instances (`@opentui/react`, React JSX runtime modules, and core runtime modules).\n\nUse this for plugin systems in both normal Bun runs and standalone compiled executables.\n\n```ts\nimport \"@opentui/react/runtime-plugin-support\"\n\nconst mod = await import(pathToFileURL(pluginPath).href)\nregistry.register(mod.loadExternalPlugin())\n```\n\n## What React adds\n\n- `createReactSlotRegistry(renderer, context, options?)` — create a registry typed for `ReactNode`. Accepts the same [`SlotRegistryOptions`](/docs/plugins/slots#registry-options) as `createSlotRegistry`.\n- `Slot<TSlots, TContext>` — generic slot component that takes `registry` as a required prop\n- `createSlot(registry, options?)` — optional convenience helper that returns a registry-bound `<Slot />` component\n- `ReactPlugin<TSlots, TContext>` — convenience type alias for a plugin that returns `ReactNode`\n- `@opentui/react/runtime-plugin-support` — one-line runtime support for external plugin/module loading\n\nRegister plugins directly with `registry.register()` — no wrapper function is needed (unlike core's `registerCorePlugin`).\n\n## Basic usage\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createReactSlotRegistry, createRoot, Slot } from \"@opentui/react\"\n\ntype Slots = {\n  statusbar: { user: string }\n}\n\nconst context = { appName: \"react-app\", version: \"1.0.0\" }\nconst renderer = await createCliRenderer()\n\nconst registry = createReactSlotRegistry<Slots, typeof context>(renderer, context)\n\nconst unregister = registry.register({\n  id: \"clock-plugin\",\n  slots: {\n    statusbar(ctx, props) {\n      return <text>{`${ctx.appName}:${props.user}`}</text>\n    },\n  },\n})\n\nconst AppSlot = Slot<Slots, typeof context>\n\nfunction App() {\n  return (\n    <AppSlot registry={registry} name=\"statusbar\" user=\"sam\" mode=\"replace\">\n      <text>fallback-statusbar</text>\n    </AppSlot>\n  )\n}\n\ncreateRoot(renderer).render(<App />)\n```\n\n## Optional convenience helper\n\nIf you prefer not to pass `registry` each time, you can still bind one once:\n\n```tsx\nconst AppSlot = createSlot(registry)\n```\n\n## `<Slot>` props\n\n| Prop                       | Type                                       | Required | Description                                                                                                |\n| -------------------------- | ------------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------- |\n| `registry`                 | `SlotRegistry<ReactNode, Slots, Context>`  | yes      | Registry to resolve plugins from                                                                           |\n| `name`                     | `keyof Slots`                              | yes      | Which slot to render                                                                                       |\n| `mode`                     | `SlotMode`                                 | no       | `\"append\"` (default), `\"replace\"`, or `\"single_winner\"`. See [slot modes](/docs/plugins/slots#slot-modes). |\n| `pluginFailurePlaceholder` | `(failure: PluginErrorEvent) => ReactNode` | no       | Per-slot placeholder UI when a plugin throws                                                               |\n| `children`                 | `ReactNode`                                | no       | Fallback UI                                                                                                |\n| _remaining_                | `Slots[name]`                              | —        | Slot-specific props forwarded to plugin renderers                                                          |\n\n## `ReactSlotOptions` (for `createSlot`)\n\n| Option                     | Type                                       | Required | Description                                 |\n| -------------------------- | ------------------------------------------ | -------- | ------------------------------------------- |\n| `pluginFailurePlaceholder` | `(failure: PluginErrorEvent) => ReactNode` | no       | Creates placeholder UI when a plugin throws |\n\n## Plugin failure placeholders\n\n```tsx\nconst Slot = createSlot(registry, {\n  pluginFailurePlaceholder(failure) {\n    return <text>{`plugin-error:${failure.pluginId}:${failure.phase}`}</text>\n  },\n})\n```\n\nIf a plugin throws, the slot renders the placeholder. If no placeholder is provided (or it returns `null`), the slot falls back to `children`.\n\nPlugins that throw during the initial render call are caught inline. Plugins that throw during a React re-render are caught by an internal error boundary that resets on registry changes.\n\n## Observability\n\n```tsx\nregistry.onPluginError((event) => {\n  console.error(event.pluginId, event.phase, event.source, event.error.message)\n})\n```\n\nExample: `packages/react/examples/plugin-slots-errors.tsx`\n"
  },
  {
    "path": "packages/web/src/content/docs/plugins/slots.mdx",
    "content": "---\ntitle: Plugin Slots\ndescription: Shared slot registry API used by core, React, and Solid\norder: 1\n---\n\n# Plugin slots\n\nMost TUI apps start as one codebase. As features and teams grow, you may want extension points without asking users to fork the app.\n\nPlugin slots give you that. The host application defines named places in the layout (for example, a status bar, sidebar, or panel), and plugins contribute UI for those places at runtime.\n\nThe host stays in control of layout, rendering mode, and type contracts. Plugins only receive the context and slot props the host intentionally exposes.\n\nThis page describes the **shared registry API** that the core, React, and Solid bindings are built on. If you are building an app, start with the page for the binding you use:\n\n- [Core slots](/docs/plugins/core) — framework-free, `BaseRenderable` nodes\n- [React slots](/docs/plugins/react) — `ReactNode`\n- [Solid slots](/docs/plugins/solid) — `JSX.Element`\n\nIf you load plugin modules from disk at runtime, see the runtime support notes on the [React slots page](/docs/plugins/react#runtime-loaded-external-plugins) and [Solid slots page](/docs/plugins/solid#runtime-loaded-external-plugins). For custom hosts, use `@opentui/core/runtime-plugin-support` or `createRuntimePlugin` from `@opentui/core/runtime-plugin`.\n\nRead on if you need to understand the underlying model, want to build a custom binding for another framework, or need direct access to the registry API.\n\n## Concepts\n\n- **Host**: defines slot names and slot prop types.\n- **Plugin**: contributes one or more slot renderer callbacks. May include lifecycle hooks.\n- **Registry**: registers plugins and resolves contributions for a slot.\n- **Slot mode**: controls how plugin output and fallback UI combine (`append`, `replace`, or `single_winner`).\n\n## Define slots and host context\n\nSlot names map to the props each slot receives. The host context is a shared object passed to every plugin renderer.\n\n```typescript\nimport type { PluginContext } from \"@opentui/core\"\n\ntype AppSlots = {\n  statusbar: { user: string }\n  sidebar: { section: \"left\" | \"right\" }\n}\n\ninterface AppContext extends PluginContext {\n  appName: string\n  version: string\n}\n```\n\nThe context can be any object. `PluginContext` is a type alias for `object` — extending it is not required but gives your plugins a named type to reference.\n\n## Create a registry\n\n```typescript\nimport { createSlotRegistry } from \"@opentui/core\"\n\nconst context = { appName: \"my-app\", version: \"1.0.0\" }\n\nconst registry = createSlotRegistry<string, AppSlots, typeof context>(renderer, \"my-app:plugins\", context)\n```\n\nThe first type parameter (`TNode`) is the node type your framework returns — `BaseRenderable` for core, `ReactNode` for React, `JSX.Element` for Solid. The framework-specific helpers fill this in for you.\n\n`createSlotRegistry` is renderer-scoped. For a given `(renderer, key)` pair, you always get the same registry instance. The `key` parameter namespaces registries within a renderer so multiple independent registries can coexist.\n\nCalling `createSlotRegistry` again with the same `(renderer, key)` pair returns the existing registry and applies the new `options` via `configure()`. The `context` argument **must** be the same object reference — passing a different context object throws an error. This means you should create the context object once and reuse it:\n\n```typescript\n// correct — same object reference\nconst context = { appName: \"my-app\" }\nconst reg1 = createSlotRegistry(renderer, \"my-key\", context)\nconst reg2 = createSlotRegistry(renderer, \"my-key\", context) // returns reg1\n\n// throws — different object even if structurally identical\nconst reg3 = createSlotRegistry(renderer, \"my-key\", { appName: \"my-app\" })\n```\n\nWhen the renderer is destroyed, all registries scoped to it are automatically cleared and disposed.\n\nThe framework-specific helpers (`createCoreSlotRegistry`, `createReactSlotRegistry`, `createSolidSlotRegistry`) call `createSlotRegistry` internally with a fixed key. Use `createSlotRegistry` directly only if you need multiple independent registries per renderer.\n\n### Registry options\n\nAll `create*SlotRegistry` functions accept an optional `SlotRegistryOptions` object:\n\n| Option              | Type                                | Default | Description                                                 |\n| ------------------- | ----------------------------------- | ------- | ----------------------------------------------------------- |\n| `onPluginError`     | `(event: PluginErrorEvent) => void` | —       | Callback invoked on every plugin error                      |\n| `debugPluginErrors` | `boolean`                           | `false` | When `true`, errors are also logged via `console.debug`     |\n| `maxPluginErrors`   | `number`                            | `100`   | Maximum number of buffered errors before oldest are dropped |\n\n## Register plugins\n\n```typescript\nconst unregister = registry.register({\n  id: \"clock-plugin\",\n  order: 0,\n  setup(ctx, renderer) {\n    // called once on registration — initialize resources here\n  },\n  dispose() {\n    // called when the plugin is unregistered — clean up resources here\n  },\n  slots: {\n    statusbar(ctx, props) {\n      return `${ctx.appName}:${props.user}`\n    },\n  },\n})\n\n// later: remove this plugin\nunregister()\n```\n\n`register()` returns an unregister function. Calling it removes the plugin and invokes its `dispose` hook.\n\n### Plugin interface\n\n| Field     | Type                                    | Required | Description                                                                      |\n| --------- | --------------------------------------- | -------- | -------------------------------------------------------------------------------- |\n| `id`      | `string`                                | yes      | Unique identifier. Duplicate ids throw.                                          |\n| `order`   | `number`                                | no       | Sort priority (ascending). Defaults to `0`.                                      |\n| `setup`   | `(ctx, renderer) => void`               | no       | Called once at registration time. If it throws, the plugin is not registered.    |\n| `dispose` | `() => void`                            | no       | Called when the plugin is unregistered or the registry is cleared.               |\n| `slots`   | `{ [slotName]: (ctx, props) => TNode }` | yes      | Slot renderer callbacks. Each receives the host context and slot-specific props. |\n\nThe core binding extends this with [managed slot objects](/docs/plugins/core#managed-slot-contributions) that add lifecycle hooks.\n\n### Ordering\n\nPlugins are resolved in this order:\n\n1. `order` ascending (lower numbers first)\n2. Registration order (earlier registrations first)\n3. `id` lexicographic (tie-breaker)\n\n## Slot modes\n\nEvery slot mount or `<Slot>` component accepts a `mode`. The mode controls how plugin output and fallback UI combine.\n\n| Mode            | Behavior                                                                   |\n| --------------- | -------------------------------------------------------------------------- |\n| `append`        | Fallback first, then all plugin output (default)                           |\n| `replace`       | Plugin output only; fallback shown only when no plugins produce output     |\n| `single_winner` | Only the first plugin by resolved order; fallback if it produces no output |\n\n## Resolve contributions\n\n```typescript\nconst entries = registry.resolveEntries(\"statusbar\")\n// Array<{ id: string, renderer: (ctx, props) => TNode }>\n\nconst slotRenderers = registry.resolve(\"statusbar\")\n// Array<(ctx, props) => TNode>\n```\n\n`renderer` here means the plugin's **slot renderer callback**, not the `CliRenderer` instance.\n\nUse `resolveEntries` when you need plugin ids alongside the callbacks. Use `resolve` when you only need the callbacks.\n\n## Registry methods\n\n| Method                      | Description                                                                                                                 |\n| --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |\n| `register(plugin)`          | Add a plugin. Returns an unregister function.                                                                               |\n| `unregister(id)`            | Remove a plugin by id. Returns `true` if found.                                                                             |\n| `updateOrder(id, order)`    | Change a plugin's sort order. Returns `true` if found.                                                                      |\n| `clear()`                   | Remove and dispose all plugins.                                                                                             |\n| `resolve(slot)`             | Return ordered slot renderer callbacks.                                                                                     |\n| `resolveEntries(slot)`      | Return ordered `{ id, renderer }` entries.                                                                                  |\n| `subscribe(listener)`       | Listen for registration changes. Returns an unsubscribe function. Used internally by React and Solid to trigger re-renders. |\n| `configure(options)`        | Update `SlotRegistryOptions` after creation.                                                                                |\n| `onPluginError(listener)`   | Listen for plugin errors. Returns an unsubscribe function.                                                                  |\n| `getPluginErrors()`         | Return buffered `PluginErrorEvent` array.                                                                                   |\n| `clearPluginErrors()`       | Clear the error buffer.                                                                                                     |\n| `reportPluginError(report)` | Manually report a plugin error. Used by framework integrations.                                                             |\n| `renderer`                  | Getter — the `CliRenderer` this registry is scoped to.                                                                      |\n| `context`                   | Getter — the host context object.                                                                                           |\n\n## Error handling\n\nRegistries expose plugin error events:\n\n```typescript\nregistry.onPluginError((event) => {\n  console.error(event.pluginId, event.phase, event.source, event.error.message)\n})\n```\n\nYou can also read and clear buffered errors:\n\n```typescript\nconst history = registry.getPluginErrors()\nregistry.clearPluginErrors()\n```\n\n### PluginErrorReport\n\nThe `reportPluginError` method accepts a `PluginErrorReport`:\n\n| Field      | Type                  | Required | Description                                                  |\n| ---------- | --------------------- | -------- | ------------------------------------------------------------ |\n| `pluginId` | `string`              | yes      | The plugin that caused the error                             |\n| `slot`     | `string \\| undefined` | no       | The slot name, if the error is slot-specific                 |\n| `phase`    | `PluginErrorPhase`    | yes      | `\"setup\"`, `\"render\"`, `\"dispose\"`, or `\"error_placeholder\"` |\n| `source`   | `PluginErrorSource`   | no       | Defaults to `\"registry\"` if omitted                          |\n| `error`    | `unknown`             | yes      | The raw error — normalized to `Error` internally             |\n\n### PluginErrorEvent\n\n| Field       | Type                  | Description                                                  |\n| ----------- | --------------------- | ------------------------------------------------------------ |\n| `pluginId`  | `string`              | The plugin that caused the error                             |\n| `slot`      | `string \\| undefined` | The slot name, if the error is slot-specific                 |\n| `phase`     | `PluginErrorPhase`    | `\"setup\"`, `\"render\"`, `\"dispose\"`, or `\"error_placeholder\"` |\n| `source`    | `PluginErrorSource`   | `\"registry\"`, `\"core\"`, or a framework-defined source string |\n| `error`     | `Error`               | The normalized error object                                  |\n| `timestamp` | `number`              | `Date.now()` at the time of the error                        |\n\n## Next: concrete host APIs\n\nUse the shared model above with one of these host integrations:\n\n- [Core slots](/docs/plugins/core) — framework-free, returns `BaseRenderable` nodes\n- [React slots](/docs/plugins/react) — returns `ReactNode`\n- [Solid slots](/docs/plugins/solid) — returns `JSX.Element`\n"
  },
  {
    "path": "packages/web/src/content/docs/plugins/solid.mdx",
    "content": "---\ntitle: Solid\ndescription: Solid slot components backed by the shared plugin registry\norder: 4\n---\n\n# Solid slots\n\nThis page shows Solid integration for plugin slots.\n\nUse slots when you want external modules to contribute `JSX.Element` UI in host-defined regions without forking your app. The host keeps ownership of layout and slot typing; plugins only see the context and props you expose.\n\nIf you have not read the shared model yet, start with [Plugin Slots](/docs/plugins/slots).\n\n## Runtime-loaded external plugins\n\nIf your app loads plugins from disk at runtime (for example `await import(fileUrl)`), add this import once in your app entry:\n\n```ts\nimport \"@opentui/solid/runtime-plugin-support\"\n```\n\nThis is a Bun drop-in that installs runtime transform support so external TS/TSX plugin modules use the same runtime instances as the host app (`@opentui/solid`, `@opentui/core`, `@opentui/core/3d`, `@opentui/core/testing`, `solid-js`, and `solid-js/store`).\n\nUse this for plugin systems in both normal Bun runs and standalone compiled executables.\n\n```ts\nimport \"@opentui/solid/runtime-plugin-support\"\n\nconst mod = await import(pathToFileURL(pluginPath).href)\nregistry.register(mod.loadExternalPlugin())\n```\n\n## What Solid adds\n\n- `createSolidSlotRegistry(renderer, context, options?)` — create a registry typed for `JSX.Element`. Accepts the same [`SlotRegistryOptions`](/docs/plugins/slots#registry-options) as `createSlotRegistry`.\n- `Slot<TSlots, TContext>` — generic slot component that takes `registry` as a required prop\n- `createSlot(registry, options?)` — optional convenience helper that returns a registry-bound `<Slot />` component\n- `SolidPlugin<TSlots, TContext>` — convenience type alias for a plugin that returns `JSX.Element`\n- `@opentui/solid/runtime-plugin-support` — one-line runtime support for external plugin/module loading\n\nRegister plugins directly with `registry.register()` — no wrapper function is needed (unlike core's `registerCorePlugin`).\n\n## Basic usage\n\n```tsx\nimport { createCliRenderer } from \"@opentui/core\"\nimport { createSolidSlotRegistry, Slot, render } from \"@opentui/solid\"\n\ntype Slots = {\n  statusbar: { user: string }\n}\n\nconst context = { appName: \"solid-app\", version: \"1.0.0\" }\nconst renderer = await createCliRenderer()\n\nconst registry = createSolidSlotRegistry<Slots, typeof context>(renderer, context)\n\nconst unregister = registry.register({\n  id: \"clock-plugin\",\n  slots: {\n    statusbar(ctx, props) {\n      return <text>{`${ctx.appName}:${props.user}`}</text>\n    },\n  },\n})\n\nconst AppSlot = Slot<Slots, typeof context>\n\nconst App = () => (\n  <AppSlot registry={registry} name=\"statusbar\" user=\"sam\" mode=\"replace\">\n    <text>fallback-statusbar</text>\n  </AppSlot>\n)\n\nrender(() => <App />, renderer)\n```\n\n## Optional convenience helper\n\nIf you prefer not to pass `registry` each time, you can still bind one once:\n\n```tsx\nconst AppSlot = createSlot(registry)\n```\n\n## `<Slot>` props\n\n| Prop                       | Type                                         | Required | Description                                                                                                |\n| -------------------------- | -------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |\n| `registry`                 | `SlotRegistry<JSX.Element, Slots, Context>`  | yes      | Registry to resolve plugins from                                                                           |\n| `name`                     | `keyof Slots`                                | yes      | Which slot to render                                                                                       |\n| `mode`                     | `SlotMode`                                   | no       | `\"append\"` (default), `\"replace\"`, or `\"single_winner\"`. See [slot modes](/docs/plugins/slots#slot-modes). |\n| `pluginFailurePlaceholder` | `(failure: PluginErrorEvent) => JSX.Element` | no       | Per-slot placeholder UI when a plugin throws                                                               |\n| `children`                 | `JSX.Element`                                | no       | Fallback UI                                                                                                |\n| _remaining_                | `Slots[name]`                                | —        | Slot-specific props forwarded to plugin renderers                                                          |\n\n## `SolidSlotOptions` (for `createSlot`)\n\n| Option                     | Type                                         | Required | Description                                 |\n| -------------------------- | -------------------------------------------- | -------- | ------------------------------------------- |\n| `pluginFailurePlaceholder` | `(failure: PluginErrorEvent) => JSX.Element` | no       | Creates placeholder UI when a plugin throws |\n\n## Plugin failure placeholders\n\n```tsx\nconst Slot = createSlot(registry, {\n  pluginFailurePlaceholder(failure) {\n    return <text>{`plugin-error:${failure.pluginId}:${failure.phase}`}</text>\n  },\n})\n```\n\nIf a plugin throws, the slot renders the placeholder. If no placeholder is provided (or it returns `null`), the slot falls back to `children`.\n\nPlugins that throw during the initial render call are caught inline. Plugins that throw during a Solid re-render are caught by an internal `<ErrorBoundary>` that reports the error to the registry.\n\n## Observability\n\n```tsx\nregistry.onPluginError((event) => {\n  console.error(event.pluginId, event.phase, event.source, event.error.message)\n})\n```\n\nExample: `packages/solid/examples/components/plugin-slots-demo.tsx`\n"
  },
  {
    "path": "packages/web/src/content/docs/reference/env-vars.mdx",
    "content": "---\ntitle: Environment variables\ndescription: Runtime configuration flags for OpenTUI\norder: 1\n---\n\n# Environment variables\n\nOpenTUI reads environment variables at runtime. Bun loads `.env` automatically, so you can set these in your shell or in a `.env` file.\n\n## Variables\n\n| Variable                       | Type      | Default | Description                                                |\n| ------------------------------ | --------- | ------- | ---------------------------------------------------------- |\n| `OTUI_TS_STYLE_WARN`           | `string`  | `false` | Enable warnings for missing syntax styles                  |\n| `OTUI_TREE_SITTER_WORKER_PATH` | `string`  | `\"\"`    | Path to the Tree-sitter worker                             |\n| `XDG_CONFIG_HOME`              | `string`  | `\"\"`    | Base directory for user-specific configuration files       |\n| `XDG_DATA_HOME`                | `string`  | `\"\"`    | Base directory for user-specific data files                |\n| `OTUI_DEBUG_FFI`               | `boolean` | `false` | Enable debug logging for the FFI bindings                  |\n| `OTUI_SHOW_STATS`              | `boolean` | `false` | Show the debug overlay at startup                          |\n| `OTUI_TRACE_FFI`               | `boolean` | `false` | Enable tracing for the FFI bindings                        |\n| `OPENTUI_FORCE_WCWIDTH`        | `boolean` | `false` | Use wcwidth for character width calculations               |\n| `OPENTUI_FORCE_UNICODE`        | `boolean` | `false` | Force Mode 2026 Unicode support in terminal capabilities   |\n| `OPENTUI_GRAPHICS`             | `boolean` | `true`  | Enable Kitty graphics protocol detection                   |\n| `OPENTUI_FORCE_NOZWJ`          | `boolean` | `false` | Use no_zwj width method (Unicode without ZWJ joining)      |\n| `OPENTUI_FORCE_EXPLICIT_WIDTH` | `string`  | -       | Force explicit width detection (`true`/`1` or `false`/`0`) |\n| `OTUI_USE_CONSOLE`             | `boolean` | `true`  | Enable or disable the built-in console capture             |\n| `SHOW_CONSOLE`                 | `boolean` | `false` | Show the console overlay at startup                        |\n| `OTUI_DUMP_CAPTURES`           | `boolean` | `false` | Dump captured output when the renderer exits               |\n| `OTUI_NO_NATIVE_RENDER`        | `boolean` | `false` | Disable native rendering (debug only)                      |\n| `OTUI_USE_ALTERNATE_SCREEN`    | `boolean` | `true`  | Use the terminal alternate screen buffer                   |\n| `OTUI_OVERRIDE_STDOUT`         | `boolean` | `true`  | Override the stdout stream (debug only)                    |\n| `OTUI_DEBUG`                   | `boolean` | `false` | Enable debug mode to capture raw input                     |\n\n## Notes\n\n- `OPENTUI_FORCE_EXPLICIT_WIDTH=false` skips OSC 66 queries on older terminals.\n"
  },
  {
    "path": "packages/web/src/content/docs/reference/tree-sitter.mdx",
    "content": "---\ntitle: Tree-sitter\ndescription: Add parsers and syntax highlighting with Tree-sitter\norder: 2\n---\n\n# Tree-sitter\n\nOpenTUI integrates Tree-sitter for fast, accurate syntax highlighting. You can register parsers globally or per client.\n\n## Add parsers globally\n\nUse `addDefaultParsers()` before creating clients:\n\n```typescript\nimport { addDefaultParsers, getTreeSitterClient } from \"@opentui/core\"\n\naddDefaultParsers([\n  {\n    filetype: \"python\",\n    wasm: \"https://github.com/tree-sitter/tree-sitter-python/releases/download/v0.23.6/tree-sitter-python.wasm\",\n    queries: {\n      highlights: [\"https://raw.githubusercontent.com/tree-sitter/tree-sitter-python/master/queries/highlights.scm\"],\n    },\n  },\n])\n\nconst client = getTreeSitterClient()\nawait client.initialize()\n```\n\n## Add parsers per client\n\n```typescript\nimport { TreeSitterClient } from \"@opentui/core\"\n\nconst client = new TreeSitterClient({ dataPath: \"./cache\" })\nawait client.initialize()\n\nclient.addFiletypeParser({\n  filetype: \"rust\",\n  wasm: \"https://github.com/tree-sitter/tree-sitter-rust/releases/download/v0.23.2/tree-sitter-rust.wasm\",\n  queries: {\n    highlights: [\"https://raw.githubusercontent.com/tree-sitter/tree-sitter-rust/master/queries/highlights.scm\"],\n  },\n})\n```\n\n## Parser configuration\n\n```typescript\ninterface FiletypeParserOptions {\n  filetype: string\n  aliases?: string[]\n  wasm: string\n  queries: {\n    highlights: string[]\n    injections?: string[]\n  }\n  injectionMapping?: {\n    nodeTypes?: Record<string, string>\n    infoStringMap?: Record<string, string>\n  }\n}\n```\n\n`aliases` maps additional filetype ids to the same parser assets.\n\n## Language injections\n\nUse `queries.injections` to highlight embedded languages.\n\n- `injectionMapping.nodeTypes` maps injected node types to filetype ids.\n- `injectionMapping.infoStringMap` maps code fence language labels to filetype ids.\n\n```typescript\nclient.addFiletypeParser({\n  filetype: \"markdown\",\n  wasm: \"https://github.com/tree-sitter-grammars/tree-sitter-markdown/releases/download/v0.5.1/tree-sitter-markdown.wasm\",\n  queries: {\n    highlights: [\"./assets/markdown/highlights.scm\"],\n    injections: [\n      \"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/markdown/injections.scm\",\n    ],\n  },\n  injectionMapping: {\n    nodeTypes: {\n      inline: \"markdown_inline\",\n      pipe_table_cell: \"markdown_inline\",\n    },\n    infoStringMap: {\n      js: \"javascript\",\n      jsx: \"javascriptreact\",\n      ts: \"typescript\",\n      tsx: \"typescriptreact\",\n    },\n  },\n})\n```\n\nIf `infoStringMap` has no match, the fence language label is used as the filetype id.\n\n## Use local files\n\n```typescript\nimport pythonWasm from \"./parsers/tree-sitter-python.wasm\" with { type: \"file\" }\nimport pythonHighlights from \"./queries/python/highlights.scm\" with { type: \"file\" }\n\naddDefaultParsers([\n  {\n    filetype: \"python\",\n    wasm: pythonWasm,\n    queries: {\n      highlights: [pythonHighlights],\n    },\n  },\n])\n```\n\n## Automated asset management\n\nUse the `updateAssets` utility to download parsers and generate imports.\n\n### CLI usage\n\n```json\n{\n  \"scripts\": {\n    \"prebuild\": \"bun node_modules/@opentui/core/lib/tree-sitter/assets/update.ts --config ./parsers-config.json --assets ./src/parsers --output ./src/parsers.ts\"\n  }\n}\n```\n\n### Programmatic usage\n\n```typescript\nimport { updateAssets } from \"@opentui/core\"\n\nawait updateAssets({\n  configPath: \"./parsers-config.json\",\n  assetsDir: \"./src/parsers\",\n  outputPath: \"./src/parsers.ts\",\n})\n```\n\n## Using with CodeRenderable\n\n```typescript\nimport { CodeRenderable, getTreeSitterClient } from \"@opentui/core\"\n\nconst client = getTreeSitterClient()\nawait client.initialize()\n\nconst code = new CodeRenderable(renderer, {\n  id: \"code\",\n  content: \"const x = 1\",\n  filetype: \"typescript\",\n  syntaxStyle,\n  treeSitterClient: client,\n})\n```\n\n## Caching\n\nParser and query files are cached in the client `dataPath`. Set a custom cache directory:\n\n```typescript\nconst client = new TreeSitterClient({\n  dataPath: \"./my-cache\",\n})\n```\n\n## File type resolution\n\n```typescript\nimport { pathToFiletype, extToFiletype, infoStringToFiletype } from \"@opentui/core\"\n\nconst ft1 = pathToFiletype(\"src/main.rs\")\nconst ft2 = extToFiletype(\"ts\")\nconst ft3 = infoStringToFiletype(\"TSX title=Button.tsx\")\n```\n\n`infoStringToFiletype()` is used for fenced markdown code blocks.\n\nYou can extend or override mappings at runtime:\n\n```typescript\nimport { extensionToFiletype, basenameToFiletype } from \"@opentui/core\"\n\nextensionToFiletype.set(\"templ\", \"html\")\nbasenameToFiletype.set(\"mytoolrc\", \"yaml\")\n```\n"
  },
  {
    "path": "packages/web/src/layouts/Base.astro",
    "content": "---\ninterface Props {\n  title: string\n  description?: string\n}\n\nconst { \n  title, \n  description = \"OpenTUI is a TypeScript library on a native Zig core for building terminal user interfaces (TUIs)\"\n} = Astro.props\n---\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"description\" content={description} />\n    <title>{title}</title>\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Inter:opsz,wght@14..32,100..900&display=swap\" rel=\"stylesheet\" />\n  </head>\n  <body>\n    <slot />\n  </body>\n</html>\n\n<style is:global>\n  @import \"../styles/global.css\";\n</style>\n"
  },
  {
    "path": "packages/web/src/layouts/Docs.astro",
    "content": "---\nimport Base from \"./Base.astro\"\n\ninterface Props {\n  title: string\n  description?: string\n}\n\nconst { title, description } = Astro.props\n\nconst currentPath = Astro.url.pathname\nconst isActive = (path: string) => currentPath === path || currentPath === `${path}/`\n---\n\n<Base title={title} description={description}>\n  <div class=\"docs-page\">\n    <div class=\"docs-container\">\n      <!-- Header -->\n      <header class=\"header\">\n        <div class=\"header-content\">\n          <a href=\"/\" class=\"logo\">\n            <svg class=\"logo-icon\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n              <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" fill=\"currentColor\" />\n              <rect x=\"6\" y=\"8\" width=\"3\" height=\"8\" fill=\"hsl(0, 20%, 99%)\" />\n            </svg>\n            OpenTUI\n          </a>\n          <button\n            class=\"mobile-menu-btn\"\n            aria-label=\"Open navigation menu\"\n            aria-expanded=\"false\"\n            aria-controls=\"mobile-nav\"\n          >\n            <svg class=\"menu-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <rect x=\"5\" y=\"8\" width=\"14\" height=\"2\"></rect>\n              <rect x=\"5\" y=\"14\" width=\"14\" height=\"2\"></rect>\n            </svg>\n            <svg class=\"close-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <rect x=\"5\" y=\"11\" width=\"14\" height=\"2\" transform=\"rotate(45 12 12)\"></rect>\n              <rect x=\"5\" y=\"11\" width=\"14\" height=\"2\" transform=\"rotate(-45 12 12)\"></rect>\n            </svg>\n          </button>\n          <nav class=\"nav\">\n            <a href=\"https://github.com/anomalyco/opentui\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a>\n            <a href=\"/docs/getting-started\">Docs</a>\n          </nav>\n        </div>\n      </header>\n\n      <main class=\"main\">\n        <div class=\"mobile-nav-overlay\" aria-hidden=\"true\"></div>\n        <div class=\"docs-layout\">\n          <aside class=\"sidebar\" id=\"mobile-nav\">\n            <div class=\"sidebar-section\">\n              <div class=\"sidebar-title\">Getting Started</div>\n              <ul class=\"sidebar-links\">\n                <li><a href=\"/docs/getting-started\" class:list={{ active: isActive(\"/docs/getting-started\") }}>Introduction</a></li>\n              </ul>\n            </div>\n\n            <div class=\"sidebar-section\">\n              <div class=\"sidebar-title\">Core Concepts</div>\n              <ul class=\"sidebar-links\">\n                <li><a href=\"/docs/core-concepts/renderer\" class:list={{ active: isActive(\"/docs/core-concepts/renderer\") }}>Renderer</a></li>\n                <li><a href=\"/docs/core-concepts/renderables\" class:list={{ active: isActive(\"/docs/core-concepts/renderables\") }}>Renderables</a></li>\n                <li><a href=\"/docs/core-concepts/constructs\" class:list={{ active: isActive(\"/docs/core-concepts/constructs\") }}>Constructs</a></li>\n                <li><a href=\"/docs/core-concepts/renderables-vs-constructs\" class:list={{ active: isActive(\"/docs/core-concepts/renderables-vs-constructs\") }}>Renderables vs Constructs</a></li>\n                <li><a href=\"/docs/core-concepts/layout\" class:list={{ active: isActive(\"/docs/core-concepts/layout\") }}>Layout</a></li>\n                <li><a href=\"/docs/core-concepts/keyboard\" class:list={{ active: isActive(\"/docs/core-concepts/keyboard\") }}>Keyboard</a></li>\n                <li><a href=\"/docs/core-concepts/console\" class:list={{ active: isActive(\"/docs/core-concepts/console\") }}>Console</a></li>\n                <li><a href=\"/docs/core-concepts/colors\" class:list={{ active: isActive(\"/docs/core-concepts/colors\") }}>Colors</a></li>\n                <li><a href=\"/docs/core-concepts/lifecycle\" class:list={{ active: isActive(\"/docs/core-concepts/lifecycle\") }}>Lifecycle</a></li>\n              </ul>\n            </div>\n\n            <div class=\"sidebar-section\">\n              <div class=\"sidebar-title\">Plugin API</div>\n              <ul class=\"sidebar-links\">\n                <li><a href=\"/docs/plugins/slots\" class:list={{ active: isActive(\"/docs/plugins/slots\") }}>Overview</a></li>\n                <li><a href=\"/docs/plugins/core\" class:list={{ active: isActive(\"/docs/plugins/core\") }}>Core</a></li>\n                <li><a href=\"/docs/plugins/react\" class:list={{ active: isActive(\"/docs/plugins/react\") }}>React</a></li>\n                <li><a href=\"/docs/plugins/solid\" class:list={{ active: isActive(\"/docs/plugins/solid\") }}>Solid</a></li>\n              </ul>\n            </div>\n\n            <div class=\"sidebar-section\">\n              <div class=\"sidebar-title\">Components</div>\n              <ul class=\"sidebar-links\">\n                <li><a href=\"/docs/components/text\" class:list={{ active: isActive(\"/docs/components/text\") }}>Text</a></li>\n                <li><a href=\"/docs/components/box\" class:list={{ active: isActive(\"/docs/components/box\") }}>Box</a></li>\n                <li><a href=\"/docs/components/input\" class:list={{ active: isActive(\"/docs/components/input\") }}>Input</a></li>\n                <li><a href=\"/docs/components/textarea\" class:list={{ active: isActive(\"/docs/components/textarea\") }}>Textarea</a></li>\n                <li><a href=\"/docs/components/select\" class:list={{ active: isActive(\"/docs/components/select\") }}>Select</a></li>\n                <li><a href=\"/docs/components/tab-select\" class:list={{ active: isActive(\"/docs/components/tab-select\") }}>TabSelect</a></li>\n                <li><a href=\"/docs/components/scrollbox\" class:list={{ active: isActive(\"/docs/components/scrollbox\") }}>ScrollBox</a></li>\n                <li><a href=\"/docs/components/scrollbar\" class:list={{ active: isActive(\"/docs/components/scrollbar\") }}>ScrollBar</a></li>\n                <li><a href=\"/docs/components/slider\" class:list={{ active: isActive(\"/docs/components/slider\") }}>Slider</a></li>\n                <li><a href=\"/docs/components/code\" class:list={{ active: isActive(\"/docs/components/code\") }}>Code</a></li>\n                <li><a href=\"/docs/components/markdown\" class:list={{ active: isActive(\"/docs/components/markdown\") }}>Markdown</a></li>\n                <li><a href=\"/docs/components/line-number\" class:list={{ active: isActive(\"/docs/components/line-number\") }}>Line numbers</a></li>\n                <li><a href=\"/docs/components/frame-buffer\" class:list={{ active: isActive(\"/docs/components/frame-buffer\") }}>FrameBuffer</a></li>\n                <li><a href=\"/docs/components/ascii-font\" class:list={{ active: isActive(\"/docs/components/ascii-font\") }}>ASCIIFont</a></li>\n                <li><a href=\"/docs/components/diff\" class:list={{ active: isActive(\"/docs/components/diff\") }}>Diff</a></li>\n              </ul>\n            </div>\n\n            <div class=\"sidebar-section\">\n              <div class=\"sidebar-title\">Bindings</div>\n              <ul class=\"sidebar-links\">\n                <li><a href=\"/docs/bindings/solid\" class:list={{ active: isActive(\"/docs/bindings/solid\") }}>Solid.js</a></li>\n                <li><a href=\"/docs/bindings/react\" class:list={{ active: isActive(\"/docs/bindings/react\") }}>React</a></li>\n              </ul>\n            </div>\n\n            <div class=\"sidebar-section\">\n              <div class=\"sidebar-title\">Reference</div>\n              <ul class=\"sidebar-links\">\n                <li><a href=\"/docs/reference/env-vars\" class:list={{ active: isActive(\"/docs/reference/env-vars\") }}>Environment variables</a></li>\n                <li><a href=\"/docs/reference/tree-sitter\" class:list={{ active: isActive(\"/docs/reference/tree-sitter\") }}>Tree-sitter</a></li>\n              </ul>\n            </div>\n          </aside>\n\n          <article class=\"content\">\n            <slot />\n          </article>\n        </div>\n      </main>\n\n      <footer class=\"footer\">\n        <div class=\"footer-content\">\n          <span>OpenTUI - Terminal UIs</span>\n          <a href=\"https://github.com/anomalyco/opentui\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a>\n        </div>\n      </footer>\n    </div>\n  </div>\n\n  <script>\n    // Mobile menu toggle\n    const menuBtn = document.querySelector('.mobile-menu-btn')\n    const sidebar = document.querySelector('.sidebar')\n    const overlay = document.querySelector('.mobile-nav-overlay')\n\n    function openMenu() {\n      document.body.classList.add('mobile-nav-open')\n      menuBtn?.setAttribute('aria-expanded', 'true')\n      menuBtn?.setAttribute('aria-label', 'Close navigation menu')\n    }\n\n    function closeMenu() {\n      document.body.classList.remove('mobile-nav-open')\n      menuBtn?.setAttribute('aria-expanded', 'false')\n      menuBtn?.setAttribute('aria-label', 'Open navigation menu')\n    }\n\n    menuBtn?.addEventListener('click', () => {\n      const isOpen = document.body.classList.contains('mobile-nav-open')\n      if (isOpen) {\n        closeMenu()\n      } else {\n        openMenu()\n      }\n    })\n\n    overlay?.addEventListener('click', closeMenu)\n\n    document.addEventListener('keydown', (e) => {\n      if (e.key === 'Escape' && document.body.classList.contains('mobile-nav-open')) {\n        closeMenu()\n      }\n    })\n\n    // Close menu when clicking a link (for navigation)\n    sidebar?.querySelectorAll('a').forEach((link) => {\n      link.addEventListener('click', closeMenu)\n    })\n\n    // Wrap tables in scrollable containers\n    document.querySelectorAll('.content table').forEach((table) => {\n      const wrapper = document.createElement('div')\n      wrapper.className = 'table-wrapper'\n      table.parentNode?.insertBefore(wrapper, table)\n      wrapper.appendChild(table)\n    })\n\n    document.querySelectorAll('pre[data-code]').forEach((pre) => {\n      const btn = document.createElement('button')\n      btn.className = 'copy-btn'\n      btn.setAttribute('aria-label', 'Copy code')\n      btn.innerHTML = `\n        <svg class=\"copy-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n          <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n          <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n        </svg>\n        <svg class=\"check-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n          <polyline points=\"20 6 9 17 4 12\"></polyline>\n        </svg>\n      `\n      pre.appendChild(btn)\n\n      btn.addEventListener('click', async () => {\n        const code = pre.dataset.code || ''\n        await navigator.clipboard.writeText(code)\n        btn.classList.add('copied')\n        setTimeout(() => btn.classList.remove('copied'), 1500)\n      })\n    })\n  </script>\n</Base>\n"
  },
  {
    "path": "packages/web/src/pages/404.astro",
    "content": "---\nimport Base from \"../layouts/Base.astro\"\n---\n\n<Base title=\"404 - Page Not Found | OpenTUI\">\n  <main class=\"landing\">\n    <div class=\"landing-container\">\n      <!-- Header -->\n      <header class=\"landing-header\">\n        <a href=\"/\" class=\"landing-logo\">\n          <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n            <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" fill=\"currentColor\" />\n            <rect x=\"6\" y=\"8\" width=\"3\" height=\"8\" fill=\"hsl(0, 20%, 99%)\" />\n          </svg>\n          OpenTUI\n        </a>\n        <nav class=\"landing-nav\">\n          <a href=\"/docs/getting-started\">Docs</a>\n          <a href=\"https://github.com/anomalyco/opentui\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a>\n        </nav>\n      </header>\n\n      <!-- Content -->\n      <div class=\"landing-content\">\n        <section class=\"error-section\">\n          <div class=\"error-code\">404</div>\n          <h1>Page not found</h1>\n          <p>The page you're looking for doesn't exist or has been moved.</p>\n          <a href=\"/\" class=\"btn-dark\">\n            <span>Back to home</span>\n            <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M6.5 12L17 12M13 16.5L17.5 12L13 7.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\"/>\n            </svg>\n          </a>\n        </section>\n      </div>\n\n      <!-- Footer -->\n      <footer class=\"landing-footer\">\n        <div class=\"landing-footer-cell\"><a href=\"https://github.com/anomalyco/opentui\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a></div>\n        <div class=\"landing-footer-cell\"><a href=\"/docs/getting-started\">Docs</a></div>\n      </footer>\n    </div>\n  </main>\n</Base>\n\n<style>\n  .error-section {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n    padding: var(--space-4xl) var(--section-padding-x);\n    min-height: 50vh;\n  }\n\n  .error-code {\n    font-family: var(--font-mono);\n    font-size: 120px;\n    font-weight: 700;\n    line-height: 1;\n    color: var(--color-text-strong);\n    margin-bottom: var(--space-md);\n  }\n\n  .error-section h1 {\n    font-family: var(--font-mono);\n    font-size: 24px;\n    font-weight: 500;\n    color: var(--color-text-strong);\n    margin-bottom: var(--space-sm);\n  }\n\n  .error-section p {\n    font-size: 16px;\n    color: var(--color-text);\n    margin-bottom: var(--space-xl);\n  }\n\n  @media (max-width: 600px) {\n    .error-code {\n      font-size: 80px;\n    }\n\n    .error-section h1 {\n      font-size: 20px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "packages/web/src/pages/docs/[...slug].astro",
    "content": "---\nimport { getCollection } from \"astro:content\"\nimport Docs from \"../../layouts/Docs.astro\"\n\nexport async function getStaticPaths() {\n  const docs = await getCollection(\"docs\")\n  return docs.map((entry) => ({\n    params: { slug: entry.slug },\n    props: { entry },\n  }))\n}\n\nconst { entry } = Astro.props\nconst { Content } = await entry.render()\n---\n\n<Docs title={entry.data.title} description={entry.data.description}>\n  <Content />\n</Docs>\n"
  },
  {
    "path": "packages/web/src/pages/index.astro",
    "content": "---\nimport Base from \"../layouts/Base.astro\"\nimport TuiSurface from \"../components/TuiSurface.astro\"\n\nconst installCommands = {\n  create: { cmd: \"bun create\", pkg: \"tui\" },\n  manual: { cmd: \"bun add\", pkg: \"@opentui/core\" },\n  skill: { cmd: \"npx skills add\", pkg: \"msmps/opentui-skill\" },\n}\n\nconst features = [\n  {\n    type: \"layout\",\n    title: \"Flexbox layout\",\n    description: \"Yoga-powered layout engine with familiar CSS-like positioning and sizing\"\n  },\n  {\n    type: \"syntax\",\n    title: \"Syntax highlighting\",\n    description: \"Built-in tree-sitter integration for beautiful code rendering\"\n  },\n  {\n    type: \"components\",\n    title: \"Rich components\",\n    description: \"Text, Box, Input, Select, ScrollBox, Code, Diff, and more\"\n  },\n  {\n    type: \"keyboard\",\n    title: \"Keyboard handling\",\n    description: \"Focus management and input handling built in\"\n  },\n  {\n    type: \"react\",\n    title: \"React and Solid.js\",\n    description: \"First-class bindings for building UIs with your favorite framework\"\n  },\n  {\n    type: \"animations\",\n    title: \"Animations\",\n    description: \"Timeline API for smooth, performant terminal animations\"\n  },\n]\n---\n\n<Base title=\"OpenTUI - Terminal UIs\">\n  <main class=\"landing\">\n    <div class=\"landing-container\">\n      <!-- Header -->\n      <header class=\"landing-header\">\n        <a href=\"/\" class=\"landing-logo\">\n          <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n            <rect x=\"2\" y=\"3\" width=\"20\" height=\"18\" fill=\"currentColor\" />\n            <rect x=\"6\" y=\"8\" width=\"3\" height=\"8\" fill=\"hsl(0, 20%, 99%)\" />\n          </svg>\n          OpenTUI\n        </a>\n        <button\n          class=\"mobile-menu-btn\"\n          aria-label=\"Open navigation menu\"\n          aria-expanded=\"false\"\n          aria-controls=\"landing-mobile-nav\"\n        >\n          <svg class=\"menu-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n            <rect x=\"5\" y=\"8\" width=\"14\" height=\"2\"></rect>\n            <rect x=\"5\" y=\"14\" width=\"14\" height=\"2\"></rect>\n          </svg>\n          <svg class=\"close-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n            <rect x=\"5\" y=\"11\" width=\"14\" height=\"2\" transform=\"rotate(45 12 12)\"></rect>\n            <rect x=\"5\" y=\"11\" width=\"14\" height=\"2\" transform=\"rotate(-45 12 12)\"></rect>\n          </svg>\n        </button>\n        <nav class=\"landing-nav\">\n          <a href=\"https://github.com/anomalyco/opentui\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a>\n          <a href=\"/docs/getting-started\">Docs</a>\n        </nav>\n      </header>\n\n      <!-- Mobile nav overlay -->\n      <nav class=\"mobile-nav\" id=\"landing-mobile-nav\">\n        <a href=\"https://github.com/anomalyco/opentui\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a>\n        <a href=\"/docs/getting-started\">Docs</a>\n      </nav>\n\n      <!-- Content -->\n      <div class=\"landing-content\">\n        <!-- Hero -->\n        <section class=\"hero\">\n          <div class=\"hero-content\">\n            <div class=\"hero-copy\">\n              <h1>Terminal UIs on a Native Zig Core</h1>\n              <p class=\"hero-subtitle\">\n                Build rich, interactive terminal interfaces with TypeScript bindings, first-class React/Solid support, and a C ABI for any language.\n              </p>\n            </div>\n\n            <!-- Install Tabs -->\n            <div class=\"install-section\" aria-label=\"Install options\">\n              <div class=\"install-tabs\" role=\"tablist\">\n                {Object.keys(installCommands).map((key, i) => (\n                  <button \n                    role=\"tab\" \n                    class=\"install-tab\" \n                    data-tab={key}\n                    aria-selected={i === 0 ? \"true\" : \"false\"}\n                  >\n                    {key}\n                  </button>\n                ))}\n              </div>\n              <div class=\"install-panels\">\n                <button class=\"install-command\" id=\"install-display\" data-command=\"bun create tui\">\n                  <code>\n                    <span class=\"cmd\">bun create </span><span class=\"highlight\">tui</span>\n                  </code>\n                  <span class=\"copy-btn\">\n                    <svg class=\"copy-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n                      <path d=\"M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z\" stroke-linecap=\"square\"/>\n                    </svg>\n                    <svg class=\"check-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                      <path d=\"M2.75 15.0938L9 20.25L21.25 3.75\" stroke-linecap=\"square\"/>\n                    </svg>\n                  </span>\n                </button>\n              </div>\n            </div>\n          </div>\n\n        </section>\n\n        <!-- Code Example -->\n        <section class=\"code-section\">\n          <div class=\"code-editor\">\n            <div class=\"code-editor-header\">\n              <div class=\"code-editor-dots\">\n                <span></span><span></span><span></span>\n              </div>\n              <div class=\"code-editor-tabs\" role=\"tablist\">\n                <button role=\"tab\" class=\"code-editor-tab\" data-tab=\"core\" aria-selected=\"true\">\n                  <svg class=\"code-editor-tab-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                    <path d=\"M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z\"/>\n                    <polyline points=\"13 2 13 9 20 9\"/>\n                  </svg>\n                  hello.ts\n                </button>\n                <button role=\"tab\" class=\"code-editor-tab\" data-tab=\"solid\" aria-selected=\"false\">\n                  <svg class=\"code-editor-tab-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                    <path d=\"M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z\"/>\n                    <polyline points=\"13 2 13 9 20 9\"/>\n                  </svg>\n                  input.ts\n                </button>\n                <button role=\"tab\" class=\"code-editor-tab\" data-tab=\"layout\" aria-selected=\"false\">\n                  <svg class=\"code-editor-tab-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                    <path d=\"M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z\"/>\n                    <polyline points=\"13 2 13 9 20 9\"/>\n                  </svg>\n                  layout.ts\n                </button>\n              </div>\n              <button class=\"code-editor-copy\" id=\"code-copy-btn\" aria-label=\"Copy code\">\n                <svg class=\"copy-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n                  <path d=\"M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z\" stroke-linecap=\"square\"/>\n                </svg>\n                <svg class=\"check-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n                  <path d=\"M2.75 15.0938L9 20.25L21.25 3.75\" stroke-linecap=\"square\"/>\n                </svg>\n              </button>\n            </div>\n            \n            <!-- Core API Example -->\n            <div class=\"code-editor-body\" data-panel=\"core\" data-active=\"true\">\n              <div class=\"code-editor-lines\">\n                <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span>\n              </div>\n              <pre><code><span class=\"keyword\">import</span> {\"{\"} createCliRenderer, Text {\"}\"} <span class=\"keyword\">from</span> <span class=\"string\">\"@opentui/core\"</span>\n\n<span class=\"keyword\">const</span> renderer <span class=\"operator\">=</span> <span class=\"keyword\">await</span> <span class=\"function\">createCliRenderer</span>()\n\nrenderer.root.<span class=\"function\">add</span>(\n  <span class=\"function\">Text</span>({\"{\"}\n    <span class=\"property\">content</span><span class=\"operator\">:</span> <span class=\"string\">\"Hello, OpenTUI!\"</span>,\n    <span class=\"property\">fg</span><span class=\"operator\">:</span> <span class=\"string\">\"#00FF00\"</span>,\n  {\"}\"})\n)</code></pre>\n            </div>\n            \n            <!-- Input Example -->\n            <div class=\"code-editor-body\" data-panel=\"solid\" data-active=\"false\">\n              <div class=\"code-editor-lines\">\n                <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span><span>11</span>\n              </div>\n              <pre><code><span class=\"keyword\">import</span> {\"{\"} createCliRenderer, Input {\"}\"} <span class=\"keyword\">from</span> <span class=\"string\">\"@opentui/core\"</span>\n\n<span class=\"keyword\">const</span> renderer <span class=\"operator\">=</span> <span class=\"keyword\">await</span> <span class=\"function\">createCliRenderer</span>()\n\n<span class=\"keyword\">const</span> input <span class=\"operator\">=</span> <span class=\"function\">Input</span>({\"{\"}\n  <span class=\"property\">placeholder</span><span class=\"operator\">:</span> <span class=\"string\">\"Type something...\"</span>,\n  <span class=\"property\">width</span><span class=\"operator\">:</span> <span class=\"number\">30</span>,\n{\"}\"})\n\ninput.<span class=\"function\">focus</span>()\nrenderer.root.<span class=\"function\">add</span>(input)</code></pre>\n            </div>\n            \n            <!-- Layout Example -->\n            <div class=\"code-editor-body\" data-panel=\"layout\" data-active=\"false\">\n              <div class=\"code-editor-lines\">\n                <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span><span>11</span><span>12</span><span>13</span><span>14</span>\n              </div>\n              <pre><code><span class=\"keyword\">import</span> {\"{\"} createCliRenderer, Box, Text {\"}\"} <span class=\"keyword\">from</span> <span class=\"string\">\"@opentui/core\"</span>\n\n<span class=\"keyword\">const</span> renderer <span class=\"operator\">=</span> <span class=\"keyword\">await</span> <span class=\"function\">createCliRenderer</span>()\n\nrenderer.root.<span class=\"function\">add</span>(\n  <span class=\"function\">Box</span>(\n    {\"{\"} <span class=\"property\">width</span><span class=\"operator\">:</span> <span class=\"string\">\"100%\"</span>, <span class=\"property\">height</span><span class=\"operator\">:</span> <span class=\"string\">\"100%\"</span>, <span class=\"property\">flexDirection</span><span class=\"operator\">:</span> <span class=\"string\">\"row\"</span>, <span class=\"property\">gap</span><span class=\"operator\">:</span> <span class=\"number\">2</span> {\"}\"},\n    <span class=\"function\">Box</span>(\n      {\"{\"} <span class=\"property\">flexGrow</span><span class=\"operator\">:</span> <span class=\"number\">1</span>, <span class=\"property\">backgroundColor</span><span class=\"operator\">:</span> <span class=\"string\">\"#1a1b26\"</span> {\"}\"},\n      <span class=\"function\">Text</span>({\"{\"} <span class=\"property\">content</span><span class=\"operator\">:</span> <span class=\"string\">\"Sidebar\"</span>, <span class=\"property\">fg</span><span class=\"operator\">:</span> <span class=\"string\">\"#bb9af7\"</span> {\"}\"})\n    ),\n    <span class=\"function\">Box</span>({\"{\"} <span class=\"property\">flexGrow</span><span class=\"operator\">:</span> <span class=\"number\">3</span>, <span class=\"property\">backgroundColor</span><span class=\"operator\">:</span> <span class=\"string\">\"#24283b\"</span> {\"}\"}, <span class=\"function\">Text</span>({\"{\"} <span class=\"property\">content</span><span class=\"operator\">:</span> <span class=\"string\">\"Main\"</span> {\"}\"}))\n  )\n)</code></pre>\n            </div>\n          </div>\n        </section>\n\n        <!-- What is OpenTUI -->\n        <section class=\"what-section\">\n          <div class=\"what-section-title\">\n            <h3>What is OpenTUI?</h3>\n            <p>OpenTUI is a native terminal UI core written in Zig with TypeScript bindings. The native core exposes a C ABI and can be used from any language. OpenTUI powers OpenCode in production today and will also power terminal.shop. It is an extensible core with a focus on correctness, stability, and high performance. It provides a component-based architecture with flexible layout capabilities, allowing you to create complex terminal applications.</p>\n          </div>\n          \n          <div class=\"features-split\">\n            <div class=\"features-list\">\n              <ul class=\"what-list\">\n                {features.map((feature, i) => (\n                  <li class={`feature-item ${i === 0 ? 'active' : ''}`} data-feature={feature.type} tabindex=\"0\">\n                    <span class=\"bullet\">[{i === 0 ? 'o' : '*'}]</span>\n                    <div><strong>{feature.title}</strong> {feature.description}</div>\n                  </li>\n                ))}\n              </ul>\n              \n              <div class=\"what-actions\">\n                <a href=\"/docs/getting-started\" class=\"btn-dark\">\n                  <span>Read docs</span>\n                  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                    <path d=\"M6.5 12L17 12M13 16.5L17.5 12L13 7.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"square\"/>\n                  </svg>\n                </a>\n              </div>\n            </div>\n            \n            <div class=\"features-visual\">\n              <TuiSurface />\n            </div>\n          </div>\n        </section>\n\n        <!-- Footer -->\n        <footer class=\"landing-footer\">\n          <div class=\"landing-footer-cell\"><a href=\"https://github.com/anomalyco/opentui\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a></div>\n          <div class=\"landing-footer-cell\"><a href=\"/docs/getting-started\">Docs</a></div>\n        </footer>\n      </div>\n    </div>\n\n    <!-- Legal - outside container, centered -->\n    <div class=\"landing-legal\">\n      <span>&copy; 2026 <a href=\"https://anoma.ly\">Anomaly</a></span>\n    </div>\n  </main>\n</Base>\n\n<script>\n  const installCommands: Record<string, { cmd: string; pkg: string }> = {\n    create: { cmd: \"bun create\", pkg: \"tui\" },\n    manual: { cmd: \"bun add\", pkg: \"@opentui/core\" },\n    skill: { cmd: \"npx skills add\", pkg: \"msmps/opentui-skill\" },\n  }\n\n  // Tab switching\n  const tabs = document.querySelectorAll('.install-tab')\n  const commandBtn = document.getElementById('install-display') as HTMLButtonElement\n  \n  tabs.forEach(tab => {\n    tab.addEventListener('click', () => {\n      const key = tab.getAttribute('data-tab') as keyof typeof installCommands\n      const { cmd, pkg } = installCommands[key]\n      \n      // Update active tab\n      tabs.forEach(t => t.setAttribute('aria-selected', 'false'))\n      tab.setAttribute('aria-selected', 'true')\n      \n      // Update command display\n      if (commandBtn) {\n        const codeEl = commandBtn.querySelector('code')\n        if (codeEl) {\n          codeEl.innerHTML = `<span class=\"cmd\">${cmd} </span><span class=\"highlight\">${pkg}</span>`\n        }\n        commandBtn.setAttribute('data-command', `${cmd} ${pkg}`)\n      }\n    })\n  })\n\n  // Copy functionality - clicking the whole command button copies\n  if (commandBtn) {\n    commandBtn.addEventListener('click', async () => {\n      const command = commandBtn.getAttribute('data-command')\n      if (command) {\n        await navigator.clipboard.writeText(command)\n        const copyBtn = commandBtn.querySelector('.copy-btn')\n        if (copyBtn) {\n          copyBtn.classList.add('copied')\n          setTimeout(() => copyBtn.classList.remove('copied'), 1500)\n        }\n      }\n    })\n  }\n\n  // Code editor tab switching\n  const editorTabs = document.querySelectorAll('.code-editor-tab')\n  const editorPanels = document.querySelectorAll('.code-editor-body')\n  \n  editorTabs.forEach(tab => {\n    tab.addEventListener('click', () => {\n      const tabId = tab.getAttribute('data-tab')\n      \n      editorTabs.forEach(t => t.setAttribute('aria-selected', 'false'))\n      tab.setAttribute('aria-selected', 'true')\n      \n      editorPanels.forEach(panel => {\n        panel.setAttribute('data-active', panel.getAttribute('data-panel') === tabId ? 'true' : 'false')\n      })\n    })\n  })\n\n  // Feature switching logic\n  const featureItems = document.querySelectorAll('.feature-item');\n  \n  featureItems.forEach(item => {\n    item.addEventListener('click', () => {\n      // Update UI\n      featureItems.forEach(i => {\n        i.classList.remove('active');\n        const bull = i.querySelector('.bullet');\n        if (bull) bull.textContent = '[*]';\n      });\n      item.classList.add('active');\n      const bull = item.querySelector('.bullet');\n      if (bull) bull.textContent = '[o]';\n      \n      // Dispatch event\n      const feature = item.getAttribute('data-feature');\n      window.dispatchEvent(new CustomEvent('feature-change', { detail: feature }));\n    });\n    \n    // Keyboard support\n    item.addEventListener('keydown', (e: any) => {\n      if (e.key === 'Enter' || e.key === ' ') {\n        e.preventDefault();\n        (item as HTMLElement).click();\n      }\n    });\n  });\n\n  const codeCopyBtn = document.getElementById('code-copy-btn')\n  \n  if (codeCopyBtn) {\n    codeCopyBtn.addEventListener('click', async () => {\n      const activePanel = document.querySelector('.code-editor-body[data-active=\"true\"]')\n      if (activePanel) {\n        const codeEl = activePanel.querySelector('pre code')\n        if (codeEl) {\n          const code = codeEl.textContent || ''\n          await navigator.clipboard.writeText(code)\n          \n          codeCopyBtn.classList.add('copied')\n          setTimeout(() => codeCopyBtn.classList.remove('copied'), 1500)\n        }\n      }\n    })\n  }\n\n  // Mobile menu toggle\n  const menuBtn = document.querySelector('.mobile-menu-btn')\n  const mobileNav = document.querySelector('.mobile-nav')\n\n  function openMenu() {\n    document.body.classList.add('mobile-nav-open')\n    menuBtn?.setAttribute('aria-expanded', 'true')\n    menuBtn?.setAttribute('aria-label', 'Close navigation menu')\n  }\n\n  function closeMenu() {\n    document.body.classList.remove('mobile-nav-open')\n    menuBtn?.setAttribute('aria-expanded', 'false')\n    menuBtn?.setAttribute('aria-label', 'Open navigation menu')\n  }\n\n  menuBtn?.addEventListener('click', () => {\n    const isOpen = document.body.classList.contains('mobile-nav-open')\n    if (isOpen) {\n      closeMenu()\n    } else {\n      openMenu()\n    }\n  })\n\n  document.addEventListener('keydown', (e) => {\n    if (e.key === 'Escape' && document.body.classList.contains('mobile-nav-open')) {\n      closeMenu()\n    }\n  })\n\n  mobileNav?.querySelectorAll('a').forEach((link) => {\n    link.addEventListener('click', closeMenu)\n  })\n</script>\n"
  },
  {
    "path": "packages/web/src/styles/global.css",
    "content": "/* OpenTUI Documentation - Light theme matching opencode.ai */\n\n:root {\n  /* Colors - matching opencode.ai exactly */\n  --color-bg: hsl(0, 20%, 99%);\n  --color-bg-weak: hsl(0, 8%, 97%);\n  --color-bg-weak-hover: hsl(0, 8%, 94%);\n  --color-bg-strong: hsl(0, 5%, 12%);\n  --color-bg-strong-hover: hsl(0, 5%, 18%);\n\n  --color-text: hsl(0, 1%, 39%);\n  --color-text-weak: hsl(0, 1%, 60%);\n  --color-text-strong: hsl(0, 5%, 12%);\n  --color-text-inverted: hsl(0, 20%, 99%);\n\n  /* Border color matching opencode.ai: rgba(15, 0, 0, 0.12) */\n  --color-border: rgba(15, 0, 0, 0.12);\n  --color-border-weak: rgba(15, 0, 0, 0.12);\n\n  --color-icon: hsl(0, 1%, 55%);\n  --color-success: #03b000;\n\n  /* Typography - Inter variable + IBM Plex Mono */\n  --font-sans: \"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n  --font-mono: \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, monospace;\n\n  /* Sizing - matching opencode.ai container layout */\n  --container-max-width: 1080px;\n  --section-padding-x: 80px;\n  --content-width: 720px;\n  --sidebar-width: 220px;\n  --nav-gap: 40px;\n\n  /* Spacing */\n  --space-xs: 4px;\n  --space-sm: 8px;\n  --space-md: 16px;\n  --space-lg: 24px;\n  --space-xl: 32px;\n  --space-2xl: 48px;\n  --space-3xl: 64px;\n  --space-4xl: 96px;\n\n  /* Header */\n  --header-height: 72px;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n* {\n  scrollbar-width: thin;\n  scrollbar-color: var(--color-text-weak) transparent;\n}\n\nhtml {\n  font-size: 14px;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\nbody {\n  margin: 0;\n  padding: 0;\n  font-family: var(--font-sans);\n  font-size: 14px;\n  line-height: 1.6;\n  color: var(--color-text);\n  background-color: var(--color-bg);\n  transition:\n    background-color 0.2s ease,\n    color 0.2s ease;\n}\n\na {\n  color: var(--color-text-strong);\n  text-decoration: none;\n}\n\na:hover {\n  text-decoration: underline;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  margin: 0;\n  font-weight: 500;\n  line-height: 1.3;\n  color: var(--color-text-strong);\n}\n\np {\n  margin: 0;\n}\n\ncode {\n  font-family: var(--font-mono);\n  font-size: 0.9em;\n}\n\npre {\n  font-family: var(--font-mono);\n  font-size: 14px;\n  line-height: 1.6;\n  margin: 0;\n}\n\n/* ============\n   LANDING PAGE \n   ============\n\n/* Main wrapper */\n.landing {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 100vh;\n  gap: var(--space-3xl);\n}\n\n/* Container - the bordered box that holds everything */\n.landing-container {\n  width: 100%;\n  max-width: var(--container-max-width);\n  border-left: 1px solid var(--color-border);\n  border-right: 1px solid var(--color-border);\n  border-bottom: 1px solid var(--color-border);\n  overflow-x: clip;\n}\n\n/* Header */\n.landing-header {\n  position: sticky;\n  top: 0;\n  z-index: 100;\n  background: var(--color-bg);\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: var(--space-lg) var(--section-padding-x);\n  border-bottom: 1px solid var(--color-border);\n  transition:\n    background-color 0.2s ease,\n    border-color 0.2s ease;\n}\n\n.landing-logo {\n  display: flex;\n  align-items: center;\n  gap: var(--space-xs);\n  font-weight: 700;\n  font-size: 16px;\n  color: var(--color-text-strong);\n  text-decoration: none;\n}\n\n.landing-logo:hover {\n  text-decoration: none;\n}\n\n.landing-logo svg {\n  width: 24px;\n  height: 24px;\n}\n\n.landing-nav {\n  display: flex;\n  align-items: center;\n  gap: var(--nav-gap);\n}\n\n.landing-nav a {\n  color: var(--color-text-strong);\n  font-size: 14px;\n  border-bottom: 1px solid transparent;\n}\n\n.landing-nav a:hover {\n  text-decoration: none;\n  border-bottom: 1px solid currentColor;\n}\n\n/* Mobile nav for landing page */\n.mobile-nav {\n  display: none;\n  position: fixed;\n  top: var(--header-height);\n  left: 0;\n  right: 0;\n  bottom: 0;\n  height: calc(100vh - var(--header-height));\n  height: calc(100dvh - var(--header-height));\n  background: var(--color-bg);\n  z-index: 99;\n  padding: var(--space-xl) var(--section-padding-x);\n  flex-direction: column;\n  gap: var(--space-md);\n  opacity: 0;\n  visibility: hidden;\n  transition:\n    opacity 0.2s ease,\n    visibility 0.2s ease;\n}\n\n.mobile-nav a {\n  color: var(--color-text-strong);\n  font-size: 16px;\n  padding: var(--space-sm) 0;\n}\n\n.mobile-nav a:hover {\n  text-decoration: none;\n}\n\n.mobile-nav-open .mobile-nav {\n  opacity: 1;\n  visibility: visible;\n}\n\n/* Hero Section */\n.hero {\n  padding: var(--space-4xl) var(--section-padding-x);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  text-align: center;\n  max-width: 900px;\n  margin: 0 auto;\n}\n\n.hero-content {\n  display: flex;\n  flex-direction: column;\n}\n\n.hero-copy {\n  margin-bottom: 0;\n}\n\n.hero h1 {\n  font-family: var(--font-mono);\n  font-size: 38px;\n  font-weight: 700;\n  margin-bottom: 8px;\n  line-height: 1.5;\n  color: var(--color-text-strong);\n}\n\n.hero-subtitle {\n  font-size: 16px;\n  color: var(--color-text);\n  line-height: 2;\n  margin-bottom: var(--space-xl);\n}\n\n/* Install Region */\n.install-section {\n  width: 100%;\n  max-width: 100%;\n}\n\n.install-tabs {\n  display: flex;\n  gap: 40px;\n  padding: 0 20px;\n  border: 1px solid var(--color-border);\n  border-bottom: none;\n  border-radius: 6px 6px 0 0;\n  background: var(--color-bg-weak);\n}\n\n.install-tab {\n  padding: 16px 0;\n  font-size: 16px;\n  font-family: var(--font-mono);\n  background: transparent;\n  border: none;\n  color: var(--color-text-weak);\n  cursor: pointer;\n  transition: color 0.15s;\n}\n\n.install-tab:hover {\n  color: var(--color-text-strong);\n}\n\n.install-tab[aria-selected=\"true\"] {\n  color: var(--color-text-strong);\n  border-bottom: 2px solid var(--color-text-strong);\n}\n\n.install-panels {\n  border: 1px solid var(--color-border);\n  border-radius: 0 0 6px 6px;\n  background: var(--color-bg-weak);\n}\n\n.install-command {\n  display: flex;\n  align-items: center;\n  gap: var(--space-md);\n  padding: 16px 20px;\n  background: transparent;\n  font-family: var(--font-mono);\n  font-size: 16px;\n  cursor: pointer;\n  transition: background 0.15s;\n  border: none;\n  width: auto;\n  text-align: left;\n}\n\n.install-command:hover {\n  background: var(--color-bg-weak);\n}\n\n.install-command code {\n  color: var(--color-text);\n}\n\n.install-command .highlight {\n  color: var(--color-text-strong);\n  font-weight: 500;\n}\n\n.install-command .copy-btn {\n  color: var(--color-icon);\n  display: flex;\n  align-items: center;\n  transition: color 0.15s;\n}\n\n.install-command:hover .copy-btn {\n  color: var(--color-text-strong);\n}\n\n.install-command .copy-btn svg {\n  width: 16px;\n  height: 16px;\n}\n\n.install-command .copy-btn .check-icon {\n  color: var(--color-success);\n  display: none;\n}\n\n.install-command .copy-btn.copied .copy-icon {\n  display: none;\n}\n\n.install-command .copy-btn.copied .check-icon {\n  display: block;\n}\n\n/* CTA Buttons */\n.hero-actions {\n  display: flex;\n  gap: var(--space-sm);\n  justify-content: center;\n  margin-top: var(--space-xl);\n}\n\n/* Secondary/outline button for light bg */\n.btn-outline {\n  display: inline-flex;\n  align-items: center;\n  gap: 12px;\n  padding: 8px 20px;\n  height: 40px;\n  font-family: var(--font-mono);\n  font-size: 15px;\n  font-weight: 500;\n  line-height: 1.5;\n  border-radius: 4px;\n  background: transparent;\n  color: var(--color-text-strong);\n  border: 1px solid var(--color-border);\n  text-decoration: none;\n  cursor: pointer;\n  transition: all 0.15s;\n}\n\n.btn-outline:hover {\n  background: var(--color-bg-weak);\n  text-decoration: none;\n}\n\n/* What is OpenTUI Section - Light background with top border */\n.what-section {\n  padding: var(--space-3xl) var(--section-padding-x);\n  border-top: 1px solid var(--color-border);\n}\n\n.what-section-title {\n  margin-bottom: var(--space-xl);\n}\n\n.what-section-title h3 {\n  font-size: 20px;\n  font-weight: 500;\n  margin-bottom: var(--space-sm);\n  color: var(--color-text-strong);\n}\n\n.what-section-title p {\n  color: var(--color-text);\n  font-size: 16px;\n  line-height: 1.5;\n}\n\n.what-list {\n  list-style: none;\n  padding: 0;\n  margin: 0 0 var(--space-xl) 0;\n}\n\n.what-list li {\n  display: flex;\n  align-items: baseline;\n  gap: var(--space-md);\n  padding: var(--space-sm) 0;\n}\n\n.what-list .bullet {\n  font-family: var(--font-mono);\n  color: var(--color-text-weak);\n  flex-shrink: 0;\n  font-size: 14px;\n}\n\n.what-list div {\n  font-size: 16px;\n  line-height: 1.6;\n  color: var(--color-text);\n}\n\n.what-list strong {\n  font-weight: 600;\n  color: var(--color-text-strong);\n}\n\n/* Features Split Layout */\n.features-split {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: var(--space-3xl);\n  align-items: start;\n}\n\n.features-list {\n  display: flex;\n  flex-direction: column;\n}\n\n.features-visual {\n  position: sticky;\n  top: 120px;\n  background: transparent;\n}\n\n.feature-item {\n  cursor: pointer;\n  border-radius: 6px;\n  padding: 12px;\n  margin: 0 -12px;\n  transition: color 0.15s;\n}\n\n.feature-item.active strong {\n  color: var(--color-text-strong);\n}\n\n.feature-item.active .bullet {\n  color: var(--color-text-strong);\n  font-weight: bold;\n}\n\n.what-actions {\n  display: flex;\n  justify-content: flex-start;\n  /* Left align in split view */\n  margin-top: var(--space-xl);\n  padding-left: 0;\n}\n\n@media (max-width: 980px) {\n  .features-split {\n    grid-template-columns: 1fr;\n    gap: var(--space-xl);\n  }\n\n  .features-visual {\n    display: none;\n  }\n}\n\n/* Button - dark bg, light text (primary action) */\n.btn-dark {\n  display: inline-flex;\n  align-items: center;\n  gap: 12px;\n  padding: 8px 12px 8px 20px;\n  height: 40px;\n  font-family: var(--font-mono);\n  font-size: 15px;\n  font-weight: 500;\n  line-height: 1.5;\n  border-radius: 4px;\n  border: none;\n  background: var(--color-bg-strong);\n  color: var(--color-text-inverted);\n  text-decoration: none;\n  cursor: pointer;\n  transition: background 0.15s;\n}\n\n.btn-dark:hover {\n  background: var(--color-bg-strong-hover);\n  text-decoration: none;\n}\n\n.btn-dark svg {\n  width: 24px;\n  height: 24px;\n}\n\n.code-section {\n  /* \"Precision\" Light Theme */\n  --editor-bg: #ffffff;\n  --editor-bg-header: #fafafa;\n  --editor-bg-tab: #ffffff;\n  --editor-bg-tab-active: #ffffff;\n  --editor-bg-tab-hover: #f4f4f5;\n  --editor-border: rgba(0, 0, 0, 0.1);\n  --editor-gutter: #a1a1aa;\n  --editor-text: #27272a;\n  --editor-text-muted: #71717a;\n\n  background: var(--editor-bg-header);\n  border-top: 1px solid var(--color-border);\n  border-bottom: 1px solid var(--color-border);\n  transition:\n    background-color 0.2s ease,\n    border-color 0.2s ease;\n}\n\n.code-editor-header {\n  border-bottom: 1px solid var(--editor-border);\n}\n\n.code-editor-tab[aria-selected=\"true\"] {\n  color: var(--color-text-strong);\n  border-bottom: 2px solid var(--color-text-strong);\n}\n\n.code-editor {\n  display: flex;\n  flex-direction: column;\n}\n\n.code-editor-header {\n  display: flex;\n  align-items: stretch;\n  background: var(--editor-bg-header);\n  border-bottom: 1px solid var(--editor-border);\n}\n\n.code-editor-dots {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 0 var(--section-padding-x);\n  border-right: 1px solid var(--editor-border);\n}\n\n.code-editor-dots span {\n  width: 10px;\n  height: 10px;\n  border-radius: 50%;\n  background: var(--editor-gutter);\n}\n\n.code-editor-tabs {\n  display: flex;\n  align-items: stretch;\n  flex: 1;\n}\n\n.code-editor-tab {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 12px 20px;\n  font-family: var(--font-mono);\n  font-size: 13px;\n  color: var(--editor-text-muted);\n  background: transparent;\n  border: none;\n  border-right: 1px solid var(--editor-border);\n  cursor: pointer;\n  transition: all 0.15s;\n}\n\n.code-editor-tab:hover {\n  background: var(--editor-bg-tab-hover);\n  color: var(--editor-text);\n}\n\n.code-editor-tab[aria-selected=\"true\"] {\n  background: var(--editor-bg-tab-active);\n  color: var(--editor-text);\n  border-bottom: 2px solid var(--editor-text);\n  margin-bottom: -1px;\n}\n\n.code-editor-tab-icon {\n  width: 14px;\n  height: 14px;\n  opacity: 0.7;\n}\n\n.code-editor-tab[aria-selected=\"true\"] .code-editor-tab-icon {\n  opacity: 1;\n}\n\n.code-editor-copy {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0 20px;\n  background: transparent;\n  border: none;\n  border-left: 1px solid var(--editor-border);\n  cursor: pointer;\n  color: var(--editor-text-muted);\n  transition:\n    color 0.15s,\n    background 0.15s;\n}\n\n.code-editor-copy:hover {\n  color: var(--editor-text);\n  background: var(--editor-bg-tab-hover);\n}\n\n.code-editor-copy svg {\n  width: 16px;\n  height: 16px;\n}\n\n.code-editor-copy .check-icon {\n  display: none;\n  color: var(--color-success);\n}\n\n.code-editor-copy.copied .copy-icon {\n  display: none;\n}\n\n.code-editor-copy.copied .check-icon {\n  display: block;\n}\n\n.code-editor-body {\n  display: none;\n  padding: var(--space-2xl) var(--section-padding-x);\n  -webkit-text-size-adjust: 100%;\n  text-size-adjust: 100%;\n}\n\n.code-editor-body[data-active=\"true\"] {\n  display: flex;\n}\n\n.code-editor-lines {\n  display: flex;\n  flex-direction: column;\n  padding-right: var(--space-lg);\n  margin-right: var(--space-lg);\n  font-family: var(--font-mono);\n  font-size: 15px;\n  line-height: 1.7;\n  color: var(--editor-gutter);\n  text-align: right;\n  user-select: none;\n  min-width: 24px;\n  -webkit-text-size-adjust: 100%;\n  text-size-adjust: 100%;\n}\n\n.code-editor-body pre {\n  color: var(--editor-text);\n  font-size: 15px;\n  line-height: 1.7;\n  margin: 0;\n  background: transparent;\n  -webkit-text-size-adjust: 100%;\n  text-size-adjust: 100%;\n}\n\n.code-editor-body .keyword {\n  color: #3f3f46;\n  font-weight: 500;\n}\n\n.code-editor-body .function {\n  color: #52525b;\n}\n\n.code-editor-body .string {\n  color: #71717a;\n}\n\n.code-editor-body .number {\n  color: #71717a;\n}\n\n.code-editor-body .comment {\n  color: #a1a1aa;\n  font-style: italic;\n}\n\n.code-editor-body .property {\n  color: #52525b;\n}\n\n.code-editor-body .type {\n  color: #3f3f46;\n}\n\n.code-editor-body .operator {\n  color: #71717a;\n}\n\n.code-editor-body .punctuation {\n  color: #a1a1aa;\n}\n\n/* Landing Footer - opencode.ai style */\n.landing-footer {\n  display: flex;\n  border-top: 1px solid var(--color-border);\n}\n\n.landing-footer-cell {\n  flex: 1 1 0%;\n  border-left: 1px solid var(--color-border);\n}\n\n.landing-footer-cell:first-child {\n  border-left: none;\n}\n\n.landing-footer-cell a {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 88px;\n  color: var(--color-text-strong);\n  font-size: 16px;\n  text-decoration: none;\n}\n\n.landing-footer-cell a:hover {\n  background-color: var(--color-bg-weak);\n  text-decoration: underline;\n  text-underline-offset: 4px;\n}\n\n/* Legal section - outside container, centered */\n.landing-legal {\n  display: flex;\n  justify-content: center;\n  gap: 32px;\n  padding-bottom: var(--space-4xl);\n  color: hsl(0, 1%, 60%);\n  font-size: 16px;\n}\n\n.landing-legal a {\n  color: hsl(0, 1%, 60%);\n  text-decoration: none;\n}\n\n.landing-legal a:hover {\n  color: var(--color-text);\n  text-decoration: none;\n  border-bottom: 1px solid currentColor;\n}\n\n/* ============================================\n   DOCS LAYOUT\n   ============================================ */\n\n/* Docs page wrapper */\n.docs-page {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 100vh;\n}\n\n/* Docs container - the bordered box */\n.docs-container {\n  width: 100%;\n  max-width: var(--container-max-width);\n  border-left: 1px solid var(--color-border);\n  border-right: 1px solid var(--color-border);\n  border-bottom: 1px solid var(--color-border);\n  overflow-x: clip;\n}\n\n/* Header */\n.header {\n  position: sticky;\n  top: 0;\n  z-index: 100;\n  background: var(--color-bg);\n  border-bottom: 1px solid var(--color-border);\n  display: flex;\n  align-items: center;\n  transition:\n    background-color 0.2s ease,\n    border-color 0.2s ease;\n}\n\n.header-content {\n  width: 100%;\n  padding: var(--space-lg) var(--section-padding-x);\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.logo {\n  display: flex;\n  align-items: center;\n  gap: var(--space-xs);\n  font-weight: 700;\n  font-size: 16px;\n  color: var(--color-text-strong);\n  text-decoration: none;\n}\n\n.logo:hover {\n  text-decoration: none;\n}\n\n.logo-icon {\n  width: 24px;\n  height: 24px;\n}\n\n.nav {\n  display: flex;\n  align-items: center;\n  gap: var(--nav-gap);\n}\n\n.nav a {\n  color: var(--color-text-strong);\n  font-size: 14px;\n  border-bottom: 1px solid transparent;\n}\n\n.nav a:hover {\n  text-decoration: none;\n  border-bottom: 1px solid currentColor;\n}\n\n/* Mobile menu button */\n.mobile-menu-btn {\n  display: none;\n  align-items: center;\n  justify-content: center;\n  width: 40px;\n  height: 40px;\n  padding: 0;\n  background: none;\n  border: none;\n  color: color-mix(in srgb, var(--color-text-strong) 70%, var(--color-text) 30%);\n  cursor: pointer;\n}\n\n.mobile-menu-btn:hover {\n  background: var(--color-bg-subtle);\n  border-color: var(--color-text-weak);\n}\n\n.mobile-menu-btn svg {\n  width: 20px;\n  height: 20px;\n}\n\n.mobile-menu-btn .close-icon {\n  display: none;\n}\n\n.mobile-nav-open .mobile-menu-btn .menu-icon {\n  display: none;\n}\n\n.mobile-nav-open .mobile-menu-btn .close-icon {\n  display: block;\n}\n\n/* Mobile nav overlay */\n.mobile-nav-overlay {\n  display: none;\n  position: fixed;\n  inset: 0;\n  background: rgba(0, 0, 0, 0.5);\n  z-index: 90;\n  opacity: 0;\n  transition: opacity 0.2s ease;\n}\n\n/* Main layout */\n.main {\n  padding: var(--space-xl) var(--section-padding-x);\n}\n\n/* Docs layout */\n.docs-layout {\n  display: grid;\n  grid-template-columns: var(--sidebar-width) 1fr;\n  gap: var(--space-3xl);\n}\n\n.sidebar {\n  position: sticky;\n  top: calc(var(--header-height) + var(--space-xl));\n  height: fit-content;\n  max-height: calc(100vh - var(--header-height) - var(--space-2xl));\n  overflow-y: auto;\n}\n\n.sidebar-section {\n  margin-bottom: var(--space-xl);\n}\n\n.sidebar-title {\n  font-size: 12px;\n  font-weight: 500;\n  color: var(--color-text-strong);\n  margin-bottom: var(--space-sm);\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n}\n\n.sidebar-links {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n.sidebar-links li {\n  margin: 0;\n}\n\n.sidebar-links a {\n  display: block;\n  padding: 4px 0;\n  color: var(--color-text);\n  font-size: 13px;\n  transition: color 0.15s;\n}\n\n.sidebar-links a:hover {\n  color: var(--color-text-strong);\n  text-decoration: none;\n}\n\n.sidebar-links a.active {\n  color: var(--color-text-strong);\n  font-weight: 500;\n}\n\n/* Content */\n.content {\n  max-width: var(--content-width);\n  min-width: 0;\n  overflow-wrap: break-word;\n}\n\n.content > *:first-child {\n  margin-top: 0;\n}\n\n.content h1 {\n  font-size: 26px;\n  font-weight: 500;\n  margin-bottom: var(--space-sm);\n}\n\n.content h2 {\n  font-size: 22px;\n  font-weight: 500;\n  margin-top: var(--space-2xl);\n  margin-bottom: var(--space-sm);\n}\n\n.content h3 {\n  font-size: 18px;\n  font-weight: 500;\n  margin-top: var(--space-xl);\n  margin-bottom: var(--space-xs);\n}\n\n.content a {\n  text-decoration: underline;\n  text-underline-offset: 0.25rem;\n}\n\n.content a:hover {\n  text-decoration: underline;\n}\n\n.content p {\n  margin-bottom: var(--space-md);\n}\n\n.content pre {\n  position: relative;\n  background: var(--color-bg-weak) !important;\n  padding: var(--space-md);\n  border-radius: 6px;\n  overflow-x: auto;\n  margin: var(--space-md) 0;\n  transition: background-color 0.2s ease;\n}\n\n.content pre .copy-btn {\n  position: absolute;\n  top: 8px;\n  right: 8px;\n  background: var(--color-bg);\n  border: 1px solid var(--color-border);\n  border-radius: 4px;\n  padding: 6px;\n  cursor: pointer;\n  opacity: 0;\n  transition: opacity 0.15s;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.content pre:hover .copy-btn {\n  opacity: 1;\n}\n\n.content pre .copy-btn:hover {\n  background: var(--color-bg-weak);\n}\n\n.content pre .copy-btn svg {\n  width: 16px;\n  height: 16px;\n  color: var(--color-icon);\n}\n\n.content pre .copy-btn .check-icon {\n  display: none;\n  color: #22c55e;\n}\n\n.content pre .copy-btn.copied .copy-icon {\n  display: none;\n}\n\n.content pre .copy-btn.copied .check-icon {\n  display: block;\n}\n\n.content code {\n  background: var(--color-bg-weak);\n  padding: 2px 6px;\n  border-radius: 4px;\n  font-size: 13px;\n}\n\n.content pre code {\n  background: none;\n  padding: 0;\n}\n\n/* Tables */\n.table-wrapper {\n  overflow-x: auto;\n  -webkit-overflow-scrolling: touch;\n  margin: var(--space-md) 0;\n}\n\n.content table {\n  border-collapse: collapse;\n  font-size: 13px;\n  width: max-content;\n  min-width: 100%;\n}\n\n.content th,\n.content td {\n  padding: var(--space-sm) var(--space-md);\n  text-align: left;\n  border-bottom: 1px solid var(--color-border);\n  white-space: nowrap;\n}\n\n.content th {\n  font-weight: 500;\n  color: var(--color-text-strong);\n}\n\n/* Footer */\n.footer {\n  border-top: 1px solid var(--color-border);\n  padding: var(--space-lg) var(--section-padding-x);\n  margin-top: var(--space-2xl);\n}\n\n.footer-content {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  color: var(--color-text-weak);\n  font-size: 13px;\n}\n\n.footer a {\n  color: var(--color-text-weak);\n}\n\n.footer a:hover {\n  color: var(--color-text-strong);\n  text-decoration: none;\n  border-bottom: 1px solid currentColor;\n}\n\n/* Responsive */\n@media (max-width: 1120px) {\n  :root {\n    --section-padding-x: 40px;\n  }\n}\n\n@media (max-width: 900px) {\n  :root {\n    --section-padding-x: 24px;\n  }\n\n  .docs-layout {\n    grid-template-columns: 1fr;\n  }\n\n  /* Hide desktop nav, show mobile menu button */\n  .nav,\n  .landing-nav {\n    display: none;\n  }\n\n  .mobile-menu-btn {\n    display: flex;\n    order: 1;\n    margin-left: auto;\n  }\n\n  .mobile-nav {\n    display: flex;\n  }\n\n  /* Mobile sidebar - fullscreen overlay */\n  .sidebar {\n    position: fixed;\n    top: var(--header-height);\n    left: 0;\n    right: 0;\n    bottom: 0;\n    height: calc(100vh - var(--header-height));\n    height: calc(100dvh - var(--header-height));\n    max-height: none;\n    background: var(--color-bg);\n    z-index: 99;\n    padding: var(--space-xl) var(--section-padding-x);\n    overflow-y: auto;\n    opacity: 0;\n    visibility: hidden;\n    transition:\n      opacity 0.2s ease,\n      visibility 0.2s ease;\n  }\n\n  .sidebar-title {\n    font-size: 14px;\n  }\n\n  .sidebar-links a {\n    font-size: 15px;\n    padding: 8px 0;\n  }\n\n  /* Mobile nav open state */\n  .mobile-nav-open .sidebar {\n    opacity: 1;\n    visibility: visible;\n  }\n\n  .landing-container,\n  .docs-container {\n    border-left: none;\n    border-right: none;\n  }\n}\n\n@media (max-width: 600px) {\n  :root {\n    --section-padding-x: 16px;\n  }\n\n  .hero {\n    padding: var(--space-2xl) var(--section-padding-x);\n    grid-template-columns: 1fr;\n  }\n\n  .hero h1 {\n    font-size: 24px;\n  }\n\n  .hero-subtitle {\n    font-size: 14px;\n  }\n\n  .hero-actions {\n    flex-direction: column;\n    align-items: stretch;\n  }\n\n  .install-tabs {\n    gap: 2px;\n  }\n\n  .install-tab {\n    padding: 6px 8px;\n    font-size: 12px;\n  }\n\n  /* Code editor mobile fixes */\n  .code-editor {\n    overflow: hidden;\n  }\n\n  .code-editor-header {\n    flex-wrap: nowrap;\n  }\n\n  .code-editor-dots {\n    display: none;\n  }\n\n  .code-editor-tabs {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n    scrollbar-width: none;\n  }\n\n  .code-editor-tabs::-webkit-scrollbar {\n    display: none;\n  }\n\n  .code-editor-tab {\n    padding: 10px 12px;\n    font-size: 12px;\n    white-space: nowrap;\n    flex-shrink: 0;\n  }\n\n  .code-editor-copy {\n    padding: 0 12px;\n    flex-shrink: 0;\n  }\n\n  .code-editor-body {\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n  }\n\n  .code-editor-body pre {\n    font-size: 13px;\n    white-space: pre;\n    overflow-x: visible;\n  }\n\n  .code-editor-lines {\n    font-size: 13px;\n    padding-right: var(--space-md);\n    margin-right: var(--space-md);\n  }\n\n  /* What is OpenTUI section mobile spacing */\n  .feature-item {\n    margin: 0;\n    padding: 10px 0;\n  }\n\n  .what-list div {\n    font-size: 15px;\n  }\n}\n\n/* ============================================\n   DARK THEME\n============================================ */\n@media (prefers-color-scheme: dark) {\n  :root {\n    /* Backgrounds - warm-tinted dark, not pure black */\n    --color-bg: hsl(0, 5%, 9%);\n    --color-bg-weak: hsl(0, 4%, 12%);\n    --color-bg-weak-hover: hsl(0, 4%, 16%);\n    --color-bg-strong: hsl(0, 10%, 92%);\n    --color-bg-strong-hover: hsl(0, 10%, 98%);\n\n    /* Text - proper contrast hierarchy */\n    --color-text: hsl(0, 5%, 68%);\n    --color-text-weak: hsl(0, 3%, 50%);\n    --color-text-strong: hsl(0, 10%, 92%);\n    --color-text-inverted: hsl(0, 5%, 9%);\n\n    /* Borders - subtle, works on dark backgrounds */\n    --color-border: rgba(255, 255, 255, 0.12);\n    --color-border-weak: rgba(255, 255, 255, 0.08);\n\n    --color-icon: hsl(0, 3%, 55%);\n    --color-success: #22c55e;\n  }\n\n  /* Dark mode code editor theme */\n  .code-section {\n    --editor-bg: hsl(0, 5%, 9%);\n    --editor-bg-header: hsl(0, 4%, 11%);\n    --editor-bg-tab: hsl(0, 5%, 9%);\n    --editor-bg-tab-active: hsl(0, 5%, 9%);\n    --editor-bg-tab-hover: hsl(0, 4%, 14%);\n    --editor-border: rgba(255, 255, 255, 0.1);\n    --editor-gutter: hsl(0, 3%, 45%);\n    --editor-text: hsl(0, 5%, 85%);\n    --editor-text-muted: hsl(0, 3%, 55%);\n  }\n\n  /* Dark mode syntax highlighting - designed for dark backgrounds */\n  .code-editor-body .keyword {\n    color: #c9a0dc;\n  }\n\n  .code-editor-body .function {\n    color: #7dd3fc;\n  }\n\n  .code-editor-body .string {\n    color: #a5d6a7;\n  }\n\n  .code-editor-body .number {\n    color: #ffcc80;\n  }\n\n  .code-editor-body .comment {\n    color: hsl(0, 3%, 45%);\n    font-style: italic;\n  }\n\n  .code-editor-body .property {\n    color: #90caf9;\n  }\n\n  .code-editor-body .type {\n    color: #ce93d8;\n  }\n\n  .code-editor-body .operator {\n    color: #b0bec5;\n  }\n\n  .code-editor-body .punctuation {\n    color: hsl(0, 3%, 55%);\n  }\n\n  /* Dark mode Shiki/Astro code blocks */\n  .astro-code,\n  pre.astro-code {\n    background-color: var(--color-bg-weak) !important;\n  }\n\n  .astro-code span {\n    color: var(--shiki-dark, inherit) !important;\n    font-style: var(--shiki-dark-font-style, inherit) !important;\n    font-weight: var(--shiki-dark-font-weight, inherit) !important;\n    text-decoration: var(--shiki-dark-text-decoration, inherit) !important;\n  }\n\n  /* Dark mode logo adjustment inner rect */\n  .logo-icon rect:last-child,\n  .landing-logo svg rect:last-child {\n    fill: hsl(0, 5%, 9%);\n  }\n}\n"
  },
  {
    "path": "packages/web/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"compilerOptions\": {\n    \"strictNullChecks\": true\n  }\n}\n"
  },
  {
    "path": "scripts/create-snapshot.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\n# Colors for output\nGREEN='\\033[0;32m'\nBLUE='\\033[0;34m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m' # No Color\n\necho -e \"${BLUE}OpenTUI Snapshot Version Generator${NC}\"\necho \"====================================\"\necho \"\"\n\n# Get the short commit SHA (8 characters)\nCOMMIT_SHA=$(git rev-parse --short=8 HEAD)\n\n# Generate snapshot version: 0.0.0-YYYYMMDD-COMMITHASH\nVERSION=\"0.0.0-$(date +%Y%m%d)-${COMMIT_SHA}\"\n\necho -e \"${GREEN}Generated snapshot version: ${VERSION}${NC}\"\necho \"\"\n\n# Check if there are uncommitted changes\nif [[ -n $(git status -s) ]]; then\n  echo -e \"${YELLOW}WARNING: You have uncommitted changes${NC}\"\n  echo \"It's recommended to commit your changes before creating a snapshot.\"\n  echo \"\"\n  read -p \"Continue anyway? (y/n) \" -n 1 -r\n  echo \"\"\n  if [[ ! $REPLY =~ ^[Yy]$ ]]; then\n    echo \"Aborted.\"\n    exit 1\n  fi\nfi\n\n# Update package versions\necho -e \"${BLUE}Updating package versions...${NC}\"\nbun run prepare-release \"$VERSION\"\n\necho \"\"\necho -e \"${GREEN}Snapshot version prepared!${NC}\"\n"
  },
  {
    "path": "scripts/link-opentui-dev.sh",
    "content": "#!/bin/bash\n\nset -e \n\nLINK_REACT=false\nLINK_SOLID=false\nTARGET_ROOT=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n    case $1 in\n        --react)\n            LINK_REACT=true\n            shift\n            ;;\n        --solid)\n            LINK_SOLID=true\n            shift\n            ;;\n        *)\n            TARGET_ROOT=\"$1\"\n            shift\n            ;;\n    esac\ndone\n\nif [ -z \"$TARGET_ROOT\" ]; then\n    echo \"Usage: $0 <target-project-root> [--react] [--solid]\"\n    echo \"Example: $0 /path/to/your/project\"\n    echo \"Example: $0 /path/to/your/project --solid\"\n    echo \"Example: $0 /path/to/your/project --react\"\n    echo \"\"\n    echo \"This script links OpenTUI dev packages into Bun's cache directory.\"\n    echo \"All workspace packages will automatically resolve through the cache.\"\n    echo \"\"\n    echo \"Options:\"\n    echo \"  --react    Also link @opentui/react and React dependencies\"\n    echo \"  --solid    Also link @opentui/solid and solid-js\"\n    exit 1\nfi\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nOPENTUI_ROOT=\"$(cd \"$SCRIPT_DIR/..\" && pwd)\"\nNODE_MODULES_DIR=\"$TARGET_ROOT/node_modules\"\n\nif [ ! -d \"$TARGET_ROOT\" ]; then\n    echo \"Error: Target project root directory does not exist: $TARGET_ROOT\"\n    exit 1\nfi\n\nif [ ! -d \"$NODE_MODULES_DIR\" ]; then\n    echo \"Error: node_modules directory does not exist: $NODE_MODULES_DIR\"\n    echo \"Please run 'bun install' in the target project first.\"\n    exit 1\nfi\n\nif [ ! -d \"$NODE_MODULES_DIR/.bun\" ]; then\n    echo \"Error: Bun cache directory not found: $NODE_MODULES_DIR/.bun\"\n    echo \"This script is designed for Bun package manager.\"\n    exit 1\nfi\n\necho \"Linking OpenTUI dev packages from: $OPENTUI_ROOT\"\necho \"To Bun cache in: $NODE_MODULES_DIR/.bun\"\necho\n\n# Helper function to link a package in Bun cache\nlink_in_bun_cache() {\n    local package_pattern=\"$1\"\n    local package_name=\"$2\"\n    local source_path=\"$3\"\n    \n    local cache_dirs=$(find \"$NODE_MODULES_DIR/.bun\" -maxdepth 1 -type d -name \"$package_pattern\" 2>/dev/null)\n    \n    if [ -z \"$cache_dirs\" ]; then\n        echo \"⚠ Warning: No Bun cache found for $package_name\"\n        return 0\n    fi\n    \n    echo \"$cache_dirs\" | while read -r cache_dir; do\n        if [ -n \"$cache_dir\" ] && [ -d \"$cache_dir\" ]; then\n            local target_dir=\"$cache_dir/node_modules/$package_name\"\n            local target_parent=$(dirname \"$target_dir\")\n            \n            # Remove existing directory/symlink\n            if [ -e \"$target_dir\" ] || [ -L \"$target_dir\" ]; then\n                rm -rf \"$target_dir\"\n            fi\n            \n            # Create parent directory if needed\n            mkdir -p \"$target_parent\"\n            \n            # Create symlink\n            ln -s \"$source_path\" \"$target_dir\"\n            echo \"  ✓ Linked $package_name in $(basename \"$cache_dir\")\"\n        fi\n    done\n}\n\n# Always link @opentui/core\necho \"Linking @opentui/core...\"\nlink_in_bun_cache \"@opentui+core@*\" \"@opentui/core\" \"$OPENTUI_ROOT/packages/core\"\n\n# Link yoga-layout (required by core)\necho \"Linking yoga-layout...\"\nif [ -d \"$OPENTUI_ROOT/node_modules/yoga-layout\" ]; then\n    link_in_bun_cache \"yoga-layout@*\" \"yoga-layout\" \"$OPENTUI_ROOT/node_modules/yoga-layout\"\nelif [ -d \"$OPENTUI_ROOT/packages/core/node_modules/yoga-layout\" ]; then\n    link_in_bun_cache \"yoga-layout@*\" \"yoga-layout\" \"$OPENTUI_ROOT/packages/core/node_modules/yoga-layout\"\nelse\n    echo \"⚠ Warning: yoga-layout not found in OpenTUI node_modules\"\nfi\n\n# Link web-tree-sitter (required by core)\necho \"Linking web-tree-sitter...\"\nif [ -d \"$OPENTUI_ROOT/node_modules/web-tree-sitter\" ]; then\n    link_in_bun_cache \"web-tree-sitter@*\" \"web-tree-sitter\" \"$OPENTUI_ROOT/node_modules/web-tree-sitter\"\nelif [ -d \"$OPENTUI_ROOT/packages/core/node_modules/web-tree-sitter\" ]; then\n    link_in_bun_cache \"web-tree-sitter@*\" \"web-tree-sitter\" \"$OPENTUI_ROOT/packages/core/node_modules/web-tree-sitter\"\nelse\n    echo \"⚠ Warning: web-tree-sitter not found in OpenTUI node_modules\"\nfi\n\n# Link @opentui/solid if requested\nif [ \"$LINK_SOLID\" = true ]; then\n    echo \"Linking @opentui/solid...\"\n    link_in_bun_cache \"@opentui+solid@*\" \"@opentui/solid\" \"$OPENTUI_ROOT/packages/solid\"\n    \n    # Link solid-js\n    echo \"Linking solid-js...\"\n    if [ -d \"$OPENTUI_ROOT/node_modules/solid-js\" ]; then\n        link_in_bun_cache \"solid-js@*\" \"solid-js\" \"$OPENTUI_ROOT/node_modules/solid-js\"\n    elif [ -d \"$OPENTUI_ROOT/packages/solid/node_modules/solid-js\" ]; then\n        link_in_bun_cache \"solid-js@*\" \"solid-js\" \"$OPENTUI_ROOT/packages/solid/node_modules/solid-js\"\n    else\n        echo \"⚠ Warning: solid-js not found in OpenTUI node_modules\"\n    fi\nfi\n\n# Link @opentui/react if requested\nif [ \"$LINK_REACT\" = true ]; then\n    echo \"Linking @opentui/react...\"\n    link_in_bun_cache \"@opentui+react@*\" \"@opentui/react\" \"$OPENTUI_ROOT/packages/react\"\n    \n    # Link react dependencies\n    echo \"Linking react...\"\n    if [ -d \"$OPENTUI_ROOT/node_modules/react\" ]; then\n        link_in_bun_cache \"react@*\" \"react\" \"$OPENTUI_ROOT/node_modules/react\"\n    elif [ -d \"$OPENTUI_ROOT/packages/react/node_modules/react\" ]; then\n        link_in_bun_cache \"react@*\" \"react\" \"$OPENTUI_ROOT/packages/react/node_modules/react\"\n    else\n        echo \"⚠ Warning: react not found in OpenTUI node_modules\"\n    fi\n    \n    echo \"Linking react-dom...\"\n    if [ -d \"$OPENTUI_ROOT/node_modules/react-dom\" ]; then\n        link_in_bun_cache \"react-dom@*\" \"react-dom\" \"$OPENTUI_ROOT/node_modules/react-dom\"\n    elif [ -d \"$OPENTUI_ROOT/packages/react/node_modules/react-dom\" ]; then\n        link_in_bun_cache \"react-dom@*\" \"react-dom\" \"$OPENTUI_ROOT/packages/react/node_modules/react-dom\"\n    else\n        echo \"⚠ Warning: react-dom not found in OpenTUI node_modules\"\n    fi\n    \n    echo \"Linking react-reconciler...\"\n    if [ -d \"$OPENTUI_ROOT/node_modules/react-reconciler\" ]; then\n        link_in_bun_cache \"react-reconciler@*\" \"react-reconciler\" \"$OPENTUI_ROOT/node_modules/react-reconciler\"\n    elif [ -d \"$OPENTUI_ROOT/packages/react/node_modules/react-reconciler\" ]; then\n        link_in_bun_cache \"react-reconciler@*\" \"react-reconciler\" \"$OPENTUI_ROOT/packages/react/node_modules/react-reconciler\"\n    else\n        echo \"⚠ Warning: react-reconciler not found in OpenTUI node_modules\"\n    fi\nfi\n\necho\necho \"✓ OpenTUI development linking complete!\"\necho \"  All workspace packages will now resolve to your dev version through Bun's cache.\"\n"
  },
  {
    "path": "scripts/pre-publish.ts",
    "content": "import { spawnSync, type SpawnSyncReturns } from \"node:child_process\"\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport process from \"node:process\"\nimport { fileURLToPath } from \"node:url\"\n\ninterface PackageJson {\n  name: string\n  version: string\n  dependencies?: Record<string, string>\n  optionalDependencies?: Record<string, string>\n}\n\ninterface VersionMismatch {\n  name: string\n  dir: string\n  expected: string\n  actual: string\n}\n\ninterface PackageConfig {\n  name: string\n  rootDir: string\n  distDir: string\n  requiresCore?: boolean\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = resolve(__dirname, \"..\")\n\n// Package configurations\nconst ALL_PACKAGES: PackageConfig[] = [\n  {\n    name: \"@opentui/core\",\n    rootDir: join(rootDir, \"packages\", \"core\"),\n    distDir: join(rootDir, \"packages\", \"core\", \"dist\"),\n  },\n  {\n    name: \"@opentui/react\",\n    rootDir: join(rootDir, \"packages\", \"react\"),\n    distDir: join(rootDir, \"packages\", \"react\", \"dist\"),\n    requiresCore: true,\n  },\n  {\n    name: \"@opentui/solid\",\n    rootDir: join(rootDir, \"packages\", \"solid\"),\n    distDir: join(rootDir, \"packages\", \"solid\", \"dist\"),\n    requiresCore: true,\n  },\n]\n\nconst PACKAGES = ALL_PACKAGES\n\nfunction setupNpmAuth(): void {\n  if (!process.env.NPM_AUTH_TOKEN) {\n    console.log(\"WARNING: NPM_AUTH_TOKEN not found, skipping auth setup\")\n    return\n  }\n\n  const npmrcPath = join(process.env.HOME as string, \".npmrc\")\n  const npmrcContent = `//registry.npmjs.org/:_authToken=${process.env.NPM_AUTH_TOKEN}\\n`\n\n  if (existsSync(npmrcPath)) {\n    const existing = readFileSync(npmrcPath, \"utf8\")\n    if (!existing.includes(\"//registry.npmjs.org/:_authToken\")) {\n      writeFileSync(npmrcPath, existing + \"\\n\" + npmrcContent)\n      console.log(\"SUCCESS: NPM auth token added to existing ~/.npmrc\")\n    } else {\n      console.log(\"SUCCESS: NPM auth token already present in ~/.npmrc\")\n    }\n  } else {\n    writeFileSync(npmrcPath, npmrcContent)\n    console.log(\"SUCCESS: NPM auth token written to ~/.npmrc\")\n  }\n}\n\nfunction verifyNpmAuth(): void {\n  console.log(\"INFO: Verifying NPM authentication...\")\n  const npmAuth: SpawnSyncReturns<Buffer> = spawnSync(\"npm\", [\"whoami\"], {})\n  if (npmAuth.status !== 0) {\n    console.error(\"ERROR: NPM authentication failed. Please run 'npm login' or ensure NPM_AUTH_TOKEN is set\")\n    process.exit(1)\n  }\n  console.log(\"SUCCESS: NPM authentication verified\")\n}\n\nfunction checkVersionExists(packageName: string, version: string): boolean {\n  try {\n    const versions: string[] = JSON.parse(\n      spawnSync(\"npm\", [\"view\", packageName, \"versions\", \"--json\"], {}).stdout.toString().trim(),\n    )\n    return Array.isArray(versions) ? versions.includes(version) : versions === version\n  } catch {\n    // Package doesn't exist yet or network error - assume version doesn't exist\n    return false\n  }\n}\n\nfunction validatePackage(config: PackageConfig): void {\n  console.log(`\\nINFO: Validating ${config.name}...`)\n\n  // Check if package.json exists\n  const packageJsonPath = join(config.rootDir, \"package.json\")\n  if (!existsSync(packageJsonPath)) {\n    console.error(`ERROR: package.json not found: ${packageJsonPath}`)\n    process.exit(1)\n  }\n\n  const packageJson: PackageJson = JSON.parse(readFileSync(packageJsonPath, \"utf8\"))\n\n  // Check if version already exists on npm\n  if (checkVersionExists(packageJson.name, packageJson.version)) {\n    console.error(`ERROR: ${packageJson.name}@${packageJson.version} already exists on npm`)\n    console.error(\"Please update the version before publishing\")\n    process.exit(1)\n  }\n  console.log(`SUCCESS: Version ${packageJson.version} is available on npm`)\n\n  // Check if dist directory exists\n  if (!existsSync(config.distDir)) {\n    console.error(`ERROR: dist directory not found: ${config.distDir}`)\n    console.error(\"Please run 'bun run build' first\")\n    process.exit(1)\n  }\n  console.log(`SUCCESS: dist directory exists`)\n\n  // Check dist package.json\n  const distPackageJsonPath = join(config.distDir, \"package.json\")\n  if (!existsSync(distPackageJsonPath)) {\n    console.error(`ERROR: dist/package.json not found: ${distPackageJsonPath}`)\n    process.exit(1)\n  }\n\n  const distPackageJson: PackageJson = JSON.parse(readFileSync(distPackageJsonPath, \"utf8\"))\n\n  // Check version mismatch between source and dist\n  if (distPackageJson.version !== packageJson.version) {\n    console.error(`ERROR: Version mismatch between source and dist package.json`)\n    console.error(`  Source version: ${packageJson.version}`)\n    console.error(`  Dist version: ${distPackageJson.version}`)\n    console.error(\"Please rebuild the package with 'bun run build'\")\n    process.exit(1)\n  }\n  console.log(`SUCCESS: Source and dist versions match`)\n\n  // For core package, check optional dependencies versions\n  if (config.name === \"@opentui/core\") {\n    const mismatches: VersionMismatch[] = []\n\n    if (distPackageJson.optionalDependencies) {\n      for (const depName of Object.keys(distPackageJson.optionalDependencies).filter((x) =>\n        x.startsWith(\"@opentui/core\"),\n      )) {\n        const nativeDir = join(config.rootDir, \"node_modules\", depName)\n        if (!existsSync(nativeDir)) {\n          console.error(`ERROR: Native package directory not found: ${nativeDir}`)\n          console.error(\"Please run 'bun run build:native' first\")\n          process.exit(1)\n        }\n\n        const nativePackageJson: PackageJson = JSON.parse(readFileSync(join(nativeDir, \"package.json\"), \"utf8\"))\n\n        if (nativePackageJson.version !== packageJson.version) {\n          mismatches.push({\n            name: depName,\n            dir: nativeDir,\n            expected: packageJson.version,\n            actual: nativePackageJson.version,\n          })\n        }\n\n        // Also check if this version exists on npm\n        if (checkVersionExists(depName, packageJson.version)) {\n          console.error(`ERROR: ${depName}@${packageJson.version} already exists on npm`)\n          console.error(\"Please update the version before publishing\")\n          process.exit(1)\n        }\n      }\n    }\n\n    if (mismatches.length > 0) {\n      console.error(\"ERROR: Version mismatch detected between root package and native packages:\")\n      mismatches.forEach((m) =>\n        console.error(`  - ${m.name}: expected ${m.expected}, found ${m.actual}\\n    ^ \"${m.dir}\"`),\n      )\n      process.exit(1)\n    }\n    console.log(`SUCCESS: All optional dependencies versions match`)\n  }\n\n  // For react/solid packages, check @opentui/core dependency version\n  if (config.requiresCore) {\n    const coreDependencyVersion = distPackageJson.dependencies?.[\"@opentui/core\"]\n    if (coreDependencyVersion !== packageJson.version) {\n      console.error(`ERROR: @opentui/core dependency version mismatch in dist`)\n      console.error(`  Expected: ${packageJson.version}`)\n      console.error(`  Found: ${coreDependencyVersion}`)\n      console.error(\"Please rebuild the package with 'bun run build'\")\n      process.exit(1)\n    }\n    console.log(`SUCCESS: @opentui/core dependency version matches`)\n  }\n\n  console.log(`SUCCESS: ${config.name} validation complete`)\n}\n\nfunction getUserConfirmation(): void {\n  console.log(\n    `\n\nPre-publish checklist:\n\n1. [OK] Version fields in package.json files have been updated\n2. [OK] All packages have been built (bun run build) \n3. [OK] Changes have been committed and pushed to GitHub\n4. [OK] All validation checks have passed\n\nContinue with publishing? (y/n)\n`.trim(),\n  )\n\n  if (process.env.CI === \"true\") {\n    console.log(\"INFO: Running in CI environment, skipping user confirmation\")\n    return\n  }\n\n  const confirm: SpawnSyncReturns<Buffer> = spawnSync(\n    \"node\",\n    [\n      \"-e\",\n      `\n      process.stdin.setRawMode(true);\n      process.stdin.resume();\n      process.stdin.on('data', (data) => {\n        const input = data.toString().toLowerCase();\n        if (input === 'y') process.exit(0);\n        if (input === 'n' || input === '\\\\x03') process.exit(1);\n      });\n      `,\n    ],\n    {\n      shell: false,\n      stdio: \"inherit\",\n    },\n  )\n\n  if (confirm.status !== 0) {\n    console.log(\"ABORTED: Publishing cancelled\")\n    process.exit(1)\n  }\n}\n\nfunction main(): void {\n  console.log(\"OpenTUI Pre-Publish Validation\")\n  console.log(\"=\".repeat(50))\n\n  // Setup NPM authentication once\n  console.log(\"\\nINFO: Setting up NPM authentication...\")\n  setupNpmAuth()\n  verifyNpmAuth()\n\n  // Validate all packages\n  console.log(\"\\nINFO: Validating all packages...\")\n  for (const packageConfig of PACKAGES) {\n    validatePackage(packageConfig)\n  }\n\n  // Get user confirmation\n  console.log(\"\\n\" + \"=\".repeat(50))\n  console.log(\"SUCCESS: All validation checks passed!\")\n  getUserConfirmation()\n\n  console.log(\"\\nSUCCESS: Pre-publish validation complete! Ready to publish.\")\n  console.log(\"\\nNext steps:\")\n  console.log(\"  • Run: bun run publish\")\n}\n\nmain()\n"
  },
  {
    "path": "scripts/prepare-release.ts",
    "content": "import { readFileSync, writeFileSync } from \"fs\"\nimport { join, resolve, dirname } from \"path\"\nimport { fileURLToPath } from \"url\"\nimport { execSync } from \"child_process\"\nimport process from \"process\"\n\ninterface PackageJson {\n  name: string\n  version: string\n  optionalDependencies?: Record<string, string>\n  dependencies?: Record<string, string>\n  [key: string]: any\n}\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\nconst rootDir = resolve(__dirname, \"..\")\n\nconst args = process.argv.slice(2)\nlet version = args.find((arg) => !arg.startsWith(\"--\"))\n\nif (!version) {\n  console.error(\"Error: Please provide a version number\")\n  console.error(\"Usage: bun scripts/prepare-release.ts <version>\")\n  console.error(\"Example: bun scripts/prepare-release.ts 0.2.0\")\n  console.error(\"         bun scripts/prepare-release.ts '*' (auto-increment patch)\")\n  process.exit(1)\n}\n\n// Handle auto-increment case\nif (version === \"*\") {\n  try {\n    const corePackageJsonPath = join(rootDir, \"packages\", \"core\", \"package.json\")\n    const corePackageJson: PackageJson = JSON.parse(readFileSync(corePackageJsonPath, \"utf8\"))\n    const currentVersion = corePackageJson.version\n\n    // Parse current version and increment patch\n    const versionParts = currentVersion.split(\".\")\n    if (versionParts.length !== 3) {\n      console.error(`Error: Invalid current version format: ${currentVersion}`)\n      process.exit(1)\n    }\n\n    const major = parseInt(versionParts[0])\n    const minor = parseInt(versionParts[1])\n    const patch = parseInt(versionParts[2]) + 1\n\n    version = `${major}.${minor}.${patch}`\n    console.log(`Auto-incrementing version from ${currentVersion} to ${version}`)\n  } catch (error) {\n    console.error(`Error: Failed to read current version: ${error}`)\n    process.exit(1)\n  }\n}\n\nif (!/^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$/.test(version)) {\n  console.error(`Error: Invalid version format: ${version}`)\n  console.error(\"Version should follow semver format (e.g., 1.0.0, 1.0.0-beta.1)\")\n  process.exit(1)\n}\n\nconsole.log(`\\nPreparing release ${version} for core, react, and solid packages...\\n`)\n\nconst corePackageJsonPath = join(rootDir, \"packages\", \"core\", \"package.json\")\nconsole.log(\"Updating @opentui/core...\")\n\ntry {\n  const corePackageJson: PackageJson = JSON.parse(readFileSync(corePackageJsonPath, \"utf8\"))\n\n  corePackageJson.version = version\n\n  if (corePackageJson.optionalDependencies) {\n    for (const depName in corePackageJson.optionalDependencies) {\n      if (depName.startsWith(\"@opentui/core-\")) {\n        corePackageJson.optionalDependencies[depName] = version\n        console.log(`  Updated ${depName} to ${version}`)\n      }\n    }\n  }\n\n  writeFileSync(corePackageJsonPath, JSON.stringify(corePackageJson, null, 2) + \"\\n\")\n  console.log(`  @opentui/core updated to version ${version}`)\n} catch (error) {\n  console.error(`  Failed to update @opentui/core: ${error}`)\n  process.exit(1)\n}\n\nconst reactPackageJsonPath = join(rootDir, \"packages\", \"react\", \"package.json\")\nconsole.log(\"\\nUpdating @opentui/react...\")\n\ntry {\n  const reactPackageJson: PackageJson = JSON.parse(readFileSync(reactPackageJsonPath, \"utf8\"))\n\n  reactPackageJson.version = version\n\n  writeFileSync(reactPackageJsonPath, JSON.stringify(reactPackageJson, null, 2) + \"\\n\")\n  console.log(`  @opentui/react updated to version ${version}`)\n  console.log(`  Note: @opentui/core dependency will be set to ${version} during build`)\n} catch (error) {\n  console.error(`  Failed to update @opentui/react: ${error}`)\n  process.exit(1)\n}\n\nconst solidPackageJsonPath = join(rootDir, \"packages\", \"solid\", \"package.json\")\nconsole.log(\"\\nUpdating @opentui/solid...\")\n\ntry {\n  const solidPackageJson: PackageJson = JSON.parse(readFileSync(solidPackageJsonPath, \"utf8\"))\n\n  solidPackageJson.version = version\n\n  writeFileSync(solidPackageJsonPath, JSON.stringify(solidPackageJson, null, 2) + \"\\n\")\n  console.log(`  @opentui/solid updated to version ${version}`)\n  console.log(`  Note: @opentui/core dependency will be set to ${version} during build`)\n} catch (error) {\n  console.error(`  Failed to update @opentui/solid: ${error}`)\n  process.exit(1)\n}\n\nconsole.log(\"\\nUpdating bun.lock...\")\ntry {\n  execSync(\"bun install\", { cwd: rootDir, stdio: \"inherit\" })\n  console.log(\"  bun.lock updated successfully\")\n} catch (error) {\n  console.error(`  Failed to update bun.lock: ${error}`)\n  process.exit(1)\n}\n\nconsole.log(`\nSuccessfully prepared release ${version} for core, react, and solid packages!\n\nNext steps:\n1. Review the changes: git diff\n2. Build the packages: bun run build\n3. Commit the changes: git add -A && git commit -m \"Release v${version}\" && git push\n4. Publish to npm: bun run publish\n5. Push to GitHub: git push\n  `)\n"
  }
]